Skip to content

Commit 72641ac

Browse files
Adds course sharing option (for teachers)
An option is implemented that allows teachers to share the code so that students can self-enroll in their courses. Also adds some bug fixes.
1 parent 1226255 commit 72641ac

File tree

12 files changed

+181
-23
lines changed

12 files changed

+181
-23
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { BrowserModule } from '@angular/platform-browser';
77
import { AppRoutingModule } from './app-routing.module';
88
import { AppComponent } from './app.component';
99
import { DelaySinceComponent } from "./components/helpers/delay-since/delay-since.component";
10+
import { NotSupportedFileSystemAccessApiComponent } from './components/helpers/not-supported-file-system-access-api/not-supported-file-system-access-api.component';
1011
import { ProgressBarComponent } from './components/helpers/progress-bar/progress-bar.component';
1112
import { AsideComponent } from "./components/layout/aside/aside.component";
1213
import { HeaderComponent } from "./components/layout/header/header.component";
@@ -19,6 +20,7 @@ import { ExistingFilesDetectedComponent } from './components/private/student/cou
1920
import { InProgressExerciseComponent } from './components/private/student/course/exercise-status/in-progress-exercise/in-progress-exercise.component';
2021
import { NotStartedExerciseComponent } from "./components/private/student/course/exercise-status/not-started-exercise/not-started-exercise.component";
2122
import { StudentCourseComponent } from './components/private/student/course/student-course.component';
23+
import { SharingCodeComponent } from './components/private/teacher/course/course-details/sharing-code/sharing-code.component';
2224
import { TeacherCourseComponent } from "./components/private/teacher/course/teacher-course.component";
2325
import { GeneralStatisticsComponent } from './components/private/teacher/course/teacher-exercise/general-statistics/general-statistics.component';
2426
import { IndividualStudentProgressComponent } from './components/private/teacher/course/teacher-exercise/students-progress/individual-student-progress/individual-student-progress.component';
@@ -32,7 +34,6 @@ import { HttpRequestInterceptor } from "./services/rest-api/interceptor/http-req
3234
import { UrlService } from "./services/url/url.service";
3335
import { WebSocketHandler } from "./services/ws/web-socket-handler";
3436
import { WebSocketHandlerFactory } from "./services/ws/web-socket-handler-factory.service";
35-
import { NotSupportedFileSystemAccessApiComponent } from './components/helpers/not-supported-file-system-access-api/not-supported-file-system-access-api.component';
3637

