Skip to content

Commit 2157925

Browse files
Added interface for managing users enrolled in a course.
Course teachers have the ability to add and remove students and other teachers from their courses known by their username or full name.
1 parent 72641ac commit 2157925

File tree

15 files changed

+449
-16
lines changed

15 files changed

+449
-16
lines changed

vscode4teaching-webapp/angular.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"styles": [
3232
"node_modules/bootstrap/dist/css/bootstrap.min.css",
3333
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
34+
"node_modules/@ng-select/ng-select/themes/default.theme.css",
3435
"src/styles.scss"
3536
],
3637
"scripts": [

vscode4teaching-webapp/package-lock.json

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vscode4teaching-webapp/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@angular/platform-browser-dynamic": "^16.2.9",
2020
"@angular/router": "^16.2.9",
2121
"@fortawesome/fontawesome-free": "^6.4.2",
22+
"@ng-select/ng-select": "^11",
2223
"@types/bootstrap": "^5.2.7",
2324
"@types/wicg-file-system-access": "^2023.10.1",
2425
"@zip.js/zip.js": "^2.7.30",

vscode4teaching-webapp/src/app/app.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { HTTP_INTERCEPTORS, HttpClientModule } from "@angular/common/http";
33
import { NgModule } from '@angular/core';
44
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
55
import { BrowserModule } from '@angular/platform-browser';
6+
import { NgSelectModule } from "@ng-select/ng-select";
67

78
import { AppRoutingModule } from './app-routing.module';
89
import { AppComponent } from './app.component';
@@ -21,6 +22,7 @@ import { InProgressExerciseComponent } from './components/private/student/course
2122
import { NotStartedExerciseComponent } from "./components/private/student/course/exercise-status/not-started-exercise/not-started-exercise.component";
2223
import { StudentCourseComponent } from './components/private/student/course/student-course.component';
2324
import { SharingCodeComponent } from './components/private/teacher/course/course-details/sharing-code/sharing-code.component';
25+
import { EnrolledUsersManagementComponent } from "./components/private/teacher/course/course-details/enrolled-users-management/enrolled-users-management.component";
2426
import { TeacherCourseComponent } from "./components/private/teacher/course/teacher-course.component";
2527
import { GeneralStatisticsComponent } from './components/private/teacher/course/teacher-exercise/general-statistics/general-statistics.component';
2628
import { IndividualStudentProgressComponent } from './components/private/teacher/course/teacher-exercise/students-progress/individual-student-progress/individual-student-progress.component';
@@ -67,6 +69,7 @@ import { WebSocketHandlerFactory } from "./services/ws/web-socket-handler-factor
6769

6870
// private/teacher/course
6971
TeacherCourseComponent,
72+
EnrolledUsersManagementComponent,
7073
SharingCodeComponent,
7174
TeacherExerciseComponent,
7275
GeneralStatisticsComponent,
@@ -79,6 +82,7 @@ import { WebSocketHandlerFactory } from "./services/ws/web-socket-handler-factor
7982
HttpClientModule,
8083
ReactiveFormsModule,
8184
FormsModule,
85+
NgSelectModule,
8286
NgOptimizedImage
8387
],
8488
providers: [
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<button class="btn btn-v4t btn-sm" (click)="this.openStudentManagementModal()">
2+
<i class="fa fa-users-gear"></i> Manage enrolled users
3+
</button>
4+
<div class="modal modal-xl fade" tabindex="-1" #enrolledUsersModal>
5+
<div class="modal-dialog">
6+
<div class="modal-content">
7+
<div class="modal-header">
8+
<h5 class="modal-title"><i class="fa fa-users-gear"></i> Enrolled users management</h5>
9+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
10+
</div>
11+
<div class="modal-body" *ngIf="!this.enrolledStudents || !this.enrolledTeachers">
12+
<div class="alert alert-v4t text-center">
13+
<i class="fa fa-circle-notch fa-spin"></i> Loading enrolled students information...
14+
</div>
15+
</div>
16+
<div class="modal-body" *ngIf="this.enrolledStudents && this.enrolledTeachers">
17+
<div class="subtitle">Teachers</div>
18+
<div *ngFor="let enrolledTeacher of this.enrolledTeachers" class="user">
19+
<div style="display: flex; flex-direction: row; align-items: center">
20+
<i class="fa fa-user-secret"></i>
21+
<div style="margin-left: .5rem">
22+
<div>{{ enrolledTeacher.name }} {{ enrolledTeacher.lastName }}</div>
23+
<div *ngIf="enrolledTeacher.username === creator?.username" class="badge color-not-started bg-not-started border-not-started py-1 px-3 rounded-pill">Creator</div>
24+
</div>
25+
</div>
26+
<div class="username">{{ enrolledTeacher.username }}</div>
27+
<div class="actions">
28+
<button class="btn btn-v4t btn-xs" *ngIf="enrolledTeacher.username !== creator?.username && enrolledTeacher.username !== this.curUserUsername" (click)="this.showRemoveUserConfirmation(enrolledTeacher)">
29+
<i class="fa fa-times"></i>
30+
</button>
31+
</div>
32+
</div>
33+
<div class="subtitle">Students</div>
34+
<div *ngIf="this.enrolledStudents.length === 0" class="alert alert-light mx-1 my-2">
35+
<i class="fas fa-info-circle"></i> There are no students enrolled in this course.
36+
</div>
37+
<div *ngFor="let enrolledStudent of this.enrolledStudents" class="user">
38+
<div style="display: flex; flex-direction: row; align-items: center">
39+
<i class="fa fa-user"></i>
40+
<div style="margin-left: .5rem">{{ enrolledStudent.name }} {{ enrolledStudent.lastName }}</div>
41+
</div>
42+
<div class="username">{{ enrolledStudent.username }}</div>
43+
<div class="actions">
44+
<button class="btn btn-v4t btn-xs" (click)="this.showRemoveUserConfirmation(enrolledStudent)">
45+
<i class="fa fa-times"></i>
46+
</button>
47+
</div>
48+
</div>
49+
50+
<hr>
51+
52+
<div class="subtitle">Enroll new user</div>
53+
<div class="alert alert-light mx-1 my-2">
54+
<i class="fas fa-info-circle"></i> New users can be enrolled in this course by selecting them from the list below and clicking the "Enroll" button.
55+
<br><i class="fa fa-exclamation-circle"></i> If a teacher is enrolled, they will be granted permissions to create, edit, and delete course content.
56+
</div>
57+
<div style="display: flex; flex-direction: row; align-items: center; column-gap: .5rem">
58+
<ng-select [(ngModel)]="this.selectedUser" [clearable]="false" [searchable]="true" placeholder="Select user…" notFoundText="No users found" style="flex: 1 0">
59+
<ng-option *ngFor="let enrolledUser of this.availableUsers" [value]="enrolledUser">{{ enrolledUser.name }} {{ enrolledUser.lastName }} ({{ enrolledUser.username }}){{ enrolledUser.isTeacher ? " (teacher)" : "" }}</ng-option>
60+
</ng-select>
61+
<button class="btn btn-v4t" style="flex: 0 0 15%" (click)="this.enrollSelectedUser()"><i class="fa fa-user-plus"></i> Enroll user</button>
62+
</div>
63+
</div>
64+
</div>
65+
</div>
66+
</div>
67+
<div class="modal fade" tabindex="-1" #confirmUserToRemove>
68+
<div class="modal-dialog">
69+
<div class="modal-content">
70+
<div class="modal-body modal-alert" *ngIf="this.userToRemove">
71+
<div class="icon color-in-progress">
72+
<i class="fa fa-exclamation-triangle"></i>
73+
</div>
74+
<div class="title">
75+
Remove user
76+
</div>
77+
<div class="message">
78+
Are you sure you want to remove <strong>{{ this.userToRemove.name }} {{ this.userToRemove.lastName }}</strong> ({{ this.userToRemove.username }}) from this course?
79+
</div>
80+
<div class="actions">
81+
<button class="btn btn-sm btn-outline-secondary" (click)="this.hideRemoveUserConfirmation()"><i class="fa fa-chevron-left"></i> Cancel</button>
82+
<button class="btn btn-sm btn-outline-v4t" (click)="this.removeUser()"><i class="fa fa-times"></i> Delete</button>
83+
</div>
84+
</div>
85+
</div>
86+
</div>
87+
</div>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
.subtitle {
2+
font-size: 1.1rem;
3+
font-weight: 500;
4+
}
5+
6+
.user {
7+
display: flex;
8+
flex-direction: row;
9+
justify-content: space-between;
10+
align-items: center;
11+
gap: 1rem;
12+
13+
background-color: #FFF0F0;
14+
border-radius: 200px;
15+
padding: .5rem 1.5rem;
16+
margin: .5rem 0;
17+
18+
> * {
19+
flex: 1 0;
20+
border-right: 1px solid #F44A3E;
21+
22+
&.username {
23+
text-align: center;
24+
}
25+
26+
&.actions {
27+
display: flex;
28+
justify-content: right;
29+
}
30+
31+
&:last-child {
32+
border-right: none;
33+
}
34+
}
35+
36+
&:nth-child(2n) {
37+
background-color: #FEE2E1;
38+
}
39+
}
40+
41+
.modal-alert {
42+
> .icon {
43+
text-align: center;
44+
font-size: 3rem;
45+
}
46+
47+
> .title {
48+
font-weight: 500;
49+
font-size: 1.5rem;
50+
text-align: center;
51+
}
52+
53+
> .message {
54+
margin: .5rem 1rem;
55+
text-align: center;
56+
}
57+
58+
> .actions {
59+
display: flex;
60+
justify-content: center;
61+
gap: 1rem;
62+
margin-top: 1rem;
63+
padding-top: 1rem;
64+
border-top: 1px solid #F44A3E;
65+
}
66+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { EnrolledUsersManagementComponent } from './enrolled-users-management.component';
4+
5+
describe('StudentManagementComponent', () => {
6+
let component: EnrolledUsersManagementComponent;
7+
let fixture: ComponentFixture<EnrolledUsersManagementComponent>;
8+
9+
beforeEach(() => {
10+
TestBed.configureTestingModule({
11+
declarations: [EnrolledUsersManagementComponent]
12+
});
13+
fixture = TestBed.createComponent(EnrolledUsersManagementComponent);
14+
component = fixture.componentInstance;
15+
fixture.detectChanges();
16+
});
17+
18+
it('should create', () => {
19+
expect(component).toBeTruthy();
20+
});
21+
});
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { AfterViewInit, Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
2+
import { Modal } from "bootstrap";
3+
import { Course } from "../../../../../../model/course.model";
4+
import { User } from "../../../../../../model/user.model";
5+
import { CurrentUserService } from "../../../../../../services/auth/current-user/current-user.service";
6+
import { CourseService } from "../../../../../../services/rest-api/model-entities/course/course.service";
7+
import { UserService } from "../../../../../../services/rest-api/model-entities/user/user.service";
8+
9+
@Component({
10+
selector: 'app-teacher-course-details-enrolled-users-management',
11+
templateUrl: './enrolled-users-management.component.html',
12+
styleUrls: ['./enrolled-users-management.component.scss']
13+
})
14+
export class EnrolledUsersManagementComponent implements OnInit, AfterViewInit {
15+
// Course (coming from parent component)
16+
@Input("course") course?: Course;
17+
18+
// Event thrown when enrolled users are updated
19+
@Output("enrolledUsersUpdated") enrolledUsersUpdated = new EventEmitter<void>();
20+
21+
// Lists of enrolled students and teachers
22+
public enrolledStudents?: User[];
23+
public enrolledTeachers?: User[];
24+
public creator?: User;
25+
26+
// Current user's username (to check the current user to prevent removing himself from the course)
27+
public curUserUsername?: string;
28+
29+
// Available users to enroll (both students and teachers), generated from the difference between all users and enrolled users (refreshEnrollmentData)
30+
public availableUsers?: User[];
31+
// Selected user to enroll (coming from the ng-select element in the template)
32+
public selectedUser?: User;
33+
// Selected user to remove (coming from pressing the remove button in the template)
34+
public userToRemove?: User;
35+
36+
// Elements to manage the main modal
37+
private enrolledUsersManagementModal!: Modal;
38+
@ViewChild("enrolledUsersModal") private enrolledUsersManagementModalElementRef!: ElementRef;
39+
// Elements to manage the remove user confirmation modal
40+
protected confirmRemoveUserModal!: Modal;
41+
@ViewChild("confirmUserToRemove") private confirmRemoveUserModalElementRef!: ElementRef;
42+
43+
constructor(private courseService: CourseService,
44+
private userService: UserService,
45+
public curUserService: CurrentUserService
46+
) {
47+
}
48+
49+
public async ngOnInit(): Promise<void> {
50+
this.curUserUsername = (await this.curUserService.currentUser)?.username;
51+
}
52+
53+
public ngAfterViewInit(): void {
54+
this.enrolledUsersManagementModal = new Modal(this.enrolledUsersManagementModalElementRef.nativeElement);
55+
this.confirmRemoveUserModal = new Modal(this.confirmRemoveUserModalElementRef.nativeElement, { backdrop: "static", keyboard: false });
56+
}
57+
58+
59+
public openStudentManagementModal(): void {
60+
if (this.course) {
61+
this.creator = this.course.creator;
62+
this.refreshEnrollmentData();
63+
this.enrolledUsersManagementModal.show();
64+
}
65+
}
66+
67+
public showRemoveUserConfirmation(pickedUser: User): void {
68+
this.enrolledUsersManagementModal.hide();
69+
this.userToRemove = pickedUser;
70+
this.confirmRemoveUserModal.show();
71+
}
72+
73+
public hideRemoveUserConfirmation(): void {
74+
this.confirmRemoveUserModal.hide();
75+
this.userToRemove = undefined;
76+
this.enrolledUsersManagementModal.show();
77+
}
78+
79+
80+
public async enrollSelectedUser(): Promise<void> {
81+
if (this.course && this.selectedUser) {
82+
await this.courseService.addUserToCourse(this.course, this.selectedUser);
83+
this.enrolledUsersUpdated.emit();
84+
this.refreshEnrollmentData();
85+
this.selectedUser = undefined;
86+
}
87+
}
88+
89+
public async removeUser(): Promise<void> {
90+
if (this.course && this.userToRemove) {
91+
await this.courseService.removeUserFromCourse(this.course, this.userToRemove);
92+
this.enrolledUsersUpdated.emit();
93+
this.refreshEnrollmentData();
94+
this.userToRemove = undefined;
95+
this.hideRemoveUserConfirmation();
96+
}
97+
}
98+
99+
100+
private refreshEnrollmentData(): void {
101+
if (this.course) {
102+
this.courseService.getEnrolledUsersByCourse(this.course).then(async (users: User[]) => {
103+
// Students are distinguished from teachers by the isTeacher property
104+
this.enrolledStudents = users.filter(user => !user.isTeacher).sort((a, b) => a.username.localeCompare(b.username));
105+
this.enrolledTeachers = users.filter(user => user.isTeacher).sort((a, b) => a.username.localeCompare(b.username));
106+
107+
// Available users are the difference between all users and enrolled users
108+
this.availableUsers = (await this.userService.getAllUsers()).filter(user => !users.some(enrolledUser => enrolledUser.id === user.id));
109+
});
110+
}
111+
}
112+
}

vscode4teaching-webapp/src/app/components/private/teacher/course/teacher-course.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<h2>{{ this.course.name }}</h2>
1717

1818
<div class="col">
19+
<app-teacher-course-details-enrolled-users-management [course]="this.course" (enrolledUsersUpdated)="this.refreshCourseInformation()"></app-teacher-course-details-enrolled-users-management>
1920
<app-teacher-course-details-sharing-code [course]="this.course"></app-teacher-course-details-sharing-code>
2021
</div>
2122

0 commit comments

Comments
 (0)