diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index b848686768..e4f7b79cff 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -1,14 +1,20 @@ -import { HttpClient } from '@angular/common/http'; -import { Entity, EntityMapping } from 'ngx-entity-service'; -import { Observable, tap } from 'rxjs'; -import { AppInjector } from 'src/app/app-injector'; -import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; -import { Grade, GroupSet, TutorialStream, Unit } from './doubtfire-model'; -import { TaskDefinitionService } from '../services/task-definition.service'; - -export type UploadRequirement = { key: string; name: string; type: string; tiiCheck?: boolean; tiiPct?: number }; +import {HttpClient} from '@angular/common/http'; +import {Entity, EntityMapping} from 'ngx-entity-service'; +import {Observable, tap} from 'rxjs'; +import {AppInjector} from 'src/app/app-injector'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {Grade, GroupSet, TutorialStream, Unit} from './doubtfire-model'; +import {TaskDefinitionService} from '../services/task-definition.service'; + +export type UploadRequirement = { + key: string; + name: string; + type: string; + tiiCheck?: boolean; + tiiPct?: number; +}; -export type SimilarityCheck = { key: string; type: string; pattern: string }; +export type SimilarityCheck = {key: string; type: string; pattern: string}; export class TaskDefinition extends Entity { id: number; @@ -37,6 +43,8 @@ export class TaskDefinition extends Entity { scormBypassTest: boolean; scormTimeDelayEnabled: boolean; scormAttemptLimit: number = 0; + tutorialSelfEnrolmentEnabled: boolean; + tutorialSelfEnrolmentStream: TutorialStream = null; hasTaskAssessmentResources: boolean; isGraded: boolean; maxQualityPts: number; @@ -73,7 +81,7 @@ export class TaskDefinition extends Entity { entity: this, cache: this.unit.taskDefinitionCache, constructorParams: this.unit, - } + }, ); } else { return svc.update( @@ -81,7 +89,7 @@ export class TaskDefinition extends Entity { unitId: this.unit.id, id: this.id, }, - { entity: this } + {entity: this}, ); } } @@ -128,7 +136,10 @@ export class TaskDefinition extends Entity { } public matches(text: string): boolean { - return this.abbreviation.toLowerCase().indexOf(text) !== -1 || this.name.toLowerCase().indexOf(text) !== -1; + return ( + this.abbreviation.toLowerCase().indexOf(text) !== -1 || + this.name.toLowerCase().indexOf(text) !== -1 + ); } /** @@ -221,7 +232,9 @@ export class TaskDefinition extends Entity { public deleteTaskResources(): Observable { const httpClient = AppInjector.get(HttpClient); - return httpClient.delete(this.taskResourcesUploadUrl).pipe(tap(() => (this.hasTaskResources = false))); + return httpClient + .delete(this.taskResourcesUploadUrl) + .pipe(tap(() => (this.hasTaskResources = false))); } public deleteScormData(): Observable { diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index 0914929fbe..f65ec82c30 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -1,11 +1,11 @@ -import { CachedEntityService } from 'ngx-entity-service'; -import { TaskDefinition, Unit } from 'src/app/api/models/doubtfire-model'; -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import {CachedEntityService} from 'ngx-entity-service'; +import {TaskDefinition, Unit} from 'src/app/api/models/doubtfire-model'; +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; import API_URL from 'src/app/config/constants/apiURL'; -import { MappingFunctions } from './mapping-fn'; -import { AppInjector } from 'src/app/app-injector'; -import { Observable } from 'rxjs'; +import {MappingFunctions} from './mapping-fn'; +import {AppInjector} from 'src/app/app-injector'; +import {Observable} from 'rxjs'; @Injectable() export class TaskDefinitionService extends CachedEntityService { @@ -81,6 +81,15 @@ export class TaskDefinitionService extends CachedEntityService { return taskDef.tutorialStream?.abbreviation; }, }, + { + keys: ['tutorialSelfEnrolmentStream', 'tutorial_self_enrolment_stream_abbr'], + toEntityFn: (data: object, key: string, taskDef: TaskDefinition, params?: any) => { + return taskDef.unit.tutorialStreamsCache.get(data[key]); + }, + toJsonFn: (taskDef: TaskDefinition, key: string) => { + return taskDef.tutorialSelfEnrolmentStream?.abbreviation; + }, + }, 'plagiarismWarnPct', 'restrictStatusUpdates', { @@ -100,6 +109,7 @@ export class TaskDefinitionService extends CachedEntityService { 'hasTaskResources', 'hasTaskAssessmentResources', 'scormEnabled', + 'tutorialSelfEnrolmentEnabled', 'hasScormData', 'scormAllowReview', 'scormBypassTest', @@ -108,7 +118,7 @@ export class TaskDefinitionService extends CachedEntityService { 'isGraded', 'maxQualityPts', 'overseerImageId', - 'assessmentEnabled' + 'assessmentEnabled', ); this.mapping.mapAllKeysToJsonExcept( @@ -116,7 +126,7 @@ export class TaskDefinitionService extends CachedEntityService { 'hasTaskSheet', 'hasTaskResources', 'hasTaskAssessmentResources', - 'hasScormData' + 'hasScormData', ); } diff --git a/src/app/common/modals/tutorial-enrolment-modal/tutorial-enrolment-modal.component.html b/src/app/common/modals/tutorial-enrolment-modal/tutorial-enrolment-modal.component.html new file mode 100644 index 0000000000..500ddff2d2 --- /dev/null +++ b/src/app/common/modals/tutorial-enrolment-modal/tutorial-enrolment-modal.component.html @@ -0,0 +1,30 @@ +

Tutorial Enrolment

+
+

+ Please select from the dropdown below the tutorial you would like to enrol in. +

+ + + Tutorials + + @for (tutorial of getTutorialsForStream(); track tutorial) { + {{ tutorial.abbreviation }} + } + + +
+ +
+ + +
diff --git a/src/app/common/modals/tutorial-enrolment-modal/tutorial-enrolment-modal.component.ts b/src/app/common/modals/tutorial-enrolment-modal/tutorial-enrolment-modal.component.ts new file mode 100644 index 0000000000..d001aa2408 --- /dev/null +++ b/src/app/common/modals/tutorial-enrolment-modal/tutorial-enrolment-modal.component.ts @@ -0,0 +1,55 @@ +import {Component, Inject, LOCALE_ID} from '@angular/core'; +import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog'; +import { + TaskComment, + TaskCommentService, + Task, + Unit, + Tutorial, +} from 'src/app/api/models/doubtfire-model'; +import {AppInjector} from 'src/app/app-injector'; +import {FormControl, Validators, FormGroup, FormGroupDirective, NgForm} from '@angular/forms'; +import {ErrorStateMatcher} from '@angular/material/core'; +import {AlertService} from '../../services/alert.service'; + +export interface TutorialEnrolmentModalData { + task: Task; +} + +@Component({ + selector: 'f-tutorial-enrolment-modal', + templateUrl: './tutorial-enrolment-modal.component.html', +}) +export class TutorialEnrolmentModalComponent { + tutorialsFormControl = new FormControl(null); + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: TutorialEnrolmentModalData, + private alerts: AlertService, + ) {} + + public getTutorialsForStream() { + const tutorialStreamAbbreviation = + this.data.task.definition.tutorialSelfEnrolmentStream.abbreviation; + + const student = this.data.task.project; + return this.data.task.unit.tutorials.filter((tutorial) => { + // Filter workshops based on campus allocation + const result: boolean = + student.campus == null || + tutorial.campus == null || + student.campus.id === tutorial.campus.id; + + if (!result) return result; + if (tutorial.tutorialStream) { + return tutorial.tutorialStream.abbreviation === tutorialStreamAbbreviation; + } + }); + } + + attemptTutorialEnrolment() { + const selectedTutorial = this.tutorialsFormControl.value; + this.data.task.project.switchToTutorial(selectedTutorial); + } +} diff --git a/src/app/common/modals/tutorial-enrolment-modal/tutorial-enrolment-modal.service.ts b/src/app/common/modals/tutorial-enrolment-modal/tutorial-enrolment-modal.service.ts new file mode 100644 index 0000000000..01b72f0b31 --- /dev/null +++ b/src/app/common/modals/tutorial-enrolment-modal/tutorial-enrolment-modal.service.ts @@ -0,0 +1,24 @@ +import {Injectable} from '@angular/core'; +import {Task} from 'src/app/api/models/task'; +import {MatDialogRef, MatDialog} from '@angular/material/dialog'; +import { + TutorialEnrolmentModalComponent, + TutorialEnrolmentModalData, +} from './tutorial-enrolment-modal.component'; + +@Injectable({ + providedIn: 'root', +}) +export class TutorialEnrolmentModalService { + constructor(public dialog: MatDialog) {} + + public show(data: TutorialEnrolmentModalData) { + let dialogRef: MatDialogRef; + + dialogRef = this.dialog.open(TutorialEnrolmentModalComponent, {data}); + + dialogRef.afterOpened().subscribe((result: any) => {}); + + dialogRef.afterClosed().subscribe((result: any) => {}); + } +} diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index c714ff0e5a..b186c06370 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -253,6 +253,9 @@ import {ScormExtensionModalComponent} from './common/modals/scorm-extension-moda import { GradeIconComponent } from './common/grade-icon/grade-icon.component'; import { GradeTaskModalComponent } from './tasks/modals/grade-task-modal/grade-task-modal.component'; import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; +import { TaskDefinitionTutorialEnrolmentComponent } from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-tutorial-enrolment/task-definition-tutorial-enrolment.component'; +import {TaskTutorialEnrolmentCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-tutorial-enrolment-card/task-tutorial-enrolment-card.component'; +import { TutorialEnrolmentModalComponent } from './common/modals/tutorial-enrolment-modal/tutorial-enrolment-modal.component'; // See https://stackoverflow.com/questions/55721254/how-to-change-mat-datepicker-date-format-to-dd-mm-yyyy-in-simplest-way/58189036#58189036 const MY_DATE_FORMAT = { @@ -389,6 +392,9 @@ import { UnitStudentEnrolmentModalComponent } from './units/modals/unit-student- TaskScormCardComponent, ScormExtensionCommentComponent, ScormExtensionModalComponent, + TaskDefinitionTutorialEnrolmentComponent, + TaskTutorialEnrolmentCardComponent, + TutorialEnrolmentModalComponent, ], // Services we provide providers: [ diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index d9dc6955d5..db14f2abc2 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -224,6 +224,7 @@ import {GradeService} from './common/services/grade.service'; import {TaskScormCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component'; import { UnitStudentEnrolmentModalService } from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; +import {TaskTutorialEnrolmentCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-tutorial-enrolment-card/task-tutorial-enrolment-card.component'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -495,3 +496,8 @@ DoubtfireAngularJSModule.directive( 'fTaskVisualisation', downgradeComponent({ component: TaskVisualisationComponent }) ); + +DoubtfireAngularJSModule.directive( + 'fTaskTutorialEnrolmentCard', + downgradeComponent({component: TaskTutorialEnrolmentCardComponent}), +); diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-tutorial-enrolment-card/task-tutorial-enrolment-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-tutorial-enrolment-card/task-tutorial-enrolment-card.component.html new file mode 100644 index 0000000000..d1a30bebd7 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-tutorial-enrolment-card/task-tutorial-enrolment-card.component.html @@ -0,0 +1,34 @@ + + + Tutorial Enrolment + + +

+ This task lets you choose a tutorial to enrol in for the teaching period. Your selected + tutorial will determine which tutor is assigned to mark your work throughout the teaching + period. +

+ @if (!isStudentEnrolledInTutorialStream()) { +

To complete this task, you must enrol in a tutorial from the provided list.

+ } + @if (isStudentEnrolledInTutorialStream()) { +

You're enrolled! You can now proceed to the next task.

+

+ You can confirm your enrolment in the + Tutorials List. +

+ } +
+ @if (!isStudentEnrolledInTutorialStream()) { + + + + } +
\ No newline at end of file diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-tutorial-enrolment-card/task-tutorial-enrolment-card.component.scss b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-tutorial-enrolment-card/task-tutorial-enrolment-card.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-tutorial-enrolment-card/task-tutorial-enrolment-card.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-tutorial-enrolment-card/task-tutorial-enrolment-card.component.ts new file mode 100644 index 0000000000..28fc291938 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-tutorial-enrolment-card/task-tutorial-enrolment-card.component.ts @@ -0,0 +1,42 @@ +import {Component, Input} from '@angular/core'; +import {Task, User, UserService} from 'src/app/api/models/doubtfire-model'; +import {TutorialEnrolmentModalService} from 'src/app/common/modals/tutorial-enrolment-modal/tutorial-enrolment-modal.service'; + +@Component({ + selector: 'f-task-tutorial-enrolment-card', + templateUrl: './task-tutorial-enrolment-card.component.html', + styleUrls: ['./task-tutorial-enrolment-card.component.scss'], +}) +export class TaskTutorialEnrolmentCardComponent { + @Input() task: Task; + user: User; + + constructor( + private userService: UserService, + private tutorialEnrolmentModalService: TutorialEnrolmentModalService, + ) { + this.user = this.userService.currentUser; + } + + isStudentEnrolledInTutorialStream(): boolean { + const tutorialStreamAbbreviation = + this.task.definition.tutorialSelfEnrolmentStream.abbreviation; + let isEnrolledInTutorialStream = false; + + const studentTutorialEnrolments = this.task.project.tutorials; + for (const tutorial of studentTutorialEnrolments) { + if (tutorial.tutorialStream.abbreviation === tutorialStreamAbbreviation) { + isEnrolledInTutorialStream = true; + } + } + return isEnrolledInTutorialStream; + } + + isNotStudent(): boolean { + return this.user !== this.task.project.student; + } + + launchTutorialEnrolment(): void { + this.tutorialEnrolmentModalService.show({task: this.task}); + } +} diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.tpl.html b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.tpl.html index d2797fa663..7a4b783d16 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.tpl.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.tpl.html @@ -42,6 +42,7 @@ + diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.html index 7d6224f343..a91b67a62a 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.html @@ -152,7 +152,29 @@

SCORM test

8 +
+

+ Tutorial enrolment form +

+

+ Select the tutorial stream(s) that allow students to enrol in tutorials from +

+
+ +
+
+ +
+
+
+ 9 +
+

Optional settings

Apply other options

diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-tutorial-enrolment/task-definition-tutorial-enrolment.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-tutorial-enrolment/task-definition-tutorial-enrolment.component.html new file mode 100644 index 0000000000..b1180917dd --- /dev/null +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-tutorial-enrolment/task-definition-tutorial-enrolment.component.html @@ -0,0 +1,25 @@ +
+ + Enable tutorial self enrolment for task + + + @if (taskDefinition.tutorialSelfEnrolmentEnabled) { +
+ + Tutorial Streams - Which stream students can enrol into a tutorial from? + + @for (stream of unit.tutorialStreams; track stream) { + {{ stream.name }} — ({{ + stream.tutorialsIn(taskDefinition.unit).length + }} + tutorials) + } + + +
+ } +
diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-tutorial-enrolment/task-definition-tutorial-enrolment.component.scss b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-tutorial-enrolment/task-definition-tutorial-enrolment.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-tutorial-enrolment/task-definition-tutorial-enrolment.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-tutorial-enrolment/task-definition-tutorial-enrolment.component.ts new file mode 100644 index 0000000000..2146e114f0 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-tutorial-enrolment/task-definition-tutorial-enrolment.component.ts @@ -0,0 +1,52 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {FormControl, Validators} from '@angular/forms'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {TaskDefinition} from 'src/app/api/models/task-definition'; +import {Unit} from 'src/app/api/models/unit'; +import {TaskDefinitionService} from 'src/app/api/services/task-definition.service'; +import {TutorialStream} from 'src/app/api/models/doubtfire-model'; + +@Component({ + selector: 'f-task-definition-tutorial-enrolment', + templateUrl: 'task-definition-tutorial-enrolment.component.html', + styleUrls: ['task-definition-tutorial-enrolment.component.scss'], +}) +export class TaskDefinitionTutorialEnrolmentComponent implements OnInit { + @Input() taskDefinition: TaskDefinition; + + public _tutorialStreams: TutorialStream[] = []; + + public _selectedTutorialStreams: TutorialStream[] = []; + + tutoralStreamControl = new FormControl(null, Validators.required); + + public ngOnInit(): void { + const tutorialStreams = this.taskDefinition.unit.tutorialStreamsCache.currentValues; + console.log(tutorialStreams); + for (const stream of tutorialStreams) { + console.log(stream); + this._tutorialStreams.push(stream); + } + + + this.tutoralStreamControl.setValue(this.taskDefinition.tutorialSelfEnrolmentStream); + } + + public onSelectStream(): void { + const selectedStream = this.tutoralStreamControl.value; + if (selectedStream) { + console.log(selectedStream); + + this.taskDefinition.tutorialSelfEnrolmentStream = selectedStream; + } + } + + constructor( + private alerts: AlertService, + private taskDefinitionService: TaskDefinitionService, + ) {} + + public get unit(): Unit { + return this.taskDefinition?.unit; + } +}