3738
@NgModule({
3839
declarations: [
@@ -66,6 +67,7 @@ import { NotSupportedFileSystemAccessApiComponent } from './components/helpers/n
6667

6768
// private/teacher/course
6869
TeacherCourseComponent,
70+
SharingCodeComponent,
6971
TeacherExerciseComponent,
7072
GeneralStatisticsComponent,
7173
StudentsProgressComponent,

vscode4teaching-webapp/src/app/components/private/student/course/exercise-status/in-progress-exercise/auto-sync-server/auto-sync-server.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
</div>
2424
</div>
2525

26-
<div #syncDetailsModal class="modal fade" id="syncDetailsModal">
26+
<div #syncDetailsModal class="modal fade" [id]="'syncDetailsModalExercise' + this.eui.id">
2727
<div class="modal-dialog modal-lg">
2828
<div class="modal-content">
2929
<div class="modal-header">
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<button class="btn btn-v4t btn-sm" (click)="this.getSharingCode()">
2+
<i class="fa fa-share-alt"></i> Share with students
3+
</button>
4+
<div class="modal fade" tabindex="-1" #sharingCodeModal>
5+
<div class="modal-dialog">
6+
<div class="modal-content">
7+
<div class="modal-header">
8+
<h5 class="modal-title">Sharing course with students</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.sharingCode">
12+
<div class="alert alert-v4t text-center">
13+
<i class="fa fa-circle-notch fa-spin"></i> Loading sharing information...
14+
</div>
15+
</div>
16+
<div class="modal-body sharingCode" *ngIf="this.course && this.sharingCode">
17+
<div class="description">
18+
This share code can be used by students to join this course. They can join this course by inserting it in the application when logged in.
19+
</div>
20+
<div class="shareInput">
21+
<label for="sharingCode">Sharing code</label>
22+
<div class="input-group">
23+
<input type="text" id="sharingCode" class="form-control form-control-sm form-control-v4t text-center" [value]="this.sharingCode.value" readonly>
24+
<button class="btn btn-v4t btn-sm" (click)="this.copySharingCode(this.sharingCode)">
25+
<span *ngIf="!this.sharingCode.copied"><i class="fa fa-clipboard"></i> Copy</span>
26+
<span *ngIf="this.sharingCode.copied"><i class="fa fa-check"></i> Copied!</span>
27+
</button>
28+
</div>
29+
</div>
30+
<hr>
31+
<div class="description">
32+
In addition, the following URL can be shared with students. That URL will contain a step-by-step guide for students to join the course customized with current course's sharing code.
33+
</div>
34+
<div class="shareInput">
35+
<label for="sharingURL">Sharing URL</label>
36+
<div class="input-group">
37+
<input type="text" id="sharingURL" class="form-control form-control-sm form-control-v4t text-center" [value]="this.sharingUrl.value" readonly>
38+
<button class="btn btn-v4t btn-sm" (click)="this.copySharingCode(this.sharingUrl)">
39+
<span *ngIf="!this.sharingUrl.copied"><i class="fa fa-clipboard"></i> Copy</span>
40+
<span *ngIf="this.sharingUrl.copied"><i class="fa fa-check"></i> Copied!</span>
41+
</button>
42+
</div>
43+
</div>
44+
</div>
45+
</div>
46+
</div>
47+
</div>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.sharingCode {
2+
label {
3+
font-size: 1rem;
4+
font-weight: 500;
5+
}
6+
7+
.description {
8+
font-size: 0.9rem;
9+
font-weight: 400;
10+
margin: .25rem 0;
11+
text-align: justify;
12+
}
13+
14+
.shareInput {
15+
margin: .75rem 0 1rem;
16+
17+
&:last-child {
18+
margin-bottom: 0;
19+
}
20+
}
21+
}
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 { SharingCodeComponent } from './sharing-code.component';
4+
5+
describe('SharingCodeComponent', () => {
6+
let component: SharingCodeComponent;
7+
let fixture: ComponentFixture<SharingCodeComponent>;
8+
9+
beforeEach(() => {
10+
TestBed.configureTestingModule({
11+
declarations: [SharingCodeComponent]
12+
});
13+
fixture = TestBed.createComponent(SharingCodeComponent);
14+
component = fixture.componentInstance;
15+
fixture.detectChanges();
16+
});
17+
18+
it('should create', () => {
19+
expect(component).toBeTruthy();
20+
});
21+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
2+
import { Modal } from "bootstrap";
3+
import { Course } from "../../../../../../model/course.model";
4+
import { CourseService } from "../../../../../../services/rest-api/model-entities/course/course.service";
5+
import { UrlService } from "../../../../../../services/url/url.service";
6+
7+
type SharingCode = { value?: string, copied: boolean };
8+
9+
@Component({
10+
selector: 'app-teacher-course-details-sharing-code',
11+
templateUrl: './sharing-code.component.html',
12+
styleUrls: ['./sharing-code.component.scss']
13+
})
14+
export class SharingCodeComponent implements AfterViewInit {
15+
@Input("course") course?: Course;
16+
17+
public sharingCode: SharingCode;
18+
public sharingUrl: SharingCode;
19+
20+
private sharingCodeModal!: Modal;
21+
@ViewChild("sharingCodeModal") private sharingCodeModalElementRef!: ElementRef;
22+
23+
constructor(private courseService: CourseService, private urlService: UrlService) {
24+
this.sharingCode = { value: undefined, copied: false };
25+
this.sharingUrl = { value: undefined, copied: false };
26+
}
27+
28+
ngAfterViewInit(): void {
29+
this.sharingCodeModal = new Modal(this.sharingCodeModalElementRef.nativeElement);
30+
}
31+
32+
public async getSharingCode(): Promise<void> {
33+
if (this.course) {
34+
this.courseService.getSharingCodeByCourse(this.course)
35+
.then(sharingCode => {
36+
this.sharingCode.value = sharingCode
37+
this.sharingUrl.value = encodeURI(this.urlService.webBaseURL + "/?code=" + this.sharingCode.value);
38+
});
39+
this.sharingCodeModal.show();
40+
}
41+
}
42+
43+
public async copySharingCode(sharingCode: SharingCode): Promise<void> {
44+
if (sharingCode.value) {
45+
await navigator.clipboard.writeText(sharingCode.value);
46+
sharingCode.copied = true;
47+
setTimeout(() => sharingCode.copied = false, 2000);
48+
}
49+
}
50+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
<div class="container" *ngIf="this.course && this.exercises">
1616
<h2>{{ this.course.name }}</h2>
1717

18+
<div class="col">
19+
<app-teacher-course-details-sharing-code [course]="this.course"></app-teacher-course-details-sharing-code>
20+
</div>
21+
1822
<hr>
1923
<h3>Exercises</h3>
2024
<div class="exercises">

vscode4teaching-webapp/src/app/components/private/teacher/course/teacher-exercise/students-progress/students-progress.component.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,19 @@ export class StudentsProgressComponent {
7171
public getAllStudentsFiles(): void {
7272
if (this.courseDirectory && !this.isActiveDownload) {
7373
this.isActiveDownload = "STUDENTS";
74-
return this.handleDownloadUnzip(
74+
this.handleDownloadUnzip(
7575
this.fileExchangeService.getAllProposalsByExerciseId(this.exercise.id),
7676
this.courseDirectory
77-
);
77+
).then(() => {
78+
this.filesLastUpdateTimestamp = new Date();
79+
})
7880
}
7981
}
8082

8183
public async getTemplateFiles(): Promise<void> {
8284
if (this.courseDirectory && !this.isActiveDownload) {
8385
this.isActiveDownload = "TEMPLATE";
84-
this.handleDownloadUnzip(
86+
await this.handleDownloadUnzip(
8587
this.fileExchangeService.getTemplateByExerciseId(this.exercise.id),
8688
await this.courseDirectory.getDirectoryHandle("template", { create: true })
8789
);
@@ -91,7 +93,7 @@ export class StudentsProgressComponent {
9193
public async getSolutionFiles(): Promise<void> {
9294
if (this.courseDirectory && !this.isActiveDownload) {
9395
this.isActiveDownload = "SOLUTION";
94-
this.handleDownloadUnzip(
96+
await this.handleDownloadUnzip(
9597
this.fileExchangeService.getSolutionByExerciseId(this.exercise.id),
9698
await this.courseDirectory.getDirectoryHandle("solution", { create: true })
9799
);
@@ -100,18 +102,20 @@ export class StudentsProgressComponent {
100102

101103
private handleDownloadUnzip(fileRequest: Observable<HttpEvent<Blob>>,
102104
targetDirectory: FileSystemDirectoryHandle
103-
) {
105+
): Promise<void> {
104106
this.downloadProgressBar.visible = true;
105-
this.downloadUnzipService.downloadAndUnzip(fileRequest, targetDirectory).subscribe({
106-
next: (downloadUnzipDTO: DownloadUnzipDTO) => {
107-
this.downloadProgressBar.process = downloadUnzipDTO.operation;
108-
this.downloadProgressBar.percentage = downloadUnzipDTO.percentage;
109-
},
110-
complete: () => {
111-
this.isActiveDownload = false;
112-
this.downloadProgressBar.visible = false;
113-
this.filesLastUpdateTimestamp = new Date();
114-
}
115-
});
107+
return new Promise((res) =>
108+
this.downloadUnzipService.downloadAndUnzip(fileRequest, targetDirectory).subscribe({
109+
next: (downloadUnzipDTO: DownloadUnzipDTO) => {
110+
this.downloadProgressBar.process = downloadUnzipDTO.operation;
111+
this.downloadProgressBar.percentage = downloadUnzipDTO.percentage;
112+
},
113+
complete: () => {
114+
this.isActiveDownload = false;
115+
this.downloadProgressBar.visible = false;
116+
res();
117+
}
118+
})
119+
);
116120
}
117121
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class TeacherExerciseComponent implements OnInit, OnDestroy {
5151
this.studentEUIs = await this.euiService.getAllStudentsExerciseUsersInfoByExercise(this.course?.exercises?.find(e => e.id === this.exerciseId) as Exercise);
5252
},
5353
onError: (err) => {
54-
console.warn(err);
54+
// TODO To be implemented
5555
}
5656
});
5757
}

vscode4teaching-webapp/src/app/model/course.model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ExerciseDTO } from "./rest-api/exercise.dto";
66
export class Course {
77
readonly #id: number;
88
readonly #name: string;
9-
#creator: User | undefined;
9+
readonly #creator: User | undefined;
1010
#exercises: Exercise[] | undefined;
1111

1212
constructor(dto: CourseDTO) {

0 commit comments

Comments
 (0)