From ec7524aaae040794de76521363c2d885a0425db0 Mon Sep 17 00:00:00 2001 From: Bahnschrift Date: Fri, 31 May 2024 12:56:24 +1000 Subject: [PATCH 001/776] fix: marking shortcuts no longer conflict with common browser shortcuts --- .../staff-task-list.component.ts | 12 ++++----- .../states/tasks/inbox/inbox.component.ts | 26 +++++++++++++++---- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts b/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts index 95b498af20..5e427de56e 100644 --- a/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts +++ b/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts @@ -123,26 +123,26 @@ export class StaffTaskListComponent implements OnInit, OnChanges, OnDestroy { } } ngOnDestroy(): void { - this.hotkeys.removeShortcuts('meta.shift.arrowdown'); - this.hotkeys.removeShortcuts('meta.shift.arrowup'); + this.hotkeys.removeShortcuts('alt.shift.arrowdown'); + this.hotkeys.removeShortcuts('alt.shift.arrowup'); } ngOnInit(): void { const registeredHotkeys = this.hotkeys.getHotkeys().map((hotkey) => hotkey.keys); - if (!registeredHotkeys.includes('meta.shift.arrowdown')) { + if (!registeredHotkeys.includes('alt.shift..arrowdown')) { this.hotkeys .addShortcut({ - keys: 'meta.shift.arrowdown', + keys: 'alt.shift.arrowdown', description: 'Select next task', }) .subscribe(() => this.nextTask()); } - if (!registeredHotkeys.includes('meta.shift.arrowup')) { + if (!registeredHotkeys.includes('alt.shift.arrowup')) { this.hotkeys .addShortcut({ - keys: 'meta.shift.arrowup', + keys: 'alt.shift.arrowup', description: 'Select previous task', }) .subscribe(() => this.previousTask()); diff --git a/src/app/units/states/tasks/inbox/inbox.component.ts b/src/app/units/states/tasks/inbox/inbox.component.ts index 79b4cfe355..ff7865da78 100644 --- a/src/app/units/states/tasks/inbox/inbox.component.ts +++ b/src/app/units/states/tasks/inbox/inbox.component.ts @@ -19,6 +19,7 @@ import {SelectedTaskService} from 'src/app/projects/states/dashboard/selected-ta import {HotkeysService, HotkeysHelpComponent} from '@ngneat/hotkeys'; import {MatDialog} from '@angular/material/dialog'; import {UserService} from 'src/app/api/services/user.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; @Component({ selector: 'f-inbox', @@ -58,6 +59,7 @@ export class InboxComponent implements OnInit, AfterViewInit { private router: UIRouter, public dialog: MatDialog, private userService: UserService, + private constants: DoubtfireConstants, ) { this.selectedTask.currentPdfUrl$.subscribe((url) => { this.visiblePdfUrl = url; @@ -76,7 +78,7 @@ export class InboxComponent implements OnInit, AfterViewInit { const ref = this.dialog.open(HotkeysHelpComponent, { // width: '250px', }); - ref.componentInstance.title = 'Formatif Marking Shortcuts'; + ref.componentInstance.title = `${this.constants.ExternalName.value} Marking Shortcuts`; ref.componentInstance.dismiss.subscribe(() => ref.close()); }); } @@ -85,18 +87,32 @@ export class InboxComponent implements OnInit, AfterViewInit { ngOnInit(): void { this.hotkeys .addShortcut({ - keys: 'control.c', - description: 'Mark selected task as complete', + keys: 'alt.shift.r', + description: 'Mark selected task as redo', }) - .subscribe(() => this.selectedTask.selectedTask?.updateTaskStatus('complete')); + .subscribe(() => this.selectedTask.selectedTask?.updateTaskStatus('redo')); this.hotkeys .addShortcut({ - keys: 'control.f', + keys: 'alt.shift.f', description: 'Mark selected task as fix', }) .subscribe(() => this.selectedTask.selectedTask?.updateTaskStatus('fix_and_resubmit')); + this.hotkeys + .addShortcut({ + keys: 'alt.shift.c', + description: 'Mark selected task as complete', + }) + .subscribe(() => this.selectedTask.selectedTask?.updateTaskStatus('complete')); + + this.hotkeys + .addShortcut({ + keys: 'alt.shift.d', + description: 'Mark selected task as discuss', + }) + .subscribe(() => this.selectedTask.selectedTask?.updateTaskStatus('discuss')); + this.dragMoveAudited$ = this.dragMove$.pipe( withLatestFrom(this.inboxStartSize$), auditTime(30), From 6445f9f998db70fbe9b9abf723dc17fd298b5d2f Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Wed, 5 Jun 2024 12:04:12 +1000 Subject: [PATCH 002/776] fix: ensure download blob supports 206 responses --- .../common/file-downloader/file-downloader.ts | 112 ++++++++++++++++-- 1 file changed, 103 insertions(+), 9 deletions(-) diff --git a/src/app/common/file-downloader/file-downloader.ts b/src/app/common/file-downloader/file-downloader.ts index ba49cd64da..031e26d191 100644 --- a/src/app/common/file-downloader/file-downloader.ts +++ b/src/app/common/file-downloader/file-downloader.ts @@ -2,28 +2,122 @@ import { HttpClient, HttpResponse } from '@angular/common/http'; import { Inject, Injectable } from '@angular/core'; import { alertService } from 'src/app/ajs-upgraded-providers'; +interface FileDownloaderData { + url: string, + response: HttpResponse, + success: (url: string, response: HttpResponse) => void, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + failure: (error: any) => void, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + binaryData: Blob[], +} + @Injectable({ providedIn: 'root', }) export class FileDownloaderService { - constructor(private httpClient: HttpClient, @Inject(alertService) private alerts: any) {} + constructor( + private httpClient: HttpClient, + @Inject(alertService) private alerts: any, + ) {} + + private processPartialBlob(data: FileDownloaderData) { + // We now need to ask for the next part of the file + const range = data.response.headers.get('Content-Range'); + if (range) { + // The range header is in the format "bytes start-end/totalSize" + const parts = range.split('/'); + + // Split into the range and the total size + if (parts.length === 2) { + // Parse the total size and the range + const totalSize = parseInt(parts[1], 10); + + // Extract the range after the "bytes" part + const contentRange = parts[0].split(' ')[1]; + + // Extract the parts of the range + const contentRangeParts = contentRange.split('-'); + + // If we have two parts, we have a valid range and size + if (contentRangeParts.length === 2) { + const start = parseInt(contentRangeParts[0], 10); + const end = parseInt(contentRangeParts[1], 10); + + // Check the start is the same as the length of the binary data received + if (start !== data.binaryData.map((value) => value.size).reduce((pv, cv) => pv + cv, 0)) { + console.log('Error: start != oldLen'); + this.alerts.add('danger', 'Error downloading file part received out of order'); + } + data.binaryData.push(data.response.body); + + // If the end is less than the total size, we need to request the next part + if (end + 1 < totalSize) { + const rangeHeader = {Range: `bytes=${end + 1}-${totalSize}`}; + this.httpClient + .get(data.url, {responseType: 'blob', observe: 'response', headers: rangeHeader}) + .subscribe({ + next: (response2) => { + data.response = response2; + this.processHttpResponse(data); + }, + error: (error) => { + if (data.failure) data.failure(error); + }, + }); + return; + } else { + // we have all of the data, so we can report success + this.reportSuccess(data); + } + } + } + } else { + // no range... so we can't do anything! + console.log('Error reading response from server - no range with 206 response'); + if (data.failure) data.failure('Unable to read data from server'); + } + } + + private processHttpResponse(data: FileDownloaderData) { + // Check if we have a partial content response + if (data.response.status === 206) { + this.processPartialBlob(data); + } else { + // Save the binary data we have received so far + data.binaryData.push(data.response.body); + this.reportSuccess(data); + } + } + + private reportSuccess(data: FileDownloaderData) { + const resourceUrl: string = window.URL.createObjectURL( + new Blob(data.binaryData, {type: data.response.body.type}), + ); + data.success(resourceUrl, data.response); + } public downloadBlob( url: string, success: (url: string, response: HttpResponse) => void, - failure: (error: any) => void + failure: (error: any) => void, ) { - this.httpClient.get(url, { responseType: 'blob', observe: 'response' }).subscribe({ + // Declare binary data outside of the subscription so that it can be accessed in the second requests when partial content is returned + const binaryData = []; + + this.httpClient.get(url, {responseType: 'blob', observe: 'response'}).subscribe({ next: (response) => { - const binaryData = []; - binaryData.push(response.body); - // response.headers.get('content-type') - const resourceUrl: string = window.URL.createObjectURL(new Blob(binaryData, { type: response.body.type })); - success(resourceUrl, response); + this.processHttpResponse({ + url: url, + response: response, + success: success, + failure: failure, + binaryData: binaryData, + }); }, error: (error) => { if (failure) failure(error); - } + }, }); } From c241f0ee348ce32bc4a3da9f92b306e9c664a7e9 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Wed, 5 Jun 2024 12:04:21 +1000 Subject: [PATCH 003/776] chore(release): 7.0.24 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d4d8b3c44..fb26e47aa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [7.0.24](https://github.com/macite/doubtfire-deploy/compare/v7.0.23...v7.0.24) (2024-06-05) + + +### Bug Fixes + +* ensure download blob supports 206 responses ([6445f9f](https://github.com/macite/doubtfire-deploy/commit/6445f9f998db70fbe9b9abf723dc17fd298b5d2f)) + ### [7.0.23](https://github.com/macite/doubtfire-deploy/compare/v7.0.22...v7.0.23) (2024-06-04) diff --git a/package-lock.json b/package-lock.json index 69d1ed0912..8cd4e2f7e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "7.0.23", + "version": "7.0.24", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "7.0.23", + "version": "7.0.24", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^14.2.4", diff --git a/package.json b/package.json index 2a179c703c..3eed4ceaef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "7.0.23", + "version": "7.0.24", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 8eba913ec4462e1c252a8a698b4b6de67c4ec25f Mon Sep 17 00:00:00 2001 From: maddernd <87599686+maddernd@users.noreply.github.com> Date: Sun, 10 Sep 2023 12:21:51 +1000 Subject: [PATCH 004/776] feat: add new Numbas Feature Added new Numbas Service to the frontend as part of Integration Changed by: Daniel Maddern --- src/app/api/services/numbas.service.ts | 66 ++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/app/api/services/numbas.service.ts diff --git a/src/app/api/services/numbas.service.ts b/src/app/api/services/numbas.service.ts new file mode 100644 index 0000000000..a0dc765644 --- /dev/null +++ b/src/app/api/services/numbas.service.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http'; +import { Observable, throwError } from 'rxjs'; +import { catchError, map, retry } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class NumbasService { + private readonly API_URL = 'http://localhost:3000/api/numbas_api'; + + constructor(private http: HttpClient) {} + + fetchResource(unitId: string, taskId: string, resourcePath: string): Observable { + const resourceUrl = `${this.API_URL}/${unitId}/${taskId}/${resourcePath}`; + const resourceMimeType = this.getMimeType(resourcePath); + + return this.http.get(resourceUrl, { responseType: 'blob' }).pipe( + retry(3), // Retrying up to 3 times before failing + map((blob) => new Blob([blob], { type: resourceMimeType })), + catchError((error: HttpErrorResponse) => { + console.error('Error fetching Numbas resource:', error); + return throwError('Error fetching Numbas resource.'); + }) + ); + } + + getMimeType(resourcePath: string): string { + const extension = resourcePath.split('.').pop()?.toLowerCase(); + const mimeTypeMap: { [key: string]: string } = { + 'html': 'text/html', + 'css': 'text/css', + 'js': 'application/javascript', + 'json': 'application/json', + 'png': 'image/png', + 'jpg': 'image/jpeg', + 'gif': 'image/gif', + 'svg': 'image/svg+xml' + }; + + return mimeTypeMap[extension || ''] || 'text/plain'; + } + uploadTest(unitId: string, taskId: string, file: File): Observable { + const uploadUrl = `${this.API_URL}/uploadNumbasTest`; + const formData = new FormData(); + + formData.append('file', file); + formData.append('unit_code', unitId); + formData.append('task_definition_id', taskId); + + const httpOptions = { + headers: new HttpHeaders({ + // You might need to set some headers here depending on your backend requirements + 'Accept': 'application/json' + }) + }; + + return this.http.post(uploadUrl, formData, httpOptions).pipe( + retry(3), + catchError((error: HttpErrorResponse) => { + console.error('Error uploading Numbas test:', error); + return throwError('Error uploading Numbas test.'); + }) + ); + } +} From 6f1ac4e4c1a78115c426838bce350bce286e44f0 Mon Sep 17 00:00:00 2001 From: maddernd <87599686+maddernd@users.noreply.github.com> Date: Mon, 11 Sep 2023 17:11:47 +1000 Subject: [PATCH 005/776] feat: add new Numbas Feature adjusted lint on edit-profile-component.spec.ts Changed by: Daniel Maddern --- src/app/account/edit-profile/edit-profile.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/account/edit-profile/edit-profile.component.spec.ts b/src/app/account/edit-profile/edit-profile.component.spec.ts index 189015c5a9..e15f8e5820 100644 --- a/src/app/account/edit-profile/edit-profile.component.spec.ts +++ b/src/app/account/edit-profile/edit-profile.component.spec.ts @@ -7,7 +7,7 @@ describe('EditProfileComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [EditProfileComponent] + declarations: [EditProfileComponent], }).compileComponents(); fixture = TestBed.createComponent(EditProfileComponent); From cee13b727b35f804fc16070885f0e57c422c9982 Mon Sep 17 00:00:00 2001 From: maddernd <87599686+maddernd@users.noreply.github.com> Date: Tue, 12 Sep 2023 08:54:37 +1000 Subject: [PATCH 006/776] feat: numbas-test-numbas-service Added numbas service and numbas service test daniel --- src/app/api/services/numbas.service.ts | 29 ++++++++- .../api/services/spec/numbas.service.spec.ts | 65 +++++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 src/app/api/services/spec/numbas.service.spec.ts diff --git a/src/app/api/services/numbas.service.ts b/src/app/api/services/numbas.service.ts index a0dc765644..663c778f04 100644 --- a/src/app/api/services/numbas.service.ts +++ b/src/app/api/services/numbas.service.ts @@ -2,21 +2,30 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError, map, retry } from 'rxjs/operators'; +import API_URL from 'src/app/config/constants/apiURL'; @Injectable({ providedIn: 'root' }) export class NumbasService { - private readonly API_URL = 'http://localhost:3000/api/numbas_api'; + private readonly API_URL = `${API_URL}/numbas_api`; constructor(private http: HttpClient) {} + /** + * Fetches a specified resource for a given unit and task. + * + * @param unitId - The ID of the unit + * @param taskId - The ID of the task + * @param resourcePath - Path to the desired resource + * @returns An Observable with the Blob of the fetched resource + */ fetchResource(unitId: string, taskId: string, resourcePath: string): Observable { const resourceUrl = `${this.API_URL}/${unitId}/${taskId}/${resourcePath}`; const resourceMimeType = this.getMimeType(resourcePath); return this.http.get(resourceUrl, { responseType: 'blob' }).pipe( - retry(3), // Retrying up to 3 times before failing + retry(3), map((blob) => new Blob([blob], { type: resourceMimeType })), catchError((error: HttpErrorResponse) => { console.error('Error fetching Numbas resource:', error); @@ -25,6 +34,12 @@ export class NumbasService { ); } + /** + * Determines the MIME type of a resource based on its extension. + * + * @param resourcePath - Path of the resource + * @returns MIME type string corresponding to the resource's extension + */ getMimeType(resourcePath: string): string { const extension = resourcePath.split('.').pop()?.toLowerCase(); const mimeTypeMap: { [key: string]: string } = { @@ -40,6 +55,15 @@ export class NumbasService { return mimeTypeMap[extension || ''] || 'text/plain'; } + + /** + * Uploads a Numbas test file for a given unit and task. + * + * @param unitId - The ID of the unit + * @param taskId - The ID of the task + * @param file - File object representing the Numbas test to be uploaded + * @returns An Observable with the response from the server + */ uploadTest(unitId: string, taskId: string, file: File): Observable { const uploadUrl = `${this.API_URL}/uploadNumbasTest`; const formData = new FormData(); @@ -50,7 +74,6 @@ export class NumbasService { const httpOptions = { headers: new HttpHeaders({ - // You might need to set some headers here depending on your backend requirements 'Accept': 'application/json' }) }; diff --git a/src/app/api/services/spec/numbas.service.spec.ts b/src/app/api/services/spec/numbas.service.spec.ts new file mode 100644 index 0000000000..7ef1530473 --- /dev/null +++ b/src/app/api/services/spec/numbas.service.spec.ts @@ -0,0 +1,65 @@ +import { TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { NumbasService } from '../numbas.service'; +import { HttpRequest } from '@angular/common/http'; + +describe('NumbasService', () => { + let numbasService: NumbasService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [NumbasService], + }); + + numbasService = TestBed.inject(NumbasService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should fetch resource as expected', fakeAsync(() => { + const dummyBlob = new Blob(['dummy blob'], { type: 'text/html' }); + + const unitId = 'sampleUnitId'; + const taskId = 'sampleTaskId'; + const resourcePath = 'sampleResource.html'; + + numbasService.fetchResource(unitId, taskId, resourcePath).subscribe((blob) => { + expect(blob.size).toBe(dummyBlob.size); + expect(blob.type).toBe(dummyBlob.type); + }); + + const req = httpMock.expectOne(`http://localhost:3000/api/numbas_api/${unitId}/${taskId}/${resourcePath}`); + expect(req.request.method).toBe('GET'); + + req.flush(dummyBlob); + + tick(); + })); + + it('should upload test as expected', fakeAsync(() => { + const dummyResponse = { success: true, message: 'File uploaded successfully' }; + + const unitId = 'sampleUnitId'; + const taskId = 'sampleTaskId'; + const file = new File(['dummy content'], 'sample.txt', { type: 'text/plain' }); + + numbasService.uploadTest(unitId, taskId, file).subscribe((response) => { + expect(response).toEqual(dummyResponse); + }); + + const req = httpMock.expectOne(`http://localhost:3000/api/numbas_api/uploadNumbasTest`); + expect(req.request.method).toBe('POST'); + + req.flush(dummyResponse); + + tick(); + })); + +}); + + From 8d3e3fd362a408e49fb1539f474e9e171830f5eb Mon Sep 17 00:00:00 2001 From: maddernd <87599686+maddernd@users.noreply.github.com> Date: Tue, 12 Sep 2023 11:20:15 +1000 Subject: [PATCH 007/776] test: add numbas service test file added a spec test file for numbas service daniel --- src/app/api/services/spec/numbas.service.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/api/services/spec/numbas.service.spec.ts b/src/app/api/services/spec/numbas.service.spec.ts index 7ef1530473..a29b13e149 100644 --- a/src/app/api/services/spec/numbas.service.spec.ts +++ b/src/app/api/services/spec/numbas.service.spec.ts @@ -3,6 +3,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/ import { NumbasService } from '../numbas.service'; import { HttpRequest } from '@angular/common/http'; + describe('NumbasService', () => { let numbasService: NumbasService; let httpMock: HttpTestingController; From 471d34486f2582e95aac84469187f087277cc079 Mon Sep 17 00:00:00 2001 From: maddernd <87599686+maddernd@users.noreply.github.com> Date: Thu, 14 Sep 2023 09:29:45 +1000 Subject: [PATCH 008/776] feat: added numbas-lms service code added the lms service code and functionality Added by Daniel --- src/app/api/services/numbas-lms.service.ts | 248 +++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 src/app/api/services/numbas-lms.service.ts diff --git a/src/app/api/services/numbas-lms.service.ts b/src/app/api/services/numbas-lms.service.ts new file mode 100644 index 0000000000..268c158484 --- /dev/null +++ b/src/app/api/services/numbas-lms.service.ts @@ -0,0 +1,248 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BehaviorSubject, Observable, throwError } from 'rxjs'; +import { TaskService } from './task.service'; +import { UserService } from './user.service'; +import API_URL from 'src/app/config/constants/apiURL'; + +declare let pipwerks: any; + +@Injectable({ + providedIn: 'root' +}) +export class NumbasLmsService { + + private readonly apiBaseUrl = `${API_URL}/savetests`;; + + private defaultValues: { [key: string]: string } = { + 'cmi.completion_status': 'not attempted', + 'cmi.entry': 'ab-initio', + 'numbas.user_role': 'learner', + 'numbas.duration_extension.units': 'seconds', + 'cmi.mode': 'normal', + 'cmi.undefinedlearner_response': '1', + 'cmi.undefinedresult' : '0' + + }; + + private testId: number = 0; + private taskId: number; + private learnerId: string; + initializationComplete$ = new BehaviorSubject(false); + + private scormErrors: { [key: string]: string } = { + "0": "No error", + "101": "General exception", + }; + + dataStore: { [key: string]: any } = this.getDefaultDataStore(); + + constructor( + private http: HttpClient, + private taskService: TaskService, + private userService: UserService +) { + pipwerks.SCORM.version = "2004"; + console.log(`SCORM version is set to: ${pipwerks.SCORM.version}`); + this.learnerId = this.userService.currentUser.studentId; + } + + getDefaultDataStore() { + // Use spread operator to merge defaultValues into the dataStore + return { + ...this.defaultValues, + pass_status: false, + completed: false, + }; + } + + Initialize(mode: 'attempt' | 'review' = 'attempt'): string { + console.log('Initialize() function called'); + const examName = 'test Exam Name 1'; + let xhr = new XMLHttpRequest(); + if (mode === 'review') { + this.SetValue('cmi.mode', 'review'); + + xhr.open("GET", `${this.apiBaseUrl}/completed-latest`, false); + xhr.send(); + console.log(xhr.responseText); + + if (xhr.status !== 200) { + console.error('Error fetching latest completed test result:', xhr.statusText); + return 'false'; + } + + try { + const completedTest = JSON.parse(xhr.responseText); + const parsedExamData = JSON.parse(completedTest.data.exam_data || '{}'); + + // Set entire suspendData string to cmi.suspend_data + this.SetValue('cmi.suspend_data', JSON.stringify(parsedExamData)); + + // Use SetValue to set parsedExamData values to dataStore + Object.keys(parsedExamData).forEach(key => { + this.SetValue(key, parsedExamData[key]); + }); + + this.SetValue('cmi.entry', 'RO'); + this.SetValue('cmi.mode', 'review'); + + console.log('Latest completed test data:', completedTest); + return 'true'; + + } catch (error) { + console.error('Error:', error); + return 'false'; + } + } + + xhr.open("GET", `${this.apiBaseUrl}/latest`, false); + xhr.send(); + console.log(xhr.responseText); + + if (xhr.status !== 200) { + console.error('Error fetching latest test result:', xhr.statusText); + return 'false'; + } + + let latestTest; + try { + latestTest = JSON.parse(xhr.responseText); + console.log('Latest test result:', latestTest); + this.testId = latestTest.data.id; + + if (latestTest.data['cmi_entry'] === 'ab-initio') { + console.log("starting new test"); + this.SetValue('cmi.learner_id', this.learnerId); + this.dataStore['name'] = examName; + this.dataStore['attempt_number'] = latestTest.data['attempt_number']; + console.log(this.dataStore); + } else if (latestTest.data['cmi_entry'] === 'resume') { + console.log("resuming test"); + const parsedExamData = JSON.parse(latestTest.data.exam_data || '{}'); + + this.dataStore = JSON.parse(JSON.stringify(parsedExamData)); + + console.log(this.dataStore); + } + + this.initializationComplete$.next(true); + + console.log("finished initlizing"); + return 'true'; + } catch (error) { + console.error('Error:', error); + return 'false'; + } +} + + + + isTestCompleted(): boolean { + return this.dataStore?.['completed'] || false; + } + + private resetDataStore() { + this.dataStore = this.getDefaultDataStore(); + } + + Terminate(): string { + console.log('Terminate Called'); + const examResult = this.dataStore["cmi.score.raw"]; + const status = this.GetValue("cmi.completion_status"); + this.dataStore['completed'] = true; + const currentAttemptNumber = this.dataStore['attempt_number'] || 0; + const ExamName = this.dataStore['name']; + this.SetValue('cmi.entry', 'RO'); + const cmientry = this.GetValue('cmi.entry'); + const data = { + task_id: this.taskId, + name: ExamName, + attempt_number: currentAttemptNumber, + pass_status: status === 'passed', + exam_data: JSON.stringify(this.dataStore), + completed: true, + exam_result: examResult, + cmi_entry: cmientry + }; + + const xhr = new XMLHttpRequest(); + if (this.testId) { + xhr.open("PUT", `${this.apiBaseUrl}/${this.testId}`, false); + } else { + xhr.open("POST", this.apiBaseUrl, false); + } + xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + xhr.send(JSON.stringify(data)); + + if (xhr.status !== 200) { + console.error('Error sending test data:', xhr.statusText); + return 'false'; + } + this.resetDataStore(); + return 'true'; + } + + GetValue(element: string): string { + return this.dataStore[element] || ''; + } + + SetValue(element: string, value: any): string { + if (element.startsWith('cmi.')) { + this.dataStore[element] = value; + } + return 'true'; + } +//function to save the state of the exam. +Commit(): string { + if (!this.initializationComplete$.getValue()) { + console.warn('Initialization not complete. Cannot commit.'); + return 'false'; + } + + // Set cmi.entry to 'resume' before committing dataStore + this.dataStore['cmi.entry'] = 'resume'; + if (!this.isTestCompleted()) { + this.dataStore['cmi.exit'] = 'suspend'; + } + console.log("Committing dataStore:", this.dataStore); + + // Directly stringify the dataStore + const jsonData = JSON.stringify(this.dataStore); + + // Use XHR to send the request + const xhr = new XMLHttpRequest(); + xhr.open('PUT', `${this.apiBaseUrl}/${this.testId}/suspend`, true); + xhr.setRequestHeader('Content-Type', 'application/json'); + + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 400) { + console.log('Suspend data saved successfully.'); + } else { + console.error('Error saving suspend data:', xhr.responseText); + } + }; + + xhr.onerror = () => { + console.error('Request failed.'); + }; + + xhr.send(jsonData); + return 'true'; +} + + // Placeholder methods for SCORM error handling + GetLastError(): string { + //console.log('Get Last Error called'); + return "0"; + } + + GetErrorString(errorCode: string): string { + return ''; + } + + GetDiagnostic(errorCode: string): string { + //console.log('Get Diagnoistic called'); + return ''; + } +} From 5d9d2c185fcf65f01a5a2630338b58324e1c49b8 Mon Sep 17 00:00:00 2001 From: maddernd <87599686+maddernd@users.noreply.github.com> Date: Thu, 14 Sep 2023 09:35:31 +1000 Subject: [PATCH 009/776] test: added numbas-lms spec test added the spec test basic version for numbas-lms service Added by Daniel --- .../services/spec/numbas-lms.service.spec.ts | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/app/api/services/spec/numbas-lms.service.spec.ts diff --git a/src/app/api/services/spec/numbas-lms.service.spec.ts b/src/app/api/services/spec/numbas-lms.service.spec.ts new file mode 100644 index 0000000000..f456d5a894 --- /dev/null +++ b/src/app/api/services/spec/numbas-lms.service.spec.ts @@ -0,0 +1,88 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { NumbasLmsService } from '../numbas-lms.service'; +import { TaskService } from '../task.service'; +import { UserService } from '../user.service'; +import { of } from 'rxjs'; + +describe('NumbasLmsService', () => { + let service: NumbasLmsService; + let httpTestingController: HttpTestingController; + let mockUserService: Partial; + let mockTaskService: Partial; + + const mockUserData = { + currentUser: { studentId: '12345' } + }; + + beforeEach(() => { + mockUserService = { + currentUser: mockUserData.currentUser + }; + + mockTaskService = { + // you can add mocked methods if needed for the TaskService + }; + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + NumbasLmsService, + { provide: UserService, useValue: mockUserService }, + { provide: TaskService, useValue: mockTaskService } + ] + }); + + service = TestBed.inject(NumbasLmsService); + httpTestingController = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpTestingController.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should initialize with default values', () => { + expect(service.GetValue('cmi.completion_status')).toBe('not attempted'); + expect(service.GetValue('cmi.entry')).toBe('ab-initio'); + }); + + describe('Initialize function', () => { + + it('should handle review mode and get latest completed test result', () => { + const mockResponse = { + data: { + exam_data: JSON.stringify({ someData: 'value' }) + } + }; + + service.Initialize('review'); + const req = httpTestingController.expectOne(`${service['apiBaseUrl']}/completed-latest`); + expect(req.request.method).toEqual('GET'); + req.flush(mockResponse); + + expect(service.GetValue('cmi.suspend_data')).toEqual(JSON.stringify({ someData: 'value' })); + }); + + it('should handle attempt mode and get latest test result', () => { + const mockResponse = { + data: { + id: 1, + cmi_entry: 'ab-initio', + attempt_number: 2 + } + }; + + service.Initialize('attempt'); + const req = httpTestingController.expectOne(`${service['apiBaseUrl']}/latest`); + expect(req.request.method).toEqual('GET'); + req.flush(mockResponse); + + expect(service.GetValue('cmi.learner_id')).toBe('12345'); + }); + }); + +}); From 316abc775eeba6ca846c7c742bc88bec61d05a5e Mon Sep 17 00:00:00 2001 From: maddernd <87599686+maddernd@users.noreply.github.com> Date: Thu, 14 Sep 2023 13:14:48 +1000 Subject: [PATCH 010/776] fix: adjusted edit profile accidental change removed the addtional comma added into this component daniel --- src/app/account/edit-profile/edit-profile.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/account/edit-profile/edit-profile.component.spec.ts b/src/app/account/edit-profile/edit-profile.component.spec.ts index e15f8e5820..189015c5a9 100644 --- a/src/app/account/edit-profile/edit-profile.component.spec.ts +++ b/src/app/account/edit-profile/edit-profile.component.spec.ts @@ -7,7 +7,7 @@ describe('EditProfileComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [EditProfileComponent], + declarations: [EditProfileComponent] }).compileComponents(); fixture = TestBed.createComponent(EditProfileComponent); From edbd536e73ace8b6b4cced1481c809fd36524dce Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 5 Mar 2024 23:38:23 +1100 Subject: [PATCH 011/776] feat: add Numbas test upload section and reorder editor sections --- .../task-definition-editor.component.html | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) 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 5bbf1e64e5..bd2c7ec65d 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 @@ -104,6 +104,24 @@

+
+

Upload Numbas test

+

Upload the corresponding Numbas test

+
+ +
+
+ + +
+
+
+ 7 +
+
+

Task assessment automation @@ -123,7 +141,7 @@

- 7 + 8

From 4ecaee8ad1c0caed9a5d848066a173000989f79b Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 5 Mar 2024 23:39:46 +1100 Subject: [PATCH 012/776] feat: add Numbas upload component and related functions to task-definition model --- src/app/api/models/task-definition.ts | 19 +++++ src/app/doubtfire-angular.module.ts | 2 + .../task-definition-numbas.component.html | 18 +++++ .../task-definition-numbas.component.scss | 0 .../task-definition-numbas.component.ts | 69 +++++++++++++++++++ 5 files changed, 108 insertions(+) create mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html create mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.scss create mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index 1b49a2e856..b5c4f5155a 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -31,6 +31,7 @@ export class TaskDefinition extends Entity { groupSet: GroupSet = null; hasTaskSheet: boolean; hasTaskResources: boolean; + hasNumbasTest: boolean; hasTaskAssessmentResources: boolean; isGraded: boolean; maxQualityPts: number; @@ -152,6 +153,13 @@ export class TaskDefinition extends Entity { }`; } + public getNumbasTestUrl(asAttachment: boolean = false) { + const constants = AppInjector.get(DoubtfireConstants); + return `${constants.API_URL}/units/${this.unit.id}/task_definitions/${this.id}/numbas_test.json${ + asAttachment ? '?as_attachment=true' : '' + }`; + } + public get targetGradeText(): string { return Grade.GRADES[this.targetGrade]; } @@ -176,6 +184,12 @@ export class TaskDefinition extends Entity { }/task_resources`; } + public get numbasTestUploadUrl(): string { + return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.unit.id}/task_definitions/${ + this.id + }/numbas_test`; + } + public get taskAssessmentResourcesUploadUrl(): string { return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.unit.id}/task_definitions/${ this.id @@ -198,6 +212,11 @@ export class TaskDefinition extends Entity { return httpClient.delete(this.taskResourcesUploadUrl).pipe(tap(() => (this.hasTaskResources = false))); } + public deleteNumbasTest(): Observable { + const httpClient = AppInjector.get(HttpClient); + return httpClient.delete(this.numbasTestUploadUrl).pipe(tap(() => (this.hasNumbasTest = false))); + } + public deleteTaskAssessmentResources(): Observable { const httpClient = AppInjector.get(HttpClient); return httpClient diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 9284745988..a50478b800 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -203,6 +203,7 @@ import {TaskDefinitionUploadComponent} from './units/states/edit/directives/unit import {TaskDefinitionOptionsComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-options/task-definition-options.component'; import {TaskDefinitionResourcesComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-resources/task-definition-resources.component'; import {TaskDefinitionOverseerComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component'; +import {TaskDefinitionNumbasComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component'; import {UnitAnalyticsComponent} from './units/states/analytics/unit-analytics-route.component'; import {FileDropComponent} from './common/file-drop/file-drop.component'; import {UnitTaskEditorComponent} from './units/states/edit/directives/unit-tasks-editor/unit-task-editor.component'; @@ -262,6 +263,7 @@ import {GradeService} from './common/services/grade.service'; TaskDefinitionOptionsComponent, TaskDefinitionResourcesComponent, TaskDefinitionOverseerComponent, + TaskDefinitionNumbasComponent, UnitAnalyticsComponent, StudentTutorialSelectComponent, StudentCampusSelectComponent, diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html new file mode 100644 index 0000000000..70367a462d --- /dev/null +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html @@ -0,0 +1,18 @@ +
+ + @if (taskDefinition.hasNumbasTest) { +
+ + +
+ } +
diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.scss b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.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-numbas/task-definition-numbas.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts new file mode 100644 index 0000000000..596f2933ca --- /dev/null +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts @@ -0,0 +1,69 @@ +import { Component, Inject, Input } from '@angular/core'; +import { alertService } from 'src/app/ajs-upgraded-providers'; +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 { FileDownloaderService } from 'src/app/common/file-downloader/file-downloader.service'; + +@Component({ + selector: 'f-task-definition-numbas', + templateUrl: 'task-definition-numbas.component.html', + styleUrls: ['task-definition-numbas.component.scss'], +}) +export class TaskDefinitionNumbasComponent { + @Input() taskDefinition: TaskDefinition; + + constructor( + private fileDownloaderService: FileDownloaderService, + @Inject(alertService) private alerts: any, + private taskDefinitionService: TaskDefinitionService + ) {} + + public get unit(): Unit { + return this.taskDefinition?.unit; + } + + public downloadNumbasTest() { + this.fileDownloaderService.downloadFile( + this.taskDefinition.getNumbasTestUrl(true), + this.taskDefinition.name + '-Numbas.zip', + ); + } + + public removeNumbasTest() { + this.taskDefinition.deleteNumbasTest().subscribe({ + next: () => this.alerts.add('success', 'Deleted Numbas test', 2000), + error: (message) => this.alerts.add('danger', message, 6000), + }); + } + + public uploadNumbasTest(files: FileList) { + const validFiles = Array.from(files as ArrayLike).filter((f) => f.type === 'application/zip'); + if (validFiles.length > 0) { + const file = validFiles[0]; + // Temporary until Numbas backend is fixed: save uploaded file to local Downloads folder + this.saveZipFile(file); + this.taskDefinition.hasNumbasTest = true; + // this.taskDefinitionService.uploadNumbasTest(this.taskDefinition, file).subscribe({ + // next: () => this.alerts.add('success', 'Uploaded Numbas test', 2000), + // error: (message) => this.alerts.add('danger', message, 6000), + // }); + } else { + this.alerts.add('danger', 'Please drop a ZIP to upload for this task', 6000); + } + } + + private saveZipFile(zipData) { + const blob = new Blob([zipData], {type: 'application/zip'}); + + // Create an anchor element and set its href to the blob URL + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = 'numbas.zip'; + + // Append the link to the document, trigger the download, then remove the link + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } +} From 7e52ad5e5759291d7d61805070ec482eb49c90be Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Thu, 7 Mar 2024 16:01:00 +1100 Subject: [PATCH 013/776] feat: insert Numbas test rules options in the task editor --- src/app/api/models/task-definition.ts | 9 ++- .../task-definition-numbas.component.html | 58 ++++++++++++++----- .../task-definition-numbas.component.ts | 2 +- 3 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index b5c4f5155a..9d19d5d1b7 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -31,7 +31,12 @@ export class TaskDefinition extends Entity { groupSet: GroupSet = null; hasTaskSheet: boolean; hasTaskResources: boolean; - hasNumbasTest: boolean; + hasEnabledNumbasTest: boolean; + hasUploadedNumbasTest: boolean; + hasUnlimitedRetriesForNumbas: boolean; + hasTimeDelayForNumbas: boolean; + isNumbasRestrictedTo1Attempt: boolean; + numbasTimeDelay: string = 'no delay'; hasTaskAssessmentResources: boolean; isGraded: boolean; maxQualityPts: number; @@ -214,7 +219,7 @@ export class TaskDefinition extends Entity { public deleteNumbasTest(): Observable { const httpClient = AppInjector.get(HttpClient); - return httpClient.delete(this.numbasTestUploadUrl).pipe(tap(() => (this.hasNumbasTest = false))); + return httpClient.delete(this.numbasTestUploadUrl).pipe(tap(() => (this.hasUploadedNumbasTest = false))); } public deleteTaskAssessmentResources(): Observable { diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html index 70367a462d..bd657d847b 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html @@ -1,18 +1,44 @@ -
- - @if (taskDefinition.hasNumbasTest) { -
- - -
+
+ + Enable Numbas Test + + +
+ + @if (taskDefinition.hasUploadedNumbasTest) { +
+ + +
+ } +
+ +
+ Select test rules: + Unlimited retries + Time delay + Restrict to 1 attempt +
+ + @if (taskDefinition.hasTimeDelayForNumbas) { + + Time delay + + No delay + 30 min + 2 hours + 1 day + See tutor + + }
diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts index 596f2933ca..b36218bd10 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts @@ -43,7 +43,7 @@ export class TaskDefinitionNumbasComponent { const file = validFiles[0]; // Temporary until Numbas backend is fixed: save uploaded file to local Downloads folder this.saveZipFile(file); - this.taskDefinition.hasNumbasTest = true; + this.taskDefinition.hasUploadedNumbasTest = true; // this.taskDefinitionService.uploadNumbasTest(this.taskDefinition, file).subscribe({ // next: () => this.alerts.add('success', 'Uploaded Numbas test', 2000), // error: (message) => this.alerts.add('danger', message, 6000), From 2c7dab555fc9a226b980a8dae96f5738bf517b1b Mon Sep 17 00:00:00 2001 From: ublefo <90136978+ublefo@users.noreply.github.com> Date: Thu, 21 Mar 2024 14:46:53 +1100 Subject: [PATCH 014/776] feat: implement numbas test data upload in task definition service renamed api endpoint to reduce confusion between components --- src/app/api/models/task-definition.ts | 4 +-- .../api/services/task-definition.service.ts | 6 +++++ .../task-definition-numbas.component.ts | 26 ++++--------------- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index 9d19d5d1b7..e575b40677 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -160,7 +160,7 @@ export class TaskDefinition extends Entity { public getNumbasTestUrl(asAttachment: boolean = false) { const constants = AppInjector.get(DoubtfireConstants); - return `${constants.API_URL}/units/${this.unit.id}/task_definitions/${this.id}/numbas_test.json${ + return `${constants.API_URL}/units/${this.unit.id}/task_definitions/${this.id}/numbas_data.json${ asAttachment ? '?as_attachment=true' : '' }`; } @@ -192,7 +192,7 @@ export class TaskDefinition extends Entity { public get numbasTestUploadUrl(): string { return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.unit.id}/task_definitions/${ this.id - }/numbas_test`; + }/numbas_data`; } public get taskAssessmentResourcesUploadUrl(): string { diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index 13a1dd2797..f9f14fb10d 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -128,4 +128,10 @@ export class TaskDefinitionService extends CachedEntityService { formData.append('file', file); return AppInjector.get(HttpClient).post(taskDefinition.taskAssessmentResourcesUploadUrl, formData); } + + public uploadNumbasData(taskDefinition: TaskDefinition, file: File): Observable { + const formData = new FormData(); + formData.append('file', file); + return AppInjector.get(HttpClient).post(taskDefinition.numbasTestUploadUrl, formData); + } } diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts index b36218bd10..3c18b31743 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts @@ -41,29 +41,13 @@ export class TaskDefinitionNumbasComponent { const validFiles = Array.from(files as ArrayLike).filter((f) => f.type === 'application/zip'); if (validFiles.length > 0) { const file = validFiles[0]; - // Temporary until Numbas backend is fixed: save uploaded file to local Downloads folder - this.saveZipFile(file); + this.taskDefinitionService.uploadNumbasData(this.taskDefinition, file).subscribe({ + next: () => this.alerts.add('success', 'Uploaded Numbas test data', 2000), + error: (message) => this.alerts.add('danger', message, 6000), + }); this.taskDefinition.hasUploadedNumbasTest = true; - // this.taskDefinitionService.uploadNumbasTest(this.taskDefinition, file).subscribe({ - // next: () => this.alerts.add('success', 'Uploaded Numbas test', 2000), - // error: (message) => this.alerts.add('danger', message, 6000), - // }); } else { - this.alerts.add('danger', 'Please drop a ZIP to upload for this task', 6000); + this.alerts.add('danger', 'Please drop a zip file to upload Numbas test data for this task', 6000); } } - - private saveZipFile(zipData) { - const blob = new Blob([zipData], {type: 'application/zip'}); - - // Create an anchor element and set its href to the blob URL - const link = document.createElement('a'); - link.href = URL.createObjectURL(blob); - link.download = 'numbas.zip'; - - // Append the link to the document, trigger the download, then remove the link - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } } From ff28e4802b86dc22be339cc196ef82ade67934f7 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:31:21 +1100 Subject: [PATCH 015/776] feat: add Numbas config options to task def service keys --- src/app/api/services/task-definition.service.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index f9f14fb10d..903d3c73f9 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -93,6 +93,12 @@ export class TaskDefinitionService extends CachedEntityService { 'hasTaskSheet', 'hasTaskResources', 'hasTaskAssessmentResources', + 'hasEnabledNumbasTest', + 'hasUploadedNumbasTest', + 'hasUnlimitedRetriesForNumbas', + 'hasTimeDelayForNumbas', + 'isNumbasRestrictedTo1Attempt', + 'numbasTimeDelay', 'isGraded', 'maxQualityPts', 'overseerImageId', @@ -103,7 +109,8 @@ export class TaskDefinitionService extends CachedEntityService { 'id', 'hasTaskSheet', 'hasTaskResources', - 'hasTaskAssessmentResources' + 'hasTaskAssessmentResources', + 'hasUploadedNumbasTest' ); } From 821feb93a8a40d095ef4f50adedbfd9216278fa9 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Fri, 22 Mar 2024 14:29:06 +1100 Subject: [PATCH 016/776] fix: show delete and download buttons in editor when Numbas test exists --- src/app/api/models/task-definition.ts | 4 ++-- src/app/api/services/task-definition.service.ts | 4 ++-- .../task-definition-numbas.component.html | 2 +- .../task-definition-numbas.component.ts | 1 - 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index e575b40677..81b2793d1f 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -32,7 +32,7 @@ export class TaskDefinition extends Entity { hasTaskSheet: boolean; hasTaskResources: boolean; hasEnabledNumbasTest: boolean; - hasUploadedNumbasTest: boolean; + hasNumbasData: boolean; hasUnlimitedRetriesForNumbas: boolean; hasTimeDelayForNumbas: boolean; isNumbasRestrictedTo1Attempt: boolean; @@ -219,7 +219,7 @@ export class TaskDefinition extends Entity { public deleteNumbasTest(): Observable { const httpClient = AppInjector.get(HttpClient); - return httpClient.delete(this.numbasTestUploadUrl).pipe(tap(() => (this.hasUploadedNumbasTest = false))); + return httpClient.delete(this.numbasTestUploadUrl).pipe(tap(() => (this.hasNumbasData = false))); } public deleteTaskAssessmentResources(): Observable { diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index 903d3c73f9..a64649b9d9 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -94,7 +94,7 @@ export class TaskDefinitionService extends CachedEntityService { 'hasTaskResources', 'hasTaskAssessmentResources', 'hasEnabledNumbasTest', - 'hasUploadedNumbasTest', + 'hasNumbasData', 'hasUnlimitedRetriesForNumbas', 'hasTimeDelayForNumbas', 'isNumbasRestrictedTo1Attempt', @@ -110,7 +110,7 @@ export class TaskDefinitionService extends CachedEntityService { 'hasTaskSheet', 'hasTaskResources', 'hasTaskAssessmentResources', - 'hasUploadedNumbasTest' + 'hasNumbasData' ); } diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html index bd657d847b..38beb3ab78 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html @@ -10,7 +10,7 @@ accept="application/zip" [desiredFileName]="'Numbas zip'" /> - @if (taskDefinition.hasUploadedNumbasTest) { + @if (taskDefinition.hasNumbasData) {
-
- Select test rules: - Unlimited retries - Time delay - Restrict to 1 attempt -
+ @if (taskDefinition.hasEnabledNumbasTest) { +
+ Select test rules: + Unlimited retries + Time delay + Restrict to 1 attempt +
- @if (taskDefinition.hasTimeDelayForNumbas) { - - Time delay - - No delay - 30 min - 2 hours - 1 day - See tutor - - + @if (taskDefinition.hasTimeDelayForNumbas) { + + Time delay + + No delay + 30 min + 2 hours + 1 day + See tutor + + + } }
From 6e96584e7f0efba22b6f3b64ad1bcbfcbf31ee0f Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Sat, 23 Mar 2024 00:03:29 +1100 Subject: [PATCH 019/776] refactor: replace Numbas config checkboxes with input fields --- src/app/api/models/task-definition.ts | 4 +--- .../api/services/task-definition.service.ts | 4 +--- .../task-definition-numbas.component.html | 24 +++++++++++-------- .../task-definition-numbas.component.ts | 3 +++ 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index 81b2793d1f..7d012fb2b0 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -33,10 +33,8 @@ export class TaskDefinition extends Entity { hasTaskResources: boolean; hasEnabledNumbasTest: boolean; hasNumbasData: boolean; - hasUnlimitedRetriesForNumbas: boolean; - hasTimeDelayForNumbas: boolean; - isNumbasRestrictedTo1Attempt: boolean; numbasTimeDelay: string = 'no delay'; + numbasAttemptLimit: number = 0; hasTaskAssessmentResources: boolean; isGraded: boolean; maxQualityPts: number; diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index a64649b9d9..3724b29b16 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -95,10 +95,8 @@ export class TaskDefinitionService extends CachedEntityService { 'hasTaskAssessmentResources', 'hasEnabledNumbasTest', 'hasNumbasData', - 'hasUnlimitedRetriesForNumbas', - 'hasTimeDelayForNumbas', - 'isNumbasRestrictedTo1Attempt', 'numbasTimeDelay', + 'numbasAttemptLimit', 'isGraded', 'maxQualityPts', 'overseerImageId', diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html index 3786ea90a3..712f01547a 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html @@ -23,15 +23,8 @@
@if (taskDefinition.hasEnabledNumbasTest) { -
- Select test rules: - Unlimited retries - Time delay - Restrict to 1 attempt -
- - @if (taskDefinition.hasTimeDelayForNumbas) { - +
+ Time delay No delay @@ -41,6 +34,17 @@ See tutor - } + + Attempt Limit + + +
} diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts index 878ec25319..6a26501ba3 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts @@ -1,4 +1,5 @@ import { Component, Inject, Input } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; import { alertService } from 'src/app/ajs-upgraded-providers'; import { TaskDefinition } from 'src/app/api/models/task-definition'; import { Unit } from 'src/app/api/models/unit'; @@ -19,6 +20,8 @@ export class TaskDefinitionNumbasComponent { private taskDefinitionService: TaskDefinitionService ) {} + public scoreControl = new FormControl('', [Validators.max(100), Validators.min(0)]); + public get unit(): Unit { return this.taskDefinition?.unit; } From 7c1734b137ac369b3b605b38749c845df38b9a78 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Sun, 17 Dec 2023 21:28:21 +1100 Subject: [PATCH 020/776] feat: add numbas component --- .../numbas-component.component.html | 4 + .../numbas-component.component.scss | 0 .../numbas-component.component.spec.ts | 23 ++++++ .../numbas-component.component.ts | 77 +++++++++++++++++++ src/app/doubtfire-angular.module.ts | 6 ++ 5 files changed, 110 insertions(+) create mode 100644 src/app/common/numbas-component/numbas-component.component.html create mode 100644 src/app/common/numbas-component/numbas-component.component.scss create mode 100644 src/app/common/numbas-component/numbas-component.component.spec.ts create mode 100644 src/app/common/numbas-component/numbas-component.component.ts diff --git a/src/app/common/numbas-component/numbas-component.component.html b/src/app/common/numbas-component/numbas-component.component.html new file mode 100644 index 0000000000..438dbc6d19 --- /dev/null +++ b/src/app/common/numbas-component/numbas-component.component.html @@ -0,0 +1,4 @@ +

Run Numbas Test

+ + + diff --git a/src/app/common/numbas-component/numbas-component.component.scss b/src/app/common/numbas-component/numbas-component.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/common/numbas-component/numbas-component.component.spec.ts b/src/app/common/numbas-component/numbas-component.component.spec.ts new file mode 100644 index 0000000000..31dad5e305 --- /dev/null +++ b/src/app/common/numbas-component/numbas-component.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NumbasComponent } from './numbas-component.component'; + +describe('NumbasComponent', () => { + let component: NumbasComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ NumbasComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(NumbasComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/common/numbas-component/numbas-component.component.ts b/src/app/common/numbas-component/numbas-component.component.ts new file mode 100644 index 0000000000..7a4fa96b9c --- /dev/null +++ b/src/app/common/numbas-component/numbas-component.component.ts @@ -0,0 +1,77 @@ +import { Component, OnInit } from '@angular/core'; +import { NumbasService } from 'src/app/api/services/numbas.service'; +import { NumbasLmsService } from 'src/app/api/services/numbas-lms.service'; + +declare global { + interface Window { API_1484_11: any; } +} + +@Component({ + selector: 'numbas-component', + templateUrl: './numbas-component.component.html' +}) +export class NumbasComponent implements OnInit { + currentMode: 'attempt' | 'review' = 'attempt'; + constructor( + private numbasService: NumbasService, + private lmsService: NumbasLmsService + ) {} + + ngOnInit(): void { + this.interceptIframeRequests(); + + window.API_1484_11 = { + Initialize: () => this.lmsService.Initialize(this.currentMode), + Terminate: () => this.lmsService.Terminate(), + GetValue: (element: string) => this.lmsService.GetValue(element), + SetValue: (element: string, value: string) => this.lmsService.SetValue(element, value), + Commit: () => this.lmsService.Commit(), + GetLastError: () => this.lmsService.GetLastError(), + GetErrorString: (errorCode: string) => this.lmsService.GetErrorString(errorCode), + GetDiagnostic: (errorCode: string) => this.lmsService.GetDiagnostic(errorCode) + }; + } + + launchNumbasTest(mode: 'attempt' | 'review' = 'attempt'): void { + this.currentMode = mode; + const iframe = document.createElement('iframe'); + iframe.src = 'http://localhost:4201/api/numbas_api/index.html'; + iframe.style.width = '100%'; + iframe.style.height = '800px'; + document.body.appendChild(iframe); + } + setReviewMode(): void { + this.reviewTest(); + } + + removeNumbasTest(): void { + const iframe = document.getElementsByTagName('iframe')[0]; + iframe?.parentNode?.removeChild(iframe); + } + reviewTest(): void { + this.launchNumbasTest('review'); + } + + interceptIframeRequests(): void { + const originalOpen = XMLHttpRequest.prototype.open; + const numbasService = this.numbasService; + XMLHttpRequest.prototype.open = function (this: XMLHttpRequest, method: string, url: string | URL, async: boolean = true, username?: string | null, password?: string | null) { + if (typeof url === 'string' && url.startsWith('/api/numbas_api/')) { + const resourcePath = url.replace('/api/numbas_api/', ''); + this.abort(); + numbasService.fetchResource('1', '1', resourcePath).subscribe( + (resourceData) => { + if (this.onload) { + this.onload.call(this, resourceData); + } + }, + (error) => { + console.error('Error fetching Numbas resource:', error); + } + ); + } else { + originalOpen.call(this, method, url, async, username, password); + } + }; + } +} diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index a50478b800..d15c23b234 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -225,6 +225,9 @@ import {FTaskSheetViewComponent} from './units/states/tasks/viewer/directives/f- import {TasksViewerComponent} from './units/states/tasks/tasks-viewer/tasks-viewer.component'; import {UnitCodeComponent} from './common/unit-code/unit-code.component'; import {GradeService} from './common/services/grade.service'; +import {NumbasComponent} from './common/numbas-component/numbas-component.component'; +import {NumbasService} from './api/services/numbas.service'; +import {NumbasLmsService} from './api/services/numbas-lms.service'; @NgModule({ // Components we declare @@ -327,6 +330,7 @@ import {GradeService} from './common/services/grade.service'; FUsersComponent, FTaskBadgeComponent, FUnitsComponent, + NumbasComponent, ], // Services we provide providers: [ @@ -398,6 +402,8 @@ import {GradeService} from './common/services/grade.service'; TasksForInboxSearchPipe, IsActiveUnitRole, CreateNewUnitModal, + NumbasService, + NumbasLmsService, provideLottieOptions({ player: () => player, }), From e61295c50006dbc89cfc0639bb3c68a09a3cda9d Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Sat, 23 Mar 2024 00:15:06 +1100 Subject: [PATCH 021/776] feat: add Numbas test section on ready for feedback --- .../numbas-component.component.html | 1 - .../numbas-component.component.ts | 2 +- .../upload-submission-modal.coffee | 2 +- .../upload-submission-modal.tpl.html | 18 ++++++++++++++++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/app/common/numbas-component/numbas-component.component.html b/src/app/common/numbas-component/numbas-component.component.html index 438dbc6d19..0f0f2eb14c 100644 --- a/src/app/common/numbas-component/numbas-component.component.html +++ b/src/app/common/numbas-component/numbas-component.component.html @@ -1,4 +1,3 @@ -

Run Numbas Test

diff --git a/src/app/common/numbas-component/numbas-component.component.ts b/src/app/common/numbas-component/numbas-component.component.ts index 7a4fa96b9c..2e5cff929e 100644 --- a/src/app/common/numbas-component/numbas-component.component.ts +++ b/src/app/common/numbas-component/numbas-component.component.ts @@ -7,7 +7,7 @@ declare global { } @Component({ - selector: 'numbas-component', + selector: 'f-numbas-component', templateUrl: './numbas-component.component.html' }) export class NumbasComponent implements OnInit { diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee index b9c9ebd2b9..9fea8fbad9 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee @@ -100,7 +100,7 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) # States functionality states = { # All possible states - all: ['group', 'files', 'alignment', 'comments', 'uploading'] + all: ['group', 'numbas', 'files', 'alignment', 'comments', 'uploading'] # Only states which are shown (populated in initialise) shown: [] # The currently active state (set in initialise) diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html index 9caab7897e..edd0fda105 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html @@ -43,6 +43,24 @@

+
+
+
+

+ Attempt Numbas Test +

+ + Complete the Numbas test first to proceed to upload evidence of your task completion. + +
+
+ +
+
+
From 56e1a5de44ee275f9544b77909b7472f1020c2c3 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Sat, 23 Mar 2024 17:36:52 +1100 Subject: [PATCH 022/776] fix: show previously configured Numbas attempt limit --- .../task-definition-numbas.component.html | 6 +++--- .../task-definition-numbas.component.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html index 712f01547a..7ff44a6049 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html @@ -35,14 +35,14 @@ - Attempt Limit + Attempt limit
diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts index 6a26501ba3..6588453430 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts @@ -20,7 +20,7 @@ export class TaskDefinitionNumbasComponent { private taskDefinitionService: TaskDefinitionService ) {} - public scoreControl = new FormControl('', [Validators.max(100), Validators.min(0)]); + public attemptLimitControl = new FormControl('', [Validators.max(100), Validators.min(0)]); public get unit(): Unit { return this.taskDefinition?.unit; From 0afa7197293c90a154c1716db91ecadae3677d53 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Sun, 24 Mar 2024 13:41:29 +1100 Subject: [PATCH 023/776] feat: change Numbas time delay config to enable incremental delays --- src/app/api/models/task-definition.ts | 2 +- src/app/api/services/task-definition.service.ts | 2 +- .../task-definition-numbas.component.html | 13 +++---------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index 7d012fb2b0..a669b2fe36 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -33,7 +33,7 @@ export class TaskDefinition extends Entity { hasTaskResources: boolean; hasEnabledNumbasTest: boolean; hasNumbasData: boolean; - numbasTimeDelay: string = 'no delay'; + hasNumbasTimeDelay: boolean; numbasAttemptLimit: number = 0; hasTaskAssessmentResources: boolean; isGraded: boolean; diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index 3724b29b16..6047af0546 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -95,7 +95,7 @@ export class TaskDefinitionService extends CachedEntityService { 'hasTaskAssessmentResources', 'hasEnabledNumbasTest', 'hasNumbasData', - 'numbasTimeDelay', + 'hasNumbasTimeDelay', 'numbasAttemptLimit', 'isGraded', 'maxQualityPts', diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html index 7ff44a6049..e0021065aa 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html @@ -24,16 +24,9 @@ @if (taskDefinition.hasEnabledNumbasTest) {
- - Time delay - - No delay - 30 min - 2 hours - 1 day - See tutor - - + + Enable incremental time delays between test attempts + Attempt limit Date: Wed, 27 Mar 2024 23:48:29 +1100 Subject: [PATCH 024/776] feat: show launch button on ready for feedback if Numbas test is enabled for the task --- .../common/numbas-component/numbas-component.component.html | 6 +++--- src/app/doubtfire-angularjs.module.ts | 4 ++++ .../upload-submission-modal/upload-submission-modal.coffee | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/app/common/numbas-component/numbas-component.component.html b/src/app/common/numbas-component/numbas-component.component.html index 0f0f2eb14c..dba53ea116 100644 --- a/src/app/common/numbas-component/numbas-component.component.html +++ b/src/app/common/numbas-component/numbas-component.component.html @@ -1,3 +1,3 @@ - - - + diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 868e381213..317094dc2e 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -225,6 +225,8 @@ import {FUnitsComponent} from './admin/states/f-units/f-units.component'; import {MarkedPipe} from './common/pipes/marked.pipe'; import {AlertService} from './common/services/alert.service'; import {GradeService} from './common/services/grade.service'; +import {NumbasComponent} from './common/numbas-component/numbas-component.component'; + export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', 'doubtfire.sessions', @@ -440,6 +442,8 @@ DoubtfireAngularJSModule.directive( ); DoubtfireAngularJSModule.directive('fUnits', downgradeComponent({component: FUnitsComponent})); +DoubtfireAngularJSModule.directive('fNumbasComponent', downgradeComponent({component: NumbasComponent})); + // Global configuration DoubtfireAngularJSModule.directive( 'taskCommentsViewer', diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee index 9fea8fbad9..485843f780 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee @@ -128,6 +128,7 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) removed.push('group') if !isRFF || !task.isGroupTask() removed.push('alignment') if !isRFF || !task.unit.ilos.length > 0 removed.push('comments') if isTestSubmission + removed.push('numbas') if !task.definition.hasEnabledNumbasTest removed # Initialises the states initialise: -> From 097df7960d34a8c905ee495b8c6c8b8843e43b36 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:02:02 +1100 Subject: [PATCH 025/776] feat: add Numbas test attempt model --- src/app/api/models/test-attempt.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/app/api/models/test-attempt.ts diff --git a/src/app/api/models/test-attempt.ts b/src/app/api/models/test-attempt.ts new file mode 100644 index 0000000000..77e4af1724 --- /dev/null +++ b/src/app/api/models/test-attempt.ts @@ -0,0 +1,14 @@ +import { Entity } from "ngx-entity-service"; + +export class TestAttempt extends Entity { + id: number; + name: string; + attemptNumber: number; + passStatus: boolean; + examData: string; + completed: boolean; + cmiEntry: string; + examResult: string; + attemptedAt: Date; + associatedTaskId: number; +} From d47b8edc745660801984346c4fa67d6bb371f4cb Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:02:56 +1100 Subject: [PATCH 026/776] fix: integrate Numbas services well with the existing system --- src/app/api/services/numbas-lms.service.ts | 144 +++++++++--------- src/app/api/services/numbas.service.ts | 10 +- .../numbas-component.component.ts | 49 +++--- .../upload-submission-modal.tpl.html | 2 +- 4 files changed, 110 insertions(+), 95 deletions(-) diff --git a/src/app/api/services/numbas-lms.service.ts b/src/app/api/services/numbas-lms.service.ts index 268c158484..caf3ad8d6f 100644 --- a/src/app/api/services/numbas-lms.service.ts +++ b/src/app/api/services/numbas-lms.service.ts @@ -1,9 +1,10 @@ -import { Injectable } from '@angular/core'; +import { Injectable, Input } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { BehaviorSubject, Observable, throwError } from 'rxjs'; import { TaskService } from './task.service'; import { UserService } from './user.service'; import API_URL from 'src/app/config/constants/apiURL'; +import { Task } from '../models/task'; declare let pipwerks: any; @@ -12,7 +13,7 @@ declare let pipwerks: any; }) export class NumbasLmsService { - private readonly apiBaseUrl = `${API_URL}/savetests`;; + private readonly apiBaseUrl = `${API_URL}/test_attempts`; private defaultValues: { [key: string]: string } = { 'cmi.completion_status': 'not attempted', @@ -22,7 +23,6 @@ export class NumbasLmsService { 'cmi.mode': 'normal', 'cmi.undefinedlearner_response': '1', 'cmi.undefinedresult' : '0' - }; private testId: number = 0; @@ -41,12 +41,16 @@ export class NumbasLmsService { private http: HttpClient, private taskService: TaskService, private userService: UserService -) { + ) { pipwerks.SCORM.version = "2004"; console.log(`SCORM version is set to: ${pipwerks.SCORM.version}`); this.learnerId = this.userService.currentUser.studentId; } + setTask(task: Task) { + this.taskId = task.id; + } + getDefaultDataStore() { // Use spread operator to merge defaultValues into the dataStore return { @@ -63,40 +67,39 @@ export class NumbasLmsService { if (mode === 'review') { this.SetValue('cmi.mode', 'review'); - xhr.open("GET", `${this.apiBaseUrl}/completed-latest`, false); + xhr.open("GET", `${this.apiBaseUrl}/completed-latest?task_id=${this.taskId}`, false); xhr.send(); console.log(xhr.responseText); if (xhr.status !== 200) { - console.error('Error fetching latest completed test result:', xhr.statusText); - return 'false'; + console.error('Error fetching latest completed test result:', xhr.statusText); + return 'false'; } try { - const completedTest = JSON.parse(xhr.responseText); - const parsedExamData = JSON.parse(completedTest.data.exam_data || '{}'); + const completedTest = JSON.parse(xhr.responseText); + const parsedExamData = JSON.parse(completedTest.data.exam_data || '{}'); - // Set entire suspendData string to cmi.suspend_data - this.SetValue('cmi.suspend_data', JSON.stringify(parsedExamData)); + // Set entire suspendData string to cmi.suspend_data + this.SetValue('cmi.suspend_data', JSON.stringify(parsedExamData)); - // Use SetValue to set parsedExamData values to dataStore - Object.keys(parsedExamData).forEach(key => { - this.SetValue(key, parsedExamData[key]); - }); + // Use SetValue to set parsedExamData values to dataStore + Object.keys(parsedExamData).forEach(key => { + this.SetValue(key, parsedExamData[key]); + }); - this.SetValue('cmi.entry', 'RO'); - this.SetValue('cmi.mode', 'review'); - - console.log('Latest completed test data:', completedTest); - return 'true'; + this.SetValue('cmi.entry', 'RO'); + this.SetValue('cmi.mode', 'review'); + console.log('Latest completed test data:', completedTest); + return 'true'; } catch (error) { - console.error('Error:', error); - return 'false'; + console.error('Error:', error); + return 'false'; } } - xhr.open("GET", `${this.apiBaseUrl}/latest`, false); + xhr.open("GET", `${this.apiBaseUrl}/latest?task_id=${this.taskId}`, false); xhr.send(); console.log(xhr.responseText); @@ -107,36 +110,34 @@ export class NumbasLmsService { let latestTest; try { - latestTest = JSON.parse(xhr.responseText); - console.log('Latest test result:', latestTest); - this.testId = latestTest.data.id; - - if (latestTest.data['cmi_entry'] === 'ab-initio') { - console.log("starting new test"); - this.SetValue('cmi.learner_id', this.learnerId); - this.dataStore['name'] = examName; - this.dataStore['attempt_number'] = latestTest.data['attempt_number']; - console.log(this.dataStore); + latestTest = JSON.parse(xhr.responseText); + console.log('Latest test result:', latestTest); + this.testId = latestTest.data.id; + + if (latestTest.data['cmi_entry'] === 'ab-initio') { + console.log("starting new test"); + this.SetValue('cmi.learner_id', this.learnerId); + this.dataStore['name'] = examName; + this.dataStore['attempt_number'] = latestTest.data['attempt_number']; + console.log(this.dataStore); } else if (latestTest.data['cmi_entry'] === 'resume') { - console.log("resuming test"); - const parsedExamData = JSON.parse(latestTest.data.exam_data || '{}'); + console.log("resuming test"); + const parsedExamData = JSON.parse(latestTest.data.exam_data || '{}'); - this.dataStore = JSON.parse(JSON.stringify(parsedExamData)); + this.dataStore = JSON.parse(JSON.stringify(parsedExamData)); - console.log(this.dataStore); - } + console.log(this.dataStore); + } - this.initializationComplete$.next(true); + this.initializationComplete$.next(true); - console.log("finished initlizing"); - return 'true'; + console.log("finished initlizing"); + return 'true'; } catch (error) { - console.error('Error:', error); - return 'false'; + console.error('Error:', error); + return 'false'; } -} - - + } isTestCompleted(): boolean { return this.dataStore?.['completed'] || false; @@ -193,43 +194,44 @@ export class NumbasLmsService { } return 'true'; } -//function to save the state of the exam. -Commit(): string { - if (!this.initializationComplete$.getValue()) { + + //function to save the state of the exam. + Commit(): string { + if (!this.initializationComplete$.getValue()) { console.warn('Initialization not complete. Cannot commit.'); return 'false'; - } + } - // Set cmi.entry to 'resume' before committing dataStore - this.dataStore['cmi.entry'] = 'resume'; - if (!this.isTestCompleted()) { - this.dataStore['cmi.exit'] = 'suspend'; - } - console.log("Committing dataStore:", this.dataStore); + // Set cmi.entry to 'resume' before committing dataStore + this.dataStore['cmi.entry'] = 'resume'; + if (!this.isTestCompleted()) { + this.dataStore['cmi.exit'] = 'suspend'; + } + console.log("Committing dataStore:", this.dataStore); - // Directly stringify the dataStore - const jsonData = JSON.stringify(this.dataStore); + // Directly stringify the dataStore + const jsonData = JSON.stringify(this.dataStore); - // Use XHR to send the request - const xhr = new XMLHttpRequest(); - xhr.open('PUT', `${this.apiBaseUrl}/${this.testId}/suspend`, true); - xhr.setRequestHeader('Content-Type', 'application/json'); + // Use XHR to send the request + const xhr = new XMLHttpRequest(); + xhr.open('PUT', `${this.apiBaseUrl}/${this.testId}/exam_data`, true); + xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.onload = () => { + xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 400) { - console.log('Suspend data saved successfully.'); + console.log('Suspend data saved successfully.'); } else { - console.error('Error saving suspend data:', xhr.responseText); + console.error('Error saving suspend data:', xhr.responseText); } - }; + }; - xhr.onerror = () => { + xhr.onerror = () => { console.error('Request failed.'); - }; + }; - xhr.send(jsonData); - return 'true'; -} + xhr.send(jsonData); + return 'true'; + } // Placeholder methods for SCORM error handling GetLastError(): string { diff --git a/src/app/api/services/numbas.service.ts b/src/app/api/services/numbas.service.ts index 0fb41fdcc5..d9d4a2a8bb 100644 --- a/src/app/api/services/numbas.service.ts +++ b/src/app/api/services/numbas.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError, map, retry } from 'rxjs/operators'; import API_URL from 'src/app/config/constants/apiURL'; @@ -8,20 +8,18 @@ import API_URL from 'src/app/config/constants/apiURL'; providedIn: 'root' }) export class NumbasService { - private readonly API_URL = `${API_URL}/numbas_api`; - constructor(private http: HttpClient) {} /** * Fetches a specified resource for a given unit and task. * * @param unitId - The ID of the unit - * @param taskId - The ID of the task + * @param taskDefId - The ID of the task definition * @param resourcePath - Path to the desired resource * @returns An Observable with the Blob of the fetched resource */ - fetchResource(unitId: string, taskId: string, resourcePath: string): Observable { - const resourceUrl = `${this.API_URL}/${unitId}/${taskId}/${resourcePath}`; + fetchResource(unitId: number, taskDefId: number, resourcePath: string): Observable { + const resourceUrl = `${API_URL}/units/${unitId}/task_definitions/${taskDefId}/numbas_data/${resourcePath}`; const resourceMimeType = this.getMimeType(resourcePath); return this.http.get(resourceUrl, { responseType: 'blob' }).pipe( diff --git a/src/app/common/numbas-component/numbas-component.component.ts b/src/app/common/numbas-component/numbas-component.component.ts index 2e5cff929e..2c16411ea6 100644 --- a/src/app/common/numbas-component/numbas-component.component.ts +++ b/src/app/common/numbas-component/numbas-component.component.ts @@ -1,6 +1,8 @@ -import { Component, OnInit } from '@angular/core'; -import { NumbasService } from 'src/app/api/services/numbas.service'; +import { Component, Input, OnChanges } from '@angular/core'; +import { Task } from 'src/app/api/models/task'; +import { Unit } from 'src/app/api/models/unit'; import { NumbasLmsService } from 'src/app/api/services/numbas-lms.service'; +import { NumbasService } from 'src/app/api/services/numbas.service'; declare global { interface Window { API_1484_11: any; } @@ -10,36 +12,46 @@ declare global { selector: 'f-numbas-component', templateUrl: './numbas-component.component.html' }) -export class NumbasComponent implements OnInit { +export class NumbasComponent implements OnChanges { + @Input() task: Task; + unit: Unit; + currentMode: 'attempt' | 'review' = 'attempt'; + constructor( private numbasService: NumbasService, private lmsService: NumbasLmsService ) {} - ngOnInit(): void { - this.interceptIframeRequests(); + ngOnChanges(): void { + if (this.task) { + this.lmsService.setTask(this.task); + this.unit = this.task.unit; - window.API_1484_11 = { - Initialize: () => this.lmsService.Initialize(this.currentMode), - Terminate: () => this.lmsService.Terminate(), - GetValue: (element: string) => this.lmsService.GetValue(element), - SetValue: (element: string, value: string) => this.lmsService.SetValue(element, value), - Commit: () => this.lmsService.Commit(), - GetLastError: () => this.lmsService.GetLastError(), - GetErrorString: (errorCode: string) => this.lmsService.GetErrorString(errorCode), - GetDiagnostic: (errorCode: string) => this.lmsService.GetDiagnostic(errorCode) - }; + this.interceptIframeRequests(); + + window.API_1484_11 = { + Initialize: () => this.lmsService.Initialize(this.currentMode), + Terminate: () => this.lmsService.Terminate(), + GetValue: (element: string) => this.lmsService.GetValue(element), + SetValue: (element: string, value: string) => this.lmsService.SetValue(element, value), + Commit: () => this.lmsService.Commit(), + GetLastError: () => this.lmsService.GetLastError(), + GetErrorString: (errorCode: string) => this.lmsService.GetErrorString(errorCode), + GetDiagnostic: (errorCode: string) => this.lmsService.GetDiagnostic(errorCode) + }; + } } launchNumbasTest(mode: 'attempt' | 'review' = 'attempt'): void { this.currentMode = mode; const iframe = document.createElement('iframe'); - iframe.src = 'http://localhost:4201/api/numbas_api/index.html'; + iframe.src = 'http://localhost:4200/api/numbas_api/index.html'; iframe.style.width = '100%'; iframe.style.height = '800px'; document.body.appendChild(iframe); } + setReviewMode(): void { this.reviewTest(); } @@ -48,6 +60,7 @@ export class NumbasComponent implements OnInit { const iframe = document.getElementsByTagName('iframe')[0]; iframe?.parentNode?.removeChild(iframe); } + reviewTest(): void { this.launchNumbasTest('review'); } @@ -55,11 +68,13 @@ export class NumbasComponent implements OnInit { interceptIframeRequests(): void { const originalOpen = XMLHttpRequest.prototype.open; const numbasService = this.numbasService; + const unitId = this.unit.id; + const taskDefId = this.task.definition.id; XMLHttpRequest.prototype.open = function (this: XMLHttpRequest, method: string, url: string | URL, async: boolean = true, username?: string | null, password?: string | null) { if (typeof url === 'string' && url.startsWith('/api/numbas_api/')) { const resourcePath = url.replace('/api/numbas_api/', ''); this.abort(); - numbasService.fetchResource('1', '1', resourcePath).subscribe( + numbasService.fetchResource(unitId, taskDefId, resourcePath).subscribe( (resourceData) => { if (this.onload) { this.onload.call(this, resourceData); diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html index edd0fda105..d6636d914e 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html @@ -57,7 +57,7 @@

- +
From c9c2fbe10db156512946355f3cabfe54461f0eba Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 9 Apr 2024 10:44:09 +1000 Subject: [PATCH 027/776] fix: show Numbas button component and modify iframe request --- src/app/api/services/numbas-lms.service.ts | 4 -- .../numbas-component.component.ts | 69 +++++++------------ .../upload-submission-modal.tpl.html | 6 +- 3 files changed, 31 insertions(+), 48 deletions(-) diff --git a/src/app/api/services/numbas-lms.service.ts b/src/app/api/services/numbas-lms.service.ts index caf3ad8d6f..de5a25bcc9 100644 --- a/src/app/api/services/numbas-lms.service.ts +++ b/src/app/api/services/numbas-lms.service.ts @@ -6,8 +6,6 @@ import { UserService } from './user.service'; import API_URL from 'src/app/config/constants/apiURL'; import { Task } from '../models/task'; -declare let pipwerks: any; - @Injectable({ providedIn: 'root' }) @@ -42,8 +40,6 @@ export class NumbasLmsService { private taskService: TaskService, private userService: UserService ) { - pipwerks.SCORM.version = "2004"; - console.log(`SCORM version is set to: ${pipwerks.SCORM.version}`); this.learnerId = this.userService.currentUser.studentId; } diff --git a/src/app/common/numbas-component/numbas-component.component.ts b/src/app/common/numbas-component/numbas-component.component.ts index 2c16411ea6..1193adcc19 100644 --- a/src/app/common/numbas-component/numbas-component.component.ts +++ b/src/app/common/numbas-component/numbas-component.component.ts @@ -1,5 +1,6 @@ -import { Component, Input, OnChanges } from '@angular/core'; +import { Component, Input, OnInit, Renderer2 } from '@angular/core'; import { Task } from 'src/app/api/models/task'; +import { TaskDefinition } from 'src/app/api/models/task-definition'; import { Unit } from 'src/app/api/models/unit'; import { NumbasLmsService } from 'src/app/api/services/numbas-lms.service'; import { NumbasService } from 'src/app/api/services/numbas.service'; @@ -12,69 +13,51 @@ declare global { selector: 'f-numbas-component', templateUrl: './numbas-component.component.html' }) -export class NumbasComponent implements OnChanges { +export class NumbasComponent implements OnInit { @Input() task: Task; - unit: Unit; + @Input() unit: Unit; + @Input() taskDef: TaskDefinition; currentMode: 'attempt' | 'review' = 'attempt'; constructor( private numbasService: NumbasService, - private lmsService: NumbasLmsService - ) {} + private lmsService: NumbasLmsService, + private renderer: Renderer2 + ) { } - ngOnChanges(): void { - if (this.task) { - this.lmsService.setTask(this.task); - this.unit = this.task.unit; + ngOnInit(): void { + this.interceptIframeRequests(); - this.interceptIframeRequests(); - - window.API_1484_11 = { - Initialize: () => this.lmsService.Initialize(this.currentMode), - Terminate: () => this.lmsService.Terminate(), - GetValue: (element: string) => this.lmsService.GetValue(element), - SetValue: (element: string, value: string) => this.lmsService.SetValue(element, value), - Commit: () => this.lmsService.Commit(), - GetLastError: () => this.lmsService.GetLastError(), - GetErrorString: (errorCode: string) => this.lmsService.GetErrorString(errorCode), - GetDiagnostic: (errorCode: string) => this.lmsService.GetDiagnostic(errorCode) - }; - } + window.API_1484_11 = { + Initialize: () => this.lmsService.Initialize(this.currentMode), + Terminate: () => this.lmsService.Terminate(), + GetValue: (element: string) => this.lmsService.GetValue(element), + SetValue: (element: string, value: string) => this.lmsService.SetValue(element, value), + Commit: () => this.lmsService.Commit(), + GetLastError: () => this.lmsService.GetLastError(), + GetErrorString: (errorCode: string) => this.lmsService.GetErrorString(errorCode), + GetDiagnostic: (errorCode: string) => this.lmsService.GetDiagnostic(errorCode) + }; } launchNumbasTest(mode: 'attempt' | 'review' = 'attempt'): void { this.currentMode = mode; - const iframe = document.createElement('iframe'); - iframe.src = 'http://localhost:4200/api/numbas_api/index.html'; - iframe.style.width = '100%'; - iframe.style.height = '800px'; - document.body.appendChild(iframe); - } - - setReviewMode(): void { - this.reviewTest(); - } - - removeNumbasTest(): void { - const iframe = document.getElementsByTagName('iframe')[0]; - iframe?.parentNode?.removeChild(iframe); - } - - reviewTest(): void { - this.launchNumbasTest('review'); + const iframe = this.renderer.createElement('iframe'); + this.renderer.setAttribute(iframe, 'src', 'http://localhost:3000/api/numbas_api/units/1/task_definitions/1/numbas_data/index.html'); + this.renderer.setStyle(iframe, 'width', '100%'); + this.renderer.setStyle(iframe, 'height', '800px'); + this.renderer.appendChild(document.body, iframe); } interceptIframeRequests(): void { const originalOpen = XMLHttpRequest.prototype.open; const numbasService = this.numbasService; - const unitId = this.unit.id; - const taskDefId = this.task.definition.id; XMLHttpRequest.prototype.open = function (this: XMLHttpRequest, method: string, url: string | URL, async: boolean = true, username?: string | null, password?: string | null) { if (typeof url === 'string' && url.startsWith('/api/numbas_api/')) { const resourcePath = url.replace('/api/numbas_api/', ''); this.abort(); - numbasService.fetchResource(unitId, taskDefId, resourcePath).subscribe( + numbasService.fetchResource(1, 1, resourcePath).subscribe( (resourceData) => { if (this.onload) { this.onload.call(this, resourceData); diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html index d6636d914e..cd207a3637 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html @@ -57,7 +57,11 @@

- + +
From f53befd82b52bba24bc8c593a9266c11f9155d32 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 9 Apr 2024 12:23:02 +1000 Subject: [PATCH 028/776] fix: show Numbas iframe on top of other elements --- .../numbas-component.component.ts | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/app/common/numbas-component/numbas-component.component.ts b/src/app/common/numbas-component/numbas-component.component.ts index 1193adcc19..d2b8bea626 100644 --- a/src/app/common/numbas-component/numbas-component.component.ts +++ b/src/app/common/numbas-component/numbas-component.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit, Renderer2 } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { Task } from 'src/app/api/models/task'; import { TaskDefinition } from 'src/app/api/models/task-definition'; import { Unit } from 'src/app/api/models/unit'; @@ -23,13 +23,12 @@ export class NumbasComponent implements OnInit { constructor( private numbasService: NumbasService, private lmsService: NumbasLmsService, - private renderer: Renderer2 - ) { } + ) {} ngOnInit(): void { this.interceptIframeRequests(); - window.API_1484_11 = { + window.API_1484_11 = { Initialize: () => this.lmsService.Initialize(this.currentMode), Terminate: () => this.lmsService.Terminate(), GetValue: (element: string) => this.lmsService.GetValue(element), @@ -43,11 +42,20 @@ export class NumbasComponent implements OnInit { launchNumbasTest(mode: 'attempt' | 'review' = 'attempt'): void { this.currentMode = mode; - const iframe = this.renderer.createElement('iframe'); - this.renderer.setAttribute(iframe, 'src', 'http://localhost:3000/api/numbas_api/units/1/task_definitions/1/numbas_data/index.html'); - this.renderer.setStyle(iframe, 'width', '100%'); - this.renderer.setStyle(iframe, 'height', '800px'); - this.renderer.appendChild(document.body, iframe); + const iframe = document.createElement('iframe'); + iframe.src = 'http://example.org'; + iframe.style.position = 'fixed'; + iframe.style.top = '0'; + iframe.style.left = '0'; + iframe.style.width = '100%'; + iframe.style.height = '100%'; + iframe.style.zIndex = '9999'; // Set a high z-index value + + // Get the topmost element in the document + var topElement = document.documentElement.firstChild; + + // Replace the top element with the iframe + document.documentElement.replaceChild(iframe, topElement); } interceptIframeRequests(): void { From 3df59dcc58f021ef16d81938d1d05473818bc75e Mon Sep 17 00:00:00 2001 From: ublefo <90136978+ublefo@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:28:28 +1000 Subject: [PATCH 029/776] fix: update numbas api path --- src/app/api/services/numbas.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/services/numbas.service.ts b/src/app/api/services/numbas.service.ts index d9d4a2a8bb..6f2f750bf0 100644 --- a/src/app/api/services/numbas.service.ts +++ b/src/app/api/services/numbas.service.ts @@ -19,7 +19,7 @@ export class NumbasService { * @returns An Observable with the Blob of the fetched resource */ fetchResource(unitId: number, taskDefId: number, resourcePath: string): Observable { - const resourceUrl = `${API_URL}/units/${unitId}/task_definitions/${taskDefId}/numbas_data/${resourcePath}`; + const resourceUrl = `${API_URL}/numbas_api/${taskDefId}/numbas_data/${resourcePath}`; const resourceMimeType = this.getMimeType(resourcePath); return this.http.get(resourceUrl, { responseType: 'blob' }).pipe( From bee0a0bb1eb905c964caf7888419effe70553520 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Wed, 10 Apr 2024 11:58:40 +1000 Subject: [PATCH 030/776] fix: show correct Numbas test from the task def with all assets loaded --- src/app/api/services/numbas.service.ts | 5 ++- .../numbas-component.component.ts | 33 ++++++++++--------- .../upload-submission-modal.tpl.html | 6 +--- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/app/api/services/numbas.service.ts b/src/app/api/services/numbas.service.ts index 6f2f750bf0..f8812f1ac7 100644 --- a/src/app/api/services/numbas.service.ts +++ b/src/app/api/services/numbas.service.ts @@ -13,13 +13,12 @@ export class NumbasService { /** * Fetches a specified resource for a given unit and task. * - * @param unitId - The ID of the unit * @param taskDefId - The ID of the task definition * @param resourcePath - Path to the desired resource * @returns An Observable with the Blob of the fetched resource */ - fetchResource(unitId: number, taskDefId: number, resourcePath: string): Observable { - const resourceUrl = `${API_URL}/numbas_api/${taskDefId}/numbas_data/${resourcePath}`; + fetchResource(taskDefId: number, resourcePath: string): Observable { + const resourceUrl = `${API_URL}/numbas_api/${taskDefId}/${resourcePath}`; const resourceMimeType = this.getMimeType(resourcePath); return this.http.get(resourceUrl, { responseType: 'blob' }).pipe( diff --git a/src/app/common/numbas-component/numbas-component.component.ts b/src/app/common/numbas-component/numbas-component.component.ts index d2b8bea626..529a64b9ae 100644 --- a/src/app/common/numbas-component/numbas-component.component.ts +++ b/src/app/common/numbas-component/numbas-component.component.ts @@ -1,7 +1,5 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { Task } from 'src/app/api/models/task'; -import { TaskDefinition } from 'src/app/api/models/task-definition'; -import { Unit } from 'src/app/api/models/unit'; import { NumbasLmsService } from 'src/app/api/services/numbas-lms.service'; import { NumbasService } from 'src/app/api/services/numbas.service'; @@ -11,12 +9,11 @@ declare global { @Component({ selector: 'f-numbas-component', - templateUrl: './numbas-component.component.html' + templateUrl: './numbas-component.component.html', + styleUrls: ['numbas-component.component.scss'], }) -export class NumbasComponent implements OnInit { +export class NumbasComponent implements OnInit, OnChanges { @Input() task: Task; - @Input() unit: Unit; - @Input() taskDef: TaskDefinition; currentMode: 'attempt' | 'review' = 'attempt'; @@ -40,32 +37,38 @@ export class NumbasComponent implements OnInit { }; } + ngOnChanges(changes: SimpleChanges): void { + if (changes.task) { + this.task = changes.task.currentValue; + this.lmsService.setTask(this.task); + } + } + launchNumbasTest(mode: 'attempt' | 'review' = 'attempt'): void { this.currentMode = mode; + const iframe = document.createElement('iframe'); - iframe.src = 'http://example.org'; + iframe.src = `http://localhost:3000/api/numbas_api/${this.task.taskDefId}/index.html`; + iframe.style.position = 'fixed'; iframe.style.top = '0'; iframe.style.left = '0'; iframe.style.width = '100%'; iframe.style.height = '100%'; - iframe.style.zIndex = '9999'; // Set a high z-index value - - // Get the topmost element in the document - var topElement = document.documentElement.firstChild; + iframe.style.zIndex = '9999'; - // Replace the top element with the iframe - document.documentElement.replaceChild(iframe, topElement); + document.body.appendChild(iframe); } interceptIframeRequests(): void { const originalOpen = XMLHttpRequest.prototype.open; const numbasService = this.numbasService; + const taskDefId = this.task.taskDefId; XMLHttpRequest.prototype.open = function (this: XMLHttpRequest, method: string, url: string | URL, async: boolean = true, username?: string | null, password?: string | null) { if (typeof url === 'string' && url.startsWith('/api/numbas_api/')) { const resourcePath = url.replace('/api/numbas_api/', ''); this.abort(); - numbasService.fetchResource(1, 1, resourcePath).subscribe( + numbasService.fetchResource(taskDefId, resourcePath).subscribe( (resourceData) => { if (this.onload) { this.onload.call(this, resourceData); diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html index cd207a3637..0b99e7eb8f 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html @@ -57,11 +57,7 @@

- - +
From 64b1bfb2918993e58a6659948d9621fc7d0b8ba4 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Sat, 13 Apr 2024 21:56:47 +1000 Subject: [PATCH 031/776] fix: use modal for Numbas and enable authentication --- src/app/ajs-upgraded-providers.ts | 7 ++ src/app/api/services/numbas.service.ts | 55 -------------- .../api/services/spec/numbas.service.spec.ts | 66 ---------------- .../numbas-component.component.html | 7 +- .../numbas-component.component.scss | 17 +++++ .../numbas-component.component.ts | 76 ++++++------------- .../numbas-modal.component.ts | 20 +++++ src/app/doubtfire-angular.module.ts | 4 +- src/app/doubtfire-angularjs.module.ts | 2 + .../upload-submission-modal.coffee | 5 +- .../upload-submission-modal.tpl.html | 6 +- 11 files changed, 83 insertions(+), 182 deletions(-) delete mode 100644 src/app/api/services/numbas.service.ts delete mode 100644 src/app/api/services/spec/numbas.service.spec.ts create mode 100644 src/app/common/numbas-component/numbas-modal.component.ts diff --git a/src/app/ajs-upgraded-providers.ts b/src/app/ajs-upgraded-providers.ts index 795869225d..9d5b0f0203 100644 --- a/src/app/ajs-upgraded-providers.ts +++ b/src/app/ajs-upgraded-providers.ts @@ -18,6 +18,7 @@ export const rootScope = new InjectionToken('$rootScope'); export const calendarModal = new InjectionToken('CalendarModal'); export const aboutDoubtfireModal = new InjectionToken('AboutDoubtfireModal'); export const plagiarismReportModal = new InjectionToken('PlagiarismReportModal'); +export const numbasModal = new InjectionToken('NumbasModal'); // Define a provider for the above injection token... // It will get the service from AngularJS via the factory @@ -116,3 +117,9 @@ export const UnitStudentEnrolmentModalProvider = { useFactory: (i) => i.get('UnitStudentEnrolmentModal'), deps: ['$injector'], }; + +export const numbasModalProvider = { + provide: numbasModal, + useFactory: (i) => i.get('NumbasModal'), + deps: ['$injector'], +}; \ No newline at end of file diff --git a/src/app/api/services/numbas.service.ts b/src/app/api/services/numbas.service.ts deleted file mode 100644 index f8812f1ac7..0000000000 --- a/src/app/api/services/numbas.service.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpClient, HttpErrorResponse } from '@angular/common/http'; -import { Observable, throwError } from 'rxjs'; -import { catchError, map, retry } from 'rxjs/operators'; -import API_URL from 'src/app/config/constants/apiURL'; - -@Injectable({ - providedIn: 'root' -}) -export class NumbasService { - constructor(private http: HttpClient) {} - - /** - * Fetches a specified resource for a given unit and task. - * - * @param taskDefId - The ID of the task definition - * @param resourcePath - Path to the desired resource - * @returns An Observable with the Blob of the fetched resource - */ - fetchResource(taskDefId: number, resourcePath: string): Observable { - const resourceUrl = `${API_URL}/numbas_api/${taskDefId}/${resourcePath}`; - const resourceMimeType = this.getMimeType(resourcePath); - - return this.http.get(resourceUrl, { responseType: 'blob' }).pipe( - retry(3), - map((blob) => new Blob([blob], { type: resourceMimeType })), - catchError((error: HttpErrorResponse) => { - console.error('Error fetching Numbas resource:', error); - return throwError('Error fetching Numbas resource.'); - }) - ); - } - - /** - * Determines the MIME type of a resource based on its extension. - * - * @param resourcePath - Path of the resource - * @returns MIME type string corresponding to the resource's extension - */ - getMimeType(resourcePath: string): string { - const extension = resourcePath.split('.').pop()?.toLowerCase(); - const mimeTypeMap: { [key: string]: string } = { - 'html': 'text/html', - 'css': 'text/css', - 'js': 'application/javascript', - 'json': 'application/json', - 'png': 'image/png', - 'jpg': 'image/jpeg', - 'gif': 'image/gif', - 'svg': 'image/svg+xml' - }; - - return mimeTypeMap[extension || ''] || 'text/plain'; - } -} diff --git a/src/app/api/services/spec/numbas.service.spec.ts b/src/app/api/services/spec/numbas.service.spec.ts deleted file mode 100644 index a29b13e149..0000000000 --- a/src/app/api/services/spec/numbas.service.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { NumbasService } from '../numbas.service'; -import { HttpRequest } from '@angular/common/http'; - - -describe('NumbasService', () => { - let numbasService: NumbasService; - let httpMock: HttpTestingController; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [NumbasService], - }); - - numbasService = TestBed.inject(NumbasService); - httpMock = TestBed.inject(HttpTestingController); - }); - - afterEach(() => { - httpMock.verify(); - }); - - it('should fetch resource as expected', fakeAsync(() => { - const dummyBlob = new Blob(['dummy blob'], { type: 'text/html' }); - - const unitId = 'sampleUnitId'; - const taskId = 'sampleTaskId'; - const resourcePath = 'sampleResource.html'; - - numbasService.fetchResource(unitId, taskId, resourcePath).subscribe((blob) => { - expect(blob.size).toBe(dummyBlob.size); - expect(blob.type).toBe(dummyBlob.type); - }); - - const req = httpMock.expectOne(`http://localhost:3000/api/numbas_api/${unitId}/${taskId}/${resourcePath}`); - expect(req.request.method).toBe('GET'); - - req.flush(dummyBlob); - - tick(); - })); - - it('should upload test as expected', fakeAsync(() => { - const dummyResponse = { success: true, message: 'File uploaded successfully' }; - - const unitId = 'sampleUnitId'; - const taskId = 'sampleTaskId'; - const file = new File(['dummy content'], 'sample.txt', { type: 'text/plain' }); - - numbasService.uploadTest(unitId, taskId, file).subscribe((response) => { - expect(response).toEqual(dummyResponse); - }); - - const req = httpMock.expectOne(`http://localhost:3000/api/numbas_api/uploadNumbasTest`); - expect(req.request.method).toBe('POST'); - - req.flush(dummyResponse); - - tick(); - })); - -}); - - diff --git a/src/app/common/numbas-component/numbas-component.component.html b/src/app/common/numbas-component/numbas-component.component.html index dba53ea116..d562f02239 100644 --- a/src/app/common/numbas-component/numbas-component.component.html +++ b/src/app/common/numbas-component/numbas-component.component.html @@ -1,3 +1,4 @@ - +
+ + +
\ No newline at end of file diff --git a/src/app/common/numbas-component/numbas-component.component.scss b/src/app/common/numbas-component/numbas-component.component.scss index e69de29bb2..5e24cc5d9e 100644 --- a/src/app/common/numbas-component/numbas-component.component.scss +++ b/src/app/common/numbas-component/numbas-component.component.scss @@ -0,0 +1,17 @@ +.mat-dialog-content { + position: relative; +} + +iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 95%; +} + +button { + position: absolute; + bottom: 20px; + right: 20px; +} \ No newline at end of file diff --git a/src/app/common/numbas-component/numbas-component.component.ts b/src/app/common/numbas-component/numbas-component.component.ts index 529a64b9ae..6c8369ab77 100644 --- a/src/app/common/numbas-component/numbas-component.component.ts +++ b/src/app/common/numbas-component/numbas-component.component.ts @@ -1,7 +1,11 @@ -import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; +import { Component, OnInit, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { Task } from 'src/app/api/models/task'; import { NumbasLmsService } from 'src/app/api/services/numbas-lms.service'; -import { NumbasService } from 'src/app/api/services/numbas.service'; +import { UserService } from 'src/app/api/services/user.service'; +import { AppInjector } from 'src/app/app-injector'; +import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; declare global { interface Window { API_1484_11: any; } @@ -10,20 +14,29 @@ declare global { @Component({ selector: 'f-numbas-component', templateUrl: './numbas-component.component.html', - styleUrls: ['numbas-component.component.scss'], + styleUrls: ['./numbas-component.component.scss'], }) -export class NumbasComponent implements OnInit, OnChanges { - @Input() task: Task; - +export class NumbasComponent implements OnInit { + task: Task; currentMode: 'attempt' | 'review' = 'attempt'; + iframeSrc: SafeResourceUrl; constructor( - private numbasService: NumbasService, + private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { task: Task, mode: 'attempt' | 'review' }, private lmsService: NumbasLmsService, + private userService: UserService, + private sanitizer: DomSanitizer ) {} ngOnInit(): void { - this.interceptIframeRequests(); + this.task = this.data.task; + this.lmsService.setTask(this.task); + + this.currentMode = this.data.mode; + + const user = this.userService.currentUser; + this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(`${AppInjector.get(DoubtfireConstants).API_URL}/numbas_api/${this.task.taskDefId}/${user.authenticationToken}/${user.username}/index.html`); window.API_1484_11 = { Initialize: () => this.lmsService.Initialize(this.currentMode), @@ -37,50 +50,7 @@ export class NumbasComponent implements OnInit, OnChanges { }; } - ngOnChanges(changes: SimpleChanges): void { - if (changes.task) { - this.task = changes.task.currentValue; - this.lmsService.setTask(this.task); - } - } - - launchNumbasTest(mode: 'attempt' | 'review' = 'attempt'): void { - this.currentMode = mode; - - const iframe = document.createElement('iframe'); - iframe.src = `http://localhost:3000/api/numbas_api/${this.task.taskDefId}/index.html`; - - iframe.style.position = 'fixed'; - iframe.style.top = '0'; - iframe.style.left = '0'; - iframe.style.width = '100%'; - iframe.style.height = '100%'; - iframe.style.zIndex = '9999'; - - document.body.appendChild(iframe); - } - - interceptIframeRequests(): void { - const originalOpen = XMLHttpRequest.prototype.open; - const numbasService = this.numbasService; - const taskDefId = this.task.taskDefId; - XMLHttpRequest.prototype.open = function (this: XMLHttpRequest, method: string, url: string | URL, async: boolean = true, username?: string | null, password?: string | null) { - if (typeof url === 'string' && url.startsWith('/api/numbas_api/')) { - const resourcePath = url.replace('/api/numbas_api/', ''); - this.abort(); - numbasService.fetchResource(taskDefId, resourcePath).subscribe( - (resourceData) => { - if (this.onload) { - this.onload.call(this, resourceData); - } - }, - (error) => { - console.error('Error fetching Numbas resource:', error); - } - ); - } else { - originalOpen.call(this, method, url, async, username, password); - } - }; + removeNumbasTest(): void { + this.dialogRef.close(); } } diff --git a/src/app/common/numbas-component/numbas-modal.component.ts b/src/app/common/numbas-component/numbas-modal.component.ts new file mode 100644 index 0000000000..73b2122c17 --- /dev/null +++ b/src/app/common/numbas-component/numbas-modal.component.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { NumbasComponent } from './numbas-component.component'; +import { Task } from 'src/app/api/models/task'; + +@Injectable({ + providedIn: 'root', +}) +export class NumbasModal { + constructor(public dialog: MatDialog) { } + + public show(task: Task, mode: 'attempt' | 'review'): void { + let dialogRef: MatDialogRef; + + dialogRef = this.dialog.open(NumbasComponent, { + data: { task, mode }, + width: '95%', height: '90%' + }); + } +} diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index d15c23b234..48f54027d1 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -226,7 +226,7 @@ import {TasksViewerComponent} from './units/states/tasks/tasks-viewer/tasks-view import {UnitCodeComponent} from './common/unit-code/unit-code.component'; import {GradeService} from './common/services/grade.service'; import {NumbasComponent} from './common/numbas-component/numbas-component.component'; -import {NumbasService} from './api/services/numbas.service'; +import {NumbasModal} from './common/numbas-component/numbas-modal.component'; import {NumbasLmsService} from './api/services/numbas-lms.service'; @NgModule({ @@ -402,7 +402,7 @@ import {NumbasLmsService} from './api/services/numbas-lms.service'; TasksForInboxSearchPipe, IsActiveUnitRole, CreateNewUnitModal, - NumbasService, + NumbasModal, NumbasLmsService, provideLottieOptions({ player: () => player, diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 317094dc2e..b88fbeb298 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -226,6 +226,7 @@ import {MarkedPipe} from './common/pipes/marked.pipe'; import {AlertService} from './common/services/alert.service'; import {GradeService} from './common/services/grade.service'; import {NumbasComponent} from './common/numbas-component/numbas-component.component'; +import {NumbasModal} from './common/numbas-component/numbas-modal.component'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -308,6 +309,7 @@ DoubtfireAngularJSModule.factory( downgradeInjectable(EditProfileDialogService), ); DoubtfireAngularJSModule.factory('CreateNewUnitModal', downgradeInjectable(CreateNewUnitModal)); +DoubtfireAngularJSModule.factory('NumbasModal', downgradeInjectable(NumbasModal)); // directive -> component DoubtfireAngularJSModule.directive( diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee index 485843f780..68d742362e 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee @@ -32,7 +32,7 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) UploadSubmissionModal ) -.controller('UploadSubmissionModalCtrl', ($scope, $rootScope, $timeout, $modalInstance, newTaskService, newProjectService, task, reuploadEvidence, outcomeService, PrivacyPolicy) -> +.controller('UploadSubmissionModalCtrl', ($scope, $rootScope, $timeout, $modalInstance, NumbasModal, newTaskService, newProjectService, task, reuploadEvidence, outcomeService, PrivacyPolicy) -> $scope.privacyPolicy = PrivacyPolicy # Expose task to scope $scope.task = task @@ -155,6 +155,9 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) previous: states.previous } + $scope.launchNumbasDialog = -> + NumbasModal.show $scope.task, 'attempt' + # Whether or not we should disable this button $scope.shouldDisableBtn = { next: -> diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html index 0b99e7eb8f..0527aadd40 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html @@ -52,12 +52,14 @@

Attempt Numbas Test

- + Complete the Numbas test first to proceed to upload evidence of your task completion.
- +
From bcaa8af150aaf68b5cf5fb07ad9cb72037212d5d Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Fri, 26 Apr 2024 22:10:46 +1000 Subject: [PATCH 032/776] fix: add accepted Numbas file types --- .../task-definition-numbas.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts index 6588453430..f6687a4af9 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts @@ -41,7 +41,9 @@ export class TaskDefinitionNumbasComponent { } public uploadNumbasTest(files: FileList) { - const validFiles = Array.from(files as ArrayLike).filter((f) => f.type === 'application/zip'); + console.log(Array.from(files).map(f => f.type)); + const validMimeTypes = ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip']; + const validFiles = Array.from(files as ArrayLike).filter(f => validMimeTypes.includes(f.type)); if (validFiles.length > 0) { const file = validFiles[0]; this.taskDefinitionService.uploadNumbasData(this.taskDefinition, file).subscribe({ From 5d0606c56eaeb929d5873cae7038ce729581512c Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 30 Apr 2024 11:30:07 +1000 Subject: [PATCH 033/776] fix: initialise SCORM API wrapper before iframe loads --- .../numbas-component/numbas-component.component.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/app/common/numbas-component/numbas-component.component.ts b/src/app/common/numbas-component/numbas-component.component.ts index 6c8369ab77..488c0a6333 100644 --- a/src/app/common/numbas-component/numbas-component.component.ts +++ b/src/app/common/numbas-component/numbas-component.component.ts @@ -3,7 +3,6 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { Task } from 'src/app/api/models/task'; import { NumbasLmsService } from 'src/app/api/services/numbas-lms.service'; -import { UserService } from 'src/app/api/services/user.service'; import { AppInjector } from 'src/app/app-injector'; import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; @@ -25,7 +24,6 @@ export class NumbasComponent implements OnInit { private dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: { task: Task, mode: 'attempt' | 'review' }, private lmsService: NumbasLmsService, - private userService: UserService, private sanitizer: DomSanitizer ) {} @@ -33,11 +31,6 @@ export class NumbasComponent implements OnInit { this.task = this.data.task; this.lmsService.setTask(this.task); - this.currentMode = this.data.mode; - - const user = this.userService.currentUser; - this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(`${AppInjector.get(DoubtfireConstants).API_URL}/numbas_api/${this.task.taskDefId}/${user.authenticationToken}/${user.username}/index.html`); - window.API_1484_11 = { Initialize: () => this.lmsService.Initialize(this.currentMode), Terminate: () => this.lmsService.Terminate(), @@ -48,6 +41,10 @@ export class NumbasComponent implements OnInit { GetErrorString: (errorCode: string) => this.lmsService.GetErrorString(errorCode), GetDiagnostic: (errorCode: string) => this.lmsService.GetDiagnostic(errorCode) }; + + this.currentMode = this.data.mode; + + this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(`${AppInjector.get(DoubtfireConstants).API_URL}/numbas_api/${this.task.taskDefId}/index.html`); } removeNumbasTest(): void { From 2810ce65d423a137600d621a98bf27dbc221921c Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 30 Apr 2024 11:32:29 +1000 Subject: [PATCH 034/776] fix: retrieve test attempt data correctly --- src/app/api/services/numbas-lms.service.ts | 29 ++++++++-------------- 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/app/api/services/numbas-lms.service.ts b/src/app/api/services/numbas-lms.service.ts index de5a25bcc9..a24c8d31ed 100644 --- a/src/app/api/services/numbas-lms.service.ts +++ b/src/app/api/services/numbas-lms.service.ts @@ -1,7 +1,5 @@ -import { Injectable, Input } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { BehaviorSubject, Observable, throwError } from 'rxjs'; -import { TaskService } from './task.service'; +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; import { UserService } from './user.service'; import API_URL from 'src/app/config/constants/apiURL'; import { Task } from '../models/task'; @@ -10,7 +8,6 @@ import { Task } from '../models/task'; providedIn: 'root' }) export class NumbasLmsService { - private readonly apiBaseUrl = `${API_URL}/test_attempts`; private defaultValues: { [key: string]: string } = { @@ -35,11 +32,7 @@ export class NumbasLmsService { dataStore: { [key: string]: any } = this.getDefaultDataStore(); - constructor( - private http: HttpClient, - private taskService: TaskService, - private userService: UserService - ) { + constructor(private userService: UserService) { this.learnerId = this.userService.currentUser.studentId; } @@ -74,7 +67,7 @@ export class NumbasLmsService { try { const completedTest = JSON.parse(xhr.responseText); - const parsedExamData = JSON.parse(completedTest.data.exam_data || '{}'); + const parsedExamData = JSON.parse(completedTest.exam_data || '{}'); // Set entire suspendData string to cmi.suspend_data this.SetValue('cmi.suspend_data', JSON.stringify(parsedExamData)); @@ -108,17 +101,17 @@ export class NumbasLmsService { try { latestTest = JSON.parse(xhr.responseText); console.log('Latest test result:', latestTest); - this.testId = latestTest.data.id; + this.testId = latestTest.id; - if (latestTest.data['cmi_entry'] === 'ab-initio') { + if (latestTest['cmi_entry'] === 'ab-initio') { console.log("starting new test"); this.SetValue('cmi.learner_id', this.learnerId); this.dataStore['name'] = examName; - this.dataStore['attempt_number'] = latestTest.data['attempt_number']; + this.dataStore['attempt_number'] = latestTest['attempt_number']; console.log(this.dataStore); - } else if (latestTest.data['cmi_entry'] === 'resume') { + } else if (latestTest['cmi_entry'] === 'resume') { console.log("resuming test"); - const parsedExamData = JSON.parse(latestTest.data.exam_data || '{}'); + const parsedExamData = JSON.parse(latestTest.exam_data || '{}'); this.dataStore = JSON.parse(JSON.stringify(parsedExamData)); @@ -127,7 +120,7 @@ export class NumbasLmsService { this.initializationComplete$.next(true); - console.log("finished initlizing"); + console.log("finished initializing"); return 'true'; } catch (error) { console.error('Error:', error); @@ -153,7 +146,7 @@ export class NumbasLmsService { this.SetValue('cmi.entry', 'RO'); const cmientry = this.GetValue('cmi.entry'); const data = { - task_id: this.taskId, + id: this.taskId, name: ExamName, attempt_number: currentAttemptNumber, pass_status: status === 'passed', From e46214dd59d86014bb04d1c31f3a3bf21240e32b Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 30 Apr 2024 13:02:50 +1000 Subject: [PATCH 035/776] fix: send task id with numbas completed attempt data --- src/app/api/services/numbas-lms.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/api/services/numbas-lms.service.ts b/src/app/api/services/numbas-lms.service.ts index a24c8d31ed..40ce0942b3 100644 --- a/src/app/api/services/numbas-lms.service.ts +++ b/src/app/api/services/numbas-lms.service.ts @@ -69,7 +69,7 @@ export class NumbasLmsService { const completedTest = JSON.parse(xhr.responseText); const parsedExamData = JSON.parse(completedTest.exam_data || '{}'); - // Set entire suspendData string to cmi.suspend_data + // Set entire parsedExamData string to cmi.suspend_data this.SetValue('cmi.suspend_data', JSON.stringify(parsedExamData)); // Use SetValue to set parsedExamData values to dataStore @@ -146,14 +146,14 @@ export class NumbasLmsService { this.SetValue('cmi.entry', 'RO'); const cmientry = this.GetValue('cmi.entry'); const data = { - id: this.taskId, name: ExamName, attempt_number: currentAttemptNumber, pass_status: status === 'passed', exam_data: JSON.stringify(this.dataStore), completed: true, exam_result: examResult, - cmi_entry: cmientry + cmi_entry: cmientry, + task_id: this.taskId }; const xhr = new XMLHttpRequest(); From 84646dcf7b28c65c8c28f046197d08d3325795be Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Sun, 5 May 2024 16:33:19 +1000 Subject: [PATCH 036/776] refactor: modify numbas files to match PoC --- src/app/api/services/numbas-lms.service.ts | 38 +++++++++---------- .../services/spec/numbas-lms.service.spec.ts | 2 +- .../numbas-component.component.ts | 2 + 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/app/api/services/numbas-lms.service.ts b/src/app/api/services/numbas-lms.service.ts index 40ce0942b3..6479a6ed93 100644 --- a/src/app/api/services/numbas-lms.service.ts +++ b/src/app/api/services/numbas-lms.service.ts @@ -17,7 +17,7 @@ export class NumbasLmsService { 'numbas.duration_extension.units': 'seconds', 'cmi.mode': 'normal', 'cmi.undefinedlearner_response': '1', - 'cmi.undefinedresult' : '0' + 'cmi.undefinedresult': '0' }; private testId: number = 0; @@ -67,14 +67,14 @@ export class NumbasLmsService { try { const completedTest = JSON.parse(xhr.responseText); - const parsedExamData = JSON.parse(completedTest.exam_data || '{}'); + let parsedSuspendData = JSON.parse(completedTest.data.suspend_data || '{}'); - // Set entire parsedExamData string to cmi.suspend_data - this.SetValue('cmi.suspend_data', JSON.stringify(parsedExamData)); + // Set entire suspendData string to cmi.suspend_data + this.SetValue('cmi.suspend_data', JSON.stringify(parsedSuspendData)); - // Use SetValue to set parsedExamData values to dataStore - Object.keys(parsedExamData).forEach(key => { - this.SetValue(key, parsedExamData[key]); + // Use SetValue to set parsedSuspendData values to dataStore + Object.keys(parsedSuspendData).forEach(key => { + this.SetValue(key, parsedSuspendData[key]); }); this.SetValue('cmi.entry', 'RO'); @@ -101,19 +101,19 @@ export class NumbasLmsService { try { latestTest = JSON.parse(xhr.responseText); console.log('Latest test result:', latestTest); - this.testId = latestTest.id; + this.testId = latestTest.data.id; - if (latestTest['cmi_entry'] === 'ab-initio') { + if (latestTest.data['cmi_entry'] === 'ab-initio') { console.log("starting new test"); this.SetValue('cmi.learner_id', this.learnerId); this.dataStore['name'] = examName; - this.dataStore['attempt_number'] = latestTest['attempt_number']; + this.dataStore['attempt_number'] = latestTest.data['attempt_number']; console.log(this.dataStore); - } else if (latestTest['cmi_entry'] === 'resume') { + } else if (latestTest.data['cmi_entry'] === 'resume') { console.log("resuming test"); - const parsedExamData = JSON.parse(latestTest.exam_data || '{}'); + let parsedSuspendData = JSON.parse(latestTest.data.suspend_data || '{}'); - this.dataStore = JSON.parse(JSON.stringify(parsedExamData)); + this.dataStore = JSON.parse(JSON.stringify(parsedSuspendData)); console.log(this.dataStore); } @@ -149,7 +149,7 @@ export class NumbasLmsService { name: ExamName, attempt_number: currentAttemptNumber, pass_status: status === 'passed', - exam_data: JSON.stringify(this.dataStore), + suspend_data: JSON.stringify(this.dataStore), completed: true, exam_result: examResult, cmi_entry: cmientry, @@ -184,7 +184,7 @@ export class NumbasLmsService { return 'true'; } - //function to save the state of the exam. + // Saves the state of the exam. Commit(): string { if (!this.initializationComplete$.getValue()) { console.warn('Initialization not complete. Cannot commit.'); @@ -198,12 +198,9 @@ export class NumbasLmsService { } console.log("Committing dataStore:", this.dataStore); - // Directly stringify the dataStore - const jsonData = JSON.stringify(this.dataStore); - // Use XHR to send the request const xhr = new XMLHttpRequest(); - xhr.open('PUT', `${this.apiBaseUrl}/${this.testId}/exam_data`, true); + xhr.open('PUT', `${this.apiBaseUrl}/${this.testId}/suspend`, true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onload = () => { @@ -218,7 +215,8 @@ export class NumbasLmsService { console.error('Request failed.'); }; - xhr.send(jsonData); + const requestData = { suspend_data: this.dataStore }; + xhr.send(JSON.stringify(requestData)); return 'true'; } diff --git a/src/app/api/services/spec/numbas-lms.service.spec.ts b/src/app/api/services/spec/numbas-lms.service.spec.ts index f456d5a894..7d29ef75e0 100644 --- a/src/app/api/services/spec/numbas-lms.service.spec.ts +++ b/src/app/api/services/spec/numbas-lms.service.spec.ts @@ -55,7 +55,7 @@ describe('NumbasLmsService', () => { it('should handle review mode and get latest completed test result', () => { const mockResponse = { data: { - exam_data: JSON.stringify({ someData: 'value' }) + suspend_data: JSON.stringify({ someData: 'value' }) } }; diff --git a/src/app/common/numbas-component/numbas-component.component.ts b/src/app/common/numbas-component/numbas-component.component.ts index 488c0a6333..f5453e0dcc 100644 --- a/src/app/common/numbas-component/numbas-component.component.ts +++ b/src/app/common/numbas-component/numbas-component.component.ts @@ -48,6 +48,8 @@ export class NumbasComponent implements OnInit { } removeNumbasTest(): void { + const iframe = document.getElementsByTagName('iframe')[0]; + iframe?.parentNode?.removeChild(iframe); this.dialogRef.close(); } } From 48a31da1442f1e14f497c614053d9002c9f2631b Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 7 May 2024 03:00:27 +1000 Subject: [PATCH 037/776] feat: display numbas task comments --- src/app/api/models/task.ts | 6 +++ src/app/doubtfire-angular.module.ts | 2 + .../numbas-comment.component.html | 10 ++++ .../numbas-comment.component.scss | 51 +++++++++++++++++++ .../numbas-comment.component.ts | 21 ++++++++ .../task-comments-viewer.component.html | 8 +++ .../task-comments-viewer.component.scss | 2 +- .../task-comments-viewer.component.ts | 6 ++- 8 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.html create mode 100644 src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.scss create mode 100644 src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.ts diff --git a/src/app/api/models/task.ts b/src/app/api/models/task.ts index 2aa167adfd..403165cfee 100644 --- a/src/app/api/models/task.ts +++ b/src/app/api/models/task.ts @@ -507,6 +507,12 @@ export class Task extends Entity { ); } + public get numbasEnabled(): boolean { + return ( + this.definition.hasEnabledNumbasTest && this.definition.hasNumbasData + ); + } + public submissionUrl(asAttachment: boolean = false): string { return `${AppInjector.get(DoubtfireConstants).API_URL}/projects/${ this.project.id diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 48f54027d1..c634965f6f 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -228,6 +228,7 @@ import {GradeService} from './common/services/grade.service'; import {NumbasComponent} from './common/numbas-component/numbas-component.component'; import {NumbasModal} from './common/numbas-component/numbas-modal.component'; import {NumbasLmsService} from './api/services/numbas-lms.service'; +import {NumbasCommentComponent} from './tasks/task-comments-viewer/numbas-comment/numbas-comment.component'; @NgModule({ // Components we declare @@ -331,6 +332,7 @@ import {NumbasLmsService} from './api/services/numbas-lms.service'; FTaskBadgeComponent, FUnitsComponent, NumbasComponent, + NumbasCommentComponent, ], // Services we provide providers: [ diff --git a/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.html b/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.html new file mode 100644 index 0000000000..d87863bee8 --- /dev/null +++ b/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.html @@ -0,0 +1,10 @@ +
+
+
+
+
+ +
+
+
+
diff --git a/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.scss b/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.scss new file mode 100644 index 0000000000..31df73023a --- /dev/null +++ b/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.scss @@ -0,0 +1,51 @@ +div { + width: 100%; +} + +p { + color: #2c2c2c; + text-align: center; +} + +hr { + width: 100%; +} + +.hr-fade { + background: linear-gradient(to right, transparent, #9696969d, transparent); + width: 100%; + margin-top: 1px; +} + +.hr-text { + margin: 0; + line-height: 1em; + position: relative; + outline: 0; + border: 0; + color: black; + text-align: center; + height: 1.5em; + opacity: 0.8; + &:before { + content: ""; + background: linear-gradient(to right, transparent, #9696969d, transparent); + position: absolute; + left: 0; + top: 50%; + width: 100%; + height: 1px; + } + &:after { + content: attr(data-content); + position: relative; + display: inline-block; + color: black; + + padding: 0 0.5em; + line-height: 1.5em; + + color: #9696969d; + background-color: #fff; + } +} diff --git a/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.ts b/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.ts new file mode 100644 index 0000000000..eed512fe19 --- /dev/null +++ b/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.ts @@ -0,0 +1,21 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Task, TaskComment } from 'src/app/api/models/doubtfire-model'; +import { NumbasModal } from 'src/app/common/numbas-component/numbas-modal.component'; + +@Component({ + selector: 'numbas-comment', + templateUrl: './numbas-comment.component.html', + styleUrls: ['./numbas-comment.component.scss'], +}) +export class NumbasCommentComponent implements OnInit { + @Input() task: Task; + @Input() comment: TaskComment; + + constructor(private modalService: NumbasModal) {} + + ngOnInit() {} + + reviewNumbasTest() { + this.modalService.show(this.task, 'review'); + } +} diff --git a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html index d104df63af..cd18eda9ba 100644 --- a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html +++ b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html @@ -72,6 +72,14 @@ > +
+ +
+
{ if ( @@ -150,7 +154,7 @@ export class TaskCommentsViewerComponent implements OnChanges, OnInit { } shouldShowAuthorIcon(commentType: string) { - return !(commentType === 'extension' || commentType === 'status' || commentType == 'assessment'); + return !(commentType === 'extension' || commentType === 'status' || commentType == 'assessment' || commentType == 'numbas'); } commentClasses(comment: TaskComment): object { From 0652b56f69eb850c2dfb43be54d07beb4c4eb469 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 7 May 2024 07:10:54 +1000 Subject: [PATCH 038/776] feat: add test attempt service and minor numbas related changes --- src/app/api/models/test-attempt.ts | 14 +++++++++++--- src/app/api/services/numbas-lms.service.ts | 12 ++++++------ src/app/api/services/task-comment.service.ts | 2 +- src/app/doubtfire-angular.module.ts | 2 ++ src/app/doubtfire-angularjs.module.ts | 2 ++ .../upload-submission-modal.coffee | 2 +- 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/app/api/models/test-attempt.ts b/src/app/api/models/test-attempt.ts index 77e4af1724..e6f2e5d61d 100644 --- a/src/app/api/models/test-attempt.ts +++ b/src/app/api/models/test-attempt.ts @@ -1,14 +1,22 @@ import { Entity } from "ngx-entity-service"; +import { Task } from "./task"; export class TestAttempt extends Entity { - id: number; + public id: number; name: string; attemptNumber: number; passStatus: boolean; - examData: string; + suspendData: string; completed: boolean; cmiEntry: string; examResult: string; attemptedAt: Date; - associatedTaskId: number; + taskId: number; + + task: Task; + + constructor(task: Task) { + super(); + this.task = task; + } } diff --git a/src/app/api/services/numbas-lms.service.ts b/src/app/api/services/numbas-lms.service.ts index 6479a6ed93..59d94e0526 100644 --- a/src/app/api/services/numbas-lms.service.ts +++ b/src/app/api/services/numbas-lms.service.ts @@ -21,7 +21,7 @@ export class NumbasLmsService { }; private testId: number = 0; - private taskId: number; + private task: Task; private learnerId: string; initializationComplete$ = new BehaviorSubject(false); @@ -37,7 +37,7 @@ export class NumbasLmsService { } setTask(task: Task) { - this.taskId = task.id; + this.task = task; } getDefaultDataStore() { @@ -56,7 +56,7 @@ export class NumbasLmsService { if (mode === 'review') { this.SetValue('cmi.mode', 'review'); - xhr.open("GET", `${this.apiBaseUrl}/completed-latest?task_id=${this.taskId}`, false); + xhr.open("GET", `${this.apiBaseUrl}/completed-latest?task_id=${this.task.id}`, false); xhr.send(); console.log(xhr.responseText); @@ -88,7 +88,7 @@ export class NumbasLmsService { } } - xhr.open("GET", `${this.apiBaseUrl}/latest?task_id=${this.taskId}`, false); + xhr.open("GET", `${this.apiBaseUrl}/latest?task_id=${this.task.id}`, false); xhr.send(); console.log(xhr.responseText); @@ -139,7 +139,7 @@ export class NumbasLmsService { Terminate(): string { console.log('Terminate Called'); const examResult = this.dataStore["cmi.score.raw"]; - const status = this.GetValue("cmi.completion_status"); + const status = this.GetValue("cmi.success_status"); this.dataStore['completed'] = true; const currentAttemptNumber = this.dataStore['attempt_number'] || 0; const ExamName = this.dataStore['name']; @@ -153,7 +153,7 @@ export class NumbasLmsService { completed: true, exam_result: examResult, cmi_entry: cmientry, - task_id: this.taskId + task_id: this.task.id }; const xhr = new XMLHttpRequest(); diff --git a/src/app/api/services/task-comment.service.ts b/src/app/api/services/task-comment.service.ts index 9e646be77d..e3c80797a4 100644 --- a/src/app/api/services/task-comment.service.ts +++ b/src/app/api/services/task-comment.service.ts @@ -145,7 +145,7 @@ export class TaskCommentService extends CachedEntityService { const opts: RequestOptions = { endpointFormat: this.commentEndpointFormat }; // Based on the comment type - add to the body and configure the end point - if (commentType === 'text') { + if (commentType === 'text' || commentType === 'numbas') { body.append('comment', data); } else if (commentType === 'discussion') { opts.endpointFormat = this.discussionEndpointFormat; diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index c634965f6f..e1f4d2356f 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -229,6 +229,7 @@ import {NumbasComponent} from './common/numbas-component/numbas-component.compon import {NumbasModal} from './common/numbas-component/numbas-modal.component'; import {NumbasLmsService} from './api/services/numbas-lms.service'; import {NumbasCommentComponent} from './tasks/task-comments-viewer/numbas-comment/numbas-comment.component'; +import {TestAttemptService} from './api/services/test-attempt.service'; @NgModule({ // Components we declare @@ -406,6 +407,7 @@ import {NumbasCommentComponent} from './tasks/task-comments-viewer/numbas-commen CreateNewUnitModal, NumbasModal, NumbasLmsService, + TestAttemptService, provideLottieOptions({ player: () => player, }), diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index b88fbeb298..fa817ced4e 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -227,6 +227,7 @@ import {AlertService} from './common/services/alert.service'; import {GradeService} from './common/services/grade.service'; import {NumbasComponent} from './common/numbas-component/numbas-component.component'; import {NumbasModal} from './common/numbas-component/numbas-modal.component'; +import {TestAttemptService} from './api/services/test-attempt.service'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -310,6 +311,7 @@ DoubtfireAngularJSModule.factory( ); DoubtfireAngularJSModule.factory('CreateNewUnitModal', downgradeInjectable(CreateNewUnitModal)); DoubtfireAngularJSModule.factory('NumbasModal', downgradeInjectable(NumbasModal)); +DoubtfireAngularJSModule.factory('testAttemptService', downgradeInjectable(TestAttemptService)); // directive -> component DoubtfireAngularJSModule.directive( diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee index 68d742362e..f9552b3567 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee @@ -128,7 +128,7 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) removed.push('group') if !isRFF || !task.isGroupTask() removed.push('alignment') if !isRFF || !task.unit.ilos.length > 0 removed.push('comments') if isTestSubmission - removed.push('numbas') if !task.definition.hasEnabledNumbasTest + removed.push('numbas') if !isRFF || !task.definition.hasEnabledNumbasTest removed # Initialises the states initialise: -> From a576f484bc434728eab9632c022bccf1ed26cb01 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 7 May 2024 07:11:52 +1000 Subject: [PATCH 039/776] feat: add test attempt service --- src/app/api/services/test-attempt.service.ts | 40 ++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/app/api/services/test-attempt.service.ts diff --git a/src/app/api/services/test-attempt.service.ts b/src/app/api/services/test-attempt.service.ts new file mode 100644 index 0000000000..094f2baa0c --- /dev/null +++ b/src/app/api/services/test-attempt.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from "@angular/core"; +import { EntityService } from "ngx-entity-service"; +import { TestAttempt } from "../models/test-attempt"; +import { HttpClient } from "@angular/common/http"; +import API_URL from "src/app/config/constants/apiURL"; +import { Task } from "../models/task"; +import { Observable } from "rxjs"; +import { AppInjector } from "src/app/app-injector"; +import { DoubtfireConstants } from "src/app/config/constants/doubtfire-constants"; + +@Injectable() +export class TestAttemptService extends EntityService { + protected readonly endpointFormat = '/test_attempts?id=:id:'; + + constructor(httpClient: HttpClient) { + super(httpClient, API_URL); + + this.mapping.addKeys( + 'id', + 'name', + 'attemptNumber', + 'passStatus', + 'suspendData', + 'completed', + 'cmiEntry', + 'examResult', + 'attemptedAt', + 'taskId' + ); + } + + public createInstanceFrom(json: object, other?: any): TestAttempt { + return new TestAttempt(other as Task); + } + + public getLatestCompletedTestAttempt(task: Task): Observable { + const url = `${AppInjector.get(DoubtfireConstants).API_URL}/test_attempts/completed-latest?task_id=${task.id}`; + return AppInjector.get(HttpClient).get(url); + } +} \ No newline at end of file From 2b1dcfc717eb770dd623c62ac99d8d961d1c3124 Mon Sep 17 00:00:00 2001 From: ublefo <90136978+ublefo@users.noreply.github.com> Date: Tue, 7 May 2024 16:29:44 +1000 Subject: [PATCH 040/776] fix: ensure counters are incremented after object creation --- src/app/api/services/numbas-lms.service.ts | 25 ++++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/app/api/services/numbas-lms.service.ts b/src/app/api/services/numbas-lms.service.ts index 59d94e0526..5593fba3f0 100644 --- a/src/app/api/services/numbas-lms.service.ts +++ b/src/app/api/services/numbas-lms.service.ts @@ -13,16 +13,16 @@ export class NumbasLmsService { private defaultValues: { [key: string]: string } = { 'cmi.completion_status': 'not attempted', 'cmi.entry': 'ab-initio', + 'cmi.objectives._count': '0', + 'cmi.interactions._count': '0', 'numbas.user_role': 'learner', - 'numbas.duration_extension.units': 'seconds', 'cmi.mode': 'normal', - 'cmi.undefinedlearner_response': '1', - 'cmi.undefinedresult': '0' }; private testId: number = 0; private task: Task; - private learnerId: string; + private readonly learnerId: string; + private readonly learnerName: string; initializationComplete$ = new BehaviorSubject(false); private scormErrors: { [key: string]: string } = { @@ -33,7 +33,9 @@ export class NumbasLmsService { dataStore: { [key: string]: any } = this.getDefaultDataStore(); constructor(private userService: UserService) { - this.learnerId = this.userService.currentUser.studentId; + const user = this.userService.currentUser; + this.learnerId = user.studentId; + this.learnerName = user.firstName + user.lastName; } setTask(task: Task) { @@ -106,6 +108,7 @@ export class NumbasLmsService { if (latestTest.data['cmi_entry'] === 'ab-initio') { console.log("starting new test"); this.SetValue('cmi.learner_id', this.learnerId); + this.SetValue('cmi.learner_name', this.learnerName); this.dataStore['name'] = examName; this.dataStore['attempt_number'] = latestTest.data['attempt_number']; console.log(this.dataStore); @@ -178,9 +181,17 @@ export class NumbasLmsService { } SetValue(element: string, value: any): string { - if (element.startsWith('cmi.')) { - this.dataStore[element] = value; + console.log(`SetValue:`, element, value); + this.dataStore[element] = value; + if (element.match('cmi.interactions.\\d+.id')) { + console.log('Incrementing cmi.interactions._count'); + this.dataStore['cmi.interactions._count']++; } + if (element.match('cmi.objectives.\\d+.id')) { + console.log('Incrementing cmi.objectives._count'); + this.dataStore['cmi.objectives._count']++; + } + // console.log("dataStore after value set:", this.dataStore); return 'true'; } From aa20a273f6a4e8e0f597da164adb330deaff470c Mon Sep 17 00:00:00 2001 From: ublefo <90136978+ublefo@users.noreply.github.com> Date: Tue, 7 May 2024 23:20:13 +1000 Subject: [PATCH 041/776] refactor: rename scorm service --- .../{numbas-lms.service.ts => scorm-lms.service.ts} | 2 +- src/app/api/services/spec/numbas-lms.service.spec.ts | 8 ++++---- .../common/numbas-component/numbas-component.component.ts | 4 ++-- src/app/doubtfire-angular.module.ts | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) rename src/app/api/services/{numbas-lms.service.ts => scorm-lms.service.ts} (99%) diff --git a/src/app/api/services/numbas-lms.service.ts b/src/app/api/services/scorm-lms.service.ts similarity index 99% rename from src/app/api/services/numbas-lms.service.ts rename to src/app/api/services/scorm-lms.service.ts index 5593fba3f0..bd7b5f1085 100644 --- a/src/app/api/services/numbas-lms.service.ts +++ b/src/app/api/services/scorm-lms.service.ts @@ -7,7 +7,7 @@ import { Task } from '../models/task'; @Injectable({ providedIn: 'root' }) -export class NumbasLmsService { +export class ScormLmsService { private readonly apiBaseUrl = `${API_URL}/test_attempts`; private defaultValues: { [key: string]: string } = { diff --git a/src/app/api/services/spec/numbas-lms.service.spec.ts b/src/app/api/services/spec/numbas-lms.service.spec.ts index 7d29ef75e0..f38d5fc080 100644 --- a/src/app/api/services/spec/numbas-lms.service.spec.ts +++ b/src/app/api/services/spec/numbas-lms.service.spec.ts @@ -1,12 +1,12 @@ import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { NumbasLmsService } from '../numbas-lms.service'; +import { ScormLmsService } from '../scorm-lms.service'; import { TaskService } from '../task.service'; import { UserService } from '../user.service'; import { of } from 'rxjs'; describe('NumbasLmsService', () => { - let service: NumbasLmsService; + let service: ScormLmsService; let httpTestingController: HttpTestingController; let mockUserService: Partial; let mockTaskService: Partial; @@ -27,13 +27,13 @@ describe('NumbasLmsService', () => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ - NumbasLmsService, + ScormLmsService, { provide: UserService, useValue: mockUserService }, { provide: TaskService, useValue: mockTaskService } ] }); - service = TestBed.inject(NumbasLmsService); + service = TestBed.inject(ScormLmsService); httpTestingController = TestBed.inject(HttpTestingController); }); diff --git a/src/app/common/numbas-component/numbas-component.component.ts b/src/app/common/numbas-component/numbas-component.component.ts index f5453e0dcc..2f977ef60c 100644 --- a/src/app/common/numbas-component/numbas-component.component.ts +++ b/src/app/common/numbas-component/numbas-component.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { Task } from 'src/app/api/models/task'; -import { NumbasLmsService } from 'src/app/api/services/numbas-lms.service'; +import { ScormLmsService } from 'src/app/api/services/scorm-lms.service'; import { AppInjector } from 'src/app/app-injector'; import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; @@ -23,7 +23,7 @@ export class NumbasComponent implements OnInit { constructor( private dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: { task: Task, mode: 'attempt' | 'review' }, - private lmsService: NumbasLmsService, + private lmsService: ScormLmsService, private sanitizer: DomSanitizer ) {} diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index e1f4d2356f..e5b2e1b2a7 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -227,7 +227,7 @@ import {UnitCodeComponent} from './common/unit-code/unit-code.component'; import {GradeService} from './common/services/grade.service'; import {NumbasComponent} from './common/numbas-component/numbas-component.component'; import {NumbasModal} from './common/numbas-component/numbas-modal.component'; -import {NumbasLmsService} from './api/services/numbas-lms.service'; +import {ScormLmsService} from './api/services/scorm-lms.service'; import {NumbasCommentComponent} from './tasks/task-comments-viewer/numbas-comment/numbas-comment.component'; import {TestAttemptService} from './api/services/test-attempt.service'; @@ -406,7 +406,7 @@ import {TestAttemptService} from './api/services/test-attempt.service'; IsActiveUnitRole, CreateNewUnitModal, NumbasModal, - NumbasLmsService, + ScormLmsService, TestAttemptService, provideLottieOptions({ player: () => player, From fdd5667d1744f5ad135bba946a8d2daee6648605 Mon Sep 17 00:00:00 2001 From: ublefo <90136978+ublefo@users.noreply.github.com> Date: Wed, 8 May 2024 22:08:12 +1000 Subject: [PATCH 042/776] refactor: generalize scorm player components and services --- src/app/ajs-upgraded-providers.ts | 10 +++--- ...ms.service.ts => scorm-adapter.service.ts} | 8 ++--- ....spec.ts => scorm-adapter.service.spec.ts} | 11 +++--- .../scorm-player-modal.component.ts} | 10 +++--- .../scorm-player.component.html} | 4 +-- .../scorm-player.component.scss} | 0 .../scorm-player.component.spec.ts} | 12 +++---- .../scorm-player.component.ts} | 36 ++++++++++--------- src/app/doubtfire-angular.module.ts | 12 +++---- src/app/doubtfire-angularjs.module.ts | 11 +++--- .../upload-submission-modal.coffee | 11 +++--- .../upload-submission-modal.tpl.html | 8 ++--- .../numbas-comment.component.ts | 4 +-- 13 files changed, 71 insertions(+), 66 deletions(-) rename src/app/api/services/{scorm-lms.service.ts => scorm-adapter.service.ts} (97%) rename src/app/api/services/spec/{numbas-lms.service.spec.ts => scorm-adapter.service.spec.ts} (91%) rename src/app/common/{numbas-component/numbas-modal.component.ts => scorm-player/scorm-player-modal.component.ts} (64%) rename src/app/common/{numbas-component/numbas-component.component.html => scorm-player/scorm-player.component.html} (74%) rename src/app/common/{numbas-component/numbas-component.component.scss => scorm-player/scorm-player.component.scss} (100%) rename src/app/common/{numbas-component/numbas-component.component.spec.ts => scorm-player/scorm-player.component.spec.ts} (51%) rename src/app/common/{numbas-component/numbas-component.component.ts => scorm-player/scorm-player.component.ts} (50%) diff --git a/src/app/ajs-upgraded-providers.ts b/src/app/ajs-upgraded-providers.ts index 9d5b0f0203..f114da1c79 100644 --- a/src/app/ajs-upgraded-providers.ts +++ b/src/app/ajs-upgraded-providers.ts @@ -18,7 +18,7 @@ export const rootScope = new InjectionToken('$rootScope'); export const calendarModal = new InjectionToken('CalendarModal'); export const aboutDoubtfireModal = new InjectionToken('AboutDoubtfireModal'); export const plagiarismReportModal = new InjectionToken('PlagiarismReportModal'); -export const numbasModal = new InjectionToken('NumbasModal'); +export const scormPlayerModal = new InjectionToken('ScormPlayerModal'); // Define a provider for the above injection token... // It will get the service from AngularJS via the factory @@ -118,8 +118,8 @@ export const UnitStudentEnrolmentModalProvider = { deps: ['$injector'], }; -export const numbasModalProvider = { - provide: numbasModal, - useFactory: (i) => i.get('NumbasModal'), +export const ScormPlayerModalProvider = { + provide: scormPlayerModal, + useFactory: (i) => i.get('ScormPlayerModal'), deps: ['$injector'], -}; \ No newline at end of file +}; diff --git a/src/app/api/services/scorm-lms.service.ts b/src/app/api/services/scorm-adapter.service.ts similarity index 97% rename from src/app/api/services/scorm-lms.service.ts rename to src/app/api/services/scorm-adapter.service.ts index bd7b5f1085..3202331804 100644 --- a/src/app/api/services/scorm-lms.service.ts +++ b/src/app/api/services/scorm-adapter.service.ts @@ -7,7 +7,7 @@ import { Task } from '../models/task'; @Injectable({ providedIn: 'root' }) -export class ScormLmsService { +export class ScormAdapterService { private readonly apiBaseUrl = `${API_URL}/test_attempts`; private defaultValues: { [key: string]: string } = { @@ -207,7 +207,7 @@ export class ScormLmsService { if (!this.isTestCompleted()) { this.dataStore['cmi.exit'] = 'suspend'; } - console.log("Committing dataStore:", this.dataStore); + console.log("Committing DataModel:", this.dataStore); // Use XHR to send the request const xhr = new XMLHttpRequest(); @@ -216,9 +216,9 @@ export class ScormLmsService { xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 400) { - console.log('Suspend data saved successfully.'); + console.log('DataModel saved successfully.'); } else { - console.error('Error saving suspend data:', xhr.responseText); + console.error('Error saving DataModel:', xhr.responseText); } }; diff --git a/src/app/api/services/spec/numbas-lms.service.spec.ts b/src/app/api/services/spec/scorm-adapter.service.spec.ts similarity index 91% rename from src/app/api/services/spec/numbas-lms.service.spec.ts rename to src/app/api/services/spec/scorm-adapter.service.spec.ts index f38d5fc080..5d3a52caac 100644 --- a/src/app/api/services/spec/numbas-lms.service.spec.ts +++ b/src/app/api/services/spec/scorm-adapter.service.spec.ts @@ -1,12 +1,11 @@ import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { ScormLmsService } from '../scorm-lms.service'; +import { ScormAdapterService } from '../scorm-adapter.service'; import { TaskService } from '../task.service'; import { UserService } from '../user.service'; -import { of } from 'rxjs'; -describe('NumbasLmsService', () => { - let service: ScormLmsService; +describe('ScormAdapterService', () => { + let service: ScormAdapterService; let httpTestingController: HttpTestingController; let mockUserService: Partial; let mockTaskService: Partial; @@ -27,13 +26,13 @@ describe('NumbasLmsService', () => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ - ScormLmsService, + ScormAdapterService, { provide: UserService, useValue: mockUserService }, { provide: TaskService, useValue: mockTaskService } ] }); - service = TestBed.inject(ScormLmsService); + service = TestBed.inject(ScormAdapterService); httpTestingController = TestBed.inject(HttpTestingController); }); diff --git a/src/app/common/numbas-component/numbas-modal.component.ts b/src/app/common/scorm-player/scorm-player-modal.component.ts similarity index 64% rename from src/app/common/numbas-component/numbas-modal.component.ts rename to src/app/common/scorm-player/scorm-player-modal.component.ts index 73b2122c17..443add9bed 100644 --- a/src/app/common/numbas-component/numbas-modal.component.ts +++ b/src/app/common/scorm-player/scorm-player-modal.component.ts @@ -1,18 +1,18 @@ import { Injectable } from '@angular/core'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; -import { NumbasComponent } from './numbas-component.component'; +import { ScormPlayerComponent } from './scorm-player.component'; import { Task } from 'src/app/api/models/task'; @Injectable({ providedIn: 'root', }) -export class NumbasModal { +export class ScormPlayerModal { constructor(public dialog: MatDialog) { } - + public show(task: Task, mode: 'attempt' | 'review'): void { - let dialogRef: MatDialogRef; + let dialogRef: MatDialogRef; - dialogRef = this.dialog.open(NumbasComponent, { + dialogRef = this.dialog.open(ScormPlayerComponent, { data: { task, mode }, width: '95%', height: '90%' }); diff --git a/src/app/common/numbas-component/numbas-component.component.html b/src/app/common/scorm-player/scorm-player.component.html similarity index 74% rename from src/app/common/numbas-component/numbas-component.component.html rename to src/app/common/scorm-player/scorm-player.component.html index d562f02239..5089ad9de3 100644 --- a/src/app/common/numbas-component/numbas-component.component.html +++ b/src/app/common/scorm-player/scorm-player.component.html @@ -1,4 +1,4 @@
- -
\ No newline at end of file + +
diff --git a/src/app/common/numbas-component/numbas-component.component.scss b/src/app/common/scorm-player/scorm-player.component.scss similarity index 100% rename from src/app/common/numbas-component/numbas-component.component.scss rename to src/app/common/scorm-player/scorm-player.component.scss diff --git a/src/app/common/numbas-component/numbas-component.component.spec.ts b/src/app/common/scorm-player/scorm-player.component.spec.ts similarity index 51% rename from src/app/common/numbas-component/numbas-component.component.spec.ts rename to src/app/common/scorm-player/scorm-player.component.spec.ts index 31dad5e305..7980df7c3c 100644 --- a/src/app/common/numbas-component/numbas-component.component.spec.ts +++ b/src/app/common/scorm-player/scorm-player.component.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NumbasComponent } from './numbas-component.component'; +import { ScormPlayerComponent } from './scorm-player.component'; -describe('NumbasComponent', () => { - let component: NumbasComponent; - let fixture: ComponentFixture; +describe('ScormPlayerComponent', () => { + let component: ScormPlayerComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ NumbasComponent ] + declarations: [ ScormPlayerComponent ] }) .compileComponents(); - fixture = TestBed.createComponent(NumbasComponent); + fixture = TestBed.createComponent(ScormPlayerComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/common/numbas-component/numbas-component.component.ts b/src/app/common/scorm-player/scorm-player.component.ts similarity index 50% rename from src/app/common/numbas-component/numbas-component.component.ts rename to src/app/common/scorm-player/scorm-player.component.ts index 2f977ef60c..7ed98b3cfe 100644 --- a/src/app/common/numbas-component/numbas-component.component.ts +++ b/src/app/common/scorm-player/scorm-player.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { Task } from 'src/app/api/models/task'; -import { ScormLmsService } from 'src/app/api/services/scorm-lms.service'; +import { ScormAdapterService } from 'src/app/api/services/scorm-adapter.service'; import { AppInjector } from 'src/app/app-injector'; import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; @@ -11,35 +11,35 @@ declare global { } @Component({ - selector: 'f-numbas-component', - templateUrl: './numbas-component.component.html', - styleUrls: ['./numbas-component.component.scss'], + selector: 'f-scorm-player', + templateUrl: './scorm-player.component.html', + styleUrls: ['./scorm-player.component.scss'], }) -export class NumbasComponent implements OnInit { +export class ScormPlayerComponent implements OnInit { task: Task; currentMode: 'attempt' | 'review' = 'attempt'; iframeSrc: SafeResourceUrl; constructor( - private dialogRef: MatDialogRef, + private dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: { task: Task, mode: 'attempt' | 'review' }, - private lmsService: ScormLmsService, + private scormAdapter: ScormAdapterService, private sanitizer: DomSanitizer ) {} ngOnInit(): void { this.task = this.data.task; - this.lmsService.setTask(this.task); + this.scormAdapter.setTask(this.task); window.API_1484_11 = { - Initialize: () => this.lmsService.Initialize(this.currentMode), - Terminate: () => this.lmsService.Terminate(), - GetValue: (element: string) => this.lmsService.GetValue(element), - SetValue: (element: string, value: string) => this.lmsService.SetValue(element, value), - Commit: () => this.lmsService.Commit(), - GetLastError: () => this.lmsService.GetLastError(), - GetErrorString: (errorCode: string) => this.lmsService.GetErrorString(errorCode), - GetDiagnostic: (errorCode: string) => this.lmsService.GetDiagnostic(errorCode) + Initialize: () => this.scormAdapter.Initialize(this.currentMode), + Terminate: () => this.scormAdapter.Terminate(), + GetValue: (element: string) => this.scormAdapter.GetValue(element), + SetValue: (element: string, value: string) => this.scormAdapter.SetValue(element, value), + Commit: () => this.scormAdapter.Commit(), + GetLastError: () => this.scormAdapter.GetLastError(), + GetErrorString: (errorCode: string) => this.scormAdapter.GetErrorString(errorCode), + GetDiagnostic: (errorCode: string) => this.scormAdapter.GetDiagnostic(errorCode) }; this.currentMode = this.data.mode; @@ -47,7 +47,9 @@ export class NumbasComponent implements OnInit { this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(`${AppInjector.get(DoubtfireConstants).API_URL}/numbas_api/${this.task.taskDefId}/index.html`); } - removeNumbasTest(): void { + close(): void { + console.log('SCORM player closing, commiting DataModel!'); + this.scormAdapter.Commit(); const iframe = document.getElementsByTagName('iframe')[0]; iframe?.parentNode?.removeChild(iframe); this.dialogRef.close(); diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index e5b2e1b2a7..23145ccaea 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -72,6 +72,7 @@ import { gradeTaskModalProvider, uploadSubmissionModalProvider, ConfirmationModalProvider, + ScormPlayerModalProvider, } from './ajs-upgraded-providers'; import { TaskCommentComposerComponent, @@ -225,9 +226,8 @@ import {FTaskSheetViewComponent} from './units/states/tasks/viewer/directives/f- import {TasksViewerComponent} from './units/states/tasks/tasks-viewer/tasks-viewer.component'; import {UnitCodeComponent} from './common/unit-code/unit-code.component'; import {GradeService} from './common/services/grade.service'; -import {NumbasComponent} from './common/numbas-component/numbas-component.component'; -import {NumbasModal} from './common/numbas-component/numbas-modal.component'; -import {ScormLmsService} from './api/services/scorm-lms.service'; +import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component'; +import {ScormAdapterService} from './api/services/scorm-adapter.service'; import {NumbasCommentComponent} from './tasks/task-comments-viewer/numbas-comment/numbas-comment.component'; import {TestAttemptService} from './api/services/test-attempt.service'; @@ -332,7 +332,7 @@ import {TestAttemptService} from './api/services/test-attempt.service'; FUsersComponent, FTaskBadgeComponent, FUnitsComponent, - NumbasComponent, + ScormPlayerComponent, NumbasCommentComponent, ], // Services we provide @@ -405,8 +405,8 @@ import {TestAttemptService} from './api/services/test-attempt.service'; TasksForInboxSearchPipe, IsActiveUnitRole, CreateNewUnitModal, - NumbasModal, - ScormLmsService, + ScormPlayerModalProvider, + ScormAdapterService, TestAttemptService, provideLottieOptions({ player: () => player, diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index fa817ced4e..3cd6555bd8 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -225,8 +225,8 @@ import {FUnitsComponent} from './admin/states/f-units/f-units.component'; import {MarkedPipe} from './common/pipes/marked.pipe'; import {AlertService} from './common/services/alert.service'; import {GradeService} from './common/services/grade.service'; -import {NumbasComponent} from './common/numbas-component/numbas-component.component'; -import {NumbasModal} from './common/numbas-component/numbas-modal.component'; +import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component'; +import {ScormPlayerModal} from './common/scorm-player/scorm-player-modal.component'; import {TestAttemptService} from './api/services/test-attempt.service'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ @@ -310,7 +310,7 @@ DoubtfireAngularJSModule.factory( downgradeInjectable(EditProfileDialogService), ); DoubtfireAngularJSModule.factory('CreateNewUnitModal', downgradeInjectable(CreateNewUnitModal)); -DoubtfireAngularJSModule.factory('NumbasModal', downgradeInjectable(NumbasModal)); +DoubtfireAngularJSModule.factory('ScormPlayerModal', downgradeInjectable(ScormPlayerModal)); DoubtfireAngularJSModule.factory('testAttemptService', downgradeInjectable(TestAttemptService)); // directive -> component @@ -446,7 +446,10 @@ DoubtfireAngularJSModule.directive( ); DoubtfireAngularJSModule.directive('fUnits', downgradeComponent({component: FUnitsComponent})); -DoubtfireAngularJSModule.directive('fNumbasComponent', downgradeComponent({component: NumbasComponent})); +DoubtfireAngularJSModule.directive( + 'fScormPlayerComponent', + downgradeComponent({component: ScormPlayerComponent}), +); // Global configuration DoubtfireAngularJSModule.directive( diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee index f9552b3567..6a17bd558c 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee @@ -32,7 +32,7 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) UploadSubmissionModal ) -.controller('UploadSubmissionModalCtrl', ($scope, $rootScope, $timeout, $modalInstance, NumbasModal, newTaskService, newProjectService, task, reuploadEvidence, outcomeService, PrivacyPolicy) -> +.controller('UploadSubmissionModalCtrl', ($scope, $rootScope, $timeout, $modalInstance, ScormPlayerModal, newTaskService, newProjectService, task, reuploadEvidence, outcomeService, PrivacyPolicy) -> $scope.privacyPolicy = PrivacyPolicy # Expose task to scope $scope.task = task @@ -100,7 +100,7 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) # States functionality states = { # All possible states - all: ['group', 'numbas', 'files', 'alignment', 'comments', 'uploading'] + all: ['group', 'scorm-assessment', 'files', 'alignment', 'comments', 'uploading'] # Only states which are shown (populated in initialise) shown: [] # The currently active state (set in initialise) @@ -128,7 +128,7 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) removed.push('group') if !isRFF || !task.isGroupTask() removed.push('alignment') if !isRFF || !task.unit.ilos.length > 0 removed.push('comments') if isTestSubmission - removed.push('numbas') if !isRFF || !task.definition.hasEnabledNumbasTest + removed.push('scorm-assessment') if !isRFF || !task.definition.hasEnabledNumbasTest removed # Initialises the states initialise: -> @@ -155,8 +155,9 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) previous: states.previous } - $scope.launchNumbasDialog = -> - NumbasModal.show $scope.task, 'attempt' + $scope.launchScormPlayer = -> + console.clear() + ScormPlayerModal.show $scope.task, 'attempt' # Whether or not we should disable this button $scope.shouldDisableBtn = { diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html index 0527aadd40..8b6c89bea3 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html @@ -44,9 +44,9 @@

+ class="state state-scorm-assessment" + ng-class="{'state-hidden-left': isHidden('scorm-assessment').left, + 'state-hidden-right': isHidden('scorm-assessment').right}">

@@ -57,7 +57,7 @@

-
diff --git a/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.ts b/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.ts index eed512fe19..650a110d0f 100644 --- a/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.ts +++ b/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, Input } from '@angular/core'; import { Task, TaskComment } from 'src/app/api/models/doubtfire-model'; -import { NumbasModal } from 'src/app/common/numbas-component/numbas-modal.component'; +import { ScormPlayerModal } from 'src/app/common/scorm-player/scorm-player-modal.component'; @Component({ selector: 'numbas-comment', @@ -11,7 +11,7 @@ export class NumbasCommentComponent implements OnInit { @Input() task: Task; @Input() comment: TaskComment; - constructor(private modalService: NumbasModal) {} + constructor(private modalService: ScormPlayerModal) {} ngOnInit() {} From 226d9193251fb675c92bec8265508477857a4ec9 Mon Sep 17 00:00:00 2001 From: ublefo <90136978+ublefo@users.noreply.github.com> Date: Wed, 8 May 2024 22:29:37 +1000 Subject: [PATCH 043/776] fix: use nullish coalescing when retrieving data from the datamodel also disallow dismissing modal to ensure datamodel is committed --- src/app/api/services/scorm-adapter.service.ts | 10 +++++----- .../scorm-player/scorm-player-modal.component.ts | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/app/api/services/scorm-adapter.service.ts b/src/app/api/services/scorm-adapter.service.ts index 3202331804..22c0205a33 100644 --- a/src/app/api/services/scorm-adapter.service.ts +++ b/src/app/api/services/scorm-adapter.service.ts @@ -69,7 +69,7 @@ export class ScormAdapterService { try { const completedTest = JSON.parse(xhr.responseText); - let parsedSuspendData = JSON.parse(completedTest.data.suspend_data || '{}'); + let parsedSuspendData = JSON.parse(completedTest.data.suspend_data ?? '{}'); // Set entire suspendData string to cmi.suspend_data this.SetValue('cmi.suspend_data', JSON.stringify(parsedSuspendData)); @@ -114,7 +114,7 @@ export class ScormAdapterService { console.log(this.dataStore); } else if (latestTest.data['cmi_entry'] === 'resume') { console.log("resuming test"); - let parsedSuspendData = JSON.parse(latestTest.data.suspend_data || '{}'); + let parsedSuspendData = JSON.parse(latestTest.data.suspend_data ?? '{}'); this.dataStore = JSON.parse(JSON.stringify(parsedSuspendData)); @@ -132,7 +132,7 @@ export class ScormAdapterService { } isTestCompleted(): boolean { - return this.dataStore?.['completed'] || false; + return this.dataStore?.['completed'] ?? false; } private resetDataStore() { @@ -144,7 +144,7 @@ export class ScormAdapterService { const examResult = this.dataStore["cmi.score.raw"]; const status = this.GetValue("cmi.success_status"); this.dataStore['completed'] = true; - const currentAttemptNumber = this.dataStore['attempt_number'] || 0; + const currentAttemptNumber = this.dataStore['attempt_number'] ?? 0; const ExamName = this.dataStore['name']; this.SetValue('cmi.entry', 'RO'); const cmientry = this.GetValue('cmi.entry'); @@ -177,7 +177,7 @@ export class ScormAdapterService { } GetValue(element: string): string { - return this.dataStore[element] || ''; + return this.dataStore[element] ?? ''; } SetValue(element: string, value: any): string { diff --git a/src/app/common/scorm-player/scorm-player-modal.component.ts b/src/app/common/scorm-player/scorm-player-modal.component.ts index 443add9bed..190e7bf570 100644 --- a/src/app/common/scorm-player/scorm-player-modal.component.ts +++ b/src/app/common/scorm-player/scorm-player-modal.component.ts @@ -14,7 +14,8 @@ export class ScormPlayerModal { dialogRef = this.dialog.open(ScormPlayerComponent, { data: { task, mode }, - width: '95%', height: '90%' + width: '95%', height: '90%', + disableClose: true, }); } } From 20472257c87711955b79008fa3cb7517151a4424 Mon Sep 17 00:00:00 2001 From: ublefo <90136978+ublefo@users.noreply.github.com> Date: Thu, 9 May 2024 06:18:20 +1000 Subject: [PATCH 044/776] refactor: separate out scorm datamodel and player context --- src/app/api/models/doubtfire-model.ts | 2 + src/app/api/models/scorm-datamodel.ts | 58 ++++++ src/app/api/models/scorm-player-context.ts | 33 +++ src/app/api/services/scorm-adapter.service.ts | 190 +++++++++--------- .../scorm-player/scorm-player.component.ts | 4 +- 5 files changed, 193 insertions(+), 94 deletions(-) create mode 100644 src/app/api/models/scorm-datamodel.ts create mode 100644 src/app/api/models/scorm-player-context.ts diff --git a/src/app/api/models/doubtfire-model.ts b/src/app/api/models/doubtfire-model.ts index 3cb4911083..baca2f22d2 100644 --- a/src/app/api/models/doubtfire-model.ts +++ b/src/app/api/models/doubtfire-model.ts @@ -32,6 +32,8 @@ export * from './task-comment/discussion-comment'; export * from '../services/task-outcome-alignment.service'; export * from './task-similarity'; export * from './tii-action'; +export * from './scorm-datamodel'; +export * from './scorm-player-context'; // Users -- are students or staff export * from './user/user'; diff --git a/src/app/api/models/scorm-datamodel.ts b/src/app/api/models/scorm-datamodel.ts new file mode 100644 index 0000000000..e9d105f3c7 --- /dev/null +++ b/src/app/api/models/scorm-datamodel.ts @@ -0,0 +1,58 @@ +export class ScormDataModel { + initState: {[key: string]: string} = { + 'cmi.completion_status': 'not attempted', + 'cmi.entry': 'ab-initio', + 'cmi.objectives._count': '0', + 'cmi.interactions._count': '0', + 'cmi.mode': 'normal', + }; + + dataModel: {[key: string]: any} = {}; + readonly msgPrefix = 'SCORM DataModel: '; + + constructor() { + this.dataModel = {}; + } + + public init() { + console.log(this.msgPrefix + 'initializing DataModel with default values'); + this.dataModel = this.initState; + } + + public restore(dataModel: {[key: string]: any} = {}) { + console.log(this.msgPrefix + 'restoring DataModel with provided data'); + this.dataModel = dataModel; + } + + public get(key: string): string { + return this.dataModel[key] ?? ''; + } + + public dump(): {[key: string]: any} { + return this.dataModel; + } + + public set(key: string, value: any): string { + console.log(this.msgPrefix + 'set: ', key, value); + this.dataModel[key] = value; + if (key.match('cmi.interactions.\\d+.id')) { + const interactionPath = key.match('cmi.interactions.\\d+'); + const objectivesCounterForInteraction = interactionPath.toString() + '.objectives._count'; + console.log('Incrementing cmi.interactions._count'); + this.dataModel['cmi.interactions._count']++; + console.log(`Initializing ${objectivesCounterForInteraction}`); + this.dataModel[objectivesCounterForInteraction] = 0; + } + if (key.match('cmi.interactions.\\d+.objectives.\\d+.id')) { + const interactionPath = key.match('cmi.interactions.\\d+.objectives'); + const objectivesCounterForInteraction = interactionPath.toString() + '._count'; + console.log(`Incrementing ${objectivesCounterForInteraction}`); + this.dataModel[objectivesCounterForInteraction.toString()]++; + } + if (key.match('cmi.objectives.\\d+.id')) { + console.log('Incrementing cmi.objectives._count'); + this.dataModel['cmi.objectives._count']++; + } + return 'true'; + } +} diff --git a/src/app/api/models/scorm-player-context.ts b/src/app/api/models/scorm-player-context.ts new file mode 100644 index 0000000000..5f1c4bd3ba --- /dev/null +++ b/src/app/api/models/scorm-player-context.ts @@ -0,0 +1,33 @@ +import { Task, User } from 'src/app/api/models/doubtfire-model'; + +export class ScormPlayerContext { + task: Task; + mode: 'browse' | 'normal' | 'review'; + user: User; + attemptNumber: number; + attemptId: number; + learnerName: string; + learnerId: number; + + constructor(user: User) { + this.user = user; + this.learnerId = user.id; + this.learnerName = user.firstName + ' ' + user.lastName; + } + + public setTask(task: Task): void { + this.task = task; + } + + public setMode(mode: 'browse' | 'normal' | 'review'): void { + this.mode = mode; + } + + public setAttemptNumber(attemptNumber: number = 1): void { + this.attemptNumber = attemptNumber; + } + + public setAttemptId(attemptId: number): void { + this.attemptId = attemptId; + } +} diff --git a/src/app/api/services/scorm-adapter.service.ts b/src/app/api/services/scorm-adapter.service.ts index 22c0205a33..6c5353746b 100644 --- a/src/app/api/services/scorm-adapter.service.ts +++ b/src/app/api/services/scorm-adapter.service.ts @@ -2,63 +2,76 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { UserService } from './user.service'; import API_URL from 'src/app/config/constants/apiURL'; -import { Task } from '../models/task'; +import { Task, ScormDataModel, ScormPlayerContext } from 'src/app/api/models/doubtfire-model'; @Injectable({ providedIn: 'root' }) export class ScormAdapterService { private readonly apiBaseUrl = `${API_URL}/test_attempts`; + private dataModel: ScormDataModel; + private playerContext: ScormPlayerContext; - private defaultValues: { [key: string]: string } = { - 'cmi.completion_status': 'not attempted', - 'cmi.entry': 'ab-initio', - 'cmi.objectives._count': '0', - 'cmi.interactions._count': '0', - 'numbas.user_role': 'learner', - 'cmi.mode': 'normal', - }; - - private testId: number = 0; - private task: Task; - private readonly learnerId: string; - private readonly learnerName: string; initializationComplete$ = new BehaviorSubject(false); - private scormErrors: { [key: string]: string } = { - "0": "No error", - "101": "General exception", + private scormErrorCodes: {[key: string]: string} = { + '0': 'No Error', + '101': 'General Exception', + '102': 'General Initialization Failure', + '103': 'Already Initialized', + '104': 'Content Instance Terminated', + '111': 'General Termination Failure', + '112': 'Termination Before Initialization', + '113': 'Termination After Termination', + '122': 'Retrieve Data Before Initialization', + '123': 'Retrieve Data After Termination', + '132': 'Store Data Before Initialization', + '133': 'Store Data After Termination', + '142': 'Commit Before Initialization', + '143': 'Commit After Termination', + '201': 'General Argument Error', + '301': 'General Get Failure', + '351': 'General Set Failure', + '391': 'General Commit Failure', + '401': 'Undefined Data Model Element', + '402': 'Unimplemented Data Model Element', + '403': 'Data Model Element Value Not Initialized', + '404': 'Data Model Element Is Read Only', + '405': 'Data Model Element Is Write Only', + '406': 'Data Model Element Type Mismatch', + '407': 'Data Model Element Value Out Of Range', + '408': 'Data Model Dependency Not Established', }; - dataStore: { [key: string]: any } = this.getDefaultDataStore(); - constructor(private userService: UserService) { - const user = this.userService.currentUser; - this.learnerId = user.studentId; - this.learnerName = user.firstName + user.lastName; + this.dataModel = new ScormDataModel(); + this.playerContext = new ScormPlayerContext(this.userService.currentUser); } setTask(task: Task) { - this.task = task; + this.playerContext.setTask(task); } - getDefaultDataStore() { - // Use spread operator to merge defaultValues into the dataStore - return { - ...this.defaultValues, - pass_status: false, - completed: false, - }; - } + // getDefaultDataStore() { + // // Use spread operator to merge defaultValues into the dataStore + // return { + // ...this.defaultValues, + // pass_status: false, + // completed: false, + // }; + // } Initialize(mode: 'attempt' | 'review' = 'attempt'): string { console.log('Initialize() function called'); - const examName = 'test Exam Name 1'; - let xhr = new XMLHttpRequest(); + const xhr = new XMLHttpRequest(); if (mode === 'review') { this.SetValue('cmi.mode', 'review'); - xhr.open("GET", `${this.apiBaseUrl}/completed-latest?task_id=${this.task.id}`, false); + xhr.open( + 'GET', + `${this.apiBaseUrl}/completed-latest?task_id=${this.playerContext.task.id}`, + false, + ); xhr.send(); console.log(xhr.responseText); @@ -69,15 +82,17 @@ export class ScormAdapterService { try { const completedTest = JSON.parse(xhr.responseText); - let parsedSuspendData = JSON.parse(completedTest.data.suspend_data ?? '{}'); + const parsedDataModel = JSON.parse(completedTest.data.suspend_data ?? '{}'); // Set entire suspendData string to cmi.suspend_data - this.SetValue('cmi.suspend_data', JSON.stringify(parsedSuspendData)); + this.SetValue('cmi.suspend_data', JSON.stringify(parsedDataModel)); - // Use SetValue to set parsedSuspendData values to dataStore - Object.keys(parsedSuspendData).forEach(key => { - this.SetValue(key, parsedSuspendData[key]); - }); + // // Use SetValue to set parsedSuspendData values to dataStore + // Object.keys(parsedDataModel).forEach((key) => { + // this.SetValue(key, parsedDataModel[key]); + // }); + + this.dataModel.restore(parsedDataModel); this.SetValue('cmi.entry', 'RO'); this.SetValue('cmi.mode', 'review'); @@ -90,7 +105,7 @@ export class ScormAdapterService { } } - xhr.open("GET", `${this.apiBaseUrl}/latest?task_id=${this.task.id}`, false); + xhr.open('GET', `${this.apiBaseUrl}/latest?task_id=${this.playerContext.task.id}`, false); xhr.send(); console.log(xhr.responseText); @@ -103,27 +118,27 @@ export class ScormAdapterService { try { latestTest = JSON.parse(xhr.responseText); console.log('Latest test result:', latestTest); - this.testId = latestTest.data.id; + this.playerContext.attemptId = latestTest.data.id; if (latestTest.data['cmi_entry'] === 'ab-initio') { - console.log("starting new test"); - this.SetValue('cmi.learner_id', this.learnerId); - this.SetValue('cmi.learner_name', this.learnerName); - this.dataStore['name'] = examName; - this.dataStore['attempt_number'] = latestTest.data['attempt_number']; - console.log(this.dataStore); - } else if (latestTest.data['cmi_entry'] === 'resume') { - console.log("resuming test"); - let parsedSuspendData = JSON.parse(latestTest.data.suspend_data ?? '{}'); + console.log('starting new test'); + this.dataModel.init(); + this.SetValue('cmi.learner_id', this.playerContext.learnerId); + this.SetValue('cmi.learner_name', this.playerContext.learnerName); - this.dataStore = JSON.parse(JSON.stringify(parsedSuspendData)); + this.dataModel.set('attempt_number', latestTest.data['attempt_number']); + console.log(this.dataModel.dump()); + } else if (latestTest.data['cmi_entry'] === 'resume') { + console.log('resuming test'); + const restoredDataModel = JSON.parse(latestTest.data.suspend_data ?? '{}'); + this.dataModel.restore(JSON.parse(JSON.stringify(restoredDataModel))); - console.log(this.dataStore); + console.log(this.dataModel.dump()); } this.initializationComplete$.next(true); - console.log("finished initializing"); + console.log('finished initializing'); return 'true'; } catch (error) { console.error('Error:', error); @@ -131,67 +146,56 @@ export class ScormAdapterService { } } - isTestCompleted(): boolean { - return this.dataStore?.['completed'] ?? false; - } - - private resetDataStore() { - this.dataStore = this.getDefaultDataStore(); - } + // isTestCompleted(): boolean { + // return this.dataModel.get('completed') ?? false; + // } Terminate(): string { console.log('Terminate Called'); - const examResult = this.dataStore["cmi.score.raw"]; - const status = this.GetValue("cmi.success_status"); - this.dataStore['completed'] = true; - const currentAttemptNumber = this.dataStore['attempt_number'] ?? 0; - const ExamName = this.dataStore['name']; - this.SetValue('cmi.entry', 'RO'); - const cmientry = this.GetValue('cmi.entry'); + const examResult = this.dataModel.get('cmi.score.raw'); + const status = this.dataModel.get('cmi.success_status'); + this.dataModel.set('completed', true); + const currentAttemptNumber = this.dataModel.get('attempt_number') ?? 0; + const ExamName = this.dataModel.get('name'); + this.dataModel.set('cmi.entry', 'RO'); + const cmientry = this.dataModel.get('cmi.entry'); const data = { name: ExamName, attempt_number: currentAttemptNumber, pass_status: status === 'passed', - suspend_data: JSON.stringify(this.dataStore), + suspend_data: JSON.stringify(this.dataModel.dump()), completed: true, exam_result: examResult, cmi_entry: cmientry, - task_id: this.task.id + task_id: this.playerContext.task.id }; const xhr = new XMLHttpRequest(); - if (this.testId) { - xhr.open("PUT", `${this.apiBaseUrl}/${this.testId}`, false); + if (this.playerContext.attemptId) { + xhr.open('PUT', `${this.apiBaseUrl}/${this.playerContext.attemptId}`, false); } else { - xhr.open("POST", this.apiBaseUrl, false); + xhr.open('POST', this.apiBaseUrl, false); } - xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); xhr.send(JSON.stringify(data)); if (xhr.status !== 200) { console.error('Error sending test data:', xhr.statusText); return 'false'; } - this.resetDataStore(); + this.dataModel.init(); return 'true'; } GetValue(element: string): string { - return this.dataStore[element] ?? ''; + const value = this.dataModel.get(element); + console.log(`GetValue:`, element, value); + return value; } SetValue(element: string, value: any): string { console.log(`SetValue:`, element, value); - this.dataStore[element] = value; - if (element.match('cmi.interactions.\\d+.id')) { - console.log('Incrementing cmi.interactions._count'); - this.dataStore['cmi.interactions._count']++; - } - if (element.match('cmi.objectives.\\d+.id')) { - console.log('Incrementing cmi.objectives._count'); - this.dataStore['cmi.objectives._count']++; - } - // console.log("dataStore after value set:", this.dataStore); + this.dataModel.set(element, value); return 'true'; } @@ -203,15 +207,15 @@ export class ScormAdapterService { } // Set cmi.entry to 'resume' before committing dataStore - this.dataStore['cmi.entry'] = 'resume'; - if (!this.isTestCompleted()) { - this.dataStore['cmi.exit'] = 'suspend'; - } - console.log("Committing DataModel:", this.dataStore); + this.dataModel.set('cmi.entry', 'resume'); + // if (!this.isTestCompleted()) { + // this.dataModel.set('cmi.exit', 'suspend'); + // } + console.log('Committing DataModel:', this.dataModel.dump()); // Use XHR to send the request const xhr = new XMLHttpRequest(); - xhr.open('PUT', `${this.apiBaseUrl}/${this.testId}/suspend`, true); + xhr.open('PUT', `${this.apiBaseUrl}/${this.playerContext.attemptId}/suspend`, true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onload = () => { @@ -226,7 +230,7 @@ export class ScormAdapterService { console.error('Request failed.'); }; - const requestData = { suspend_data: this.dataStore }; + const requestData = { suspend_data: this.dataModel.dump() }; xhr.send(JSON.stringify(requestData)); return 'true'; } diff --git a/src/app/common/scorm-player/scorm-player.component.ts b/src/app/common/scorm-player/scorm-player.component.ts index 7ed98b3cfe..428f6869b7 100644 --- a/src/app/common/scorm-player/scorm-player.component.ts +++ b/src/app/common/scorm-player/scorm-player.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; -import { Task } from 'src/app/api/models/task'; +import { Task, ScormDataModel, ScormPlayerContext } from 'src/app/api/models/doubtfire-model'; import { ScormAdapterService } from 'src/app/api/services/scorm-adapter.service'; import { AppInjector } from 'src/app/app-injector'; import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; @@ -16,6 +16,8 @@ declare global { styleUrls: ['./scorm-player.component.scss'], }) export class ScormPlayerComponent implements OnInit { + context: ScormPlayerContext; + task: Task; currentMode: 'attempt' | 'review' = 'attempt'; iframeSrc: SafeResourceUrl; From b0863c7cf9319862b224705b50a238d2fabc5ec9 Mon Sep 17 00:00:00 2001 From: ublefo <90136978+ublefo@users.noreply.github.com> Date: Sun, 12 May 2024 08:43:22 +1000 Subject: [PATCH 045/776] refactor: rewrite test attempt code --- src/app/api/models/scorm-datamodel.ts | 24 +- src/app/api/models/scorm-player-context.ts | 69 +++- src/app/api/models/task-definition.ts | 14 +- src/app/api/models/task.ts | 2 +- src/app/api/services/scorm-adapter.service.ts | 350 +++++++++--------- src/app/api/services/task-comment.service.ts | 2 +- .../api/services/task-definition.service.ts | 10 +- .../scorm-player-modal.component.ts | 15 +- .../scorm-player/scorm-player.component.ts | 41 +- .../upload-submission-modal.coffee | 2 +- .../numbas-comment.component.ts | 6 +- .../task-comments-viewer.component.html | 2 +- .../task-comments-viewer.component.scss | 2 +- .../task-definition-numbas.component.html | 10 +- 14 files changed, 289 insertions(+), 260 deletions(-) diff --git a/src/app/api/models/scorm-datamodel.ts b/src/app/api/models/scorm-datamodel.ts index e9d105f3c7..9454db78f6 100644 --- a/src/app/api/models/scorm-datamodel.ts +++ b/src/app/api/models/scorm-datamodel.ts @@ -1,12 +1,4 @@ export class ScormDataModel { - initState: {[key: string]: string} = { - 'cmi.completion_status': 'not attempted', - 'cmi.entry': 'ab-initio', - 'cmi.objectives._count': '0', - 'cmi.interactions._count': '0', - 'cmi.mode': 'normal', - }; - dataModel: {[key: string]: any} = {}; readonly msgPrefix = 'SCORM DataModel: '; @@ -14,17 +6,13 @@ export class ScormDataModel { this.dataModel = {}; } - public init() { - console.log(this.msgPrefix + 'initializing DataModel with default values'); - this.dataModel = this.initState; - } - - public restore(dataModel: {[key: string]: any} = {}) { + public restore(dataModel: string) { console.log(this.msgPrefix + 'restoring DataModel with provided data'); - this.dataModel = dataModel; + this.dataModel = JSON.parse(dataModel); } public get(key: string): string { + // console.log(`SCORM DataModel: get ${key} ${this.dataModel[key]}`); return this.dataModel[key] ?? ''; } @@ -33,23 +21,27 @@ export class ScormDataModel { } public set(key: string, value: any): string { - console.log(this.msgPrefix + 'set: ', key, value); + // console.log(this.msgPrefix + 'set: ', key, value); this.dataModel[key] = value; if (key.match('cmi.interactions.\\d+.id')) { + // cmi.interactions._count must be incremented after a new interaction is crated const interactionPath = key.match('cmi.interactions.\\d+'); const objectivesCounterForInteraction = interactionPath.toString() + '.objectives._count'; console.log('Incrementing cmi.interactions._count'); this.dataModel['cmi.interactions._count']++; + // cmi.interactions.n.objectives._count must be initialized after an interaction is created console.log(`Initializing ${objectivesCounterForInteraction}`); this.dataModel[objectivesCounterForInteraction] = 0; } if (key.match('cmi.interactions.\\d+.objectives.\\d+.id')) { const interactionPath = key.match('cmi.interactions.\\d+.objectives'); const objectivesCounterForInteraction = interactionPath.toString() + '._count'; + // cmi.interactions.n.objectives._count must be incremented after objective creation console.log(`Incrementing ${objectivesCounterForInteraction}`); this.dataModel[objectivesCounterForInteraction.toString()]++; } if (key.match('cmi.objectives.\\d+.id')) { + // cmi.objectives._count must be incremented after a new objective is crated console.log('Incrementing cmi.objectives._count'); this.dataModel['cmi.objectives._count']++; } diff --git a/src/app/api/models/scorm-player-context.ts b/src/app/api/models/scorm-player-context.ts index 5f1c4bd3ba..f3b3b8a13f 100644 --- a/src/app/api/models/scorm-player-context.ts +++ b/src/app/api/models/scorm-player-context.ts @@ -1,9 +1,56 @@ -import { Task, User } from 'src/app/api/models/doubtfire-model'; +import {Task, User} from 'src/app/api/models/doubtfire-model'; + +type DataModelState = 'Uninitialized' | 'Initialized' | 'Terminated'; + +type DataModelError = Record; +const CMIErrorCodes: DataModelError = { + 0: 'No Error', + 101: 'General Exception', + 102: 'General Initialization Failure', + 103: 'Already Initialized', + 104: 'Content Instance Terminated', + 111: 'General Termination Failure', + 112: 'Termination Before Initialization', + 113: 'Termination After Termination', + 122: 'Retrieve Data Before Initialization', + 123: 'Retrieve Data After Termination', + 132: 'Store Data Before Initialization', + 133: 'Store Data After Termination', + 142: 'Commit Before Initialization', + 143: 'Commit After Termination', + 201: 'General Argument Error', + 301: 'General Get Failure', + 351: 'General Set Failure', + 391: 'General Commit Failure', + 401: 'Undefined Data Model Element', + 402: 'Unimplemented Data Model Element', + 403: 'Data Model Element Value Not Initialized', + 404: 'Data Model Element Is Read Only', + 405: 'Data Model Element Is Write Only', + 406: 'Data Model Element Type Mismatch', + 407: 'Data Model Element Value Out Of Range', + 408: 'Data Model Dependency Not Established', +}; export class ScormPlayerContext { - task: Task; mode: 'browse' | 'normal' | 'review'; + state: DataModelState; + + private _errorCode: number; + get errorCode() { + return this._errorCode; + } + set errorCode(value: number) { + this._errorCode = value; + } + + getErrorMessage(value: string): string { + return CMIErrorCodes[value]; + } + + task: Task; user: User; + attemptNumber: number; attemptId: number; learnerName: string; @@ -13,21 +60,7 @@ export class ScormPlayerContext { this.user = user; this.learnerId = user.id; this.learnerName = user.firstName + ' ' + user.lastName; - } - - public setTask(task: Task): void { - this.task = task; - } - - public setMode(mode: 'browse' | 'normal' | 'review'): void { - this.mode = mode; - } - - public setAttemptNumber(attemptNumber: number = 1): void { - this.attemptNumber = attemptNumber; - } - - public setAttemptId(attemptId: number): void { - this.attemptId = attemptId; + this.state = 'Uninitialized'; + this.errorCode = 0; } } diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index a669b2fe36..985b810137 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -31,10 +31,10 @@ export class TaskDefinition extends Entity { groupSet: GroupSet = null; hasTaskSheet: boolean; hasTaskResources: boolean; - hasEnabledNumbasTest: boolean; - hasNumbasData: boolean; - hasNumbasTimeDelay: boolean; - numbasAttemptLimit: number = 0; + scormEnabled: boolean; + hasScormData: boolean; + scormTimeDelayEnabled: boolean; + scormAttemptLimit: number = 0; hasTaskAssessmentResources: boolean; isGraded: boolean; maxQualityPts: number; @@ -158,7 +158,7 @@ export class TaskDefinition extends Entity { public getNumbasTestUrl(asAttachment: boolean = false) { const constants = AppInjector.get(DoubtfireConstants); - return `${constants.API_URL}/units/${this.unit.id}/task_definitions/${this.id}/numbas_data.json${ + return `${constants.API_URL}/units/${this.unit.id}/task_definitions/${this.id}/scorm_data.json${ asAttachment ? '?as_attachment=true' : '' }`; } @@ -190,7 +190,7 @@ export class TaskDefinition extends Entity { public get numbasTestUploadUrl(): string { return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.unit.id}/task_definitions/${ this.id - }/numbas_data`; + }/scorm_data`; } public get taskAssessmentResourcesUploadUrl(): string { @@ -217,7 +217,7 @@ export class TaskDefinition extends Entity { public deleteNumbasTest(): Observable { const httpClient = AppInjector.get(HttpClient); - return httpClient.delete(this.numbasTestUploadUrl).pipe(tap(() => (this.hasNumbasData = false))); + return httpClient.delete(this.numbasTestUploadUrl).pipe(tap(() => (this.hasScormData = false))); } public deleteTaskAssessmentResources(): Observable { diff --git a/src/app/api/models/task.ts b/src/app/api/models/task.ts index 403165cfee..3803d325b6 100644 --- a/src/app/api/models/task.ts +++ b/src/app/api/models/task.ts @@ -509,7 +509,7 @@ export class Task extends Entity { public get numbasEnabled(): boolean { return ( - this.definition.hasEnabledNumbasTest && this.definition.hasNumbasData + this.definition.scormEnabled && this.definition.hasScormData ); } diff --git a/src/app/api/services/scorm-adapter.service.ts b/src/app/api/services/scorm-adapter.service.ts index 6c5353746b..8886121031 100644 --- a/src/app/api/services/scorm-adapter.service.ts +++ b/src/app/api/services/scorm-adapter.service.ts @@ -1,222 +1,212 @@ -import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; -import { UserService } from './user.service'; +import {Injectable} from '@angular/core'; +import {UserService} from './user.service'; import API_URL from 'src/app/config/constants/apiURL'; -import { Task, ScormDataModel, ScormPlayerContext } from 'src/app/api/models/doubtfire-model'; +import {Task, ScormDataModel, ScormPlayerContext} from 'src/app/api/models/doubtfire-model'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ScormAdapterService { private readonly apiBaseUrl = `${API_URL}/test_attempts`; private dataModel: ScormDataModel; - private playerContext: ScormPlayerContext; - - initializationComplete$ = new BehaviorSubject(false); - - private scormErrorCodes: {[key: string]: string} = { - '0': 'No Error', - '101': 'General Exception', - '102': 'General Initialization Failure', - '103': 'Already Initialized', - '104': 'Content Instance Terminated', - '111': 'General Termination Failure', - '112': 'Termination Before Initialization', - '113': 'Termination After Termination', - '122': 'Retrieve Data Before Initialization', - '123': 'Retrieve Data After Termination', - '132': 'Store Data Before Initialization', - '133': 'Store Data After Termination', - '142': 'Commit Before Initialization', - '143': 'Commit After Termination', - '201': 'General Argument Error', - '301': 'General Get Failure', - '351': 'General Set Failure', - '391': 'General Commit Failure', - '401': 'Undefined Data Model Element', - '402': 'Unimplemented Data Model Element', - '403': 'Data Model Element Value Not Initialized', - '404': 'Data Model Element Is Read Only', - '405': 'Data Model Element Is Write Only', - '406': 'Data Model Element Type Mismatch', - '407': 'Data Model Element Value Out Of Range', - '408': 'Data Model Dependency Not Established', - }; + private context: ScormPlayerContext; + private xhr: XMLHttpRequest; constructor(private userService: UserService) { this.dataModel = new ScormDataModel(); - this.playerContext = new ScormPlayerContext(this.userService.currentUser); + this.context = new ScormPlayerContext(this.userService.currentUser); + this.xhr = new XMLHttpRequest(); } - setTask(task: Task) { - this.playerContext.setTask(task); + set task(task: Task) { + this.context.task = task; } - // getDefaultDataStore() { - // // Use spread operator to merge defaultValues into the dataStore - // return { - // ...this.defaultValues, - // pass_status: false, - // completed: false, - // }; - // } - - Initialize(mode: 'attempt' | 'review' = 'attempt'): string { - console.log('Initialize() function called'); - const xhr = new XMLHttpRequest(); - if (mode === 'review') { - this.SetValue('cmi.mode', 'review'); - - xhr.open( - 'GET', - `${this.apiBaseUrl}/completed-latest?task_id=${this.playerContext.task.id}`, - false, - ); - xhr.send(); - console.log(xhr.responseText); + get state() { + return this.context.state; + } - if (xhr.status !== 200) { - console.error('Error fetching latest completed test result:', xhr.statusText); - return 'false'; - } + destroy() { + this.dataModel = new ScormDataModel(); + this.context.state = 'Uninitialized'; + } - try { - const completedTest = JSON.parse(xhr.responseText); - const parsedDataModel = JSON.parse(completedTest.data.suspend_data ?? '{}'); + Initialize(): string { + console.log('API_1484_11: Initialize'); + + // TODO: error handling and reporting + switch (this.context.state) { + case 'Initialized': + this.context.errorCode = 103; + console.log('Already Initialized'); + break; + case 'Terminated': + this.context.errorCode = 104; + console.log('Content Instance Terminated'); + break; + } - // Set entire suspendData string to cmi.suspend_data - this.SetValue('cmi.suspend_data', JSON.stringify(parsedDataModel)); + // TODO: move this part into the player component + this.xhr.open('GET', `${this.apiBaseUrl}/${this.context.task.id}/latest`, false); - // // Use SetValue to set parsedSuspendData values to dataStore - // Object.keys(parsedDataModel).forEach((key) => { - // this.SetValue(key, parsedDataModel[key]); - // }); + let noTestFound = false; + let startNewTest = false; - this.dataModel.restore(parsedDataModel); + this.xhr.onload = () => { + if (this.xhr.status >= 200 && this.xhr.status < 400) { + console.log('Retrieved the latest attempt.'); + } else if (this.xhr.status == 404) { + console.log('Not found.'); + noTestFound = true; + } else { + console.error('Error saving DataModel:', this.xhr.responseText); + } + }; - this.SetValue('cmi.entry', 'RO'); - this.SetValue('cmi.mode', 'review'); + this.xhr.send(); + console.log(this.xhr.responseText); - console.log('Latest completed test data:', completedTest); - return 'true'; - } catch (error) { - console.error('Error:', error); - return 'false'; + if (!noTestFound) { + const latestSession = JSON.parse(this.xhr.responseText); + console.log('Latest exam session:', latestSession); + this.context.attemptId = latestSession.id; + if (latestSession.completion_status) { + startNewTest = true; } + } else { + startNewTest = true; } - xhr.open('GET', `${this.apiBaseUrl}/latest?task_id=${this.playerContext.task.id}`, false); - xhr.send(); - console.log(xhr.responseText); - - if (xhr.status !== 200) { - console.error('Error fetching latest test result:', xhr.statusText); - return 'false'; + if (!startNewTest) { + this.xhr.open( + 'PATCH', + `${this.apiBaseUrl}/${this.context.task.id}/session/${this.context.attemptId}`, + false, + ); + this.xhr.send(); + console.log(this.xhr.responseText); + + const currentSession = JSON.parse(this.xhr.responseText); + console.log('Current exam session:', currentSession); + this.context.attemptId = currentSession.id; + this.dataModel.restore(currentSession.cmi_datamodel); + console.log(this.dataModel.dump()); + } else { + this.xhr.open('POST', `${this.apiBaseUrl}/${this.context.task.id}/session`, false); + this.xhr.send(); + console.log(this.xhr.responseText); + + const currentSession = JSON.parse(this.xhr.responseText); + console.log('Current exam session:', currentSession); + this.context.attemptId = currentSession.id; + this.dataModel.restore(currentSession.cmi_datamodel); + console.log(this.dataModel.dump()); } - let latestTest; - try { - latestTest = JSON.parse(xhr.responseText); - console.log('Latest test result:', latestTest); - this.playerContext.attemptId = latestTest.data.id; - - if (latestTest.data['cmi_entry'] === 'ab-initio') { - console.log('starting new test'); - this.dataModel.init(); - this.SetValue('cmi.learner_id', this.playerContext.learnerId); - this.SetValue('cmi.learner_name', this.playerContext.learnerName); - - this.dataModel.set('attempt_number', latestTest.data['attempt_number']); - console.log(this.dataModel.dump()); - } else if (latestTest.data['cmi_entry'] === 'resume') { - console.log('resuming test'); - const restoredDataModel = JSON.parse(latestTest.data.suspend_data ?? '{}'); - this.dataModel.restore(JSON.parse(JSON.stringify(restoredDataModel))); - - console.log(this.dataModel.dump()); - } - - this.initializationComplete$.next(true); - - console.log('finished initializing'); - return 'true'; - } catch (error) { - console.error('Error:', error); - return 'false'; - } + this.context.state = 'Initialized'; + return 'true'; } - // isTestCompleted(): boolean { - // return this.dataModel.get('completed') ?? false; - // } - Terminate(): string { - console.log('Terminate Called'); - const examResult = this.dataModel.get('cmi.score.raw'); - const status = this.dataModel.get('cmi.success_status'); - this.dataModel.set('completed', true); - const currentAttemptNumber = this.dataModel.get('attempt_number') ?? 0; - const ExamName = this.dataModel.get('name'); - this.dataModel.set('cmi.entry', 'RO'); - const cmientry = this.dataModel.get('cmi.entry'); - const data = { - name: ExamName, - attempt_number: currentAttemptNumber, - pass_status: status === 'passed', - suspend_data: JSON.stringify(this.dataModel.dump()), - completed: true, - exam_result: examResult, - cmi_entry: cmientry, - task_id: this.playerContext.task.id - }; - - const xhr = new XMLHttpRequest(); - if (this.playerContext.attemptId) { - xhr.open('PUT', `${this.apiBaseUrl}/${this.playerContext.attemptId}`, false); - } else { - xhr.open('POST', this.apiBaseUrl, false); + console.log('API_1484_11: Terminate'); + + // TODO: error handling and reporting + switch (this.context.state) { + case 'Uninitialized': + this.context.errorCode = 112; + console.log('Termination Before Initialization'); + break; + case 'Terminated': + this.context.errorCode = 113; + console.log('Termination After Termination'); + break; } - xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); - xhr.send(JSON.stringify(data)); - if (xhr.status !== 200) { - console.error('Error sending test data:', xhr.statusText); - return 'false'; - } - this.dataModel.init(); + this.xhr.open( + 'PATCH', + `${this.apiBaseUrl}/${this.context.task.id}/session/${this.context.attemptId}`, + false, + ); + this.xhr.setRequestHeader('Content-Type', 'application/json'); + const requestData = { + terminated: true, + }; + this.xhr.send(JSON.stringify(requestData)); + console.log(this.xhr.responseText); + + // all done, clearing datamodel and setting state to terminated + this.dataModel = new ScormDataModel(); + this.context.state = 'Terminated'; return 'true'; } GetValue(element: string): string { const value = this.dataModel.get(element); - console.log(`GetValue:`, element, value); + + // TODO: error reporting + // TODO: can't get until init is done + switch (this.context.state) { + case 'Uninitialized': + this.context.errorCode = 122; + console.log('Retrieve Data Before Initialization'); + break; + case 'Terminated': + this.context.errorCode = 123; + console.log('Retrieve Data After Termination'); + break; + } + + console.log(`API_1484_11: GetValue:`, element, value); return value; } SetValue(element: string, value: any): string { - console.log(`SetValue:`, element, value); + console.log(`API_1484_11: SetValue:`, element, value); + + // TODO: error reporting + // TODO: can't set until init is done + switch (this.context.state) { + case 'Uninitialized': + this.context.errorCode = 132; + console.log('Store Data Before Initialization'); + break; + case 'Terminated': + this.context.errorCode = 133; + console.log('Store Data After Termination'); + break; + } + this.dataModel.set(element, value); return 'true'; } - // Saves the state of the exam. Commit(): string { - if (!this.initializationComplete$.getValue()) { - console.warn('Initialization not complete. Cannot commit.'); - return 'false'; + console.log('API_1484_11: Commit'); + + // TODO: error reporting + // TODO: can't commit until init is done + switch (this.context.state) { + case 'Uninitialized': + this.context.errorCode = 142; + console.log('Commit Before Initialization'); + break; + case 'Terminated': + this.context.errorCode = 143; + console.log('Commit After Termination'); + break; } - // Set cmi.entry to 'resume' before committing dataStore - this.dataModel.set('cmi.entry', 'resume'); - // if (!this.isTestCompleted()) { - // this.dataModel.set('cmi.exit', 'suspend'); - // } - console.log('Committing DataModel:', this.dataModel.dump()); - - // Use XHR to send the request const xhr = new XMLHttpRequest(); - xhr.open('PUT', `${this.apiBaseUrl}/${this.playerContext.attemptId}/suspend`, true); + xhr.open( + 'PATCH', + `${this.apiBaseUrl}/${this.context.task.id}/session/${this.context.attemptId}`, + true, + ); xhr.setRequestHeader('Content-Type', 'application/json'); + const requestData = { + cmi_datamodel: JSON.stringify(this.dataModel.dump()), + }; + xhr.send(JSON.stringify(requestData)); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 400) { @@ -230,23 +220,27 @@ export class ScormAdapterService { console.error('Request failed.'); }; - const requestData = { suspend_data: this.dataModel.dump() }; - xhr.send(JSON.stringify(requestData)); + this.context.errorCode = 0; return 'true'; } - // Placeholder methods for SCORM error handling GetLastError(): string { - //console.log('Get Last Error called'); - return "0"; + const lastError = this.context.errorCode.toString(); + if (lastError !== '0') { + console.log(`API_1484_11: GetLastError: ${lastError}`); + } + return lastError; } GetErrorString(errorCode: string): string { - return ''; + const errorString = this.context.getErrorMessage(errorCode); + console.log(`API_1484_11: GetErrorString:`, errorCode, errorString); + return errorString; } GetDiagnostic(errorCode: string): string { - //console.log('Get Diagnoistic called'); - return ''; + // TODO: implement this + console.log(`API_1484_11: GetDiagnostic:`, errorCode); + return 'GetDiagnostic is currently not implemented'; } } diff --git a/src/app/api/services/task-comment.service.ts b/src/app/api/services/task-comment.service.ts index e3c80797a4..5f034b870d 100644 --- a/src/app/api/services/task-comment.service.ts +++ b/src/app/api/services/task-comment.service.ts @@ -145,7 +145,7 @@ export class TaskCommentService extends CachedEntityService { const opts: RequestOptions = { endpointFormat: this.commentEndpointFormat }; // Based on the comment type - add to the body and configure the end point - if (commentType === 'text' || commentType === 'numbas') { + if (commentType === 'text' || commentType === 'scorm') { body.append('comment', data); } else if (commentType === 'discussion') { opts.endpointFormat = this.discussionEndpointFormat; diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index 6047af0546..a738626884 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -93,10 +93,10 @@ export class TaskDefinitionService extends CachedEntityService { 'hasTaskSheet', 'hasTaskResources', 'hasTaskAssessmentResources', - 'hasEnabledNumbasTest', - 'hasNumbasData', - 'hasNumbasTimeDelay', - 'numbasAttemptLimit', + 'scormEnabled', + 'hasScormData', + 'scormTimeDelayEnabled', + 'scormAttemptLimit', 'isGraded', 'maxQualityPts', 'overseerImageId', @@ -108,7 +108,7 @@ export class TaskDefinitionService extends CachedEntityService { 'hasTaskSheet', 'hasTaskResources', 'hasTaskAssessmentResources', - 'hasNumbasData' + 'hasScormData' ); } diff --git a/src/app/common/scorm-player/scorm-player-modal.component.ts b/src/app/common/scorm-player/scorm-player-modal.component.ts index 190e7bf570..2fb4630263 100644 --- a/src/app/common/scorm-player/scorm-player-modal.component.ts +++ b/src/app/common/scorm-player/scorm-player-modal.component.ts @@ -1,20 +1,21 @@ -import { Injectable } from '@angular/core'; -import { MatDialog, MatDialogRef } from '@angular/material/dialog'; -import { ScormPlayerComponent } from './scorm-player.component'; -import { Task } from 'src/app/api/models/task'; +import {Injectable} from '@angular/core'; +import {MatDialog, MatDialogRef} from '@angular/material/dialog'; +import {ScormPlayerComponent} from './scorm-player.component'; +import {Task} from 'src/app/api/models/task'; @Injectable({ providedIn: 'root', }) export class ScormPlayerModal { - constructor(public dialog: MatDialog) { } + constructor(public dialog: MatDialog) {} public show(task: Task, mode: 'attempt' | 'review'): void { let dialogRef: MatDialogRef; dialogRef = this.dialog.open(ScormPlayerComponent, { - data: { task, mode }, - width: '95%', height: '90%', + data: {task, mode}, + width: '95%', + height: '90%', disableClose: true, }); } diff --git a/src/app/common/scorm-player/scorm-player.component.ts b/src/app/common/scorm-player/scorm-player.component.ts index 428f6869b7..1b89e36bae 100644 --- a/src/app/common/scorm-player/scorm-player.component.ts +++ b/src/app/common/scorm-player/scorm-player.component.ts @@ -1,13 +1,15 @@ -import { Component, OnInit, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; -import { Task, ScormDataModel, ScormPlayerContext } from 'src/app/api/models/doubtfire-model'; -import { ScormAdapterService } from 'src/app/api/services/scorm-adapter.service'; -import { AppInjector } from 'src/app/app-injector'; -import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; +import {Component, OnInit, Inject} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser'; +import {Task, ScormPlayerContext} from 'src/app/api/models/doubtfire-model'; +import {ScormAdapterService} from 'src/app/api/services/scorm-adapter.service'; +import {AppInjector} from 'src/app/app-injector'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; declare global { - interface Window { API_1484_11: any; } + interface Window { + API_1484_11: any; + } } @Component({ @@ -24,34 +26,41 @@ export class ScormPlayerComponent implements OnInit { constructor( private dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: { task: Task, mode: 'attempt' | 'review' }, + @Inject(MAT_DIALOG_DATA) public data: {task: Task; mode: 'attempt' | 'review'}, private scormAdapter: ScormAdapterService, - private sanitizer: DomSanitizer + private sanitizer: DomSanitizer, ) {} ngOnInit(): void { this.task = this.data.task; - this.scormAdapter.setTask(this.task); + this.scormAdapter.task = this.task; window.API_1484_11 = { - Initialize: () => this.scormAdapter.Initialize(this.currentMode), + Initialize: () => this.scormAdapter.Initialize(), Terminate: () => this.scormAdapter.Terminate(), GetValue: (element: string) => this.scormAdapter.GetValue(element), SetValue: (element: string, value: string) => this.scormAdapter.SetValue(element, value), Commit: () => this.scormAdapter.Commit(), GetLastError: () => this.scormAdapter.GetLastError(), GetErrorString: (errorCode: string) => this.scormAdapter.GetErrorString(errorCode), - GetDiagnostic: (errorCode: string) => this.scormAdapter.GetDiagnostic(errorCode) + GetDiagnostic: (errorCode: string) => this.scormAdapter.GetDiagnostic(errorCode), }; this.currentMode = this.data.mode; - this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(`${AppInjector.get(DoubtfireConstants).API_URL}/numbas_api/${this.task.taskDefId}/index.html`); + this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl( + `${AppInjector.get(DoubtfireConstants).API_URL}/scorm/${this.task.taskDefId}/index.html`, + ); } close(): void { - console.log('SCORM player closing, commiting DataModel!'); - this.scormAdapter.Commit(); + if (this.scormAdapter.state == 'Initialized') { + console.log('SCORM player closing during an initialized session, commiting DataModel'); + this.scormAdapter.Commit(); + } + // TODO: would be nice if we can destroy this entire adapter object when the modal is closed + console.log('Clearing player context and DataModel'); + this.scormAdapter.destroy(); const iframe = document.getElementsByTagName('iframe')[0]; iframe?.parentNode?.removeChild(iframe); this.dialogRef.close(); diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee index 6a17bd558c..7c0baf7ee9 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee @@ -128,7 +128,7 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) removed.push('group') if !isRFF || !task.isGroupTask() removed.push('alignment') if !isRFF || !task.unit.ilos.length > 0 removed.push('comments') if isTestSubmission - removed.push('scorm-assessment') if !isRFF || !task.definition.hasEnabledNumbasTest + removed.push('scorm-assessment') if !isRFF || !task.definition.scormEnabled removed # Initialises the states initialise: -> diff --git a/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.ts b/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.ts index 650a110d0f..c8ba676c55 100644 --- a/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.ts +++ b/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.ts @@ -1,6 +1,6 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { Task, TaskComment } from 'src/app/api/models/doubtfire-model'; -import { ScormPlayerModal } from 'src/app/common/scorm-player/scorm-player-modal.component'; +import {Component, OnInit, Input} from '@angular/core'; +import {Task, TaskComment} from 'src/app/api/models/doubtfire-model'; +import {ScormPlayerModal} from 'src/app/common/scorm-player/scorm-player-modal.component'; @Component({ selector: 'numbas-comment', diff --git a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html index cd18eda9ba..0be0e7b758 100644 --- a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html +++ b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html @@ -72,7 +72,7 @@ >
-
+
- + Enable Numbas Test @@ -10,7 +10,7 @@ accept="application/zip" [desiredFileName]="'Numbas zip'" /> - @if (taskDefinition.hasNumbasData) { + @if (taskDefinition.hasScormData) {
- @if (taskDefinition.hasEnabledNumbasTest) { + @if (taskDefinition.scormEnabled) {
- + Enable incremental time delays between test attempts @@ -34,7 +34,7 @@ min="0" max="100" type="number" - [(ngModel)]="taskDefinition.numbasAttemptLimit" + [(ngModel)]="taskDefinition.scormAttemptLimit" [formControl]="attemptLimitControl" /> From 644c025e698480276a068d717589be547817ddc8 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 14 May 2024 13:18:33 +1000 Subject: [PATCH 046/776] refactor: rename numbas references to scorm and fix typos --- src/app/api/models/scorm-datamodel.ts | 4 +- src/app/api/models/task-definition.ts | 8 +- src/app/api/models/task.ts | 2 +- .../spec/scorm-adapter.service.spec.ts | 87 ------------------- .../api/services/task-definition.service.ts | 4 +- .../scorm-player-modal.component.ts | 2 +- .../scorm-player/scorm-player.component.ts | 4 +- src/app/doubtfire-angular.module.ts | 8 +- .../upload-submission-modal.coffee | 2 +- .../upload-submission-modal.tpl.html | 6 +- .../scorm-comment.component.html} | 4 +- .../scorm-comment.component.scss} | 0 .../scorm-comment.component.ts} | 10 +-- .../task-comments-viewer.component.html | 8 +- .../task-comments-viewer.component.ts | 6 +- .../task-definition-editor.component.html | 6 +- .../task-definition-scorm.component.html} | 14 +-- .../task-definition-scorm.component.scss} | 0 .../task-definition-scorm.component.ts} | 32 +++---- 19 files changed, 60 insertions(+), 147 deletions(-) delete mode 100644 src/app/api/services/spec/scorm-adapter.service.spec.ts rename src/app/tasks/task-comments-viewer/{numbas-comment/numbas-comment.component.html => scorm-comment/scorm-comment.component.html} (65%) rename src/app/tasks/task-comments-viewer/{numbas-comment/numbas-comment.component.scss => scorm-comment/scorm-comment.component.scss} (100%) rename src/app/tasks/task-comments-viewer/{numbas-comment/numbas-comment.component.ts => scorm-comment/scorm-comment.component.ts} (66%) rename src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/{task-definition-numbas/task-definition-numbas.component.html => task-definition-scorm/task-definition-scorm.component.html} (78%) rename src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/{task-definition-numbas/task-definition-numbas.component.scss => task-definition-scorm/task-definition-scorm.component.scss} (100%) rename src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/{task-definition-numbas/task-definition-numbas.component.ts => task-definition-scorm/task-definition-scorm.component.ts} (58%) diff --git a/src/app/api/models/scorm-datamodel.ts b/src/app/api/models/scorm-datamodel.ts index 9454db78f6..7559318eb5 100644 --- a/src/app/api/models/scorm-datamodel.ts +++ b/src/app/api/models/scorm-datamodel.ts @@ -24,7 +24,7 @@ export class ScormDataModel { // console.log(this.msgPrefix + 'set: ', key, value); this.dataModel[key] = value; if (key.match('cmi.interactions.\\d+.id')) { - // cmi.interactions._count must be incremented after a new interaction is crated + // cmi.interactions._count must be incremented after a new interaction is created const interactionPath = key.match('cmi.interactions.\\d+'); const objectivesCounterForInteraction = interactionPath.toString() + '.objectives._count'; console.log('Incrementing cmi.interactions._count'); @@ -41,7 +41,7 @@ export class ScormDataModel { this.dataModel[objectivesCounterForInteraction.toString()]++; } if (key.match('cmi.objectives.\\d+.id')) { - // cmi.objectives._count must be incremented after a new objective is crated + // cmi.objectives._count must be incremented after a new objective is created console.log('Incrementing cmi.objectives._count'); this.dataModel['cmi.objectives._count']++; } diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index 985b810137..538b449338 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -156,7 +156,7 @@ export class TaskDefinition extends Entity { }`; } - public getNumbasTestUrl(asAttachment: boolean = false) { + public getScormDataUrl(asAttachment: boolean = false) { const constants = AppInjector.get(DoubtfireConstants); return `${constants.API_URL}/units/${this.unit.id}/task_definitions/${this.id}/scorm_data.json${ asAttachment ? '?as_attachment=true' : '' @@ -187,7 +187,7 @@ export class TaskDefinition extends Entity { }/task_resources`; } - public get numbasTestUploadUrl(): string { + public get scormDataUploadUrl(): string { return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.unit.id}/task_definitions/${ this.id }/scorm_data`; @@ -215,9 +215,9 @@ export class TaskDefinition extends Entity { return httpClient.delete(this.taskResourcesUploadUrl).pipe(tap(() => (this.hasTaskResources = false))); } - public deleteNumbasTest(): Observable { + public deleteScormData(): Observable { const httpClient = AppInjector.get(HttpClient); - return httpClient.delete(this.numbasTestUploadUrl).pipe(tap(() => (this.hasScormData = false))); + return httpClient.delete(this.scormDataUploadUrl).pipe(tap(() => (this.hasScormData = false))); } public deleteTaskAssessmentResources(): Observable { diff --git a/src/app/api/models/task.ts b/src/app/api/models/task.ts index 3803d325b6..556d0a7dd2 100644 --- a/src/app/api/models/task.ts +++ b/src/app/api/models/task.ts @@ -507,7 +507,7 @@ export class Task extends Entity { ); } - public get numbasEnabled(): boolean { + public get scormEnabled(): boolean { return ( this.definition.scormEnabled && this.definition.hasScormData ); diff --git a/src/app/api/services/spec/scorm-adapter.service.spec.ts b/src/app/api/services/spec/scorm-adapter.service.spec.ts deleted file mode 100644 index 5d3a52caac..0000000000 --- a/src/app/api/services/spec/scorm-adapter.service.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { ScormAdapterService } from '../scorm-adapter.service'; -import { TaskService } from '../task.service'; -import { UserService } from '../user.service'; - -describe('ScormAdapterService', () => { - let service: ScormAdapterService; - let httpTestingController: HttpTestingController; - let mockUserService: Partial; - let mockTaskService: Partial; - - const mockUserData = { - currentUser: { studentId: '12345' } - }; - - beforeEach(() => { - mockUserService = { - currentUser: mockUserData.currentUser - }; - - mockTaskService = { - // you can add mocked methods if needed for the TaskService - }; - - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [ - ScormAdapterService, - { provide: UserService, useValue: mockUserService }, - { provide: TaskService, useValue: mockTaskService } - ] - }); - - service = TestBed.inject(ScormAdapterService); - httpTestingController = TestBed.inject(HttpTestingController); - }); - - afterEach(() => { - httpTestingController.verify(); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - it('should initialize with default values', () => { - expect(service.GetValue('cmi.completion_status')).toBe('not attempted'); - expect(service.GetValue('cmi.entry')).toBe('ab-initio'); - }); - - describe('Initialize function', () => { - - it('should handle review mode and get latest completed test result', () => { - const mockResponse = { - data: { - suspend_data: JSON.stringify({ someData: 'value' }) - } - }; - - service.Initialize('review'); - const req = httpTestingController.expectOne(`${service['apiBaseUrl']}/completed-latest`); - expect(req.request.method).toEqual('GET'); - req.flush(mockResponse); - - expect(service.GetValue('cmi.suspend_data')).toEqual(JSON.stringify({ someData: 'value' })); - }); - - it('should handle attempt mode and get latest test result', () => { - const mockResponse = { - data: { - id: 1, - cmi_entry: 'ab-initio', - attempt_number: 2 - } - }; - - service.Initialize('attempt'); - const req = httpTestingController.expectOne(`${service['apiBaseUrl']}/latest`); - expect(req.request.method).toEqual('GET'); - req.flush(mockResponse); - - expect(service.GetValue('cmi.learner_id')).toBe('12345'); - }); - }); - -}); diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index a738626884..defd83dc77 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -134,9 +134,9 @@ export class TaskDefinitionService extends CachedEntityService { return AppInjector.get(HttpClient).post(taskDefinition.taskAssessmentResourcesUploadUrl, formData); } - public uploadNumbasData(taskDefinition: TaskDefinition, file: File): Observable { + public uploadScormData(taskDefinition: TaskDefinition, file: File): Observable { const formData = new FormData(); formData.append('file', file); - return AppInjector.get(HttpClient).post(taskDefinition.numbasTestUploadUrl, formData); + return AppInjector.get(HttpClient).post(taskDefinition.scormDataUploadUrl, formData); } } diff --git a/src/app/common/scorm-player/scorm-player-modal.component.ts b/src/app/common/scorm-player/scorm-player-modal.component.ts index 2fb4630263..cd7740571c 100644 --- a/src/app/common/scorm-player/scorm-player-modal.component.ts +++ b/src/app/common/scorm-player/scorm-player-modal.component.ts @@ -9,7 +9,7 @@ import {Task} from 'src/app/api/models/task'; export class ScormPlayerModal { constructor(public dialog: MatDialog) {} - public show(task: Task, mode: 'attempt' | 'review'): void { + public show(task: Task, mode: 'browse' | 'normal' | 'review'): void { let dialogRef: MatDialogRef; dialogRef = this.dialog.open(ScormPlayerComponent, { diff --git a/src/app/common/scorm-player/scorm-player.component.ts b/src/app/common/scorm-player/scorm-player.component.ts index 1b89e36bae..106e6a8520 100644 --- a/src/app/common/scorm-player/scorm-player.component.ts +++ b/src/app/common/scorm-player/scorm-player.component.ts @@ -21,12 +21,12 @@ export class ScormPlayerComponent implements OnInit { context: ScormPlayerContext; task: Task; - currentMode: 'attempt' | 'review' = 'attempt'; + currentMode: 'browse' | 'normal' | 'review' = 'normal'; iframeSrc: SafeResourceUrl; constructor( private dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: {task: Task; mode: 'attempt' | 'review'}, + @Inject(MAT_DIALOG_DATA) public data: {task: Task, mode: 'browse' | 'normal' | 'review'}, private scormAdapter: ScormAdapterService, private sanitizer: DomSanitizer, ) {} diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 23145ccaea..110ae4a3a2 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -204,7 +204,7 @@ import {TaskDefinitionUploadComponent} from './units/states/edit/directives/unit import {TaskDefinitionOptionsComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-options/task-definition-options.component'; import {TaskDefinitionResourcesComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-resources/task-definition-resources.component'; import {TaskDefinitionOverseerComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component'; -import {TaskDefinitionNumbasComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component'; +import {TaskDefinitionScormComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component'; import {UnitAnalyticsComponent} from './units/states/analytics/unit-analytics-route.component'; import {FileDropComponent} from './common/file-drop/file-drop.component'; import {UnitTaskEditorComponent} from './units/states/edit/directives/unit-tasks-editor/unit-task-editor.component'; @@ -228,7 +228,7 @@ import {UnitCodeComponent} from './common/unit-code/unit-code.component'; import {GradeService} from './common/services/grade.service'; import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component'; import {ScormAdapterService} from './api/services/scorm-adapter.service'; -import {NumbasCommentComponent} from './tasks/task-comments-viewer/numbas-comment/numbas-comment.component'; +import {ScormCommentComponent} from './tasks/task-comments-viewer/scorm-comment/scorm-comment.component'; import {TestAttemptService} from './api/services/test-attempt.service'; @NgModule({ @@ -268,7 +268,7 @@ import {TestAttemptService} from './api/services/test-attempt.service'; TaskDefinitionOptionsComponent, TaskDefinitionResourcesComponent, TaskDefinitionOverseerComponent, - TaskDefinitionNumbasComponent, + TaskDefinitionScormComponent, UnitAnalyticsComponent, StudentTutorialSelectComponent, StudentCampusSelectComponent, @@ -333,7 +333,7 @@ import {TestAttemptService} from './api/services/test-attempt.service'; FTaskBadgeComponent, FUnitsComponent, ScormPlayerComponent, - NumbasCommentComponent, + ScormCommentComponent, ], // Services we provide providers: [ diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee index 7c0baf7ee9..976c116f33 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee @@ -157,7 +157,7 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) $scope.launchScormPlayer = -> console.clear() - ScormPlayerModal.show $scope.task, 'attempt' + ScormPlayerModal.show $scope.task, 'normal' # Whether or not we should disable this button $scope.shouldDisableBtn = { diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html index 8b6c89bea3..0b296c2099 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html @@ -50,15 +50,15 @@

- Attempt Numbas Test + Attempt SCORM Test

- Complete the Numbas test first to proceed to upload evidence of your task completion. + Complete the SCORM test first to proceed to upload evidence of your task completion.
diff --git a/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.html b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html similarity index 65% rename from src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.html rename to src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html index d87863bee8..b74db43a07 100644 --- a/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.html +++ b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html @@ -1,9 +1,9 @@
-
+
- +
diff --git a/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.scss b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.scss similarity index 100% rename from src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.scss rename to src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.scss diff --git a/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.ts b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts similarity index 66% rename from src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.ts rename to src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts index c8ba676c55..817a6cd64f 100644 --- a/src/app/tasks/task-comments-viewer/numbas-comment/numbas-comment.component.ts +++ b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts @@ -3,11 +3,11 @@ import {Task, TaskComment} from 'src/app/api/models/doubtfire-model'; import {ScormPlayerModal} from 'src/app/common/scorm-player/scorm-player-modal.component'; @Component({ - selector: 'numbas-comment', - templateUrl: './numbas-comment.component.html', - styleUrls: ['./numbas-comment.component.scss'], + selector: 'scorm-comment', + templateUrl: './scorm-comment.component.html', + styleUrls: ['./scorm-comment.component.scss'], }) -export class NumbasCommentComponent implements OnInit { +export class ScormCommentComponent implements OnInit { @Input() task: Task; @Input() comment: TaskComment; @@ -15,7 +15,7 @@ export class NumbasCommentComponent implements OnInit { ngOnInit() {} - reviewNumbasTest() { + reviewScormTest() { this.modalService.show(this.task, 'review'); } } diff --git a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html index 0be0e7b758..aa20275ff6 100644 --- a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html +++ b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html @@ -72,12 +72,12 @@ >
-
- + + *ngIf="scormEnabled" + >
diff --git a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.ts b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.ts index 7436cd7d79..8ad719f339 100644 --- a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.ts +++ b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.ts @@ -98,8 +98,8 @@ export class TaskCommentsViewerComponent implements OnChanges, OnInit { return this.constants.IsOverseerEnabled.value; } - get numbasEnabled(): boolean { - return this.task.numbasEnabled; + get scormEnabled(): boolean { + return this.task.scormEnabled; } uploadFiles(event) { @@ -154,7 +154,7 @@ export class TaskCommentsViewerComponent implements OnChanges, OnInit { } shouldShowAuthorIcon(commentType: string) { - return !(commentType === 'extension' || commentType === 'status' || commentType == 'assessment' || commentType == 'numbas'); + return !(commentType === 'extension' || commentType === 'status' || commentType == 'assessment' || commentType == 'scorm'); } commentClasses(comment: TaskComment): object { 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 bd2c7ec65d..6bc386569e 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 @@ -105,10 +105,10 @@

-

Upload Numbas test

-

Upload the corresponding Numbas test

+

Upload SCORM test

+

Upload the corresponding SCORM 2004 test (e.g. Numbas)

- +

diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.html similarity index 78% rename from src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html rename to src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.html index 36acb4ca4c..433405669b 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.html @@ -1,22 +1,22 @@
- Enable Numbas Test + Enable test for task
@if (taskDefinition.hasScormData) {
- -
} diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.scss b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.scss similarity index 100% rename from src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.scss rename to src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.scss diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.ts similarity index 58% rename from src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts rename to src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.ts index f6687a4af9..8f2b72fcb9 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-numbas/task-definition-numbas.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.ts @@ -7,11 +7,11 @@ import { TaskDefinitionService } from 'src/app/api/services/task-definition.serv import { FileDownloaderService } from 'src/app/common/file-downloader/file-downloader.service'; @Component({ - selector: 'f-task-definition-numbas', - templateUrl: 'task-definition-numbas.component.html', - styleUrls: ['task-definition-numbas.component.scss'], + selector: 'f-task-definition-scorm', + templateUrl: 'task-definition-scorm.component.html', + styleUrls: ['task-definition-scorm.component.scss'], }) -export class TaskDefinitionNumbasComponent { +export class TaskDefinitionScormComponent { @Input() taskDefinition: TaskDefinition; constructor( @@ -26,32 +26,32 @@ export class TaskDefinitionNumbasComponent { return this.taskDefinition?.unit; } - public downloadNumbasTest() { + public downloadScormData() { this.fileDownloaderService.downloadFile( - this.taskDefinition.getNumbasTestUrl(true), - this.taskDefinition.name + '-Numbas.zip', + this.taskDefinition.getScormDataUrl(true), + this.taskDefinition.name + '-SCORM.zip', ); } - public removeNumbasTest() { - this.taskDefinition.deleteNumbasTest().subscribe({ - next: () => this.alerts.add('success', 'Deleted Numbas test', 2000), - error: (message) => this.alerts.add('danger', message, 6000), + public removeScormData() { + this.taskDefinition.deleteScormData().subscribe({ + next: () => this.alerts.success('Deleted SCORM test data', 2000), + error: (message) => this.alerts.error(message, 6000), }); } - public uploadNumbasTest(files: FileList) { + public uploadScormData(files: FileList) { console.log(Array.from(files).map(f => f.type)); const validMimeTypes = ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip']; const validFiles = Array.from(files as ArrayLike).filter(f => validMimeTypes.includes(f.type)); if (validFiles.length > 0) { const file = validFiles[0]; - this.taskDefinitionService.uploadNumbasData(this.taskDefinition, file).subscribe({ - next: () => this.alerts.add('success', 'Uploaded Numbas test data', 2000), - error: (message) => this.alerts.add('danger', message, 6000), + this.taskDefinitionService.uploadScormData(this.taskDefinition, file).subscribe({ + next: () => this.alerts.success('Uploaded SCORM test data', 2000), + error: (message) => this.alerts.error(message, 6000), }); } else { - this.alerts.add('danger', 'Please drop a zip file to upload Numbas test data for this task', 6000); + this.alerts.error('Please drop a zip file to upload SCORM test data for this task', 6000); } } } From 2ac487f13c3743f2b5b805521964bdcbca7cdc15 Mon Sep 17 00:00:00 2001 From: ublefo <90136978+ublefo@users.noreply.github.com> Date: Tue, 14 May 2024 13:25:23 +1000 Subject: [PATCH 047/776] fix: ensure datamodel is updated on termination --- src/app/api/services/scorm-adapter.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/api/services/scorm-adapter.service.ts b/src/app/api/services/scorm-adapter.service.ts index 8886121031..9519f4cc2c 100644 --- a/src/app/api/services/scorm-adapter.service.ts +++ b/src/app/api/services/scorm-adapter.service.ts @@ -129,6 +129,7 @@ export class ScormAdapterService { ); this.xhr.setRequestHeader('Content-Type', 'application/json'); const requestData = { + cmi_datamodel: JSON.stringify(this.dataModel.dump()), terminated: true, }; this.xhr.send(JSON.stringify(requestData)); From fc023af462656e3557e2970f211fa4d59ee1e3d5 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 14 May 2024 14:24:31 +1000 Subject: [PATCH 048/776] feat: allow changing scorm review config and add minor UI changes --- src/app/api/models/task-definition.ts | 1 + .../api/services/task-definition.service.ts | 1 + .../task-definition-editor.component.html | 2 +- .../task-definition-scorm.component.html | 49 ++++++++++--------- 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index 538b449338..ca2d188eb6 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -33,6 +33,7 @@ export class TaskDefinition extends Entity { hasTaskResources: boolean; scormEnabled: boolean; hasScormData: boolean; + scormAllowReview: boolean; scormTimeDelayEnabled: boolean; scormAttemptLimit: number = 0; hasTaskAssessmentResources: boolean; diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index defd83dc77..3776432fd8 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -95,6 +95,7 @@ export class TaskDefinitionService extends CachedEntityService { 'hasTaskAssessmentResources', 'scormEnabled', 'hasScormData', + 'scormAllowReview', 'scormTimeDelayEnabled', 'scormAttemptLimit', 'isGraded', 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 6bc386569e..bf75eda5b5 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 @@ -105,7 +105,7 @@

-

Upload SCORM test

+

SCORM test

Upload the corresponding SCORM 2004 test (e.g. Numbas)

diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.html index 433405669b..5b3b54c111 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.html @@ -3,30 +3,35 @@ Enable test for task -
- - @if (taskDefinition.hasScormData) { -
- - -
- } -
- @if (taskDefinition.scormEnabled) { +
+ + @if (taskDefinition.hasScormData) { +
+ + +
+ } +
+
- - Enable incremental time delays between test attempts - +
+ + Enable incremental time delays between test attempts + + + Allow students to review completed test attempt + +
Attempt limit Date: Tue, 14 May 2024 14:29:48 +1000 Subject: [PATCH 049/776] refactor: remove test attempt model and service --- src/app/api/models/test-attempt.ts | 22 ----------- src/app/api/services/test-attempt.service.ts | 40 -------------------- src/app/doubtfire-angular.module.ts | 2 - src/app/doubtfire-angularjs.module.ts | 2 - 4 files changed, 66 deletions(-) delete mode 100644 src/app/api/models/test-attempt.ts delete mode 100644 src/app/api/services/test-attempt.service.ts diff --git a/src/app/api/models/test-attempt.ts b/src/app/api/models/test-attempt.ts deleted file mode 100644 index e6f2e5d61d..0000000000 --- a/src/app/api/models/test-attempt.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Entity } from "ngx-entity-service"; -import { Task } from "./task"; - -export class TestAttempt extends Entity { - public id: number; - name: string; - attemptNumber: number; - passStatus: boolean; - suspendData: string; - completed: boolean; - cmiEntry: string; - examResult: string; - attemptedAt: Date; - taskId: number; - - task: Task; - - constructor(task: Task) { - super(); - this.task = task; - } -} diff --git a/src/app/api/services/test-attempt.service.ts b/src/app/api/services/test-attempt.service.ts deleted file mode 100644 index 094f2baa0c..0000000000 --- a/src/app/api/services/test-attempt.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable } from "@angular/core"; -import { EntityService } from "ngx-entity-service"; -import { TestAttempt } from "../models/test-attempt"; -import { HttpClient } from "@angular/common/http"; -import API_URL from "src/app/config/constants/apiURL"; -import { Task } from "../models/task"; -import { Observable } from "rxjs"; -import { AppInjector } from "src/app/app-injector"; -import { DoubtfireConstants } from "src/app/config/constants/doubtfire-constants"; - -@Injectable() -export class TestAttemptService extends EntityService { - protected readonly endpointFormat = '/test_attempts?id=:id:'; - - constructor(httpClient: HttpClient) { - super(httpClient, API_URL); - - this.mapping.addKeys( - 'id', - 'name', - 'attemptNumber', - 'passStatus', - 'suspendData', - 'completed', - 'cmiEntry', - 'examResult', - 'attemptedAt', - 'taskId' - ); - } - - public createInstanceFrom(json: object, other?: any): TestAttempt { - return new TestAttempt(other as Task); - } - - public getLatestCompletedTestAttempt(task: Task): Observable { - const url = `${AppInjector.get(DoubtfireConstants).API_URL}/test_attempts/completed-latest?task_id=${task.id}`; - return AppInjector.get(HttpClient).get(url); - } -} \ No newline at end of file diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 110ae4a3a2..f464c6c13d 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -229,7 +229,6 @@ import {GradeService} from './common/services/grade.service'; import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component'; import {ScormAdapterService} from './api/services/scorm-adapter.service'; import {ScormCommentComponent} from './tasks/task-comments-viewer/scorm-comment/scorm-comment.component'; -import {TestAttemptService} from './api/services/test-attempt.service'; @NgModule({ // Components we declare @@ -407,7 +406,6 @@ import {TestAttemptService} from './api/services/test-attempt.service'; CreateNewUnitModal, ScormPlayerModalProvider, ScormAdapterService, - TestAttemptService, provideLottieOptions({ player: () => player, }), diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 3cd6555bd8..eb119b464b 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -227,7 +227,6 @@ import {AlertService} from './common/services/alert.service'; import {GradeService} from './common/services/grade.service'; import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component'; import {ScormPlayerModal} from './common/scorm-player/scorm-player-modal.component'; -import {TestAttemptService} from './api/services/test-attempt.service'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -311,7 +310,6 @@ DoubtfireAngularJSModule.factory( ); DoubtfireAngularJSModule.factory('CreateNewUnitModal', downgradeInjectable(CreateNewUnitModal)); DoubtfireAngularJSModule.factory('ScormPlayerModal', downgradeInjectable(ScormPlayerModal)); -DoubtfireAngularJSModule.factory('testAttemptService', downgradeInjectable(TestAttemptService)); // directive -> component DoubtfireAngularJSModule.directive( From ce53396ab93a98b30a78d9259849262bcec5e9ff Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Wed, 15 May 2024 17:39:20 +1000 Subject: [PATCH 050/776] refactor: use task card and new tab for scorm and match comment display --- src/app/ajs-upgraded-providers.ts | 7 --- src/app/api/models/scorm-player-context.ts | 4 +- src/app/api/services/scorm-adapter.service.ts | 20 +++++---- .../scorm-player-modal.component.ts | 22 ---------- .../scorm-player/scorm-player.component.html | 5 +-- .../scorm-player/scorm-player.component.scss | 12 ++---- .../scorm-player/scorm-player.component.ts | 43 +++++++++++-------- src/app/doubtfire-angular.module.ts | 4 +- src/app/doubtfire-angularjs.module.ts | 13 +++--- src/app/doubtfire.states.ts | 30 +++++++++++++ .../task-scorm-card.component.html | 27 ++++++++++++ .../task-scorm-card.component.scss | 0 .../task-scorm-card.component.ts | 31 +++++++++++++ .../task-dashboard/task-dashboard.tpl.html | 1 + .../upload-submission-modal.coffee | 9 +--- .../upload-submission-modal.tpl.html | 20 --------- .../scorm-comment.component.html | 5 ++- .../scorm-comment/scorm-comment.component.ts | 5 +-- 18 files changed, 147 insertions(+), 111 deletions(-) delete mode 100644 src/app/common/scorm-player/scorm-player-modal.component.ts create mode 100644 src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html create mode 100644 src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.scss create mode 100644 src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts diff --git a/src/app/ajs-upgraded-providers.ts b/src/app/ajs-upgraded-providers.ts index f114da1c79..795869225d 100644 --- a/src/app/ajs-upgraded-providers.ts +++ b/src/app/ajs-upgraded-providers.ts @@ -18,7 +18,6 @@ export const rootScope = new InjectionToken('$rootScope'); export const calendarModal = new InjectionToken('CalendarModal'); export const aboutDoubtfireModal = new InjectionToken('AboutDoubtfireModal'); export const plagiarismReportModal = new InjectionToken('PlagiarismReportModal'); -export const scormPlayerModal = new InjectionToken('ScormPlayerModal'); // Define a provider for the above injection token... // It will get the service from AngularJS via the factory @@ -117,9 +116,3 @@ export const UnitStudentEnrolmentModalProvider = { useFactory: (i) => i.get('UnitStudentEnrolmentModal'), deps: ['$injector'], }; - -export const ScormPlayerModalProvider = { - provide: scormPlayerModal, - useFactory: (i) => i.get('ScormPlayerModal'), - deps: ['$injector'], -}; diff --git a/src/app/api/models/scorm-player-context.ts b/src/app/api/models/scorm-player-context.ts index f3b3b8a13f..ed7df07c0a 100644 --- a/src/app/api/models/scorm-player-context.ts +++ b/src/app/api/models/scorm-player-context.ts @@ -1,4 +1,4 @@ -import {Task, User} from 'src/app/api/models/doubtfire-model'; +import {User} from 'src/app/api/models/doubtfire-model'; type DataModelState = 'Uninitialized' | 'Initialized' | 'Terminated'; @@ -48,7 +48,7 @@ export class ScormPlayerContext { return CMIErrorCodes[value]; } - task: Task; + taskId: number; user: User; attemptNumber: number; diff --git a/src/app/api/services/scorm-adapter.service.ts b/src/app/api/services/scorm-adapter.service.ts index 9519f4cc2c..cb6a780952 100644 --- a/src/app/api/services/scorm-adapter.service.ts +++ b/src/app/api/services/scorm-adapter.service.ts @@ -1,7 +1,7 @@ import {Injectable} from '@angular/core'; import {UserService} from './user.service'; import API_URL from 'src/app/config/constants/apiURL'; -import {Task, ScormDataModel, ScormPlayerContext} from 'src/app/api/models/doubtfire-model'; +import {ScormDataModel, ScormPlayerContext} from 'src/app/api/models/doubtfire-model'; @Injectable({ providedIn: 'root', @@ -18,8 +18,12 @@ export class ScormAdapterService { this.xhr = new XMLHttpRequest(); } - set task(task: Task) { - this.context.task = task; + set taskId(taskId: number) { + this.context.taskId = taskId; + } + + set mode(mode: 'browse' | 'normal' | 'review') { + this.context.mode = mode; } get state() { @@ -47,7 +51,7 @@ export class ScormAdapterService { } // TODO: move this part into the player component - this.xhr.open('GET', `${this.apiBaseUrl}/${this.context.task.id}/latest`, false); + this.xhr.open('GET', `${this.apiBaseUrl}/${this.context.taskId}/latest`, false); let noTestFound = false; let startNewTest = false; @@ -80,7 +84,7 @@ export class ScormAdapterService { if (!startNewTest) { this.xhr.open( 'PATCH', - `${this.apiBaseUrl}/${this.context.task.id}/session/${this.context.attemptId}`, + `${this.apiBaseUrl}/${this.context.taskId}/session/${this.context.attemptId}`, false, ); this.xhr.send(); @@ -92,7 +96,7 @@ export class ScormAdapterService { this.dataModel.restore(currentSession.cmi_datamodel); console.log(this.dataModel.dump()); } else { - this.xhr.open('POST', `${this.apiBaseUrl}/${this.context.task.id}/session`, false); + this.xhr.open('POST', `${this.apiBaseUrl}/${this.context.taskId}/session`, false); this.xhr.send(); console.log(this.xhr.responseText); @@ -124,7 +128,7 @@ export class ScormAdapterService { this.xhr.open( 'PATCH', - `${this.apiBaseUrl}/${this.context.task.id}/session/${this.context.attemptId}`, + `${this.apiBaseUrl}/${this.context.taskId}/session/${this.context.attemptId}`, false, ); this.xhr.setRequestHeader('Content-Type', 'application/json'); @@ -200,7 +204,7 @@ export class ScormAdapterService { const xhr = new XMLHttpRequest(); xhr.open( 'PATCH', - `${this.apiBaseUrl}/${this.context.task.id}/session/${this.context.attemptId}`, + `${this.apiBaseUrl}/${this.context.taskId}/session/${this.context.attemptId}`, true, ); xhr.setRequestHeader('Content-Type', 'application/json'); diff --git a/src/app/common/scorm-player/scorm-player-modal.component.ts b/src/app/common/scorm-player/scorm-player-modal.component.ts deleted file mode 100644 index cd7740571c..0000000000 --- a/src/app/common/scorm-player/scorm-player-modal.component.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {Injectable} from '@angular/core'; -import {MatDialog, MatDialogRef} from '@angular/material/dialog'; -import {ScormPlayerComponent} from './scorm-player.component'; -import {Task} from 'src/app/api/models/task'; - -@Injectable({ - providedIn: 'root', -}) -export class ScormPlayerModal { - constructor(public dialog: MatDialog) {} - - public show(task: Task, mode: 'browse' | 'normal' | 'review'): void { - let dialogRef: MatDialogRef; - - dialogRef = this.dialog.open(ScormPlayerComponent, { - data: {task, mode}, - width: '95%', - height: '90%', - disableClose: true, - }); - } -} diff --git a/src/app/common/scorm-player/scorm-player.component.html b/src/app/common/scorm-player/scorm-player.component.html index 5089ad9de3..4855cf4d2b 100644 --- a/src/app/common/scorm-player/scorm-player.component.html +++ b/src/app/common/scorm-player/scorm-player.component.html @@ -1,4 +1 @@ -
- - -
+ diff --git a/src/app/common/scorm-player/scorm-player.component.scss b/src/app/common/scorm-player/scorm-player.component.scss index 5e24cc5d9e..f011d35aee 100644 --- a/src/app/common/scorm-player/scorm-player.component.scss +++ b/src/app/common/scorm-player/scorm-player.component.scss @@ -1,5 +1,7 @@ -.mat-dialog-content { +f-scorm-player { position: relative; + height: 100vh; + width: 100vw; } iframe { @@ -7,11 +9,5 @@ iframe { top: 0; left: 0; width: 100%; - height: 95%; + height: 100%; } - -button { - position: absolute; - bottom: 20px; - right: 20px; -} \ No newline at end of file diff --git a/src/app/common/scorm-player/scorm-player.component.ts b/src/app/common/scorm-player/scorm-player.component.ts index 106e6a8520..e1c68dc872 100644 --- a/src/app/common/scorm-player/scorm-player.component.ts +++ b/src/app/common/scorm-player/scorm-player.component.ts @@ -1,10 +1,10 @@ -import {Component, OnInit, Inject} from '@angular/core'; -import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {Component, OnInit, Input, HostListener} from '@angular/core'; import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser'; -import {Task, ScormPlayerContext} from 'src/app/api/models/doubtfire-model'; +import {ScormPlayerContext} from 'src/app/api/models/doubtfire-model'; import {ScormAdapterService} from 'src/app/api/services/scorm-adapter.service'; import {AppInjector} from 'src/app/app-injector'; import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {GlobalStateService, ViewType} from 'src/app/projects/states/index/global-state.service'; declare global { interface Window { @@ -20,20 +20,29 @@ declare global { export class ScormPlayerComponent implements OnInit { context: ScormPlayerContext; - task: Task; - currentMode: 'browse' | 'normal' | 'review' = 'normal'; + @Input() + taskId: number; + + @Input() + taskDefId: number; + + @Input() + mode: 'browse' | 'normal' | 'review'; + iframeSrc: SafeResourceUrl; constructor( - private dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: {task: Task, mode: 'browse' | 'normal' | 'review'}, + private globalState: GlobalStateService, private scormAdapter: ScormAdapterService, private sanitizer: DomSanitizer, ) {} ngOnInit(): void { - this.task = this.data.task; - this.scormAdapter.task = this.task; + this.globalState.setView(ViewType.OTHER); + this.globalState.hideHeader(); + + this.scormAdapter.taskId = this.taskId; + this.scormAdapter.mode = this.mode; window.API_1484_11 = { Initialize: () => this.scormAdapter.Initialize(), @@ -46,23 +55,21 @@ export class ScormPlayerComponent implements OnInit { GetDiagnostic: (errorCode: string) => this.scormAdapter.GetDiagnostic(errorCode), }; - this.currentMode = this.data.mode; - this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl( - `${AppInjector.get(DoubtfireConstants).API_URL}/scorm/${this.task.taskDefId}/index.html`, + `${AppInjector.get(DoubtfireConstants).API_URL}/scorm/${this.taskDefId}/index.html`, ); } - close(): void { + @HostListener('window:beforeunload', ['$event']) + beforeUnload($event: any): void { if (this.scormAdapter.state == 'Initialized') { console.log('SCORM player closing during an initialized session, commiting DataModel'); this.scormAdapter.Commit(); } - // TODO: would be nice if we can destroy this entire adapter object when the modal is closed - console.log('Clearing player context and DataModel'); + } + + @HostListener('window:unload', ['$event']) + onUnload($event: any): void { this.scormAdapter.destroy(); - const iframe = document.getElementsByTagName('iframe')[0]; - iframe?.parentNode?.removeChild(iframe); - this.dialogRef.close(); } } diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index f464c6c13d..dabcd49145 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -72,7 +72,6 @@ import { gradeTaskModalProvider, uploadSubmissionModalProvider, ConfirmationModalProvider, - ScormPlayerModalProvider, } from './ajs-upgraded-providers'; import { TaskCommentComposerComponent, @@ -229,6 +228,7 @@ import {GradeService} from './common/services/grade.service'; import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component'; import {ScormAdapterService} from './api/services/scorm-adapter.service'; import {ScormCommentComponent} from './tasks/task-comments-viewer/scorm-comment/scorm-comment.component'; +import {TaskScormCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component'; @NgModule({ // Components we declare @@ -333,6 +333,7 @@ import {ScormCommentComponent} from './tasks/task-comments-viewer/scorm-comment/ FUnitsComponent, ScormPlayerComponent, ScormCommentComponent, + TaskScormCardComponent, ], // Services we provide providers: [ @@ -404,7 +405,6 @@ import {ScormCommentComponent} from './tasks/task-comments-viewer/scorm-comment/ TasksForInboxSearchPipe, IsActiveUnitRole, CreateNewUnitModal, - ScormPlayerModalProvider, ScormAdapterService, provideLottieOptions({ player: () => player, diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index eb119b464b..bc5ad5ed4a 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -225,8 +225,7 @@ import {FUnitsComponent} from './admin/states/f-units/f-units.component'; import {MarkedPipe} from './common/pipes/marked.pipe'; import {AlertService} from './common/services/alert.service'; import {GradeService} from './common/services/grade.service'; -import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component'; -import {ScormPlayerModal} from './common/scorm-player/scorm-player-modal.component'; +import {TaskScormCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -309,7 +308,6 @@ DoubtfireAngularJSModule.factory( downgradeInjectable(EditProfileDialogService), ); DoubtfireAngularJSModule.factory('CreateNewUnitModal', downgradeInjectable(CreateNewUnitModal)); -DoubtfireAngularJSModule.factory('ScormPlayerModal', downgradeInjectable(ScormPlayerModal)); // directive -> component DoubtfireAngularJSModule.directive( @@ -367,6 +365,10 @@ DoubtfireAngularJSModule.directive( 'activityTypeList', downgradeComponent({component: ActivityTypeListComponent}), ); +DoubtfireAngularJSModule.directive( + 'fTaskScormCard', + downgradeComponent({component: TaskScormCardComponent}), +); DoubtfireAngularJSModule.directive( 'fTaskStatusCard', downgradeComponent({component: TaskStatusCardComponent}), @@ -444,11 +446,6 @@ DoubtfireAngularJSModule.directive( ); DoubtfireAngularJSModule.directive('fUnits', downgradeComponent({component: FUnitsComponent})); -DoubtfireAngularJSModule.directive( - 'fScormPlayerComponent', - downgradeComponent({component: ScormPlayerComponent}), -); - // Global configuration DoubtfireAngularJSModule.directive( 'taskCommentsViewer', diff --git a/src/app/doubtfire.states.ts b/src/app/doubtfire.states.ts index b9f95e88af..23b60886d6 100644 --- a/src/app/doubtfire.states.ts +++ b/src/app/doubtfire.states.ts @@ -8,6 +8,7 @@ import {TeachingPeriodListComponent} from './admin/states/teaching-periods/teach import {AcceptEulaComponent} from './eula/accept-eula/accept-eula.component'; import {FUsersComponent} from './admin/states/f-users/f-users.component'; import {FUnitsComponent} from './admin/states/f-units/f-units.component'; +import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component'; /* * Use this file to store any states that are sourced by angular components. @@ -291,6 +292,34 @@ const ViewAllUnits: NgHybridStateDeclaration = { }, }; +/** + * Define the SCORM Player state. + */ +const ScormPlayerState: NgHybridStateDeclaration = { + name: 'scorm-player', + url: '/task_def/:task_def_id/task/:task_id/scorm-player/:mode', + resolve: { + taskId: function ($stateParams) { + return $stateParams.task_id; + }, + taskDefId: function ($stateParams) { + return $stateParams.task_def_id; + }, + mode: function ($stateParams) { + return $stateParams.mode; + }, + }, + views: { + main: { + component: ScormPlayerComponent, + }, + }, + data: { + pageTitle: 'Knowledge Check', + roleWhitelist: ['Student', 'Tutor', 'Convenor', 'Admin'], + }, +}; + /** * Export the list of states we have created in angular */ @@ -306,4 +335,5 @@ export const doubtfireStates = [ ViewAllProjectsState, ViewAllUnits, AdministerUnits, + ScormPlayerState, ]; diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html new file mode 100644 index 0000000000..67265461cd --- /dev/null +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html @@ -0,0 +1,27 @@ + + + Knowledge Check + + +

+ You have to successfully pass this knowledge check to complete the task. +

+

+ You have {{ (task.definition.scormAttemptLimit > 0) ? task.definition.scormAttemptLimit : 'unlimited' }} attempts to complete this test. +

+

+ There will be an increased time delay between test attempts. First 2 attempts will not have a time delay in between. +

+
+ + + + +
diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.scss b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts new file mode 100644 index 0000000000..7d7dce6e1b --- /dev/null +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts @@ -0,0 +1,31 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {Task} from 'src/app/api/models/task'; +import {TaskService} from 'src/app/api/services/task.service'; + +@Component({ + selector: 'f-task-scorm-card', + templateUrl: './task-scorm-card.component.html', + styleUrls: ['./task-scorm-card.component.scss'], +}) +export class TaskScormCardComponent implements OnInit { + @Input() task: Task; + attemptsLeft: number; + + constructor( + private taskService: TaskService, + ) {} + + ngOnInit(): void { + if (this.task) { + + } + } + + launchScormPlayer(): void { + window.open(`#/task_def/${this.task.taskDefId}/task/${this.task.id}/scorm-player/normal`, '_blank'); + } + + requestMoreAttempts(): void { + + } +} 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 0962ddd82a..8402f4252c 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/tasks/modals/upload-submission-modal/upload-submission-modal.coffee b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee index 976c116f33..b9c9ebd2b9 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee @@ -32,7 +32,7 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) UploadSubmissionModal ) -.controller('UploadSubmissionModalCtrl', ($scope, $rootScope, $timeout, $modalInstance, ScormPlayerModal, newTaskService, newProjectService, task, reuploadEvidence, outcomeService, PrivacyPolicy) -> +.controller('UploadSubmissionModalCtrl', ($scope, $rootScope, $timeout, $modalInstance, newTaskService, newProjectService, task, reuploadEvidence, outcomeService, PrivacyPolicy) -> $scope.privacyPolicy = PrivacyPolicy # Expose task to scope $scope.task = task @@ -100,7 +100,7 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) # States functionality states = { # All possible states - all: ['group', 'scorm-assessment', 'files', 'alignment', 'comments', 'uploading'] + all: ['group', 'files', 'alignment', 'comments', 'uploading'] # Only states which are shown (populated in initialise) shown: [] # The currently active state (set in initialise) @@ -128,7 +128,6 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) removed.push('group') if !isRFF || !task.isGroupTask() removed.push('alignment') if !isRFF || !task.unit.ilos.length > 0 removed.push('comments') if isTestSubmission - removed.push('scorm-assessment') if !isRFF || !task.definition.scormEnabled removed # Initialises the states initialise: -> @@ -155,10 +154,6 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) previous: states.previous } - $scope.launchScormPlayer = -> - console.clear() - ScormPlayerModal.show $scope.task, 'normal' - # Whether or not we should disable this button $scope.shouldDisableBtn = { next: -> diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html index 0b296c2099..9caab7897e 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html @@ -43,26 +43,6 @@

-
-
-
-

- Attempt SCORM Test -

- - Complete the SCORM test first to proceed to upload evidence of your task completion. - -
-
- -
-
-
diff --git a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html index b74db43a07..ffe375f19d 100644 --- a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html +++ b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html @@ -1,9 +1,10 @@
-
+
- + +

diff --git a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts index 817a6cd64f..b584629668 100644 --- a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts +++ b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts @@ -1,6 +1,5 @@ import {Component, OnInit, Input} from '@angular/core'; import {Task, TaskComment} from 'src/app/api/models/doubtfire-model'; -import {ScormPlayerModal} from 'src/app/common/scorm-player/scorm-player-modal.component'; @Component({ selector: 'scorm-comment', @@ -11,11 +10,11 @@ export class ScormCommentComponent implements OnInit { @Input() task: Task; @Input() comment: TaskComment; - constructor(private modalService: ScormPlayerModal) {} + constructor() {} ngOnInit() {} reviewScormTest() { - this.modalService.show(this.task, 'review'); + window.open(`#/task_def/${this.task.taskDefId}/task/${this.task.id}/scorm-player/review`, '_blank'); } } From c022c925bc8b569a416c24696b54580248413001 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Sat, 1 Jun 2024 20:14:54 +1000 Subject: [PATCH 051/776] refactor: change url params for test attempts --- src/app/api/models/scorm-player-context.ts | 3 +- src/app/api/services/scorm-adapter.service.ts | 39 +++++++++---------- .../scorm-player/scorm-player.component.ts | 5 ++- src/app/doubtfire.states.ts | 8 ++-- .../task-scorm-card.component.ts | 5 ++- .../scorm-comment/scorm-comment.component.ts | 5 ++- 6 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/app/api/models/scorm-player-context.ts b/src/app/api/models/scorm-player-context.ts index ed7df07c0a..c065957ac0 100644 --- a/src/app/api/models/scorm-player-context.ts +++ b/src/app/api/models/scorm-player-context.ts @@ -48,7 +48,8 @@ export class ScormPlayerContext { return CMIErrorCodes[value]; } - taskId: number; + projectId: number; + taskDefId: number; user: User; attemptNumber: number; diff --git a/src/app/api/services/scorm-adapter.service.ts b/src/app/api/services/scorm-adapter.service.ts index cb6a780952..233c7aed6a 100644 --- a/src/app/api/services/scorm-adapter.service.ts +++ b/src/app/api/services/scorm-adapter.service.ts @@ -7,7 +7,6 @@ import {ScormDataModel, ScormPlayerContext} from 'src/app/api/models/doubtfire-m providedIn: 'root', }) export class ScormAdapterService { - private readonly apiBaseUrl = `${API_URL}/test_attempts`; private dataModel: ScormDataModel; private context: ScormPlayerContext; private xhr: XMLHttpRequest; @@ -18,8 +17,12 @@ export class ScormAdapterService { this.xhr = new XMLHttpRequest(); } - set taskId(taskId: number) { - this.context.taskId = taskId; + set projectId(projectId: number) { + this.context.projectId = projectId; + } + + set taskDefId(taskDefId: number) { + this.context.taskDefId = taskDefId; } set mode(mode: 'browse' | 'normal' | 'review') { @@ -51,7 +54,11 @@ export class ScormAdapterService { } // TODO: move this part into the player component - this.xhr.open('GET', `${this.apiBaseUrl}/${this.context.taskId}/latest`, false); + this.xhr.open( + 'GET', + `${API_URL}/projects/${this.context.projectId}/task_def_id/${this.context.taskDefId}/test_attempts/latest`, + false, + ); let noTestFound = false; let startNewTest = false; @@ -82,11 +89,7 @@ export class ScormAdapterService { } if (!startNewTest) { - this.xhr.open( - 'PATCH', - `${this.apiBaseUrl}/${this.context.taskId}/session/${this.context.attemptId}`, - false, - ); + this.xhr.open('PATCH', `${API_URL}/test_attempts/${this.context.attemptId}`, false); this.xhr.send(); console.log(this.xhr.responseText); @@ -96,7 +99,11 @@ export class ScormAdapterService { this.dataModel.restore(currentSession.cmi_datamodel); console.log(this.dataModel.dump()); } else { - this.xhr.open('POST', `${this.apiBaseUrl}/${this.context.taskId}/session`, false); + this.xhr.open( + 'POST', + `${API_URL}/projects/${this.context.projectId}/task_def_id/${this.context.taskDefId}/test_attempts`, + false, + ); this.xhr.send(); console.log(this.xhr.responseText); @@ -126,11 +133,7 @@ export class ScormAdapterService { break; } - this.xhr.open( - 'PATCH', - `${this.apiBaseUrl}/${this.context.taskId}/session/${this.context.attemptId}`, - false, - ); + this.xhr.open('PATCH', `${API_URL}/test_attempts/${this.context.attemptId}`, false); this.xhr.setRequestHeader('Content-Type', 'application/json'); const requestData = { cmi_datamodel: JSON.stringify(this.dataModel.dump()), @@ -202,11 +205,7 @@ export class ScormAdapterService { } const xhr = new XMLHttpRequest(); - xhr.open( - 'PATCH', - `${this.apiBaseUrl}/${this.context.taskId}/session/${this.context.attemptId}`, - true, - ); + xhr.open('PATCH', `${API_URL}/test_attempts/${this.context.attemptId}`, true); xhr.setRequestHeader('Content-Type', 'application/json'); const requestData = { cmi_datamodel: JSON.stringify(this.dataModel.dump()), diff --git a/src/app/common/scorm-player/scorm-player.component.ts b/src/app/common/scorm-player/scorm-player.component.ts index e1c68dc872..9c2ee9edb4 100644 --- a/src/app/common/scorm-player/scorm-player.component.ts +++ b/src/app/common/scorm-player/scorm-player.component.ts @@ -21,7 +21,7 @@ export class ScormPlayerComponent implements OnInit { context: ScormPlayerContext; @Input() - taskId: number; + projectId: number; @Input() taskDefId: number; @@ -41,7 +41,8 @@ export class ScormPlayerComponent implements OnInit { this.globalState.setView(ViewType.OTHER); this.globalState.hideHeader(); - this.scormAdapter.taskId = this.taskId; + this.scormAdapter.projectId = this.projectId; + this.scormAdapter.taskDefId = this.taskDefId; this.scormAdapter.mode = this.mode; window.API_1484_11 = { diff --git a/src/app/doubtfire.states.ts b/src/app/doubtfire.states.ts index 23b60886d6..8f48cd06fd 100644 --- a/src/app/doubtfire.states.ts +++ b/src/app/doubtfire.states.ts @@ -297,13 +297,13 @@ const ViewAllUnits: NgHybridStateDeclaration = { */ const ScormPlayerState: NgHybridStateDeclaration = { name: 'scorm-player', - url: '/task_def/:task_def_id/task/:task_id/scorm-player/:mode', + url: '/projects/:project_id/task_def_id/:task_definition_id/scorm-player/:mode', resolve: { - taskId: function ($stateParams) { - return $stateParams.task_id; + projectId: function ($stateParams) { + return $stateParams.project_id; }, taskDefId: function ($stateParams) { - return $stateParams.task_def_id; + return $stateParams.task_definition_id; }, mode: function ($stateParams) { return $stateParams.mode; diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts index 7d7dce6e1b..8172bcf1ff 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts @@ -22,7 +22,10 @@ export class TaskScormCardComponent implements OnInit { } launchScormPlayer(): void { - window.open(`#/task_def/${this.task.taskDefId}/task/${this.task.id}/scorm-player/normal`, '_blank'); + window.open( + `#/projects/${this.task.project.id}/task_def_id/${this.task.taskDefId}/scorm-player/normal`, + '_blank', + ); } requestMoreAttempts(): void { diff --git a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts index b584629668..b16184a042 100644 --- a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts +++ b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts @@ -15,6 +15,9 @@ export class ScormCommentComponent implements OnInit { ngOnInit() {} reviewScormTest() { - window.open(`#/task_def/${this.task.taskDefId}/task/${this.task.id}/scorm-player/review`, '_blank'); + window.open( + `#/projects/${this.task.project.id}/task_def_id/${this.task.taskDefId}/scorm-player/review`, + '_blank', + ); } } From 561b9241c2f44fd69d3f09c656a025f514bbaf3a Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Sun, 2 Jun 2024 02:20:03 +1000 Subject: [PATCH 052/776] feat: enable reviewing, passing, deleting test attempts and add test attempt model and service --- src/app/api/models/doubtfire-model.ts | 3 + .../api/models/task-comment/scorm-comment.ts | 9 ++ src/app/api/models/test-attempt.ts | 20 ++++ src/app/api/services/scorm-adapter.service.ts | 29 +++++ src/app/api/services/task-comment.service.ts | 22 +++- src/app/api/services/test-attempt.service.ts | 103 ++++++++++++++++++ .../scorm-player/scorm-player.component.ts | 11 +- src/app/doubtfire-angular.module.ts | 2 + src/app/doubtfire.states.ts | 38 ++++++- .../scorm-comment.component.html | 11 +- .../scorm-comment/scorm-comment.component.ts | 29 ++++- .../task-comments-viewer.component.html | 4 +- 12 files changed, 259 insertions(+), 22 deletions(-) create mode 100644 src/app/api/models/task-comment/scorm-comment.ts create mode 100644 src/app/api/models/test-attempt.ts create mode 100644 src/app/api/services/test-attempt.service.ts diff --git a/src/app/api/models/doubtfire-model.ts b/src/app/api/models/doubtfire-model.ts index baca2f22d2..d6e4230f6e 100644 --- a/src/app/api/models/doubtfire-model.ts +++ b/src/app/api/models/doubtfire-model.ts @@ -34,6 +34,8 @@ export * from './task-similarity'; export * from './tii-action'; export * from './scorm-datamodel'; export * from './scorm-player-context'; +export * from './test-attempt'; +export * from './task-comment/scorm-comment'; // Users -- are students or staff export * from './user/user'; @@ -58,3 +60,4 @@ export * from '../services/teaching-period-break.service'; export * from '../services/learning-outcome.service'; export * from '../services/group-set.service'; export * from '../services/task-similarity.service'; +export * from '../services/test-attempt.service'; diff --git a/src/app/api/models/task-comment/scorm-comment.ts b/src/app/api/models/task-comment/scorm-comment.ts new file mode 100644 index 0000000000..3356b62b93 --- /dev/null +++ b/src/app/api/models/task-comment/scorm-comment.ts @@ -0,0 +1,9 @@ +import {Task, TaskComment, TestAttempt} from '../doubtfire-model'; + +export class ScormComment extends TaskComment { + testAttempt: TestAttempt; + + constructor(task: Task) { + super(task); + } +} diff --git a/src/app/api/models/test-attempt.ts b/src/app/api/models/test-attempt.ts new file mode 100644 index 0000000000..02bb4bb4e9 --- /dev/null +++ b/src/app/api/models/test-attempt.ts @@ -0,0 +1,20 @@ +import {Entity} from 'ngx-entity-service'; +import {Task} from './doubtfire-model'; + +export class TestAttempt extends Entity { + id: number; + attemptNumber: number; + terminated: boolean; + completionStatus: boolean; + successStatus: boolean; + scoreScaled: number; + cmiDatamodel: string; + attemptedTime: Date; + + task: Task; + + constructor(task: Task) { + super(); + this.task = task; + } +} diff --git a/src/app/api/services/scorm-adapter.service.ts b/src/app/api/services/scorm-adapter.service.ts index 233c7aed6a..b6014f14b4 100644 --- a/src/app/api/services/scorm-adapter.service.ts +++ b/src/app/api/services/scorm-adapter.service.ts @@ -29,6 +29,10 @@ export class ScormAdapterService { this.context.mode = mode; } + set testAttemptId(testAttemptId: number) { + this.context.attemptId = testAttemptId; + } + get state() { return this.context.state; } @@ -53,6 +57,31 @@ export class ScormAdapterService { break; } + if (this.context.mode === 'review') { + this.xhr.open('GET', `${API_URL}/test_attempts/${this.context.attemptId}/review`, false); + + this.xhr.onload = () => { + if (this.xhr.status >= 200 && this.xhr.status < 400) { + console.log('Retrieved the attempt.'); + } else if (this.xhr.status == 404) { + console.log('Not found.'); + noTestFound = true; + } else { + console.error('Error saving DataModel:', this.xhr.responseText); + } + }; + + this.xhr.send(); + console.log(this.xhr.responseText); + + const reviewSession = JSON.parse(this.xhr.responseText); + this.dataModel.restore(reviewSession.cmi_datamodel); + console.log(this.dataModel.dump()); + + this.context.state = 'Initialized'; + return 'true'; + } + // TODO: move this part into the player component this.xhr.open( 'GET', diff --git a/src/app/api/services/task-comment.service.ts b/src/app/api/services/task-comment.service.ts index 5f034b870d..c97ec4c3e0 100644 --- a/src/app/api/services/task-comment.service.ts +++ b/src/app/api/services/task-comment.service.ts @@ -1,4 +1,4 @@ -import { Task, TaskComment, UserService } from 'src/app/api/models/doubtfire-model'; +import { ScormComment, Task, TaskComment, TestAttemptService, UserService } from 'src/app/api/models/doubtfire-model'; import { EventEmitter, Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @@ -32,7 +32,8 @@ export class TaskCommentService extends CachedEntityService { httpClient: HttpClient, private emojiService: EmojiService, private userService: UserService, - private downloader: FileDownloaderService + private downloader: FileDownloaderService, + private testAttemptService: TestAttemptService, ) { super(httpClient, API_URL); @@ -85,7 +86,20 @@ export class TaskCommentService extends CachedEntityService { 'status', 'numberOfPrompts', 'timeDiscussionComplete', - 'timeDiscussionStarted' + 'timeDiscussionStarted', + + // Scorm Comments + { + keys: 'testAttempt', + toEntityFn: (data: object, key: string, comment: ScormComment) => { + const testAttempt = this.testAttemptService.cache.getOrCreate( + data[key].id, + testAttemptService, + data[key], + ); + return testAttempt; + }, + }, ); this.mapping.addJsonKey( @@ -103,6 +117,8 @@ export class TaskCommentService extends CachedEntityService { return new DiscussionComment(other); case 'extension': return new ExtensionComment(other); + case 'scorm': + return new ScormComment(other); default: return new TaskComment(other); } diff --git a/src/app/api/services/test-attempt.service.ts b/src/app/api/services/test-attempt.service.ts new file mode 100644 index 0000000000..7a7ac25ca2 --- /dev/null +++ b/src/app/api/services/test-attempt.service.ts @@ -0,0 +1,103 @@ +import {Injectable} from '@angular/core'; +import {CachedEntityService} from 'ngx-entity-service'; +import API_URL from 'src/app/config/constants/apiURL'; +import {Task, TestAttempt} from 'src/app/api/models/doubtfire-model'; +import {Observable} from 'rxjs'; +import {AppInjector} from 'src/app/app-injector'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {HttpClient} from '@angular/common/http'; + +@Injectable() +export class TestAttemptService extends CachedEntityService { + protected readonly endpointFormat = 'test_attempts/:id:'; + protected readonly forTaskEndpoint = + '/projects/:project_id:/task_definition_id/:task_def_id:/test_attempts'; + protected readonly latestCompletedEndpoint = + this.forTaskEndpoint + '/latest?completed=:completed:'; + + constructor(httpClient: HttpClient) { + super(httpClient, API_URL); + + this.mapping.addKeys( + 'id', + 'attemptNumber', + 'terminated', + 'completionStatus', + 'successStatus', + 'scoreScaled', + 'cmiDatamodel', + 'attemptedTime', + ); + } + + public override createInstanceFrom(_json: object, constructorParams: Task): TestAttempt { + return new TestAttempt(constructorParams); + } + + public getAttemptsForTask(task: Task): Observable { + return this.query( + { + project_id: task.project.id, + task_def_id: task.taskDefId, + }, + { + endpointFormat: this.forTaskEndpoint, + constructorParams: task, + }, + ); + } + + public getLatestCompletedAttempt(task: Task): Observable { + return this.get( + { + project_id: task.project.id, + task_def_id: task.taskDefId, + completed: true, + }, + { + endpointFormat: this.latestCompletedEndpoint, + constructorParams: task, + }, + ); + } + + public overrideSuccessStatus(testAttemptId: number, successStatus: boolean): void { + const http = AppInjector.get(HttpClient); + + http + .patch( + `${AppInjector.get(DoubtfireConstants).API_URL}/test_attempts/${testAttemptId}?success_status=${successStatus}`, + {}, + ) + .subscribe({ + next: (_data) => { + (AppInjector.get(AlertService) as AlertService).success( + 'Attempt pass status successfully overridden.', + 6000, + ); + }, + error: (message) => { + (AppInjector.get(AlertService) as AlertService).error(message, 6000); + }, + }); + } + + public deleteAttempt(testAttemptId: number): void { + const http = AppInjector.get(HttpClient); + + http + .delete(`${AppInjector.get(DoubtfireConstants).API_URL}/test_attempts/${testAttemptId}`, {}) + .subscribe({ + next: (_data) => { + (AppInjector.get(AlertService) as AlertService).success( + 'Attempt successfully deleted.', + 6000, + ); + }, + error: (message) => { + (AppInjector.get(AlertService) as AlertService).error(message, 6000); + }, + }); + } +} diff --git a/src/app/common/scorm-player/scorm-player.component.ts b/src/app/common/scorm-player/scorm-player.component.ts index 9c2ee9edb4..4a32eb0ac7 100644 --- a/src/app/common/scorm-player/scorm-player.component.ts +++ b/src/app/common/scorm-player/scorm-player.component.ts @@ -29,6 +29,9 @@ export class ScormPlayerComponent implements OnInit { @Input() mode: 'browse' | 'normal' | 'review'; + @Input() + testAttemptId: number; + iframeSrc: SafeResourceUrl; constructor( @@ -41,9 +44,13 @@ export class ScormPlayerComponent implements OnInit { this.globalState.setView(ViewType.OTHER); this.globalState.hideHeader(); - this.scormAdapter.projectId = this.projectId; - this.scormAdapter.taskDefId = this.taskDefId; this.scormAdapter.mode = this.mode; + if (this.mode === 'normal') { + this.scormAdapter.projectId = this.projectId; + this.scormAdapter.taskDefId = this.taskDefId; + } else if (this.mode === 'review') { + this.scormAdapter.testAttemptId = this.testAttemptId; + } window.API_1484_11 = { Initialize: () => this.scormAdapter.Initialize(), diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index dabcd49145..5f6e91e775 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -229,6 +229,7 @@ import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component import {ScormAdapterService} from './api/services/scorm-adapter.service'; import {ScormCommentComponent} from './tasks/task-comments-viewer/scorm-comment/scorm-comment.component'; import {TaskScormCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component'; +import {TestAttemptService} from './api/services/test-attempt.service'; @NgModule({ // Components we declare @@ -406,6 +407,7 @@ import {TaskScormCardComponent} from './projects/states/dashboard/directives/tas IsActiveUnitRole, CreateNewUnitModal, ScormAdapterService, + TestAttemptService, provideLottieOptions({ player: () => player, }), diff --git a/src/app/doubtfire.states.ts b/src/app/doubtfire.states.ts index 8f48cd06fd..0338a550a9 100644 --- a/src/app/doubtfire.states.ts +++ b/src/app/doubtfire.states.ts @@ -295,9 +295,9 @@ const ViewAllUnits: NgHybridStateDeclaration = { /** * Define the SCORM Player state. */ -const ScormPlayerState: NgHybridStateDeclaration = { - name: 'scorm-player', - url: '/projects/:project_id/task_def_id/:task_definition_id/scorm-player/:mode', +const ScormPlayerNormalState: NgHybridStateDeclaration = { + name: 'scorm-player-normal', + url: '/projects/:project_id/task_def_id/:task_definition_id/scorm-player/normal', resolve: { projectId: function ($stateParams) { return $stateParams.project_id; @@ -305,8 +305,8 @@ const ScormPlayerState: NgHybridStateDeclaration = { taskDefId: function ($stateParams) { return $stateParams.task_definition_id; }, - mode: function ($stateParams) { - return $stateParams.mode; + mode: function () { + return 'normal'; }, }, views: { @@ -320,6 +320,31 @@ const ScormPlayerState: NgHybridStateDeclaration = { }, }; +const ScormPlayerReviewState: NgHybridStateDeclaration = { + name: 'scorm-player-review', + url: '/task_def_id/:task_definition_id/scorm-player/review/:test_attempt_id', + resolve: { + taskDefId: function ($stateParams) { + return $stateParams.task_definition_id; + }, + testAttemptId: function ($stateParams) { + return $stateParams.test_attempt_id; + }, + mode: function () { + return 'review'; + }, + }, + views: { + main: { + component: ScormPlayerComponent, + }, + }, + data: { + pageTitle: 'Review Knowledge Check', + roleWhitelist: ['Student', 'Tutor', 'Convenor', 'Admin'], + }, +}; + /** * Export the list of states we have created in angular */ @@ -335,5 +360,6 @@ export const doubtfireStates = [ ViewAllProjectsState, ViewAllUnits, AdministerUnits, - ScormPlayerState, + ScormPlayerNormalState, + ScormPlayerReviewState, ]; diff --git a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html index ffe375f19d..b0536d2ce6 100644 --- a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html +++ b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html @@ -1,10 +1,13 @@ -
-
+
+

- -
+
+ + + +

diff --git a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts index b16184a042..201e35544a 100644 --- a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts +++ b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts @@ -1,23 +1,42 @@ import {Component, OnInit, Input} from '@angular/core'; -import {Task, TaskComment} from 'src/app/api/models/doubtfire-model'; +import {Task, ScormComment, User, UserService, TestAttemptService} from 'src/app/api/models/doubtfire-model'; @Component({ - selector: 'scorm-comment', + selector: 'f-scorm-comment', templateUrl: './scorm-comment.component.html', styleUrls: ['./scorm-comment.component.scss'], }) export class ScormCommentComponent implements OnInit { @Input() task: Task; - @Input() comment: TaskComment; + @Input() comment: ScormComment; - constructor() {} + user: User; + + constructor( + private userService: UserService, + private testAttemptService: TestAttemptService, + ) { + this.user = this.userService.currentUser; + } ngOnInit() {} + get canOverridePass(): boolean { + return this.user.isStaff && !this.comment.testAttempt.successStatus; + } + reviewScormTest() { window.open( - `#/projects/${this.task.project.id}/task_def_id/${this.task.taskDefId}/scorm-player/review`, + `#/task_def_id/${this.task.taskDefId}/scorm-player/review/${this.comment.testAttempt.id}`, '_blank', ); } + + passScormAttempt() { + this.testAttemptService.overrideSuccessStatus(this.comment.testAttempt.id, true); + } + + deleteScormAttempt() { + this.testAttemptService.deleteAttempt(this.comment.testAttempt.id); + } } diff --git a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html index aa20275ff6..f6228c576a 100644 --- a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html +++ b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html @@ -73,11 +73,11 @@
- + >
From e605a3cfb56f8979b6e8d3ddf16dab7a8d253c03 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Sun, 2 Jun 2024 21:00:31 +1000 Subject: [PATCH 053/776] refactor: use new alert service for scorm editor --- .../task-definition-scorm.component.ts | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.ts index 8f2b72fcb9..3e41aa39d2 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.ts @@ -1,10 +1,10 @@ -import { Component, Inject, Input } from '@angular/core'; -import { FormControl, Validators } from '@angular/forms'; -import { alertService } from 'src/app/ajs-upgraded-providers'; -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 { FileDownloaderService } from 'src/app/common/file-downloader/file-downloader.service'; +import {Component, Input} 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 {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; @Component({ selector: 'f-task-definition-scorm', @@ -16,8 +16,8 @@ export class TaskDefinitionScormComponent { constructor( private fileDownloaderService: FileDownloaderService, - @Inject(alertService) private alerts: any, - private taskDefinitionService: TaskDefinitionService + private alerts: AlertService, + private taskDefinitionService: TaskDefinitionService, ) {} public attemptLimitControl = new FormControl('', [Validators.max(100), Validators.min(0)]); @@ -41,9 +41,11 @@ export class TaskDefinitionScormComponent { } public uploadScormData(files: FileList) { - console.log(Array.from(files).map(f => f.type)); + console.log(Array.from(files).map((f) => f.type)); const validMimeTypes = ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip']; - const validFiles = Array.from(files as ArrayLike).filter(f => validMimeTypes.includes(f.type)); + const validFiles = Array.from(files as ArrayLike).filter((f) => + validMimeTypes.includes(f.type), + ); if (validFiles.length > 0) { const file = validFiles[0]; this.taskDefinitionService.uploadScormData(this.taskDefinition, file).subscribe({ From 58c24c3a6760af168a918bc099e755f475eac5f0 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Mon, 3 Jun 2024 00:24:20 +1000 Subject: [PATCH 054/776] fix: show correct attempts left and allow tutor to review attempt always --- src/app/api/models/task.ts | 20 +++++++++++++ src/app/api/services/test-attempt.service.ts | 20 ++----------- .../task-scorm-card.component.html | 18 ++++++----- .../task-scorm-card.component.ts | 30 +++++++++++-------- .../scorm-comment.component.html | 18 ++++++++--- 5 files changed, 65 insertions(+), 41 deletions(-) diff --git a/src/app/api/models/task.ts b/src/app/api/models/task.ts index 556d0a7dd2..86ac445fc2 100644 --- a/src/app/api/models/task.ts +++ b/src/app/api/models/task.ts @@ -15,6 +15,8 @@ import { TaskCommentService, TaskSimilarity, TaskSimilarityService, + TestAttempt, + TestAttemptService, } from './doubtfire-model'; import {Grade} from './grade'; import {LOCALE_ID} from '@angular/core'; @@ -53,6 +55,7 @@ export class Task extends Entity { public readonly commentCache: EntityCache = new EntityCache(); public readonly similarityCache: EntityCache = new EntityCache(); + public readonly testAttemptCache: EntityCache = new EntityCache(); private _unit: Unit; @@ -770,4 +773,21 @@ export class Task extends Entity { }, ); } + + /** + * Fetch the SCORM test attempts for this task. + */ + public fetchTestAttempts(): Observable { + const testAttemptService: TestAttemptService = AppInjector.get(TestAttemptService); + return testAttemptService.query( + { + project_id: this.project.id, + task_def_id: this.taskDefId, + }, + { + cache: this.testAttemptCache, + constructorParams: this, + }, + ); + } } diff --git a/src/app/api/services/test-attempt.service.ts b/src/app/api/services/test-attempt.service.ts index 7a7ac25ca2..6c32ed01a8 100644 --- a/src/app/api/services/test-attempt.service.ts +++ b/src/app/api/services/test-attempt.service.ts @@ -10,11 +10,10 @@ import {HttpClient} from '@angular/common/http'; @Injectable() export class TestAttemptService extends CachedEntityService { - protected readonly endpointFormat = 'test_attempts/:id:'; - protected readonly forTaskEndpoint = - '/projects/:project_id:/task_definition_id/:task_def_id:/test_attempts'; + protected readonly endpointFormat = + '/projects/:project_id:/task_def_id/:task_def_id:/test_attempts'; protected readonly latestCompletedEndpoint = - this.forTaskEndpoint + '/latest?completed=:completed:'; + this.endpointFormat + '/latest?completed=:completed:'; constructor(httpClient: HttpClient) { super(httpClient, API_URL); @@ -35,19 +34,6 @@ export class TestAttemptService extends CachedEntityService { return new TestAttempt(constructorParams); } - public getAttemptsForTask(task: Task): Observable { - return this.query( - { - project_id: task.project.id, - task_def_id: task.taskDefId, - }, - { - endpointFormat: this.forTaskEndpoint, - constructorParams: task, - }, - ); - } - public getLatestCompletedAttempt(task: Task): Observable { return this.get( { diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html index 67265461cd..9d518fb1c4 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html @@ -3,24 +3,28 @@ Knowledge Check +

You have to successfully pass this knowledge check to complete the task.

- You have to successfully pass this knowledge check to complete the task. -

-

- You have {{ (task.definition.scormAttemptLimit > 0) ? task.definition.scormAttemptLimit : 'unlimited' }} attempts to complete this test. + You have {{ attemptsLeft !== undefined ? attemptsLeft : 'unlimited' }} attempts left to + complete this test.

- There will be an increased time delay between test attempts. First 2 attempts will not have a time delay in between. + There will be an increased time delay between test attempts. First 2 attempts will not have a + time delay in between.

- - diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts index 8172bcf1ff..cdeb73df0e 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts @@ -1,23 +1,29 @@ -import {Component, Input, OnInit} from '@angular/core'; -import {Task} from 'src/app/api/models/task'; -import {TaskService} from 'src/app/api/services/task.service'; +import {Component, Input, OnChanges, SimpleChanges} from '@angular/core'; +import {Task} from 'src/app/api/models/doubtfire-model'; @Component({ selector: 'f-task-scorm-card', templateUrl: './task-scorm-card.component.html', styleUrls: ['./task-scorm-card.component.scss'], }) -export class TaskScormCardComponent implements OnInit { +export class TaskScormCardComponent implements OnChanges { @Input() task: Task; attemptsLeft: number; - constructor( - private taskService: TaskService, - ) {} - - ngOnInit(): void { - if (this.task) { + ngOnChanges(changes: SimpleChanges) { + if (changes.task && changes.task.currentValue) { + this.attemptsLeft = undefined; + this.getAttemptsLeft(); + } + } + getAttemptsLeft(): void { + if (this.task.definition.scormAttemptLimit != 0) { + this.task.fetchTestAttempts().subscribe((attempts) => { + let count = attempts.length; + if (count > 0 && attempts[0].terminated === false) count--; + this.attemptsLeft = this.task.definition.scormAttemptLimit - count; + }); } } @@ -28,7 +34,5 @@ export class TaskScormCardComponent implements OnInit { ); } - requestMoreAttempts(): void { - - } + requestMoreAttempts(): void {} } diff --git a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html index b0536d2ce6..c1dacaa788 100644 --- a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html +++ b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html @@ -2,11 +2,21 @@

-
+
- - - + + +

From d904ffd6fd674f6e61894d517f5aa147d1db6d29 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:17:47 +1000 Subject: [PATCH 055/776] feat: enable students to request extra scorm attempt --- src/app/api/models/doubtfire-model.ts | 1 + .../task-comment/scorm-extension-comment.ts | 38 +++++++++++ src/app/api/models/task.ts | 1 + src/app/api/services/task-comment.service.ts | 43 ++++++++++++ src/app/api/services/task.service.ts | 1 + .../scorm-extension-modal.component.html | 45 +++++++++++++ .../scorm-extension-modal.component.ts | 67 +++++++++++++++++++ .../scorm-extension-modal.service.ts | 26 +++++++ src/app/doubtfire-angular.module.ts | 4 ++ .../task-scorm-card.component.html | 6 +- .../task-scorm-card.component.ts | 11 ++- .../scorm-extension-comment.component.html | 32 +++++++++ .../scorm-extension-comment.component.scss | 55 +++++++++++++++ .../scorm-extension-comment.component.ts | 63 +++++++++++++++++ .../task-comments-viewer.component.html | 5 ++ .../task-comments-viewer.component.scss | 4 +- .../task-comments-viewer.component.ts | 8 ++- 17 files changed, 402 insertions(+), 8 deletions(-) create mode 100644 src/app/api/models/task-comment/scorm-extension-comment.ts create mode 100644 src/app/common/modals/scorm-extension-modal/scorm-extension-modal.component.html create mode 100644 src/app/common/modals/scorm-extension-modal/scorm-extension-modal.component.ts create mode 100644 src/app/common/modals/scorm-extension-modal/scorm-extension-modal.service.ts create mode 100644 src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.html create mode 100644 src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.scss create mode 100644 src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.ts diff --git a/src/app/api/models/doubtfire-model.ts b/src/app/api/models/doubtfire-model.ts index d6e4230f6e..fffc361a4e 100644 --- a/src/app/api/models/doubtfire-model.ts +++ b/src/app/api/models/doubtfire-model.ts @@ -36,6 +36,7 @@ export * from './scorm-datamodel'; export * from './scorm-player-context'; export * from './test-attempt'; export * from './task-comment/scorm-comment'; +export * from './task-comment/scorm-extension-comment'; // Users -- are students or staff export * from './user/user'; diff --git a/src/app/api/models/task-comment/scorm-extension-comment.ts b/src/app/api/models/task-comment/scorm-extension-comment.ts new file mode 100644 index 0000000000..3b4dfbccb6 --- /dev/null +++ b/src/app/api/models/task-comment/scorm-extension-comment.ts @@ -0,0 +1,38 @@ +import {Observable} from 'rxjs'; +import {tap} from 'rxjs/operators'; +import {AppInjector} from 'src/app/app-injector'; +import {TaskCommentService} from '../../services/task-comment.service'; +import {TaskComment, Task} from '../doubtfire-model'; + +export class ScormExtensionComment extends TaskComment { + assessed: boolean; + granted: boolean; + dateAssessed: Date; + taskScormExtensions: number; + + constructor(task: Task) { + super(task); + } + + private assessScormExtension(): Observable { + const tcs: TaskCommentService = AppInjector.get(TaskCommentService); + return tcs.assessScormExtension(this).pipe( + tap((tc: TaskComment) => { + const scormExtension: ScormExtensionComment = tc as ScormExtensionComment; + + const task = tc.task; + task.scormExtensions = scormExtension.taskScormExtensions; + }), + ); + } + + public deny(): Observable { + this.granted = false; + return this.assessScormExtension(); + } + + public grant(): Observable { + this.granted = true; + return this.assessScormExtension(); + } +} diff --git a/src/app/api/models/task.ts b/src/app/api/models/task.ts index 86ac445fc2..ba4bce8e04 100644 --- a/src/app/api/models/task.ts +++ b/src/app/api/models/task.ts @@ -32,6 +32,7 @@ export class Task extends Entity { status: TaskStatusEnum = 'not_started'; dueDate: Date; extensions: number; + scormExtensions: number; submissionDate: Date; completionDate: Date; timesAssessed: number; diff --git a/src/app/api/services/task-comment.service.ts b/src/app/api/services/task-comment.service.ts index c97ec4c3e0..e7f76d37b1 100644 --- a/src/app/api/services/task-comment.service.ts +++ b/src/app/api/services/task-comment.service.ts @@ -11,6 +11,7 @@ import API_URL from 'src/app/config/constants/apiURL'; import { EmojiService } from 'src/app/common/services/emoji.service'; import { MappingFunctions } from './mapping-fn'; import { FileDownloaderService } from 'src/app/common/file-downloader/file-downloader.service'; +import { ScormExtensionComment } from '../models/task-comment/scorm-extension-comment'; @Injectable() export class TaskCommentService extends CachedEntityService { @@ -22,6 +23,10 @@ export class TaskCommentService extends CachedEntityService { 'projects/:projectId:/task_def_id/:taskDefinitionId:/assess_extension/:id:'; private readonly requestExtensionEndpointFormat = 'projects/:projectId:/task_def_id/:taskDefinitionId:/request_extension'; + private readonly scormExtensionGrantEndpointFormat = + 'projects/:projectId:/task_def_id/:taskDefinitionId:/assess_scorm_extension/:id:'; + private readonly scormRequestExtensionEndpointFormat = + 'projects/:projectId:/task_def_id/:taskDefinitionId:/request_scorm_extension'; private readonly discussionCommentReplyEndpointFormat = "/projects/:project_id:/task_def_id/:task_definition_id:/comments/:task_comment_id:/discussion_comment/reply"; private readonly getDiscussionCommentPromptEndpointFormat = "/projects/:project_id:/task_def_id/:task_definition_id:/comments/:task_comment_id:/discussion_comment/prompt_number/:prompt_number:"; @@ -100,6 +105,9 @@ export class TaskCommentService extends CachedEntityService { return testAttempt; }, }, + + // Scorm Extension Comments + ['taskScormExtensions', 'scorm_extensions'] ); this.mapping.addJsonKey( @@ -119,6 +127,8 @@ export class TaskCommentService extends CachedEntityService { return new ExtensionComment(other); case 'scorm': return new ScormComment(other); + case 'scorm_extension': + return new ScormExtensionComment(other); default: return new TaskComment(other); } @@ -218,6 +228,39 @@ export class TaskCommentService extends CachedEntityService { ); } + public assessScormExtension(extension: ScormExtensionComment): Observable { + const opts: RequestOptions = { + endpointFormat: this.scormExtensionGrantEndpointFormat, + entity: extension, + }; + + return super.update( + { + id: extension.id, + projectId: extension.project.id, + taskDefinitionId: extension.task.definition.id, + }, + opts, + ); + } + + public requestScormExtension(reason: string, task: any): Observable { + const opts: RequestOptions = { + endpointFormat: this.scormRequestExtensionEndpointFormat, + body: { + comment: reason, + }, + cache: task.commentCache, + }; + return super.create( + { + projectId: task.project.id, + taskDefinitionId: task.definition.id, + }, + opts, + ); + } + public postDiscussionReply(comment: TaskComment, replyAudio: Blob): Observable{ const form = new FormData(); const pathIds = { diff --git a/src/app/api/services/task.service.ts b/src/app/api/services/task.service.ts index 344494c0eb..737fe1ea4c 100644 --- a/src/app/api/services/task.service.ts +++ b/src/app/api/services/task.service.ts @@ -46,6 +46,7 @@ export class TaskService extends CachedEntityService { toEntityFn: MappingFunctions.mapDateToEndOfDay, }, 'extensions', + 'scormExtensions', { keys: 'submissionDate', toEntityFn: MappingFunctions.mapDateToDay, diff --git a/src/app/common/modals/scorm-extension-modal/scorm-extension-modal.component.html b/src/app/common/modals/scorm-extension-modal/scorm-extension-modal.component.html new file mode 100644 index 0000000000..f39a9580b5 --- /dev/null +++ b/src/app/common/modals/scorm-extension-modal/scorm-extension-modal.component.html @@ -0,0 +1,45 @@ +

Extra attempt request

+
+

+ Please explain why you require an extra attempt for this knowledge check, the teaching team will + assess the request shortly. +

+ + + Reason + + {{ extensionData.controls.extensionReason.value.length }} / {{ reasonMaxLength }} + @if (extensionData.controls.extensionReason.hasError('required')) { + You must enter a reason + } + @if (extensionData.controls.extensionReason.hasError('minlength')) { + The reason must be at least {{ reasonMinLength }} characters long + } + @if (extensionData.controls.extensionReason.hasError('maxlength')) { + The reason must be less than {{ reasonMaxLength }} characters long + } + +
+ +
+ + +
diff --git a/src/app/common/modals/scorm-extension-modal/scorm-extension-modal.component.ts b/src/app/common/modals/scorm-extension-modal/scorm-extension-modal.component.ts new file mode 100644 index 0000000000..c7f5833821 --- /dev/null +++ b/src/app/common/modals/scorm-extension-modal/scorm-extension-modal.component.ts @@ -0,0 +1,67 @@ +import {Component, Inject, LOCALE_ID} from '@angular/core'; +import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog'; +import {TaskComment, TaskCommentService, Task} 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'; + +/** Error when invalid control is dirty, touched, or submitted. */ +export class ReasonErrorStateMatcher implements ErrorStateMatcher { + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const isSubmitted = form && form.submitted; + return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted)); + } +} + +@Component({ + selector: 'f-scorm-extension-modal', + templateUrl: './scorm-extension-modal.component.html', +}) +export class ScormExtensionModalComponent { + protected reasonMinLength: number = 15; + protected reasonMaxLength: number = 256; + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: {task: Task; afterApplication?: () => void}, + private alerts: AlertService, + ) {} + + matcher = new ReasonErrorStateMatcher(); + currentLocale = AppInjector.get(LOCALE_ID); + extensionData = new FormGroup({ + extensionReason: new FormControl('', [ + Validators.required, + Validators.minLength(this.reasonMinLength), + Validators.maxLength(this.reasonMaxLength), + ]), + }); + + private scrollCommentsDown(): void { + setTimeout(() => { + const objDiv = document.querySelector('div.comments-body'); + // let wrappedResult = angular.element(objDiv); + objDiv.scrollTop = objDiv.scrollHeight; + }, 50); + } + + submitApplication() { + const tcs: TaskCommentService = AppInjector.get(TaskCommentService); + tcs + .requestScormExtension(this.extensionData.controls.extensionReason.value, this.data.task) + .subscribe({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + next: ((tc: TaskComment) => { + this.alerts.success('Extra attempt requested.', 2000); + this.scrollCommentsDown(); + if (typeof this.data.afterApplication === 'function') { + this.data.afterApplication(); + } + }).bind(this), + error: ((response: never) => { + this.alerts.error('Error requesting extra attempt ' + response); + console.log(response); + }).bind(this), + }); + } +} diff --git a/src/app/common/modals/scorm-extension-modal/scorm-extension-modal.service.ts b/src/app/common/modals/scorm-extension-modal/scorm-extension-modal.service.ts new file mode 100644 index 0000000000..7e9b46f8e9 --- /dev/null +++ b/src/app/common/modals/scorm-extension-modal/scorm-extension-modal.service.ts @@ -0,0 +1,26 @@ +import {Injectable} from '@angular/core'; +import {Task} from 'src/app/api/models/task'; +import {MatDialogRef, MatDialog} from '@angular/material/dialog'; +import {ScormExtensionModalComponent} from './scorm-extension-modal.component'; + +@Injectable({ + providedIn: 'root', +}) +export class ScormExtensionModalService { + constructor(public dialog: MatDialog) {} + + public show(task: Task, afterApplication?: any) { + let dialogRef: MatDialogRef; + + dialogRef = this.dialog.open(ScormExtensionModalComponent, { + data: { + task, + afterApplication, + }, + }); + + 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 5f6e91e775..67e3c3f1dd 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -230,6 +230,8 @@ import {ScormAdapterService} from './api/services/scorm-adapter.service'; import {ScormCommentComponent} from './tasks/task-comments-viewer/scorm-comment/scorm-comment.component'; import {TaskScormCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component'; import {TestAttemptService} from './api/services/test-attempt.service'; +import {ScormExtensionCommentComponent} from './tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component'; +import {ScormExtensionModalComponent} from './common/modals/scorm-extension-modal/scorm-extension-modal.component'; @NgModule({ // Components we declare @@ -335,6 +337,8 @@ import {TestAttemptService} from './api/services/test-attempt.service'; ScormPlayerComponent, ScormCommentComponent, TaskScormCardComponent, + ScormExtensionCommentComponent, + ScormExtensionModalComponent, ], // Services we provide providers: [ diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html index 9d518fb1c4..1929afb248 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html @@ -22,10 +22,10 @@ diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts index cdeb73df0e..2713c9ee33 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts @@ -1,5 +1,6 @@ import {Component, Input, OnChanges, SimpleChanges} from '@angular/core'; import {Task} from 'src/app/api/models/doubtfire-model'; +import {ScormExtensionModalService} from 'src/app/common/modals/scorm-extension-modal/scorm-extension-modal.service'; @Component({ selector: 'f-task-scorm-card', @@ -10,6 +11,8 @@ export class TaskScormCardComponent implements OnChanges { @Input() task: Task; attemptsLeft: number; + constructor(private extensions: ScormExtensionModalService) {} + ngOnChanges(changes: SimpleChanges) { if (changes.task && changes.task.currentValue) { this.attemptsLeft = undefined; @@ -22,7 +25,7 @@ export class TaskScormCardComponent implements OnChanges { this.task.fetchTestAttempts().subscribe((attempts) => { let count = attempts.length; if (count > 0 && attempts[0].terminated === false) count--; - this.attemptsLeft = this.task.definition.scormAttemptLimit - count; + this.attemptsLeft = this.task.definition.scormAttemptLimit + this.task.scormExtensions - count; }); } } @@ -34,5 +37,9 @@ export class TaskScormCardComponent implements OnChanges { ); } - requestMoreAttempts(): void {} + requestExtraAttempt(): void { + this.extensions.show(this.task, () => { + this.task.refresh(); + }); + } } diff --git a/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.html b/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.html new file mode 100644 index 0000000000..fa7c4cb39f --- /dev/null +++ b/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.html @@ -0,0 +1,32 @@ +
+ @if (comment.assessed) { +
+
+

reason: {{ comment.text }}

+
+ } + + @if (!comment.assessed) { +
+
+

+ {{ message }}
+ reason: {{ comment.text }} +

+ @if (isNotStudent) { +
+ + +
+ } +
+
+ } +
diff --git a/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.scss b/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.scss new file mode 100644 index 0000000000..c2917f902e --- /dev/null +++ b/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.scss @@ -0,0 +1,55 @@ +div { + width: 100%; +} + +p { + color: #2c2c2c; + text-align: center; +} + +hr { + width: 100%; +} + +.hr-fade { + background: linear-gradient(to right, transparent, #9696969d, transparent); + width: 100%; +} + +.fade-text { + color: #9696969d; + opacity: 0.8; +} + +.hr-text { + margin: 0; + line-height: 1em; + position: relative; + outline: 0; + border: 0; + color: black; + text-align: center; + height: 1.5em; + opacity: 0.8; + &:before { + content: ""; + background: linear-gradient(to right, transparent, #9696969d, transparent); + position: absolute; + left: 0; + top: 50%; + width: 100%; + height: 1px; + } + &:after { + content: attr(data-content); + position: relative; + display: inline-block; + color: black; + + padding: 0 0.5em; + line-height: 1.5em; + + color: #9696969d; + background-color: #fff; + } +} diff --git a/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.ts b/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.ts new file mode 100644 index 0000000000..eb1790edd4 --- /dev/null +++ b/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.ts @@ -0,0 +1,63 @@ +import {Component, OnInit, Input} from '@angular/core'; +import {ScormExtensionComment, TaskComment, Task} from 'src/app/api/models/doubtfire-model'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-scorm-extension-comment', + templateUrl: './scorm-extension-comment.component.html', + styleUrls: ['./scorm-extension-comment.component.scss'], +}) +export class ScormExtensionCommentComponent implements OnInit { + @Input() comment: ScormExtensionComment; + @Input() task: Task; + + constructor(private alerts: AlertService) {} + + private handleError(error: any) { + this.alerts.error('Error: ' + error.data.error, 6000); + } + + ngOnInit() {} + + get message() { + const studentName = this.comment.author.name; + if (this.comment.assessed && this.comment.granted) { + return 'Extra attempt granted.'; + } else if (this.comment.assessed && !this.comment.granted) { + return 'Extra attempt request rejected.'; + } + const subject = this.isStudent ? 'You have ' : studentName + ' has '; + const message = 'requested an extra attempt for the knowledge check.'; + return subject + message; + } + + get isStudent() { + return !this.isNotStudent; + } + + get isNotStudent() { + return this.task.unit.currentUserIsStaff; + } + + denyExtension() { + this.comment.deny().subscribe({ + next: (tc: TaskComment) => { + this.alerts.success('Attempt request denied', 2000); + }, + error: (response) => { + this.handleError(response); + }, + }); + } + + grantExtension() { + this.comment.grant().subscribe({ + next: (tc: TaskComment) => { + this.alerts.success('Attempt request granted', 2000); + }, + error: (response) => { + this.handleError(response); + }, + }); + } +} diff --git a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html index f6228c576a..81b09e6011 100644 --- a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html +++ b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html @@ -95,6 +95,11 @@
+
+ + +
+
diff --git a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.scss b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.scss index d82983038f..a1b4811941 100644 --- a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.scss +++ b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.scss @@ -140,7 +140,7 @@ $comment-inner-border-radius: 4px; } } - .comment-container .comment-extension { + .comment-container .comment-extension, .comment-container .comment-scorm-extension { width: 100%; } @@ -354,7 +354,7 @@ $comment-inner-border-radius: 4px; } } - .comment .extension-bubble { + .comment .extension-bubble, .comment .scorm_extension-bubble { width: 100%; background-color: transparent; } diff --git a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.ts b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.ts index 8ad719f339..857f4581f2 100644 --- a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.ts +++ b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.ts @@ -154,7 +154,13 @@ export class TaskCommentsViewerComponent implements OnChanges, OnInit { } shouldShowAuthorIcon(commentType: string) { - return !(commentType === 'extension' || commentType === 'status' || commentType == 'assessment' || commentType == 'scorm'); + return !( + commentType === 'extension' || + commentType === 'status' || + commentType == 'assessment' || + commentType === 'scorm' || + commentType === 'scorm_extension' + ); } commentClasses(comment: TaskComment): object { From 97e1ea187b769a51ebb66b82d76f21193bb29386 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Wed, 5 Jun 2024 23:15:15 +1000 Subject: [PATCH 056/776] fix: add auth headers to scorm adapter xhr requests --- src/app/api/services/scorm-adapter.service.ts | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/app/api/services/scorm-adapter.service.ts b/src/app/api/services/scorm-adapter.service.ts index b6014f14b4..085d41610b 100644 --- a/src/app/api/services/scorm-adapter.service.ts +++ b/src/app/api/services/scorm-adapter.service.ts @@ -59,6 +59,8 @@ export class ScormAdapterService { if (this.context.mode === 'review') { this.xhr.open('GET', `${API_URL}/test_attempts/${this.context.attemptId}/review`, false); + this.xhr.setRequestHeader('Auth-Token', this.context.user.authenticationToken); + this.xhr.setRequestHeader('Username', this.context.user.username); this.xhr.onload = () => { if (this.xhr.status >= 200 && this.xhr.status < 400) { @@ -88,6 +90,8 @@ export class ScormAdapterService { `${API_URL}/projects/${this.context.projectId}/task_def_id/${this.context.taskDefId}/test_attempts/latest`, false, ); + this.xhr.setRequestHeader('Auth-Token', this.context.user.authenticationToken); + this.xhr.setRequestHeader('Username', this.context.user.username); let noTestFound = false; let startNewTest = false; @@ -119,6 +123,8 @@ export class ScormAdapterService { if (!startNewTest) { this.xhr.open('PATCH', `${API_URL}/test_attempts/${this.context.attemptId}`, false); + this.xhr.setRequestHeader('Auth-Token', this.context.user.authenticationToken); + this.xhr.setRequestHeader('Username', this.context.user.username); this.xhr.send(); console.log(this.xhr.responseText); @@ -133,6 +139,8 @@ export class ScormAdapterService { `${API_URL}/projects/${this.context.projectId}/task_def_id/${this.context.taskDefId}/test_attempts`, false, ); + this.xhr.setRequestHeader('Auth-Token', this.context.user.authenticationToken); + this.xhr.setRequestHeader('Username', this.context.user.username); this.xhr.send(); console.log(this.xhr.responseText); @@ -163,6 +171,8 @@ export class ScormAdapterService { } this.xhr.open('PATCH', `${API_URL}/test_attempts/${this.context.attemptId}`, false); + this.xhr.setRequestHeader('Auth-Token', this.context.user.authenticationToken); + this.xhr.setRequestHeader('Username', this.context.user.username); this.xhr.setRequestHeader('Content-Type', 'application/json'); const requestData = { cmi_datamodel: JSON.stringify(this.dataModel.dump()), @@ -233,26 +243,28 @@ export class ScormAdapterService { break; } - const xhr = new XMLHttpRequest(); - xhr.open('PATCH', `${API_URL}/test_attempts/${this.context.attemptId}`, true); - xhr.setRequestHeader('Content-Type', 'application/json'); + this.xhr.open('PATCH', `${API_URL}/test_attempts/${this.context.attemptId}`, true); + this.xhr.setRequestHeader('Auth-Token', this.context.user.authenticationToken); + this.xhr.setRequestHeader('Username', this.context.user.username); + this.xhr.setRequestHeader('Content-Type', 'application/json'); const requestData = { cmi_datamodel: JSON.stringify(this.dataModel.dump()), }; - xhr.send(JSON.stringify(requestData)); + - xhr.onload = () => { - if (xhr.status >= 200 && xhr.status < 400) { + this.xhr.onload = () => { + if (this.xhr.status >= 200 && this.xhr.status < 400) { console.log('DataModel saved successfully.'); } else { - console.error('Error saving DataModel:', xhr.responseText); + console.error('Error saving DataModel:', this.xhr.responseText); } }; - xhr.onerror = () => { + this.xhr.onerror = () => { console.error('Request failed.'); }; + this.xhr.send(JSON.stringify(requestData)); this.context.errorCode = 0; return 'true'; } From f0ff40bfd7e86b904adae94bf8cbe5d56371e011 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Wed, 5 Jun 2024 23:42:14 +1000 Subject: [PATCH 057/776] fix: remove attempt number field --- src/app/api/models/scorm-player-context.ts | 1 - src/app/api/models/test-attempt.ts | 1 - src/app/api/services/test-attempt.service.ts | 1 - 3 files changed, 3 deletions(-) diff --git a/src/app/api/models/scorm-player-context.ts b/src/app/api/models/scorm-player-context.ts index c065957ac0..195bc3cd4c 100644 --- a/src/app/api/models/scorm-player-context.ts +++ b/src/app/api/models/scorm-player-context.ts @@ -52,7 +52,6 @@ export class ScormPlayerContext { taskDefId: number; user: User; - attemptNumber: number; attemptId: number; learnerName: string; learnerId: number; diff --git a/src/app/api/models/test-attempt.ts b/src/app/api/models/test-attempt.ts index 02bb4bb4e9..646b9ed64f 100644 --- a/src/app/api/models/test-attempt.ts +++ b/src/app/api/models/test-attempt.ts @@ -3,7 +3,6 @@ import {Task} from './doubtfire-model'; export class TestAttempt extends Entity { id: number; - attemptNumber: number; terminated: boolean; completionStatus: boolean; successStatus: boolean; diff --git a/src/app/api/services/test-attempt.service.ts b/src/app/api/services/test-attempt.service.ts index 6c32ed01a8..df5654855f 100644 --- a/src/app/api/services/test-attempt.service.ts +++ b/src/app/api/services/test-attempt.service.ts @@ -20,7 +20,6 @@ export class TestAttemptService extends CachedEntityService { this.mapping.addKeys( 'id', - 'attemptNumber', 'terminated', 'completionStatus', 'successStatus', From 9bc48b03905c0a839d78ea13be9817ca73ba54cf Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Thu, 6 Jun 2024 20:53:45 +1000 Subject: [PATCH 058/776] fix: disable launch scorm test button if user is staff --- .../task-scorm-card/task-scorm-card.component.html | 7 ++++++- .../task-scorm-card/task-scorm-card.component.ts | 10 ++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html index 1929afb248..ec385b4760 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html @@ -14,7 +14,12 @@

- - -
+ }
diff --git a/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.ts b/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.ts index eb1790edd4..7585e8d7c9 100644 --- a/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.ts +++ b/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.ts @@ -39,17 +39,6 @@ export class ScormExtensionCommentComponent implements OnInit { return this.task.unit.currentUserIsStaff; } - denyExtension() { - this.comment.deny().subscribe({ - next: (tc: TaskComment) => { - this.alerts.success('Attempt request denied', 2000); - }, - error: (response) => { - this.handleError(response); - }, - }); - } - grantExtension() { this.comment.grant().subscribe({ next: (tc: TaskComment) => { From 703563c86253c60ad30d451ee2d8e0fa7ebbfabb Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 11 Jun 2024 22:20:39 +1000 Subject: [PATCH 063/776] feat: disable attempt button if passed and add button to review latest attempt in card --- .../task-scorm-card.component.html | 9 +++- .../task-scorm-card.component.ts | 49 ++++++++++++++++--- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html index ec385b4760..4781d88f2f 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html @@ -18,13 +18,20 @@ mat-stroked-button (click)="launchScormPlayer()" [hidden]="attemptsLeft === 0" - [disabled]="user.isStaff" + [disabled]="user.isStaff || checkIfPassed()" > launch Attempt test + - - - - +@if (isPassed) { + + @if (latestCompletedAttempt.scoreScaled === 1) { + + check + Knowledge Check Passed + + } + @if (latestCompletedAttempt.scoreScaled !== 1) { + + check + Knowledge Check Passed With Mistakes + + } + +

+ You have successfully completed this knowledge check. You can now proceed to submitting task + files. +

+
+ + + +
+} +@if (!isPassed) { + + @if (isPassed === false) { + + close + Knowledge Check Failed + + } + @if (isPassed === undefined) { + + Knowledge Check + + } + +

You have to successfully pass this knowledge check to complete the task.

+

+ You have {{ attemptsLeft !== undefined ? attemptsLeft : 'unlimited' }} attempts left to + complete this test. +

+

+ There will be an increased time delay between test attempts. First 2 attempts will not have + a time delay in between. +

+
+ + + + + +
+} diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts index 80744b104a..25fc9e479a 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts @@ -16,6 +16,7 @@ import {ScormExtensionModalService} from 'src/app/common/modals/scorm-extension- export class TaskScormCardComponent implements OnInit, OnChanges { @Input() task: Task; attemptsLeft: number; + isPassed: boolean; latestCompletedAttempt: TestAttempt; user: User; @@ -39,10 +40,13 @@ export class TaskScormCardComponent implements OnInit, OnChanges { refreshAttemptData(): void { this.attemptsLeft = undefined; - this.getAttemptsLeft(); + this.isPassed = undefined; this.latestCompletedAttempt = undefined; + + this.getAttemptsLeft(); this.testAttemptService.getLatestCompletedAttempt(this.task).subscribe((attempt) => { this.latestCompletedAttempt = attempt; + this.isPassed = attempt.successStatus; }); } @@ -57,13 +61,6 @@ export class TaskScormCardComponent implements OnInit, OnChanges { } } - checkIfPassed(): boolean { - if (this.latestCompletedAttempt) { - return this.latestCompletedAttempt.successStatus; - } - return false; - } - launchScormPlayer(): void { window.open( `#/projects/${this.task.project.id}/task_def_id/${this.task.taskDefId}/scorm-player/normal`, From 0fcb649a0346d78c04d179d2feccee7d920b6b53 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 14 Jun 2024 11:02:10 +1000 Subject: [PATCH 065/776] chore: ensure lf file endings --- .gitattributes | 2 ++ .gitignore | 1 - .vscode/settings.json | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 .vscode/settings.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..d56abbf304 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore index 9abc43ca67..c5c729aef4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ vendor/ .sass-cache* .bundle* tmp.scss -.vscode .tscache a.env dist/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..8582900e71 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "files.eol": "\n" +} From c5dd45c57fd34e8373f11d065a0cd08edef0a794 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 14 Jun 2024 11:27:05 +1000 Subject: [PATCH 066/776] fix: ensure tii open report alerts errors --- .../task-similarity-view.component.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component.ts index 2a6739207a..c407d28b4c 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component.ts @@ -18,7 +18,7 @@ export class TaskSimilarityViewComponent implements OnChanges { constructor( private taskSimilarityService: TaskSimilarityService, - private alertsService: AlertService, + private alertService: AlertService, private selectedTaskService: SelectedTaskService ) {} @@ -37,7 +37,7 @@ export class TaskSimilarityViewComponent implements OnChanges { this.taskSimilarityService .update({ taskId: similarity.task.id, id: similarity.id }, { entity: similarity }) .subscribe((_) => { - this.alertsService.success('Similarity flag updated'); + this.alertService.success('Similarity flag updated'); similarity.task.similarityFlag = similarity.task.similarityCache.currentValues .map((s) => { return s.flagged; @@ -50,8 +50,13 @@ export class TaskSimilarityViewComponent implements OnChanges { openReport(e: Event, similarity: TaskSimilarity) { e.stopPropagation(); // Open similarity report in new tab - similarity.fetchSimilarityReportUrl().subscribe((url) => { - window.open(url, '_blank'); + similarity.fetchSimilarityReportUrl().subscribe({ + next: (url) => { + window.open(url, '_blank'); + }, + error: (err) => { + this.alertService.error(`Error accessing TurnItIn: ${err}`); + }, }); } } From 110271cd7140a31e8dcfca377092ce46181164fc Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Sat, 15 Jun 2024 20:23:14 +1000 Subject: [PATCH 067/776] refactor: fix spacing in task scorm card --- .../task-scorm-card.component.html | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html index cec59fc939..81e11cf43c 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html @@ -17,17 +17,16 @@ } -

+

You have successfully completed this knowledge check. You can now proceed to submitting task files.

- - @@ -54,7 +53,7 @@ You have {{ attemptsLeft !== undefined ? attemptsLeft : 'unlimited' }} attempts left to complete this test.

-

+

There will be an increased time delay between test attempts. First 2 attempts will not have a time delay in between.

From 3b7576570394dc0122db73c7052dd5ed97bf0152 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Sat, 15 Jun 2024 20:25:41 +1000 Subject: [PATCH 068/776] refactor: add description for scorm time delay --- .../task-definition-scorm.component.html | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.html index 5b3b54c111..5321600cf5 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.html @@ -24,10 +24,7 @@
-
- - Enable incremental time delays between test attempts - +
Allow students to review completed test attempt @@ -44,5 +41,21 @@ />
+ +
+ + Enable incremental time delays between test attempts + + If enabled, first 2 attempts can be completed immediately. Subsequently, a time delay will + be added, increasing by 2 hours every attempt made. +
}
From ec86e4eca8e9c50ebdaf54c34c302da88dd44b31 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Sat, 15 Jun 2024 21:21:17 +1000 Subject: [PATCH 069/776] feat: prevent uploading files until scorm passed --- src/app/api/models/task-definition.ts | 1 + src/app/api/models/task.ts | 24 +++++++-- .../api/services/task-definition.service.ts | 1 + src/app/api/services/test-attempt.service.ts | 2 +- .../task-scorm-card.component.html | 9 ++-- .../task-scorm-card.component.ts | 54 +++++++------------ .../task-definition-scorm.component.html | 3 ++ 7 files changed, 50 insertions(+), 44 deletions(-) diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index ca2d188eb6..050466d740 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -34,6 +34,7 @@ export class TaskDefinition extends Entity { scormEnabled: boolean; hasScormData: boolean; scormAllowReview: boolean; + scormBypassTest: boolean; scormTimeDelayEnabled: boolean; scormAttemptLimit: number = 0; hasTaskAssessmentResources: boolean; diff --git a/src/app/api/models/task.ts b/src/app/api/models/task.ts index ba4bce8e04..2cadc79d72 100644 --- a/src/app/api/models/task.ts +++ b/src/app/api/models/task.ts @@ -512,9 +512,22 @@ export class Task extends Entity { } public get scormEnabled(): boolean { - return ( - this.definition.scormEnabled && this.definition.hasScormData - ); + return this.definition.scormEnabled && this.definition.hasScormData; + } + + public get scormPassed(): boolean { + if (this.latestCompletedTestAttempt) { + return this.latestCompletedTestAttempt.successStatus; + } + return false; + } + + public get isReadyForUpload(): boolean { + return !this.scormEnabled || this.definition.scormBypassTest || this.scormPassed; + } + + public get latestCompletedTestAttempt(): TestAttempt { + return this.testAttemptCache.currentValues.find((attempt) => attempt.terminated); } public submissionUrl(asAttachment: boolean = false): string { @@ -669,12 +682,15 @@ export class Task extends Entity { public triggerTransition(status: TaskStatusEnum): void { if (this.status === status) return; + const alerts: AlertService = AppInjector.get(AlertService); const requiresFileUpload = ['ready_for_feedback', 'need_help'].includes(status) && this.requiresFileUpload(); - if (requiresFileUpload) { + if (requiresFileUpload && this.isReadyForUpload) { this.presentTaskSubmissionModal(status); + } else if (requiresFileUpload && !this.isReadyForUpload) { + alerts.error('Complete Knowledge Check first to submit files', 6000); } else { this.updateTaskStatus(status); } diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index 3776432fd8..9a4a1eeb59 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -96,6 +96,7 @@ export class TaskDefinitionService extends CachedEntityService { 'scormEnabled', 'hasScormData', 'scormAllowReview', + 'scormBypassTest', 'scormTimeDelayEnabled', 'scormAttemptLimit', 'isGraded', diff --git a/src/app/api/services/test-attempt.service.ts b/src/app/api/services/test-attempt.service.ts index df5654855f..3e2c461619 100644 --- a/src/app/api/services/test-attempt.service.ts +++ b/src/app/api/services/test-attempt.service.ts @@ -11,7 +11,7 @@ import {HttpClient} from '@angular/common/http'; @Injectable() export class TestAttemptService extends CachedEntityService { protected readonly endpointFormat = - '/projects/:project_id:/task_def_id/:task_def_id:/test_attempts'; + 'projects/:project_id:/task_def_id/:task_def_id:/test_attempts'; protected readonly latestCompletedEndpoint = this.endpointFormat + '/latest?completed=:completed:'; diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html index 81e11cf43c..12df6a4936 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html @@ -1,6 +1,6 @@ @if (isPassed) { - @if (latestCompletedAttempt.scoreScaled === 1) { + @if (this.task.latestCompletedTestAttempt.scoreScaled === 1) { check @@ -8,7 +8,7 @@ > } - @if (latestCompletedAttempt.scoreScaled !== 1) { + @if (this.task.latestCompletedTestAttempt.scoreScaled !== 1) { check @@ -73,7 +73,10 @@ diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts index 25fc9e479a..9860d47741 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts @@ -1,11 +1,5 @@ -import {Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core'; -import { - Task, - TestAttempt, - TestAttemptService, - User, - UserService, -} from 'src/app/api/models/doubtfire-model'; +import {Component, Input, OnChanges, SimpleChanges} from '@angular/core'; +import {Task, User, UserService} from 'src/app/api/models/doubtfire-model'; import {ScormExtensionModalService} from 'src/app/common/modals/scorm-extension-modal/scorm-extension-modal.service'; @Component({ @@ -13,51 +7,39 @@ import {ScormExtensionModalService} from 'src/app/common/modals/scorm-extension- templateUrl: './task-scorm-card.component.html', styleUrls: ['./task-scorm-card.component.scss'], }) -export class TaskScormCardComponent implements OnInit, OnChanges { +export class TaskScormCardComponent implements OnChanges { @Input() task: Task; attemptsLeft: number; isPassed: boolean; - latestCompletedAttempt: TestAttempt; user: User; constructor( private extensions: ScormExtensionModalService, - private testAttemptService: TestAttemptService, private userService: UserService, ) { this.user = this.userService.currentUser; } - ngOnInit() { - this.refreshAttemptData(); - } - ngOnChanges(changes: SimpleChanges) { - if (changes.task && changes.task.currentValue) { - this.refreshAttemptData(); - } - } - - refreshAttemptData(): void { - this.attemptsLeft = undefined; - this.isPassed = undefined; - this.latestCompletedAttempt = undefined; + if (changes.task && changes.task.currentValue && changes.task.currentValue.scormEnabled) { + this.attemptsLeft = undefined; + this.isPassed = undefined; - this.getAttemptsLeft(); - this.testAttemptService.getLatestCompletedAttempt(this.task).subscribe((attempt) => { - this.latestCompletedAttempt = attempt; - this.isPassed = attempt.successStatus; - }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + this.task?.fetchTestAttempts().subscribe((_) => { + this.getAttemptsLeft(); + if (this.task.latestCompletedTestAttempt) this.isPassed = this.task.scormPassed; + }); + } } getAttemptsLeft(): void { if (this.task.definition.scormAttemptLimit != 0) { - this.task.fetchTestAttempts().subscribe((attempts) => { - let count = attempts.length; - if (count > 0 && attempts[0].terminated === false) count--; - this.attemptsLeft = - this.task.definition.scormAttemptLimit + this.task.scormExtensions - count; - }); + const attempts = this.task.testAttemptCache.currentValues; + let count = attempts.length; + if (count > 0 && attempts[0].terminated === false) count--; + this.attemptsLeft = + this.task.definition.scormAttemptLimit + this.task.scormExtensions - count; } } @@ -70,7 +52,7 @@ export class TaskScormCardComponent implements OnInit, OnChanges { reviewLatestCompletedAttempt(): void { window.open( - `#/task_def_id/${this.task.taskDefId}/scorm-player/review/${this.latestCompletedAttempt.id}`, + `#/task_def_id/${this.task.taskDefId}/scorm-player/review/${this.task.latestCompletedTestAttempt.id}`, '_blank', ); } diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.html index 5321600cf5..c2b89f9d7b 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component.html @@ -28,6 +28,9 @@ Allow students to review completed test attempt + + Allow file upload regardless of test pass status +
Attempt limit From 20da0423450fe3b9b8006660bf6f5e06670f520c Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 18 Jun 2024 19:04:03 +1000 Subject: [PATCH 070/776] fix: change success status descriptions --- .../task-scorm-card/task-scorm-card.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html index 12df6a4936..ac1221f87f 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html @@ -4,7 +4,7 @@ check - Knowledge Check PassedKnowledge Check Passed Without Mistakes } @@ -12,7 +12,7 @@ check - Knowledge Check Passed With MistakesKnowledge Check Passed } @@ -38,7 +38,7 @@ close - Knowledge Check FailedKnowledge Check Unsuccessful } From 14c66a7bc8c1d75f1736d4e1682e78dca1e365de Mon Sep 17 00:00:00 2001 From: jakerenzella Date: Mon, 3 Jun 2024 23:20:12 +1000 Subject: [PATCH 071/776] build: upgrade pdf viewer --- package-lock.json | 388 ++++++++++++++++++++++++++++++++++++++++------ package.json | 4 +- 2 files changed, 340 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index f1bfcafa3d..92e5cafe0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,8 +62,8 @@ "moment": "^2.29.4", "ng-csv": "0.2.3", "ng-file-upload": "~5.0.9", - "ng-flex-layout": "^17.3.4-beta.1", - "ng2-pdf-viewer": "^10.0", + "ng-flex-layout": "^17.3.7-beta.1", + "ng2-pdf-viewer": "^10.2", "ngx-bootstrap": "^6.1.0", "ngx-entity-service": "^0.0.38", "ngx-lottie": "^11.0.2", @@ -3836,6 +3836,90 @@ "node": ">= 0.4" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "optional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@material/animation": { "version": "15.0.0-canary.7f224ddd4.0", "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.7f224ddd4.0.tgz", @@ -6926,6 +7010,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -8134,6 +8238,21 @@ "integrity": "sha512-y8EIEvL+IW81S4hRQWCRFtly+g1cc1G+wxHpjhYR9jI2+JJjWiaKnkH8mmvNHOMOAd9fzgARDO3AEzjuR51qaA==", "dev": true }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/canvas-confetti": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.3.tgz", @@ -8573,6 +8692,15 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -8701,7 +8829,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "devOptional": true }, "node_modules/concurrently": { "version": "3.6.1", @@ -8805,6 +8933,12 @@ "date-now": "^0.1.4" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, "node_modules/constantinople": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", @@ -9452,6 +9586,18 @@ "node": ">=0.10" } }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -9620,6 +9766,12 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -9657,6 +9809,15 @@ "node": ">=0.10.0" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -9795,12 +9956,6 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/dommatrix": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dommatrix/-/dommatrix-1.0.3.tgz", - "integrity": "sha512-l32Xp/TLgWb8ReqbVJAFIvXmY7go4nTxxlWiAFyhoQw9RKEOHBZNnyGvJWqDVSPmq3Y9HlM4npqF/T6VMOXhww==", - "deprecated": "dommatrix is no longer maintained. Please use @thednp/dommatrix." - }, "node_modules/domutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", @@ -11649,7 +11804,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "devOptional": true }, "node_modules/function-bind": { "version": "1.1.2", @@ -11686,6 +11841,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/gaze": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", @@ -11830,7 +12006,7 @@ "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, + "devOptional": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -11868,7 +12044,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -11878,7 +12054,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -13455,6 +13631,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, "node_modules/has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -13984,7 +14166,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, + "devOptional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -16580,6 +16762,18 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -16999,6 +17193,12 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nan": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", + "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -17191,9 +17391,9 @@ "integrity": "sha512-298k/s42+cyHLFAWCtgmXHGOUFqAh5VviSbMuQAPfqAavK1h9ADU/A+gN7OLmgNhW9PM94bWSt22LpjO/cvKwA==" }, "node_modules/ng-flex-layout": { - "version": "17.3.4-beta.1", - "resolved": "https://registry.npmjs.org/ng-flex-layout/-/ng-flex-layout-17.3.4-beta.1.tgz", - "integrity": "sha512-MmXj7Cq9cBSDgJKT3vOok2YCq3OcUD4zhPaKup9V5lVJLZ38sHPgfPf5fFBjDZO7R6AD66wYPod0i4zW5LrrgA==", + "version": "17.3.7-beta.1", + "resolved": "https://registry.npmjs.org/ng-flex-layout/-/ng-flex-layout-17.3.7-beta.1.tgz", + "integrity": "sha512-MTjlQUldB/hEsn0DY/RoY2SKBQoyaKltlOOFjCjH2OfcYA3COHhkJS3PZe9NAjR7TBDb9Ve989m18bSucRzACQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -17206,15 +17406,12 @@ } }, "node_modules/ng2-pdf-viewer": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/ng2-pdf-viewer/-/ng2-pdf-viewer-10.0.0.tgz", - "integrity": "sha512-zEefcAsTpDoxFceQYs3ycPMaUAkt5UX4OcTstVQoNqRK6w+vOY+V8z8aFCuBwnt+7iN1EHaIpquOf4S9mWc04g==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/ng2-pdf-viewer/-/ng2-pdf-viewer-10.2.2.tgz", + "integrity": "sha512-GaKAvF0nXAiR9U4LFWuT54MM9nzp0ie8GGscp34W+lFsSOXdlwS0iFx5UPuVlODRm3YEUKx6xcK5oaJeBq0SAw==", "dependencies": { - "pdfjs-dist": "~2.16.105", + "pdfjs-dist": "^3.11.174", "tslib": "^2.3.0" - }, - "peerDependencies": { - "pdfjs-dist": "~2.16.105" } }, "node_modules/ngx-bootstrap": { @@ -17288,6 +17485,26 @@ "dev": true, "optional": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -17694,6 +17911,19 @@ "node": ">=8" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -17963,7 +18193,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -18139,7 +18369,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, + "devOptional": true, "dependencies": { "wrappy": "1" } @@ -18617,7 +18847,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -18700,21 +18930,25 @@ "node": ">=8" } }, + "node_modules/path2d-polyfill": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", + "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/pdfjs-dist": { - "version": "2.16.105", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.16.105.tgz", - "integrity": "sha512-J4dn41spsAwUxCpEoVf6GVoz908IAA3mYiLmNxg8J9kfRXc2jxpbUepcP0ocp0alVNLFthTAM8DZ1RaHh8sU0A==", - "dependencies": { - "dommatrix": "^1.0.3", - "web-streams-polyfill": "^3.2.1" - }, - "peerDependencies": { - "worker-loader": "^3.0.8" + "version": "3.11.174", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", + "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", + "engines": { + "node": ">=18" }, - "peerDependenciesMeta": { - "worker-loader": { - "optional": true - } + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d-polyfill": "^2.0.1" } }, "node_modules/performance-now": { @@ -20919,7 +21153,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, + "devOptional": true, "dependencies": { "glob": "^7.1.3" }, @@ -21794,12 +22028,43 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, "node_modules/simple-fmt": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/simple-fmt/-/simple-fmt-0.1.0.tgz", "integrity": "sha512-9a3zTDDh9LXbTR37qBhACWIQ/mP/ry5xtmbE98BJM8GR02sanCkfMzp7AdCTqYhkBZggK/w7hJtc8Pb9nmo16A==", "dev": true }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-is": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/simple-is/-/simple-is-0.2.0.tgz", @@ -23174,6 +23439,12 @@ "node": ">=0.8" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -24040,14 +24311,6 @@ "defaults": "^1.0.3" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "engines": { - "node": ">= 8" - } - }, "node_modules/webdriver-js-extender": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz", @@ -24168,6 +24431,12 @@ "node": ">=0.8.0" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, "node_modules/webpack": { "version": "5.90.3", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", @@ -24490,6 +24759,16 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -24544,6 +24823,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", @@ -24694,7 +24982,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "devOptional": true }, "node_modules/ws": { "version": "8.11.0", diff --git a/package.json b/package.json index 272c6019b9..3f95891a81 100644 --- a/package.json +++ b/package.json @@ -79,8 +79,8 @@ "moment": "^2.29.4", "ng-csv": "0.2.3", "ng-file-upload": "~5.0.9", - "ng-flex-layout": "^17.3.4-beta.1", - "ng2-pdf-viewer": "^10.0", + "ng-flex-layout": "^17.3.7-beta.1", + "ng2-pdf-viewer": "^10.2", "ngx-bootstrap": "^6.1.0", "ngx-entity-service": "^0.0.38", "ngx-lottie": "^11.0.2", From da376b9bc7905aadeb4d234cbb4f595e4c4424a1 Mon Sep 17 00:00:00 2001 From: jakerenzella Date: Sat, 15 Jun 2024 00:10:27 +1000 Subject: [PATCH 072/776] fix: fix pdf viewer for portfolios --- .../pdf-viewer/pdf-viewer.component.html | 41 +++++++--- .../common/pdf-viewer/pdf-viewer.component.ts | 9 +- .../units/states/portfolios/portfolios.coffee | 4 +- .../states/portfolios/portfolios.tpl.html | 82 +++++++++++++++---- 4 files changed, 107 insertions(+), 29 deletions(-) diff --git a/src/app/common/pdf-viewer/pdf-viewer.component.html b/src/app/common/pdf-viewer/pdf-viewer.component.html index 232c89d5ed..87dc97c996 100644 --- a/src/app/common/pdf-viewer/pdf-viewer.component.html +++ b/src/app/common/pdf-viewer/pdf-viewer.component.html @@ -3,25 +3,44 @@
search - + - -
@if (pdfBlobUrl) { - + }
diff --git a/src/app/common/pdf-viewer/pdf-viewer.component.ts b/src/app/common/pdf-viewer/pdf-viewer.component.ts index 23d5029b2f..5cd893b5a9 100644 --- a/src/app/common/pdf-viewer/pdf-viewer.component.ts +++ b/src/app/common/pdf-viewer/pdf-viewer.component.ts @@ -7,6 +7,8 @@ import { SimpleChanges, OnChanges, ViewChild, + OnInit, + AfterViewInit, } from '@angular/core'; import {PdfViewerComponent} from 'ng2-pdf-viewer'; import {FileDownloaderService} from '../file-downloader/file-downloader.service'; @@ -17,7 +19,7 @@ import {AlertService} from '../services/alert.service'; templateUrl: './pdf-viewer.component.html', styleUrls: ['./pdf-viewer.component.scss'], }) -export class fPdfViewerComponent implements OnDestroy, OnChanges { +export class fPdfViewerComponent implements OnDestroy, OnChanges, AfterViewInit { private _pdfUrl: string; public pdfBlobUrl: string; @Input() pdfUrl: string; @@ -38,6 +40,11 @@ export class fPdfViewerComponent implements OnDestroy, OnChanges { } } + ngAfterViewInit(): void { + console.log("pdfUrl"); + console.log(this.pdfUrl); + } + ngOnChanges(changes: SimpleChanges): void { this.pdfUrlChanges(changes.pdfUrl.currentValue); } diff --git a/src/app/units/states/portfolios/portfolios.coffee b/src/app/units/states/portfolios/portfolios.coffee index 7e745b33d1..41d0b60797 100644 --- a/src/app/units/states/portfolios/portfolios.coffee +++ b/src/app/units/states/portfolios/portfolios.coffee @@ -122,7 +122,9 @@ angular.module('doubtfire.units.states.portfolios', []) $scope.selectedStudent = student $scope.project = null newProjectService.loadProject(student, $scope.unit).subscribe({ - next: (project) -> $scope.project = project + next: (project) -> + $scope.project = project + $scope.project.preloadedUrl = $scope.project.portfolioUrl() error: (message) -> alertService.error( message, 6000) }) ) diff --git a/src/app/units/states/portfolios/portfolios.tpl.html b/src/app/units/states/portfolios/portfolios.tpl.html index 075680654c..2974d0719a 100644 --- a/src/app/units/states/portfolios/portfolios.tpl.html +++ b/src/app/units/states/portfolios/portfolios.tpl.html @@ -27,7 +27,11 @@

Mark Portfolios

>
@@ -39,7 +43,11 @@

Mark Portfolios

-

No portfolios found

- +
@@ -172,7 +210,9 @@

Mark Portfolios

- + @@ -237,7 +277,13 @@

Review Portfolio of {{selectedStudent.student.name}}

No Portfolio Submitted

- +
+ + +
@@ -260,8 +306,12 @@

Grade for {{selectedStudent.student.name}}

ng-class="{'no-rationale': project.gradeRationale == null}" ng-hide="editingRationale" > - -
+ +
Click to add one
From a2e8a9c1bef2f2cbf3a32683ee84302c061b90ef Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 18 Jun 2024 20:24:39 +1000 Subject: [PATCH 073/776] refactor: show consecutive scorm comments together --- .../api/models/task-comment/scorm-comment.ts | 3 ++ src/app/api/models/task.ts | 9 ++++ .../scorm-comment.component.html | 54 +++++++++++-------- .../scorm-comment.component.scss | 6 +-- .../scorm-comment/scorm-comment.component.ts | 4 -- 5 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src/app/api/models/task-comment/scorm-comment.ts b/src/app/api/models/task-comment/scorm-comment.ts index 3356b62b93..b15a2c50c8 100644 --- a/src/app/api/models/task-comment/scorm-comment.ts +++ b/src/app/api/models/task-comment/scorm-comment.ts @@ -3,6 +3,9 @@ import {Task, TaskComment, TestAttempt} from '../doubtfire-model'; export class ScormComment extends TaskComment { testAttempt: TestAttempt; + // UI rendering data + lastInScormSeries: boolean = false; + constructor(task: Task) { super(task); } diff --git a/src/app/api/models/task.ts b/src/app/api/models/task.ts index 2cadc79d72..0643f99732 100644 --- a/src/app/api/models/task.ts +++ b/src/app/api/models/task.ts @@ -17,6 +17,7 @@ import { TaskSimilarityService, TestAttempt, TestAttemptService, + ScormComment, } from './doubtfire-model'; import {Grade} from './grade'; import {LOCALE_ID} from '@angular/core'; @@ -385,6 +386,14 @@ export class Task extends Entity { if (comments[i].replyToId) { comments[i].originalComment = comments.find((tc) => tc.id === comments[i].replyToId); } + + // Scorm series + if (comments[i].commentType === 'scorm') { + comments[i].firstInSeries = i === 0 || comments[i - 1].commentType !== 'scorm'; + (comments[i] as ScormComment).lastInScormSeries = + i + 1 === comments.length || comments[i + 1]?.commentType !== 'scorm'; + if (!comments[i].firstInSeries) comments[i].shouldShowTimestamp = false; + } } comments[comments.length - 1].shouldShowAvatar = true; diff --git a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html index c1dacaa788..edde1e057c 100644 --- a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html +++ b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html @@ -1,24 +1,32 @@ -
-
-
-
-
-
- - - -
-
-
-
+
+
+ +
+ + + + +
+
diff --git a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.scss b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.scss index 31df73023a..7bc5f74d91 100644 --- a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.scss +++ b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.scss @@ -1,7 +1,3 @@ -div { - width: 100%; -} - p { color: #2c2c2c; text-align: center; @@ -14,7 +10,7 @@ hr { .hr-fade { background: linear-gradient(to right, transparent, #9696969d, transparent); width: 100%; - margin-top: 1px; + margin-top: 6px; } .hr-text { diff --git a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts index 87daf5b6ca..a16e397ad6 100644 --- a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts +++ b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts @@ -27,10 +27,6 @@ export class ScormCommentComponent { this.user = this.userService.currentUser; } - get canOverridePass(): boolean { - return this.user.isStaff && !this.comment.testAttempt.successStatus; - } - reviewScormTest() { window.open( `#/task_def_id/${this.task.taskDefId}/scorm-player/review/${this.comment.testAttempt.id}`, From b6887e85b7a06b166519924dd682ef57650a4e32 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 18 Jun 2024 20:46:43 +1000 Subject: [PATCH 074/776] fix: delete comment as well as test attempt --- .../scorm-comment/scorm-comment.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts index a16e397ad6..d26b914f7e 100644 --- a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts +++ b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts @@ -50,6 +50,7 @@ export class ScormCommentComponent { 'Are you sure you want to delete this test attempt? This action is final and will delete information associated with this test attempt.', () => { this.testAttemptService.deleteAttempt(this.comment.testAttempt.id); + this.comment.delete(); }, ); } From fd916b94e9012c0909c5880f377f93d326c18e4d Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 20 Jun 2024 22:09:57 +1000 Subject: [PATCH 075/776] fix: ensure portfolio can get grades from strings --- src/app/common/grade-icon/grade-icon.coffee | 2 +- src/app/common/services/grade.service.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/common/grade-icon/grade-icon.coffee b/src/app/common/grade-icon/grade-icon.coffee index be0ed0b0c3..54480aff9f 100644 --- a/src/app/common/grade-icon/grade-icon.coffee +++ b/src/app/common/grade-icon/grade-icon.coffee @@ -9,7 +9,7 @@ angular.module('doubtfire.common.grade-icon', []) colorful: '=?' controller: ($scope, gradeService) -> $scope.$watch 'inputGrade', (newGrade) -> - $scope.grade = if _.isString($scope.inputGrade) then gradeService.grades.indexOf($scope.inputGrade) else $scope.inputGrade + $scope.grade = if _.isString($scope.inputGrade) then gradeService.stringToGrade($scope.inputGrade) else $scope.inputGrade $scope.gradeText = (grade) -> if $scope.grade? then gradeService.grades[$scope.grade] or "Grade" $scope.gradeLetter = (grade) -> diff --git a/src/app/common/services/grade.service.ts b/src/app/common/services/grade.service.ts index 619bad36f5..35e0f24f2a 100644 --- a/src/app/common/services/grade.service.ts +++ b/src/app/common/services/grade.service.ts @@ -16,6 +16,10 @@ export class GradeService { 3: 'High Distinction', }; + public stringToGrade(value: string): number { + return this.gradeViewData.find((grade) => grade.viewValue === value)?.value; + } + gradeViewData = [ {value: -1, viewValue: 'Fail'}, {value: 0, viewValue: 'Pass'}, From ce970f5fdc4f42708edc71c98dc9d869056e9e2c Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 21 Jun 2024 21:21:07 +1000 Subject: [PATCH 076/776] fix: ensure portfolio only shown when it exists --- src/app/units/states/portfolios/portfolios.tpl.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/units/states/portfolios/portfolios.tpl.html b/src/app/units/states/portfolios/portfolios.tpl.html index a51934b9a5..2b8a2277eb 100644 --- a/src/app/units/states/portfolios/portfolios.tpl.html +++ b/src/app/units/states/portfolios/portfolios.tpl.html @@ -277,7 +277,7 @@

Review Portfolio of {{selectedStudent.student.name}}

No Portfolio Submitted

-
+
Date: Fri, 21 Jun 2024 21:21:27 +1000 Subject: [PATCH 077/776] chore(release): 8.0.10 --- CHANGELOG.md | 326 ++++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 4 +- package.json | 2 +- 3 files changed, 329 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ca7d64128..887dcb3746 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,332 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [8.0.10](https://github.com/macite/doubtfire-deploy/compare/v7.0.23...v8.0.10) (2024-06-21) + + +### Bug Fixes + +* ensure loading screen removed in sign in component ([729c438](https://github.com/macite/doubtfire-deploy/commit/729c438f7f6a988f5ff2fa4035493b5cae1c98f7)) +* ensure portfolio can get grades from strings ([fd916b9](https://github.com/macite/doubtfire-deploy/commit/fd916b94e9012c0909c5880f377f93d326c18e4d)) +* ensure portfolio only shown when it exists ([ce970f5](https://github.com/macite/doubtfire-deploy/commit/ce970f5fdc4f42708edc71c98dc9d869056e9e2c)) +* ensure tii open report alerts errors ([c5dd45c](https://github.com/macite/doubtfire-deploy/commit/c5dd45c57fd34e8373f11d065a0cd08edef0a794)) +* fix pdf viewer for portfolios ([da376b9](https://github.com/macite/doubtfire-deploy/commit/da376b9bc7905aadeb4d234cbb4f595e4c4424a1)) +* grade icon for portfolio page ([aab1b9c](https://github.com/macite/doubtfire-deploy/commit/aab1b9c3e7365cdf7dd64d767fdb6e5a9a2436a3)) +* show burndown and pie chart on portfolio page ([e70f4c7](https://github.com/macite/doubtfire-deploy/commit/e70f4c7cd1395eaab942ee389788f75f92e985c9)) + +### [8.0.9](https://github.com/macite/doubtfire-deploy/compare/v7.0.22...v8.0.9) (2024-05-31) + + +### Bug Fixes + +* comment out fix and complete hotkeys temporarily ([c1f623d](https://github.com/macite/doubtfire-deploy/commit/c1f623d17432f3563366ef32fc3da3b54dbef0b7)) + +### [8.0.8](https://github.com/macite/doubtfire-deploy/compare/v8.0.7...v8.0.8) (2024-05-30) + + +### Features + +* added percentage to tooltip when hovering student's task progress ([69ed613](https://github.com/macite/doubtfire-deploy/commit/69ed61380bd7681d4496cb109e628111873b6e82)) + + +### Bug Fixes + +* change from hiding element to not loading ([414da65](https://github.com/macite/doubtfire-deploy/commit/414da65fcef9b10367f53112239a60e1b271d475)) +* change grade values, change grade display ([081c35b](https://github.com/macite/doubtfire-deploy/commit/081c35ba0fbc7ab4ddb2fba3c58892d0e66f325c)) +* fix task inbox shortcuts ([9bc949f](https://github.com/macite/doubtfire-deploy/commit/9bc949fd6dcb71ea165e89de9a4ec9d9347e6562)) +* inbox search box width ([02a99a8](https://github.com/macite/doubtfire-deploy/commit/02a99a8f71033014b26260d3b0dbfca075ca7273)) +* remove pdf viewer on task view when no sheet ([f3e973d](https://github.com/macite/doubtfire-deploy/commit/f3e973dacc5b079ab58da239194d802c5586ca07)) +* revert index changes, html changes for grade ([8f41cd1](https://github.com/macite/doubtfire-deploy/commit/8f41cd1656971963c2035a9249acdde0a257766e)) +* task list scrolling ([0088e9e](https://github.com/macite/doubtfire-deploy/commit/0088e9e245dbd326dea4032239b53eee88754179)) +* use tailwind classes instead of css styling ([42434a5](https://github.com/macite/doubtfire-deploy/commit/42434a5fd4b866781da5d23ecdb0b9b4369aace1)) + +### [8.0.7](https://github.com/macite/doubtfire-deploy/compare/v8.0.6...v8.0.7) (2024-05-26) + + +### Bug Fixes + +* add task description editing ([ee0cfbf](https://github.com/macite/doubtfire-deploy/commit/ee0cfbf7ce804a1806ca5810bad138b97703ebfe)) + +### [8.0.6](https://github.com/macite/doubtfire-deploy/compare/v8.0.5...v8.0.6) (2024-05-25) + + +### Bug Fixes + +* ensure all admin units are fully loaded ([917325e](https://github.com/macite/doubtfire-deploy/commit/917325ed08b89690a7eea0716438673732ba1ec9)) + +### [8.0.5](https://github.com/macite/doubtfire-deploy/compare/v8.0.4...v8.0.5) (2024-05-25) + + +### Bug Fixes + +* ensure (click) is on button, not mat-icon ([99c717d](https://github.com/macite/doubtfire-deploy/commit/99c717d2333de0975da1b3f282195a0b3577a3f6)) + +### [8.0.4](https://github.com/macite/doubtfire-deploy/compare/v8.0.3...v8.0.4) (2024-05-25) + + +### Bug Fixes + +* fix various issues with admin unit list ([3720666](https://github.com/macite/doubtfire-deploy/commit/3720666fb4105bac274449eb468ce34e0f68bae7)) + +### [8.0.3](https://github.com/macite/doubtfire-deploy/compare/v8.0.2...v8.0.3) (2024-05-25) + + +### Features + +* add 4-character abbreviations ([eb62dfc](https://github.com/macite/doubtfire-deploy/commit/eb62dfc90db836870d6c396727fc43c149808e9d)) + +### [8.0.2](https://github.com/macite/doubtfire-deploy/compare/v8.0.1...v8.0.2) (2024-05-25) + + +### Bug Fixes + +* gradeservice bugs ([fd6eb1c](https://github.com/macite/doubtfire-deploy/commit/fd6eb1cd152b72cf297cbd6bd529579858afc543)) +* gradeservice bugs ([7545eb9](https://github.com/macite/doubtfire-deploy/commit/7545eb92b9353d99ac8d8d7d89982cdadf9688b1)) + +### [8.0.1](https://github.com/macite/doubtfire-deploy/compare/v8.0.0...v8.0.1) (2024-05-24) + + +### Features + +* implement live PDF search that updates dynamically as you type ([b9b9945](https://github.com/macite/doubtfire-deploy/commit/b9b9945346d55392e5ee9a6431d85ae92de1bde5)) + + +### Bug Fixes + +* fix critical bugs ([356846e](https://github.com/macite/doubtfire-deploy/commit/356846e5c385c484a6034553366965cde95ec78b)) +* fix splashscreen ([2692b7f](https://github.com/macite/doubtfire-deploy/commit/2692b7f4a3e1ca2a5bc4df3b82a9dfd4aa2ec68a)) + +## [8.0.0](https://github.com/macite/doubtfire-deploy/compare/v8.0.0-6...v8.0.0) (2024-05-23) + + +### Features + +* add multi-badge support ([44c1473](https://github.com/macite/doubtfire-deploy/commit/44c1473700a3d7a4a025d68acac5aa61189d8b52)) +* add synchronised multi-badge ([f70ebdc](https://github.com/macite/doubtfire-deploy/commit/f70ebdca92a05e5ca350d4062a8dc8efba9fd681)) +* add working unit-badge ([dd30247](https://github.com/macite/doubtfire-deploy/commit/dd302478e97228e6bfbfdbee0134abb3eabf2587)) +* add working unit-badge ([c7338ff](https://github.com/macite/doubtfire-deploy/commit/c7338ffd88edb457a3443b43ea21706c43231973)) +* qol improvements for unit codes ([f7f928e](https://github.com/macite/doubtfire-deploy/commit/f7f928e485448a69b054dca94f07d623f84e328f)) +* small fixes to home and unit code ([918e804](https://github.com/macite/doubtfire-deploy/commit/918e8047745b489aae1fafdc67720568dd8d829e)) + + +### Bug Fixes + +* add missing imports ([659b52c](https://github.com/macite/doubtfire-deploy/commit/659b52c82a26b9496a3dba81401b5147d05248ae)) +* change time exceeded to feedback exceeded ([673619d](https://github.com/macite/doubtfire-deploy/commit/673619d99f177d834fba3643171426092e0d05e0)) +* correct task resource upload actions ([5e9c9b1](https://github.com/macite/doubtfire-deploy/commit/5e9c9b1d05d6d6975fd4ba3a3176314ddd7d8514)) +* ensure loading state changed after projects and unit roles load ([9171d0c](https://github.com/macite/doubtfire-deploy/commit/9171d0c2c37c17d26f464f6aa0ba7c479851e6eb)) +* ensure staff task list checks unit exists ([dbb312d](https://github.com/macite/doubtfire-deploy/commit/dbb312dc3c5c9e2be24161914e8d95e1a5c1bc53)) +* ensure unit load updates unit object if it already exists ([e48ffad](https://github.com/macite/doubtfire-deploy/commit/e48ffadf63891212c3e40ab696d54a3935485b08)) +* hide overseer settings if automation disabled ([0c08fe3](https://github.com/macite/doubtfire-deploy/commit/0c08fe3e82a78dd1e91fb44ce6008fb5298c46ee)) +* slide unit code off left side ([8cd57b5](https://github.com/macite/doubtfire-deploy/commit/8cd57b589812fd59c6da4de02246791fb4410959)) +* sort units desc by start date ([9b78856](https://github.com/macite/doubtfire-deploy/commit/9b788561372a3ddf065d3b28e547a2194ef29655)) +* zip upload for overseer ([5ad2085](https://github.com/macite/doubtfire-deploy/commit/5ad2085b2d730235f61e7b7c603727d8b6736cf2)) + +## [8.0.0-6](https://github.com/macite/doubtfire-deploy/compare/v8.0.0-5...v8.0.0-6) (2024-05-13) + +## [8.0.0-5](https://github.com/macite/doubtfire-deploy/compare/v8.0.0-4...v8.0.0-5) (2024-05-13) + +## [8.0.0-4](https://github.com/macite/doubtfire-deploy/compare/v8.0.0-3...v8.0.0-4) (2024-05-11) + + +### Features + +* add task badge ([b48cd4c](https://github.com/macite/doubtfire-deploy/commit/b48cd4c99333e41c0c1d32531e571d32bb482a81)) + + +### Bug Fixes + +* (WIP) fix alert service migration ([3d9f4dc](https://github.com/macite/doubtfire-deploy/commit/3d9f4dc05e7a45884dc383e0f5ed4bdb09b0558d)) +* add max length validator and change reason field to text-area ([6227200](https://github.com/macite/doubtfire-deploy/commit/6227200566c3f608c279e92a3f255a7e94148c1d)) +* address linter issues in task.ts ([14e9699](https://github.com/macite/doubtfire-deploy/commit/14e9699bdfe336f1e3f3cce3d722c835d58f3cdb)) +* copy dist browser to nginx html ([08fa919](https://github.com/macite/doubtfire-deploy/commit/08fa91937e028cb3ee093709e5a31f94da44fc33)) +* correct filenames for grotesk fonts ([9bedd81](https://github.com/macite/doubtfire-deploy/commit/9bedd812b12628df9cb32e48cb91a88b5cdf69b1)) +* ensure eula state remains visible ([131540c](https://github.com/macite/doubtfire-deploy/commit/131540ca4b934773afd9f72aa2578579048ed6a2)) +* ensure unit admin shows all units ([02b7a15](https://github.com/macite/doubtfire-deploy/commit/02b7a15bbbd2306a61e344f995dffb72a3915498)) +* fix course progress bar on firefox ([ae5c1e0](https://github.com/macite/doubtfire-deploy/commit/ae5c1e0b5901bae0411cf7bbaa32ee8f340b908a)) +* fix incorrect projcets listed in dropdown ([6a7e478](https://github.com/macite/doubtfire-deploy/commit/6a7e4788b522e5af6331a82ed33e9c9137d2ca86)) +* fix margin issue on dropdown ([c8145f0](https://github.com/macite/doubtfire-deploy/commit/c8145f0972a03e52377e0203cabb2cf6353db8b3)) +* grade and quality point display on task-assessment-card ([7cba502](https://github.com/macite/doubtfire-deploy/commit/7cba502e002143e8255bf0d786551bd270f73f0e)) +* indicate mandatory fields in upload-submission-modal ([e8801e2](https://github.com/macite/doubtfire-deploy/commit/e8801e2a64fe15ad423d9017d10e5e23f6e00eda)) +* only check grade if it exists ([1c213c5](https://github.com/macite/doubtfire-deploy/commit/1c213c53571f79563e9a071e2dd9645598382522)) +* remove unused imports in extension-modal.service ([8081f51](https://github.com/macite/doubtfire-deploy/commit/8081f51b5f28ea511b9f76d026ae63f16153ae2c)) +* use tailwindcss classes ([8d7e160](https://github.com/macite/doubtfire-deploy/commit/8d7e160e2d40c42284ee0c1fc85d6d9e01166cda)) + +## [8.0.0-3](https://github.com/macite/doubtfire-deploy/compare/v8.0.0-2...v8.0.0-3) (2024-05-02) + + +### Bug Fixes + +* reenable eula html fetch ([cda9e52](https://github.com/macite/doubtfire-deploy/commit/cda9e52a259bfaa27e8252f40e579d11d1af280c)) + +## [8.0.0-2](https://github.com/macite/doubtfire-deploy/compare/v7.0.21...v8.0.0-2) (2024-05-01) + + +### Features + +* accept promela files ([df93a69](https://github.com/macite/doubtfire-deploy/commit/df93a69e45d2d9e41b9cf0802d521a3663d7a36c)) +* migrate unit list in all locations ([6651d00](https://github.com/macite/doubtfire-deploy/commit/6651d0011446fdde7179c5173f902ce2e4ea7545)) +* remove misleading progress statement ([63e276c](https://github.com/macite/doubtfire-deploy/commit/63e276c041f77d8b401d24a4f49896cfa2095442)) + + +### Bug Fixes + +* correct change detection on task def ([55daff2](https://github.com/macite/doubtfire-deploy/commit/55daff234441a242d96de097c2b2912df0c6c9a8)) +* correct npm install for dev container image ([5126c3b](https://github.com/macite/doubtfire-deploy/commit/5126c3ba7fed55217e88c54c25dc70595ec4c666)) +* ensure npm will force install ([f91751e](https://github.com/macite/doubtfire-deploy/commit/f91751eae788bc3f65d33b610ed80c96d3978974)) +* fix firefox-specific bug ([6b6054b](https://github.com/macite/doubtfire-deploy/commit/6b6054bd5c1104a96ccac413a7115009eeba8d2f)) +* remove ngClass ([d1d8c72](https://github.com/macite/doubtfire-deploy/commit/d1d8c72ace21551a68bd4d0a8ba88eabb2ebdb9d)) +* remove plagiarism checks ([21b02fb](https://github.com/macite/doubtfire-deploy/commit/21b02fb7cc616090f1fb2d26f65b336072f9105b)) +* remove the incorrect option from for scrollIntoView() ([9e752fe](https://github.com/macite/doubtfire-deploy/commit/9e752fe9d49df786ceb3e7007cbf0aad57809604)) +* remove unused loading from f-units ([c4c38fa](https://github.com/macite/doubtfire-deploy/commit/c4c38fad4fbdfe248e383cf247f949197ad0a047)) +* typo ([820c56f](https://github.com/macite/doubtfire-deploy/commit/820c56f3a1bc30b7637b98e6f80b406ca350b5b8)) + +## [8.0.0-1](https://github.com/macite/doubtfire-deploy/compare/v8.0.0-0...v8.0.0-1) (2024-03-21) + + +### Bug Fixes + +* update deploy dockerfile ([41a5161](https://github.com/macite/doubtfire-deploy/commit/41a5161b356931eafd8e4caf256f311f10bc8bb4)) + +## [8.0.0-0](https://github.com/macite/doubtfire-deploy/compare/v7.0.18...v8.0.0-0) (2024-03-21) + + +### Features + +* (wip) add similarities notification ([8e4a198](https://github.com/macite/doubtfire-deploy/commit/8e4a198a037de832bbebe96e309c4c8485d076cc)) +* add ability to accept tii eula to model ([0106b01](https://github.com/macite/doubtfire-deploy/commit/0106b01ed249139cbe06096f95a36b5d11bcd0ac)) +* add ability to accept turn it in eula ([380ad1e](https://github.com/macite/doubtfire-deploy/commit/380ad1e93252f8f928893626b65f4dab01813524)) +* add ability to open turnitin viewer ([5248e80](https://github.com/macite/doubtfire-deploy/commit/5248e80622e55b79ce4d7b35f8202ee8099c2dce)) +* add ability to update similarity flag ([de005ee](https://github.com/macite/doubtfire-deploy/commit/de005ee98a270998e2175109783cab72b92e36f3)) +* add dynamic footer ([3c62fe0](https://github.com/macite/doubtfire-deploy/commit/3c62fe06d009c3b752259f50887c625c98a371cd)) +* add empty pdf view to new task dashboard ([9c3cdfb](https://github.com/macite/doubtfire-deploy/commit/9c3cdfbb93711a6ae911ed461651200d8c75db48)) +* add file drop component ([c211003](https://github.com/macite/doubtfire-deploy/commit/c211003004ae6c7505158a65ce2006b219920827)) +* add footer content ([735c43b](https://github.com/macite/doubtfire-deploy/commit/735c43b5c2df7e8f367310ad92ef716fd5140375)) +* add link to student task from inbox ([d4301e0](https://github.com/macite/doubtfire-deploy/commit/d4301e0d1f2f6ec4c563801a7f0d1132884f9989)) +* add mobile inbox view ([56ded70](https://github.com/macite/doubtfire-deploy/commit/56ded707e638ae769665a863ce65acffea5a588a)) +* add multi-file support to f-upload ([d181969](https://github.com/macite/doubtfire-deploy/commit/d181969198e2b7611e3f0e330577b825e99a248f)) +* add new file upload component ([891ab20](https://github.com/macite/doubtfire-deploy/commit/891ab20eb6f1d545ff58d0157fdd9912b9546c9f)) +* add new inbox component ([ec0f62c](https://github.com/macite/doubtfire-deploy/commit/ec0f62c488ae3e99e9487ea1ab87f5bb2f1fbed7)) +* add new pdf-viewer ([617fbd2](https://github.com/macite/doubtfire-deploy/commit/617fbd24f8424541aeb87b16651f06737b570633)) +* add new similarity component ([48ba01c](https://github.com/macite/doubtfire-deploy/commit/48ba01c8d780a04a1f526f89fcd9bc63ef5d4aec)) +* add new task assessment card ([ac3b50f](https://github.com/macite/doubtfire-deploy/commit/ac3b50f29b8a2722d09ad94a2f82bb54c1c32da7)) +* add new task dashboard component ([dc841f9](https://github.com/macite/doubtfire-deploy/commit/dc841f9e7af811a3a56a0c21fd3b3968ec7aead9)) +* add new task due card ([3ade5ea](https://github.com/macite/doubtfire-deploy/commit/3ade5ea34eb3c2c756d7df0f7a6c250fa50290bb)) +* add new task due card content ([c43ea3e](https://github.com/macite/doubtfire-deploy/commit/c43ea3e27abdf253596bfedd5112f5ef7a7b929d)) +* add new task submission card ([6d27690](https://github.com/macite/doubtfire-deploy/commit/6d2769029413f84b73c7c825d16e9a3366142177)) +* add pdf search and zoom ([f83ce57](https://github.com/macite/doubtfire-deploy/commit/f83ce570e80dc20d118288c6d32c2d2a9ca263b5)) +* add resizable inbox panels ([4920e31](https://github.com/macite/doubtfire-deploy/commit/4920e31ce366c3dfb4340ab4da159b176381cf49)) +* add resizable panels to inbox ([5abda03](https://github.com/macite/doubtfire-deploy/commit/5abda039af5b2d3b5e2589c4864b2e1715c5e8c0)) +* add rest of layout to footer ([3b9de7c](https://github.com/macite/doubtfire-deploy/commit/3b9de7cc6d923a0e6431524f2967b5e3bc360a6f)) +* add selected task sheet url ([00a7fb5](https://github.com/macite/doubtfire-deploy/commit/00a7fb516d362d3725b915d012878a9ccf8ef009)) +* add selectedTask service ([02aa41a](https://github.com/macite/doubtfire-deploy/commit/02aa41a34232359d1366d841d1236a068a837ae5)) +* add similarities staff dashboard switch ([1ec0b72](https://github.com/macite/doubtfire-deploy/commit/1ec0b72c26cc27bd4d17376af5b47650d28bbc10)) +* add similarity flags to view ([8c7975c](https://github.com/macite/doubtfire-deploy/commit/8c7975cadd14e6d3390cfa9917983cd01498a69c)) +* add simple project progress bar ([73fda77](https://github.com/macite/doubtfire-deploy/commit/73fda776ee36c6574c1ddc0dc09b958d61de3000)) +* add start of new similarity view ([a1e51b6](https://github.com/macite/doubtfire-deploy/commit/a1e51b6bc7030086a1d7a4d715f23b3e76f7ddb3)) +* add task plagairism warning on inbox ([6bd8620](https://github.com/macite/doubtfire-deploy/commit/6bd8620f5473db4517e38fd0487ab5876f921a36)) +* add teaching period remove break function ([90c6a37](https://github.com/macite/doubtfire-deploy/commit/90c6a3730f84da9e64abec676bcdf66c10b7fc60)) +* add tii action log ([96d982d](https://github.com/macite/doubtfire-deploy/commit/96d982d5fb945bdbf9ea8b6d534a5dd18ab64ad7)) +* add unselected user icon badge in inbox ([794697b](https://github.com/macite/doubtfire-deploy/commit/794697b53d6ee81f57b37b173d03a0688c6993b5)) +* complete mobile task inbox view ([00a4b3f](https://github.com/macite/doubtfire-deploy/commit/00a4b3f1e8f52364a83b6affae34295da4a7af34)) +* disable hover on task inbox for mobile ([a59fd01](https://github.com/macite/doubtfire-deploy/commit/a59fd0153de8b1f9e9b9022d0f6df1032c92f8d1)) +* downgrade task due card ([c342ac2](https://github.com/macite/doubtfire-deploy/commit/c342ac2e3cc4213c2b92499bc4a191485184af37)) +* enhance task search with similarity flags ([65ad918](https://github.com/macite/doubtfire-deploy/commit/65ad91842882e76379033350f90b966d874c6ba4)) +* enhance ui/ux of task inbox screen ([6b275c3](https://github.com/macite/doubtfire-deploy/commit/6b275c3cd5d575a399e901d52a62808238c6d0e4)) +* expand new file uplaoder ([11d798d](https://github.com/macite/doubtfire-deploy/commit/11d798d6a6e5eca4555a7d141e1d11d2a862fcfe)) +* finish new alerts service ([3beb059](https://github.com/macite/doubtfire-deploy/commit/3beb05987e605c9f4270fc0d7bdd95f82bf64007)) +* make all datetime in aus format ([7a0f749](https://github.com/macite/doubtfire-deploy/commit/7a0f749a4328a44c1ffb5692017dfa39a061e1d5)) +* migrate new unit dialog ([0a2a1e9](https://github.com/macite/doubtfire-deploy/commit/0a2a1e9baca6b0553730c1a939b246bf1be178a3)) +* minor improvements to file-drop ([73139be](https://github.com/macite/doubtfire-deploy/commit/73139be83449f50036eedb3520ed2d8daa5da09c)) +* new footer component ([c3b4c18](https://github.com/macite/doubtfire-deploy/commit/c3b4c182921079d6ebc0851ec7e6515e65f1c15b)) +* new grade service ([31073e1](https://github.com/macite/doubtfire-deploy/commit/31073e1dbe963c1e0f2c84139a2c3f746cc31bbb)) +* new student user badge ([97c84d3](https://github.com/macite/doubtfire-deploy/commit/97c84d3016b0e322d54062fae0d46da112146d5d)) +* new task status card ([78218cf](https://github.com/macite/doubtfire-deploy/commit/78218cf6d45f5cac50203f96109d27400fa37686)) +* only download similarity resource on view ([563d102](https://github.com/macite/doubtfire-deploy/commit/563d1024ee7023e4228a9ceef9f556e48e7127d1)) +* progress redesigned inbox ([f1b672b](https://github.com/macite/doubtfire-deploy/commit/f1b672b7495a9086abb88f5e1991cb4c4f7b60c1)) +* register new inbox component ([3d79baa](https://github.com/macite/doubtfire-deploy/commit/3d79baae505e44c401179ae34de4804e8d90b7ba)) +* show TII accepted status in profile ([7b5c3e2](https://github.com/macite/doubtfire-deploy/commit/7b5c3e2c0fe9fcc30f9fffc73d9a942714db7cb2)) +* small change to task view ([f640920](https://github.com/macite/doubtfire-deploy/commit/f6409201e9d4af4f075b4db8559fc1e26f4a374f)) +* small changes to user table ([18957ac](https://github.com/macite/doubtfire-deploy/commit/18957acfc700e941ebb161d9ddb8f5e1ebe1beff)) +* trigger window resize when pdf loads ([044c170](https://github.com/macite/doubtfire-deploy/commit/044c170ebe05e7e3a368df2e8041884d2e23c752)) +* update file viewer to use file download and blobs ([8751123](https://github.com/macite/doubtfire-deploy/commit/87511232ca384a355ddfc5eb396186babd676f92)) +* wip add file uploader component ([48c73f5](https://github.com/macite/doubtfire-deploy/commit/48c73f547892ae3ce12fdac416bf93c3a2c72006)) + + +### Bug Fixes + +* add activeUntil date mapping to tps ([2cae20a](https://github.com/macite/doubtfire-deploy/commit/2cae20a6b3fa3e5ea8a001ffa40a87263c438b68)) +* add arbitrary task argument to `togglePin` in task inbox ([6e1784f](https://github.com/macite/doubtfire-deploy/commit/6e1784ff52a745330e57f2ccc2a55b3206d535d0)) +* add seperator to naviation on mobile ([fc4f052](https://github.com/macite/doubtfire-deploy/commit/fc4f0527b144fd8f3b6e98b335e629f85fe1bb3b)) +* add the correct enterkeyhint to the comment composer ([569db05](https://github.com/macite/doubtfire-deploy/commit/569db059fc9786363b1b62d7be72e44f711faba3)) +* add unloaded state to progressbar ([2c3a28e](https://github.com/macite/doubtfire-deploy/commit/2c3a28e9718879f55596221517db35bda8c09450)) +* adjust due card layout ([0360a6e](https://github.com/macite/doubtfire-deploy/commit/0360a6e0868e995d7ba5c9091be556dad4bfd474)) +* always display shortened task name in header ([a05c8ff](https://github.com/macite/doubtfire-deploy/commit/a05c8ffd9aef9c865f72458157b465f9de1e07f3)) +* center pins along items in new task inbox ([6b3c0d2](https://github.com/macite/doubtfire-deploy/commit/6b3c0d2b1d5541fe36c6fbd03dfc752f43d03e18)) +* center placeholder text in task inbox search bar & reduce padding on far-right ([ffec9b6](https://github.com/macite/doubtfire-deploy/commit/ffec9b6f4bfc9d2aee6aa762c41bcce1ed1823ba)) +* center student and task name on bottom left screen ([e986199](https://github.com/macite/doubtfire-deploy/commit/e9861992e9e5a40076973a2215bcc919c1777b5d)) +* center view all units button on home page ([9807dfd](https://github.com/macite/doubtfire-deploy/commit/9807dfd1f18e1efc9f882a67ef99f1d198a03ff7)) +* centering of Administration icons on home page ([1043f74](https://github.com/macite/doubtfire-deploy/commit/1043f74e2ca5d23cad8c4d890a202b20ddb4bc37)) +* change display name of student on bottom left screen from being nickname to full name ([5fe7790](https://github.com/macite/doubtfire-deploy/commit/5fe7790e23aec709f8be5a444bf84ebdf77c9ce5)) +* change shown name on messages in chat to be student's full name ([93636c4](https://github.com/macite/doubtfire-deploy/commit/93636c400c1850e20320a57be010347541c5137c)) +* chatbox spacing, placement of messages <> photos, distance between message & name ([dd9cb2a](https://github.com/macite/doubtfire-deploy/commit/dd9cb2ab3bdde886c2f297d8044986427febd366)) +* ensure accepted TII eula details are retained ([e60ae54](https://github.com/macite/doubtfire-deploy/commit/e60ae547121f33edd7cc8d8c5d4afd27a1f90e8c)) +* ensure blobs are freed on destroy ([ee017eb](https://github.com/macite/doubtfire-deploy/commit/ee017eb00a506c81a108ce6dec52f7f91064d350)) +* ensure font is set to roboto ([a6ff4ce](https://github.com/macite/doubtfire-deploy/commit/a6ff4cee9794e37a208af28d7d085182441fa555)) +* ensure loadedUnitRoles is public in global ([b92159f](https://github.com/macite/doubtfire-deploy/commit/b92159ff33cf9537c0ccec50d57a54636315bec4)) +* ensure logout in timeout always signs out ([3380075](https://github.com/macite/doubtfire-deploy/commit/33800759083156682014f0f63bc83a0283ae96fa)) +* ensure mat menu icons have correct tags ([f971ad1](https://github.com/macite/doubtfire-deploy/commit/f971ad1cf7fa56de0f4e97e9533a0567db897da4)) +* ensure only staff need to accept turn it in EULA ([029c40b](https://github.com/macite/doubtfire-deploy/commit/029c40b556bb1709e75547615b2d1a1109997a74)) +* ensure pdf actions loads after pdf loaded ([0425c54](https://github.com/macite/doubtfire-deploy/commit/0425c5465a678509e1a8d719fef39975c4d8e9e5)) +* ensure similarities without files still view ([5794e2f](https://github.com/macite/doubtfire-deploy/commit/5794e2fb345328396517a7d2f945cfabb1c37173)) +* ensure task pdf viewer works ([d3d1bf8](https://github.com/macite/doubtfire-deploy/commit/d3d1bf8564c6e6134130b44df90bef42430f4654)) +* ensure task submission status updated on init ([ca77c92](https://github.com/macite/doubtfire-deploy/commit/ca77c92d251bf4087964886579de642f6f300b9a)) +* ensure that timeout state not re-routed ([c16ee90](https://github.com/macite/doubtfire-deploy/commit/c16ee90c2c372ea1889d3470e2be510aefd2245f)) +* ensure welcome page still can go to timeout ([5253f2e](https://github.com/macite/doubtfire-deploy/commit/5253f2eb6fce1de1b6df585589f1cc2594ac87a8)) +* excessive spacing below unit tags on home page ([1bfaa21](https://github.com/macite/doubtfire-deploy/commit/1bfaa216440d880d82875f519e9b3f0f59ddcc23)) +* fix few type errors ([8a96e5b](https://github.com/macite/doubtfire-deploy/commit/8a96e5b33a095cd70222719b33d7dc853d67f347)) +* fix file drop to support click to upload ([599cd9e](https://github.com/macite/doubtfire-deploy/commit/599cd9e82c1518113374f0675fc54dd1483fcc85)) +* fix footer height not resetting on similarity ([19c43d7](https://github.com/macite/doubtfire-deploy/commit/19c43d7945c5848d1ca511e146fefdfa9735c49b)) +* fix incorrect closing tag ([ad28818](https://github.com/macite/doubtfire-deploy/commit/ad28818fba9dac552a2adf14785c5bbf98b4e206)) +* fix page height recalculation trigger ([8f219c3](https://github.com/macite/doubtfire-deploy/commit/8f219c31e9a3461626a9344352c4d7aa68c57c43)) +* fix various build issues ([8b947ae](https://github.com/macite/doubtfire-deploy/commit/8b947aeea1cb0dfa4bac8e22ff5bdb2a9a82ec49)) +* fix various task comment composer/display issues ([5670bf1](https://github.com/macite/doubtfire-deploy/commit/5670bf1dcd77e8010fe88bf24662ec96e4e29847)) +* hide extension for staff if can't apply ([1699e50](https://github.com/macite/doubtfire-deploy/commit/1699e50f2d7424361e0f7d5a173bdd9fffd87c24)) +* initialise inbox on return ([1249e96](https://github.com/macite/doubtfire-deploy/commit/1249e96f53ef9544cd81e3f2c3f1d5acf4a628f3)) +* initialise inbox view ([1ae4ac9](https://github.com/macite/doubtfire-deploy/commit/1ae4ac9b304348f5528368d87ded3c26e8e5cf40)) +* issues with UI in photo spacing and text alignment ([716ead3](https://github.com/macite/doubtfire-deploy/commit/716ead3497a8a2ecd1050484bd289c003a47dffa)) +* make photo in chatbox fall to bottom ([aa4e814](https://github.com/macite/doubtfire-deploy/commit/aa4e81446546f1e62b08fd74ba8ac1e563e5efc8)) +* make read receipt smaller to fix message alignment ([c869458](https://github.com/macite/doubtfire-deploy/commit/c869458cd45334e009ae83ab3c911033bc1987b9)) +* null input checks on migrated cards ([7a192af](https://github.com/macite/doubtfire-deploy/commit/7a192af22bf1eef026d05a16edcc3a97c6abbebf)) +* pass selected task to footer's user icon ([28782b1](https://github.com/macite/doubtfire-deploy/commit/28782b134cb03f5c334df48e8887f81393661bda)) +* photo from below message to being inline ([0e97a03](https://github.com/macite/doubtfire-deploy/commit/0e97a03a491f4137ec301dd18a42130a845ebe37)) +* reduce user badge clickable region ([7305f86](https://github.com/macite/doubtfire-deploy/commit/7305f866a9582e10dd870fad1861de4ce209a4e0)) +* regenerate user icons on input changes ([257ab4e](https://github.com/macite/doubtfire-deploy/commit/257ab4eb63ec15de545193f8b60176ac33e17b01)) +* remove call to teaching period name ([1e2e019](https://github.com/macite/doubtfire-deploy/commit/1e2e0198a32acd59c11932fde2a2f14a5bacf48f)) +* remove dupe case in transition hook ([9808798](https://github.com/macite/doubtfire-deploy/commit/98087981e06296a1eb9a17f527c49ab9b27caef3)) +* remove incorrect aria hidden tags ([46bfe21](https://github.com/macite/doubtfire-deploy/commit/46bfe2109e4b912a146201572671ff759fc02226)) +* remove no PDF text from overlapping with icon ([c6543cf](https://github.com/macite/doubtfire-deploy/commit/c6543cf2f9be9644b7382eeb64699e15e461447a)) +* remove old teaching periods from home ([4f4a617](https://github.com/macite/doubtfire-deploy/commit/4f4a6175f39ac9df75c7efe920ffcc15576cc98e)) +* remove pct copy data ([6bb96b4](https://github.com/macite/doubtfire-deploy/commit/6bb96b49f436a7d4bd6f5e587b30a8528e6d75b7)) +* remove user badge from old task panel ([9129956](https://github.com/macite/doubtfire-deploy/commit/9129956f41b78a8e3e84ffe9a124a7941d8905c1)) +* require eula for any user ([daf7ba9](https://github.com/macite/doubtfire-deploy/commit/daf7ba9d9056c262cafc871b623c59a0ef837274)) +* resolved issue with intelligent discussion ([18cd1fc](https://github.com/macite/doubtfire-deploy/commit/18cd1fc6f0443c353fc5f49c81dafd2428901cbc)) +* resolved issue with intelligent-discussion-dialog component rendering ([1efa758](https://github.com/macite/doubtfire-deploy/commit/1efa758fd813751788cc79fe30e01ca8a09013d8)) +* shrink header menu on small screens ([580445d](https://github.com/macite/doubtfire-deploy/commit/580445d25573bf00671246269c61685917c34867)) +* spacing (of own/person messaging) is now right/left aligned respectively ([7d10aa0](https://github.com/macite/doubtfire-deploy/commit/7d10aa0f58e2d7c8e8fd45439d8c464fabe3508c)) +* split header onto seperate lines on small screens ([d8a9016](https://github.com/macite/doubtfire-deploy/commit/d8a90168e58ef8d71f31f76d999d4f6a59232987)) +* switch max pct to similarity flag in project ([094b418](https://github.com/macite/doubtfire-deploy/commit/094b418c1bff709e755bf7c61207ebf22bfc6106)) +* task explorer 405 error ([7ea516c](https://github.com/macite/doubtfire-deploy/commit/7ea516ce8e346d46e8acf68c8b41fa81485515ba)) +* task submission pdf downloading ([b0becb4](https://github.com/macite/doubtfire-deploy/commit/b0becb4c76e4e45f10782561a6b53a322eb43665)) +* tweak theme.scss file ([393770b](https://github.com/macite/doubtfire-deploy/commit/393770b35d1688d27bce47e1d6b94735a1822494)) +* update package lock ([674f3c7](https://github.com/macite/doubtfire-deploy/commit/674f3c70bd0a4c8c33e25d34beeab76c29f56325)) +* use angular 15 ([d0104cc](https://github.com/macite/doubtfire-deploy/commit/d0104cc57edda68a1a165bdf1fd7cf75bd74ec68)) +* use correct task object in task pinning ([a5fceb0](https://github.com/macite/doubtfire-deploy/commit/a5fceb048b931dbd5ccc3ad5d4fd898b146284fb)) +* use correct theming import ([b0d3232](https://github.com/macite/doubtfire-deploy/commit/b0d3232eaed2096296cf025437dda26ddcd595e0)) +* use disabled blinding ([628ca55](https://github.com/macite/doubtfire-deploy/commit/628ca55b4e8f569f751b78ead7a50fa9e086ccec)) +* use new state name in task dropdown ([3c2fe44](https://github.com/macite/doubtfire-deploy/commit/3c2fe444d398bcedb5c998a47683d04279996680)) +* use onChanges rather than input setter ([8069c21](https://github.com/macite/doubtfire-deploy/commit/8069c21d9ecfc412dea37cade8e8a37e79730f01)) + ### [8.0.9](https://github.com/doubtfire-lms/doubtfire-deploy/compare/v8.0.8...v8.0.9) (2024-05-31) diff --git a/package-lock.json b/package-lock.json index 396f5fee98..5ec5842fc2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.9", + "version": "8.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.9", + "version": "8.0.10", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 13d95e9bc7..a6af7f3bba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.9", + "version": "8.0.10", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 2f91e8fbba7902e1e22871cc82c87f4ff797b058 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Mon, 24 Jun 2024 13:28:40 +1000 Subject: [PATCH 078/776] fix: task date picker allows direct entry --- src/app/api/services/mapping-fn.ts | 39 ++++++++++++++++++----------- src/app/doubtfire-angular.module.ts | 18 ++++++++++++- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/app/api/services/mapping-fn.ts b/src/app/api/services/mapping-fn.ts index 7302bb387f..9124152095 100644 --- a/src/app/api/services/mapping-fn.ts +++ b/src/app/api/services/mapping-fn.ts @@ -1,23 +1,34 @@ +import moment from 'moment'; + export class MappingFunctions { - public static mapDateToEndOfDay(data, key, entity, params?) { + public static mapDateToEndOfDay(data, key, _entity, _params?) { const jsonDate = new Date(data[key]); - return new Date(jsonDate.getFullYear(), jsonDate.getMonth(), jsonDate.getDate(), 23, 59, 59, 999); + return new Date( + jsonDate.getFullYear(), + jsonDate.getMonth(), + jsonDate.getDate(), + 23, // all dates map to end of day + 59, + 59, + 999, + ); } - public static mapDateToDay(data, key, entity, params?) { + public static mapDateToDay(data, key: string, _entity, _params?) { const jsonDate = new Date(data[key]); return new Date(jsonDate.getFullYear(), jsonDate.getMonth(), jsonDate.getDate()); } - public static mapDate(data, key, entity, params?) { + public static mapDate(data, key: string, _entity, _params?) { return new Date(data[key]); } public static mapDayToJson(entity: T, key: string): string { if (entity[key]) { - const month = entity[key].getMonth() + 1; - const day = entity[key].getDate(); - return `${entity[key].getFullYear()}-${month < 10 ? '0' : ''}${month}-${day < 10 ? '0' : ''}${day}`; + const dateValue = moment.isMoment(entity[key]) ? entity[key].toDate() : entity[key]; + const month = dateValue.getMonth() + 1; + const day = dateValue.getDate(); + return `${dateValue.getFullYear()}-${month < 10 ? '0' : ''}${month}-${day < 10 ? '0' : ''}${day}`; } else { return undefined; } @@ -50,12 +61,12 @@ export class MappingFunctions { } /** - * Calculate the time between two dates - * - * @param date1 days from this date - * @param date2 to this date - * @returns the time from date1 to date2 - */ + * Calculate the time between two dates + * + * @param date1 days from this date + * @param date2 to this date + * @returns the time from date1 to date2 + */ public static timeBetween(date1: Date, date2: Date): number { return date2.getTime() - date1.getTime(); } @@ -67,7 +78,7 @@ export class MappingFunctions { * @param date2 to this date * @returns the days from date1 to date2 */ - public static daysBetween(date1: Date, date2: Date): number { + public static daysBetween(date1: Date, date2: Date): number { const diff = this.timeBetween(date1, date2); return Math.ceil(diff / (1000 * 3600 * 24)); } diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 9284745988..9be5408ace 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -95,8 +95,9 @@ import {ExtensionModalComponent} from './common/modals/extension-modal/extension import {CalendarModalComponent} from './common/modals/calendar-modal/calendar-modal.component'; import {MatRadioModule} from '@angular/material/radio'; import {MatButtonToggleModule} from '@angular/material/button-toggle'; -import {MAT_DATE_LOCALE, MatOptionModule} from '@angular/material/core'; +import {DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatOptionModule} from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; +import {MomentDateAdapter} from '@angular/material-moment-adapter'; import {doubtfireStates} from './doubtfire.states'; import {MatTableModule} from '@angular/material/table'; import {MatTabsModule} from '@angular/material/tabs'; @@ -225,6 +226,19 @@ import {TasksViewerComponent} from './units/states/tasks/tasks-viewer/tasks-view import {UnitCodeComponent} from './common/unit-code/unit-code.component'; import {GradeService} from './common/services/grade.service'; +// 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 = { + parse: { + dateInput: 'DD/MM/YYYY', // this is how your date will be parsed from Input + }, + display: { + dateInput: 'DD/MM/YYYY', // this is how your date will get displayed on the Input + monthYearLabel: 'MMMM YYYY', + dateA11yLabel: 'LL', + monthYearA11yLabel: 'MMMM YYYY', + }, +}; + @NgModule({ // Components we declare declarations: [ @@ -369,6 +383,8 @@ import {GradeService} from './common/services/grade.service'; CsvUploadModalProvider, CsvResultModalProvider, {provide: MAT_DATE_LOCALE, useValue: 'en-AU'}, + {provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE]}, + {provide: MAT_DATE_FORMATS, useValue: MY_DATE_FORMAT}, UnitStudentEnrolmentModalProvider, TaskCommentService, AudioRecorderProvider, From 8a6424b6eecc08c4625e8fb36a85c79bc3a023dc Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Mon, 24 Jun 2024 13:30:10 +1000 Subject: [PATCH 079/776] chore: tidy up file downloader --- .../file-downloader/file-downloader.service.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/common/file-downloader/file-downloader.service.ts b/src/app/common/file-downloader/file-downloader.service.ts index 0ec8b90e34..681e4bde8e 100644 --- a/src/app/common/file-downloader/file-downloader.service.ts +++ b/src/app/common/file-downloader/file-downloader.service.ts @@ -3,13 +3,13 @@ import {Injectable} from '@angular/core'; import {AlertService} from '../services/alert.service'; interface FileDownloaderData { - url: string, - response: HttpResponse, - success: (url: string, response: HttpResponse) => void, + url: string; + response: HttpResponse; + success: (url: string, response: HttpResponse) => void; // eslint-disable-next-line @typescript-eslint/no-explicit-any - failure: (error: any) => void, + failure: (error: any) => void; // eslint-disable-next-line @typescript-eslint/no-explicit-any - binaryData: Blob[], + binaryData: Blob[]; } @Injectable({ @@ -100,7 +100,7 @@ export class FileDownloaderService { public downloadBlob( url: string, success: (url: string, response: HttpResponse) => void, - failure: (error: any) => void, + failure: (error) => void, ) { // Declare binary data outside of the subscription so that it can be accessed in the second requests when partial content is returned const binaryData = []; @@ -146,7 +146,7 @@ export class FileDownloaderService { downloadLink.click(); downloadLink.parentNode.removeChild(downloadLink); }, - (error: any) => { + (error) => { this.alerts.error(`Error downloading file - ${error}`); }, ); From 3a81688904159c8c9e3efdde8f3605aa5677630f Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Mon, 24 Jun 2024 13:39:53 +1000 Subject: [PATCH 080/776] fix: ensure turn it in only appears to task editor when enabled --- .../task-definition-upload.component.html | 10 +++++----- .../task-definition-upload.component.ts | 11 +++++++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component.html index fe96cfb2df..8607e6c719 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component.html @@ -25,12 +25,12 @@
diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component.ts index a8915d843b..a6d049446a 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component.ts @@ -2,6 +2,7 @@ import { Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/ import { MatTable, MatTableDataSource } from '@angular/material/table'; import { TaskDefinition, UploadRequirement } from 'src/app/api/models/task-definition'; import { Unit } from 'src/app/api/models/unit'; +import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; @Component({ selector: 'f-task-definition-upload', @@ -10,10 +11,12 @@ import { Unit } from 'src/app/api/models/unit'; }) export class TaskDefinitionUploadComponent { @Input() public taskDefinition: TaskDefinition; - @ViewChild('upreqTable', { static: true }) table: MatTable; + @ViewChild('upreqTable', {static: true}) table: MatTable; public columns: string[] = ['file-name', 'file-type', 'tii-check', 'flag-pct', 'row-actions']; + constructor(private constants: DoubtfireConstants) {} + public get unit(): Unit { return this.taskDefinition?.unit; } @@ -30,9 +33,13 @@ export class TaskDefinitionUploadComponent { this.table.renderRows(); } + public tiiEnabled(): boolean { + return this.constants.IsTiiEnabled.value; + } + public removeUpReq(upreq: UploadRequirement) { this.taskDefinition.uploadRequirements = this.taskDefinition.uploadRequirements.filter( - (anUpReq) => anUpReq.key != upreq.key + (anUpReq) => anUpReq.key != upreq.key, ); } } From c7ae09170fdc43c21452e8b2665f0f36e480dd4d Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Mon, 24 Jun 2024 13:40:07 +1000 Subject: [PATCH 081/776] chore(release): 8.0.11 --- CHANGELOG.md | 15 +++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76d7c600ff..54ea56c57d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [8.0.11](https://github.com/macite/doubtfire-deploy/compare/v8.0.10...v8.0.11) (2024-06-24) + + +### Bug Fixes + +* ensure turn it in only appears to task editor when enabled ([3a81688](https://github.com/macite/doubtfire-deploy/commit/3a81688904159c8c9e3efdde8f3605aa5677630f)) +* task date picker allows direct entry ([2f91e8f](https://github.com/macite/doubtfire-deploy/commit/2f91e8fbba7902e1e22871cc82c87f4ff797b058)) + +### [7.0.24](https://github.com/macite/doubtfire-deploy/compare/v7.0.23...v7.0.24) (2024-06-05) + + +### Bug Fixes + +* ensure download blob supports 206 responses ([6445f9f](https://github.com/macite/doubtfire-deploy/commit/6445f9f998db70fbe9b9abf723dc17fd298b5d2f)) + ### [8.0.10](https://github.com/macite/doubtfire-deploy/compare/v7.0.23...v8.0.10) (2024-06-21) diff --git a/package-lock.json b/package-lock.json index 5ec5842fc2..691cb53755 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.10", + "version": "8.0.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.10", + "version": "8.0.11", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index a6af7f3bba..fe9cf8dc2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.10", + "version": "8.0.11", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 991230f7b527957116263b6abaa588ed43d3a787 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Tue, 25 Jun 2024 15:37:00 +1000 Subject: [PATCH 082/776] fix: reinstate unit import for teaching period --- .../teaching-period-list.component.html | 42 ++++++++++++++-- .../teaching-period-list.component.ts | 49 +++++++++++++++++-- src/app/doubtfire-angularjs.module.ts | 5 -- 3 files changed, 84 insertions(+), 12 deletions(-) diff --git a/src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.html b/src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.html index 5abff6ce87..5f009464f4 100644 --- a/src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.html +++ b/src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.html @@ -2,9 +2,20 @@

Teaching periods

- +
-
- Student + Student + - Name + Name + - Tutor + Tutor + Tutorial - + - Target + Target + Submitted as - + - Stats + Stats - Portfolio? + Portfolio? + - Grade + Grade +
{{student.hasPortfolio ? "Yes" : "No"}} + {{student.hasPortfolio ? "Yes" : "No"}} + {{student.grade}}
Check Similarity - @if (upreq.type === 'document') { -TurnItIn -} + @if (upreq.type === 'document' && tiiEnabled()) { + TurnItIn + } @if (upreq.type === 'code') { -Moss -} + Moss + }
+
+ + + + + - +
Active @@ -32,6 +43,26 @@

Teaching periods

{{ element.activeUntil | date }} Actions +
+ + + + +
+
- + diff --git a/src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.ts b/src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.ts index 87be5fbef8..17dc7efdf9 100644 --- a/src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.ts +++ b/src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.ts @@ -2,12 +2,13 @@ import { Component, Inject, OnInit, ViewChild } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MatPaginator } from '@angular/material/paginator'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { MatSort } from '@angular/material/sort'; +import { MatSort, Sort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { TeachingPeriodBreak } from 'src/app/api/models/teaching-period'; import { TeachingPeriod } from 'src/app/api/models/teaching-period'; import { TeachingPeriodBreakService } from 'src/app/api/services/teaching-period-break.service'; import { TeachingPeriodService } from 'src/app/api/services/teaching-period.service'; +import { TeachingPeriodUnitImportService } from '../teaching-period-unit-import/teaching-period-unit-import.dialog'; @Component({ selector: 'f-teaching-period-list', @@ -20,8 +21,13 @@ export class TeachingPeriodListComponent implements OnInit { public dataSource = new MatTableDataSource(); - displayedColumns: string[] = ['active', 'name', 'startDate', 'endDate', 'activeUntil']; - constructor(private teachingPeriodsService: TeachingPeriodService, public dialog: MatDialog) {} + displayedColumns: string[] = ['active', 'name', 'startDate', 'endDate', 'activeUntil', 'actions']; + + constructor( + private teachingPeriodsService: TeachingPeriodService, + public dialog: MatDialog, + public teachingPeriodUnitImportService: TeachingPeriodUnitImportService, + ) {} ngOnInit(): void { // update the Teaching Periods @@ -35,6 +41,10 @@ export class TeachingPeriodListComponent implements OnInit { }); } + importUnits(teachingPeriod: TeachingPeriod) { + this.teachingPeriodUnitImportService.openImportUnitsDialog(teachingPeriod); + } + addTeachingPeriod() { this.dialog.open(NewTeachingPeriodDialogComponent, { data: {}, @@ -46,6 +56,39 @@ export class TeachingPeriodListComponent implements OnInit { this.dialog.open(NewTeachingPeriodDialogComponent, { data: { teachingPeriod: teachingPeriod } }); }); } + + /** + * Function used by implemented sortTableData to determine the order + * of values within the EntityForm once sorting has been triggered. + * + * @param aValue value to be compared against bValue. + * @param bValue value to be compared against aValue. + * + * @returns truthy comparison between aValue and bValue. + */ + protected sortCompare(aValue: number | string, bValue: number | string, isAsc: boolean) { + return (aValue < bValue ? -1 : 1) * (isAsc ? 1 : -1); + } + + // Sorting function to sort data when sort + // event is triggered + sortTableData(sort: Sort) { + if (!sort.active || sort.direction === '') { + return; + } + switch (sort.active) { + case 'active': + case 'name': + case 'startDate': + case 'endDate': + case 'activeUntil': + this.dataSource.data = this.dataSource.data.sort((a, b) => { + const isAsc = sort.direction === 'asc'; + return this.sortCompare(a[sort.active], b[sort.active], isAsc); + }); + return; + } + } } @Component({ diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 868e381213..304e0bf747 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -213,7 +213,6 @@ import {InboxComponent} from './units/states/tasks/inbox/inbox.component'; import {TaskDefinitionEditorComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component'; import {UnitAnalyticsComponent} from './units/states/analytics/unit-analytics-route.component'; import {UnitTaskEditorComponent} from './units/states/edit/directives/unit-tasks-editor/unit-task-editor.component'; -import {TeachingPeriodUnitImportService} from './admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog'; import {CreateNewUnitModal} from './admin/modals/create-new-unit-modal/create-new-unit-modal.component'; import {FUsersComponent} from './admin/states/f-users/f-users.component'; import {FUnitTaskListComponent} from './units/states/tasks/viewer/directives/f-unit-task-list/f-unit-task-list.component'; @@ -240,10 +239,6 @@ export const DoubtfireAngularJSModule = angular.module('doubtfire', [ // Downgrade angular modules that we need... // factory -> service DoubtfireAngularJSModule.factory('AboutDoubtfireModal', downgradeInjectable(AboutDoubtfireModal)); -DoubtfireAngularJSModule.factory( - 'TeachingPeriodUnitImportService', - downgradeInjectable(TeachingPeriodUnitImportService), -); DoubtfireAngularJSModule.factory('DoubtfireConstants', downgradeInjectable(DoubtfireConstants)); DoubtfireAngularJSModule.factory('ExtensionModal', downgradeInjectable(ExtensionModalService)); DoubtfireAngularJSModule.factory('Marked', downgradeInjectable(MarkedPipe)); From 32046e89eaaf2a9b36e88f078a7b695b1506886d Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Tue, 25 Jun 2024 20:05:16 +1000 Subject: [PATCH 083/776] fix: ensure null fields will not break in entity mapping --- src/app/api/models/task-definition.ts | 2 +- src/app/api/services/project.service.ts | 20 +++++++++++-------- src/app/api/services/task-comment.service.ts | 4 ++-- .../api/services/task-definition.service.ts | 14 +++++++++---- src/app/api/services/unit.service.ts | 8 ++++---- 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index 1b49a2e856..0766f85eb6 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -21,7 +21,7 @@ export class TaskDefinition extends Entity { targetDate: Date; dueDate: Date; startDate: Date; - uploadRequirements: UploadRequirement[]; + uploadRequirements: UploadRequirement[] = []; tutorialStream: TutorialStream = null; plagiarismChecks: SimilarityCheck[] = []; plagiarismReportUrl: string; diff --git a/src/app/api/services/project.service.ts b/src/app/api/services/project.service.ts index e9ffc791ed..6bb50481a3 100644 --- a/src/app/api/services/project.service.ts +++ b/src/app/api/services/project.service.ts @@ -143,7 +143,7 @@ export class ProjectService extends CachedEntityService { keys: 'tutorialEnrolments', toEntityOp: (data: object, key: string, project: Project, params?: any) => { const unit: Unit = project.unit; - data[key].forEach((tutorialEnrolment: { tutorial_id: number; }) => { + data[key]?.forEach((tutorialEnrolment: {tutorial_id: number}) => { if (tutorialEnrolment.tutorial_id) { const tutorial = unit.tutorialsCache.get(tutorialEnrolment.tutorial_id); project.tutorialEnrolmentsCache.add(tutorial); @@ -154,12 +154,16 @@ export class ProjectService extends CachedEntityService { { keys: 'groups', toEntityOp: (data: object, key: string, project: Project, params?: any) => { - data[key].forEach((group) => { - const theGroup = project.unit.groupSetsCache.get(group.group_set_id).groupsCache.getOrCreate(group.id, this.groupService, group, {constructorParams: project.unit}); + data[key]?.forEach((group) => { + const theGroup = project.unit.groupSetsCache + .get(group.group_set_id) + .groupsCache.getOrCreate(group.id, this.groupService, group, { + constructorParams: project.unit, + }); project.groupCache.add(theGroup); theGroup.projectsCache.add(project); - }) + }); }, toJsonFn: (entity: Project, key: string) => { return entity.unit?.id; @@ -169,7 +173,7 @@ export class ProjectService extends CachedEntityService { keys: 'tasks', toEntityOp: (data: object, key: string, project: Project, params?: any) => { // create tasks from json - data['tasks'].forEach(taskData => { + data['tasks']?.forEach((taskData) => { project.taskCache.getOrCreate(taskData['id'], this.taskService, taskData, {constructorParams: project}); }); @@ -179,14 +183,14 @@ export class ProjectService extends CachedEntityService { { keys: 'taskOutcomeAlignments', toEntityOp: (data: object, key: string, project: Project, params?: any) => { - data[key].forEach(alignment => { + data[key]?.forEach((alignment) => { project.taskOutcomeAlignmentsCache.getOrCreate( alignment['id'], taskOutcomeAlignmentService, alignment, { - constructorParams: project - } + constructorParams: project, + }, ); }); } diff --git a/src/app/api/services/task-comment.service.ts b/src/app/api/services/task-comment.service.ts index 9e646be77d..7fdf94bc88 100644 --- a/src/app/api/services/task-comment.service.ts +++ b/src/app/api/services/task-comment.service.ts @@ -41,7 +41,7 @@ export class TaskCommentService extends CachedEntityService { { keys: 'author', toEntityFn: (data: object, key: string, comment: TaskComment) => { - const user = this.userService.cache.getOrCreate(data[key].id, userService, data[key]); + const user = this.userService.cache.getOrCreate(data[key]?.id, userService, data[key]); comment.initials = `${user.firstName[0]}${user.lastName[0]}`.toUpperCase(); return user; } @@ -49,7 +49,7 @@ export class TaskCommentService extends CachedEntityService { { keys: 'recipient', toEntityFn: (data: object, key: string, comment: TaskComment) => { - return this.userService.cache.getOrCreate(data[key].id, userService, data[key]); + return this.userService.cache.getOrCreate(data[key]?.id, userService, data[key]); } }, 'recipientReadTime', diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index 13a1dd2797..b2499251ea 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -41,7 +41,7 @@ export class TaskDefinitionService extends CachedEntityService { keys: 'uploadRequirements', toJsonFn: (taskDef: TaskDefinition, key: string) => { return JSON.stringify( - taskDef.uploadRequirements.map((upreq) => { + taskDef.uploadRequirements?.map((upreq) => { return { key: upreq.key, name: upreq.name, @@ -49,13 +49,19 @@ export class TaskDefinitionService extends CachedEntityService { tii_check: upreq.tiiCheck, tii_pct: upreq.tiiPct, }; - }) + }), ); }, toEntityFn: (data: object, key: string, taskDef: TaskDefinition, params?: any) => { return ( - data[key] as Array<{ key: string; name: string; type: string; tii_check: boolean; tii_pct: number }> - ).map((upreq) => { + data[key] as Array<{ + key: string; + name: string; + type: string; + tii_check: boolean; + tii_pct: number; + }> + )?.map((upreq) => { return { key: upreq.key, name: upreq.name, diff --git a/src/app/api/services/unit.service.ts b/src/app/api/services/unit.service.ts index 740e9b6cc1..4f6e896495 100644 --- a/src/app/api/services/unit.service.ts +++ b/src/app/api/services/unit.service.ts @@ -58,7 +58,7 @@ export class UnitService extends CachedEntityService { const unitRoleService = AppInjector.get(UnitRoleService); // Add staff entity.staffCache.clear(); - data[key].forEach(staff => { + data[key]?.forEach(staff => { entity.staffCache.add(unitRoleService.buildInstance(staff)); }); } @@ -133,7 +133,7 @@ export class UnitService extends CachedEntityService { { keys: 'ilos', toEntityOp: (data: object, key: string, unit: Unit) => { - data[key].forEach(ilo => { + data[key]?.forEach(ilo => { unit.learningOutcomesCache.getOrCreate(ilo['id'], this.learningOutcomeService, ilo); }); } @@ -160,7 +160,7 @@ export class UnitService extends CachedEntityService { { keys: 'groupSets', toEntityOp: (data, key, unit) => { - data[key].forEach((groupSetJson: object) => { + data[key]?.forEach((groupSetJson: object) => { unit.groupSetsCache.add(this.groupSetService.buildInstance(groupSetJson, {constructorParams: unit})); }); } @@ -168,7 +168,7 @@ export class UnitService extends CachedEntityService { { keys: 'groups', toEntityOp: (data, key, unit) => { - data[key].forEach((groupJson: object) => { + data[key]?.forEach((groupJson: object) => { const group = this.groupService.buildInstance(groupJson, {constructorParams: unit}); group.groupSet.groupsCache.add(group); }); From 0bb4869aafb633a74b417e6c5b0d819d99dc60aa Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Tue, 25 Jun 2024 20:25:35 +1000 Subject: [PATCH 084/776] chore(release): 8.0.12 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ea56c57d..cae648d1cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [8.0.12](https://github.com/macite/doubtfire-deploy/compare/v8.0.11...v8.0.12) (2024-06-25) + + +### Bug Fixes + +* ensure null fields will not break in entity mapping ([32046e8](https://github.com/macite/doubtfire-deploy/commit/32046e89eaaf2a9b36e88f078a7b695b1506886d)) +* reinstate unit import for teaching period ([991230f](https://github.com/macite/doubtfire-deploy/commit/991230f7b527957116263b6abaa588ed43d3a787)) + ### [8.0.11](https://github.com/macite/doubtfire-deploy/compare/v8.0.10...v8.0.11) (2024-06-24) diff --git a/package-lock.json b/package-lock.json index 691cb53755..5ba9364f0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.11", + "version": "8.0.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.11", + "version": "8.0.12", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index fe9cf8dc2a..0492d4a2db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.11", + "version": "8.0.12", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 8fe2f9e9ca60c02e55c24d0f2bb3eb6dbea8217c Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Thu, 27 Jun 2024 02:13:51 +1000 Subject: [PATCH 085/776] feat: get unique token for scorm asset retrieval --- src/app/api/models/user/user.ts | 1 + src/app/api/services/authentication.service.ts | 18 ++++++++++++++++++ .../scorm-player/scorm-player.component.ts | 18 ++++++++++++++++-- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/app/api/models/user/user.ts b/src/app/api/models/user/user.ts index 10357a1898..552066cfd1 100644 --- a/src/app/api/models/user/user.ts +++ b/src/app/api/models/user/user.ts @@ -22,6 +22,7 @@ export class User extends Entity { receiveFeedbackNotifications: boolean; hasRunFirstTimeSetup: boolean; authenticationToken: string; + scormAuthenticationToken: string; pronouns: string | null; acceptedTiiEula: boolean; diff --git a/src/app/api/services/authentication.service.ts b/src/app/api/services/authentication.service.ts index b2cb12f402..fe2b8d0b1c 100644 --- a/src/app/api/services/authentication.service.ts +++ b/src/app/api/services/authentication.service.ts @@ -191,4 +191,22 @@ export class AuthenticationService { setTimeout(() => this.router.stateService.go('timeout'), 500); } } + + public getScormToken(): Observable { + return this.httpClient.get(this.AUTH_URL + '/scorm').pipe( + map((response) => { + this.userService.currentUser.scormAuthenticationToken = response['scorm_auth_token']; + localStorage.setItem(this.USERNAME_KEY, JSON.stringify(this.userService.currentUser)); + + // Token expires after 2 hours + setTimeout( + () => { + this.userService.currentUser.scormAuthenticationToken = ''; + localStorage.setItem(this.USERNAME_KEY, JSON.stringify(this.userService.currentUser)); + }, + 1000 * 60 * 60 * 2, + ); + }), + ); + } } diff --git a/src/app/common/scorm-player/scorm-player.component.ts b/src/app/common/scorm-player/scorm-player.component.ts index 4a32eb0ac7..b7f9b1b5a6 100644 --- a/src/app/common/scorm-player/scorm-player.component.ts +++ b/src/app/common/scorm-player/scorm-player.component.ts @@ -1,6 +1,10 @@ import {Component, OnInit, Input, HostListener} from '@angular/core'; import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser'; -import {ScormPlayerContext} from 'src/app/api/models/doubtfire-model'; +import { + AuthenticationService, + ScormPlayerContext, + UserService, +} from 'src/app/api/models/doubtfire-model'; import {ScormAdapterService} from 'src/app/api/services/scorm-adapter.service'; import {AppInjector} from 'src/app/app-injector'; import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; @@ -37,6 +41,8 @@ export class ScormPlayerComponent implements OnInit { constructor( private globalState: GlobalStateService, private scormAdapter: ScormAdapterService, + private userService: UserService, + private authService: AuthenticationService, private sanitizer: DomSanitizer, ) {} @@ -44,6 +50,14 @@ export class ScormPlayerComponent implements OnInit { this.globalState.setView(ViewType.OTHER); this.globalState.hideHeader(); + if (this.userService.currentUser.scormAuthenticationToken) { + this.setupScorm(); + } else { + this.authService.getScormToken().subscribe(() => this.setupScorm()); + } + } + + setupScorm(): void { this.scormAdapter.mode = this.mode; if (this.mode === 'normal') { this.scormAdapter.projectId = this.projectId; @@ -64,7 +78,7 @@ export class ScormPlayerComponent implements OnInit { }; this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl( - `${AppInjector.get(DoubtfireConstants).API_URL}/scorm/${this.taskDefId}/index.html`, + `${AppInjector.get(DoubtfireConstants).API_URL}/scorm/${this.taskDefId}/${this.userService.currentUser.username}/${this.userService.currentUser.scormAuthenticationToken}/index.html`, ); } From 7b5d35c89a577d0408a51d8390400f44615591eb Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 27 Jun 2024 14:51:01 +1000 Subject: [PATCH 086/776] fix: update discuss text to promote use --- src/app/api/models/task-status.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/app/api/models/task-status.ts b/src/app/api/models/task-status.ts index 0ab0f8f2d9..7470b9b143 100644 --- a/src/app/api/models/task-status.ts +++ b/src/app/api/models/task-status.ts @@ -287,17 +287,18 @@ export class TaskStatus { [ 'discuss', { - detail: "You're almost complete!", - reason: 'Your work looks good and your tutor believes it is complete.', - action: 'To mark as complete, attend class and discuss it with your tutor.', + detail: 'Your work needs to be discussed further.', + reason: 'Your work looks good and your tutor believes it is on track.', + action: 'For this to be marked as complete, attend class and discuss it with your tutor.', }, ], [ 'demonstrate', { - detail: "You're almost complete!", - reason: 'Your work looks good and your tutor believes it is complete.', - action: 'To mark as complete, attend class and demonstrate how your submission works to your tutor.', + detail: 'Your work needs to be demonstrated.', + reason: 'Your work looks good and your tutor believes it is on track.', + action: + 'For this to be marked as complete you need to attend class and demonstrate how your submission works for your tutor.', }, ], [ From dbb3a682639c959e946e1f3273d9e5f2996683db Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 27 Jun 2024 15:17:57 +1000 Subject: [PATCH 087/776] chore(release): 8.0.13 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cae648d1cf..fed8da62dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [8.0.13](https://github.com/macite/doubtfire-deploy/compare/v8.0.12...v8.0.13) (2024-06-27) + + +### Bug Fixes + +* update discuss text to promote use ([7b5d35c](https://github.com/macite/doubtfire-deploy/commit/7b5d35c89a577d0408a51d8390400f44615591eb)) + ### [8.0.12](https://github.com/macite/doubtfire-deploy/compare/v8.0.11...v8.0.12) (2024-06-25) diff --git a/package-lock.json b/package-lock.json index 5ba9364f0e..351f4d773e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.12", + "version": "8.0.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.12", + "version": "8.0.13", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 0492d4a2db..3ade29c77a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.12", + "version": "8.0.13", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 01013d3d16ab5b704acc589d34975693030def0b Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 27 Jun 2024 22:37:17 +1000 Subject: [PATCH 088/776] fix: ensure unit import allows unit code change --- .../teaching-period-unit-import.dialog.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog.ts b/src/app/admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog.ts index 2925b4b59e..1a89a5538d 100644 --- a/src/app/admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog.ts +++ b/src/app/admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog.ts @@ -126,6 +126,10 @@ export class TeachingPeriodUnitImportDialogComponent implements OnInit { public codeChange(code: string, value: UnitImportData) { value.relatedUnits = this.relatedUnits(code); + // add source unit to realted units - so that it is retained on code change + if (value.sourceUnit && !value.relatedUnits.find((u) => u.value.id === value.sourceUnit.id)) { + value.relatedUnits.unshift({value: value.sourceUnit, text: value.sourceUnit.codeAndPeriod}); + } value.sourceUnit = value.relatedUnits.length > 0 ? value.relatedUnits[0].value : null; } From 4c966245d270af46107aae94bdd38e84b2188b65 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 27 Jun 2024 22:37:37 +1000 Subject: [PATCH 089/776] fix: ensure unit load sets main convenor user in all cases --- src/app/api/services/unit.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/api/services/unit.service.ts b/src/app/api/services/unit.service.ts index 4f6e896495..c3c38c7406 100644 --- a/src/app/api/services/unit.service.ts +++ b/src/app/api/services/unit.service.ts @@ -66,7 +66,9 @@ export class UnitService extends CachedEntityService { { keys: ['mainConvenor', 'main_convenor_id'], toEntityFn: (data, key, entity) => { - return entity.staffCache.get(data[key]); + let result = entity.staffCache.get(data[key]); + entity.mainConvenorUser = result?.user; + return result; }, toJsonFn: (unit: Unit, key: string) => { return unit.mainConvenor?.id; From 1dc58ce230e67c4a682600fb4b8153fc82586461 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 27 Jun 2024 22:46:27 +1000 Subject: [PATCH 090/776] chore(release): 8.0.14 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fed8da62dd..9081badf55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [8.0.14](https://github.com/macite/doubtfire-deploy/compare/v8.0.13...v8.0.14) (2024-06-27) + + +### Bug Fixes + +* ensure unit import allows unit code change ([01013d3](https://github.com/macite/doubtfire-deploy/commit/01013d3d16ab5b704acc589d34975693030def0b)) +* ensure unit load sets main convenor user in all cases ([4c96624](https://github.com/macite/doubtfire-deploy/commit/4c966245d270af46107aae94bdd38e84b2188b65)) + ### [8.0.13](https://github.com/macite/doubtfire-deploy/compare/v8.0.12...v8.0.13) (2024-06-27) diff --git a/package-lock.json b/package-lock.json index 351f4d773e..56e93ef1bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.13", + "version": "8.0.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.13", + "version": "8.0.14", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 3ade29c77a..c1de295951 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.13", + "version": "8.0.14", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 1857bdc708c81dc39d467f42c9e316a8664dc080 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 28 Jun 2024 19:06:54 +1000 Subject: [PATCH 091/776] fix: ensure zip uploads work on windows --- .../task-definition-overseer.component.html | 14 +++++++++++-- .../task-definition-overseer.component.ts | 4 +++- .../task-definition-resources.component.ts | 4 ++-- .../unit-task-editor.component.html | 21 ------------------- 4 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html index 92ef121ff3..31c683077f 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html @@ -1,5 +1,10 @@
- + Automation Enabled @@ -13,7 +18,12 @@ Docker image for Overseer - +
-
From a5690871a448f5b3aeb442df47a410ee2e3ca1a0 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 28 Jun 2024 19:07:01 +1000 Subject: [PATCH 092/776] chore(release): 8.0.15 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9081badf55..362eaeac79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [8.0.15](https://github.com/macite/doubtfire-deploy/compare/v8.0.14...v8.0.15) (2024-06-28) + + +### Bug Fixes + +* ensure zip uploads work on windows ([1857bdc](https://github.com/macite/doubtfire-deploy/commit/1857bdc708c81dc39d467f42c9e316a8664dc080)) + ### [8.0.14](https://github.com/macite/doubtfire-deploy/compare/v8.0.13...v8.0.14) (2024-06-27) diff --git a/package-lock.json b/package-lock.json index 56e93ef1bd..1bcdf3b7fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.14", + "version": "8.0.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.14", + "version": "8.0.15", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index c1de295951..fb492d216c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.14", + "version": "8.0.15", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From f0505d7b62b97f1b821b19b1c4178973503789da Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 28 Jun 2024 19:31:49 +1000 Subject: [PATCH 093/776] fix: ensure drop works on task resource upload in windows --- .../task-definition-resources.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-resources/task-definition-resources.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-resources/task-definition-resources.component.html index 82ae176482..51bf4db54f 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-resources/task-definition-resources.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-resources/task-definition-resources.component.html @@ -19,7 +19,7 @@ @if (taskDefinition.hasTaskResources) { From 576076f1a7871f458526253ab18eade70fbd316a Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 28 Jun 2024 19:31:58 +1000 Subject: [PATCH 094/776] chore(release): 8.0.16 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 362eaeac79..0ccd7cdc27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [8.0.16](https://github.com/macite/doubtfire-deploy/compare/v8.0.15...v8.0.16) (2024-06-28) + + +### Bug Fixes + +* ensure drop works on task resource upload in windows ([f0505d7](https://github.com/macite/doubtfire-deploy/commit/f0505d7b62b97f1b821b19b1c4178973503789da)) + ### [8.0.15](https://github.com/macite/doubtfire-deploy/compare/v8.0.14...v8.0.15) (2024-06-28) diff --git a/package-lock.json b/package-lock.json index 1bcdf3b7fe..53a1fccd78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.15", + "version": "8.0.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.15", + "version": "8.0.16", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index fb492d216c..254a8eb450 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.15", + "version": "8.0.16", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 2394efb07c03da18fb8114d87b0c89e68bf7fd2d Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Mon, 1 Jul 2024 17:04:31 +1000 Subject: [PATCH 095/776] fix: switch date formats to date-fns Issues with moment caused failure to load task edit page. --- package-lock.json | 52 +++++++++++++------ package.json | 6 ++- src/app/doubtfire-angular.module.ts | 20 ++++--- .../unit-task-editor.component.ts | 3 +- 4 files changed, 55 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 53a1fccd78..f31a23897f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,8 @@ "@angular/compiler": "^17.3.6", "@angular/core": "^17.3.6", "@angular/forms": "^17.3.6", - "@angular/material": "^17.3.6", - "@angular/material-moment-adapter": "^17.3.6", + "@angular/material": "^17.3.10", + "@angular/material-date-fns-adapter": "^17.3.10", "@angular/platform-browser": "^17.3.6", "@angular/platform-browser-dynamic": "^17.3.6", "@angular/router": "^17.3.6", @@ -52,6 +52,7 @@ "codemirror": "5.65.0", "core-js": "^3.21.1", "d3": "3.5.17", + "date-fns": "^3.6.0", "es5-shim": "^4.5.12", "file-saver": "^2.0.5", "font-awesome": "~4.7.0", @@ -604,8 +605,9 @@ } }, "node_modules/@angular/cdk": { - "version": "17.3.8", - "license": "MIT", + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.10.tgz", + "integrity": "sha512-b1qktT2c1TTTe5nTji/kFAVW92fULK0YhYAvJ+BjZTPKu2FniZNe8o4qqQ0pUuvtMu+ZQxp/QqFYoidIVCjScg==", "dependencies": { "tslib": "^2.3.0" }, @@ -790,8 +792,9 @@ } }, "node_modules/@angular/material": { - "version": "17.3.8", - "license": "MIT", + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.10.tgz", + "integrity": "sha512-hHMQES0tQPH5JW33W+mpBPuM8ybsloDTqFPuRV8cboDjosAWfJhzAKF3ozICpNlUrs62La/2Wu/756GcQrxebg==", "dependencies": { "@material/animation": "15.0.0-canary.7f224ddd4.0", "@material/auto-init": "15.0.0-canary.7f224ddd4.0", @@ -844,7 +847,7 @@ }, "peerDependencies": { "@angular/animations": "^17.0.0 || ^18.0.0", - "@angular/cdk": "17.3.8", + "@angular/cdk": "17.3.10", "@angular/common": "^17.0.0 || ^18.0.0", "@angular/core": "^17.0.0 || ^18.0.0", "@angular/forms": "^17.0.0 || ^18.0.0", @@ -852,16 +855,17 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@angular/material-moment-adapter": { - "version": "17.3.8", - "license": "MIT", + "node_modules/@angular/material-date-fns-adapter": { + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/material-date-fns-adapter/-/material-date-fns-adapter-17.3.10.tgz", + "integrity": "sha512-Q4QAPGImZTjKW9ZhLSTkBeQX21I0dtak3JbexYx4CN/pHxKRpen6KaVAEqiORqq6vNUP2Kwb7cZznQyj6L7oQw==", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/core": "^17.0.0 || ^18.0.0", - "@angular/material": "17.3.8", - "moment": "^2.18.1" + "@angular/material": "17.3.10", + "date-fns": ">2.20.0 <4.0" } }, "node_modules/@angular/platform-browser": { @@ -8067,6 +8071,12 @@ "node": ">=4.0.0" } }, + "node_modules/concurrently/node_modules/date-fns": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", + "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", + "dev": true + }, "node_modules/concurrently/node_modules/has-flag": { "version": "1.0.0", "dev": true, @@ -8651,9 +8661,13 @@ } }, "node_modules/date-fns": { - "version": "1.30.1", - "dev": true, - "license": "MIT" + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } }, "node_modules/date-format": { "version": "4.0.14", @@ -12917,6 +12931,14 @@ "tslib": "^2.1.0" } }, + "node_modules/install": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", + "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/internal-slot": { "version": "1.0.7", "dev": true, diff --git a/package.json b/package.json index 254a8eb450..df30c7bebd 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,8 @@ "@angular/compiler": "^17.3.6", "@angular/core": "^17.3.6", "@angular/forms": "^17.3.6", - "@angular/material": "^17.3.6", - "@angular/material-moment-adapter": "^17.3.6", + "@angular/material": "^17.3.10", + "@angular/material-date-fns-adapter": "^17.3.10", "@angular/platform-browser": "^17.3.6", "@angular/platform-browser-dynamic": "^17.3.6", "@angular/router": "^17.3.6", @@ -69,9 +69,11 @@ "codemirror": "5.65.0", "core-js": "^3.21.1", "d3": "3.5.17", + "date-fns": "^3.6.0", "es5-shim": "^4.5.12", "file-saver": "^2.0.5", "font-awesome": "~4.7.0", + "install": "^0.13.0", "jquery": "2.1.4", "lodash": "~4.17", "lottie-web": "^5.12.2", diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 9be5408ace..69acaf196e 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -97,7 +97,11 @@ import {MatRadioModule} from '@angular/material/radio'; import {MatButtonToggleModule} from '@angular/material/button-toggle'; import {DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatOptionModule} from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; -import {MomentDateAdapter} from '@angular/material-moment-adapter'; + +import { DateFnsAdapter, MAT_DATE_FNS_FORMATS } from '@angular/material-date-fns-adapter'; +import { enAU } from 'date-fns/locale'; + + import {doubtfireStates} from './doubtfire.states'; import {MatTableModule} from '@angular/material/table'; import {MatTabsModule} from '@angular/material/tabs'; @@ -229,13 +233,13 @@ import {GradeService} from './common/services/grade.service'; // 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 = { parse: { - dateInput: 'DD/MM/YYYY', // this is how your date will be parsed from Input + dateInput: 'dd/MM/yyyy', // this is how your date will be parsed from Input }, display: { - dateInput: 'DD/MM/YYYY', // this is how your date will get displayed on the Input - monthYearLabel: 'MMMM YYYY', - dateA11yLabel: 'LL', - monthYearA11yLabel: 'MMMM YYYY', + dateInput: 'dd/MM/yyyy', // this is how your date will get displayed on the Input + monthYearLabel: 'MMMM yyyy', + dateA11yLabel: 'do MMMM yyyy', + monthYearA11yLabel: 'MMMM yyyy', }, }; @@ -382,8 +386,8 @@ const MY_DATE_FORMAT = { dateServiceProvider, CsvUploadModalProvider, CsvResultModalProvider, - {provide: MAT_DATE_LOCALE, useValue: 'en-AU'}, - {provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE]}, + {provide: MAT_DATE_LOCALE, useValue: enAU}, + {provide: DateAdapter, useClass: DateFnsAdapter, deps: [MAT_DATE_LOCALE]}, {provide: MAT_DATE_FORMATS, useValue: MY_DATE_FORMAT}, UnitStudentEnrolmentModalProvider, TaskCommentService, diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts index ee0f135c26..72c309371d 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts @@ -8,6 +8,7 @@ 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 { AlertService } from 'src/app/common/services/alert.service'; +import { addWeeks } from 'date-fns'; @Component({ selector: 'f-unit-task-editor', @@ -183,7 +184,7 @@ export class UnitTaskEditorComponent implements AfterViewInit { task.abbreviation = abbr; task.description = 'New Description'; task.startDate = new Date(); - task.targetDate = new Date(); + task.targetDate = addWeeks(new Date(), 2); task.uploadRequirements = []; task.weighting = 4; task.targetGrade = 0; From 4d81fa1fabb07e40d0d42068f23b8e756f4cdf4b Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Mon, 1 Jul 2024 17:08:20 +1000 Subject: [PATCH 096/776] fix: correct broken template in activities list --- .../activity-type-list.component.html | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/src/app/admin/institution-settings/activity-type-list/activity-type-list.component.html b/src/app/admin/institution-settings/activity-type-list/activity-type-list.component.html index 9b294d5597..9c91b329f7 100644 --- a/src/app/admin/institution-settings/activity-type-list/activity-type-list.component.html +++ b/src/app/admin/institution-settings/activity-type-list/activity-type-list.component.html @@ -15,27 +15,10 @@

Activities

{{ activityType.name }} } @else { - -
- - - - - -
- - } - - + } @@ -52,12 +35,11 @@

Activities

{{ activityType.abbreviation }}
- } @else { #edit| } - + } @else { - + } @@ -76,8 +58,7 @@

Activities

edit - } @else { #edit| } - + } @else {
-
+ }
From 17958955f65bab2b268826f2a7e19f311071615b Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Mon, 1 Jul 2024 17:11:09 +1000 Subject: [PATCH 097/776] fix: correct campus list templates --- .../campus-list/campus-list.component.html | 63 +++++++------------ 1 file changed, 21 insertions(+), 42 deletions(-) diff --git a/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.html b/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.html index a97dcd2e16..3fe38272ec 100644 --- a/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.html +++ b/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.html @@ -11,31 +11,14 @@

Campuses

Name @if (!editing(campus)) { -
- {{ campus.name }} -
+
+ {{ campus.name }} +
} @else { - -
- - - - - -
- - } - - + } @@ -49,15 +32,14 @@

Campuses

Abbreviation @if (!editing(campus)) { -
- {{ campus.abbreviation }} -
- } @else { #edit| } - +
+ {{ campus.abbreviation }} +
+ } @else { -
+ } @@ -71,11 +53,10 @@

Campuses

Default Sync Mode @if (!editing(campus)) { -
- {{ campus.mode | titlecase }} -
- } @else { #edit| } - +
+ {{ campus.mode | titlecase }} +
+ } @else { Default Sync Mode @@ -86,7 +67,7 @@

Campuses

}
-
+ } @@ -107,13 +88,12 @@

Campuses

Active @if (!editing(campus)) { -
- -
- } @else { #edit| } - +
+ +
+ } @else { -
+ } @@ -135,8 +115,7 @@

Campuses

- } @else { #edit| } - + } @else {
-
+ }
From a2c5a4490224ab49cfb4ff966e86bb3dd44afaf5 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Mon, 1 Jul 2024 17:11:32 +1000 Subject: [PATCH 098/776] chore(release): 8.0.17 --- CHANGELOG.md | 9 +++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ccd7cdc27..b578191a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [8.0.17](https://github.com/macite/doubtfire-deploy/compare/v8.0.16...v8.0.17) (2024-07-01) + + +### Bug Fixes + +* correct broken template in activities list ([4d81fa1](https://github.com/macite/doubtfire-deploy/commit/4d81fa1fabb07e40d0d42068f23b8e756f4cdf4b)) +* correct campus list templates ([1795895](https://github.com/macite/doubtfire-deploy/commit/17958955f65bab2b268826f2a7e19f311071615b)) +* switch date formats to date-fns ([2394efb](https://github.com/macite/doubtfire-deploy/commit/2394efb07c03da18fb8114d87b0c89e68bf7fd2d)) + ### [8.0.16](https://github.com/macite/doubtfire-deploy/compare/v8.0.15...v8.0.16) (2024-06-28) diff --git a/package-lock.json b/package-lock.json index f31a23897f..8236e0d60d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.16", + "version": "8.0.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.16", + "version": "8.0.17", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index df30c7bebd..47e0c6b97a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.16", + "version": "8.0.17", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 1b1710f5015456cefad1d74617305981a22962ad Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:47:16 +1000 Subject: [PATCH 099/776] refactor: center scorm comments if no review button --- .../scorm-comment/scorm-comment.component.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html index edde1e057c..0c44be728c 100644 --- a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html +++ b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html @@ -8,7 +8,11 @@ > Review -
+ @if (!user.isStaff && !task.definition.scormAllowReview) { +
+ } @else { +
+ }
- -
-
diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.scss b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.scss index 05e8bffddc..82dc723d88 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.scss +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.scss @@ -8,3 +8,8 @@ .form-group { } + +#task-def-head { + background-color: white; + z-index: 10; +} diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-options/task-definition-options.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-options/task-definition-options.component.html index a479f20ce3..634c71a98c 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-options/task-definition-options.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-options/task-definition-options.component.html @@ -18,7 +18,7 @@
- Maximum Score + Quality Stars - Provide a score alongside the task status. We recommend avoiding this practice. + Provide a number of stars alongside the task status. Make sure you have a clear reason for + each star within your task description.
From f5388e45715597f0c92f252f721cf274bb8de0d5 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Wed, 10 Jul 2024 17:03:45 +1000 Subject: [PATCH 105/776] fix: ensure pdf is visible in task viewer mobile/narrow --- .../units/states/tasks/tasks-viewer/tasks-viewer.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/units/states/tasks/tasks-viewer/tasks-viewer.component.html b/src/app/units/states/tasks/tasks-viewer/tasks-viewer.component.html index 845e7203a5..56e528f29e 100644 --- a/src/app/units/states/tasks/tasks-viewer/tasks-viewer.component.html +++ b/src/app/units/states/tasks/tasks-viewer/tasks-viewer.component.html @@ -36,5 +36,6 @@
+ From e9046400e827481e09492bcb458b00b4e3aca347 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Wed, 10 Jul 2024 17:04:29 +1000 Subject: [PATCH 106/776] feat: add ability to minimise task details in task viewer --- .../f-task-details-view.component.html | 14 +++++++++++++- .../f-task-details-view.component.ts | 4 +++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/app/units/states/tasks/viewer/directives/f-task-details-view/f-task-details-view.component.html b/src/app/units/states/tasks/viewer/directives/f-task-details-view/f-task-details-view.component.html index 2ee4198b7c..5ca7a38d06 100644 --- a/src/app/units/states/tasks/viewer/directives/f-task-details-view/f-task-details-view.component.html +++ b/src/app/units/states/tasks/viewer/directives/f-task-details-view/f-task-details-view.component.html @@ -1,6 +1,18 @@
- + + + Task Details + + {{ panelOpenState() ? 'Hide' : 'Show' }} task details + + + +
diff --git a/src/app/units/states/tasks/viewer/directives/f-task-details-view/f-task-details-view.component.ts b/src/app/units/states/tasks/viewer/directives/f-task-details-view/f-task-details-view.component.ts index 57ea0b7993..8940b75542 100644 --- a/src/app/units/states/tasks/viewer/directives/f-task-details-view/f-task-details-view.component.ts +++ b/src/app/units/states/tasks/viewer/directives/f-task-details-view/f-task-details-view.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnInit, signal } from '@angular/core'; import { TaskDefinition } from 'src/app/api/models/task-definition'; import { Unit } from 'src/app/api/models/unit'; import { TasksViewerService } from '../../../tasks-viewer.service'; @@ -19,4 +19,6 @@ export class FTaskDetailsViewComponent implements OnInit { this.taskDef = taskDef; }); } + + public readonly panelOpenState = signal(false); } From 71df80b88ca626776e8e41d6530fbad6fe1355ec Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Wed, 10 Jul 2024 22:14:30 +1000 Subject: [PATCH 107/776] chore(release): 8.0.19 --- CHANGELOG.md | 13 +++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f108796bd4..c73daa7f9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [8.0.19](https://github.com/macite/doubtfire-deploy/compare/v8.0.18...v8.0.19) (2024-07-10) + + +### Features + +* add ability to minimise task details in task viewer ([e904640](https://github.com/macite/doubtfire-deploy/commit/e9046400e827481e09492bcb458b00b4e3aca347)) + + +### Bug Fixes + +* ensure pdf is visible in task viewer mobile/narrow ([f5388e4](https://github.com/macite/doubtfire-deploy/commit/f5388e45715597f0c92f252f721cf274bb8de0d5)) +* task def editor so save is not over fields and header visible for task being edited ([22a2616](https://github.com/macite/doubtfire-deploy/commit/22a26165c9f2ef634b3e79d1e5d5d6b74e6f2731)) + ### [8.0.18](https://github.com/macite/doubtfire-deploy/compare/v8.0.17...v8.0.18) (2024-07-03) diff --git a/package-lock.json b/package-lock.json index 52445d86a0..4571e71e31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.18", + "version": "8.0.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.18", + "version": "8.0.19", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 8529506a2d..6237d6dae1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.18", + "version": "8.0.19", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 3d44c115b498366b80e5b8b99ccd3eaa660ff613 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Mon, 15 Jul 2024 19:51:28 +1000 Subject: [PATCH 108/776] fix: ensure turn it in eula is accessible by all --- src/app/doubtfire.states.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/doubtfire.states.ts b/src/app/doubtfire.states.ts index b9f95e88af..c578d87b26 100644 --- a/src/app/doubtfire.states.ts +++ b/src/app/doubtfire.states.ts @@ -225,8 +225,8 @@ const EulaState: NgHybridStateDeclaration = { }, }, data: { - pageTitle: 'Teaching Periods', - roleWhitelist: ['Convenor', 'Admin'], + pageTitle: 'End User License Agreement', + roleWhitelist: ['Student', 'Tutor', 'Convenor', 'Admin', 'Auditor'], }, }; From f4360977d42466dcf2706c487b7648cd0faa409b Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Mon, 15 Jul 2024 19:51:43 +1000 Subject: [PATCH 109/776] chore(release): 8.0.20 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c73daa7f9c..a5fe9f894e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [8.0.20](https://github.com/macite/doubtfire-deploy/compare/v8.0.19...v8.0.20) (2024-07-15) + + +### Bug Fixes + +* ensure turn it in eula is accessible by all ([3d44c11](https://github.com/macite/doubtfire-deploy/commit/3d44c115b498366b80e5b8b99ccd3eaa660ff613)) + ### [8.0.19](https://github.com/macite/doubtfire-deploy/compare/v8.0.18...v8.0.19) (2024-07-10) diff --git a/package-lock.json b/package-lock.json index 4571e71e31..d176f2345c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.19", + "version": "8.0.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.19", + "version": "8.0.20", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 6237d6dae1..762969722e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.19", + "version": "8.0.20", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 9436f57e327eaee86b192a7adccbe43d800d3146 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Sat, 20 Jul 2024 14:37:39 +1000 Subject: [PATCH 110/776] refactor: create initial files for migration of comments-modal --- .../comments-modal/comments-modal.component.html | 0 .../comments-modal/comments-modal.component.scss | 0 .../modals/comments-modal/comments-modal.component.ts | 10 ++++++++++ 3 files changed, 10 insertions(+) create mode 100644 src/app/common/modals/comments-modal/comments-modal.component.html create mode 100644 src/app/common/modals/comments-modal/comments-modal.component.scss create mode 100644 src/app/common/modals/comments-modal/comments-modal.component.ts diff --git a/src/app/common/modals/comments-modal/comments-modal.component.html b/src/app/common/modals/comments-modal/comments-modal.component.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/common/modals/comments-modal/comments-modal.component.scss b/src/app/common/modals/comments-modal/comments-modal.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/common/modals/comments-modal/comments-modal.component.ts b/src/app/common/modals/comments-modal/comments-modal.component.ts new file mode 100644 index 0000000000..993f67ce00 --- /dev/null +++ b/src/app/common/modals/comments-modal/comments-modal.component.ts @@ -0,0 +1,10 @@ +import {Component, Input, Inject} from '@angular/core'; + +@Component({ + selector: 'comments-modal', + templateUrl: 'comments-modal.component.html', + styleUrls: ['comments-modal.component.scss'], +}) +export class CommentsModalComponent { + constructor() {} +} From 152b2fb30a6cbf0eaa9225bf04049b78c6161c4a Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Sat, 20 Jul 2024 15:18:37 +1000 Subject: [PATCH 111/776] refactor: create service file for migration of comments-modal --- src/app/common/modals/comments-modal/comments-modal.service.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/app/common/modals/comments-modal/comments-modal.service.ts diff --git a/src/app/common/modals/comments-modal/comments-modal.service.ts b/src/app/common/modals/comments-modal/comments-modal.service.ts new file mode 100644 index 0000000000..e69de29bb2 From 13710e2fa17419f5c22940af87091d9c55dc0a87 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Sat, 20 Jul 2024 20:53:31 +1000 Subject: [PATCH 112/776] refactor: migrate comments-modal --- .../comments-modal.component.html | 7 +++++++ .../comments-modal.component.scss | 17 +++++++++++++++++ .../comments-modal.component.ts | 19 ++++++++++++++----- .../comments-modal/comments-modal.service.ts | 18 ++++++++++++++++++ src/app/common/modals/modals.coffee | 1 - src/app/doubtfire-angular.module.ts | 2 ++ src/app/doubtfire-angularjs.module.ts | 5 +++-- 7 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/app/common/modals/comments-modal/comments-modal.component.html b/src/app/common/modals/comments-modal/comments-modal.component.html index e69de29bb2..888da289d2 100644 --- a/src/app/common/modals/comments-modal/comments-modal.component.html +++ b/src/app/common/modals/comments-modal/comments-modal.component.html @@ -0,0 +1,7 @@ + diff --git a/src/app/common/modals/comments-modal/comments-modal.component.scss b/src/app/common/modals/comments-modal/comments-modal.component.scss index e69de29bb2..76d21e070f 100644 --- a/src/app/common/modals/comments-modal/comments-modal.component.scss +++ b/src/app/common/modals/comments-modal/comments-modal.component.scss @@ -0,0 +1,17 @@ +.modal-comment { + padding: 15px; + + .image-comment { + width: 100%; + height: 100%; + align-content: center; + border-radius: 5px; + padding: 0; + border: none; + } + .pdf-comment { + width: 100%; + height: 80vh; + align-content: center; + } +} diff --git a/src/app/common/modals/comments-modal/comments-modal.component.ts b/src/app/common/modals/comments-modal/comments-modal.component.ts index 993f67ce00..6fa5e57b01 100644 --- a/src/app/common/modals/comments-modal/comments-modal.component.ts +++ b/src/app/common/modals/comments-modal/comments-modal.component.ts @@ -1,10 +1,19 @@ -import {Component, Input, Inject} from '@angular/core'; +import {Component, Input, Inject, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA} from '@angular/material/dialog'; @Component({ selector: 'comments-modal', - templateUrl: 'comments-modal.component.html', - styleUrls: ['comments-modal.component.scss'], + templateUrl: './comments-modal.component.html', + styleUrls: ['./comments-modal.component.scss'], }) -export class CommentsModalComponent { - constructor() {} +export class CommentsModalComponent implements OnInit { + @Input() commentType: string; + @Input() commentResourceUrl: string; + + constructor(@Inject(MAT_DIALOG_DATA) public data: any) {} + + ngOnInit(): void { + this.commentType = this.data.commentType; + this.commentResourceUrl = this.data.commentResourceUrl; + } } diff --git a/src/app/common/modals/comments-modal/comments-modal.service.ts b/src/app/common/modals/comments-modal/comments-modal.service.ts index e69de29bb2..d81a678160 100644 --- a/src/app/common/modals/comments-modal/comments-modal.service.ts +++ b/src/app/common/modals/comments-modal/comments-modal.service.ts @@ -0,0 +1,18 @@ +import {Injectable} from '@angular/core'; +import {MatDialog} from '@angular/material/dialog'; +import {CommentsModalComponent} from './comments-modal.component'; + +@Injectable({ + providedIn: 'root', +}) +export class CommentsModalService { + constructor(public dialog: MatDialog) {} + + public show(commentResourceUrl: string, commentType: string) { + this.dialog.open(CommentsModalComponent, { + data: {commentResourceUrl, commentType}, + width: '100%', + maxWidth: '800px', + }); + } +} diff --git a/src/app/common/modals/modals.coffee b/src/app/common/modals/modals.coffee index 16d2be1ec8..003a522629 100644 --- a/src/app/common/modals/modals.coffee +++ b/src/app/common/modals/modals.coffee @@ -1,5 +1,4 @@ angular.module("doubtfire.common.modals", [ 'doubtfire.common.modals.csv-result-modal' 'doubtfire.common.modals.confirmation-modal' - 'doubtfire.common.modals.comments-modal' ]) diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 9284745988..58dc8baa67 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -224,6 +224,7 @@ import {FTaskSheetViewComponent} from './units/states/tasks/viewer/directives/f- import {TasksViewerComponent} from './units/states/tasks/tasks-viewer/tasks-viewer.component'; import {UnitCodeComponent} from './common/unit-code/unit-code.component'; import {GradeService} from './common/services/grade.service'; +import {CommentsModalComponent} from './common/modals/comments-modal/comments-modal.component'; @NgModule({ // Components we declare @@ -325,6 +326,7 @@ import {GradeService} from './common/services/grade.service'; FUsersComponent, FTaskBadgeComponent, FUnitsComponent, + CommentsModalComponent, ], // Services we provide providers: [ diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 868e381213..d58937e8ad 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -119,7 +119,6 @@ import 'build/src/app/units/states/analytics/analytics.js'; import 'build/src/app/common/filters/filters.js'; import 'build/src/app/common/content-editable/content-editable.js'; import 'build/src/app/common/modals/confirmation-modal/confirmation-modal.js'; -import 'build/src/app/common/modals/comments-modal/comments-modal.js'; import 'build/src/app/common/modals/csv-result-modal/csv-result-modal.js'; import 'build/src/app/common/modals/modals.js'; import 'build/src/app/common/grade-icon/grade-icon.js'; @@ -220,11 +219,12 @@ import {FUnitTaskListComponent} from './units/states/tasks/viewer/directives/f-u import {FTaskDetailsViewComponent} from './units/states/tasks/viewer/directives/f-task-details-view/f-task-details-view.component'; import {FTaskSheetViewComponent} from './units/states/tasks/viewer/directives/f-task-sheet-view/f-task-sheet-view.component'; import {TasksViewerComponent} from './units/states/tasks/tasks-viewer/tasks-viewer.component'; - import {FUnitsComponent} from './admin/states/f-units/f-units.component'; import {MarkedPipe} from './common/pipes/marked.pipe'; import {AlertService} from './common/services/alert.service'; import {GradeService} from './common/services/grade.service'; +import {CommentsModalService} from './common/modals/comments-modal/comments-modal.service'; + export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', 'doubtfire.sessions', @@ -306,6 +306,7 @@ DoubtfireAngularJSModule.factory( downgradeInjectable(EditProfileDialogService), ); DoubtfireAngularJSModule.factory('CreateNewUnitModal', downgradeInjectable(CreateNewUnitModal)); +DoubtfireAngularJSModule.factory('CommentsModal', downgradeInjectable(CommentsModalService)); // directive -> component DoubtfireAngularJSModule.directive( From 4400a027d5388d2b14ae6e314d3fc973cd8012eb Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Sat, 20 Jul 2024 20:56:57 +1000 Subject: [PATCH 113/776] refactor: remove old comments-modal files --- .../comments-modal/comments-modal.coffee | 32 ------------------- .../modals/comments-modal/comments-modal.scss | 15 --------- .../comments-modal/comments-modal.tpl.html | 10 ------ 3 files changed, 57 deletions(-) delete mode 100644 src/app/common/modals/comments-modal/comments-modal.coffee delete mode 100644 src/app/common/modals/comments-modal/comments-modal.scss delete mode 100644 src/app/common/modals/comments-modal/comments-modal.tpl.html diff --git a/src/app/common/modals/comments-modal/comments-modal.coffee b/src/app/common/modals/comments-modal/comments-modal.coffee deleted file mode 100644 index 59874c47e1..0000000000 --- a/src/app/common/modals/comments-modal/comments-modal.coffee +++ /dev/null @@ -1,32 +0,0 @@ -angular.module("doubtfire.common.modals.comments-modal", []) -# -# Modal to contain an image used in user comments. -# -.factory("CommentsModal", ($modal) -> - CommentsModal = {} - CommentsModal.show = (commentResourceUrl, commentType) -> - $modal.open - templateUrl: 'common/modals/comments-modal/comments-modal.tpl.html' - controller: 'CommentsModalCtrl' - size: 'lg' - resolve: - commentResourceUrl: -> commentResourceUrl - commentType: -> commentType - CommentsModal -) -.controller("CommentsModalCtrl", ($scope, $modalInstance, $sce, commentResourceUrl, commentType, alertService, fileDownloaderService) -> - # $scope.commentResourceUrl = $sce.trustAsResourceUrl(commentResourceUrl) - $scope.commentType = commentType - $scope.close = -> - fileDownloaderService.releaseBlob($scope.rawResourceUrl) - $modalInstance.dismiss() - - fileDownloaderService.downloadBlob( - commentResourceUrl, - (url, response) -> - $scope.rawResourceUrl = url - $scope.commentResourceUrl = $sce.trustAsResourceUrl(url) - (error) -> - alertService.error( "Error downloading comment: #{error}") - ) -) diff --git a/src/app/common/modals/comments-modal/comments-modal.scss b/src/app/common/modals/comments-modal/comments-modal.scss deleted file mode 100644 index 22ccacf4f8..0000000000 --- a/src/app/common/modals/comments-modal/comments-modal.scss +++ /dev/null @@ -1,15 +0,0 @@ -.modal-comment { - .image-comment { - width: 100%; - height: 100%; - align-content: center; - border-radius: 5px; - padding: 0; - border: none; - } - .pdf-comment { - width: 100%; - height: 80vh; - align-content: center; - } -} \ No newline at end of file diff --git a/src/app/common/modals/comments-modal/comments-modal.tpl.html b/src/app/common/modals/comments-modal/comments-modal.tpl.html deleted file mode 100644 index 59985945ff..0000000000 --- a/src/app/common/modals/comments-modal/comments-modal.tpl.html +++ /dev/null @@ -1,10 +0,0 @@ -

+ + {{ message }} + + + + + diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.component.scss b/src/app/common/modals/confirmation-modal/confirmation-modal.component.scss index 9afc6bd60f..e69de29bb2 100644 --- a/src/app/common/modals/confirmation-modal/confirmation-modal.component.scss +++ b/src/app/common/modals/confirmation-modal/confirmation-modal.component.scss @@ -1,3 +0,0 @@ -.confirmation-modal .modal-body { - font-size: 1.5em; - } \ No newline at end of file From 9eca5ff0f23b14e169a93e7b1f7197f1b077f073 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Sun, 11 Aug 2024 19:12:55 +1000 Subject: [PATCH 141/776] refactor: improve modal size --- .../confirmation-modal.component.ts | 24 ++++++++++------ .../confirmation-modal.service.ts | 28 +++++++++++-------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.component.ts b/src/app/common/modals/confirmation-modal/confirmation-modal.component.ts index 27f0d89d8c..f9506e5b8b 100644 --- a/src/app/common/modals/confirmation-modal/confirmation-modal.component.ts +++ b/src/app/common/modals/confirmation-modal/confirmation-modal.component.ts @@ -1,9 +1,12 @@ -/* eslint-disable prettier/prettier */ -/* eslint-disable @angular-eslint/component-selector */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Component, OnInit, Input, Inject} from '@angular/core'; -import { AlertService } from '../../services/alert.service'; -import { MatDialogRef } from '@angular/material/dialog'; +import {Component, OnInit, Input, Inject} from '@angular/core'; +import {AlertService} from '../../services/alert.service'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; + +export interface ConfirmationModalData { + title: string; + message: string; + action?: any; +} @Component({ selector: 'confirmation-modal', @@ -16,12 +19,16 @@ export class ConfirmationModalComponent implements OnInit { @Input() action: () => void; constructor( - @Inject(AlertService) private alertService: any, + @Inject(AlertService) private alertService: AlertService, + @Inject(MAT_DIALOG_DATA) public data: ConfirmationModalData, + public dialogRef: MatDialogRef, ) {} ngOnInit(): void { - console.log('confirmation-model ngOnInit()'); + this.title = this.data.title; + this.message = this.data.message; + this.action = this.data.action; } public confirmAction() { @@ -30,7 +37,6 @@ export class ConfirmationModalComponent implements OnInit { } else { this.alertService.error(`${this.title} action failed.`); } - this.dialogRef.close(); } diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts b/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts index 06c4da61e8..6257277eff 100644 --- a/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts +++ b/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts @@ -1,9 +1,6 @@ -/* eslint-disable prefer-const */ -/* eslint-disable prettier/prettier */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Injectable } from '@angular/core'; -import { MatDialogRef, MatDialog } from '@angular/material/dialog'; -import { ConfirmationModalComponent } from './confirmation-modal.component'; +import {Injectable} from '@angular/core'; +import {MatDialog} from '@angular/material/dialog'; +import {ConfirmationModalComponent, ConfirmationModalData} from './confirmation-modal.component'; @Injectable({ providedIn: 'root', @@ -12,11 +9,18 @@ export class ConfirmationModalService { constructor(public dialog: MatDialog) {} public show(title: string, message: string, action?: any) { - let dialogRef: MatDialogRef; - dialogRef = this.dialog.open(ConfirmationModalComponent, {position: {top: '2.5%'}}); - dialogRef.updateSize('42.5%', ''); - dialogRef.componentInstance.title = title; - dialogRef.componentInstance.message = message; - dialogRef.componentInstance.action = action; + this.dialog.open( + ConfirmationModalComponent, + { + data: { + title, + message, + action, + }, + position: {top: '2.5%'}, + width: '100%', + maxWidth: '650px', + }, + ); } } From 955d0a17ae23be77954145ba189ac8bb0aa90da0 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Sun, 11 Aug 2024 19:18:08 +1000 Subject: [PATCH 142/776] fix: remove duplicate alert --- .../unit-tasks-editor/unit-task-editor.component.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts index ee0f135c26..bab5d97b37 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts @@ -137,9 +137,7 @@ export class UnitTaskEditorComponent implements AfterViewInit { () => { this.unit.deleteTaskDefinition(taskDefinition); //TODO: reinstate ProgressModal.show "Deleting Task #{task.abbreviation}", 'Please wait while student projects are updated.', promise - - this.alerts.success('Task deleted'); - } + }, ); } From 27e9525147d8b500ac1a4df90080815cb32f67fe Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Sun, 11 Aug 2024 19:24:09 +1000 Subject: [PATCH 143/776] refactor: align caption with modal title --- .../confirmation-modal/confirmation-modal.component.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.component.html b/src/app/common/modals/confirmation-modal/confirmation-modal.component.html index c2d7eb5cfe..3ec07fc729 100644 --- a/src/app/common/modals/confirmation-modal/confirmation-modal.component.html +++ b/src/app/common/modals/confirmation-modal/confirmation-modal.component.html @@ -1,10 +1,12 @@

-
+
-
{{ title }}
+
+
{{ title }}
+ Please confirm that you want to perform this action. +
- Please confirm that you want to perform this action.

{{ message }} From fb4a2ce527508fd64fe2fc6e89c10cc521234151 Mon Sep 17 00:00:00 2001 From: JackSCarroll <64730336+JackSCarroll@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:27:51 +1000 Subject: [PATCH 144/776] refactor: add button on frontend for future jplag report viewer --- src/app/common/footer/footer.component.html | 4 ++++ src/app/common/footer/footer.component.ts | 4 ++++ src/app/projects/states/dashboard/selected-task.service.ts | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/src/app/common/footer/footer.component.html b/src/app/common/footer/footer.component.html index 7e28c72b32..d34e3c265f 100644 --- a/src/app/common/footer/footer.component.html +++ b/src/app/common/footer/footer.component.html @@ -108,6 +108,10 @@ crisis_alert View similarities + -
@if (collapsable) { - - } @if (!isTaskDefMode) { - - } @if (isTaskDefMode) { - + + } + @if (!isTaskDefMode) { + + } + @if (isTaskDefMode) { + } + @@ -93,16 +102,20 @@ name="taskDefID" (selectionChange)="taskDefinitionIdChanged()" > - All Task Definitions + All Task Definitions @for (td of unit.taskDefinitionCache.values | async; track td) { - - {{ td.abbreviation + ' - ' + td.name }} - + + {{ td.abbreviation + ' - ' + td.name }} + } @@ -140,14 +158,19 @@ - +
-
+
@if (!isNarrow) { -
- -
+
+ +
}
@@ -170,44 +195,45 @@ @if (filteredTasks) { - - - - @if (task) { -
+ + -
- - -
-

{{ task.project.student.name }}

-
- {{ task.definition.abbreviation }} - - {{ task.definition.name }} -
- -
- @if (task.hasGrade()) { -
- {{ task.gradeDesc() }} -
- } - - @if (!isTaskDefMode) { -
- - - - - + + @if (!isTaskDefMode) { +
+ + + + + +
+ } +
+
- } -
- -
- } - + } + - - - + + + }
diff --git a/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts b/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts index 95b498af20..a6d26643c2 100644 --- a/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts +++ b/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts @@ -226,6 +226,18 @@ export class StaffTaskListComponent implements OnInit, OnChanges, OnDestroy { ); } + downloadJPLAGReport() { + const taskDef = this.filters.taskDefinition; + this.fileDownloaderService.downloadFile( + //this.taskData.selectedTask.jplagReportUrl() + `${AppInjector.get(DoubtfireConstants).API_URL}/units/${ + this.unit.id + }/task_definitions/${taskDef.id}/jplag_report`, + `${this.unit.code}-${taskDef.abbreviation}-jplag-report.zip`, + ); + window.open('https://jplag.github.io/JPlag/', '_blank'); + } + openDialog() { const dialogRef = this.dialog.open(this.searchDialog); From 1ef531644cf34957640c8c002a7bf4655b4f505d Mon Sep 17 00:00:00 2001 From: JackSCarroll <64730336+JackSCarroll@users.noreply.github.com> Date: Tue, 24 Sep 2024 22:02:46 +1000 Subject: [PATCH 150/776] chore: commit edited files --- src/app/common/footer/footer.component.html | 4 ---- src/app/common/footer/footer.component.ts | 1 - .../states/dashboard/selected-task.service.ts | 5 ----- .../task-definition-upload.component.html | 2 ++ .../staff-task-list.component.html | 6 +++++- .../staff-task-list/staff-task-list.component.ts | 15 +++++++++++++++ 6 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/app/common/footer/footer.component.html b/src/app/common/footer/footer.component.html index 9d595ac33d..7e28c72b32 100644 --- a/src/app/common/footer/footer.component.html +++ b/src/app/common/footer/footer.component.html @@ -108,10 +108,6 @@ crisis_alert View similarities - - diff --git a/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts b/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts index a6d26643c2..12e1148714 100644 --- a/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts +++ b/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts @@ -206,6 +206,21 @@ export class StaffTaskListComponent implements OnInit, OnChanges, OnDestroy { return this.taskData.taskDefMode; } + // public get taskHasJplagReport(): boolean { + // console.log('taskHasSimilarityChecks getter called'); + // if (!this.selectedTask.hasSimilarity()) { + // console.error('filters.taskDefinition is not defined'); + // return false; + // } + // if (typeof this.filters.taskDefinition.hasPlagiarismCheck !== 'function') { + // console.error('hasPlagiarismCheck is not a function'); + // return false; + // } + // const result = this.filters.taskDefinition.hasPlagiarismCheck(); + // console.log('taskHasSimilarityChecks result:', result); + // return result; + // } + downloadSubmissionPdfs() { const taskDef = this.filters.taskDefinition; this.fileDownloaderService.downloadFile( From d9c64b09e4b8770091157594bef97613460d918e Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Sat, 28 Sep 2024 16:41:10 +1000 Subject: [PATCH 151/776] refactor: replace iframe with native pdf-viewer --- .../modals/comments-modal/comments-modal.component.html | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/app/common/modals/comments-modal/comments-modal.component.html b/src/app/common/modals/comments-modal/comments-modal.component.html index 1831477b8d..707bcff36c 100644 --- a/src/app/common/modals/comments-modal/comments-modal.component.html +++ b/src/app/common/modals/comments-modal/comments-modal.component.html @@ -2,10 +2,6 @@ @if (taskComment.commentType === 'image') { } @else if (taskComment.commentType === 'pdf') { - + }
From 90830cdba73bcfe564362015b5abaf2c40810f5f Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Sat, 28 Sep 2024 17:02:52 +1000 Subject: [PATCH 152/776] feat: add startpage property to auto scroll to on pdf render --- .../common/pdf-viewer/pdf-viewer.component.html | 2 +- .../common/pdf-viewer/pdf-viewer.component.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/app/common/pdf-viewer/pdf-viewer.component.html b/src/app/common/pdf-viewer/pdf-viewer.component.html index b060046125..ed361fecd8 100644 --- a/src/app/common/pdf-viewer/pdf-viewer.component.html +++ b/src/app/common/pdf-viewer/pdf-viewer.component.html @@ -58,7 +58,7 @@ [render-text]="true" [original-size]="false" (after-load-complete)="onLoaded()" - (page-rendered)="loaded = true" + (page-rendered)="onPageRendered()" > } } @else { diff --git a/src/app/common/pdf-viewer/pdf-viewer.component.ts b/src/app/common/pdf-viewer/pdf-viewer.component.ts index 8f21f8e47c..b1b3945acb 100644 --- a/src/app/common/pdf-viewer/pdf-viewer.component.ts +++ b/src/app/common/pdf-viewer/pdf-viewer.component.ts @@ -24,6 +24,8 @@ export class fPdfViewerComponent implements OnDestroy, OnChanges, AfterViewInit public useNativePdfViewer = false; @Input() pdfUrl: string; + @Input() startPage: number = 1; + @ViewChild(PdfViewerComponent) private pdfComponent: PdfViewerComponent; pdfSearchString: string; zoomValue = 1; @@ -75,6 +77,14 @@ export class fPdfViewerComponent implements OnDestroy, OnChanges, AfterViewInit }); } + scrollToPage(pageNumber: number) { + if (pageNumber <= this.pdfComponent.pdfViewer.pagesCount) { + this.pdfComponent.pdfViewer.scrollPageIntoView({ + pageNumber, + }); + } + } + public zoomIn() { if (this.zoomValue < 2.5) { this.zoomValue += 0.1; @@ -111,4 +121,11 @@ export class fPdfViewerComponent implements OnDestroy, OnChanges, AfterViewInit this.loaded = true; window.dispatchEvent(new Event('resize')); } + + onPageRendered() { + this.loaded = true; + if (this.startPage > 1) { + this.scrollToPage(this.startPage); + } + } } From 45cc89e18cff8fcde66480448eea178940d316a2 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Sat, 28 Sep 2024 17:04:27 +1000 Subject: [PATCH 153/776] feat: init pdf viewer on page 2 for student submissions --- .../task-dashboard/task-dashboard.component.html | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.html index b2d723387c..835fead6ca 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.html @@ -1,11 +1,15 @@
- + - + @@ -15,7 +19,7 @@ - + From 1348da2d345b080002a62a24d717fac15c4b0a92 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:22:03 +1000 Subject: [PATCH 154/776] feat: persistent pdf zoom level --- src/app/common/pdf-viewer/pdf-viewer.component.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/app/common/pdf-viewer/pdf-viewer.component.ts b/src/app/common/pdf-viewer/pdf-viewer.component.ts index 8f21f8e47c..17f6d85fbd 100644 --- a/src/app/common/pdf-viewer/pdf-viewer.component.ts +++ b/src/app/common/pdf-viewer/pdf-viewer.component.ts @@ -19,6 +19,9 @@ import {AlertService} from '../services/alert.service'; styleUrls: ['./pdf-viewer.component.scss'], }) export class fPdfViewerComponent implements OnDestroy, OnChanges, AfterViewInit { + private readonly ZOOM_MIN = 0.5; + private readonly ZOOM_MAX = 2.5; + private _pdfUrl: string; public pdfBlobUrl: string; public useNativePdfViewer = false; @@ -43,6 +46,9 @@ export class fPdfViewerComponent implements OnDestroy, OnChanges, AfterViewInit ngAfterViewInit(): void { this.useNativePdfViewer = localStorage.getItem('useNativePdfViewer') === 'true'; + const storedZoomValue = parseFloat(localStorage.getItem('pdfViewerZoom')) || 1; + // Clamp zoom value between ZOOM_MIN and ZOOM_MAX + this.zoomValue = Math.min(Math.max(storedZoomValue, this.ZOOM_MIN), this.ZOOM_MAX); } ngOnChanges(changes: SimpleChanges): void { @@ -76,13 +82,15 @@ export class fPdfViewerComponent implements OnDestroy, OnChanges, AfterViewInit } public zoomIn() { - if (this.zoomValue < 2.5) { + if (this.zoomValue < this.ZOOM_MAX) { this.zoomValue += 0.1; + localStorage.setItem('pdfViewerZoom', this.zoomValue.toString()); } } public zoomOut() { - if (this.zoomValue > 0.5) { + if (this.zoomValue > this.ZOOM_MIN) { this.zoomValue -= 0.1; + localStorage.setItem('pdfViewerZoom', this.zoomValue.toString()); } } From 8549817a48f46f81ffc7918c194d815870ac080a Mon Sep 17 00:00:00 2001 From: JackSCarroll <64730336+JackSCarroll@users.noreply.github.com> Date: Fri, 11 Oct 2024 19:48:40 +1100 Subject: [PATCH 155/776] refactor: add features for jplag report download --- src/app/api/models/task-definition.ts | 4 +-- src/app/api/models/task-similarity.ts | 6 ++--- .../api/services/task-definition.service.ts | 2 +- .../task-similarity-view.component.html | 27 +++++++++++++------ .../task-similarity-view.component.ts | 18 ++++++++++++- .../task-definition-upload.component.html | 20 +++++++------- 6 files changed, 51 insertions(+), 26 deletions(-) diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index 1b49a2e856..48ce4699fa 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -36,7 +36,7 @@ export class TaskDefinition extends Entity { maxQualityPts: number; overseerImageId: number; assessmentEnabled: boolean; - mossLanguage: string = 'moss c'; + jplagLanguage: string = 'c'; readonly unit: Unit; @@ -160,7 +160,7 @@ export class TaskDefinition extends Entity { return this.plagiarismChecks?.length > 0; } - public get needsMoss(): boolean { + public get needsJplag(): boolean { return this.uploadRequirements.some((upreq) => upreq.type === 'code' && upreq.tiiCheck); } diff --git a/src/app/api/models/task-similarity.ts b/src/app/api/models/task-similarity.ts index c3a72e556f..be8b3d9b09 100644 --- a/src/app/api/models/task-similarity.ts +++ b/src/app/api/models/task-similarity.ts @@ -5,7 +5,7 @@ import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants import { Observable } from 'rxjs'; export enum TaskSimilarityType { - Moss = 'MossTaskSimilarity', + Jplag = 'JplagTaskSimilarity', TurnItIn = 'TiiTaskSimilarity', } @@ -62,8 +62,8 @@ export class TaskSimilarity extends Entity { public get friendlyTypeName(): string { switch (this.type) { - case TaskSimilarityType.Moss: - return 'MOSS'; + case TaskSimilarityType.Jplag: + return 'JPLAG'; case TaskSimilarityType.TurnItIn: return 'TurnItIn'; } diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index 13a1dd2797..e0b685c20b 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -21,7 +21,7 @@ export class TaskDefinitionService extends CachedEntityService { 'description', 'weighting', 'targetGrade', - 'mossLanguage', + 'jplagLanguage', { keys: 'targetDate', toEntityFn: MappingFunctions.mapDateToEndOfDay, diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component.html index f068cb609f..2f42a7974d 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component.html @@ -23,14 +23,25 @@

{{ part.description }} @if (similarity.readyForViewer) { - + @if (similarity.type === 'JplagTaskSimilarity') { + + } @else { + + } } @if (i === 0) { -
diff --git a/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts b/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts index 12e1148714..2b2c032869 100644 --- a/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts +++ b/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts @@ -77,6 +77,8 @@ export class StaffTaskListComponent implements OnInit, OnChanges, OnDestroy { tasks: any[] = null; + hasJplagReport: boolean; + watchingTaskKey: any; panelOpenState = false; @@ -206,20 +208,19 @@ export class StaffTaskListComponent implements OnInit, OnChanges, OnDestroy { return this.taskData.taskDefMode; } - // public get taskHasJplagReport(): boolean { - // console.log('taskHasSimilarityChecks getter called'); - // if (!this.selectedTask.hasSimilarity()) { - // console.error('filters.taskDefinition is not defined'); - // return false; - // } - // if (typeof this.filters.taskDefinition.hasPlagiarismCheck !== 'function') { - // console.error('hasPlagiarismCheck is not a function'); - // return false; - // } - // const result = this.filters.taskDefinition.hasPlagiarismCheck(); - // console.log('taskHasSimilarityChecks result:', result); - // return result; - // } + // TODO: Get this to return correct value from the API + public async taskHasJplagReport(): Promise { + const taskDef = this.filters.taskDefinition; + return taskDef + .hasJplagReport() + .then((hasReport) => { + return hasReport ?? false; + }) + .catch((error) => { + console.error(error); + return false; + }); + } downloadSubmissionPdfs() { const taskDef = this.filters.taskDefinition; @@ -376,6 +377,10 @@ export class StaffTaskListComponent implements OnInit, OnChanges, OnDestroy { this.alertService.error(message, 6000); }, }); + this.taskHasJplagReport().then((hasReport) => { + this.hasJplagReport = hasReport; + }); + console.log('HAS JPLAG REPORT:', this.hasJplagReport); } setSelectedTask(task: Task) { From a5316c47842a80f4e7bb0bbf1a84e27ccdc24e2d Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 25 Oct 2024 21:14:39 +1100 Subject: [PATCH 158/776] chore(release): 8.0.29 --- CHANGELOG.md | 97 +++++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 4 +- package.json | 2 +- 3 files changed, 100 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67a49d5d89..28577a1a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,103 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [8.0.29](https://github.com/macite/doubtfire-deploy/compare/v8.0.28...v8.0.29) (2024-10-25) + + +### Features + +* add support for the view language ([151ca04](https://github.com/macite/doubtfire-deploy/commit/151ca04ca8da9059890d7846293bf34ae67f05c5)) + + +### Bug Fixes + +* correct task scroll into view ([7984b39](https://github.com/macite/doubtfire-deploy/commit/7984b393752381e0a219dbbdafc0e7b44c3fcdb3)) + +### [8.0.27](https://github.com/macite/doubtfire-deploy/compare/v8.0.26...v8.0.27) (2024-08-09) + + +### Bug Fixes + +* encode . in the username in scorm player ([1ec1754](https://github.com/macite/doubtfire-deploy/commit/1ec175419ab3ef80f22ac8a6384f6ed40a17c217)) + +### [8.0.26](https://github.com/macite/doubtfire-deploy/compare/v8.0.25...v8.0.26) (2024-08-09) + + +### Bug Fixes + +* register service names in state resolve functions ([64e41f4](https://github.com/macite/doubtfire-deploy/commit/64e41f4aedcf519a2786d7fe4929900f86731896)) + +### [8.0.25](https://github.com/macite/doubtfire-deploy/compare/v8.0.24...v8.0.25) (2024-08-09) + + +### Bug Fixes + +* adjust shortcuts and unregister on inbox destroy ([8a2ce3b](https://github.com/macite/doubtfire-deploy/commit/8a2ce3bfe32d99be1bc0667ca1aba55d36f987d2)) +* switch to control shift ([a0c1185](https://github.com/macite/doubtfire-deploy/commit/a0c1185bf4af3acb8f58c97cc27021c952861603)) + +### [8.0.24](https://github.com/macite/doubtfire-deploy/compare/v8.0.23...v8.0.24) (2024-08-09) + + +### Features + +* add ability to preview scorm test ([7ffbea9](https://github.com/macite/doubtfire-deploy/commit/7ffbea9f56c393d44571c18f30735e090701b554)) +* add new Numbas Feature ([6f1ac4e](https://github.com/macite/doubtfire-deploy/commit/6f1ac4e4c1a78115c426838bce350bce286e44f0)) +* add new Numbas Feature ([8eba913](https://github.com/macite/doubtfire-deploy/commit/8eba913ec4462e1c252a8a698b4b6de67c4ec25f)) +* add numbas component ([7c1734b](https://github.com/macite/doubtfire-deploy/commit/7c1734b137ac369b3b605b38749c845df38b9a78)) +* add Numbas config options to task def service keys ([ff28e48](https://github.com/macite/doubtfire-deploy/commit/ff28e4802b86dc22be339cc196ef82ade67934f7)) +* add Numbas test attempt model ([097df79](https://github.com/macite/doubtfire-deploy/commit/097df7960d34a8c905ee495b8c6c8b8843e43b36)) +* add Numbas test section on ready for feedback ([e61295c](https://github.com/macite/doubtfire-deploy/commit/e61295c50006dbc89cfc0639bb3c68a09a3cda9d)) +* add Numbas test upload section and reorder editor sections ([edbd536](https://github.com/macite/doubtfire-deploy/commit/edbd536e73ace8b6b4cced1481c809fd36524dce)) +* add Numbas upload component and related functions to task-definition model ([4ecaee8](https://github.com/macite/doubtfire-deploy/commit/4ecaee8ad1c0caed9a5d848066a173000989f79b)) +* add test attempt service ([a576f48](https://github.com/macite/doubtfire-deploy/commit/a576f484bc434728eab9632c022bccf1ed26cb01)) +* add test attempt service and minor numbas related changes ([0652b56](https://github.com/macite/doubtfire-deploy/commit/0652b56f69eb850c2dfb43be54d07beb4c4eb469)) +* added numbas-lms service code ([471d344](https://github.com/macite/doubtfire-deploy/commit/471d34486f2582e95aac84469187f087277cc079)) +* allow changing scorm review config and add minor UI changes ([fc023af](https://github.com/macite/doubtfire-deploy/commit/fc023af462656e3557e2970f211fa4d59ee1e3d5)) +* change Numbas time delay config to enable incremental delays ([0afa719](https://github.com/macite/doubtfire-deploy/commit/0afa7197293c90a154c1716db91ecadae3677d53)) +* disable attempt button if passed and add button to review latest attempt in card ([703563c](https://github.com/macite/doubtfire-deploy/commit/703563c86253c60ad30d451ee2d8e0fa7ebbfabb)) +* display numbas task comments ([48a31da](https://github.com/macite/doubtfire-deploy/commit/48a31da1442f1e14f497c614053d9002c9f2631b)) +* enable reviewing, passing, deleting test attempts and add test attempt model and service ([561b924](https://github.com/macite/doubtfire-deploy/commit/561b9241c2f44fd69d3f09c656a025f514bbaf3a)) +* enable students to request extra scorm attempt ([d904ffd](https://github.com/macite/doubtfire-deploy/commit/d904ffd6fd674f6e61894d517f5aa147d1db6d29)) +* get unique token for scorm asset retrieval ([8fe2f9e](https://github.com/macite/doubtfire-deploy/commit/8fe2f9e9ca60c02e55c24d0f2bb3eb6dbea8217c)) +* implement numbas test data upload in task definition service ([2c7dab5](https://github.com/macite/doubtfire-deploy/commit/2c7dab555fc9a226b980a8dae96f5738bf517b1b)) +* insert Numbas test rules options in the task editor ([7e52ad5](https://github.com/macite/doubtfire-deploy/commit/7e52ad5e5759291d7d61805070ec482eb49c90be)) +* numbas-test-numbas-service ([cee13b7](https://github.com/macite/doubtfire-deploy/commit/cee13b727b35f804fc16070885f0e57c422c9982)) +* prevent uploading files until scorm passed ([ec86e4e](https://github.com/macite/doubtfire-deploy/commit/ec86e4eca8e9c50ebdaf54c34c302da88dd44b31)) +* show banner based on scorm success status ([db16172](https://github.com/macite/doubtfire-deploy/commit/db161721fab1359694c44d8df7237cd01892938b)) +* show launch button on ready for feedback if Numbas test is enabled for the task ([17af5b7](https://github.com/macite/doubtfire-deploy/commit/17af5b7fe9e1fbc8e5f18e2fece6e029b017408c)) +* use confirmation modal when passing or deleting test attempts ([3fb25bb](https://github.com/macite/doubtfire-deploy/commit/3fb25bb373acab837a3ef787620dd654bfd6803f)) + + +### Bug Fixes + +* add accepted Numbas file types ([bcaa8af](https://github.com/macite/doubtfire-deploy/commit/bcaa8af150aaf68b5cf5fb07ad9cb72037212d5d)) +* add auth headers to scorm adapter xhr requests ([97e1ea1](https://github.com/macite/doubtfire-deploy/commit/97e1ea187b769a51ebb66b82d76f21193bb29386)) +* adjusted edit profile accidental change ([316abc7](https://github.com/macite/doubtfire-deploy/commit/316abc775eeba6ca846c7c742bc88bec61d05a5e)) +* change success status descriptions ([20da042](https://github.com/macite/doubtfire-deploy/commit/20da0423450fe3b9b8006660bf6f5e06670f520c)) +* delete comment as well as test attempt ([b6887e8](https://github.com/macite/doubtfire-deploy/commit/b6887e85b7a06b166519924dd682ef57650a4e32)) +* disable launch scorm test button if user is staff ([9bc48b0](https://github.com/macite/doubtfire-deploy/commit/9bc48b03905c0a839d78ea13be9817ca73ba54cf)) +* ensure counters are incremented after object creation ([2b1dcfc](https://github.com/macite/doubtfire-deploy/commit/2b1dcfc717eb770dd623c62ac99d8d961d1c3124)) +* ensure datamodel is updated on termination ([2ac487f](https://github.com/macite/doubtfire-deploy/commit/2ac487f13c3743f2b5b805521964bdcbca7cdc15)) +* ensure scorm frame loads when src is available ([6ae4295](https://github.com/macite/doubtfire-deploy/commit/6ae42954162996acf145cef211981153381ff49d)) +* hide config options if numbas test is disabled ([0b15b1c](https://github.com/macite/doubtfire-deploy/commit/0b15b1c38b8f09a30f53bb42dccd1971109bce01)) +* indicate task def has scorm data when zip is uploaded ([4b983ba](https://github.com/macite/doubtfire-deploy/commit/4b983bae1f522013f750b8c36d5e1a52624abbbc)) +* initialise SCORM API wrapper before iframe loads ([5d0606c](https://github.com/macite/doubtfire-deploy/commit/5d0606c56eaeb929d5873cae7038ce729581512c)) +* integrate Numbas services well with the existing system ([d47b8ed](https://github.com/macite/doubtfire-deploy/commit/d47b8edc745660801984346c4fa67d6bb371f4cb)) +* marking shortcuts no longer conflict with common browser shortcuts ([ec7524a](https://github.com/macite/doubtfire-deploy/commit/ec7524aaae040794de76521363c2d885a0425db0)) +* remove attempt number field ([f0ff40b](https://github.com/macite/doubtfire-deploy/commit/f0ff40bfd7e86b904adae94bf8cbe5d56371e011)) +* remove saved scorm token and always get ([c8fc702](https://github.com/macite/doubtfire-deploy/commit/c8fc702bde6f1c448fc585042e5265625552bae8)) +* retrieve test attempt data correctly ([2810ce6](https://github.com/macite/doubtfire-deploy/commit/2810ce65d423a137600d621a98bf27dbc221921c)) +* send task id with numbas completed attempt data ([e46214d](https://github.com/macite/doubtfire-deploy/commit/e46214dd59d86014bb04d1c31f3a3bf21240e32b)) +* show correct attempts left and allow tutor to review attempt always ([58c24c3](https://github.com/macite/doubtfire-deploy/commit/58c24c3a6760af168a918bc099e755f475eac5f0)) +* show correct Numbas test from the task def with all assets loaded ([bee0a0b](https://github.com/macite/doubtfire-deploy/commit/bee0a0bb1eb905c964caf7888419effe70553520)) +* show delete and download buttons in editor when Numbas test exists ([821feb9](https://github.com/macite/doubtfire-deploy/commit/821feb93a8a40d095ef4f50adedbfd9216278fa9)) +* show Numbas button component and modify iframe request ([c9c2fbe](https://github.com/macite/doubtfire-deploy/commit/c9c2fbe10db156512946355f3cabfe54461f0eba)) +* show Numbas iframe on top of other elements ([f53befd](https://github.com/macite/doubtfire-deploy/commit/f53befd82b52bba24bc8c593a9266c11f9155d32)) +* show previously configured Numbas attempt limit ([56e1a5d](https://github.com/macite/doubtfire-deploy/commit/56e1a5de44ee275f9544b77909b7472f1020c2c3)) +* update numbas api path ([3df59dc](https://github.com/macite/doubtfire-deploy/commit/3df59dcc58f021ef16d81938d1d05473818bc75e)) +* use modal for Numbas and enable authentication ([64b1bfb](https://github.com/macite/doubtfire-deploy/commit/64b1bfb2918993e58a6659948d9621fc7d0b8ba4)) +* use nullish coalescing when retrieving data from the datamodel ([226d919](https://github.com/macite/doubtfire-deploy/commit/226d9193251fb675c92bec8265508477857a4ec9)) + ### [8.0.28](https://github.com/macite/doubtfire-deploy/compare/v8.0.23...v8.0.28) (2024-09-05) diff --git a/package-lock.json b/package-lock.json index a02d52eb80..7ff345e6cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.28", + "version": "8.0.29", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.28", + "version": "8.0.29", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index eb6a1b46be..db93e90aad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.28", + "version": "8.0.29", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From ce7e69f2aa43b409bbf91eec29273e9cd19cd973 Mon Sep 17 00:00:00 2001 From: ShounakB <65479699+Shounaks@users.noreply.github.com> Date: Wed, 13 Nov 2024 17:29:57 +1100 Subject: [PATCH 159/776] rebase migration:grade-icon onto origin/8.0.x --- src/app/common/common.coffee | 1 - src/app/common/grade-icon/grade-icon.coffee | 16 - .../grade-icon/grade-icon.component.html | 16 + .../grade-icon/grade-icon.component.scss | 20 + .../common/grade-icon/grade-icon.component.ts | 35 ++ src/app/common/grade-icon/grade-icon.scss | 48 --- src/app/common/grade-icon/grade-icon.tpl.html | 8 - src/app/doubtfire-angular.module.ts | 2 + src/app/doubtfire-angularjs.module.ts | 3 +- ...roup-member-contribution-assigner.tpl.html | 82 ++-- .../project-progress-dashboard.tpl.html | 2 +- .../grade-task-modal.tpl.html | 54 +-- .../states/portfolios/portfolios.tpl.html | 6 +- .../students-list/students-list.tpl.html | 364 +++++++++--------- 14 files changed, 329 insertions(+), 328 deletions(-) delete mode 100644 src/app/common/grade-icon/grade-icon.coffee create mode 100644 src/app/common/grade-icon/grade-icon.component.html create mode 100644 src/app/common/grade-icon/grade-icon.component.scss create mode 100644 src/app/common/grade-icon/grade-icon.component.ts delete mode 100644 src/app/common/grade-icon/grade-icon.scss delete mode 100644 src/app/common/grade-icon/grade-icon.tpl.html diff --git a/src/app/common/common.coffee b/src/app/common/common.coffee index e5bbea48a2..f330d8f6ac 100644 --- a/src/app/common/common.coffee +++ b/src/app/common/common.coffee @@ -3,6 +3,5 @@ angular.module("doubtfire.common", [ 'doubtfire.common.filters' 'doubtfire.common.modals' 'doubtfire.common.file-uploader' - 'doubtfire.common.grade-icon' 'doubtfire.common.content-editable' ]) diff --git a/src/app/common/grade-icon/grade-icon.coffee b/src/app/common/grade-icon/grade-icon.coffee deleted file mode 100644 index f35f7fbfa4..0000000000 --- a/src/app/common/grade-icon/grade-icon.coffee +++ /dev/null @@ -1,16 +0,0 @@ -angular.module('doubtfire.common.grade-icon', []) - -.directive 'gradeIcon', -> - restrict: 'E' - replace: true - templateUrl: 'common/grade-icon/grade-icon.tpl.html' - scope: - inputGrade: '=?grade' - colorful: '=?' - controller: ($scope, gradeService) -> - $scope.$watch 'inputGrade', (newGrade) -> - $scope.grade = if _.isString($scope.inputGrade) then gradeService.stringToGrade($scope.inputGrade) else $scope.inputGrade - $scope.gradeText = (grade) -> - if grade? then gradeService.grades[grade] or "Grade" - $scope.gradeLetter = (grade) -> - gradeService.gradeAcronyms[grade] or 'G' diff --git a/src/app/common/grade-icon/grade-icon.component.html b/src/app/common/grade-icon/grade-icon.component.html new file mode 100644 index 0000000000..2cb10af9d8 --- /dev/null +++ b/src/app/common/grade-icon/grade-icon.component.html @@ -0,0 +1,16 @@ +
+ + {{ gradeLetter }} + +
diff --git a/src/app/common/grade-icon/grade-icon.component.scss b/src/app/common/grade-icon/grade-icon.component.scss new file mode 100644 index 0000000000..76ea15e629 --- /dev/null +++ b/src/app/common/grade-icon/grade-icon.component.scss @@ -0,0 +1,20 @@ +.grade-icon { + color: #fff; + font-size: 1em; + background-color: #333333; + border-radius: 100%; + width: 2.25em; + height: 2.25em; + font-weight: 100; + font-size: 1em; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: center; +} +.text-left .grade-icon { + margin-left: 0; +} +.text-right .grade-icon { + margin-right: 0; +} diff --git a/src/app/common/grade-icon/grade-icon.component.ts b/src/app/common/grade-icon/grade-icon.component.ts new file mode 100644 index 0000000000..5c539d5ead --- /dev/null +++ b/src/app/common/grade-icon/grade-icon.component.ts @@ -0,0 +1,35 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Component, Inject, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; +import { GradeService } from '../services/grade.service'; +import { Project } from 'src/app/api/models/project'; + +@Component({ + selector: 'grade-icon', + templateUrl: './grade-icon.component.html', + styleUrls: ['./grade-icon.component.scss'], +}) +export class GradeIconComponent implements OnInit, OnChanges { + @Input() grade?: number; + @Input() colorful: boolean = false; + + InputGrade?: number; + gradeText: string = 'Grade'; + gradeLetter: string = 'G'; + + constructor(private gradeService: GradeService) {} + + ngOnInit(): void { + this.updateGrade(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['grade']) { + this.updateGrade(); + } + } + + private updateGrade(): void { + this.gradeText = this.gradeService.grades[this.grade] || 'Grade'; + this.gradeLetter = this.gradeService.gradeAcronyms[this.grade] || 'G'; + } +} diff --git a/src/app/common/grade-icon/grade-icon.scss b/src/app/common/grade-icon/grade-icon.scss deleted file mode 100644 index 08db661a8b..0000000000 --- a/src/app/common/grade-icon/grade-icon.scss +++ /dev/null @@ -1,48 +0,0 @@ -.grade-icon.text-muted { - background-color: $text-muted; -} -.grade-icon.text-primary { - background-color: $brand-primary; -} -.grade-icon.text-success { - background-color: $brand-success; -} -.grade-icon.text-danger { - background-color: $brand-danger; -} -.grade-icon.text-info { - background-color: $brand-info; -} -.grade-icon.text-warning { - background-color: $brand-warning; -} -a .grade-icon:hover { - background-color: $link-hover-color; -} -.grade-icon { - color: #fff; - font-size: 1em; - background-color: $text-color; - border-radius: 100%; - width: 2.25em; - height: 2.25em; - font-weight: 100; - font-size: 1em; - @include no-select; - margin: 0 auto; - display: flex; - align-items: center; - justify-content: center; -} -.grade-icon.colorful { - &.grade-0 { background-color: $grade-color-p; } - &.grade-1 { background-color: $grade-color-c; } - &.grade-2 { background-color: $grade-color-d; } - &.grade-3 { background-color: $grade-color-hd; } -} -.text-left .grade-icon { - margin-left: 0; -} -.text-right .grade-icon { - margin-right: 0; -} diff --git a/src/app/common/grade-icon/grade-icon.tpl.html b/src/app/common/grade-icon/grade-icon.tpl.html deleted file mode 100644 index 1939b21361..0000000000 --- a/src/app/common/grade-icon/grade-icon.tpl.html +++ /dev/null @@ -1,8 +0,0 @@ -
- - {{gradeLetter(grade)}} - -
diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 991ac397b4..4b06266dc1 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -170,6 +170,7 @@ import {TaskAssessmentModalComponent} from './common/modals/task-assessment-moda import {TaskSubmissionHistoryComponent} from './tasks/task-submission-history/task-submission-history.component'; import {HomeComponent} from './home/states/home/home.component'; import {IsActiveUnitRole} from './common/pipes/is-active-unit-role.pipe'; +import {GradeIconComponent} from './common/grade-icon/grade-icon.component'; import {HeaderComponent} from './common/header/header.component'; import {UnitDropdownComponent} from './common/header/unit-dropdown/unit-dropdown.component'; import {TaskDropdownComponent} from './common/header/task-dropdown/task-dropdown.component'; @@ -316,6 +317,7 @@ const MY_DATE_FORMAT = { TaskAssessmentCommentComponent, TaskAssessmentModalComponent, TaskSubmissionHistoryComponent, + GradeIconComponent, HeaderComponent, UnitDropdownComponent, TaskDropdownComponent, diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index e14fdbb545..1f42ca4907 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -122,7 +122,6 @@ import 'build/src/app/common/modals/confirmation-modal/confirmation-modal.js'; import 'build/src/app/common/modals/comments-modal/comments-modal.js'; import 'build/src/app/common/modals/csv-result-modal/csv-result-modal.js'; import 'build/src/app/common/modals/modals.js'; -import 'build/src/app/common/grade-icon/grade-icon.js'; import 'build/src/app/common/file-uploader/file-uploader.js'; import 'build/src/app/common/common.js'; import 'build/src/app/common/services/listener-service.js'; @@ -193,6 +192,7 @@ import {CheckForUpdateService} from './sessions/service-worker-updater/check-for import {TaskSubmissionService} from './common/services/task-submission.service'; import {TaskAssessmentModalService} from './common/modals/task-assessment-modal/task-assessment-modal.service'; import {TaskSubmissionHistoryComponent} from './tasks/task-submission-history/task-submission-history.component'; +import {GradeIconComponent} from './common/grade-icon/grade-icon.component'; import {HeaderComponent} from './common/header/header.component'; import {SplashScreenComponent} from './home/splash-screen/splash-screen.component'; import {GlobalStateService} from './projects/states/index/global-state.service'; @@ -313,6 +313,7 @@ DoubtfireAngularJSModule.directive( 'objectSelect', downgradeComponent({component: ObjectSelectComponent}), ); +DoubtfireAngularJSModule.directive('gradeIcon', downgradeComponent({component: GradeIconComponent})); DoubtfireAngularJSModule.directive('appHeader', downgradeComponent({component: HeaderComponent})); DoubtfireAngularJSModule.directive( 'splashScreen', diff --git a/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.tpl.html b/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.tpl.html index 4caab308f5..149ebb8a13 100644 --- a/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.tpl.html +++ b/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.tpl.html @@ -1,41 +1,41 @@ -
- - - - - - - - - - - - - - - -
Team MemberTarget GradeContribution
{{contrib.project.student.name}} - - - - - - - {{contrib.percent}} % effort - - - No effort - - -
-
+
+ + + + + + + + + + + + + + + +
Team MemberTarget GradeContribution
{{contrib.project.student.name}} + + + + + + + {{contrib.percent}} % effort + + + No effort + + +
+
diff --git a/src/app/projects/project-progress-dashboard/project-progress-dashboard.tpl.html b/src/app/projects/project-progress-dashboard/project-progress-dashboard.tpl.html index 5dc40dc5b0..7e59cac17b 100644 --- a/src/app/projects/project-progress-dashboard/project-progress-dashboard.tpl.html +++ b/src/app/projects/project-progress-dashboard/project-progress-dashboard.tpl.html @@ -22,7 +22,7 @@

Target Grade

diff --git a/src/app/tasks/modals/grade-task-modal/grade-task-modal.tpl.html b/src/app/tasks/modals/grade-task-modal/grade-task-modal.tpl.html index 0906d7b593..4d02cce31f 100644 --- a/src/app/tasks/modals/grade-task-modal/grade-task-modal.tpl.html +++ b/src/app/tasks/modals/grade-task-modal/grade-task-modal.tpl.html @@ -1,27 +1,27 @@ -
- - - -
+
+ + + +
diff --git a/src/app/units/states/portfolios/portfolios.tpl.html b/src/app/units/states/portfolios/portfolios.tpl.html index 2b8a2277eb..800d4f4cbe 100644 --- a/src/app/units/states/portfolios/portfolios.tpl.html +++ b/src/app/units/states/portfolios/portfolios.tpl.html @@ -65,7 +65,7 @@

Mark Portfolios

btn-radio="{{$index}}" > Mark Portfolios

{{student.tutorNames()}} {{student.shortTutorialDescription()}} - + - + diff --git a/src/app/units/states/students-list/students-list.tpl.html b/src/app/units/states/students-list/students-list.tpl.html index 929cefe386..d81fdb4cd6 100644 --- a/src/app/units/states/students-list/students-list.tpl.html +++ b/src/app/units/states/students-list/students-list.tpl.html @@ -1,182 +1,182 @@ -
-
-
-
- - - - -
-

- -
-
- -
-
- - -
-
-
-
- -
-
- - - -
-
-

- Click the button twice to reverse the sort ordering. -

-
-
-
-
-

No students found

-

- No students were found using the filters specified. -

-
-
- - - - - - - - - - - - - - - - - - - - - - - -
- - Username - - - - Name - - - - Stats - - - - Flags - - - - - Campus - - - - Tutorial - -
- - - {{project.student.username || "N/A"}} - - {{project.student.name}} - - - - {{bar.value !== bar.value ? 'No Interaction' : (bar.value + '%')}} - - - - - - - - - - - - - - - - - - -
- - - +
+
+
+
+ + + + +
+

+ +
+
+ +
+
+ + +
+
+
+
+ +
+
+ + + +
+
+

+ Click the button twice to reverse the sort ordering. +

+
+
+
+
+

No students found

+

+ No students were found using the filters specified. +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + Username + + + + Name + + + + Stats + + + + Flags + + + + + Campus + + + + Tutorial + +
+ + + {{project.student.username || "N/A"}} + + {{project.student.name}} + + + + {{bar.value !== bar.value ? 'No Interaction' : (bar.value + '%')}} + + + + + + + + + + + + + + + + + + +
+ + + From 91b42988188a2e9ebfdfbcca338382da656a7e32 Mon Sep 17 00:00:00 2001 From: ShounakB <65479699+Shounaks@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:21:41 +1100 Subject: [PATCH 160/776] Fixing All components and CSS Cleanup --- .../common/grade-icon/grade-icon.component.html | 14 +++++++------- .../common/grade-icon/grade-icon.component.scss | 14 -------------- .../common/grade-icon/grade-icon.component.ts | 17 +++++++++-------- src/app/doubtfire-angularjs.module.ts | 2 +- .../group-member-contribution-assigner.tpl.html | 2 +- .../group-member-list.tpl.html | 2 +- .../project-progress-dashboard.tpl.html | 2 +- .../portfolio-grade-select-step.tpl.html | 6 +++--- .../grade-task-modal/grade-task-modal.tpl.html | 2 +- .../units/states/portfolios/portfolios.tpl.html | 8 ++++---- .../states/students-list/students-list.tpl.html | 2 +- 11 files changed, 29 insertions(+), 42 deletions(-) diff --git a/src/app/common/grade-icon/grade-icon.component.html b/src/app/common/grade-icon/grade-icon.component.html index 2cb10af9d8..7af15c0ba3 100644 --- a/src/app/common/grade-icon/grade-icon.component.html +++ b/src/app/common/grade-icon/grade-icon.component.html @@ -1,12 +1,12 @@ -
+
{{contrib.project.student.name}} - + No members in group {{member.student.username || "N/A"}} {{member.student.name}} - +

diff --git a/src/app/tasks/modals/grade-task-modal/grade-task-modal.tpl.html b/src/app/tasks/modals/grade-task-modal/grade-task-modal.tpl.html index 4d02cce31f..7fa8e7055e 100644 --- a/src/app/tasks/modals/grade-task-modal/grade-task-modal.tpl.html +++ b/src/app/tasks/modals/grade-task-modal/grade-task-modal.tpl.html @@ -7,7 +7,7 @@

Assess Task Quality

Please provide a grade to change the student's status for task .

diff --git a/src/app/units/states/portfolios/portfolios.tpl.html b/src/app/units/states/portfolios/portfolios.tpl.html index 800d4f4cbe..c67b13e400 100644 --- a/src/app/units/states/portfolios/portfolios.tpl.html +++ b/src/app/units/states/portfolios/portfolios.tpl.html @@ -64,12 +64,12 @@

Mark Portfolios

ng-model="filterOptions.selectedGrade" btn-radio="{{$index}}" > - + > @@ -194,10 +194,10 @@

Mark Portfolios

{{student.tutorNames()}} {{student.shortTutorialDescription()}} - + - + diff --git a/src/app/units/states/students-list/students-list.tpl.html b/src/app/units/states/students-list/students-list.tpl.html index d81fdb4cd6..830d0a4c3f 100644 --- a/src/app/units/states/students-list/students-list.tpl.html +++ b/src/app/units/states/students-list/students-list.tpl.html @@ -130,7 +130,7 @@

No students found

- + From 3da7a4b54a5892ea99cd365ae439677b0f52ed6f Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:40:38 +1100 Subject: [PATCH 161/776] feat: add editor for task feedback templates --- src/app/api/models/doubtfire-model.ts | 2 +- src/app/api/models/feedback-template.ts | 78 +++++++++ src/app/api/models/task-definition.ts | 7 +- .../api/services/feedback-template.service.ts | 28 +++ src/app/doubtfire-angular.module.ts | 4 + .../task-definition-editor.component.html | 21 ++- .../task-definition-feedback.component.html | 127 ++++++++++++++ .../task-definition-feedback.component.scss | 0 .../task-definition-feedback.component.ts | 163 ++++++++++++++++++ 9 files changed, 426 insertions(+), 4 deletions(-) create mode 100644 src/app/api/models/feedback-template.ts create mode 100644 src/app/api/services/feedback-template.service.ts create mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component.html create mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component.scss create mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component.ts diff --git a/src/app/api/models/doubtfire-model.ts b/src/app/api/models/doubtfire-model.ts index fffc361a4e..38b886a2ae 100644 --- a/src/app/api/models/doubtfire-model.ts +++ b/src/app/api/models/doubtfire-model.ts @@ -37,6 +37,7 @@ export * from './scorm-player-context'; export * from './test-attempt'; export * from './task-comment/scorm-comment'; export * from './task-comment/scorm-extension-comment'; +export * from './feedback-template'; // Users -- are students or staff export * from './user/user'; @@ -44,7 +45,6 @@ export * from './user/user'; // WebCal -- calendars used to track task due dates export * from './webcal/webcal'; - export * from '../services/authentication.service'; export * from '../services/unit.service'; export * from '../services/project.service'; diff --git a/src/app/api/models/feedback-template.ts b/src/app/api/models/feedback-template.ts new file mode 100644 index 0000000000..85da6af509 --- /dev/null +++ b/src/app/api/models/feedback-template.ts @@ -0,0 +1,78 @@ +import {Entity, EntityMapping} from 'ngx-entity-service'; +import {Observable} from 'rxjs'; +import {AppInjector} from 'src/app/app-injector'; +import {TaskDefinition} from './task-definition'; +import {FeedbackTemplateService} from '../services/feedback-template.service'; + +export class FeedbackTemplate extends Entity { + id: number; + learningOutcome: string; + chipText: string; + description: string; + commentText: string; + summaryText: string; + + readonly taskDefinition: TaskDefinition; + + constructor(taskDef: TaskDefinition) { + super(); + this.taskDefinition = taskDef; + } + + public toJson(mappingData: EntityMapping, ignoreKeys?: string[]): object { + return { + feedback_template: super.toJson(mappingData, ignoreKeys), + }; + } + + public save(): Observable { + const svc = AppInjector.get(FeedbackTemplateService); + + if (this.isNew) { + return svc.create( + { + taskDefId: this.taskDefinition.id, + }, + { + entity: this, + cache: this.taskDefinition.feedbackTemplateCache, + constructorParams: this.taskDefinition, + }, + ); + } else { + return svc.update( + { + taskDefId: this.taskDefinition.id, + id: this.id, + }, + {entity: this}, + ); + } + } + + private originalSaveData: string; + + public get hasOriginalSaveData(): boolean { + return this.originalSaveData !== undefined && this.originalSaveData !== null; + } + + public setOriginalSaveData(mapping: EntityMapping) { + this.originalSaveData = JSON.stringify(this.toJson(mapping)); + } + + public hasChanges(mapping: EntityMapping): boolean { + if (!this.originalSaveData) { + return false; + } + + return this.originalSaveData != JSON.stringify(this.toJson(mapping)); + } + + public get isNew(): boolean { + return !this.id; + } + + public get taskDefId(): number { + return this.taskDefinition.id; + } +} diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index b848686768..2e17ac6cda 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -1,9 +1,9 @@ import { HttpClient } from '@angular/common/http'; -import { Entity, EntityMapping } from 'ngx-entity-service'; +import { Entity, EntityCache, 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 { FeedbackTemplate, 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 }; @@ -44,6 +44,9 @@ export class TaskDefinition extends Entity { assessmentEnabled: boolean; mossLanguage: string = 'moss c'; + public readonly feedbackTemplateCache: EntityCache = + new EntityCache(); + readonly unit: Unit; constructor(unit: Unit) { diff --git a/src/app/api/services/feedback-template.service.ts b/src/app/api/services/feedback-template.service.ts new file mode 100644 index 0000000000..4d0bef50de --- /dev/null +++ b/src/app/api/services/feedback-template.service.ts @@ -0,0 +1,28 @@ +import {Injectable} from '@angular/core'; +import {CachedEntityService} from 'ngx-entity-service'; +import {FeedbackTemplate} from '../models/feedback-template'; +import {HttpClient} from '@angular/common/http'; +import API_URL from 'src/app/config/constants/apiURL'; +import {TaskDefinition} from '../models/task-definition'; + +@Injectable() +export class FeedbackTemplateService extends CachedEntityService { + protected readonly endpointFormat = 'feedback_templates/'; + + constructor(httpClient: HttpClient) { + super(httpClient, API_URL); + + this.mapping.addKeys( + 'id', + 'learningOutcome', + 'chipText', + 'description', + 'commentText', + 'summaryText', + ); + } + + public override createInstanceFrom(json: object, other?: any): FeedbackTemplate { + return new FeedbackTemplate(other as TaskDefinition); + } +} diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 991ac397b4..fe50c5a40b 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -207,6 +207,7 @@ import {TaskDefinitionDatesComponent} from './units/states/edit/directives/unit- import {TaskDefinitionUploadComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component'; import {TaskDefinitionOptionsComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-options/task-definition-options.component'; import {TaskDefinitionResourcesComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-resources/task-definition-resources.component'; +import {TaskDefinitionFeedbackComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component'; import {TaskDefinitionOverseerComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component'; import {TaskDefinitionScormComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component'; import {UnitAnalyticsComponent} from './units/states/analytics/unit-analytics-route.component'; @@ -237,6 +238,7 @@ import {TaskScormCardComponent} from './projects/states/dashboard/directives/tas import {TestAttemptService} from './api/services/test-attempt.service'; import {ScormExtensionCommentComponent} from './tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component'; import {ScormExtensionModalComponent} from './common/modals/scorm-extension-modal/scorm-extension-modal.component'; +import {FeedbackTemplateService} from './api/services/feedback-template.service'; // 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 = { @@ -287,6 +289,7 @@ const MY_DATE_FORMAT = { TaskDefinitionUploadComponent, TaskDefinitionOptionsComponent, TaskDefinitionResourcesComponent, + TaskDefinitionFeedbackComponent, TaskDefinitionOverseerComponent, TaskDefinitionScormComponent, UnitAnalyticsComponent, @@ -435,6 +438,7 @@ const MY_DATE_FORMAT = { provideLottieOptions({ player: () => player, }), + FeedbackTemplateService, ], imports: [ FlexLayoutModule, 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..e33ba9297f 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 @@ -133,6 +133,25 @@

7 +
+

Task feedback templates

+

+ Upload feedback templates for tutors +

+
+ +
+
+ + +
+
+
+ 8 +
+

SCORM test

@@ -149,7 +168,7 @@

SCORM test

- 8 + 9
diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component.html new file mode 100644 index 0000000000..8cdecc7f46 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component.html @@ -0,0 +1,127 @@ +
+
+ Enable feedback templates + +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Learning Outcome + {{ feedbackTemplate.learningOutcome }} + Chip Text + {{ feedbackTemplate.chipText }} + Description + {{ feedbackTemplate.description }} + Comment Text + {{ feedbackTemplate.commentText }} + Summary Text + {{ feedbackTemplate.summaryText }} + + @if (feedbackTemplateHasChanges(feedbackTemplate)) { + + } + +
+ + + + + + + + + +
diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component.scss b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.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-feedback/task-definition-feedback.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component.ts new file mode 100644 index 0000000000..7036148564 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component.ts @@ -0,0 +1,163 @@ +import {AfterViewInit, Component, Inject, Input, ViewChild} from '@angular/core'; +import {MatTable, MatTableDataSource} from '@angular/material/table'; +import {MatPaginator} from '@angular/material/paginator'; +import {TaskDefinition, FeedbackTemplate, Unit} from 'src/app/api/models/doubtfire-model'; +import {TaskDefinitionService} from 'src/app/api/services/task-definition.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {MatSort, Sort} from '@angular/material/sort'; +import { + confirmationModal, + csvResultModalService, + csvUploadModalService, +} from 'src/app/ajs-upgraded-providers'; +import {Subscription} from 'rxjs'; +import {FeedbackTemplateService} from 'src/app/api/services/feedback-template.service'; + +@Component({ + selector: 'f-task-definition-feedback', + templateUrl: 'task-definition-feedback.component.html', + styleUrls: ['task-definition-feedback.component.scss'], +}) +export class TaskDefinitionFeedbackComponent implements AfterViewInit { + @ViewChild(MatTable, {static: false}) table: MatTable; + @ViewChild(MatSort, {static: false}) sort: MatSort; + @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; + + @Input() taskDefinition: TaskDefinition; + + public feedbackTemplateSource: MatTableDataSource; + public columns: string[] = [ + 'learningOutcome', + 'chipText', + 'description', + 'commentText', + 'summaryText', + 'feedbackTemplateAction', + ]; + public filter: string; + public selectedFeedbackTemplate: FeedbackTemplate; + + constructor( + private alerts: AlertService, + private taskDefinitionService: TaskDefinitionService, + private feedbackTemplateService: FeedbackTemplateService, + @Inject(csvResultModalService) private csvResultModalService: any, + @Inject(csvUploadModalService) private csvUploadModal: any, + @Inject(confirmationModal) private confirmationModal: any, + ) {} + + public get unit(): Unit { + return this.taskDefinition?.unit; + } + + ngAfterViewInit(): void { + this.subscriptions.push( + this.taskDefinition.feedbackTemplateCache.values.subscribe((feedbackTemplates) => { + this.feedbackTemplateSource = new MatTableDataSource(feedbackTemplates); + this.feedbackTemplateSource.paginator = this.paginator; + this.feedbackTemplateSource.sort = this.sort; + this.feedbackTemplateSource.filterPredicate = (data: any, filter: string) => + data.matches(filter); + }), + ); + } + + public saveFeedbackTemplate(feedbackTemplate: FeedbackTemplate) { + feedbackTemplate.save().subscribe(() => { + this.alerts.success('Template saved'); + feedbackTemplate.setOriginalSaveData(this.feedbackTemplateService.mapping); + }); + } + + private subscriptions: Subscription[] = []; + ngOnDestroy(): void { + this.subscriptions.forEach((s) => s.unsubscribe()); + } + + public selectFeedbackTemplate(feedbackTemplate: FeedbackTemplate) { + if (this.selectedFeedbackTemplate === feedbackTemplate) { + this.selectedFeedbackTemplate = null; + } else { + this.selectedFeedbackTemplate = feedbackTemplate; + + if (!this.selectedFeedbackTemplate.hasOriginalSaveData) { + this.selectedFeedbackTemplate.setOriginalSaveData(this.feedbackTemplateService.mapping); + } + } + } + + public sortData(sort: Sort) { + const data = this.feedbackTemplateSource.data; + + if (!sort.active || sort.direction === '') { + this.feedbackTemplateSource.data = data; + return; + } + + this.feedbackTemplateSource.data = data.sort((a, b) => { + const isAsc = sort.direction === 'asc'; + switch (sort.active) { + case 'learningOutcome': + return this.compare(a.learningOutcome, b.learningOutcome, isAsc); + case 'chipText': + return this.compare(a.chipText, b.chipText, isAsc); + case 'description': + return this.compare(a.description, b.description, isAsc); + case 'commentText': + return this.compare(a.commentText, b.commentText, isAsc); + case 'summaryText': + return this.compare(a.summaryText, b.summaryText, isAsc); + default: + return 0; + } + }); + } + + public compare(a: number | string, b: number | string, isAsc: boolean): number { + return (a < b ? -1 : 1) * (isAsc ? 1 : -1); + } + + applyFilter(filterValue: string) { + this.feedbackTemplateSource.filter = filterValue.trim().toLowerCase(); + + if (this.feedbackTemplateSource.paginator) { + this.feedbackTemplateSource.paginator.firstPage(); + } + } + + public feedbackTemplateHasChanges(feedbackTemplate: FeedbackTemplate): boolean { + return feedbackTemplate.hasChanges(this.feedbackTemplateService.mapping); + } + + public deleteFeedbackTemplate(feedbackTemplate: FeedbackTemplate) { + this.confirmationModal.show( + 'Delete feedback template', + 'Are you sure you want to delete this template? This action is final.', + () => { + this.alerts.success('Task deleted'); + }, + ); + } + + public uploadFeedbackTemplatesCsv() { + this.csvUploadModal.show( + 'Upload Feedback Templates as CSV', + 'Test message', + {file: {name: 'Feedback Template CSV Data', type: 'csv'}}, + this.unit.getTaskDefinitionBatchUploadUrl(), + (response: any) => {}, + ); + } + + public createFeedbackTemplate() { + const feedbackTemplate = new FeedbackTemplate(this.taskDefinition); + + feedbackTemplate.learningOutcome = 'TLO'; + feedbackTemplate.chipText = 'lorem'; + feedbackTemplate.description = 'Lorem ipsum dolor'; + feedbackTemplate.commentText = 'Lorem dolor'; + feedbackTemplate.summaryText = 'Lorem ipsum'; + + this.selectedFeedbackTemplate = feedbackTemplate; + } +} From c12536bc4375b1ae5f19d4e54d0783b6948921eb Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Wed, 20 Nov 2024 22:19:56 +1100 Subject: [PATCH 162/776] feat: combine outcome and template editor --- .../feedback-template-editor.component.html | 258 +++++++++++++++++ .../feedback-template-editor.component.ts | 270 ++++++++++++++++++ src/app/doubtfire-angular.module.ts | 4 +- .../task-definition-editor.component.html | 44 ++- .../task-definition-feedback.component.html | 127 -------- .../task-definition-feedback.component.scss | 0 .../task-definition-feedback.component.ts | 163 ----------- 7 files changed, 551 insertions(+), 315 deletions(-) create mode 100644 src/app/common/feedback-template/feedback-template-editor.component.html create mode 100644 src/app/common/feedback-template/feedback-template-editor.component.ts delete mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component.html delete mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component.scss delete mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component.ts diff --git a/src/app/common/feedback-template/feedback-template-editor.component.html b/src/app/common/feedback-template/feedback-template-editor.component.html new file mode 100644 index 0000000000..6922dab4ed --- /dev/null +++ b/src/app/common/feedback-template/feedback-template-editor.component.html @@ -0,0 +1,258 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Number + {{ learningOutcome.iloNumber }} + Abbreviation + {{ learningOutcome.abbreviation }} + Name + {{ learningOutcome.name }} + Description + {{ learningOutcome.description }} + + @if (learningOutcomeHasChanges(learningOutcome)) { + + } + +
+ +
+ + + +
+ + + + + + + +
+ @if (selectedOutcome) { +
+ + Outcome Number + + + + + Abbreviation + + + + + Name + + +
+ + + Description + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Learning Outcome + {{ feedbackTemplate.learningOutcome }} + Chip Text + {{ feedbackTemplate.chipText }} + Description + {{ feedbackTemplate.description }} + Comment Text + {{ feedbackTemplate.commentText }} + Summary Text + {{ feedbackTemplate.summaryText }} + + @if (feedbackTemplateHasChanges(feedbackTemplate)) { + + } + +
+ +
+ + + +
+ + + + + + + +
+
+ } +
diff --git a/src/app/common/feedback-template/feedback-template-editor.component.ts b/src/app/common/feedback-template/feedback-template-editor.component.ts new file mode 100644 index 0000000000..b5a94b3e91 --- /dev/null +++ b/src/app/common/feedback-template/feedback-template-editor.component.ts @@ -0,0 +1,270 @@ +import {AfterViewInit, Component, Inject, Input, ViewChild} from '@angular/core'; +import {MatTable, MatTableDataSource} from '@angular/material/table'; +import {MatPaginator} from '@angular/material/paginator'; +import {TaskDefinition, Unit, LearningOutcome, LearningOutcomeService, FeedbackTemplate} from 'src/app/api/models/doubtfire-model'; +import {TaskDefinitionService} from 'src/app/api/services/task-definition.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {MatSort, Sort} from '@angular/material/sort'; +import { + confirmationModal, + csvResultModalService, + csvUploadModalService, +} from 'src/app/ajs-upgraded-providers'; +import {Subscription} from 'rxjs'; +import {FeedbackTemplateService} from 'src/app/api/services/feedback-template.service'; + +@Component({ + selector: 'f-feedback-template-editor', + templateUrl: 'feedback-template-editor.component.html', +}) +export class FeedbackTemplateEditorComponent implements AfterViewInit { + @Input() taskDefinition: TaskDefinition; + + @ViewChild(MatTable, {static: false}) outcomeTable: MatTable; + @ViewChild(MatSort, {static: false}) outcomeSort: MatSort; + @ViewChild(MatPaginator, {static: false}) outcomePaginator: MatPaginator; + + public outcomeSource: MatTableDataSource; + public outcomeColumns: string[] = [ + 'number', + 'abbreviation', + 'name', + 'description', + 'learningOutcomeAction', + ]; + public outcomeFilter: string; + public selectedOutcome: LearningOutcome; + + @ViewChild(MatTable, {static: false}) templateTable: MatTable; + @ViewChild(MatSort, {static: false}) templateSort: MatSort; + @ViewChild(MatPaginator, {static: false}) templatePaginator: MatPaginator; + + public templateSource: MatTableDataSource; + public templateColumns: string[] = [ + 'learningOutcome', + 'chipText', + 'description', + 'commentText', + 'summaryText', + 'feedbackTemplateAction', + ]; + public templateFilter: string; + public selectedTemplate: FeedbackTemplate; + + constructor( + private alerts: AlertService, + private taskDefinitionService: TaskDefinitionService, + private learningOutcomeService: LearningOutcomeService, + private feedbackTemplateService: FeedbackTemplateService, + @Inject(csvResultModalService) private csvResultModalService: any, + @Inject(csvUploadModalService) private csvUploadModal: any, + @Inject(confirmationModal) private confirmationModal: any, + ) {} + + public get unit(): Unit { + return this.taskDefinition?.unit; + } + + ngAfterViewInit(): void { + this.subscriptions.push( + this.unit.learningOutcomesCache.values.subscribe((learningOutcomes) => { + this.outcomeSource = new MatTableDataSource(learningOutcomes); + this.outcomeSource.paginator = this.outcomePaginator; + this.outcomeSource.sort = this.outcomeSort; + this.outcomeSource.filterPredicate = (data: any, filter: string) => data.matches(filter); + }), + this.taskDefinition.feedbackTemplateCache.values.subscribe((feedbackTemplates) => { + this.templateSource = new MatTableDataSource(feedbackTemplates); + this.templateSource.paginator = this.templatePaginator; + this.templateSource.sort = this.templateSort; + this.templateSource.filterPredicate = (data: any, filter: string) => data.matches(filter); + }), + ); + } + + public saveLearningOutcome(learningOutcome: LearningOutcome) { + // learningOutcome.save().subscribe(() => { + // this.alerts.success('Outcome saved'); + // learningOutcome.setOriginalSaveData(this.learningOutcomeService.mapping); + // }); + } + + public saveFeedbackTemplate(feedbackTemplate: FeedbackTemplate) { + feedbackTemplate.save().subscribe(() => { + this.alerts.success('Template saved'); + feedbackTemplate.setOriginalSaveData(this.feedbackTemplateService.mapping); + }); + } + + private subscriptions: Subscription[] = []; + ngOnDestroy(): void { + this.subscriptions.forEach((s) => s.unsubscribe()); + } + + public selectLearningOutcome(learningOutcome: LearningOutcome) { + if (this.selectedOutcome === learningOutcome) { + this.selectedOutcome = null; + } else { + this.selectedOutcome = learningOutcome; + + // if (!this.selectedFeedbackTemplate.hasOriginalSaveData) { + // this.selectedFeedbackTemplate.setOriginalSaveData(this.feedbackTemplateService.mapping); + // } + } + } + + public selectFeedbackTemplate(feedbackTemplate: FeedbackTemplate) { + if (this.selectedTemplate === feedbackTemplate) { + this.selectedTemplate = null; + } else { + this.selectedTemplate = feedbackTemplate; + + if (!this.selectedTemplate.hasOriginalSaveData) { + this.selectedTemplate.setOriginalSaveData(this.feedbackTemplateService.mapping); + } + } + } + + public sortOutcomeData(sort: Sort) { + const data = this.outcomeSource.data; + + if (!sort.active || sort.direction === '') { + this.outcomeSource.data = data; + return; + } + + this.outcomeSource.data = data.sort((a, b) => { + const isAsc = sort.direction === 'asc'; + switch (sort.active) { + case 'number': + return this.compare(a.iloNumber, b.iloNumber, isAsc); + case 'abbreviation': + return this.compare(a.abbreviation, b.abbreviation, isAsc); + case 'name': + return this.compare(a.name, b.name, isAsc); + case 'description': + return this.compare(a.description, b.description, isAsc); + default: + return 0; + } + }); + } + + public sortTemplateData(sort: Sort) { + const data = this.templateSource.data; + + if (!sort.active || sort.direction === '') { + this.templateSource.data = data; + return; + } + + this.templateSource.data = data.sort((a, b) => { + const isAsc = sort.direction === 'asc'; + switch (sort.active) { + case 'learningOutcome': + return this.compare(a.learningOutcome, b.learningOutcome, isAsc); + case 'chipText': + return this.compare(a.chipText, b.chipText, isAsc); + case 'description': + return this.compare(a.description, b.description, isAsc); + case 'commentText': + return this.compare(a.commentText, b.commentText, isAsc); + case 'summaryText': + return this.compare(a.summaryText, b.summaryText, isAsc); + default: + return 0; + } + }); + } + + public compare(a: number | string, b: number | string, isAsc: boolean): number { + return (a < b ? -1 : 1) * (isAsc ? 1 : -1); + } + + applyOutcomeFilter(filterValue: string) { + this.outcomeSource.filter = filterValue.trim().toLowerCase(); + + if (this.outcomeSource.paginator) { + this.outcomeSource.paginator.firstPage(); + } + } + + applyTemplateFilter(filterValue: string) { + this.templateSource.filter = filterValue.trim().toLowerCase(); + + if (this.templateSource.paginator) { + this.templateSource.paginator.firstPage(); + } + } + + public learningOutcomeHasChanges(learningOutcome: LearningOutcome): boolean { + return learningOutcome.hasChanges(this.learningOutcomeService.mapping); + } + + public feedbackTemplateHasChanges(feedbackTemplate: FeedbackTemplate): boolean { + return feedbackTemplate.hasChanges(this.feedbackTemplateService.mapping); + } + + public deleteLearningOutcome(learningOutcome: LearningOutcome) { + this.confirmationModal.show( + 'Delete learning outcome', + 'Are you sure you want to delete this outcome? This action is final.', + () => { + this.alerts.success('Outcome deleted'); + }, + ); + } + + public deleteFeedbackTemplate(feedbackTemplate: FeedbackTemplate) { + this.confirmationModal.show( + 'Delete feedback template', + 'Are you sure you want to delete this template? This action is final.', + () => { + this.alerts.success('Template deleted'); + }, + ); + } + + public uploadLearningOutcomesCsv() { + this.csvUploadModal.show( + 'Upload Learning Outcomes as CSV', + 'Test message', + {file: {name: 'Learning Outcome CSV Data', type: 'csv'}}, + this.unit.getTaskDefinitionBatchUploadUrl(), + (response: any) => {}, + ); + } + + public uploadFeedbackTemplatesCsv() { + this.csvUploadModal.show( + 'Upload Feedback Templates as CSV', + 'Test message', + {file: {name: 'Feedback Template CSV Data', type: 'csv'}}, + this.unit.getTaskDefinitionBatchUploadUrl(), + (response: any) => {}, + ); + } + + public createLearningOutcome() { + const learningOutcome = new LearningOutcome(); + + learningOutcome.iloNumber = 1; + learningOutcome.abbreviation = 'lm'; + learningOutcome.name = 'lorem'; + learningOutcome.description = 'Lorem ipsum dolor'; + + this.selectedOutcome = learningOutcome; + } + + public createFeedbackTemplate() { + const feedbackTemplate = new FeedbackTemplate(this.taskDefinition); + + feedbackTemplate.learningOutcome = 'TLO'; + feedbackTemplate.chipText = 'lorem'; + feedbackTemplate.description = 'Lorem ipsum dolor'; + feedbackTemplate.commentText = 'Lorem dolor'; + feedbackTemplate.summaryText = 'Lorem ipsum'; + + this.selectedTemplate = feedbackTemplate; + } +} diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index fe50c5a40b..07961095af 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -207,7 +207,6 @@ import {TaskDefinitionDatesComponent} from './units/states/edit/directives/unit- import {TaskDefinitionUploadComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component'; import {TaskDefinitionOptionsComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-options/task-definition-options.component'; import {TaskDefinitionResourcesComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-resources/task-definition-resources.component'; -import {TaskDefinitionFeedbackComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component'; import {TaskDefinitionOverseerComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component'; import {TaskDefinitionScormComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-scorm/task-definition-scorm.component'; import {UnitAnalyticsComponent} from './units/states/analytics/unit-analytics-route.component'; @@ -239,6 +238,7 @@ import {TestAttemptService} from './api/services/test-attempt.service'; import {ScormExtensionCommentComponent} from './tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component'; import {ScormExtensionModalComponent} from './common/modals/scorm-extension-modal/scorm-extension-modal.component'; import {FeedbackTemplateService} from './api/services/feedback-template.service'; +import {FeedbackTemplateEditorComponent} from './common/feedback-template/feedback-template-editor.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 = { @@ -289,7 +289,6 @@ const MY_DATE_FORMAT = { TaskDefinitionUploadComponent, TaskDefinitionOptionsComponent, TaskDefinitionResourcesComponent, - TaskDefinitionFeedbackComponent, TaskDefinitionOverseerComponent, TaskDefinitionScormComponent, UnitAnalyticsComponent, @@ -360,6 +359,7 @@ const MY_DATE_FORMAT = { TaskScormCardComponent, ScormExtensionCommentComponent, ScormExtensionModalComponent, + FeedbackTemplateEditorComponent, ], // Services we provide providers: [ 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 e33ba9297f..c56b29c055 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 @@ -30,6 +30,23 @@

Task details

+
+

Task Learning Outcomes

+

Add learning outcomes for this task

+
+ +
+
+ + +
+
+
+ 3 +
+

Inbox

@@ -47,7 +64,7 @@

Inbox

- 3 + 4
@@ -65,7 +82,7 @@

Due dates

- 4 + 5
@@ -83,7 +100,7 @@

Upload requirem
- 5 + 6
@@ -105,7 +122,7 @@

- 6 + 7
@@ -125,25 +142,6 @@

-
-
-
- 7 -
-
-
-

Task feedback templates

-

- Upload feedback templates for tutors -

-
- -
-
-
-
-
- Enable feedback templates - -
- - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Learning Outcome - {{ feedbackTemplate.learningOutcome }} - Chip Text - {{ feedbackTemplate.chipText }} - Description - {{ feedbackTemplate.description }} - Comment Text - {{ feedbackTemplate.commentText }} - Summary Text - {{ feedbackTemplate.summaryText }} - - @if (feedbackTemplateHasChanges(feedbackTemplate)) { - - } - -
- - - - - - - - - -
diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component.scss b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component.ts deleted file mode 100644 index 7036148564..0000000000 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-feedback/task-definition-feedback.component.ts +++ /dev/null @@ -1,163 +0,0 @@ -import {AfterViewInit, Component, Inject, Input, ViewChild} from '@angular/core'; -import {MatTable, MatTableDataSource} from '@angular/material/table'; -import {MatPaginator} from '@angular/material/paginator'; -import {TaskDefinition, FeedbackTemplate, Unit} from 'src/app/api/models/doubtfire-model'; -import {TaskDefinitionService} from 'src/app/api/services/task-definition.service'; -import {AlertService} from 'src/app/common/services/alert.service'; -import {MatSort, Sort} from '@angular/material/sort'; -import { - confirmationModal, - csvResultModalService, - csvUploadModalService, -} from 'src/app/ajs-upgraded-providers'; -import {Subscription} from 'rxjs'; -import {FeedbackTemplateService} from 'src/app/api/services/feedback-template.service'; - -@Component({ - selector: 'f-task-definition-feedback', - templateUrl: 'task-definition-feedback.component.html', - styleUrls: ['task-definition-feedback.component.scss'], -}) -export class TaskDefinitionFeedbackComponent implements AfterViewInit { - @ViewChild(MatTable, {static: false}) table: MatTable; - @ViewChild(MatSort, {static: false}) sort: MatSort; - @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; - - @Input() taskDefinition: TaskDefinition; - - public feedbackTemplateSource: MatTableDataSource; - public columns: string[] = [ - 'learningOutcome', - 'chipText', - 'description', - 'commentText', - 'summaryText', - 'feedbackTemplateAction', - ]; - public filter: string; - public selectedFeedbackTemplate: FeedbackTemplate; - - constructor( - private alerts: AlertService, - private taskDefinitionService: TaskDefinitionService, - private feedbackTemplateService: FeedbackTemplateService, - @Inject(csvResultModalService) private csvResultModalService: any, - @Inject(csvUploadModalService) private csvUploadModal: any, - @Inject(confirmationModal) private confirmationModal: any, - ) {} - - public get unit(): Unit { - return this.taskDefinition?.unit; - } - - ngAfterViewInit(): void { - this.subscriptions.push( - this.taskDefinition.feedbackTemplateCache.values.subscribe((feedbackTemplates) => { - this.feedbackTemplateSource = new MatTableDataSource(feedbackTemplates); - this.feedbackTemplateSource.paginator = this.paginator; - this.feedbackTemplateSource.sort = this.sort; - this.feedbackTemplateSource.filterPredicate = (data: any, filter: string) => - data.matches(filter); - }), - ); - } - - public saveFeedbackTemplate(feedbackTemplate: FeedbackTemplate) { - feedbackTemplate.save().subscribe(() => { - this.alerts.success('Template saved'); - feedbackTemplate.setOriginalSaveData(this.feedbackTemplateService.mapping); - }); - } - - private subscriptions: Subscription[] = []; - ngOnDestroy(): void { - this.subscriptions.forEach((s) => s.unsubscribe()); - } - - public selectFeedbackTemplate(feedbackTemplate: FeedbackTemplate) { - if (this.selectedFeedbackTemplate === feedbackTemplate) { - this.selectedFeedbackTemplate = null; - } else { - this.selectedFeedbackTemplate = feedbackTemplate; - - if (!this.selectedFeedbackTemplate.hasOriginalSaveData) { - this.selectedFeedbackTemplate.setOriginalSaveData(this.feedbackTemplateService.mapping); - } - } - } - - public sortData(sort: Sort) { - const data = this.feedbackTemplateSource.data; - - if (!sort.active || sort.direction === '') { - this.feedbackTemplateSource.data = data; - return; - } - - this.feedbackTemplateSource.data = data.sort((a, b) => { - const isAsc = sort.direction === 'asc'; - switch (sort.active) { - case 'learningOutcome': - return this.compare(a.learningOutcome, b.learningOutcome, isAsc); - case 'chipText': - return this.compare(a.chipText, b.chipText, isAsc); - case 'description': - return this.compare(a.description, b.description, isAsc); - case 'commentText': - return this.compare(a.commentText, b.commentText, isAsc); - case 'summaryText': - return this.compare(a.summaryText, b.summaryText, isAsc); - default: - return 0; - } - }); - } - - public compare(a: number | string, b: number | string, isAsc: boolean): number { - return (a < b ? -1 : 1) * (isAsc ? 1 : -1); - } - - applyFilter(filterValue: string) { - this.feedbackTemplateSource.filter = filterValue.trim().toLowerCase(); - - if (this.feedbackTemplateSource.paginator) { - this.feedbackTemplateSource.paginator.firstPage(); - } - } - - public feedbackTemplateHasChanges(feedbackTemplate: FeedbackTemplate): boolean { - return feedbackTemplate.hasChanges(this.feedbackTemplateService.mapping); - } - - public deleteFeedbackTemplate(feedbackTemplate: FeedbackTemplate) { - this.confirmationModal.show( - 'Delete feedback template', - 'Are you sure you want to delete this template? This action is final.', - () => { - this.alerts.success('Task deleted'); - }, - ); - } - - public uploadFeedbackTemplatesCsv() { - this.csvUploadModal.show( - 'Upload Feedback Templates as CSV', - 'Test message', - {file: {name: 'Feedback Template CSV Data', type: 'csv'}}, - this.unit.getTaskDefinitionBatchUploadUrl(), - (response: any) => {}, - ); - } - - public createFeedbackTemplate() { - const feedbackTemplate = new FeedbackTemplate(this.taskDefinition); - - feedbackTemplate.learningOutcome = 'TLO'; - feedbackTemplate.chipText = 'lorem'; - feedbackTemplate.description = 'Lorem ipsum dolor'; - feedbackTemplate.commentText = 'Lorem dolor'; - feedbackTemplate.summaryText = 'Lorem ipsum'; - - this.selectedFeedbackTemplate = feedbackTemplate; - } -} From db2d2bc32b19165bac8ff6ba0282a16adffbb815 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Fri, 22 Nov 2024 19:04:02 +1100 Subject: [PATCH 163/776] feat: add field to connect outcomes --- .../feedback-template-editor.component.html | 48 +++++++++-- .../feedback-template-editor.component.ts | 81 +++++++++++++++++-- 2 files changed, 114 insertions(+), 15 deletions(-) diff --git a/src/app/common/feedback-template/feedback-template-editor.component.html b/src/app/common/feedback-template/feedback-template-editor.component.html index 6922dab4ed..51af89539a 100644 --- a/src/app/common/feedback-template/feedback-template-editor.component.html +++ b/src/app/common/feedback-template/feedback-template-editor.component.html @@ -16,8 +16,8 @@ {{ learningOutcome.iloNumber }} - - Abbreviation + + Tag + + Connected Learning Outcomes + + @for (outcome of connectedOutcomes(); track $index) { + + {{ outcome }} + + + } + + + + @for (outcome of filteredOutcomes(); track outcome) { + {{ outcome }} + } + + +
- - + + @@ -241,7 +270,10 @@ /> - + - + } diff --git a/src/app/common/feedback-template/feedback-template-editor.component.ts b/src/app/common/feedback-template/feedback-template-editor.component.ts index b5a94b3e91..fa8a82df27 100644 --- a/src/app/common/feedback-template/feedback-template-editor.component.ts +++ b/src/app/common/feedback-template/feedback-template-editor.component.ts @@ -1,7 +1,23 @@ -import {AfterViewInit, Component, Inject, Input, ViewChild} from '@angular/core'; +import { + AfterViewInit, + Component, + computed, + inject, + Inject, + Input, + model, + signal, + ViewChild, +} from '@angular/core'; import {MatTable, MatTableDataSource} from '@angular/material/table'; import {MatPaginator} from '@angular/material/paginator'; -import {TaskDefinition, Unit, LearningOutcome, LearningOutcomeService, FeedbackTemplate} from 'src/app/api/models/doubtfire-model'; +import { + TaskDefinition, + Unit, + LearningOutcome, + LearningOutcomeService, + FeedbackTemplate, +} from 'src/app/api/models/doubtfire-model'; import {TaskDefinitionService} from 'src/app/api/services/task-definition.service'; import {AlertService} from 'src/app/common/services/alert.service'; import {MatSort, Sort} from '@angular/material/sort'; @@ -12,6 +28,10 @@ import { } from 'src/app/ajs-upgraded-providers'; import {Subscription} from 'rxjs'; import {FeedbackTemplateService} from 'src/app/api/services/feedback-template.service'; +import {COMMA, ENTER} from '@angular/cdk/keycodes'; +import {LiveAnnouncer} from '@angular/cdk/a11y'; +import {MatChipInputEvent} from '@angular/material/chips'; +import {MatAutocompleteSelectedEvent} from '@angular/material/autocomplete'; @Component({ selector: 'f-feedback-template-editor', @@ -27,7 +47,7 @@ export class FeedbackTemplateEditorComponent implements AfterViewInit { public outcomeSource: MatTableDataSource; public outcomeColumns: string[] = [ 'number', - 'abbreviation', + 'tag', 'name', 'description', 'learningOutcomeAction', @@ -41,7 +61,7 @@ export class FeedbackTemplateEditorComponent implements AfterViewInit { public templateSource: MatTableDataSource; public templateColumns: string[] = [ - 'learningOutcome', + 'number', 'chipText', 'description', 'commentText', @@ -138,7 +158,7 @@ export class FeedbackTemplateEditorComponent implements AfterViewInit { switch (sort.active) { case 'number': return this.compare(a.iloNumber, b.iloNumber, isAsc); - case 'abbreviation': + case 'tag': return this.compare(a.abbreviation, b.abbreviation, isAsc); case 'name': return this.compare(a.name, b.name, isAsc); @@ -161,8 +181,8 @@ export class FeedbackTemplateEditorComponent implements AfterViewInit { this.templateSource.data = data.sort((a, b) => { const isAsc = sort.direction === 'asc'; switch (sort.active) { - case 'learningOutcome': - return this.compare(a.learningOutcome, b.learningOutcome, isAsc); + case 'number': + return this.compare(a.id, b.id, isAsc); case 'chipText': return this.compare(a.chipText, b.chipText, isAsc); case 'description': @@ -259,7 +279,7 @@ export class FeedbackTemplateEditorComponent implements AfterViewInit { public createFeedbackTemplate() { const feedbackTemplate = new FeedbackTemplate(this.taskDefinition); - feedbackTemplate.learningOutcome = 'TLO'; + feedbackTemplate.id = 0; feedbackTemplate.chipText = 'lorem'; feedbackTemplate.description = 'Lorem ipsum dolor'; feedbackTemplate.commentText = 'Lorem dolor'; @@ -267,4 +287,49 @@ export class FeedbackTemplateEditorComponent implements AfterViewInit { this.selectedTemplate = feedbackTemplate; } + + readonly separatorKeysCodes: number[] = [ENTER, COMMA]; + readonly currentConnectedOutcome = model(''); + readonly connectedOutcomes = signal([]); + readonly allOutcomes: string[] = ['TLO1', 'TLO2', 'TLO3', 'ULO1', 'ULO2']; + readonly filteredOutcomes = computed(() => { + const currentOutcome = this.currentConnectedOutcome().toLowerCase(); + return currentOutcome + ? this.allOutcomes.filter((outcome) => outcome.toLowerCase().includes(currentOutcome)) + : this.allOutcomes.slice(); + }); + + readonly announcer = inject(LiveAnnouncer); + + add(event: MatChipInputEvent): void { + const value = (event.value || '').trim(); + + if (value) { + this.connectedOutcomes.update((connectedOutcomes) => [...connectedOutcomes, value]); + } + + this.currentConnectedOutcome.set(''); + } + + remove(outcome: string): void { + this.connectedOutcomes.update((connectedOutcomes) => { + const index = connectedOutcomes.indexOf(outcome); + if (index < 0) { + return connectedOutcomes; + } + + connectedOutcomes.splice(index, 1); + this.announcer.announce(`Removed ${outcome}`); + return [...connectedOutcomes]; + }); + } + + selected(event: MatAutocompleteSelectedEvent): void { + this.connectedOutcomes.update((connectedOutcomes) => [ + ...connectedOutcomes, + event.option.viewValue, + ]); + this.currentConnectedOutcome.set(''); + event.option.deselect(); + } } From 9acc36502dc3fd2365817f73ccc742dd3947e293 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Fri, 22 Nov 2024 19:05:33 +1100 Subject: [PATCH 164/776] feat: add feedback template picker to comment composer --- src/app/doubtfire-angular.module.ts | 2 + .../task-comment-composer.component.html | 16 +++ .../task-comment-composer.component.scss | 7 ++ .../task-comment-composer.component.ts | 2 + .../task-feedback-templates.component.html | 103 ++++++++++++++++++ .../task-feedback-templates.component.ts | 58 ++++++++++ 6 files changed, 188 insertions(+) create mode 100644 src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.html create mode 100644 src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.ts diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 07961095af..21b56cc9e8 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -239,6 +239,7 @@ import {ScormExtensionCommentComponent} from './tasks/task-comments-viewer/scorm import {ScormExtensionModalComponent} from './common/modals/scorm-extension-modal/scorm-extension-modal.component'; import {FeedbackTemplateService} from './api/services/feedback-template.service'; import {FeedbackTemplateEditorComponent} from './common/feedback-template/feedback-template-editor.component'; +import {TaskFeedbackTemplatesComponent} from './tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.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 = { @@ -360,6 +361,7 @@ const MY_DATE_FORMAT = { ScormExtensionCommentComponent, ScormExtensionModalComponent, FeedbackTemplateEditorComponent, + TaskFeedbackTemplatesComponent, ], // Services we provide providers: [ diff --git a/src/app/tasks/task-comment-composer/task-comment-composer.component.html b/src/app/tasks/task-comment-composer/task-comment-composer.component.html index 448f88f46c..db1cc099ee 100644 --- a/src/app/tasks/task-comment-composer/task-comment-composer.component.html +++ b/src/app/tasks/task-comment-composer/task-comment-composer.component.html @@ -1,3 +1,8 @@ + + +
+ + task +
diff --git a/src/app/tasks/task-comment-composer/task-comment-composer.component.scss b/src/app/tasks/task-comment-composer/task-comment-composer.component.scss index a79e6a5be4..d17c924344 100644 --- a/src/app/tasks/task-comment-composer/task-comment-composer.component.scss +++ b/src/app/tasks/task-comment-composer/task-comment-composer.component.scss @@ -95,6 +95,13 @@ $emoji-color: lighten( border-color: darken($color: white, $amount: 10); } +.feedback-template-picker { + z-index: 1060; + position: absolute; + right: 26px; + bottom: 150px; +} + #replyContainer { margin: 0.4em 1em -0.2em 1em; font-size: 14px; diff --git a/src/app/tasks/task-comment-composer/task-comment-composer.component.ts b/src/app/tasks/task-comment-composer/task-comment-composer.component.ts index 9a4a3297ea..ca4319969d 100644 --- a/src/app/tasks/task-comment-composer/task-comment-composer.component.ts +++ b/src/app/tasks/task-comment-composer/task-comment-composer.component.ts @@ -82,6 +82,7 @@ export class TaskCommentComposerComponent implements DoCheck { emojiRegex: RegExp = /(?:\:)(.*?)(?=\:|$)/; emojiSearchResults: EmojiData[] = []; emojiMatch: string; + showFeedbackTemplatePicker: boolean = false; recording = false; cagStartWidth: number; @@ -155,6 +156,7 @@ export class TaskCommentComposerComponent implements DoCheck { e.preventDefault(); this.emojiSearchMode = false; this.showEmojiPicker = false; + this.showFeedbackTemplatePicker = false; if (this.input.first.nativeElement.innerText.trim() !== '') { this.addComment(); } diff --git a/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.html b/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.html new file mode 100644 index 0000000000..f92a30ec03 --- /dev/null +++ b/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.html @@ -0,0 +1,103 @@ + diff --git a/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.ts b/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.ts new file mode 100644 index 0000000000..6a2ea98608 --- /dev/null +++ b/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.ts @@ -0,0 +1,58 @@ +import {Component, ElementRef, ViewChild} from '@angular/core'; + +@Component({ + selector: 'f-task-feedback-templates', + templateUrl: './task-feedback-templates.component.html', +}) +export class TaskFeedbackTemplatesComponent { + searchTerm: string = ''; + hoveredTemplate: string = ''; + + categories = [ + {name: 'TLO', templates: ['Template A1', 'Template A2', 'Template A3', 'Template A4']}, + {name: 'ULO', templates: ['Template B1', 'Template B2', 'Template B3', 'Template B4']}, + {name: 'CLO', templates: ['Template C1', 'Template C2', 'Template C3', 'Template C4']}, + {name: 'GLO', templates: ['Template D1', 'Template D2', 'Template D3', 'Template D4']}, + {name: 'Section', templates: ['Template E1', 'Template E2', 'Template E3', 'Template E4']}, + ]; + + filteredCategories = [...this.categories]; + + filterTemplates() { + this.filteredCategories = this.categories.map((category) => ({ + ...category, + templates: category.templates.filter((template) => + template.toLowerCase().includes(this.searchTerm.toLowerCase()), + ), + })); + } + + @ViewChild('tloSection') tloSection!: ElementRef; + @ViewChild('uloSection') uloSection!: ElementRef; + @ViewChild('cloSection') cloSection!: ElementRef; + @ViewChild('gloSection') gloSection!: ElementRef; + @ViewChild('sectionSection') sectionSection!: ElementRef; + + scrollToSection(event: any) { + const sections = [ + this.tloSection, + this.uloSection, + this.cloSection, + this.gloSection, + this.sectionSection, + ]; + const selectedSection = sections[event.index]; + + if (selectedSection) { + selectedSection.nativeElement.scrollIntoView({behavior: 'smooth', block: 'start'}); + } + } + + selectTemplate(template: string) { + console.log('Selected template:', template); + } + + onHoverTemplate(template: string) { + this.hoveredTemplate = template; + } +} From e3fdd3b0fafe1988e1f19346b11b18a5b4274072 Mon Sep 17 00:00:00 2001 From: ShounakB <65479699+Shounaks@users.noreply.github.com> Date: Sat, 23 Nov 2024 00:46:19 +1100 Subject: [PATCH 165/776] Fixing updateGrade Method --- src/app/common/grade-icon/grade-icon.component.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app/common/grade-icon/grade-icon.component.ts b/src/app/common/grade-icon/grade-icon.component.ts index d90bc6298c..5f8f31c7bc 100644 --- a/src/app/common/grade-icon/grade-icon.component.ts +++ b/src/app/common/grade-icon/grade-icon.component.ts @@ -11,7 +11,7 @@ export class GradeIconComponent implements OnInit, OnChanges { @Input() grade?: number | string; @Input() colorful: boolean = false; - gradeText: number | string = 'Grade'; + gradeText: string = 'Grade'; gradeLetter: string = 'G'; constructor(private gradeService: GradeService) {} @@ -27,10 +27,9 @@ export class GradeIconComponent implements OnInit, OnChanges { } private updateGrade(): void { - this.gradeText = - typeof this.grade === 'string' - ? this.gradeService.stringToGrade(this.grade) - : this.gradeService.grades[this.grade] || 'Grade'; - this.gradeLetter = this.gradeService.gradeAcronyms[this.grade] || 'G'; + const grade: number = + typeof this.grade === 'string' ? this.gradeService.stringToGrade(this.grade) : this.grade; + this.gradeText = this.gradeService.grades[grade] || 'Grade'; + this.gradeLetter = this.gradeService.gradeAcronyms[grade] || 'G'; } } From a42f177f61b3f7166edf3b9c93dfd2dcd3949df7 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 26 Nov 2024 01:21:41 +1100 Subject: [PATCH 166/776] feat: make template editor reusable across contexts --- src/app/api/models/feedback-template.ts | 20 +- src/app/api/models/task-definition.ts | 27 +- src/app/api/models/unit.ts | 7 + .../api/services/feedback-template.service.ts | 9 +- .../api/services/task-definition.service.ts | 35 +- src/app/api/services/unit.service.ts | 16 +- .../feedback-template-editor.component.html | 384 ++++++++++-------- .../feedback-template-editor.component.ts | 123 ++++-- src/app/doubtfire-angularjs.module.ts | 5 + .../task-definition-editor.component.html | 2 +- src/app/units/states/edit/edit.tpl.html | 2 +- 11 files changed, 395 insertions(+), 235 deletions(-) diff --git a/src/app/api/models/feedback-template.ts b/src/app/api/models/feedback-template.ts index 85da6af509..f0679a611f 100644 --- a/src/app/api/models/feedback-template.ts +++ b/src/app/api/models/feedback-template.ts @@ -1,7 +1,7 @@ import {Entity, EntityMapping} from 'ngx-entity-service'; import {Observable} from 'rxjs'; import {AppInjector} from 'src/app/app-injector'; -import {TaskDefinition} from './task-definition'; +import {TaskDefinition, Unit} from './doubtfire-model'; import {FeedbackTemplateService} from '../services/feedback-template.service'; export class FeedbackTemplate extends Entity { @@ -12,11 +12,11 @@ export class FeedbackTemplate extends Entity { commentText: string; summaryText: string; - readonly taskDefinition: TaskDefinition; + readonly context: TaskDefinition | Unit; - constructor(taskDef: TaskDefinition) { + constructor(context: TaskDefinition | Unit) { super(); - this.taskDefinition = taskDef; + this.context = context; } public toJson(mappingData: EntityMapping, ignoreKeys?: string[]): object { @@ -31,18 +31,18 @@ export class FeedbackTemplate extends Entity { if (this.isNew) { return svc.create( { - taskDefId: this.taskDefinition.id, + contextId: this.context.id, }, { entity: this, - cache: this.taskDefinition.feedbackTemplateCache, - constructorParams: this.taskDefinition, + cache: this.context.feedbackTemplateCache, + constructorParams: this.context, }, ); } else { return svc.update( { - taskDefId: this.taskDefinition.id, + contextId: this.context.id, id: this.id, }, {entity: this}, @@ -72,7 +72,7 @@ export class FeedbackTemplate extends Entity { return !this.id; } - public get taskDefId(): number { - return this.taskDefinition.id; + public getContextId(): number { + return this.context.id; } } diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index 2e17ac6cda..a2e9b85e73 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -3,8 +3,9 @@ import { Entity, EntityCache, 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 { FeedbackTemplate, Grade, GroupSet, TutorialStream, Unit } from './doubtfire-model'; +import { FeedbackTemplate, Grade, GroupSet, LearningOutcome, TutorialStream, Unit } from './doubtfire-model'; import { TaskDefinitionService } from '../services/task-definition.service'; +import { AlertService } from 'src/app/common/services/alert.service'; export type UploadRequirement = { key: string; name: string; type: string; tiiCheck?: boolean; tiiPct?: number }; @@ -44,6 +45,8 @@ export class TaskDefinition extends Entity { assessmentEnabled: boolean; mossLanguage: string = 'moss c'; + public readonly learningOutcomesCache: EntityCache = + new EntityCache(); public readonly feedbackTemplateCache: EntityCache = new EntityCache(); @@ -114,6 +117,18 @@ export class TaskDefinition extends Entity { return this.originalSaveData != JSON.stringify(this.toJson(mapping)); } + public refresh(): void { + const alerts = AppInjector.get(AlertService); + AppInjector.get(TaskDefinitionService) + .fetch(this.id) + .subscribe({ + next: (taskDefinition) => { + console.log(taskDefinition.name); + }, + error: (message) => alerts.error(message, 6000), + }); + } + public get isNew(): boolean { return !this.id; } @@ -168,6 +183,16 @@ export class TaskDefinition extends Entity { }`; } + public getOutcomeBatchUploadUrl(): string { + const constants = AppInjector.get(DoubtfireConstants); + return `${constants.API_URL}/units/${this.unit.id}/task_definitions/${this.id}/outcomes/csv`; + } + + public getFeedbackTemplateBatchUploadUrl(): string { + const constants = AppInjector.get(DoubtfireConstants); + return `${constants.API_URL}/units/${this.unit.id}/task_definitions/${this.id}/feedback_templates/csv`; + } + /** * Open the SCORM test in a new tab - using preview mode. */ diff --git a/src/app/api/models/unit.ts b/src/app/api/models/unit.ts index b27159832e..a57f3c7db3 100644 --- a/src/app/api/models/unit.ts +++ b/src/app/api/models/unit.ts @@ -22,6 +22,7 @@ import { Project, TutorialStreamService, UnitRoleService, + FeedbackTemplate, } from './doubtfire-model'; import {LearningOutcome} from './learning-outcome'; import {AlertService} from 'src/app/common/services/alert.service'; @@ -73,6 +74,8 @@ export class Unit extends Entity { new EntityCache(); public readonly taskOutcomeAlignmentsCache: EntityCache = new EntityCache(); + public readonly feedbackTemplateCache: EntityCache = + new EntityCache(); readonly staffCache: EntityCache = new EntityCache(); @@ -388,6 +391,10 @@ export class Unit extends Entity { return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.id}/outcomes/csv`; } + public getFeedbackTemplateBatchUploadUrl(): string { + return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.id}/feedback_templates/csv`; + } + public hasStreams(): boolean { return this.tutorialStreamsCache.size > 1; } diff --git a/src/app/api/services/feedback-template.service.ts b/src/app/api/services/feedback-template.service.ts index 4d0bef50de..0c10e6debc 100644 --- a/src/app/api/services/feedback-template.service.ts +++ b/src/app/api/services/feedback-template.service.ts @@ -3,7 +3,7 @@ import {CachedEntityService} from 'ngx-entity-service'; import {FeedbackTemplate} from '../models/feedback-template'; import {HttpClient} from '@angular/common/http'; import API_URL from 'src/app/config/constants/apiURL'; -import {TaskDefinition} from '../models/task-definition'; +import {TaskDefinition, Unit} from '../models/doubtfire-model'; @Injectable() export class FeedbackTemplateService extends CachedEntityService { @@ -22,7 +22,10 @@ export class FeedbackTemplateService extends CachedEntityService { protected readonly endpointFormat = 'units/:unitId:/task_definitions/:id:'; - constructor(httpClient: HttpClient) { + constructor( + httpClient: HttpClient, + private learningOutcomeService: LearningOutcomeService, + private feedbackTemplateService: FeedbackTemplateService, + ) { super(httpClient, API_URL); this.mapping.addKeys( @@ -108,7 +113,31 @@ export class TaskDefinitionService extends CachedEntityService { 'isGraded', 'maxQualityPts', 'overseerImageId', - 'assessmentEnabled' + 'assessmentEnabled', + { + keys: 'tlos', + toEntityOp: (data: object, key: string, taskDefinition: TaskDefinition) => { + data[key]?.forEach((tlo) => { + taskDefinition.learningOutcomesCache.getOrCreate( + tlo['id'], + this.learningOutcomeService, + tlo, + ); + }); + }, + }, + { + keys: 'feedbackTemplates', + toEntityOp: (data: object, key: string, taskDefinition: TaskDefinition) => { + data[key]?.forEach((template) => { + taskDefinition.feedbackTemplateCache.getOrCreate( + template['id'], + this.feedbackTemplateService, + template, + ); + }); + }, + }, ); this.mapping.mapAllKeysToJsonExcept( diff --git a/src/app/api/services/unit.service.ts b/src/app/api/services/unit.service.ts index c3c38c7406..cd4eee6abb 100644 --- a/src/app/api/services/unit.service.ts +++ b/src/app/api/services/unit.service.ts @@ -9,6 +9,7 @@ import { TaskDefinitionService } from './task-definition.service'; import { GroupService } from './group.service'; import { Observable } from 'rxjs'; import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; +import { FeedbackTemplateService } from './feedback-template.service'; export type IloStats = { median: number; @@ -32,7 +33,8 @@ export class UnitService extends CachedEntityService { private taskDefinitionService: TaskDefinitionService, private taskOutcomeAlignmentService: TaskOutcomeAlignmentService, private groupSetService: GroupSetService, - private groupService: GroupService + private groupService: GroupService, + private feedbackTemplateService: FeedbackTemplateService, ) { super(httpClient, API_URL); @@ -210,6 +212,18 @@ export class UnitService extends CachedEntityService { }); } }, + { + keys: 'feedbackTemplates', + toEntityOp: (data: object, key: string, unit: Unit) => { + data[key]?.forEach((template) => { + unit.feedbackTemplateCache.getOrCreate( + template['id'], + this.feedbackTemplateService, + template, + ); + }); + }, + }, // 'groupMemberships', - map to group memberships ); diff --git a/src/app/common/feedback-template/feedback-template-editor.component.html b/src/app/common/feedback-template/feedback-template-editor.component.html index 51af89539a..fd4cccc16c 100644 --- a/src/app/common/feedback-template/feedback-template-editor.component.html +++ b/src/app/common/feedback-template/feedback-template-editor.component.html @@ -91,202 +91,246 @@ - - @if (selectedOutcome) { -
- - Outcome Number - - +
+
+

Edit Outcome

+ - - Abbreviation - - +
+ + Tag + + - - Name - - -
+ + Name + + +
- - Description - - + + Description + + - - Connected Learning Outcomes - - @for (outcome of connectedOutcomes(); track $index) { - - {{ outcome }} - - - } - - - - @for (outcome of filteredOutcomes(); track outcome) { - {{ outcome }} - } - - + + Connected Learning Outcomes + + @for (outcome of connectedOutcomes(); track $index) { + + {{ outcome }} + + + } + + + + @for (outcome of filteredOutcomes(); track outcome) { + {{ outcome }} + } + + -
-
Learning OutcomeNumber - {{ feedbackTemplate.learningOutcome }} + {{ feedbackTemplate.id }}
- - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - + @if (selectedTemplate) { +
+

Edit Template

+ -
- -
Number - {{ feedbackTemplate.id }} - Chip Text - {{ feedbackTemplate.chipText }} - Description - {{ feedbackTemplate.description }} - Comment Text - {{ feedbackTemplate.commentText }} - Summary Text - {{ feedbackTemplate.summaryText }} - + + + + + +
+

Edit Feedback Templates

+ + +
+ - @if (feedbackTemplateHasChanges(feedbackTemplate)) { - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Number - save - - } - Chip Text + {{ feedbackTemplate.chipText }} + Description + {{ feedbackTemplate.description }} + Comment Text + {{ feedbackTemplate.commentText }} + Summary Text + {{ feedbackTemplate.summaryText }} + + @if (feedbackTemplateHasChanges(feedbackTemplate)) { + + } + +
+ +
+ + + +
+ + + + + -
- -
- - - +
+ + Chip Text + + + + + Summary Text + + +
+ + + Comment Text + + + + + Description + + + +
+ + +
+
+ }
- - - - - - - - +
} diff --git a/src/app/common/feedback-template/feedback-template-editor.component.ts b/src/app/common/feedback-template/feedback-template-editor.component.ts index fa8a82df27..ddf0334e54 100644 --- a/src/app/common/feedback-template/feedback-template-editor.component.ts +++ b/src/app/common/feedback-template/feedback-template-editor.component.ts @@ -32,13 +32,14 @@ import {COMMA, ENTER} from '@angular/cdk/keycodes'; import {LiveAnnouncer} from '@angular/cdk/a11y'; import {MatChipInputEvent} from '@angular/material/chips'; import {MatAutocompleteSelectedEvent} from '@angular/material/autocomplete'; +import {FileDownloaderService} from '../file-downloader/file-downloader.service'; @Component({ selector: 'f-feedback-template-editor', templateUrl: 'feedback-template-editor.component.html', }) export class FeedbackTemplateEditorComponent implements AfterViewInit { - @Input() taskDefinition: TaskDefinition; + @Input() context: TaskDefinition | Unit; @ViewChild(MatTable, {static: false}) outcomeTable: MatTable; @ViewChild(MatSort, {static: false}) outcomeSort: MatSort; @@ -52,7 +53,6 @@ export class FeedbackTemplateEditorComponent implements AfterViewInit { 'description', 'learningOutcomeAction', ]; - public outcomeFilter: string; public selectedOutcome: LearningOutcome; @ViewChild(MatTable, {static: false}) templateTable: MatTable; @@ -68,7 +68,6 @@ export class FeedbackTemplateEditorComponent implements AfterViewInit { 'summaryText', 'feedbackTemplateAction', ]; - public templateFilter: string; public selectedTemplate: FeedbackTemplate; constructor( @@ -76,28 +75,42 @@ export class FeedbackTemplateEditorComponent implements AfterViewInit { private taskDefinitionService: TaskDefinitionService, private learningOutcomeService: LearningOutcomeService, private feedbackTemplateService: FeedbackTemplateService, + private fileDownloaderService: FileDownloaderService, @Inject(csvResultModalService) private csvResultModalService: any, @Inject(csvUploadModalService) private csvUploadModal: any, @Inject(confirmationModal) private confirmationModal: any, ) {} - public get unit(): Unit { - return this.taskDefinition?.unit; - } - ngAfterViewInit(): void { this.subscriptions.push( - this.unit.learningOutcomesCache.values.subscribe((learningOutcomes) => { + this.context.learningOutcomesCache.values.subscribe((learningOutcomes) => { this.outcomeSource = new MatTableDataSource(learningOutcomes); this.outcomeSource.paginator = this.outcomePaginator; this.outcomeSource.sort = this.outcomeSort; - this.outcomeSource.filterPredicate = (data: any, filter: string) => data.matches(filter); + this.outcomeSource.filterPredicate = (data: LearningOutcome, filter: string) => { + const filterValue = filter.trim().toLowerCase(); + return ( + data.iloNumber.toString().includes(filterValue) || + data.abbreviation.toLowerCase().includes(filterValue) || + data.name.toLowerCase().includes(filterValue) || + data.description.toLowerCase().includes(filterValue) + ); + }; }), - this.taskDefinition.feedbackTemplateCache.values.subscribe((feedbackTemplates) => { + this.context.feedbackTemplateCache.values.subscribe((feedbackTemplates) => { this.templateSource = new MatTableDataSource(feedbackTemplates); this.templateSource.paginator = this.templatePaginator; this.templateSource.sort = this.templateSort; - this.templateSource.filterPredicate = (data: any, filter: string) => data.matches(filter); + this.templateSource.filterPredicate = (data: FeedbackTemplate, filter: string) => { + const filterValue = filter.trim().toLowerCase(); + return ( + data.id.toString().includes(filterValue) || + data.chipText.toLowerCase().includes(filterValue) || + data.commentText.toLowerCase().includes(filterValue) || + data.summaryText.toLowerCase().includes(filterValue) || + data.description.toLowerCase().includes(filterValue) + ); + }; }), ); } @@ -201,19 +214,17 @@ export class FeedbackTemplateEditorComponent implements AfterViewInit { return (a < b ? -1 : 1) * (isAsc ? 1 : -1); } - applyOutcomeFilter(filterValue: string) { - this.outcomeSource.filter = filterValue.trim().toLowerCase(); - - if (this.outcomeSource.paginator) { - this.outcomeSource.paginator.firstPage(); - } - } - - applyTemplateFilter(filterValue: string) { - this.templateSource.filter = filterValue.trim().toLowerCase(); - - if (this.templateSource.paginator) { - this.templateSource.paginator.firstPage(); + applyFilter(filterValue: string, table: string) { + if (table === 'outcome') { + this.outcomeSource.filter = filterValue; + if (this.outcomeSource.paginator) { + this.outcomeSource.paginator.firstPage(); + } + } else if (table === 'template') { + this.templateSource.filter = filterValue; + if (this.templateSource.paginator) { + this.templateSource.paginator.firstPage(); + } } } @@ -230,6 +241,15 @@ export class FeedbackTemplateEditorComponent implements AfterViewInit { 'Delete learning outcome', 'Are you sure you want to delete this outcome? This action is final.', () => { + this.learningOutcomeService + .delete( + {id: learningOutcome.id, unitId: this.context.id}, + {entity: learningOutcome, cache: this.context.learningOutcomesCache}, + ) + .subscribe({ + next: () => this.alerts.success('Learning outcome deleted'), + error: () => this.alerts.error('Failed to delete learning outcome. Please try again.'), + }); this.alerts.success('Outcome deleted'); }, ); @@ -245,45 +265,58 @@ export class FeedbackTemplateEditorComponent implements AfterViewInit { ); } - public uploadLearningOutcomesCsv() { + public uploadCsv(type: 'Learning Outcomes' | 'Feedback Templates') { + const url = + type === 'Learning Outcomes' + ? this.context.getOutcomeBatchUploadUrl() + : this.context.getFeedbackTemplateBatchUploadUrl(); + this.csvUploadModal.show( - 'Upload Learning Outcomes as CSV', + `Upload ${type} as CSV`, 'Test message', - {file: {name: 'Learning Outcome CSV Data', type: 'csv'}}, - this.unit.getTaskDefinitionBatchUploadUrl(), - (response: any) => {}, + {file: {name: `${type} CSV Data`, type: 'csv'}}, + url, + (response: any) => { + this.csvResultModalService.show(`${type} CSV Upload Results`, response); + if (response.success.length > 0) { + this.context.refresh(); + } + }, ); } - public uploadFeedbackTemplatesCsv() { - this.csvUploadModal.show( - 'Upload Feedback Templates as CSV', - 'Test message', - {file: {name: 'Feedback Template CSV Data', type: 'csv'}}, - this.unit.getTaskDefinitionBatchUploadUrl(), - (response: any) => {}, - ); + public downloadCsv(type: 'learning-outcomes' | 'feedback-templates') { + let name: string = ''; + if (this.context instanceof TaskDefinition) name = this.context.abbreviation; + else if (this.context instanceof Unit) name = this.context.code; + + const url = + type === 'learning-outcomes' + ? this.context.getOutcomeBatchUploadUrl() + : this.context.getFeedbackTemplateBatchUploadUrl(); + + this.fileDownloaderService.downloadFile(url, `${name}-${type}.csv`); } public createLearningOutcome() { const learningOutcome = new LearningOutcome(); learningOutcome.iloNumber = 1; - learningOutcome.abbreviation = 'lm'; - learningOutcome.name = 'lorem'; - learningOutcome.description = 'Lorem ipsum dolor'; + learningOutcome.abbreviation = ''; + learningOutcome.name = ''; + learningOutcome.description = ''; this.selectedOutcome = learningOutcome; } public createFeedbackTemplate() { - const feedbackTemplate = new FeedbackTemplate(this.taskDefinition); + const feedbackTemplate = new FeedbackTemplate(this.context); feedbackTemplate.id = 0; - feedbackTemplate.chipText = 'lorem'; - feedbackTemplate.description = 'Lorem ipsum dolor'; - feedbackTemplate.commentText = 'Lorem dolor'; - feedbackTemplate.summaryText = 'Lorem ipsum'; + feedbackTemplate.chipText = ''; + feedbackTemplate.description = ''; + feedbackTemplate.commentText = ''; + feedbackTemplate.summaryText = ''; this.selectedTemplate = feedbackTemplate; } diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index e14fdbb545..c227a43c05 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -225,6 +225,7 @@ import {MarkedPipe} from './common/pipes/marked.pipe'; import {AlertService} from './common/services/alert.service'; 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 {FeedbackTemplateEditorComponent} from './common/feedback-template/feedback-template-editor.component'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -463,6 +464,10 @@ DoubtfireAngularJSModule.directive( 'statusIcon', downgradeComponent({component: StatusIconComponent}), ); +DoubtfireAngularJSModule.directive( + 'fFeedbackTemplateEditor', + downgradeComponent({component: FeedbackTemplateEditorComponent}), +); DoubtfireAngularJSModule.directive('newFUnits', downgradeComponent({component: FUnitsComponent})); // Global configuration 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 c56b29c055..59adc0f043 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 @@ -34,7 +34,7 @@

Task detailsTask Learning Outcomes

Add learning outcomes for this task

- +
diff --git a/src/app/units/states/edit/edit.tpl.html b/src/app/units/states/edit/edit.tpl.html index 3ba73129a5..c2db43e07f 100644 --- a/src/app/units/states/edit/edit.tpl.html +++ b/src/app/units/states/edit/edit.tpl.html @@ -5,7 +5,7 @@ - + From 5494384f1a4becf462dd031e89fae2734ed46e55 Mon Sep 17 00:00:00 2001 From: satikaj <117552851+satikaj@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:29:16 +1100 Subject: [PATCH 167/776] fix: remove selected outcomes from dropdown --- .../feedback-template-editor.component.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/common/feedback-template/feedback-template-editor.component.ts b/src/app/common/feedback-template/feedback-template-editor.component.ts index ddf0334e54..38358b0eab 100644 --- a/src/app/common/feedback-template/feedback-template-editor.component.ts +++ b/src/app/common/feedback-template/feedback-template-editor.component.ts @@ -327,9 +327,12 @@ export class FeedbackTemplateEditorComponent implements AfterViewInit { readonly allOutcomes: string[] = ['TLO1', 'TLO2', 'TLO3', 'ULO1', 'ULO2']; readonly filteredOutcomes = computed(() => { const currentOutcome = this.currentConnectedOutcome().toLowerCase(); - return currentOutcome - ? this.allOutcomes.filter((outcome) => outcome.toLowerCase().includes(currentOutcome)) - : this.allOutcomes.slice(); + return this.allOutcomes.filter((outcome) => { + return ( + !this.connectedOutcomes().includes(outcome) && + (!currentOutcome || outcome.toLowerCase().includes(currentOutcome)) + ); + }); }); readonly announcer = inject(LiveAnnouncer); From 9d017fbfaa1d5bae7f43da37ec7ae43e1dfafa8f Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 6 Dec 2024 10:25:51 +1100 Subject: [PATCH 168/776] feat: switch to html5 mode --- src/app/doubtfire-angularjs.module.ts | 4 +++- src/index.html | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index e14fdbb545..2f12cc2b57 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -236,7 +236,9 @@ export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.projects', 'doubtfire.groups', 'doubtfire.visualisations', -]); +]).config(['$locationProvider', ($locationProvider) => { + $locationProvider.html5Mode(true); +}]); // Downgrade angular modules that we need... // factory -> service diff --git a/src/index.html b/src/index.html index 54fed2d067..2e715e206f 100644 --- a/src/index.html +++ b/src/index.html @@ -1,6 +1,7 @@ + -
-
-

Modify Unit Staff

- Add staff members to the unit, assigning them a convenor or tutor role. -
-
-
-
This unit has no staff assigned
-
- - - - - - - - - - - - - - - - - - -
NameRoleMain ConvenorActions
- - {{staff.user.name}} -
- - -
-
- - - -
-
-
-
- -
- diff --git a/src/app/units/states/edit/edit.tpl.html b/src/app/units/states/edit/edit.tpl.html index 2c9699e496..4df8bf3d09 100644 --- a/src/app/units/states/edit/edit.tpl.html +++ b/src/app/units/states/edit/edit.tpl.html @@ -6,7 +6,7 @@ - + From 943e3bf761ab210e68d69982ccf91c0d093a836e Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:44:53 +1000 Subject: [PATCH 570/776] refactor: unit staff editor material ui (#1001) * fix: revert staff role on error * fix: ensure full search for unit role * chore: fix casing * refactor: use mat table for staff editor * chore: remove unused variables * fix: null check * refactor: use icon button * chore: remove unit staff editor coffeescript file * chore: add back staff editor tooltips --- src/app/common/header/header.component.ts | 3 +- .../unit-staff-editor.coffee | 58 ------ .../unit-staff-editor.component.html | 197 ++++++++---------- .../unit-staff-editor.component.ts | 51 ++++- 4 files changed, 127 insertions(+), 182 deletions(-) delete mode 100644 src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee diff --git a/src/app/common/header/header.component.ts b/src/app/common/header/header.component.ts index 8d9e8f6a11..4601057b15 100644 --- a/src/app/common/header/header.component.ts +++ b/src/app/common/header/header.component.ts @@ -173,8 +173,7 @@ export class HeaderComponent implements OnInit, OnDestroy { } isUniqueRole = (unit) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const units = this.unitRoles.filter((role: any) => role.unit.id === unit.unit.id); + const units = this.unitRoles.filter((role: UnitRole) => role.unit?.id === unit.unit?.id); return units.length == 1 || unit.role == 'Tutor'; }; diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee deleted file mode 100644 index 3856daefe5..0000000000 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee +++ /dev/null @@ -1,58 +0,0 @@ -angular.module('doubtfire.units.states.edit.directives.unit-staff-editor', []) - -# -# Editor for adding new staff to a unit and assigning those staff -# members new unit roles within the unit -# -.directive('unitStaffEditor', -> - replace: true - restrict: 'E' - templateUrl: 'units/states/edit/directives/unit-staff-editor/unit-staff-editor.tpl.html' - controller: ($scope, $rootScope, alertService, newUnitService, newUnitRoleService) -> - temp = [] - users = [] - - $scope.unit.staffCache.values.subscribe( (staff) -> $scope.unitStaff = staff ) - - $scope.changeRole = (unitRole, role_id) -> - unitRole.roleId = role_id - newUnitRoleService.update(unitRole).subscribe({ - next: (response) -> alertService.success( "Role changed", 2000) - error: (response) -> alertService.error( response, 6000) - }) - - $scope.changeMainConvenor = (staff) -> - $scope.unit.changeMainConvenor(staff).subscribe({ - next: (response) -> - alertService.success( "Main convenor changed", 2000) - error: (response) -> - alertService.error( response, 6000) - }) - - $scope.addSelectedStaff = -> - staff = $scope.selectedStaff - $scope.selectedStaff = null - $scope.unit.staff = [] unless $scope.unit.staff - - if staff.id? - $scope.unit.addStaff(staff).subscribe({ - next: (response) -> alertService.success( "Staff member added", 2000) - error: (response) -> alertService.error( response, 6000) - }) - else - alertService.error( "Unable to add staff member. Ensure they have a tutor or convenor account in User admin first.", 6000) - - # Used in the typeahead to filter staff already in unit - $scope.filterStaff = (staff) -> - not _.find($scope.unit.staff, (listStaff) -> staff.id == listStaff.user.id) - - $scope.removeStaff = (staff) -> - newUnitRoleService.delete(staff, {cache: $scope.unit.staffCache}).subscribe({ - next: (response) -> alertService.success( "Staff member removed", 2000) - error: (response) -> alertService.error( response, 6000) - }) - - $scope.groupSetName = (id) -> - $scope.unit.groupSetsCache.get(id)?.name || "Individual Work" - -) diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html index 84aa983f0a..82938cde3f 100644 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html +++ b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html @@ -1,116 +1,89 @@ -
- - - - - - - - -
-
-

Modify Unit Staff

-

Add staff members to the unit, assigning them a convenor or tutor role.

-
- -
-
- -
- This unit has no staff assigned. -
+
+
+

Unit Staff

+

Manage unit staff by adding members and assigning them as convenors or tutors.

+
+ + + - -
-
Name
- - - - - - - - - - - - - - - - - -
NameRoleMain ConvenorActions
- - {{ staff.user.name }} -
- - -
-
- - - -
+ +
+ {{ unitRole.user.name }}
-
-
- -
+ Convenor + + + + + Main Convenor + + @if (unitRole?.role === 'Convenor') { + + } + + + + Actions + + + + + + + + + + + + {{ staff.name }} + + +
diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts index 167ed3210f..7c53a04671 100644 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts +++ b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts @@ -4,6 +4,9 @@ import {UnitRoleService} from 'src/app/api/services/unit-role.service'; import {Unit} from 'src/app/api/models/unit'; import {User} from 'src/app/api/models/doubtfire-model'; import {UnitRole} from 'src/app/api/models/unit-role'; +import {MatTableDataSource} from '@angular/material/table'; +import {MatButtonToggleChange} from '@angular/material/button-toggle'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; @Component({ selector: 'unit-staff-editor', @@ -19,19 +22,32 @@ export class UnitStaffEditorComponent implements OnInit { filteredStaff: User[] = []; // Filtered staff members searchTerm: string = ''; // Search term entered by the user + displayedColumns: string[] = ['name', 'role', 'main-convenor', 'actions']; + dataSource = new MatTableDataSource(); + // Inject services here constructor( private alertService: AlertService, private unitRoleService: UnitRoleService, + private confirmationModalService: ConfirmationModalService, ) {} ngOnInit(): void { // Subscribe to staff cache this.unit.staffCache.values.subscribe((staff: UnitRole[]) => { this.unitStaff = staff; + this.dataSource.data = staff; }); } + onRoleChange(unitRole: UnitRole, event: MatButtonToggleChange) { + const role = event.value; + if (role !== 'Tutor' && role !== 'Convenor') { + return; + } + const roleId = role === 'Tutor' ? 2 : 3; // map however you like + this.changeRole(unitRole, roleId, role); + } /** * Changes the role of a staff member. * @@ -40,11 +56,20 @@ export class UnitStaffEditorComponent implements OnInit { * * @returns void */ - changeRole(unitRole: UnitRole, role_id: number) { - unitRole.roleId = role_id; + changeRole(unitRole: UnitRole, roleId: number, role: string) { + const previousRoleId = unitRole.roleId; + const previousRole = unitRole.role; + + unitRole.roleId = roleId; + unitRole.role = role; this.unitRoleService.update(unitRole).subscribe({ - next: (response) => this.alertService.success('Role changed', 2000), - error: (response) => this.alertService.error(response, 6000), + next: () => this.alertService.success('Role changed', 2000), + error: (response) => { + // Revert changes on error + unitRole.roleId = previousRoleId; + unitRole.role = previousRole; + this.alertService.error(response, 6000); + }, }); } @@ -56,10 +81,16 @@ export class UnitStaffEditorComponent implements OnInit { * @returns void */ changeMainConvenor(staff: UnitRole) { - this.unit.changeMainConvenor(staff).subscribe({ - next: (response) => this.alertService.success('Main convenor changed', 2000), - error: (response) => this.alertService.error(response, 6000), - }); + this.confirmationModalService.show( + 'Set Main Convenor', + `Do you want to make ${staff.user.name} the main convenor for this unit?`, + () => { + this.unit.changeMainConvenor(staff).subscribe({ + next: (_response) => this.alertService.success('Main convenor changed', 2000), + error: (response) => this.alertService.error(response, 6000), + }); + }, + ); } /** @@ -99,7 +130,7 @@ export class UnitStaffEditorComponent implements OnInit { } this.filteredStaff = this.staff.filter( (staff) => - staff.name.toLowerCase().includes(this.searchTerm.toLowerCase()) && // Find by name + staff.matches(this.searchTerm.toLowerCase()) && // Find by name !this.unit.staff.find((listStaff) => staff.id === listStaff.user.id), // Not already assigned to the unit ); } @@ -124,7 +155,7 @@ export class UnitStaffEditorComponent implements OnInit { */ removeStaff(staff: UnitRole) { this.unitRoleService.delete(staff, {cache: this.unit.staffCache}).subscribe({ - next: (response) => this.alertService.success('Staff member removed', 2000), + next: () => this.alertService.success('Staff member removed', 2000), error: (response) => this.alertService.error(response, 6000), }); } From bfae1ad69bb767cdad47dd8fac35cc84e5134534 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:45:33 +1000 Subject: [PATCH 571/776] fix: check for valid unit --- .../unit-staff-editor/unit-staff-editor.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html index 82938cde3f..9f3a65b727 100644 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html +++ b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html @@ -45,13 +45,13 @@

Unit Staff

@if (unitRole?.role === 'Convenor') { } From bfbcee98cfa4888241c5956372d004ec1cd813cb Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:49:36 +1000 Subject: [PATCH 572/776] feat: add observer only ui --- src/app/api/models/unit-role.ts | 1 + src/app/api/services/unit-role.service.ts | 3 ++- .../unit-staff-editor.component.html | 13 +++++++++++++ .../unit-staff-editor.component.ts | 16 +++++++++++++++- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/app/api/models/unit-role.ts b/src/app/api/models/unit-role.ts index 5868fa2f05..63bf9852de 100644 --- a/src/app/api/models/unit-role.ts +++ b/src/app/api/models/unit-role.ts @@ -11,6 +11,7 @@ export class UnitRole extends Entity { role: string; user: User; unit: Unit; + observerOnly: boolean; /** * The id for updated roles - but we need to move away from this to the role string... diff --git a/src/app/api/services/unit-role.service.ts b/src/app/api/services/unit-role.service.ts index 0100bb78ce..71cb194422 100644 --- a/src/app/api/services/unit-role.service.ts +++ b/src/app/api/services/unit-role.service.ts @@ -62,9 +62,10 @@ export class UnitRoleService extends CachedEntityService { return entity.unit?.id; }, }, + 'observerOnly', ); - this.mapping.addJsonKey('roleId', 'userId', 'unitId', 'role'); + this.mapping.addJsonKey('roleId', 'userId', 'unitId', 'role', 'observerOnly'); } public createInstanceFrom(json: any, other?: any): UnitRole { diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html index 9f3a65b727..ae0ca0114d 100644 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html +++ b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html @@ -57,6 +57,19 @@

Unit Staff

} + + + Observer Only + + + + Actions diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts index 7c53a04671..f64d62d921 100644 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts +++ b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts @@ -22,7 +22,7 @@ export class UnitStaffEditorComponent implements OnInit { filteredStaff: User[] = []; // Filtered staff members searchTerm: string = ''; // Search term entered by the user - displayedColumns: string[] = ['name', 'role', 'main-convenor', 'actions']; + displayedColumns: string[] = ['name', 'role', 'main-convenor', 'observer-only', 'actions']; dataSource = new MatTableDataSource(); // Inject services here @@ -73,6 +73,20 @@ export class UnitStaffEditorComponent implements OnInit { }); } + toggleObserverOnly(unitRole: UnitRole) { + const previousValue = unitRole.observerOnly; + unitRole.observerOnly = !unitRole.observerOnly; + unitRole.roleId = unitRole.role === 'Tutor' ? 2 : 3; + this.unitRoleService.update(unitRole).subscribe({ + next: () => this.alertService.success('Observer status updated', 2000), + error: (response) => { + // Revert changes on error + unitRole.observerOnly = previousValue; + this.alertService.error(response, 6000); + }, + }); + } + /** * Changes who the `Main Convenor` of the unit is. * From 75feec9c436a852c5e6ea69c93ae300d59cdaf38 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:46:50 +1000 Subject: [PATCH 573/776] chore(release): 10.0.0-45 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45aefeb73a..755a8eecd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-45](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-44...v10.0.0-45) (2025-09-22) + + +### Bug Fixes + +* check for valid unit ([bfae1ad](https://github.com/b0ink/doubtfire-deploy/commit/bfae1ad69bb767cdad47dd8fac35cc84e5134534)) + ## [10.0.0-44](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-43...v10.0.0-44) (2025-09-18) ## [10.0.0-43](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-42...v10.0.0-43) (2025-09-17) diff --git a/package-lock.json b/package-lock.json index 3500ef85ca..a8b02747b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-44", + "version": "10.0.0-45", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-44", + "version": "10.0.0-45", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 6ba1706bec..1f9c61185e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-44", + "version": "10.0.0-45", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 01db5769b77ba2d6b8a3242935ba73481f16bf00 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:13:32 +1000 Subject: [PATCH 574/776] refactor: use students preferred name in greeting chip --- src/app/api/models/user/user.ts | 9 +++++++++ .../task-feedback-templates.component.ts | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/app/api/models/user/user.ts b/src/app/api/models/user/user.ts index 4fc56c9349..03a281bf12 100644 --- a/src/app/api/models/user/user.ts +++ b/src/app/api/models/user/user.ts @@ -53,6 +53,15 @@ export class User extends Entity { return `${fn} ${sn}${nn}`; } + public get preferredName(): string { + const nickname = this.nickname.trim(); + const firstName = this.firstName.trim(); + if (nickname) { + return nickname; + } + return firstName; + } + public matches(text: string): boolean { return ( this.studentId?.toLowerCase().indexOf(text) >= 0 || diff --git a/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.ts b/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.ts index ae3453dbaf..696e5ecca9 100644 --- a/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.ts +++ b/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.ts @@ -191,7 +191,7 @@ export class TaskFeedbackTemplatesComponent implements OnInit, OnChanges { selectTemplate(template: FeedbackTemplate) { if (template.type === 'template') { if (template.chipText === 'Greeting') { - template.commentText = `Hi ${this.task.project.student.firstName}. `; + template.commentText = `Hi ${this.task.project.student.preferredName}. `; } else if (template.chipText === 'Summarise feedback') { if (!this.selectedTemplates || this.selectedTemplates.length < 1) return; template.commentText = 'Summary of the given feedback:'; From 568cb0e5730c9931225e750950506f42f6e6b4f8 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 24 Sep 2025 11:06:49 +1000 Subject: [PATCH 575/776] chore: format --- .../task-due-card.component.html | 133 +++++++++--------- 1 file changed, 70 insertions(+), 63 deletions(-) diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.html index 28baa761d9..24a8d8c1c7 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.html @@ -1,15 +1,19 @@ -@if ( (!task?.inFinalState() || (task?.inTimeExceeded() && !task?.isPastDeadline())) && !task?.inAwaitingFeedbackState() +@if ( + (!task?.inFinalState() || (task?.inTimeExceeded() && !task?.isPastDeadline())) && + !task?.inAwaitingFeedbackState() ) { -
- - @if (task?.isDueSoon() && !task?.isPastDueDate()) { - - - - Aim To Complete Soon - Due in {{ task?.timeUntilDueDateDescription() }} - - - +
+ + @if (task?.isDueSoon() && !task?.isPastDueDate()) { + + + + Aim To Complete Soon - Due in {{ task?.timeUntilDueDateDescription() }} + + + @if (flexibleDatesEnabled) {

Your target due date for this task is @@ -22,9 +26,9 @@ >. You should aim to complete this task before then to keep your progress on track.

} -
- - + + + @if (flexibleDatesEnabled) {

This target due date for this task is {{ task?.localDueDateString() }} } - - } + + } - - @if (task?.betweenDueDateAndDeadlineDate()) { + + @if (task?.betweenDueDateAndDeadlineDate()) { } - - - + + + @if (flexibleDatesEnabled) {

You should have completed this task by @@ -97,52 +101,54 @@

} @else {

- You should have completed this task by {{ task?.localDueDateString() }}. Make sure to discuss this task with your tutor as soon as possible. If this task remains on this state for an - extended period, it will be marked as Time Exceeded. + You should have completed this task by + {{ task?.localDueDateString() }}. Make sure to discuss this task with your tutor as soon as possible. If this task + remains on this state for an extended period, it will be marked as Time Exceeded.

- Tasks are only considered completed once your tutor has discussed your work - with you. + Tasks are only considered completed once your tutor has + discussed your work with you.

} +
+
+ } - - - } - - - @if (task?.isPastDeadline()) { - - - error - Passed Due Date By {{ task?.timePastDueDateDescription() }} - - - -

- You should have completed this task by {{ task?.localDueDateString() }}. This task is now past the deadline and will be marked as Time Exceeded when submitted. You should - consult with the unit assessment details to determine the impact of failing to complete this task within the - allocated time. -

-
- - -

- You should have completed this task by {{ task?.localDueDateString() }}. Make sure to discuss this task with your tutor as soon as possible. -

-

- Tasks are only considered Completed once it demonstrates the required standard, and it is - discussed with your tutor. -

-
-
- } -
+ + @if (task?.isPastDeadline()) { + + + error + Passed Due Date By {{ task?.timePastDueDateDescription() }} + + + +

+ You should have completed this task by {{ task?.localDueDateString() }}. This task is now past the deadline and will be marked as Time Exceeded when + submitted. You should consult with the unit assessment details to determine the impact + of failing to complete this task within the allocated time. +

+
+ + +

+ You should have completed this task by {{ task?.localDueDateString() }}. Make sure to discuss this task with your tutor as soon as possible. +

+

+ Tasks are only considered Completed once it demonstrates the required + standard, and it is discussed with your tutor. +

+
+
+ } +
} @@ -154,7 +160,8 @@

You have submitted this task and should now wait for feedback from your tutor. Do not re-upload new files at this time as the status will be changed to Time Exceeded.Do not re-upload new files at this time as the status will be changed to + Time Exceeded.

From 3bf25e77fb9d64fb1ef9801967d69357ff209354 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 24 Sep 2025 11:23:27 +1000 Subject: [PATCH 576/776] refactor: change overdue task wording for assess in portfolio states --- .../task-due-card/task-due-card.component.html | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.html index 24a8d8c1c7..83bc582179 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.html @@ -129,10 +129,20 @@

- You should have completed this task by {{ task?.localDueDateString() }}. This task is now past the deadline and will be marked as Time Exceeded when - submitted. You should consult with the unit assessment details to determine the impact - of failing to complete this task within the allocated time. + @if (task?.taskDefinition?.unit.markLateSubmissionsAsAssessInPortfolio) { + You should have completed this task by + {{ task?.localDueDateString() }}. This task is now past the deadline and can only be submitted directly for your + portfolio without feedback. You should consult with the unit assessment details to + determine the impact of failing to complete this task within the allocated time. + } @else { + You should have completed this task by + {{ task?.localDueDateString() }}. This task is now past the deadline and will be marked as + Time Exceeded when submitted. You should consult with the unit assessment + details to determine the impact of failing to complete this task within the allocated + time. + }

From e5b15713c1d133d4e8a55c70249fcbf28294530b Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:31:04 +1000 Subject: [PATCH 577/776] chore: return confirmation modal reference --- .../confirmation-modal/confirmation-modal.service.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts b/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts index 6257277eff..660ddec60c 100644 --- a/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts +++ b/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts @@ -1,5 +1,5 @@ import {Injectable} from '@angular/core'; -import {MatDialog} from '@angular/material/dialog'; +import {MatDialog, MatDialogRef} from '@angular/material/dialog'; import {ConfirmationModalComponent, ConfirmationModalData} from './confirmation-modal.component'; @Injectable({ @@ -8,8 +8,12 @@ import {ConfirmationModalComponent, ConfirmationModalData} from './confirmation- export class ConfirmationModalService { constructor(public dialog: MatDialog) {} - public show(title: string, message: string, action?: any) { - this.dialog.open( + public show( + title: string, + message: string, + action?: any, + ): MatDialogRef { + return this.dialog.open( ConfirmationModalComponent, { data: { From 75172ba67d3eb5f83c7421fec7464298efb7c8ee Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:31:33 +1000 Subject: [PATCH 578/776] refactor: require confirmation for enabling assess in portfolio for late submissions --- .../unit-details-editor.coffee | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.coffee b/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.coffee index a93c83b0ed..8049804f32 100644 --- a/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.coffee +++ b/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.coffee @@ -8,7 +8,7 @@ angular.module('doubtfire.units.states.edit.directives.unit-details-editor', []) replace: true restrict: 'E' templateUrl: 'units/states/edit/directives/unit-details-editor/unit-details-editor.tpl.html' - controller: ($scope, $state, $rootScope, DoubtfireConstants, newUnitService, alertService, newTeachingPeriodService, TaskSubmission, D2lUnitDetailsModal) -> + controller: ($scope, $timeout, $state, $rootScope, DoubtfireConstants, newUnitService, alertService, newTeachingPeriodService, TaskSubmission, D2lUnitDetailsModal, ConfirmationModal) -> $scope.overseerEnabled = DoubtfireConstants.IsOverseerEnabled $scope.calOptions = { @@ -83,6 +83,28 @@ angular.module('doubtfire.units.states.edit.directives.unit-details-editor', []) $scope.d2lEnabled = -> DoubtfireConstants.IsD2LEnabled.value + updatingAssessInPortfolio = false + $scope.$watch 'unit.markLateSubmissionsAsAssessInPortfolio', (newVal, oldVal) -> + return if newVal is oldVal or newVal == false or updatingAssessInPortfolio + updatingAssessInPortfolio = true + $scope.unit.markLateSubmissionsAsAssessInPortfolio = false + modal = ConfirmationModal.show( + 'Enable Assess in Portfolio?', + """ + Are you sure you want to enable "Assess in Portfolio" for late submissions? + This will update any existing Time/Feedback Exceeded tasks to the "Assess in Portfolio" state. + You will not be able to disable this setting while any tasks remain in the "Assess in Portfolio" state. + """ + () -> + $scope.unit.markLateSubmissionsAsAssessInPortfolio = true + $timeout -> + updatingAssessInPortfolio = false + ) + + modal.afterClosed().subscribe(() -> + $timeout -> + updatingAssessInPortfolio = false + ) ) From 29fffd36c437218491ef0bdffc4eb4969f5d4357 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:38:38 +1000 Subject: [PATCH 579/776] chore: reword automated assess in portfolio message --- src/app/common/footer/footer.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/common/footer/footer.component.ts b/src/app/common/footer/footer.component.ts index 6d712f071c..a53bc9700e 100644 --- a/src/app/common/footer/footer.component.ts +++ b/src/app/common/footer/footer.component.ts @@ -78,7 +78,7 @@ export class FooterComponent implements OnInit { return; } task.addComment( - `**Automated Message:** Task "${task.definition.abbreviation} ${task.definition.name}" will be graded during portfolio assessment only. You must now submit it directly for portfolio assessment before the portfolio deadline.`, + `**Automated Message:** Task "${task.definition.abbreviation} ${task.definition.name}" will be graded during portfolio assessment only. You can keep submitting it for feedback before the task deadline, but you must still submit it directly for portfolio assessment before the portfolio deadline.`, ); setTimeout(() => { task.updateTaskStatus('working_on_it'); From e59e56f290d457ebb5c5c577128aeb9001942269 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:49:33 +1000 Subject: [PATCH 580/776] chore: remove sentence regarding impact of late submission --- .../directives/task-due-card/task-due-card.component.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.html index 83bc582179..3f3d5c6c6b 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.html @@ -133,8 +133,7 @@ You should have completed this task by {{ task?.localDueDateString() }}. This task is now past the deadline and can only be submitted directly for your - portfolio without feedback. You should consult with the unit assessment details to - determine the impact of failing to complete this task within the allocated time. + portfolio without feedback. } @else { You should have completed this task by {{ task?.localDueDateString() }} Date: Wed, 24 Sep 2025 14:52:52 +1000 Subject: [PATCH 581/776] chore(release): 10.0.0-46 --- CHANGELOG.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 755a8eecd9..a02838d827 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-46](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-45...v10.0.0-46) (2025-09-24) + ## [10.0.0-45](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-44...v10.0.0-45) (2025-09-22) diff --git a/package-lock.json b/package-lock.json index a8b02747b3..199a950d63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-45", + "version": "10.0.0-46", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-45", + "version": "10.0.0-46", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 1f9c61185e..12ccc6d73b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-45", + "version": "10.0.0-46", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From e122a6464b89ee8a5f2a17b2ff4c8e0bd5947fe7 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:08:12 +1000 Subject: [PATCH 582/776] chore: require confirmation for assess in portfolio only option --- .../task-definition-options.component.html | 6 ++++- .../task-definition-options.component.ts | 27 ++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-options/task-definition-options.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-options/task-definition-options.component.html index b846a681a5..6ef203ae27 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-options/task-definition-options.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-options/task-definition-options.component.html @@ -10,7 +10,11 @@
- Assess in Portfolio Only diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-options/task-definition-options.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-options/task-definition-options.component.ts index 7fc62fa813..80a26895c1 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-options/task-definition-options.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-options/task-definition-options.component.ts @@ -1,6 +1,7 @@ -import { Component, Input } from '@angular/core'; -import { TaskDefinition } from 'src/app/api/models/task-definition'; -import { Unit } from 'src/app/api/models/unit'; +import {Component, Input} from '@angular/core'; +import {TaskDefinition} from 'src/app/api/models/task-definition'; +import {Unit} from 'src/app/api/models/unit'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; @Component({ selector: 'f-task-definition-options', @@ -9,8 +10,28 @@ import { Unit } from 'src/app/api/models/unit'; }) export class TaskDefinitionOptionsComponent { @Input() taskDefinition: TaskDefinition; + constructor(private confirmationModal: ConfirmationModalService) {} public get unit(): Unit { return this.taskDefinition?.unit; } + + public onToggleAssessInPortfolioOnly() { + if (!this.taskDefinition.assessInPortfolioOnly) { + return; + } + + setTimeout(() => { + this.taskDefinition.assessInPortfolioOnly = false; + console.log(this.taskDefinition.assessInPortfolioOnly); + + this.confirmationModal.show( + `Enable Assess in Portfolio Only?`, + `Enabling Assess in Portfolio Only will update all overdue tasks for ${this.taskDefinition.name} to the Assess in Portfolio state`, + () => { + this.taskDefinition.assessInPortfolioOnly = true; + }, + ); + }); + } } From db1ad9b8b6bde544d1e1dfc9cbe3fe3b5c79fff5 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Thu, 25 Sep 2025 12:42:34 +1000 Subject: [PATCH 583/776] refactor: migrate unit details editor (#1007) * refactor: init unit details editor migration * refactor: migrate unit details editor * refactor: migrate assess in portfolio confirmation * refactor: display teaching period dates when selected * refactor: add draft learning summary hint * chore: remove old unit details editor files * chore: remove debug logs --- src/app/doubtfire-angular.module.ts | 2 + src/app/doubtfire-angularjs.module.ts | 7 +- .../states/edit/directives/directives.coffee | 1 - .../unit-details-editor.coffee | 112 ---- .../unit-details-editor.component.html | 212 ++++++++ .../unit-details-editor.component.scss | 0 .../unit-details-editor.component.ts | 109 ++++ .../unit-details-editor.tpl.html | 501 ------------------ src/app/units/states/edit/edit.tpl.html | 2 +- 9 files changed, 330 insertions(+), 616 deletions(-) delete mode 100644 src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.coffee create mode 100644 src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.html create mode 100644 src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.scss create mode 100644 src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.ts delete mode 100644 src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.tpl.html diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 0e370154ff..cfea5a77ad 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -288,6 +288,7 @@ import {TaskPrerequisiteService} from './api/services/task-prerequisite.service' // import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; import {UnitStaffEditorComponent} from './units/states/edit/directives/unit-staff-editor/unit-staff-editor.component'; import {GroupSetSelectorComponent} from './groups/group-set-selector/group-set-selector.component'; +import {UnitDetailsEditorComponent} from './units/states/edit/directives/unit-details-editor/unit-details-editor.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 = { @@ -438,6 +439,7 @@ const MY_DATE_FORMAT = { TaskPrerequisitesCardComponent, UnitStaffEditorComponent, GroupSetSelectorComponent, + UnitDetailsEditorComponent, ], // Services we provide providers: [ diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 223544d7bf..abf5a9ff2f 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -100,7 +100,6 @@ import 'build/src/app/units/states/portfolios/portfolios.js'; import 'build/src/app/units/states/groups/groups.js'; import 'build/src/app/units/states/states.js'; import 'build/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.js'; -import 'build/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.js'; import 'build/src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.js'; import 'build/src/app/units/states/edit/directives/directives.js'; import 'build/src/app/units/states/edit/edit.js'; @@ -230,6 +229,7 @@ import {TaskPrerequisitesCardComponent} from './projects/states/dashboard/direct // import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; import {UnitStaffEditorComponent} from './units/states/edit/directives/unit-staff-editor/unit-staff-editor.component'; import {GroupSetSelectorComponent} from './groups/group-set-selector/group-set-selector.component'; +import {UnitDetailsEditorComponent} from './units/states/edit/directives/unit-details-editor/unit-details-editor.component'; export const DoubtfireAngularJSModule = angular .module('doubtfire', [ @@ -520,6 +520,11 @@ DoubtfireAngularJSModule.directive( downgradeComponent({component: TaskPrerequisitesCardComponent}), ); +DoubtfireAngularJSModule.directive( + 'unitDetailsEditor', + downgradeComponent({component: UnitDetailsEditorComponent}), +); + // Global configuration // If the user enters a URL that doesn't match any known URL (state), send them to `/home` diff --git a/src/app/units/states/edit/directives/directives.coffee b/src/app/units/states/edit/directives/directives.coffee index fe06bf4510..4da41cacfb 100644 --- a/src/app/units/states/edit/directives/directives.coffee +++ b/src/app/units/states/edit/directives/directives.coffee @@ -1,5 +1,4 @@ angular.module('doubtfire.units.states.edit.directives', [ - 'doubtfire.units.states.edit.directives.unit-details-editor' 'doubtfire.units.states.edit.directives.unit-group-set-editor' 'doubtfire.units.states.edit.directives.unit-ilo-editor' ]) diff --git a/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.coffee b/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.coffee deleted file mode 100644 index 8049804f32..0000000000 --- a/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.coffee +++ /dev/null @@ -1,112 +0,0 @@ -angular.module('doubtfire.units.states.edit.directives.unit-details-editor', []) - -# -# Editor for the basic details of a unit, such as the name, code -# start and end dates etc. -# -.directive('unitDetailsEditor', -> - replace: true - restrict: 'E' - templateUrl: 'units/states/edit/directives/unit-details-editor/unit-details-editor.tpl.html' - controller: ($scope, $timeout, $state, $rootScope, DoubtfireConstants, newUnitService, alertService, newTeachingPeriodService, TaskSubmission, D2lUnitDetailsModal, ConfirmationModal) -> - $scope.overseerEnabled = DoubtfireConstants.IsOverseerEnabled - - $scope.calOptions = { - startOpened: false - endOpened: false - portfolioAutoGenerationOpened: false - } - - # Get docker images available for automated task assessment for the unit. - TaskSubmission.getDockerImagesAsPromise().then (images) -> - $scope.dockerImages = images - - # Get the confugurable, external name of Doubtfire - $scope.externalName = DoubtfireConstants.ExternalName - - # get the teaching periods- gets an object with the loaded teaching periods - newTeachingPeriodService.query().subscribe((periods) -> - $scope.teachingPeriods = periods - $scope.teachingPeriodValues = [{value: undefined, text: "None"}] - other = _.map periods, (p) -> {value: p, text: "#{p.year} #{p.period}"} - _.each other, (d) -> $scope.teachingPeriodValues.push(d) - ) - - $scope.teachingPeriodSelected = ($event) -> - $scope.unit.teachingPeriod = $event - - $scope.unit.taskDefinitionCache.values.subscribe( - (taskDefs) -> - $scope.taskDefinitionValues = [{value: undefined, text: "None"}] - other = _.map taskDefs, (td) -> {value: td, text: "#{td.abbreviation}-#{td.name}"} - _.each other, (d) -> $scope.taskDefinitionValues.push(d) - ) - - $scope.draftTaskDefSelected = ($event) -> - $scope.unit.draftTaskDefinition = $event - - # Datepicker opener - $scope.open = ($event, pickerData) -> - $event.preventDefault() - $event.stopPropagation() - - if pickerData == 'start' - $scope.calOptions.startOpened = ! $scope.calOptions.startOpened - $scope.calOptions.endOpened = false - $scope.calOptions.portfolioAutoGenerationOpened = false - else if pickerData == 'end' - $scope.calOptions.startOpened = false - $scope.calOptions.endOpened = ! $scope.calOptions.endOpened - $scope.calOptions.portfolioAutoGenerationOpened = false - else if pickerData == 'autogen' - $scope.calOptions.startOpened = false - $scope.calOptions.endOpened = false - $scope.calOptions.portfolioAutoGenerationOpened = ! $scope.calOptions.portfolioAutoGenerationOpened - - $scope.dateOptions = { - formatYear: 'yy', - startingDay: 1 - } - $scope.studentSearch = "" - - $scope.saveUnit = -> - newUnitService.update($scope.unit).subscribe({ - next: (unit) -> - alertService.success( "Unit updated.", 2000) - error: (response) -> - alertService.error( "Failed to update unit. #{response}", 6000) - }) - - $scope.addD2lData = -> - D2lUnitDetailsModal.open($scope.unit) - - $scope.d2lEnabled = -> - DoubtfireConstants.IsD2LEnabled.value - - updatingAssessInPortfolio = false - $scope.$watch 'unit.markLateSubmissionsAsAssessInPortfolio', (newVal, oldVal) -> - return if newVal is oldVal or newVal == false or updatingAssessInPortfolio - updatingAssessInPortfolio = true - $scope.unit.markLateSubmissionsAsAssessInPortfolio = false - modal = ConfirmationModal.show( - 'Enable Assess in Portfolio?', - """ - Are you sure you want to enable "Assess in Portfolio" for late submissions? - This will update any existing Time/Feedback Exceeded tasks to the "Assess in Portfolio" state. - You will not be able to disable this setting while any tasks remain in the "Assess in Portfolio" state. - """ - () -> - $scope.unit.markLateSubmissionsAsAssessInPortfolio = true - $timeout -> - updatingAssessInPortfolio = false - ) - - modal.afterClosed().subscribe(() -> - $timeout -> - updatingAssessInPortfolio = false - ) -) - - - - diff --git a/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.html b/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.html new file mode 100644 index 0000000000..7ee30534e9 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.html @@ -0,0 +1,212 @@ +
+
+

Unit Details

+

Edit and configure all details and settings for this unit.

+
+ + + Code + + + + + Name + + + + + Description + + + + + + + Teaching Period + + None + @for (period of teachingPeriods; track period) { + {{ period.name }} + } + + + +
+ @if (!unit.teachingPeriod) { + + Start Date + + + + + + End Date + + + + + } @else { + + {{ unit.teachingPeriod.name }} Start Date + + + + {{ unit.teachingPeriod.name }} End Date + + + } +
+ + + Portfolio Auto-Generation Date + + + + + + + Draft Learning Summary + + None + @for (td of taskDefinitions; track td) { + {{ td.abbreviation }} - {{ td.name }} + } + + + When a draft learning summary task is selected, this will ensure a students uploaded draft + is automatically added to the students portfolio. + + + + + Extension duration onresubmit + + + When tutors request resubmission of a task, this setting determines how many weeks the task + will be extended to allow students to fix and resubmit their work. + + +
+ + +
+ Allow flexible dates +

Allows students to set planned due dates, without using extensions.

+
+ +
+ Allow student extensions +

When false only staff can request extensions on behalf of students.

+
+ +
+ Auto apply extensions +

+ When true, extensions will be automatically applied when they result in a date that is + between the task's due date and deadline. +

+
+ +
+ Has tasks assessed in portfolio +

+ When enabled, late submissions will not be automatically marked as "Time Exceeded" or + "Feedback Exceeded", and will instead appear as "Assess in Portfolio". Tutors can still sign + off tasks as complete unless the task definition has the "Assess in Portfolio Only" option + enabled. +

+
+ +
+ Allow students to change tutorial +

When false only staff can change student tutorials.

+
+ +
+ Send notification emails +

+ When true, emails will be set to students each week to indicate progress and suggest future + tasks for them to work on. +

+
+ +
+ Synchronise enrolments +

+ When true student enrolments will be synchronised with other systems where this is possible. +

+
+ +
+ Synchronise timetable +

+ When true timetable data will be synchronised with other systems where this is possible. +

+
+ +
+ Active +

Set to false to hide unit from students and tutors.

+
+
+ + @if (overseerEnabled.value) { + +
+ Overseer assessment +

If true, unit tasks will be able to make use of Overseer automated checking.

+
+ + + Overseer Docker Image + + @for (image of dockerImages; track image) { + {{ image.description }} + } + + Use this to select the default container used to check tasks with Overseer. + +
+ } + +
+ + + +
+
diff --git a/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.scss b/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.ts b/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.ts new file mode 100644 index 0000000000..0ea300bf8e --- /dev/null +++ b/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.ts @@ -0,0 +1,109 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {MatSlideToggleChange} from '@angular/material/slide-toggle'; +import {OverseerImage, UnitService} from 'src/app/api/models/doubtfire-model'; +import {TaskDefinition} from 'src/app/api/models/task-definition'; +import {TeachingPeriod} from 'src/app/api/models/teaching-period'; +import {Unit} from 'src/app/api/models/unit'; +import {TaskDefinitionService} from 'src/app/api/services/task-definition.service'; +import {TeachingPeriodService} from 'src/app/api/services/teaching-period.service'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {TaskSubmissionService} from 'src/app/common/services/task-submission.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {D2lUnitDetailsModal} from './d2l-details-form/d2l-unit-details-form.component'; + +@Component({ + selector: 'f-unit-details-editor', + templateUrl: 'unit-details-editor.component.html', + styleUrls: ['unit-details-editor.component.scss'], +}) +export class UnitDetailsEditorComponent implements OnInit { + @Input() unit: Unit; + + constructor( + private teachingPeriodService: TeachingPeriodService, + private taskDefinitionService: TaskDefinitionService, + private doubtfireConstants: DoubtfireConstants, + private taskSubmissionService: TaskSubmissionService, + private d2lUnitDetailsModal: D2lUnitDetailsModal, + private unitService: UnitService, + private alertsService: AlertService, + private confirmationModal: ConfirmationModalService, + ) {} + + public teachingPeriods: TeachingPeriod[]; + public taskDefinitions: TaskDefinition[]; + public dockerImages: OverseerImage[]; + + public get overseerEnabled() { + return this.doubtfireConstants.IsOverseerEnabled; + } + + public get d2lEnabled() { + return this.doubtfireConstants.IsD2LEnabled; + } + + ngOnInit(): void { + this.teachingPeriodService.query().subscribe((periods) => { + this.teachingPeriods = periods; + }); + + this.unit.taskDefinitionCache.values.subscribe((taskDefs) => { + this.taskDefinitions = taskDefs; + }); + + this.taskSubmissionService.getDockerImagesAsPromise().then((images) => { + this.dockerImages = images; + }); + } + + addD2lData() { + this.d2lUnitDetailsModal.open(this.unit); + } + + saveUnit() { + this.unitService.update(this.unit).subscribe({ + next: (_unit) => { + this.alertsService.success('Unit updated.', 2000); + }, + error: (response) => { + this.alertsService.error(`Failed to update unit. ${response}`, 6000); + }, + }); + } + + private updatingAssessInPortfolio: boolean = false; + + onToggleAssessInPortfolio(event: MatSlideToggleChange) { + if (!event.checked || this.updatingAssessInPortfolio) { + return false; + } + + if (this.updatingAssessInPortfolio) { + return; + } + + this.updatingAssessInPortfolio = true; + + setTimeout(() => { + this.unit.markLateSubmissionsAsAssessInPortfolio = false; + const modal = this.confirmationModal.show( + 'Enable Assess in Portfolio?', + `Are you sure you want to enable "Assess in Portfolio" for late submissions? + This will update any existing Time/Feedback Exceeded tasks to the "Assess in Portfolio" state. + You will not be able to disable this setting while any tasks remain in the "Assess in Portfolio" state.`, + () => { + this.unit.markLateSubmissionsAsAssessInPortfolio = true; + setTimeout(() => { + this.updatingAssessInPortfolio = false; + }); + }, + ); + modal.afterClosed().subscribe(() => { + setTimeout(() => { + this.updatingAssessInPortfolio = false; + }); + }); + }); + } +} diff --git a/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.tpl.html b/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.tpl.html deleted file mode 100644 index 5de919ee03..0000000000 --- a/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.tpl.html +++ /dev/null @@ -1,501 +0,0 @@ -
-
-
-

Create Unit

- Create a new unit with all overview unit details here. -
-
-

Update Unit

- Update overview details of the unit below. -
-
-
-
- -
- -
-
- - -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- - - - When a draft learning summary task is selected, this will ensure a students uploaded draft is - automatically added to the students portfolio. - -
-
- - -
- -
-
- - - - - -
-
-
- - -
- -
-
- - - - - -
-
-
- - -
- -
-
- - - - -
-
-
- - -
- -
-
- - -
- Allows students to set planned due dates, without using extensions. -
-
- - -
- -
-
- - -
- When false only staff can request extensions on behalf of students. -
-
- - -
- -
- - - When tutors request resubmission of a task, this setting determines how many weeks the task will be - extended to allow students to fix and resubmit their work. - -
-
- - -
- -
-
- - -
- - When true, extensions will be automatically applied when they result in a date that is between the task's - due date and deadline. - -
-
- - -
- -
-
- - -
- - When enabled, late submissions will not be automatically marked as "Time Exceeded" or - "Feedback Exceeded", and will instead appear as "Assess in Portfolio". Tutors can - still sign off tasks as complete unless the task definition has the "Assess in - Portfolio Only" option enabled. - -
-
- - -
- -
-
- - -
- When false only staff can change student tutorials. -
-
- - -
- -
-
- - -
- - When true, emails will be set to students each week to indicate progress and suggest future tasks for them - to work on. - -
-
- - -
- -
-
- - -
- - When true student enrolments will be synchronised with other systems where this is possible. - -
-
- - -
- -
-
- - -
- - When true timetable data will be synchronised with other systems where this is possible. - -
-
- - -
- -
-
- - -
- - If true, unit tasks will be able to make use of Overseer automated checking. - -
-
- -
- -
- -
-
- -
-
diff --git a/src/app/units/states/edit/edit.tpl.html b/src/app/units/states/edit/edit.tpl.html index 4df8bf3d09..8a0dc37d52 100644 --- a/src/app/units/states/edit/edit.tpl.html +++ b/src/app/units/states/edit/edit.tpl.html @@ -4,7 +4,7 @@ {{tab.title}} - + From 04d3178e2e37a64c14109deaacb188056e07f225 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:13:29 +1000 Subject: [PATCH 584/776] chore(release): 10.0.0-47 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a02838d827..ae329af052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-47](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-46...v10.0.0-47) (2025-09-25) + + +### Features + +* add observer only ui ([bfbcee9](https://github.com/b0ink/doubtfire-deploy/commit/bfbcee98cfa4888241c5956372d004ec1cd813cb)) + ## [10.0.0-46](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-45...v10.0.0-46) (2025-09-24) ## [10.0.0-45](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-44...v10.0.0-45) (2025-09-22) diff --git a/package-lock.json b/package-lock.json index 199a950d63..4b1491ce8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-46", + "version": "10.0.0-47", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-46", + "version": "10.0.0-47", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 12ccc6d73b..056ce41ad1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-46", + "version": "10.0.0-47", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 10e8ab033469c2dd4e78d125cb8fcfe0127cbf88 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:55:31 +1000 Subject: [PATCH 585/776] refactor: migrate portfolio grade select step (#1014) * Frontend migration of portfolio-grade-select-step to Angular 17 component * Addressed review comments in portfolio-grade-select-step component * Implement OnInit in PortfolioGradeSelectStepComponent * replaced bootsrap UI component with Angular material * chore: bring back deleted lines * refactor: replace css with tailwind * chore: revert import order * chore: revert import order * chore: revert import order * chore: revert order * refactor: simplify grade changing --------- Co-authored-by: Pasindu Fernando <116358471+Pasindufdo98@users.noreply.github.com> --- src/app/doubtfire-angular.module.ts | 7 +- src/app/doubtfire-angularjs.module.ts | 7 +- .../portfolio/directives/directives.coffee | 1 - .../portfolio-grade-select-step.coffee | 19 ---- ...portfolio-grade-select-step.component.html | 96 +++++++++++++++++++ ...portfolio-grade-select-step.component.scss | 0 .../portfolio-grade-select-step.component.ts | 57 +++++++++++ .../portfolio-grade-select-step.scss | 10 -- .../portfolio-grade-select-step.tpl.html | 60 ------------ .../states/portfolio/portfolio.tpl.html | 6 +- 10 files changed, 168 insertions(+), 95 deletions(-) delete mode 100644 src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee create mode 100644 src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html create mode 100644 src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.scss create mode 100644 src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.ts delete mode 100644 src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.scss delete mode 100644 src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index cfea5a77ad..369039dfbc 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -283,12 +283,12 @@ import {LtiService} from './api/services/lti.service'; import {TaskDefinitionPrerequisitesComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-prerequisites/task-definition-prerequisites.component'; import {TaskPrerequisitesCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-prerequisites-card/task-prerequisites-card.component'; import {TaskPrerequisiteService} from './api/services/task-prerequisite.service'; -// 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 {GradeTaskModalComponent} from './tasks/modals/grade-task-modal/grade-task-modal.component'; +// import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; import {UnitStaffEditorComponent} from './units/states/edit/directives/unit-staff-editor/unit-staff-editor.component'; import {GroupSetSelectorComponent} from './groups/group-set-selector/group-set-selector.component'; import {UnitDetailsEditorComponent} from './units/states/edit/directives/unit-details-editor/unit-details-editor.component'; +import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.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 = { @@ -440,6 +440,7 @@ const MY_DATE_FORMAT = { UnitStaffEditorComponent, GroupSetSelectorComponent, UnitDetailsEditorComponent, + PortfolioGradeSelectStepComponent, ], // Services we provide providers: [ diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index abf5a9ff2f..963bf7d209 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -72,7 +72,6 @@ import 'build/src/app/projects/states/outcomes/outcomes.js'; import 'build/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.js'; import 'build/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.js'; import 'build/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.js'; -import 'build/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.js'; import 'build/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.js'; import 'build/src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.js'; import 'build/src/app/projects/states/portfolio/directives/directives.js'; @@ -230,6 +229,7 @@ import {TaskPrerequisitesCardComponent} from './projects/states/dashboard/direct import {UnitStaffEditorComponent} from './units/states/edit/directives/unit-staff-editor/unit-staff-editor.component'; import {GroupSetSelectorComponent} from './groups/group-set-selector/group-set-selector.component'; import {UnitDetailsEditorComponent} from './units/states/edit/directives/unit-details-editor/unit-details-editor.component'; +import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component'; export const DoubtfireAngularJSModule = angular .module('doubtfire', [ @@ -525,6 +525,11 @@ DoubtfireAngularJSModule.directive( downgradeComponent({component: UnitDetailsEditorComponent}), ); +DoubtfireAngularJSModule.directive( + 'fPortfolioGradeSelectStep', + downgradeComponent({component: PortfolioGradeSelectStepComponent}), +); + // Global configuration // If the user enters a URL that doesn't match any known URL (state), send them to `/home` diff --git a/src/app/projects/states/portfolio/directives/directives.coffee b/src/app/projects/states/portfolio/directives/directives.coffee index 8ce9be0be3..661acd16e7 100644 --- a/src/app/projects/states/portfolio/directives/directives.coffee +++ b/src/app/projects/states/portfolio/directives/directives.coffee @@ -1,6 +1,5 @@ angular.module('doubtfire.projects.states.portfolio.directives', [ 'doubtfire.projects.states.portfolio.directives.portfolio-add-extra-files-step' - 'doubtfire.projects.states.portfolio.directives.portfolio-grade-select-step' 'doubtfire.projects.states.portfolio.directives.portfolio-learning-summary-report-step' 'doubtfire.projects.states.portfolio.directives.portfolio-review-step' 'doubtfire.projects.states.portfolio.directives.portfolio-tasks-step' diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee deleted file mode 100644 index d29d0f65da..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee +++ /dev/null @@ -1,19 +0,0 @@ -angular.module('doubtfire.projects.states.portfolio.directives.portfolio-grade-select-step', []) - -# -# Allows students to select the target grade they are hoping -# to achieve with their portfolio -# -.directive('portfolioGradeSelectStep', -> - restrict: 'E' - replace: true - templateUrl: 'projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html' - controller: ($scope, newProjectService, gradeService) -> - $scope.grades = gradeService.grades - $scope.agreedToAssessmentCriteria = $scope.projectHasLearningSummaryReport() - $scope.chooseGrade = (idx) -> - $scope.project.submittedGrade = idx - newProjectService.update($scope.project).subscribe((project) -> - $scope.project.refreshBurndownChartData() - ) -) diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html new file mode 100644 index 0000000000..63385d6e5d --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html @@ -0,0 +1,96 @@ +
+ + + + +

Select Grade

+
+
+ + +

+ In preparing your portfolio, you need to undertake a self-assessment. Use the unit's + assessment criteria to determine the grade your portfolio should be awarded. +

+ + + + + + warning + Read the assessment criteria + + + + +

+ Make sure that you have reviewed the Assessment Criteria for the grade you are applying + for. Each grade will have a list of criteria that you can use to determine if you meet + the requirements to achieve that grade. +

+
+ + + + I have read the Assessment Criteria for this unit + + +
+ + + + @if (agreedToAssessmentCriteria) { + + + + Grade Application + + + + +

+ Select the grade you are applying for {{ unit.code }} + {{ unit.name }} below. +

+
+ + + + @for (grade of gradeValues; track grade) { + + + + } + + +

+ Make sure your Learning Summary Report justifies how your portfolio + demonstrates you have + met all unit learning outcomes to a {{ targetGrade }} level +

+
+
+ } +
+ + + + + + +
+
diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.scss b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.ts b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.ts new file mode 100644 index 0000000000..cce8ee1748 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.ts @@ -0,0 +1,57 @@ +import {Component, Injector, Input} from '@angular/core'; +import {Project, Unit} from 'src/app/api/models/doubtfire-model'; +import {ProjectService} from 'src/app/api/services/project.service'; +import {GradeService} from 'src/app/common/services/grade.service'; + +@Component({ + selector: 'f-portfolio-grade-select-step', + templateUrl: 'portfolio-grade-select-step.component.html', + styleUrls: ['portfolio-grade-select-step.component.scss'], +}) +export class PortfolioGradeSelectStepComponent { + @Input() project: Project; + @Input() unit: Unit; + + public agreedToAssessmentCriteria: boolean = false; + + constructor( + private gradeService: GradeService, + private injector: Injector, + private projectService: ProjectService, + ) { + this.$scope = this.injector.get('$scope'); + } + + public get gradeValues() { + return this.gradeService.gradeValues; + } + + updateSubmittedGrade(newGrade: number): void { + const previousSubmittedGrade = this.project.submittedGrade; + this.project.submittedGrade = newGrade; + + this.projectService.update(this.project).subscribe( + (project) => { + project.refreshBurndownChartData?.(); + }, + (error) => { + this.project.submittedGrade = previousSubmittedGrade; + console.error('Error updating target grade:', error); + }, + ); + } + + // TODO: remove this once parent component has been migrated + private $scope: any; + goToNextStep(): void { + if (typeof this.$scope?.advanceActiveTab === 'function') { + this.$scope.advanceActiveTab(1); + } + } + + goToPreviousStep(): void { + if (typeof this.$scope?.advanceActiveTab === 'function') { + this.$scope.advanceActiveTab(-1); + } + } +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.scss b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.scss deleted file mode 100644 index bfc229c4b1..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.scss +++ /dev/null @@ -1,10 +0,0 @@ -.project-portfolio-wizard .portfolio-grade-select-step { - .confirm-read-assessment-criteria { - font-size: 1.2em; - } - .select-the-grade { - .btn { - padding: 1em; - } - } -} diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html deleted file mode 100644 index 5e512edf25..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html +++ /dev/null @@ -1,60 +0,0 @@ -
-
-

Select Grade

-
-
-

- In preparing your portfolio, you need to undertake a self assessment. Use the unit's assessment criteria to - determine the grade your portfolio should be awarded. -

-
-
-

Read the assessment criteria

- Make sure that you have reviewed the Assessment Criteria for the grade you are applying for. Each grade will - have a list of criteria that you can use to determine if you meet the requirements to achieve that grade. -
-
- - -
-
- -
-
-

Grade Application

- Select the grade you are applying for {{unit.name}} below. -
-
-
- -
-

- Make sure your Learning Summary Report justifies how your portfolio demonstrates you have - met all unit learning outcomes to a {{targetGrade}} level -

-
-
- -
- - -
diff --git a/src/app/projects/states/portfolio/portfolio.tpl.html b/src/app/projects/states/portfolio/portfolio.tpl.html index be61edec29..934f90e76f 100644 --- a/src/app/projects/states/portfolio/portfolio.tpl.html +++ b/src/app/projects/states/portfolio/portfolio.tpl.html @@ -7,7 +7,11 @@ - + + From 8d36c94f03563a0924cfeb7d2d9e8dd911b65960 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 8 Oct 2025 09:22:49 +1100 Subject: [PATCH 586/776] fix: add assess in portfolio tasks as completed tasks in burndown chart --- src/app/api/models/project.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/models/project.ts b/src/app/api/models/project.ts index 167aeb1ec5..1064fb3c5b 100644 --- a/src/app/api/models/project.ts +++ b/src/app/api/models/project.ts @@ -413,7 +413,7 @@ export class Project extends Entity { const tasks = this.tasks; const readyOrCompleteTasks = tasks.filter((task) => - ['ready_for_feedback', 'discuss', 'demonstrate', 'complete'].includes(task.status), + ['ready_for_feedback', 'discuss', 'demonstrate', 'complete', 'assess_in_portfolio'].includes(task.status), ); let lastTargetDate: Date; From 2b4afe24e3f8139fec23337facc7656eec9a9d3a Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:21:09 +1100 Subject: [PATCH 587/776] feat: add tutor times summary download (#1015) * feat: add tutor times summary download * fix: typo * feat: add calendar view of marking sessions * refactor: simplify date in filename * refactor: remove get tutors call * refactor: custom number of days visible, show today at the end * refactor: date pickers to filter sessions * refactor: clean up ui * refactor: create tutor times analytics component * refactor: add loading spinner * refactor: fix timezone issues * refactor: improve ui * chore: update endpoint * feat: toggle hide sessions during tutorials * chore: remove button * refactor: implement marking session entity * chore: remove unused code * chore: remove comment --- package-lock.json | 64 +++++ package.json | 1 + src/app/api/models/marking-session.ts | 40 +++ src/app/api/models/unit.ts | 59 +++- .../api/services/marking-session.service.ts | 39 +++ src/app/doubtfire-angular.module.ts | 7 + .../analytics-tutor-times.component.html | 97 +++++++ .../analytics-tutor-times.component.scss | 0 .../analytics-tutor-times.component.ts | 253 ++++++++++++++++++ .../unit-analytics-route.component.html | 34 ++- .../unit-analytics-route.component.ts | 11 +- src/styles.scss | 6 + 12 files changed, 596 insertions(+), 15 deletions(-) create mode 100644 src/app/api/models/marking-session.ts create mode 100644 src/app/api/services/marking-session.service.ts create mode 100644 src/app/units/states/analytics/directives/analytics-tutor-times.component.html create mode 100644 src/app/units/states/analytics/directives/analytics-tutor-times.component.scss create mode 100644 src/app/units/states/analytics/directives/analytics-tutor-times.component.ts diff --git a/package-lock.json b/package-lock.json index 4b1491ce8c..cd419cdb8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@uirouter/core": "^6.1.0", "@uirouter/rx": "^1.0.0", "angular": "1.5.11", + "angular-calendar": "^0.31.1", "angular-filter": "0.5.17", "angular-markdown-filter": "1.3.2", "angular-md5": "0.1.10", @@ -4632,6 +4633,12 @@ "tslib": "^2.1.0" } }, + "node_modules/@mattlewis92/dom-autoscroller": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@mattlewis92/dom-autoscroller/-/dom-autoscroller-2.4.2.tgz", + "integrity": "sha512-YbrUWREPGEjE/FU6foXcAT1YbVwqD/jkYnY1dFb0o4AxtP3s4xKBthlELjndZih8uwsDWgQZx1eNskRNe2BgZQ==", + "license": "MIT" + }, "node_modules/@ngneat/hotkeys": { "version": "4.0.0", "license": "MIT", @@ -6410,6 +6417,39 @@ "version": "1.5.11", "license": "MIT" }, + "node_modules/angular-calendar": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/angular-calendar/-/angular-calendar-0.31.1.tgz", + "integrity": "sha512-pjSIpoAaUzS/gx+14eOr4hPZhlQ8HxpiZypCSGqJNptq5PD+vOdVQ3h/Aaqnk86GraVcAQPXqfu64MtdKwTVNw==", + "license": "MIT", + "dependencies": { + "@scarf/scarf": "^1.1.1", + "angular-draggable-droppable": "^8.0.0", + "angular-resizable-element": "^7.0.0", + "calendar-utils": "^0.10.4", + "positioning": "^2.0.1", + "tslib": "^2.4.1" + }, + "funding": { + "url": "https://github.com/sponsors/mattlewis92" + }, + "peerDependencies": { + "@angular/core": ">=15.0.0" + } + }, + "node_modules/angular-draggable-droppable": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/angular-draggable-droppable/-/angular-draggable-droppable-8.0.0.tgz", + "integrity": "sha512-+gpSNBbygjV1pxTxsM3UPJKcXHXJabYoTtKcgQe74rGnb1umKc07XCBD1qDzvlG/kocthvhQ12qfYOYzHnE3ZA==", + "license": "MIT", + "dependencies": { + "@mattlewis92/dom-autoscroller": "^2.4.2", + "tslib": "^2.4.1" + }, + "peerDependencies": { + "@angular/core": ">=15.0.0" + } + }, "node_modules/angular-filter": { "version": "0.5.17", "license": "MIT", @@ -6443,6 +6483,18 @@ "nvd3": "^1.7.1" } }, + "node_modules/angular-resizable-element": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/angular-resizable-element/-/angular-resizable-element-7.0.2.tgz", + "integrity": "sha512-/BGuNiA38n9klexHO1xgnsA3VYigj9v+jUGjKtBRgfB26bCxZKsNWParSu2k3EqbATrfAJC4Nl8f7cORpJFf4w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=15.0.0" + } + }, "node_modules/angular-resource": { "version": "1.5.11", "license": "MIT" @@ -7573,6 +7625,12 @@ "node": ">=0.10.0" } }, + "node_modules/calendar-utils": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/calendar-utils/-/calendar-utils-0.10.4.tgz", + "integrity": "sha512-gBK4xCJ42yjaUKwuUha6cZOfxAmGzvSgbdAaX3xLRioeKbYoOK1x1qeD6dch72rsMZlTgATPbBBx42bnkStqgQ==", + "license": "MIT" + }, "node_modules/call-bind": { "version": "1.0.7", "license": "MIT", @@ -17668,6 +17726,12 @@ "npm": ">=1.0.0" } }, + "node_modules/positioning": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/positioning/-/positioning-2.0.1.tgz", + "integrity": "sha512-DsAgM42kV/ObuwlRpAzDTjH9E8fGKkMDJHWFX+kfNXSxh7UCCQxEmdjv/Ws5Ft1XDnt3JT8fIDYeKNSE2TbttA==", + "license": "MIT" + }, "node_modules/posix-character-classes": { "version": "0.1.1", "dev": true, diff --git a/package.json b/package.json index 056ce41ad1..f9eb667eef 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@uirouter/core": "^6.1.0", "@uirouter/rx": "^1.0.0", "angular": "1.5.11", + "angular-calendar": "^0.31.1", "angular-filter": "0.5.17", "angular-markdown-filter": "1.3.2", "angular-md5": "0.1.10", diff --git a/src/app/api/models/marking-session.ts b/src/app/api/models/marking-session.ts new file mode 100644 index 0000000000..8bfbe3a446 --- /dev/null +++ b/src/app/api/models/marking-session.ts @@ -0,0 +1,40 @@ +import {Entity, EntityMapping} from 'ngx-entity-service'; +import {User} from './doubtfire-model'; +import {Unit} from './unit'; + +export class MarkingSession extends Entity { + id: number; + + // Marking tutor + user: User; + + unit: Unit; + + startTime: Date; + endTime: Date; + duringTutorial: boolean; + durationMinutes: number; + + // Aggregated session activities count + commentsAdded: number; + assessments: number; + submissionsOpened: number; + + constructor(data?: Unit) { + super(); + if (data) { + this.unit = data; + } else { + console.error('Failed to get unit'); + } + } + + public override toJson( + mappingData: EntityMapping, + ignoreKeys?: string[], + ): object { + return { + markingSession: super.toJson(mappingData, ignoreKeys), + }; + } +} diff --git a/src/app/api/models/unit.ts b/src/app/api/models/unit.ts index 6f4a2b0622..b144a53bb3 100644 --- a/src/app/api/models/unit.ts +++ b/src/app/api/models/unit.ts @@ -30,8 +30,10 @@ import {LearningOutcome} from './learning-outcome'; import {AlertService} from 'src/app/common/services/alert.service'; import {D2lAssessmentMapping} from './d2l/d2l_assessment_mapping'; import {SidekiqJob} from './sidekiq-job'; -import {HttpClient} from '@angular/common/http'; +import {HttpClient, HttpParams} from '@angular/common/http'; import {TaskPrerequisiteService} from '../services/task-prerequisite.service'; +import {MarkingSession} from './marking-session'; +import {MarkingSessionService} from '../services/marking-session.service'; export class Unit extends Entity { id: number; @@ -603,6 +605,61 @@ export class Unit extends Entity { ); } + public getUserMarkingSessions(startDate?: Date, endDate?: Date): Observable { + let params = new HttpParams(); + if (startDate) { + params = params.set( + 'start_date', + `${startDate.getFullYear()}-${(startDate.getMonth() + 1).toString().padStart(2, '0')}-${startDate.getDate().toString().padStart(2, '0')}`, + ); + } + + if (endDate) { + params = params.set( + 'end_date', + `${endDate.getFullYear()}-${(endDate.getMonth() + 1).toString().padStart(2, '0')}-${endDate.getDate().toString().padStart(2, '0')}`, + ); + } + + // TODO: we should cache the data by the same start/end date + const markingSessionService = AppInjector.get(MarkingSessionService); + return markingSessionService.fetchAll( + { + unitId: this.id, + }, + {params, constructorParams: this}, + ); + } + + public downloadTutorTimesSummaryCsv( + startDate?: Date, + endDate?: Date, + ignoreSessionsDuringTutorials?: boolean, + ): Observable { + let params = new HttpParams(); + + if (startDate) { + params = params.set( + 'start_date', + `${startDate.getFullYear()}-${(startDate.getMonth() + 1).toString().padStart(2, '0')}-${startDate.getDate().toString().padStart(2, '0')}`, + ); + } + + if (endDate) { + params = params.set( + 'end_date', + `${endDate.getFullYear()}-${(endDate.getMonth() + 1).toString().padStart(2, '0')}-${endDate.getDate().toString().padStart(2, '0')}`, + ); + } + + params = params.set('ignore_sessions_during_tutorials', ignoreSessionsDuringTutorials ?? false); + + return AppInjector.get(HttpClient).get( + `${AppInjector.get(DoubtfireConstants).API_URL}/csv/units/${this.id}/tutor_times_summary`, + {params}, + ); + } + public hasD2lMapping(): boolean { const doubtfireConstants = AppInjector.get(DoubtfireConstants); return ( diff --git a/src/app/api/services/marking-session.service.ts b/src/app/api/services/marking-session.service.ts new file mode 100644 index 0000000000..f1ce830a02 --- /dev/null +++ b/src/app/api/services/marking-session.service.ts @@ -0,0 +1,39 @@ +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {CachedEntityService} from 'ngx-entity-service'; +import {Unit} from 'src/app/api/models/doubtfire-model'; +import API_URL from 'src/app/config/constants/apiUrl'; +import {MarkingSession} from '../models/marking-session'; + +@Injectable() +export class MarkingSessionService extends CachedEntityService { + protected readonly endpointFormat = 'units/:unitId:/marking_sessions/:id:'; + + constructor(httpClient: HttpClient) { + super(httpClient, API_URL); + + this.mapping.addKeys( + 'id', + 'startTime', + 'endTime', + 'duringTutorial', + 'durationMinutes', + + 'commentsAdded', + 'assessments', + 'submissionsOpened', + + { + keys: ['user', 'user_id'], + toEntityFn: (data: object, _key: string, markingSession: MarkingSession) => { + const userRole = markingSession.unit.staff.find((s) => s.user.id === data['user_id']); + return userRole.user; + }, + }, + ); + } + + public createInstanceFrom(json: object, other?: Unit): MarkingSession { + return new MarkingSession(other); + } +} diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 369039dfbc..47e4679234 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -111,6 +111,8 @@ import {MatDatepickerModule} from '@angular/material/datepicker'; import {DateFnsAdapter, MAT_DATE_FNS_FORMATS} from '@angular/material-date-fns-adapter'; import {enAU} from 'date-fns/locale'; +import {CalendarModule, DateAdapter as CalendarDateAdapter} from 'angular-calendar'; +import {adapterFactory} from 'angular-calendar/date-adapters/date-fns'; import {doubtfireStates} from './doubtfire.states'; import {MatTableModule} from '@angular/material/table'; @@ -289,6 +291,8 @@ import {UnitStaffEditorComponent} from './units/states/edit/directives/unit-staf import {GroupSetSelectorComponent} from './groups/group-set-selector/group-set-selector.component'; import {UnitDetailsEditorComponent} from './units/states/edit/directives/unit-details-editor/unit-details-editor.component'; import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component'; +import {AnalyticsTutorTimesComponent} from './units/states/analytics/directives/analytics-tutor-times.component'; +import {MarkingSessionService} from './api/services/marking-session.service'; // 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 = { @@ -441,6 +445,7 @@ const MY_DATE_FORMAT = { GroupSetSelectorComponent, UnitDetailsEditorComponent, PortfolioGradeSelectStepComponent, + AnalyticsTutorTimesComponent, ], // Services we provide providers: [ @@ -529,6 +534,7 @@ const MY_DATE_FORMAT = { SidekiqJobService, LtiService, TaskPrerequisiteService, + MarkingSessionService, ], imports: [ FlexLayoutModule, @@ -590,6 +596,7 @@ const MY_DATE_FORMAT = { MatDatepickerModule, MatNativeDateModule, MatDialogModuleNew, + CalendarModule.forRoot({provide: CalendarDateAdapter, useFactory: adapterFactory}), ], }) diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html new file mode 100644 index 0000000000..faef0eaab1 --- /dev/null +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html @@ -0,0 +1,97 @@ + + + +

Tutor Times Session Summary

+
+ + +
+ +
+
+ + + +
+
+ + Choose a date + + Sessions from this day onward + + + + + + Choose a date + + Sessions up to this day + + + +
+
+ Hide sessions during tutorials +
+
+ + +
+ {{ event.name }} ({{ event.duration }} minutes) + {{ event.duringTutorial ? 'T' : '' }}
+ Assessments: {{ event.assessments || 0 }}
+ Comments: {{ event.comments_added || 0 }}
+ Submissions opened: {{ event.submissions_opened || 0 }}
+ During Tutorial?: {{ event.duringTutorial ? 'yes' : 'no' }} +
+
+ +
+ @if (isLoading) { + + + } + + +
+
diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.scss b/src/app/units/states/analytics/directives/analytics-tutor-times.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts new file mode 100644 index 0000000000..19735552cc --- /dev/null +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts @@ -0,0 +1,253 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {MatDatepickerInputEvent} from '@angular/material/datepicker'; +import {CalendarEvent} from 'angular-calendar'; +import {Observable} from 'rxjs'; +import {SidekiqJob} from 'src/app/api/models/sidekiq-job'; +import {Unit} from 'src/app/api/models/unit'; +import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; +import {SidekiqProgressModalService} from 'src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.service'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-analytics-tutor-times', + templateUrl: 'analytics-tutor-times.component.html', + styleUrls: ['analytics-tutor-times.component.scss'], +}) +export class AnalyticsTutorTimesComponent implements OnInit { + @Input() unit: Unit; + + @Input() downloadCsvFn!: ( + newJob: Observable, + title: string, + filename: string, + ) => void; + + selectedUserId: number | null = null; + + viewDate = new Date(); + events = []; + filteredEvents = []; + + tutorTimeSummaryStartDate: Date; + tutorTimeSummaryEndDate: Date; + daysInWeek: number = 7; + + hideSessionsDuringTutorials: boolean = false; + + public canLoadSessions: boolean = false; + public isLoading: boolean = false; + + constructor( + private alertService: AlertService, + private sidekiqProgressModalService: SidekiqProgressModalService, + private fileDownloaderService: FileDownloaderService, + ) {} + + ngOnInit(): void { + if (!this.sidekiqProgressModalService || !this.fileDownloaderService) { + // NOTE: Our `downloadCsvFn` callback requires these services because it calls `this` context + console.error('Failed to load tutor times analytics'); + } + + this.tutorTimeSummaryEndDate = new Date(); + this.tutorTimeSummaryEndDate.setHours(0, 0, 0, 0); + + this.tutorTimeSummaryStartDate = new Date(this.tutorTimeSummaryEndDate); + this.tutorTimeSummaryStartDate.setDate(this.tutorTimeSummaryEndDate.getDate() - 7); + + const startOfWeek = new Date(this.viewDate); + startOfWeek.setDate(this.viewDate.getDate() - this.daysInWeek + 1); + + this.viewDate = startOfWeek; + + this.canLoadSessions = true; + this.getMarkingSesssions(); + } + + goPreviousWeek() { + this.canLoadSessions = true; + this.viewDate = new Date(this.viewDate.getTime() - this.daysInWeek * 24 * 60 * 60 * 1000); + } + + goNextWeek() { + this.canLoadSessions = true; + this.viewDate = new Date(this.viewDate.getTime() + this.daysInWeek * 24 * 60 * 60 * 1000); + } + + goTodayWeek() { + this.canLoadSessions = true; + + this.tutorTimeSummaryEndDate = new Date(); + this.tutorTimeSummaryStartDate = new Date( + this.tutorTimeSummaryEndDate.getTime() - 7 * 24 * 60 * 60 * 1000, + ); + this.daysInWeek = 7; + + this.viewDate = new Date(); + const startOfWeek = new Date(); + startOfWeek.setDate(this.viewDate.getDate() - this.daysInWeek + 1); + + this.viewDate = startOfWeek; + } + + public onToggleChangeHideSessionsDuringTutorial() { + setTimeout(() => { + this.applyFilters(); + }); + } + + applyFilters() { + this.filteredEvents = this.events.filter( + (e) => + (this.selectedUserId === null || e['user_id'] === this.selectedUserId) && + (!this.hideSessionsDuringTutorials || !e['duringTutorial']), + ); + } + + onDateChange(_event: MatDatepickerInputEvent) { + if (!this.tutorTimeSummaryStartDate || !this.tutorTimeSummaryEndDate) { + return; + } + + // Includes both the selected start & end days + const diffDays = + Math.floor( + (this.tutorTimeSummaryEndDate.getTime() - this.tutorTimeSummaryStartDate.getTime()) / + (1000 * 60 * 60 * 24), + ) + 1; + + if (diffDays > 366) { + this.alertService.error('You cannot select more than a year', 3000); + return; + } + console.log('diff days', diffDays); + if (diffDays < 1) { + this.tutorTimeSummaryStartDate = this.tutorTimeSummaryEndDate; + this.alertService.error('End date must be on or after the start date'); + return; + } + this.canLoadSessions = true; + this.daysInWeek = diffDays; + this.viewDate = new Date(this.tutorTimeSummaryStartDate); + } + + beforeViewRender(event): void { + console.log(event.period.start); + console.log(event.period.end); + + this.tutorTimeSummaryStartDate = event.period.start; + this.tutorTimeSummaryEndDate = event.period.end; + + this.getMarkingSesssions(); + } + + public getTutorTimesSummary() { + const start = `${this.tutorTimeSummaryStartDate.getFullYear()}-${(this.tutorTimeSummaryStartDate.getMonth() + 1).toString().padStart(2, '0')}-${this.tutorTimeSummaryStartDate.getDate().toString().padStart(2, '0')}`; + const end = `${this.tutorTimeSummaryEndDate.getFullYear()}-${(this.tutorTimeSummaryEndDate.getMonth() + 1).toString().padStart(2, '0')}-${this.tutorTimeSummaryEndDate.getDate().toString().padStart(2, '0')}`; + + this.downloadCsvFn( + this.unit.downloadTutorTimesSummaryCsv( + this.tutorTimeSummaryStartDate, + this.tutorTimeSummaryEndDate, + this.hideSessionsDuringTutorials, + ), + 'Tutor Times Summary CSV', + `${this.unit.code}-tutor-times-summary-${start}-to-${end}-${!this.hideSessionsDuringTutorials ? 'incl-tutorials' : ''}.csv`, + ); + } + + public getMarkingSesssions() { + if (!this.canLoadSessions) { + return; + } + + this.canLoadSessions = false; + this.isLoading = true; + this.unit + .getUserMarkingSessions(this.tutorTimeSummaryStartDate, this.tutorTimeSummaryEndDate) + .subscribe({ + next: (data) => { + this.isLoading = false; + this.canLoadSessions = false; + this.events = data.map((session) => { + const tutor = this.unit.staff.find((t) => t.user.id === session.user.id); + + const primary = this.stringToHexColor(tutor.user.firstName); + const secondary = this.stringToHexColor(tutor.user.firstName); + return { + start: new Date(session.startTime), + end: new Date(session.endTime), + title: `${tutor?.user.firstName} (${session.durationMinutes}m) ${session.duringTutorial ? '(T)' : ''}`, + color: {primary: secondary, secondary: primary}, + user_id: session.user.id, + comments_added: session.commentsAdded, + assessments: session.assessments, + submissions_opened: session.submissionsOpened, + duration: session.durationMinutes, + duringTutorial: session.duringTutorial, + name: tutor?.user.firstName, + }; + }); + + this.filteredEvents = this.events.filter( + (row) => this.selectedUserId === null || row['user_id'] === this.selectedUserId, + ); + + console.log(data); + }, + error: (error) => { + this.canLoadSessions = false; + + console.error(error); + }, + }); + } + + eventClicked({event}: {event: CalendarEvent}): void { + if (event['user_id'] !== undefined) { + if (this.selectedUserId === null) { + this.selectedUserId = Number(event['user_id']); + } else { + this.selectedUserId = null; + } + this.applyFilters(); + } + } + + private stringToHexColor( + name: string, + opts?: {hue?: [number, number]; sat?: [number, number]; lit?: [number, number]}, + ): string { + const options = { + hue: opts?.hue || [0, 360], + sat: opts?.sat || [40, 70], // lower saturation → softer color + lit: opts?.lit || [75, 90], // higher lightness → pastel tone + }; + + const range = (hash: number, min: number, max: number) => { + const diff = max - min; + const x = ((hash % diff) + diff) % diff; + return x + min; + }; + + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = name.charCodeAt(i) + ((hash << 5) - hash); + hash = hash & hash; + } + + const h = range(hash, options.hue[0], options.hue[1]); + const s = range(hash, options.sat[0], options.sat[1]) / 100; + const l = range(hash, options.lit[0], options.lit[1]) / 100; + + const a = s * Math.min(l, 1 - l); + const f = (n: number) => { + const k = (n + h / 30) % 12; + const color = l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); + return Math.round(255 * color); + }; + + const toHex = (c: number) => c.toString(16).padStart(2, '0'); + return `#${toHex(f(0))}${toHex(f(8))}${toHex(f(4))}`; + } +} diff --git a/src/app/units/states/analytics/unit-analytics-route.component.html b/src/app/units/states/analytics/unit-analytics-route.component.html index 361c9ce18c..9388fae8d0 100644 --- a/src/app/units/states/analytics/unit-analytics-route.component.html +++ b/src/app/units/states/analytics/unit-analytics-route.component.html @@ -1,16 +1,24 @@

Unit Statistics

-
- - - - +
+ + + + + + + + + + @if (role === 'Convenor') { + + }
diff --git a/src/app/units/states/analytics/unit-analytics-route.component.ts b/src/app/units/states/analytics/unit-analytics-route.component.ts index 12e4770d84..73419c4dfe 100644 --- a/src/app/units/states/analytics/unit-analytics-route.component.ts +++ b/src/app/units/states/analytics/unit-analytics-route.component.ts @@ -1,7 +1,10 @@ -import {Component, Input} from '@angular/core'; +import {Component, Input, OnInit} from '@angular/core'; +import {MatDatepickerInputEvent} from '@angular/material/datepicker'; +import {CalendarEvent} from 'angular-calendar'; import {Observable} from 'rxjs'; import {SidekiqJob} from 'src/app/api/models/sidekiq-job'; import {Unit} from 'src/app/api/models/unit'; +import {UserService} from 'src/app/api/services/user.service'; import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; import {SidekiqProgressModalService} from 'src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.service'; import {AlertService} from 'src/app/common/services/alert.service'; @@ -18,8 +21,14 @@ export class UnitAnalyticsComponent { private sidekiqProgressModalService: SidekiqProgressModalService, private alertsService: AlertService, private fileDownloaderService: FileDownloaderService, + private userService: UserService, + private alertService: AlertService, ) {} + get role() { + return this.unit.staff.find((s) => s.user.id === this.userService.currentUser.id)?.role; + } + public getTaskCompletionCsv() { this.downloadCsv( this.unit.downloadTaskCompletionCsv(), diff --git a/src/styles.scss b/src/styles.scss index b0756f5acd..7eec75221c 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -4,6 +4,7 @@ @include mat.core(); @import './theme.scss'; +@import '../node_modules/angular-calendar/css/angular-calendar.css'; @tailwind base; @tailwind components; @@ -35,3 +36,8 @@ } } } + +.mat-mdc-progress-spinner circle, +.mat-spinner circle { + stroke: #ddd !important; +} From 376dad3125ed6757b997ef25e5866a43cb7919d0 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:22:11 +1100 Subject: [PATCH 588/776] chore(release): 10.0.0-48 --- CHANGELOG.md | 12 ++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae329af052..f2b6a8e01e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-48](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-47...v10.0.0-48) (2025-10-12) + + +### Features + +* add tutor times summary download ([#1015](https://github.com/b0ink/doubtfire-deploy/issues/1015)) ([2b4afe2](https://github.com/b0ink/doubtfire-deploy/commit/2b4afe24e3f8139fec23337facc7656eec9a9d3a)) + + +### Bug Fixes + +* add assess in portfolio tasks as completed tasks in burndown chart ([8d36c94](https://github.com/b0ink/doubtfire-deploy/commit/8d36c94f03563a0924cfeb7d2d9e8dd911b65960)) + ## [10.0.0-47](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-46...v10.0.0-47) (2025-09-25) diff --git a/package-lock.json b/package-lock.json index cd419cdb8b..df15a9fd8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-47", + "version": "10.0.0-48", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-47", + "version": "10.0.0-48", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index f9eb667eef..f481fc788f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-47", + "version": "10.0.0-48", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From bb548b92d04bb7b8665444e7935317bc4b6a0981 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:42:50 +1100 Subject: [PATCH 589/776] feat: display session start and end time --- .../directives/analytics-tutor-times.component.html | 1 + .../directives/analytics-tutor-times.component.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html index faef0eaab1..2a8dccf5e1 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html @@ -66,6 +66,7 @@

Tutor Times Session Summary

>{{ event.name }} ({{ event.duration }} minutes) {{ event.duringTutorial ? 'T' : '' }}
+ {{ event.startHour }} — {{ event.endHour }}
Assessments: {{ event.assessments || 0 }}
Comments: {{ event.comments_added || 0 }}
Submissions opened: {{ event.submissions_opened || 0 }}
diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts index 19735552cc..a21aa9d230 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts @@ -177,6 +177,16 @@ export class AnalyticsTutorTimesComponent implements OnInit { return { start: new Date(session.startTime), end: new Date(session.endTime), + startHour: new Date(session.startTime).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + hour12: false, + }), + endHour: new Date(session.endTime).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + hour12: false, + }), title: `${tutor?.user.firstName} (${session.durationMinutes}m) ${session.duringTutorial ? '(T)' : ''}`, color: {primary: secondary, secondary: primary}, user_id: session.user.id, From a251c4f024b6dd9fa17b87cbafe3e0903dc18372 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:44:02 +1100 Subject: [PATCH 590/776] chore(release): 10.0.0-49 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2b6a8e01e..0f0f746ac0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-49](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-48...v10.0.0-49) (2025-10-12) + + +### Features + +* display session start and end time ([bb548b9](https://github.com/b0ink/doubtfire-deploy/commit/bb548b92d04bb7b8665444e7935317bc4b6a0981)) + ## [10.0.0-48](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-47...v10.0.0-48) (2025-10-12) diff --git a/package-lock.json b/package-lock.json index df15a9fd8a..df3e8388b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-48", + "version": "10.0.0-49", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-48", + "version": "10.0.0-49", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index f481fc788f..2d1bf66d8f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-48", + "version": "10.0.0-49", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 9a7a860fe799e351831e2a3f9701792792b76b38 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Mon, 13 Oct 2025 11:44:54 +1100 Subject: [PATCH 591/776] feat: jplag base code (#1010) * feat: jplag base code * chore: reword base code description --- src/app/api/models/task-definition.ts | 1 + src/app/api/services/task-definition.service.ts | 1 + .../task-definition-upload.component.html | 13 +++++++++++++ 3 files changed, 15 insertions(+) diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index 0810326cdd..30aff474bf 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -54,6 +54,7 @@ export class TaskDefinition extends Entity { similarityLanguage: string = 'c'; hasJplagReport: boolean; assessInPortfolioOnly: boolean; + useResourcesForJplagBaseCode: boolean; public readonly taskPrerequisitesCache: EntityCache = new EntityCache(); diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index 7692ab936b..63708814f8 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -136,6 +136,7 @@ export class TaskDefinitionService extends CachedEntityService { }); }, }, + 'useResourcesForJplagBaseCode', ); this.mapping.mapAllKeysToJsonExcept( diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component.html index 48d7fd0080..28015f48b3 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component.html @@ -113,4 +113,17 @@
+
+
+ + Use Task Resources for JPlag Base Code +

+ Base code is a common framework included in all submissions. When enabled, student code + matching this base code will be ignored during JPlag similarity checks. The base code will + be automatically extracted from the task resources. +

+
+
+
} From 059d54f5450da29b3e21a2b893116decd8df1879 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 13 Oct 2025 11:45:26 +1100 Subject: [PATCH 592/776] chore(release): 10.0.0-50 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f0f746ac0..28de2191c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-50](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-49...v10.0.0-50) (2025-10-13) + + +### Features + +* jplag base code ([#1010](https://github.com/b0ink/doubtfire-deploy/issues/1010)) ([9a7a860](https://github.com/b0ink/doubtfire-deploy/commit/9a7a860fe799e351831e2a3f9701792792b76b38)) + ## [10.0.0-49](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-48...v10.0.0-49) (2025-10-12) diff --git a/package-lock.json b/package-lock.json index df3e8388b7..19dd45e590 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-49", + "version": "10.0.0-50", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-49", + "version": "10.0.0-50", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 2d1bf66d8f..dcc5e480c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-49", + "version": "10.0.0-50", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 173d8c25375e231b25e64a04c1e3d2aedf741300 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Tue, 14 Oct 2025 18:00:11 +1100 Subject: [PATCH 593/776] fix: unlock tasks when prerequisite requires rff and task is in aip state --- src/app/api/models/task-prerequisite.ts | 1 + .../task-definition-prerequisites.component.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/app/api/models/task-prerequisite.ts b/src/app/api/models/task-prerequisite.ts index 43450deedf..510d6ce3b6 100644 --- a/src/app/api/models/task-prerequisite.ts +++ b/src/app/api/models/task-prerequisite.ts @@ -21,6 +21,7 @@ export class TaskPrerequisite extends Entity { public readonly STATES: Partial> = { ready_for_feedback: 1, + assess_in_portfolio: 1, discuss: 2, demonstrate: 2, complete: 3, diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-prerequisites/task-definition-prerequisites.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-prerequisites/task-definition-prerequisites.component.ts index 1e11eed7ff..eff30b4bd3 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-prerequisites/task-definition-prerequisites.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-prerequisites/task-definition-prerequisites.component.ts @@ -35,6 +35,7 @@ export class TaskDefinitionPrerequisitesComponent implements OnInit, OnChanges { public readonly STATES: Partial> = { ready_for_feedback: 1, + assess_in_portfolio: 1, discuss: 2, demonstrate: 2, complete: 3, From 267fa1b27515184149543afdb2e87fbe9c915ef8 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Tue, 14 Oct 2025 18:03:40 +1100 Subject: [PATCH 594/776] chore(release): 10.0.0-51 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 6 +++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28de2191c2..08159fda97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-51](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-50...v10.0.0-51) (2025-10-14) + + +### Bug Fixes + +* unlock tasks when prerequisite requires rff and task is in aip state ([173d8c2](https://github.com/b0ink/doubtfire-deploy/commit/173d8c25375e231b25e64a04c1e3d2aedf741300)) + ## [10.0.0-50](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-49...v10.0.0-50) (2025-10-13) diff --git a/package-lock.json b/package-lock.json index 19dd45e590..38c5bd3f5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-50", + "version": "10.0.0-51", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-50", + "version": "10.0.0-51", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index dcc5e480c8..d45df0c039 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-50", + "version": "10.0.0-51", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", @@ -16,8 +16,8 @@ "lint": "ng lint", "serve:angular17": "export NODE_OPTIONS=--max_old_space_size=4096 && ng serve --poll=2000 --configuration $NODE_ENV --proxy-config proxy.conf.json", "serve:angular17-compose": "export NODE_OPTIONS=--max_old_space_size=4096 && ng serve --configuration $NODE_ENV --proxy-config proxy-compose.conf.json", - "start": "npm-run-all -l -s build:angular1 serve:angular17", - "start-compose": "npm-run-all -l -s build:angular1 serve:angular17-compose", + "start": "npm-run-all -l -s build:angular1 -p watch:angular1 serve:angular17", + "start-compose": "npm-run-all -l -s build:angular1 -p watch:angular1 serve:angular17-compose", "watch:angular1": "grunt delta", "deploy:build2api": "ng build --delete-output-path=true --optimization=true --configuration production --output-path dist", "deploy": "run-s -l build:angular1 deploy:build2api", From 5c0b528d774cac7d73de28e6e9ec16c9d109e955 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 15 Oct 2025 10:03:30 +1100 Subject: [PATCH 595/776] chore: filter out sessions with less than 1 minute duration --- .../analytics-tutor-times.component.ts | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts index a21aa9d230..3572e946ea 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts @@ -8,6 +8,25 @@ import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloa import {SidekiqProgressModalService} from 'src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.service'; import {AlertService} from 'src/app/common/services/alert.service'; +interface SessionEvent { + start: Date; + end: Date; + startHour: string; + endHour: string; + title: string; + color: { + primary: string; + secondary: string; + }; + userId: number; + commentsAdded: number; + assessments: number; + submissionsOpened: number; + duration: number; + duringTutorial: boolean; + tutorName: string; +} + @Component({ selector: 'f-analytics-tutor-times', templateUrl: 'analytics-tutor-times.component.html', @@ -25,7 +44,7 @@ export class AnalyticsTutorTimesComponent implements OnInit { selectedUserId: number | null = null; viewDate = new Date(); - events = []; + events: SessionEvent[] = []; filteredEvents = []; tutorTimeSummaryStartDate: Date; @@ -100,7 +119,8 @@ export class AnalyticsTutorTimesComponent implements OnInit { this.filteredEvents = this.events.filter( (e) => (this.selectedUserId === null || e['user_id'] === this.selectedUserId) && - (!this.hideSessionsDuringTutorials || !e['duringTutorial']), + (!this.hideSessionsDuringTutorials || !e['duringTutorial']) && + e.duration >= 1, ); } @@ -120,7 +140,6 @@ export class AnalyticsTutorTimesComponent implements OnInit { this.alertService.error('You cannot select more than a year', 3000); return; } - console.log('diff days', diffDays); if (diffDays < 1) { this.tutorTimeSummaryStartDate = this.tutorTimeSummaryEndDate; this.alertService.error('End date must be on or after the start date'); @@ -189,21 +208,17 @@ export class AnalyticsTutorTimesComponent implements OnInit { }), title: `${tutor?.user.firstName} (${session.durationMinutes}m) ${session.duringTutorial ? '(T)' : ''}`, color: {primary: secondary, secondary: primary}, - user_id: session.user.id, - comments_added: session.commentsAdded, + userId: session.user.id, + commentsAdded: session.commentsAdded, assessments: session.assessments, - submissions_opened: session.submissionsOpened, + submissionsOpened: session.submissionsOpened, duration: session.durationMinutes, duringTutorial: session.duringTutorial, - name: tutor?.user.firstName, + tutorName: tutor?.user.firstName, }; }); - this.filteredEvents = this.events.filter( - (row) => this.selectedUserId === null || row['user_id'] === this.selectedUserId, - ); - - console.log(data); + this.applyFilters(); }, error: (error) => { this.canLoadSessions = false; From 5c0c9974569e720a6c95aa8333c2a38d0ab4a1b0 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:40:19 +1100 Subject: [PATCH 596/776] fix: filter out students from unit staff editor (#1018) --- .../unit-staff-editor/unit-staff-editor.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts index f64d62d921..f161c384aa 100644 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts +++ b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts @@ -145,7 +145,10 @@ export class UnitStaffEditorComponent implements OnInit { this.filteredStaff = this.staff.filter( (staff) => staff.matches(this.searchTerm.toLowerCase()) && // Find by name - !this.unit.staff.find((listStaff) => staff.id === listStaff.user.id), // Not already assigned to the unit + !this.unit.staff.find((listStaff) => staff.id === listStaff.user.id) && // Not already assigned to the unit + // Filter out students from the staff search + // NOTE: This is a hotfix to an issue where loading the inbox populates this.staff with students... + staff.isStaff, ); } From 4487865ed551af44adcad8de01f062cdc258dc11 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Wed, 15 Oct 2025 14:45:35 +1100 Subject: [PATCH 597/776] refactor: send timezone of client to the api (#1019) --- src/app/api/models/unit.ts | 11 ++++++++++- .../directives/analytics-tutor-times.component.ts | 11 ++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/app/api/models/unit.ts b/src/app/api/models/unit.ts index b144a53bb3..37b27568a4 100644 --- a/src/app/api/models/unit.ts +++ b/src/app/api/models/unit.ts @@ -605,7 +605,11 @@ export class Unit extends Entity { ); } - public getUserMarkingSessions(startDate?: Date, endDate?: Date): Observable { + public getUserMarkingSessions( + startDate?: Date, + endDate?: Date, + timezone?: string, + ): Observable { let params = new HttpParams(); if (startDate) { params = params.set( @@ -621,6 +625,8 @@ export class Unit extends Entity { ); } + params = params.set('timezone', timezone); + // TODO: we should cache the data by the same start/end date const markingSessionService = AppInjector.get(MarkingSessionService); return markingSessionService.fetchAll( @@ -634,6 +640,7 @@ export class Unit extends Entity { public downloadTutorTimesSummaryCsv( startDate?: Date, endDate?: Date, + timezone?: string, ignoreSessionsDuringTutorials?: boolean, ): Observable { let params = new HttpParams(); @@ -652,6 +659,8 @@ export class Unit extends Entity { ); } + params = params.set('timezone', timezone); + params = params.set('ignore_sessions_during_tutorials', ignoreSessionsDuringTutorials ?? false); return AppInjector.get(HttpClient).get( diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts index 3572e946ea..dc6aa29351 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts @@ -164,14 +164,17 @@ export class AnalyticsTutorTimesComponent implements OnInit { const start = `${this.tutorTimeSummaryStartDate.getFullYear()}-${(this.tutorTimeSummaryStartDate.getMonth() + 1).toString().padStart(2, '0')}-${this.tutorTimeSummaryStartDate.getDate().toString().padStart(2, '0')}`; const end = `${this.tutorTimeSummaryEndDate.getFullYear()}-${(this.tutorTimeSummaryEndDate.getMonth() + 1).toString().padStart(2, '0')}-${this.tutorTimeSummaryEndDate.getDate().toString().padStart(2, '0')}`; + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + this.downloadCsvFn( this.unit.downloadTutorTimesSummaryCsv( this.tutorTimeSummaryStartDate, this.tutorTimeSummaryEndDate, + tz, this.hideSessionsDuringTutorials, ), 'Tutor Times Summary CSV', - `${this.unit.code}-tutor-times-summary-${start}-to-${end}-${!this.hideSessionsDuringTutorials ? 'incl-tutorials' : ''}.csv`, + `${this.unit.code}-tutor-times-summary-${start}-to-${end}-${tz}-${!this.hideSessionsDuringTutorials ? 'incl-tutorials' : ''}.csv`, ); } @@ -180,10 +183,12 @@ export class AnalyticsTutorTimesComponent implements OnInit { return; } + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + this.canLoadSessions = false; this.isLoading = true; this.unit - .getUserMarkingSessions(this.tutorTimeSummaryStartDate, this.tutorTimeSummaryEndDate) + .getUserMarkingSessions(this.tutorTimeSummaryStartDate, this.tutorTimeSummaryEndDate, tz) .subscribe({ next: (data) => { this.isLoading = false; @@ -222,7 +227,7 @@ export class AnalyticsTutorTimesComponent implements OnInit { }, error: (error) => { this.canLoadSessions = false; - + this.alertService.error(`Failed to load sessions: ${error}`, 6000); console.error(error); }, }); From e3fbe4404661eb16b9c0e5ba19a1369dac1fb5b0 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 15 Oct 2025 16:11:24 +1100 Subject: [PATCH 598/776] feat: download marking sessions for tutor --- src/app/api/models/unit.ts | 29 +++++++++++++++++++ .../analytics-tutor-times.component.html | 29 +++++++++++++------ .../analytics-tutor-times.component.ts | 19 ++++++++++++ 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/app/api/models/unit.ts b/src/app/api/models/unit.ts index 37b27568a4..8306a94f12 100644 --- a/src/app/api/models/unit.ts +++ b/src/app/api/models/unit.ts @@ -669,6 +669,35 @@ export class Unit extends Entity { ); } + public downloadMyTutorTimeSessionsCsv( + startDate?: Date, + endDate?: Date, + timezone?: string, + ): Observable { + let params = new HttpParams(); + + if (startDate) { + params = params.set( + 'start_date', + `${startDate.getFullYear()}-${(startDate.getMonth() + 1).toString().padStart(2, '0')}-${startDate.getDate().toString().padStart(2, '0')}`, + ); + } + + if (endDate) { + params = params.set( + 'end_date', + `${endDate.getFullYear()}-${(endDate.getMonth() + 1).toString().padStart(2, '0')}-${endDate.getDate().toString().padStart(2, '0')}`, + ); + } + + params = params.set('timezone', timezone); + + return AppInjector.get(HttpClient).get( + `${AppInjector.get(DoubtfireConstants).API_URL}/csv/units/${this.id}/my_marking_sessions`, + {params}, + ); + } + public hasD2lMapping(): boolean { const doubtfireConstants = AppInjector.get(DoubtfireConstants); return ( diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html index 2a8dccf5e1..ba944fc26c 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html @@ -4,15 +4,26 @@

Tutor Times Session Summary

- +
+ + +
diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts index dc6aa29351..7e99793801 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts @@ -4,6 +4,7 @@ import {CalendarEvent} from 'angular-calendar'; import {Observable} from 'rxjs'; import {SidekiqJob} from 'src/app/api/models/sidekiq-job'; import {Unit} from 'src/app/api/models/unit'; +import {UserService} from 'src/app/api/services/user.service'; import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; import {SidekiqProgressModalService} from 'src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.service'; import {AlertService} from 'src/app/common/services/alert.service'; @@ -60,6 +61,7 @@ export class AnalyticsTutorTimesComponent implements OnInit { private alertService: AlertService, private sidekiqProgressModalService: SidekiqProgressModalService, private fileDownloaderService: FileDownloaderService, + private userService: UserService, ) {} ngOnInit(): void { @@ -178,6 +180,23 @@ export class AnalyticsTutorTimesComponent implements OnInit { ); } + public getMyTutorTimesSessions() { + const start = `${this.tutorTimeSummaryStartDate.getFullYear()}-${(this.tutorTimeSummaryStartDate.getMonth() + 1).toString().padStart(2, '0')}-${this.tutorTimeSummaryStartDate.getDate().toString().padStart(2, '0')}`; + const end = `${this.tutorTimeSummaryEndDate.getFullYear()}-${(this.tutorTimeSummaryEndDate.getMonth() + 1).toString().padStart(2, '0')}-${this.tutorTimeSummaryEndDate.getDate().toString().padStart(2, '0')}`; + + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + + this.downloadCsvFn( + this.unit.downloadMyTutorTimeSessionsCsv( + this.tutorTimeSummaryStartDate, + this.tutorTimeSummaryEndDate, + tz, + ), + 'My Marking Sessions CSV', + `${this.unit.code}-${this.userService.currentUser.name}-sessions-${start}-to-${end}-${tz}}.csv`, + ); + } + public getMarkingSesssions() { if (!this.canLoadSessions) { return; From 89560205d7316c53d77f376b5f7cfff5bf99e9c6 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 15 Oct 2025 16:23:36 +1100 Subject: [PATCH 599/776] fix: display tutor name --- .../analytics/directives/analytics-tutor-times.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html index ba944fc26c..717fac9603 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html @@ -74,7 +74,7 @@

Tutor Times Session Summary

{{ event.name }} ({{ event.duration }} minutes) + >{{ event.tutorName }} ({{ event.duration }} minutes) {{ event.duringTutorial ? 'T' : '' }}
{{ event.startHour }} — {{ event.endHour }}
From 4435fea9daaa3566ec9f1fb86aea9caad7645ce0 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 15 Oct 2025 16:25:16 +1100 Subject: [PATCH 600/776] refactor: expand hour segments ui --- .../analytics/directives/analytics-tutor-times.component.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html index 717fac9603..e2123b30c3 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html @@ -100,8 +100,10 @@

Tutor Times Session Summary

(eventClicked)="eventClicked($event)" [tooltipTemplate]="eventTooltipTemplate" [hourDuration]="60" - [hourSegments]="1" + [hourSegments]="4" [weekStartsOn]="0" + [hourSegmentHeight]="15" + [minimumEventHeight]="5" [daysInWeek]="daysInWeek" (beforeViewRender)="beforeViewRender($event)" /> From 697daf26693f20f61dcfbbeb0597a02715c9512d Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 15 Oct 2025 16:41:55 +1100 Subject: [PATCH 601/776] fix: re-enable user filtering on click --- .../directives/analytics-tutor-times.component.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts index 7e99793801..a60a756699 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts @@ -120,8 +120,8 @@ export class AnalyticsTutorTimesComponent implements OnInit { applyFilters() { this.filteredEvents = this.events.filter( (e) => - (this.selectedUserId === null || e['user_id'] === this.selectedUserId) && - (!this.hideSessionsDuringTutorials || !e['duringTutorial']) && + (this.selectedUserId === null || e.userId === this.selectedUserId) && + (!this.hideSessionsDuringTutorials || !e.duringTutorial) && e.duration >= 1, ); } @@ -252,10 +252,10 @@ export class AnalyticsTutorTimesComponent implements OnInit { }); } - eventClicked({event}: {event: CalendarEvent}): void { - if (event['user_id'] !== undefined) { + eventClicked({event}: {event: SessionEvent}): void { + if (event.userId !== undefined) { if (this.selectedUserId === null) { - this.selectedUserId = Number(event['user_id']); + this.selectedUserId = Number(event.userId); } else { this.selectedUserId = null; } From 7ea20939f45b013a56cc3405faaf0855ebe9926a Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Thu, 16 Oct 2025 15:18:48 +1100 Subject: [PATCH 602/776] refactor: collapse related tutorial list --- .../task-definition-who.component.html | 37 ++++++++++++++++--- .../task-definition-who.component.ts | 5 +++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-who/task-definition-who.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-who/task-definition-who.component.html index 814e814197..5d93345d7f 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-who/task-definition-who.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-who/task-definition-who.component.html @@ -13,7 +13,11 @@
Tutorial Stream - Which tutor provides feedback? - + @for (stream of unit.tutorialStreams; track stream) { {{ stream.name }} } @@ -23,11 +27,32 @@ Related tutorials
    - @for (tutorial of taskDefinition.tutorialStream?.tutorialsIn(unit); track tutorial) { -
  • - {{ tutorial?.abbreviation }} {{ tutorial.tutor?.name }} -
  • -} + @for ( + tutorial of taskDefinition.tutorialStream?.tutorialsIn(unit) + | slice: 0 : (showAllTutorials ? undefined : 3); + track tutorial + ) { +
  • {{ tutorial?.abbreviation }} {{ tutorial.tutor?.name }}
  • + } + + @if (!showAllTutorials && taskDefinition.tutorialStream?.tutorialsIn(unit)?.length > 3) { +
  • + +{{ taskDefinition.tutorialStream.tutorialsIn(unit).length - 3 }} more tutorials +
  • + } + @if (taskDefinition.tutorialStream?.tutorialsIn(unit).length > 3) { +
    + @if (!showAllTutorials) { + + } @else { + + } +
    + }
diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-who/task-definition-who.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-who/task-definition-who.component.ts index 9732998592..53c87c7bf1 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-who/task-definition-who.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-who/task-definition-who.component.ts @@ -10,7 +10,12 @@ import { Unit } from 'src/app/api/models/unit'; export class TaskDefinitionWhoComponent { @Input() taskDefinition: TaskDefinition; + showAllTutorials: boolean = false; public get unit(): Unit { return this.taskDefinition?.unit; } + + onTutorialStreamChange() { + this.showAllTutorials = false; + } } From 4384b376eaedf8ab66febec4c0ada53111311ff0 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:38:43 +1100 Subject: [PATCH 603/776] feat: campus timezone (#1022) --- .../campus-list/campus-list.component.html | 24 +++++++++++++++++++ .../campus-list/campus-list.component.ts | 4 +++- src/app/api/models/campus/campus.ts | 1 + src/app/api/services/campus.service.ts | 2 +- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.html b/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.html index 3fe38272ec..6fed39bc24 100644 --- a/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.html +++ b/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.html @@ -83,6 +83,30 @@

Campuses

+ + + Timezone + + @if (!editing(campus)) { +
+ {{ campus.timezone }} +
+ } @else { + + Timezone + + + + } + + + + Timezone + + + +
+ Active diff --git a/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.ts b/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.ts index 3ab6b9e71e..1fa8871cc3 100644 --- a/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.ts +++ b/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.ts @@ -18,7 +18,7 @@ export class CampusListComponent extends EntityFormComponent { syncModes = ['timetable', 'automatic', 'manual']; // Set up the table - columns: string[] = ['name', 'abbreviation', 'mode', 'active', 'options']; + columns: string[] = ['name', 'abbreviation', 'mode', 'timezone', 'active', 'options']; campuses: Campus[] = new Array(); dataSource = new MatTableDataSource(this.campuses); @@ -33,6 +33,7 @@ export class CampusListComponent extends EntityFormComponent { abbreviation: new UntypedFormControl('', [Validators.required]), name: new UntypedFormControl('', [Validators.required]), mode: new UntypedFormControl('', [Validators.required]), + timezone: new UntypedFormControl('', [Validators.required]), active: new UntypedFormControl(false), }, 'Campus', @@ -91,6 +92,7 @@ export class CampusListComponent extends EntityFormComponent { case 'name': case 'abbreviation': case 'mode': + case 'timezone': case 'active': return super.sortTableData(sort); } diff --git a/src/app/api/models/campus/campus.ts b/src/app/api/models/campus/campus.ts index 164b3704f6..9491e29e53 100644 --- a/src/app/api/models/campus/campus.ts +++ b/src/app/api/models/campus/campus.ts @@ -7,6 +7,7 @@ export class Campus extends Entity { name: string; mode: campusModes; abbreviation: string; + timezone: string; public override toJson(mappingData: EntityMapping, ignoreKeys?: string[]): object { return { diff --git a/src/app/api/services/campus.service.ts b/src/app/api/services/campus.service.ts index ddf3e95942..d46d793cc3 100644 --- a/src/app/api/services/campus.service.ts +++ b/src/app/api/services/campus.service.ts @@ -11,7 +11,7 @@ export class CampusService extends CachedEntityService { constructor(httpClient: HttpClient) { super(httpClient, API_URL); - this.mapping.addKeys('id', 'name', 'mode', 'abbreviation', 'active'); + this.mapping.addKeys('id', 'name', 'mode', 'abbreviation', 'active', 'timezone'); this.mapping.mapAllKeysToJsonExcept('id'); } From 1b08535443ffc7723d018c518cf7d77b12920a0e Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Fri, 17 Oct 2025 10:08:20 +1100 Subject: [PATCH 604/776] refactor: enable marking sessions calendar for tutors --- .../analytics-tutor-times.component.html | 21 +++++++++++-------- .../analytics-tutor-times.component.ts | 4 ++++ .../unit-analytics-route.component.html | 4 +--- .../unit-analytics-route.component.ts | 2 +- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html index e2123b30c3..d826a68de2 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html @@ -5,15 +5,18 @@

Tutor Times Session Summary

- + @if (role === 'Convenor') { + + } +
diff --git a/src/app/units/states/analytics/unit-analytics-route.component.ts b/src/app/units/states/analytics/unit-analytics-route.component.ts index 73419c4dfe..4c8e068722 100644 --- a/src/app/units/states/analytics/unit-analytics-route.component.ts +++ b/src/app/units/states/analytics/unit-analytics-route.component.ts @@ -61,7 +61,7 @@ export class UnitAnalyticsComponent { ); } - private downloadCsv(newJob: Observable, title: string, filename: string) { + public downloadCsv(newJob: Observable, title: string, filename: string) { newJob.subscribe({ next: (job) => { if (!job || !job.id) { From b972dde2047172cc45f1756d866e07f54c8a355d Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Fri, 17 Oct 2025 12:00:03 +1100 Subject: [PATCH 605/776] chore(release): 10.0.0-52 --- CHANGELOG.md | 15 +++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08159fda97..e1c0f0d73d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-52](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-51...v10.0.0-52) (2025-10-17) + + +### Features + +* campus timezone ([#1022](https://github.com/b0ink/doubtfire-deploy/issues/1022)) ([4384b37](https://github.com/b0ink/doubtfire-deploy/commit/4384b376eaedf8ab66febec4c0ada53111311ff0)) +* download marking sessions for tutor ([e3fbe44](https://github.com/b0ink/doubtfire-deploy/commit/e3fbe4404661eb16b9c0e5ba19a1369dac1fb5b0)) + + +### Bug Fixes + +* display tutor name ([8956020](https://github.com/b0ink/doubtfire-deploy/commit/89560205d7316c53d77f376b5f7cfff5bf99e9c6)) +* filter out students from unit staff editor ([#1018](https://github.com/b0ink/doubtfire-deploy/issues/1018)) ([5c0c997](https://github.com/b0ink/doubtfire-deploy/commit/5c0c9974569e720a6c95aa8333c2a38d0ab4a1b0)) +* re-enable user filtering on click ([697daf2](https://github.com/b0ink/doubtfire-deploy/commit/697daf26693f20f61dcfbbeb0597a02715c9512d)) + ## [10.0.0-51](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-50...v10.0.0-51) (2025-10-14) diff --git a/package-lock.json b/package-lock.json index 38c5bd3f5f..5a2e04aac3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-51", + "version": "10.0.0-52", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-51", + "version": "10.0.0-52", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index d45df0c039..3356d560a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-51", + "version": "10.0.0-52", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 3b684a3395c9ccdfb412ff439a08e2ab88f808d7 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Fri, 17 Oct 2025 13:50:45 +1100 Subject: [PATCH 606/776] refactor: include all submitted tasks in portfolio task list (#1023) * refactor: include all submitted tasks in portfolio task list * refactor: fetch tasks included in portfolio from api * refactor: reword * refactor: reword * refactor: show warning if no tasks found * refactor: wording * refactor: add error alert --- src/app/api/models/project.ts | 9 +++++ src/app/doubtfire-angular.module.ts | 2 ++ src/app/doubtfire-angularjs.module.ts | 6 ++++ .../portfolio-included-tasks.component.html | 26 ++++++++++++++ .../portfolio-included-tasks.component.scss | 0 .../portfolio-included-tasks.component.ts | 36 +++++++++++++++++++ .../portfolio-review-step.tpl.html | 22 +++++------- .../states/portfolio/portfolio.coffee | 2 +- 8 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.html create mode 100644 src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.scss create mode 100644 src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.ts diff --git a/src/app/api/models/project.ts b/src/app/api/models/project.ts index 1064fb3c5b..df3c3cda26 100644 --- a/src/app/api/models/project.ts +++ b/src/app/api/models/project.ts @@ -528,4 +528,13 @@ export class Project extends Entity { }), ); } + + public tasksIncludedInPortfolioUrl(): string { + return `${AppInjector.get(DoubtfireConstants).API_URL}/projects/${this.id}/portfolio_tasks`; + } + + public getTasksIncludedInPortfolio(): Observable { + const httpClient = AppInjector.get(HttpClient); + return httpClient.get(this.tasksIncludedInPortfolioUrl()); + } } diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 47e4679234..12f65dc8a5 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -293,6 +293,7 @@ import {UnitDetailsEditorComponent} from './units/states/edit/directives/unit-de import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component'; import {AnalyticsTutorTimesComponent} from './units/states/analytics/directives/analytics-tutor-times.component'; import {MarkingSessionService} from './api/services/marking-session.service'; +import {PortfolioIncludedTasksComponent} from './projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.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 = { @@ -446,6 +447,7 @@ const MY_DATE_FORMAT = { UnitDetailsEditorComponent, PortfolioGradeSelectStepComponent, AnalyticsTutorTimesComponent, + PortfolioIncludedTasksComponent, ], // Services we provide providers: [ diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 963bf7d209..886886135c 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -230,6 +230,7 @@ import {UnitStaffEditorComponent} from './units/states/edit/directives/unit-staf import {GroupSetSelectorComponent} from './groups/group-set-selector/group-set-selector.component'; import {UnitDetailsEditorComponent} from './units/states/edit/directives/unit-details-editor/unit-details-editor.component'; import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component'; +import {PortfolioIncludedTasksComponent} from './projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component'; export const DoubtfireAngularJSModule = angular .module('doubtfire', [ @@ -557,3 +558,8 @@ DoubtfireAngularJSModule.directive( 'groupSetSelector', downgradeComponent({component: GroupSetSelectorComponent}), ); + +DoubtfireAngularJSModule.directive( + 'fPortfolioIncludedTasks', + downgradeComponent({component: PortfolioIncludedTasksComponent}), +); diff --git a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.html b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.html new file mode 100644 index 0000000000..7444d7176c --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.html @@ -0,0 +1,26 @@ +@if (loading) { +
+
Loading tasks...
+ +
+} @else { + @if (tasksInPortfolio.length === 0) { +
+ assignment_late +

No tasks found

+
+ } @else { +
    + @for (task of tasksInPortfolio; track task) { +
  1. +
    +
    +
    {{ task.definition.abbreviation }} — {{ task.definition.name }}
    +
    + +
    +
  2. + } +
+ } +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.scss b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.ts b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.ts new file mode 100644 index 0000000000..e425d93976 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.ts @@ -0,0 +1,36 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {Project} from 'src/app/api/models/project'; +import {Task} from 'src/app/api/models/task'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-portfolio-included-tasks', + templateUrl: 'portfolio-included-tasks.component.html', + styleUrls: ['portfolio-included-tasks.component.scss'], +}) +export class PortfolioIncludedTasksComponent implements OnInit { + @Input() project: Project; + + constructor(private alertService: AlertService) {} + + loading: boolean = false; + + tasksInPortfolio: Task[] = []; + ngOnInit() { + this.loading = true; + this.project.getTasksIncludedInPortfolio().subscribe({ + next: (tasks) => { + for (const taskId of tasks) { + const task = this.project.tasks.find((t) => t.id === taskId); + if (task) { + this.tasksInPortfolio.push(task); + } + } + this.loading = false; + }, + error: (error) => { + this.alertService.error(`Failed to get tasks for portfolio: ${error}`, 6000); + }, + }); + } +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.tpl.html index ba23ed6f53..00dffc0906 100644 --- a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.tpl.html +++ b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.tpl.html @@ -51,19 +51,15 @@

Portfolio Processing

  • {{file.name}}
  • -
    -

    - You have included {{selectedTasks().length}} tasks in your portfolio. If you wish to add or - remove some of these tasks, please review the Select Tasks step to adjust your alignments of each - task to the unit's learning outcomes. Each task will be attached in the order below: -

    -
      -
    1. - - {{task.definition.name}} -
    2. -
    -
    +

    + Only submitted tasks will be included in your portfolio. If a task is missing from the list, + ensure that you have submitted it before compiling your portfolio. All feedback and comments + for each task will appear in the final portfolio, so you can add any additional comments now + if there's something you'd like to address. +

    The following tasks will be included + automatically in this order:

    +

    +
    diff --git a/src/app/projects/states/portfolio/portfolio.coffee b/src/app/projects/states/portfolio/portfolio.coffee index 63e93ea961..01b6bf8972 100644 --- a/src/app/projects/states/portfolio/portfolio.coffee +++ b/src/app/projects/states/portfolio/portfolio.coffee @@ -82,7 +82,7 @@ angular.module('doubtfire.projects.states.portfolio', [ # Gets selected tasks in the task selector $scope.selectedTasks = -> # Filter by included in portfolio - tasks = _.filter $scope.project.tasks, (t) -> t.includeInPortfolio + tasks = $scope.project.tasks tasks = _.filter tasks, (t) -> !_.includes(newTaskService.toBeWorkedOn, t.status) _.sortBy tasks, (t) -> t.definition.seq From ea42bd63d82f082689bc860b53f43b4ffb5f50e8 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Fri, 17 Oct 2025 13:52:04 +1100 Subject: [PATCH 607/776] chore(release): 10.0.0-53 --- CHANGELOG.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1c0f0d73d..0d250861d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-53](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-52...v10.0.0-53) (2025-10-17) + ## [10.0.0-52](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-51...v10.0.0-52) (2025-10-17) diff --git a/package-lock.json b/package-lock.json index 5a2e04aac3..e94c4152ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-52", + "version": "10.0.0-53", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-52", + "version": "10.0.0-53", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 3356d560a7..52bf9d5075 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-52", + "version": "10.0.0-53", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From b67810743c18b4f178a07421f0c9acde6b8b989a Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Fri, 17 Oct 2025 14:20:52 +1100 Subject: [PATCH 608/776] refactor: number list --- .../portfolio-welcome-step/portfolio-welcome-step.tpl.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html index 524101329b..51297a7060 100644 --- a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html +++ b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html @@ -4,10 +4,9 @@

    Portfolio Preparation

    Preparing your portfolio involves 5 steps:

    -
      +
      1. Select your Grade you are applying for
      2. Upload your Learning Summary Report
      3. -
      4. Select the Tasks you want included
      5. Upload any Other Resources you want to add
      6. Compile your resources into your portfolio and review
      From 2eec7ce1ecc229f672e7f02a27c76ffce4f45291 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Fri, 17 Oct 2025 15:10:13 +1100 Subject: [PATCH 609/776] chore: add margin --- .../portfolio-welcome-step/portfolio-welcome-step.tpl.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html index 51297a7060..cb062284f1 100644 --- a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html +++ b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html @@ -4,7 +4,7 @@

      Portfolio Preparation

    Preparing your portfolio involves 5 steps:

    -
      +
      1. Select your Grade you are applying for
      2. Upload your Learning Summary Report
      3. Upload any Other Resources you want to add
      4. From 3b225388981272f14ab2f2f17af1df8be10ecbcc Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Fri, 17 Oct 2025 15:11:05 +1100 Subject: [PATCH 610/776] chore(release): 10.0.0-54 --- CHANGELOG.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d250861d3..f710943bd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-54](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-53...v10.0.0-54) (2025-10-17) + ## [10.0.0-53](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-52...v10.0.0-53) (2025-10-17) ## [10.0.0-52](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-51...v10.0.0-52) (2025-10-17) diff --git a/package-lock.json b/package-lock.json index e94c4152ee..7b09dfd70d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-53", + "version": "10.0.0-54", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-53", + "version": "10.0.0-54", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 52bf9d5075..6eb504c354 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-53", + "version": "10.0.0-54", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 8a1dd1ac806c5acbbf8d1ccf7bdcb6af1c68dd14 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Fri, 17 Oct 2025 15:23:49 +1100 Subject: [PATCH 611/776] fix: typo --- .../analytics/directives/analytics-tutor-times.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts index b8d6ac3574..b9606b7fc7 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts @@ -197,7 +197,7 @@ export class AnalyticsTutorTimesComponent implements OnInit { tz, ), 'My Marking Sessions CSV', - `${this.unit.code}-${this.userService.currentUser.name}-sessions-${start}-to-${end}-${tz}}.csv`, + `${this.unit.code}-${this.userService.currentUser.name}-sessions-${start}-to-${end}-${tz}.csv`, ); } From c49ffeddedb2b79c39252770692e7df9dcd97bfc Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Sun, 19 Oct 2025 13:02:30 +1100 Subject: [PATCH 612/776] fix: display correct marking session details --- .../analytics/directives/analytics-tutor-times.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html index d826a68de2..dbd2f4302a 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html @@ -82,8 +82,8 @@

        Tutor Times Session Summary

        >
        {{ event.startHour }} — {{ event.endHour }}
        Assessments: {{ event.assessments || 0 }}
        - Comments: {{ event.comments_added || 0 }}
        - Submissions opened: {{ event.submissions_opened || 0 }}
        + Comments: {{ event.commentsAdded || 0 }}
        + Submissions opened: {{ event.submissionsOpened || 0 }}
        During Tutorial?: {{ event.duringTutorial ? 'yes' : 'no' }}
    From 3858e3792fef3f0542d954e213987484ac97a990 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Mon, 20 Oct 2025 13:06:08 +1100 Subject: [PATCH 613/776] feat: display task count in inbox (#1025) * feat: display number of tasks in inbox * chore: only display task count if not empty --- .../directives/staff-task-list/staff-task-list.component.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.html b/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.html index 2291a5e7a6..2a36a9704f 100644 --- a/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.html +++ b/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.html @@ -200,6 +200,9 @@ @if (filteredTasks) { + @if (filteredTasks.length) { +
    {{ filteredTasks.length }} Tasks
    + } Date: Mon, 20 Oct 2025 16:22:26 +1100 Subject: [PATCH 614/776] refactor: scrollable calendar (#1027) --- .../analytics-tutor-times.component.scss | 16 ++++++++++++++++ .../analytics-tutor-times.component.ts | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.scss b/src/app/units/states/analytics/directives/analytics-tutor-times.component.scss index e69de29bb2..2ce45fc98d 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.scss +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.scss @@ -0,0 +1,16 @@ +.cal-week-view { + position: relative; + max-height: 750px; + overflow-y: scroll; + overscroll-behavior: contain; +} + +.cal-day-headers { + position: sticky; + top: 0; + right: 0; + z-index: 2; + background-color: white; + width: 100%; + min-height: 35px; +} diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts index b9606b7fc7..9b65234bbd 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts @@ -1,6 +1,5 @@ -import {Component, Input, OnInit} from '@angular/core'; +import {Component, Input, OnInit, ViewEncapsulation} from '@angular/core'; import {MatDatepickerInputEvent} from '@angular/material/datepicker'; -import {CalendarEvent} from 'angular-calendar'; import {Observable} from 'rxjs'; import {SidekiqJob} from 'src/app/api/models/sidekiq-job'; import {Unit} from 'src/app/api/models/unit'; @@ -32,6 +31,7 @@ interface SessionEvent { selector: 'f-analytics-tutor-times', templateUrl: 'analytics-tutor-times.component.html', styleUrls: ['analytics-tutor-times.component.scss'], + encapsulation: ViewEncapsulation.None, }) export class AnalyticsTutorTimesComponent implements OnInit { @Input() unit: Unit; From f751ef30a66f4ff3638252443737767441795e1e Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 20 Oct 2025 17:32:47 +1100 Subject: [PATCH 615/776] chore(release): 10.0.0-55 --- CHANGELOG.md | 13 +++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f710943bd7..22fec1df51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-55](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-54...v10.0.0-55) (2025-10-20) + + +### Features + +* display task count in inbox ([#1025](https://github.com/b0ink/doubtfire-deploy/issues/1025)) ([3858e37](https://github.com/b0ink/doubtfire-deploy/commit/3858e3792fef3f0542d954e213987484ac97a990)) + + +### Bug Fixes + +* display correct marking session details ([c49ffed](https://github.com/b0ink/doubtfire-deploy/commit/c49ffeddedb2b79c39252770692e7df9dcd97bfc)) +* typo ([8a1dd1a](https://github.com/b0ink/doubtfire-deploy/commit/8a1dd1ac806c5acbbf8d1ccf7bdcb6af1c68dd14)) + ## [10.0.0-54](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-53...v10.0.0-54) (2025-10-17) ## [10.0.0-53](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-52...v10.0.0-53) (2025-10-17) diff --git a/package-lock.json b/package-lock.json index 7b09dfd70d..1a5c744864 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-54", + "version": "10.0.0-55", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-54", + "version": "10.0.0-55", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 6eb504c354..d9ba47fd69 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-54", + "version": "10.0.0-55", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From a4aa3525d07868d6085f331adbac481a86313291 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:16:52 +1100 Subject: [PATCH 616/776] refactor: require comment for assess in portfolio tasks submitting for feedback (#1026) * refactor: require comment for assess in portfolio tasks submitting for feedback * chore: fix comment * chore: fix indentation * fix: condition * chore: update character count text color * chore: add tooltip --- .../upload-submission-modal.coffee | 10 ++++---- .../upload-submission-modal.tpl.html | 24 ++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee index 486ae9bcbb..b6bcd6b50e 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee @@ -70,6 +70,8 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) $scope.uploader.payload.contributions = mapTeamToPayload() if _.includes(states.shown, 'group') $scope.uploader.payload.trigger = 'need_help' if $scope.submissionType == 'need_help' $scope.uploader.payload.trigger = 'assess_in_portfolio' if $scope.submissionType == 'assess_in_portfolio' || $scope.task.status == 'assess_in_portfolio' + if $scope.comment? and $scope.comment.trim() isnt '' + $scope.uploader.payload.comment = $scope.comment onSuccess: (response) -> # Ensure our response contains the data we're expecting if typeof response is 'object' and response? and response.id? and response.project_id? and response.status? @@ -88,9 +90,9 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) onComplete: -> return unless $scope.uploader.response? and $scope.uploader.response.id? $modalInstance.close(task) - unless $scope.task.isTestSubmission + # unless $scope.task.isTestSubmission # Add comment if requested - task.addComment($scope.comment) if $scope.comment.trim().length > 0 + # task.addComment($scope.comment) if $scope.comment.trim().length > 0 # Broadcast that upload is complete $rootScope.$broadcast('TaskSubmissionUploadComplete', task) # Perform as timeout to show 'Upload Complete' @@ -176,8 +178,8 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) back: -> false submit: -> - # Disable if no comment is supplied with need_help - !$scope.uploader.isReady || ($scope.comment.trim().length == 0 && $scope.submissionType == 'need_help') + # Disable if no comment is supplied with need_help, or if submitting for feedback and task is assess in portfolio only + !$scope.uploader.isReady or ($scope.comment.trim().length < 25 && ($scope.submissionType == 'ready_for_feedback' && $scope.task.definition.assessInPortfolioOnly) || $scope.submissionType == 'need_help') cancel: -> # Can't cancel whilst uploading $scope.uploader.isUploading diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html index 89067b9b11..49fb538af6 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html @@ -83,23 +83,25 @@

    ng-class="{'state-hidden-left': isHidden('comments').left, 'state-hidden-right': isHidden('comments').right}">
    -
    -

    - What do you need help with? -

    -

    - Final comments -

    - +
    +

    What do you need help with?

    + Supply a comment on what you would like help on for this task so your tutor can assist you. - +
    +
    +

    Final comments

    + + Please supply a comment specifying which areas of your submission you would like feedback on. + + Supply an optional comment about this submission for your tutor to read as they assess.
    - +
    Character count: {{comment.length}} (Min. 25)
    @@ -138,7 +140,7 @@

    Plagiarism and Collusion

    - From 32e03d111e61df42a9c67cb9e68d271f9300d5bc Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Thu, 23 Oct 2025 22:12:12 +1100 Subject: [PATCH 617/776] fix: ensure grade has been selected --- .../portfolio-grade-select-step.component.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html index 63385d6e5d..575a9f5faf 100644 --- a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html +++ b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html @@ -86,7 +86,11 @@

    Select Grade

    -
    -
    + + Optional Settings +
    +

    Apply other options

    +
    +
    + +
    +
    +
    From ec0af0cbe47392d6542d9741e1131954195d407f Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Thu, 30 Oct 2025 10:52:19 +1100 Subject: [PATCH 625/776] feat: open similarities from project dashboard (#1035) * feat: open similarities from project dashboard * chore: add task similarity view component --- src/app/doubtfire-angularjs.module.ts | 6 ++++++ .../directives/task-dashboard/task-dashboard.coffee | 2 +- .../directives/task-dashboard/task-dashboard.tpl.html | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 886886135c..ab7736ca88 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -231,6 +231,7 @@ import {GroupSetSelectorComponent} from './groups/group-set-selector/group-set-s import {UnitDetailsEditorComponent} from './units/states/edit/directives/unit-details-editor/unit-details-editor.component'; import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component'; import {PortfolioIncludedTasksComponent} from './projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component'; +import {TaskSimilarityViewComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component'; export const DoubtfireAngularJSModule = angular .module('doubtfire', [ @@ -563,3 +564,8 @@ DoubtfireAngularJSModule.directive( 'fPortfolioIncludedTasks', downgradeComponent({component: PortfolioIncludedTasksComponent}), ); + +DoubtfireAngularJSModule.directive( + 'fTaskSimilarityView', + downgradeComponent({component: TaskSimilarityViewComponent}), +); diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.coffee b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.coffee index 8fe60b7574..f70113800c 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.coffee +++ b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.coffee @@ -25,7 +25,7 @@ angular.module('doubtfire.projects.states.dashboard.directives.task-dashboard', # Is the current user a tutor? $scope.tutor = $stateParams.tutor # the ways in which the dashboard can be viewed - $scope.dashboardViews = ["details", "submission", "task"] + $scope.dashboardViews = ["details", "submission", "task", "similarities"] # set the current dashboard view to details by default updateCurrentView = -> 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 d845a5bcd9..16d998bc0a 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 @@ -22,6 +22,9 @@
  • View Task Sheet
  • +
  • + View Similarities +
  • Download Submission PDF @@ -88,6 +91,9 @@
  • +
    + +
    From fa722e6b6dedb140ebbd411ce41745a9b9e81859 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Thu, 30 Oct 2025 11:00:51 +1100 Subject: [PATCH 626/776] chore(release): 10.0.0-57 --- CHANGELOG.md | 13 +++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a671c033c..e80c7c93d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-57](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-56...v10.0.0-57) (2025-10-30) + + +### Features + +* open similarities from project dashboard ([#1035](https://github.com/b0ink/doubtfire-deploy/issues/1035)) ([ec0af0c](https://github.com/b0ink/doubtfire-deploy/commit/ec0af0cbe47392d6542d9741e1131954195d407f)) + + +### Bug Fixes + +* enable submission button for 'need help' trigger ([#1034](https://github.com/b0ink/doubtfire-deploy/issues/1034)) ([07de5b1](https://github.com/b0ink/doubtfire-deploy/commit/07de5b1e34f78f83c1418399f45e8c754aea530a)) +* typo ([2712a68](https://github.com/b0ink/doubtfire-deploy/commit/2712a68f6031794c571a7ee5866c653bf67de1dd)) + ## [10.0.0-56](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-55...v10.0.0-56) (2025-10-23) diff --git a/package-lock.json b/package-lock.json index 237e8df708..c74890a9cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-56", + "version": "10.0.0-57", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-56", + "version": "10.0.0-57", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 212482a3f5..bfd5a18315 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-56", + "version": "10.0.0-57", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 4d0fbb2b032ee3d28cf102f0026b0b576065f130 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Sat, 1 Nov 2025 12:37:29 +1100 Subject: [PATCH 627/776] refactor: imply portfolio has been submitted (#1037) --- .../portfolio-review-step/portfolio-review-step.tpl.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.tpl.html index 00dffc0906..4a9a098324 100644 --- a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.tpl.html +++ b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.tpl.html @@ -87,7 +87,14 @@

    Create Your Portfolio

    Download Portfolio

    -
    This is the exact same document your assessor will see
    +
    +

    + This is the exact same document your assessor will see. +

    +

    + You're done! There's nothing left for you to do. Your portfolio has been submitted. +

    +
    From 58ec499a8d59c7202f591de661551d8a32c37527 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Tue, 4 Nov 2025 00:47:50 +1100 Subject: [PATCH 631/776] fix: require comment for new evidence --- .../upload-submission-modal/upload-submission-modal.tpl.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html index 7cc0372a34..65695e16ec 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html @@ -140,7 +140,7 @@

    Plagiarism and Collusion

    - From 8f599725d670f37d50d015692be7d58f163eab8d Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Tue, 4 Nov 2025 21:57:19 +1100 Subject: [PATCH 632/776] chore(release): 10.0.0-59 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb40436959..d8a6450419 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-59](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-58...v10.0.0-59) (2025-11-04) + + +### Bug Fixes + +* require comment for new evidence ([58ec499](https://github.com/b0ink/doubtfire-deploy/commit/58ec499a8d59c7202f591de661551d8a32c37527)) +* require comment for new evidence ([c5397fc](https://github.com/b0ink/doubtfire-deploy/commit/c5397fc5c444e76b431aa5d6487553c60e711c2e)) + ## [10.0.0-58](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-57...v10.0.0-58) (2025-11-01) ## [10.0.0-57](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-56...v10.0.0-57) (2025-10-30) diff --git a/package-lock.json b/package-lock.json index 52d3b0a995..19cbbb3eb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-58", + "version": "10.0.0-59", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-58", + "version": "10.0.0-59", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 830c9eee29..c3df336ac0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-58", + "version": "10.0.0-59", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From c67ceaa84d02bb71626890fc28bb4aaea164b952 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:34:31 +1100 Subject: [PATCH 633/776] feat: upload grades csv (#1038) * feat: upload grades csv * chore: remove debug --- src/app/api/models/unit.ts | 4 ++ src/app/doubtfire-angular.module.ts | 2 + src/app/doubtfire-angularjs.module.ts | 6 ++ .../states/portfolios/portfolios.tpl.html | 1 + .../upload-grades.component.html | 3 + .../upload-grades.component.scss | 0 .../upload-grades/upload-grades.component.ts | 55 +++++++++++++++++++ 7 files changed, 71 insertions(+) create mode 100644 src/app/units/states/portfolios/upload-grades/upload-grades.component.html create mode 100644 src/app/units/states/portfolios/upload-grades/upload-grades.component.scss create mode 100644 src/app/units/states/portfolios/upload-grades/upload-grades.component.ts diff --git a/src/app/api/models/unit.ts b/src/app/api/models/unit.ts index 8306a94f12..0caa668085 100644 --- a/src/app/api/models/unit.ts +++ b/src/app/api/models/unit.ts @@ -414,6 +414,10 @@ export class Unit extends Entity { }/learning_alignments/csv.json`; } + public get gradesCSVUploadUrl(): string { + return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.id}/grades/csv`; + } + public taskStatusFactor(td: TaskDefinition): number { return 1; } diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 12f65dc8a5..ee3dcd3889 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -294,6 +294,7 @@ import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/dir import {AnalyticsTutorTimesComponent} from './units/states/analytics/directives/analytics-tutor-times.component'; import {MarkingSessionService} from './api/services/marking-session.service'; import {PortfolioIncludedTasksComponent} from './projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component'; +import {UploadGradesComponent} from './units/states/portfolios/upload-grades/upload-grades.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 = { @@ -448,6 +449,7 @@ const MY_DATE_FORMAT = { PortfolioGradeSelectStepComponent, AnalyticsTutorTimesComponent, PortfolioIncludedTasksComponent, + UploadGradesComponent, ], // Services we provide providers: [ diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index ab7736ca88..8187457400 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -232,6 +232,7 @@ import {UnitDetailsEditorComponent} from './units/states/edit/directives/unit-de import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component'; import {PortfolioIncludedTasksComponent} from './projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component'; import {TaskSimilarityViewComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component'; +import {UploadGradesComponent} from './units/states/portfolios/upload-grades/upload-grades.component'; export const DoubtfireAngularJSModule = angular .module('doubtfire', [ @@ -569,3 +570,8 @@ DoubtfireAngularJSModule.directive( 'fTaskSimilarityView', downgradeComponent({component: TaskSimilarityViewComponent}), ); + +DoubtfireAngularJSModule.directive( + 'fUploadGrades', + downgradeComponent({component: UploadGradesComponent}), +); diff --git a/src/app/units/states/portfolios/portfolios.tpl.html b/src/app/units/states/portfolios/portfolios.tpl.html index 98a6c019f7..376277513e 100644 --- a/src/app/units/states/portfolios/portfolios.tpl.html +++ b/src/app/units/states/portfolios/portfolios.tpl.html @@ -240,6 +240,7 @@

    Mark Portfolios

    + diff --git a/src/app/units/states/portfolios/upload-grades/upload-grades.component.html b/src/app/units/states/portfolios/upload-grades/upload-grades.component.html new file mode 100644 index 0000000000..c63db48f51 --- /dev/null +++ b/src/app/units/states/portfolios/upload-grades/upload-grades.component.html @@ -0,0 +1,3 @@ + diff --git a/src/app/units/states/portfolios/upload-grades/upload-grades.component.scss b/src/app/units/states/portfolios/upload-grades/upload-grades.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/units/states/portfolios/upload-grades/upload-grades.component.ts b/src/app/units/states/portfolios/upload-grades/upload-grades.component.ts new file mode 100644 index 0000000000..5d8e9a0020 --- /dev/null +++ b/src/app/units/states/portfolios/upload-grades/upload-grades.component.ts @@ -0,0 +1,55 @@ +import {Component, Inject, Input, OnInit} from '@angular/core'; +import {csvResultModalService, csvUploadModalService} from 'src/app/ajs-upgraded-providers'; +import {SidekiqJob} from 'src/app/api/models/sidekiq-job'; +import {Unit} from 'src/app/api/models/unit'; +import {SidekiqProgressModalService} from 'src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.service'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-upload-grades', + templateUrl: 'upload-grades.component.html', + styleUrl: 'upload-grades.component.scss', +}) +export class UploadGradesComponent implements OnInit { + @Input() unit: Unit; + + constructor( + @Inject(csvUploadModalService) private csvUploadModal: any, + private sidekiqModalService: SidekiqProgressModalService, + @Inject(csvResultModalService) private csvResultModal: any, + private alertService: AlertService, + ) {} + + public ngOnInit(): void { + if (!this.unit) { + return console.error(`Invalid unit`); + } + } + + public uploadGradesCSV() { + this.csvUploadModal.show( + 'Upload Student Grades as CSV', + 'Import student grades', + { + file: {name: 'Feedback Templates CSV Data', type: 'csv'}, + }, + this.unit.gradesCSVUploadUrl, + (response: SidekiqJob) => { + if (!response) { + this.alertService.error('Failed to import grades', 6000); + return; + } + + this.sidekiqModalService.show('Import student grades', response.id).subscribe({ + next: (job) => { + this.csvResultModal.show('Student grade import results', JSON.parse(job.result)); + }, + error: (error) => { + console.error(error); + this.alertService.error('Failed to import grades', 6000); + }, + }); + }, + ); + } +} From dd2582e2b418318dec015c715086cbfc679b48ac Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:36:07 +1100 Subject: [PATCH 634/776] chore(release): 10.0.0-60 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a6450419..4650b3c42e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-60](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-59...v10.0.0-60) (2025-11-05) + + +### Features + +* upload grades csv ([#1038](https://github.com/b0ink/doubtfire-deploy/issues/1038)) ([c67ceaa](https://github.com/b0ink/doubtfire-deploy/commit/c67ceaa84d02bb71626890fc28bb4aaea164b952)) + ## [10.0.0-59](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-58...v10.0.0-59) (2025-11-04) diff --git a/package-lock.json b/package-lock.json index 19cbbb3eb4..a2fb633540 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-59", + "version": "10.0.0-60", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-59", + "version": "10.0.0-60", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index c3df336ac0..be72c68d67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-59", + "version": "10.0.0-60", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 8bcffda3dfb73873c48d68fcce4e82920be19b4f Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:26:35 +1100 Subject: [PATCH 635/776] refactor: overseer sidekiq (#1029) * refactor: use sidekiq jobs for pulling overseer images * refactor: overseer execution script editor * chore: remove overseer view * refactor: increase size of script editor * chore: add icon for incomplete overseer runs * chore: add typing * refactor: format assessment comment layout * refactor: enable test submissions if script exists --- .../overseer-image-list.component.html | 35 +++++++++--- .../overseer-image-list.component.ts | 30 +++++++++-- .../models/overseer/overseer-assessment.ts | 28 +++++----- src/app/api/models/task-definition.ts | 1 + .../services/overseer-assessment.service.ts | 5 +- .../api/services/overseer-image.service.ts | 15 ++---- .../api/services/task-definition.service.ts | 1 + src/app/common/footer/footer.component.html | 4 ++ src/app/common/footer/footer.component.ts | 6 +++ src/app/doubtfire-angular.module.ts | 7 ++- .../task-assessment-comment.component.html | 19 ++++--- .../task-comments-viewer.component.html | 5 +- .../task-submission-history.component.html | 53 +++++++++++------- ...verseer-script-editor-modal.component.html | 14 +++++ ...verseer-script-editor-modal.component.scss | 0 .../overseer-script-editor-modal.component.ts | 54 +++++++++++++++++++ .../overseer-script-editor-modal.service.ts | 28 ++++++++++ .../task-definition-overseer.component.html | 21 +++----- .../task-definition-overseer.component.ts | 27 ++++++---- 19 files changed, 260 insertions(+), 93 deletions(-) create mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.html create mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.scss create mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.ts create mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.service.ts diff --git a/src/app/admin/institution-settings/overseer-images/overseer-image-list.component.html b/src/app/admin/institution-settings/overseer-images/overseer-image-list.component.html index d2e70e730d..bf6ca86233 100644 --- a/src/app/admin/institution-settings/overseer-images/overseer-image-list.component.html +++ b/src/app/admin/institution-settings/overseer-images/overseer-image-list.component.html @@ -1,3 +1,9 @@ + + +
    {{ data.text }}
    +
    +
    +
    @@ -5,7 +11,13 @@

    Overseer Images

    Add new image or modify existing ones used for automated task analysis

    - +
    @@ -51,7 +63,9 @@

    Overseer Images

    @@ -62,7 +76,7 @@

    Overseer Images

    @@ -80,10 +94,10 @@

    Overseer Images

    overseerImage.pulledImageStatus === 'success' ? 'green' : overseerImage.pulledImageStatus === 'loading' - ? 'orange' - : overseerImage.pulledImageStatus === 'failed' - ? 'red' - : '' + ? 'orange' + : overseerImage.pulledImageStatus === 'failed' + ? 'red' + : '' }" > @@ -113,7 +128,11 @@

    Overseer Images

    +
    Name
    - +
    Last Pulled
    - {{ overseerImage.lastPulledDate }} + {{ overseerImage.lastPulledDate | humanizedDate }}
    - diff --git a/src/app/admin/institution-settings/overseer-images/overseer-image-list.component.ts b/src/app/admin/institution-settings/overseer-images/overseer-image-list.component.ts index 703054827e..526660962d 100644 --- a/src/app/admin/institution-settings/overseer-images/overseer-image-list.component.ts +++ b/src/app/admin/institution-settings/overseer-images/overseer-image-list.component.ts @@ -1,17 +1,24 @@ -import {Component, ViewChild} from '@angular/core'; +import {AfterViewInit, Component, TemplateRef, ViewChild} from '@angular/core'; import {MatTableDataSource, MatTable} from '@angular/material/table'; import {OverseerImage, OverseerImageService} from 'src/app/api/models/doubtfire-model'; import {EntityFormComponent} from 'src/app/common/entity-form/entity-form.component'; import {UntypedFormControl, Validators} from '@angular/forms'; import {MatSort, Sort} from '@angular/material/sort'; import {AlertService} from 'src/app/common/services/alert.service'; +import {MatDialog} from '@angular/material/dialog'; +import {SidekiqProgressModalService} from 'src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.service'; @Component({ selector: 'overseer-image-list', templateUrl: 'overseer-image-list.component.html', styleUrls: ['overseer-image-list.component.scss'], }) -export class OverseerImageListComponent extends EntityFormComponent { +export class OverseerImageListComponent + extends EntityFormComponent + implements AfterViewInit +{ + @ViewChild('textDialog') textDialog!: TemplateRef; + @ViewChild(MatTable, {static: true}) table: MatTable; @ViewChild(MatSort, {static: true}) sort: MatSort; @@ -26,6 +33,8 @@ export class OverseerImageListComponent extends EntityFormComponent { - this.loading = false; + this.overseerImageService.pullDockerImage(image).subscribe((job) => { + this.sidekiqProgressModalService + .show(`Pulling image ${image.name} (${image.tag})`, job.id) + .subscribe((_job) => { + this.overseerImageService.fetch(image.id).subscribe((newImage) => { + console.log(newImage); + this.loading = false; + }); + }); }); } @@ -96,4 +112,10 @@ export class OverseerImageListComponent extends EntityFormComponent(mappingData: EntityMapping, ignoreKeys?: string[]): object { + public override toJson( + mappingData: EntityMapping, + ignoreKeys?: string[], + ): object { return { - overseer_assessment: super.toJson(mappingData, ignoreKeys) + overseer_assessment: super.toJson(mappingData, ignoreKeys), }; } } diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index 30aff474bf..e82ae135f0 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -47,6 +47,7 @@ export class TaskDefinition extends Entity { scormTimeDelayEnabled: boolean; scormAttemptLimit: number = 0; hasTaskAssessmentResources: boolean; + hasTaskAssessmentScript: boolean; isGraded: boolean; maxQualityPts: number; overseerImageId: number; diff --git a/src/app/api/services/overseer-assessment.service.ts b/src/app/api/services/overseer-assessment.service.ts index cfb21c8879..2226887caf 100644 --- a/src/app/api/services/overseer-assessment.service.ts +++ b/src/app/api/services/overseer-assessment.service.ts @@ -4,6 +4,7 @@ import {Observable} from 'rxjs'; import {HttpClient} from '@angular/common/http'; import API_URL from 'src/app/config/constants/apiUrl'; import {OverseerAssessment} from '../models/overseer/overseer-assessment'; +import {Task} from '../models/doubtfire-model'; @Injectable() export class OverseerAssessmentService extends EntityService { @@ -37,13 +38,13 @@ export class OverseerAssessmentService extends EntityService return new OverseerAssessment(other); } - public queryForTask(task: any): Observable { + public queryForTask(task: Task): Observable { const pathIds = { project_id: task.project.id, td_id: task.definition.id, }; - return this.query(pathIds, task); + return this.query(pathIds); } public triggerOverseer(assessment: OverseerAssessment): Observable { diff --git a/src/app/api/services/overseer-image.service.ts b/src/app/api/services/overseer-image.service.ts index ebb5558b3a..7edc65932f 100644 --- a/src/app/api/services/overseer-image.service.ts +++ b/src/app/api/services/overseer-image.service.ts @@ -4,6 +4,7 @@ import {OverseerImage} 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 {SidekiqJob} from '../models/sidekiq-job'; @Injectable() export class OverseerImageService extends CachedEntityService { @@ -25,16 +26,10 @@ export class OverseerImageService extends CachedEntityService { this.mapping.mapAllKeysToJsonExcept('id'); } - public pullDockerImage(image: OverseerImage): Observable { - return super - .put(image, { - endpointFormat: this.pullImageEndpointFormat, - }) - .pipe( - switchMap((response) => { - return super.update(image); - }), - ); + public pullDockerImage(image: OverseerImage): Observable { + return super.put(image, { + endpointFormat: this.pullImageEndpointFormat, + }); } public createInstanceFrom(json: object, other?: any): OverseerImage { diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index 63708814f8..06bea8da49 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -114,6 +114,7 @@ export class TaskDefinitionService extends CachedEntityService { 'hasTaskSheet', 'hasTaskResources', 'hasTaskAssessmentResources', + 'hasTaskAssessmentScript', 'scormEnabled', 'hasScormData', 'scormAllowReview', diff --git a/src/app/common/footer/footer.component.html b/src/app/common/footer/footer.component.html index 6fa4a27af5..8fd9e50a33 100644 --- a/src/app/common/footer/footer.component.html +++ b/src/app/common/footer/footer.component.html @@ -188,6 +188,10 @@ crisis_alert View similarities + -
    -
    - } @else { - -
    + @if (comment.assessment_result && comment.assessment_result.is_successful) { +
    +
    +
    {{ comment.text }}
    + +
    +
    + } @else { +
    } diff --git a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html index e0c2b036de..8cbc442d50 100644 --- a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html +++ b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html @@ -127,7 +127,10 @@
    -
    +
    -
    -
    -
    Submissions
    - +
    +
    +
    +
    Submissions
    + @for (tab of tabs; track tab) { - -
    {{tab.timestamp | date: 'dd/MM/yy, hh:mm a'}}
    -   -
    -} + +
    +
    + {{ tab.timestamp | humanizedDate }} +
    + @if (tab.status === 'pre_queued') { + schedule + } @else { + + } +
    +
    + }
    -
    +
    @for (selTab of selectedTab.content; track selTab) { - -
    {{selTab.result}} 
    - -
    -} + +
    {{ selTab.result }} 
    + +
    + }
    diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.html new file mode 100644 index 0000000000..1d6a563570 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.html @@ -0,0 +1,14 @@ +
    +
    +

    {{ data.taskDefinition.abbreviation }} {{ data.taskDefinition.name }}

    +

    Overseer script

    +
    +
    + +
    + @if (!loading) { + + } +
    diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.scss b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.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-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.ts new file mode 100644 index 0000000000..f64d3b4763 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.ts @@ -0,0 +1,54 @@ +import {HttpClient} from '@angular/common/http'; +import {Component, Inject, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {CodeModel} from '@ngstack/code-editor'; +import {AppInjector} from 'src/app/app-injector'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {OverseerScriptEditorModalData} from './overseer-script-editor-modal.service'; + +@Component({ + selector: 'f-overseer-script-editor-modal', + templateUrl: './overseer-script-editor-modal.component.html', + styleUrls: ['./overseer-script-editor-modal.component.scss'], +}) +export class OverseerScriptEditorModalComponent implements OnInit { + constructor( + @Inject(MAT_DIALOG_DATA) public data: OverseerScriptEditorModalData, + public dialogRef: MatDialogRef, + private httpClient: HttpClient, + ) {} + + public model: CodeModel = { + language: 'shell', + uri: 'run.sh', + value: '', + }; + + scriptContent: string; + + loading: boolean = false; + ngOnInit() { + this.loading = true; + // TODO: move to taskDefinition model + const url = `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.data.taskDefinition.unit.id}/task_definitions/${this.data.taskDefinition.id}/overseer_script`; + this.httpClient.get(url).subscribe((data: string) => { + this.model.value = data; + this.loading = false; + }); + } + + save() { + console.log(this.model.value); + const url = `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.data.taskDefinition.unit.id}/task_definitions/${this.data.taskDefinition.id}/overseer_script`; + + this.httpClient.put(url, {script_content: this.model.value}).subscribe({ + next: (result) => { + console.log(result); + this.dialogRef.close(); + }, + error: (error) => { + console.error(error); + }, + }); + } +} diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.service.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.service.ts new file mode 100644 index 0000000000..c238db3eaf --- /dev/null +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.service.ts @@ -0,0 +1,28 @@ +import {Injectable} from '@angular/core'; +import {MatDialog} from '@angular/material/dialog'; +import {OverseerScriptEditorModalComponent} from './overseer-script-editor-modal.component'; +import {TaskDefinition} from 'src/app/api/models/task-definition'; + +export interface OverseerScriptEditorModalData { + taskDefinition: TaskDefinition; +} + +@Injectable({ + providedIn: 'root', +}) +export class OverseerScriptEditorModalService { + constructor(public dialog: MatDialog) {} + + public show(taskDefinition: TaskDefinition) { + const _dialogRef = this.dialog.open< + OverseerScriptEditorModalComponent, + OverseerScriptEditorModalData + >(OverseerScriptEditorModalComponent, { + data: { + taskDefinition: taskDefinition, + }, + width: '100%', + maxWidth: '1200px', + }); + } +} diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html index bf187030aa..0e5c521bcb 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html @@ -18,6 +18,10 @@ Docker image for Overseer + + @if (taskDefinition.hasTaskAssessmentResources) {
    - -
    diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.ts index 597296d707..ea0f92ba89 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.ts @@ -1,5 +1,5 @@ -import { Component, Input, OnChanges } from '@angular/core'; -import { Observable } from 'rxjs'; +import {Component, Input, OnChanges} from '@angular/core'; +import {Observable} from 'rxjs'; import { OverseerAssessment, OverseerImage, @@ -8,13 +8,14 @@ import { User, UserService, } from 'src/app/api/models/doubtfire-model'; -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 { FileDownloaderService } from 'src/app/common/file-downloader/file-downloader.service'; -import { TaskAssessmentModalService } from 'src/app/common/modals/task-assessment-modal/task-assessment-modal.service'; -import { AlertService } from 'src/app/common/services/alert.service'; -import { TaskSubmissionService } from 'src/app/common/services/task-submission.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 {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; +import {TaskAssessmentModalService} from 'src/app/common/modals/task-assessment-modal/task-assessment-modal.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {TaskSubmissionService} from 'src/app/common/services/task-submission.service'; +import {OverseerScriptEditorModalService} from './overseer-script-editor-modal/overseer-script-editor-modal.service'; @Component({ selector: 'f-task-definition-overseer', @@ -34,6 +35,7 @@ export class TaskDefinitionOverseerComponent implements OnChanges { private userService: UserService, private taskDefinitionService: TaskDefinitionService, private fileDownloaderService: FileDownloaderService, + private overseerScriptEditorModal: OverseerScriptEditorModalService, ) {} public get overseerEnabled(): boolean { @@ -73,6 +75,10 @@ export class TaskDefinitionOverseerComponent implements OnChanges { this.currentUserTask.presentTaskSubmissionModal(this.currentUserTask.status, false, true); } + editScript() { + this.overseerScriptEditorModal.show(this.taskDefinition); + } + testSubmissionHistory() { this.modalService.show(this.currentUserTask); } @@ -93,7 +99,7 @@ export class TaskDefinitionOverseerComponent implements OnChanges { next: () => { this.alerts.success('Deleted Overseer Resources', 2000); this.taskDefinition.hasTaskAssessmentResources = false; - } + }, }); } @@ -104,7 +110,6 @@ export class TaskDefinitionOverseerComponent implements OnChanges { ); } - public uploadOverseerResources(files: FileList) { const validFiles = Array.from(files as ArrayLike).filter( (f) => f.type === 'application/zip' || f.type === 'application/x-zip-compressed', From 1a4a3f6c64743f7fd7e9d96697bdfa4cdb792c8e Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:36:36 +1100 Subject: [PATCH 636/776] chore(release): 10.0.0-61 --- CHANGELOG.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4650b3c42e..c2462bcfcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-61](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-60...v10.0.0-61) (2025-11-06) + ## [10.0.0-60](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-59...v10.0.0-60) (2025-11-05) diff --git a/package-lock.json b/package-lock.json index a2fb633540..0f3a3c3b45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-60", + "version": "10.0.0-61", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-60", + "version": "10.0.0-61", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index be72c68d67..09053700c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-60", + "version": "10.0.0-61", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 29fbc1498b0d48811cb59c5f2a66871fbac06849 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:59:27 +1100 Subject: [PATCH 637/776] feat: restrict assessments to tutors in the same tutorial stream (#1033) * feat: restrict assessments to tutors in the same tutorial stream * refactor: unselect tasks that are locked to a tutorial stream --- src/app/api/models/task-definition.ts | 1 + src/app/api/services/task-definition.service.ts | 1 + .../tutor-discussion.component.html | 6 +++++- .../tutor-discussion/tutor-discussion.component.ts | 14 ++++++++++++++ .../task-definition-who.component.html | 6 ++++++ 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index e82ae135f0..1560b94909 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -56,6 +56,7 @@ export class TaskDefinition extends Entity { hasJplagReport: boolean; assessInPortfolioOnly: boolean; useResourcesForJplagBaseCode: boolean; + lockAssessmentsToTutorialStream: boolean; public readonly taskPrerequisitesCache: EntityCache = new EntityCache(); diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index 06bea8da49..cb36d3cc32 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -138,6 +138,7 @@ export class TaskDefinitionService extends CachedEntityService { }, }, 'useResourcesForJplagBaseCode', + 'lockAssessmentsToTutorialStream', ); this.mapping.mapAllKeysToJsonExcept( diff --git a/src/app/projects/states/tutor-discussion/tutor-discussion.component.html b/src/app/projects/states/tutor-discussion/tutor-discussion.component.html index 0c669784dd..89cf35865f 100644 --- a/src/app/projects/states/tutor-discussion/tutor-discussion.component.html +++ b/src/app/projects/states/tutor-discussion/tutor-discussion.component.html @@ -81,7 +81,11 @@ class="clearfix w-full" style="padding: 0; height: 60px" togglePosition="before" - [selected]="task.status === 'discuss' || attendance" + [selected]=" + (task.status === 'discuss' || attendance) && + (!task.definition.lockAssessmentsToTutorialStream || + currentUserTutorsInStream(task.definition.tutorialStream)) + " > @if (task) {
    diff --git a/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts b/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts index 445eb16cc3..567988d53d 100644 --- a/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts +++ b/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts @@ -12,6 +12,7 @@ import { TaskDefinition, TaskService, TaskStatusEnum, + TutorialStream, Unit, UnitService, UserService, @@ -69,6 +70,19 @@ export class TutorDiscussionComponent implements AfterViewInit { private taskService: TaskService, ) {} + public currentUserTutorsInStream(tutorialStream: TutorialStream): boolean { + const user = this.userService.currentUser; + const tutorials = this.unit.tutorials.filter( + (t) => + t.tutorialStream.abbreviation === tutorialStream.abbreviation && + t.tutorialStream.name === tutorialStream.name, + ); + if (tutorials.some((t) => t.tutor.id === user.id)) { + return true; + } + return false; + } + onTabChange(event: MatTabChangeEvent): void { if (event.index === 0) { this.showComments(); diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-who/task-definition-who.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-who/task-definition-who.component.html index 5d93345d7f..f36a92f380 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-who/task-definition-who.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-who/task-definition-who.component.html @@ -56,3 +56,9 @@
    + +
    + Only allow tutors in this tutorial stream to assess this task +
    From 8a56b651ec2f032585de734d8e5f66908fa7098a Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:28:29 +1100 Subject: [PATCH 638/776] chore(release): 10.0.0-62 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2462bcfcf..7fde181cd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-62](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-61...v10.0.0-62) (2025-11-06) + + +### Features + +* restrict assessments to tutors in the same tutorial stream ([#1033](https://github.com/b0ink/doubtfire-deploy/issues/1033)) ([29fbc14](https://github.com/b0ink/doubtfire-deploy/commit/29fbc1498b0d48811cb59c5f2a66871fbac06849)) + ## [10.0.0-61](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-60...v10.0.0-61) (2025-11-06) ## [10.0.0-60](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-59...v10.0.0-60) (2025-11-05) diff --git a/package-lock.json b/package-lock.json index 0f3a3c3b45..8087a7e128 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-61", + "version": "10.0.0-62", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-61", + "version": "10.0.0-62", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 09053700c4..21346adcca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-61", + "version": "10.0.0-62", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 24f358ecae920e3eeb2f8b50e181be0ae68afde2 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Tue, 30 Sep 2025 13:39:20 +1000 Subject: [PATCH 639/776] refactor: replace range datepickers with three separate datepickers --- .../task-definition-dates.component.html | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-dates/task-definition-dates.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-dates/task-definition-dates.component.html index a2285a0969..c6a3eb74c6 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-dates/task-definition-dates.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-dates/task-definition-dates.component.html @@ -1,43 +1,43 @@
    - - Start date to suggested completion date - - - - - - - + + Start Date + + + + Suggested date for students to begin the task. - - Suggestion completion date to final feedback - - - - + + Target Date + + + + Recommended target date for students to complete the task. + - - + + Final Feedback Date + + + + Final deadline for receiving feedback.
    From 6c4931bd40e94b9a2fbafe3b0b6c9d8670564c74 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:17:53 +1100 Subject: [PATCH 640/776] chore: replace doubtfire with ontrack --- src/app/common/file-uploader/file-uploader.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/common/file-uploader/file-uploader.coffee b/src/app/common/file-uploader/file-uploader.coffee index c492dac115..89c0c17755 100644 --- a/src/app/common/file-uploader/file-uploader.coffee +++ b/src/app/common/file-uploader/file-uploader.coffee @@ -263,7 +263,7 @@ angular.module('doubtfire.common.file-uploader', ["ngFileUpload"]) response = JSON.parse xhr.responseText catch e if xhr.status is 0 - response = { error: 'Could not connect to the Doubtfire server' } + response = { error: 'Could not connect to the OnTrack server' } else response = xhr.responseText # Success (20x success range) From 5b4b097a9b189e5a816695cce7438104f1144283 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:21:56 +1100 Subject: [PATCH 641/776] feat: open project dashboard from portfolios view (#1040) --- src/app/units/states/portfolios/portfolios.coffee | 4 ++++ src/app/units/states/portfolios/portfolios.tpl.html | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/src/app/units/states/portfolios/portfolios.coffee b/src/app/units/states/portfolios/portfolios.coffee index e87a17027d..d13cca4433 100644 --- a/src/app/units/states/portfolios/portfolios.coffee +++ b/src/app/units/states/portfolios/portfolios.coffee @@ -149,4 +149,8 @@ angular.module('doubtfire.units.states.portfolios', []) $scope.transferToD2L = -> D2lTransferModal.open($scope.unit) + + $scope.openProject = ($event, project) -> + $event.stopPropagation() + window.open("/projects/#{project.id}/dashboard/?tutor=true", "_blank") ) diff --git a/src/app/units/states/portfolios/portfolios.tpl.html b/src/app/units/states/portfolios/portfolios.tpl.html index 376277513e..34d00d5d7f 100644 --- a/src/app/units/states/portfolios/portfolios.tpl.html +++ b/src/app/units/states/portfolios/portfolios.tpl.html @@ -214,6 +214,13 @@

    Mark Portfolios

    {{student.hasPortfolio ? "Yes" : "No"}}
    {{student.grade}} + +
    From 770b1718f307f6dbb835bef706bff1aa46bb68d5 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:23:30 +1100 Subject: [PATCH 642/776] chore(release): 10.0.0-63 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fde181cd4..25972b520c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-63](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-62...v10.0.0-63) (2025-11-10) + + +### Features + +* open project dashboard from portfolios view ([5b4b097](https://github.com/b0ink/doubtfire-deploy/commit/5b4b097a9b189e5a816695cce7438104f1144283)), closes [#1040](https://github.com/b0ink/doubtfire-deploy/issues/1040) + ## [10.0.0-62](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-61...v10.0.0-62) (2025-11-06) diff --git a/package-lock.json b/package-lock.json index 8087a7e128..ac840fcd6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-62", + "version": "10.0.0-63", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-62", + "version": "10.0.0-63", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 21346adcca..47621b97b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-62", + "version": "10.0.0-63", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 9ab015d168cf7e421500910766b9e6fb0bf907f5 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:49:02 +1100 Subject: [PATCH 643/776] fix: avoid calling window to open project in new tab --- src/app/units/states/portfolios/portfolios.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/units/states/portfolios/portfolios.coffee b/src/app/units/states/portfolios/portfolios.coffee index d13cca4433..62c70a04f3 100644 --- a/src/app/units/states/portfolios/portfolios.coffee +++ b/src/app/units/states/portfolios/portfolios.coffee @@ -152,5 +152,9 @@ angular.module('doubtfire.units.states.portfolios', []) $scope.openProject = ($event, project) -> $event.stopPropagation() - window.open("/projects/#{project.id}/dashboard/?tutor=true", "_blank") + # HACK: avoids using window.open() to prevent AngularJS error + link = document.createElement('a') + link.href = "/projects/#{project.id}/dashboard/?tutor=true" + link.target = '_blank' + link.click() ) From 7d1dbc33476cd669ab5217cfd70a53017c557d6e Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:04:28 +1100 Subject: [PATCH 644/776] refactor: move overseer script url to task definition model --- src/app/api/models/task-definition.ts | 4 ++ .../overseer-script-editor-modal.component.ts | 40 +++++++++---------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index 1560b94909..814c7a9c53 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -276,6 +276,10 @@ export class TaskDefinition extends Entity { }/task_assessment_resources.json`; } + public get taskOverseerExecutionScriptUrl() { + return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.unit.id}/task_definitions/${this.id}/overseer_script`; + } + public getJplagReportUrl() { return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.unit.id}/task_definitions/${this.id}/jplag_report`; } diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.ts index f64d3b4763..ddcf7c1d40 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.ts @@ -2,8 +2,7 @@ import {HttpClient} from '@angular/common/http'; import {Component, Inject, OnInit} from '@angular/core'; import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; import {CodeModel} from '@ngstack/code-editor'; -import {AppInjector} from 'src/app/app-injector'; -import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {AlertService} from 'src/app/common/services/alert.service'; import {OverseerScriptEditorModalData} from './overseer-script-editor-modal.service'; @Component({ @@ -16,6 +15,7 @@ export class OverseerScriptEditorModalComponent implements OnInit { @Inject(MAT_DIALOG_DATA) public data: OverseerScriptEditorModalData, public dialogRef: MatDialogRef, private httpClient: HttpClient, + private alertService: AlertService, ) {} public model: CodeModel = { @@ -29,26 +29,26 @@ export class OverseerScriptEditorModalComponent implements OnInit { loading: boolean = false; ngOnInit() { this.loading = true; - // TODO: move to taskDefinition model - const url = `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.data.taskDefinition.unit.id}/task_definitions/${this.data.taskDefinition.id}/overseer_script`; - this.httpClient.get(url).subscribe((data: string) => { - this.model.value = data; - this.loading = false; - }); + this.httpClient + .get(this.data.taskDefinition.taskOverseerExecutionScriptUrl) + .subscribe((data: string) => { + this.model.value = data; + this.loading = false; + }); } save() { - console.log(this.model.value); - const url = `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.data.taskDefinition.unit.id}/task_definitions/${this.data.taskDefinition.id}/overseer_script`; - - this.httpClient.put(url, {script_content: this.model.value}).subscribe({ - next: (result) => { - console.log(result); - this.dialogRef.close(); - }, - error: (error) => { - console.error(error); - }, - }); + this.httpClient + .put(this.data.taskDefinition.taskOverseerExecutionScriptUrl, { + script_content: this.model.value, + }) + .subscribe({ + next: (_result) => { + this.dialogRef.close(); + }, + error: (error) => { + this.alertService.error(`Failed to save script: ${error}`, 6000); + }, + }); } } From 8eaffbcb65b835d615646d4c9560cbf1881deeb9 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:17:16 +1100 Subject: [PATCH 645/776] refactor: base64 encode overseer script before sending to server --- .../overseer-script-editor-modal.component.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.ts index ddcf7c1d40..128e2410f0 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component.ts @@ -38,9 +38,12 @@ export class OverseerScriptEditorModalComponent implements OnInit { } save() { + const scriptOriginal = this.model.value; + const scriptEncoded = this.base64UrlEncode(scriptOriginal); + this.httpClient .put(this.data.taskDefinition.taskOverseerExecutionScriptUrl, { - script_content: this.model.value, + script_content: scriptEncoded, }) .subscribe({ next: (_result) => { @@ -51,4 +54,8 @@ export class OverseerScriptEditorModalComponent implements OnInit { }, }); } + + private base64UrlEncode(str) { + return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + } } From 40f2640e6579536cef8e82e9476733083b82bac1 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Thu, 13 Nov 2025 08:15:27 +1100 Subject: [PATCH 646/776] fix: use new google fonts api for proper weight loading (#1039) --- src/index.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/index.html b/src/index.html index 2e715e206f..af609f230c 100644 --- a/src/index.html +++ b/src/index.html @@ -18,7 +18,10 @@ href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet" /> - + Loading... From 5aef6cfddaf61408e6f3169002f9b8ce00b7dfac Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:09:02 +1100 Subject: [PATCH 647/776] chore: specify only time exceeded tasks are updated --- .../unit-details-editor/unit-details-editor.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.ts b/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.ts index 0ea300bf8e..7ac7161a73 100644 --- a/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.ts +++ b/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.ts @@ -90,7 +90,7 @@ export class UnitDetailsEditorComponent implements OnInit { const modal = this.confirmationModal.show( 'Enable Assess in Portfolio?', `Are you sure you want to enable "Assess in Portfolio" for late submissions? - This will update any existing Time/Feedback Exceeded tasks to the "Assess in Portfolio" state. + This will update any existing Time Exceeded tasks to the "Assess in Portfolio" state. You will not be able to disable this setting while any tasks remain in the "Assess in Portfolio" state.`, () => { this.unit.markLateSubmissionsAsAssessInPortfolio = true; From f505823498bd52f6282d613512c085f8978f48b7 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:58:21 +1100 Subject: [PATCH 648/776] chore(release): 10.0.0-64 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25972b520c..e1e42ecd66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-64](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-63...v10.0.0-64) (2025-11-25) + + +### Bug Fixes + +* avoid calling window to open project in new tab ([9ab015d](https://github.com/b0ink/doubtfire-deploy/commit/9ab015d168cf7e421500910766b9e6fb0bf907f5)) +* use new google fonts api for proper weight loading ([#1039](https://github.com/b0ink/doubtfire-deploy/issues/1039)) ([40f2640](https://github.com/b0ink/doubtfire-deploy/commit/40f2640e6579536cef8e82e9476733083b82bac1)) + ## [10.0.0-63](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-62...v10.0.0-63) (2025-11-10) diff --git a/package-lock.json b/package-lock.json index ac840fcd6d..99a5447d53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-63", + "version": "10.0.0-64", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-63", + "version": "10.0.0-64", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 47621b97b8..2945358894 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-63", + "version": "10.0.0-64", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From bdcd75d91d5ddc7d9a60317bd91289a7242251e1 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:11:37 +1100 Subject: [PATCH 649/776] fix: typo --- .../upload-submission-modal/upload-submission-modal.tpl.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html index 65695e16ec..bfda995467 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html @@ -101,7 +101,7 @@

    Final comments

    -
    Character count: {{comment.length}} (Min. 25)
    +
    Character count: {{comment.length}} (Min. 25)
    From 665d6ae86b65e9e8ba83df1ad3d4a053a4553851 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:11:44 +1100 Subject: [PATCH 650/776] chore(release): 10.0.0-65 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1e42ecd66..6935c31476 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-65](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-64...v10.0.0-65) (2025-11-25) + + +### Bug Fixes + +* typo ([bdcd75d](https://github.com/b0ink/doubtfire-deploy/commit/bdcd75d91d5ddc7d9a60317bd91289a7242251e1)) + ## [10.0.0-64](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-63...v10.0.0-64) (2025-11-25) diff --git a/package-lock.json b/package-lock.json index 99a5447d53..c30263893b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-64", + "version": "10.0.0-65", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-64", + "version": "10.0.0-65", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 2945358894..fb458e4b24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-64", + "version": "10.0.0-65", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 0611be04e8fe7e75ae07504945a2984839475e3a Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:27:41 +1100 Subject: [PATCH 651/776] fix: correctly set rollover end date --- .../directives/unit-dates-selector/unit-dates-selector.coffee | 4 ++-- .../unit-dates-selector/unit-dates-selector.tpl.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/units/states/rollover/directives/unit-dates-selector/unit-dates-selector.coffee b/src/app/units/states/rollover/directives/unit-dates-selector/unit-dates-selector.coffee index 07569a20a0..1542d89d20 100644 --- a/src/app/units/states/rollover/directives/unit-dates-selector/unit-dates-selector.coffee +++ b/src/app/units/states/rollover/directives/unit-dates-selector/unit-dates-selector.coffee @@ -62,8 +62,8 @@ angular.module('doubtfire.units.states.rollover.directives.unit-dates-selector', } else body = { - start_date: $scope.saveData.startDate - end_date: $scope.saeData.endDate + start_date: $scope.saveData.startDate, + end_date: $scope.saveData.endDate } $scope.unit.rolloverTo(body).subscribe({ next: (response) -> diff --git a/src/app/units/states/rollover/directives/unit-dates-selector/unit-dates-selector.tpl.html b/src/app/units/states/rollover/directives/unit-dates-selector/unit-dates-selector.tpl.html index c3be994325..fdfe7f5f21 100644 --- a/src/app/units/states/rollover/directives/unit-dates-selector/unit-dates-selector.tpl.html +++ b/src/app/units/states/rollover/directives/unit-dates-selector/unit-dates-selector.tpl.html @@ -39,11 +39,11 @@

    Copy {{unit.code}} {{unit.nameAndPeriod}}

    - + Date: Mon, 1 Dec 2025 13:50:42 +1100 Subject: [PATCH 652/776] feat: discussion prompts (#1042) * feat: init discussion prompts ui * refactor: improve tutor discussion ui * refactor: display discussion prompts in tutor discussion view * refactor: reword weight to priority --- src/app/api/models/discussion-prompt.ts | 59 +++++++ src/app/api/models/task-definition.ts | 5 + .../api/services/discussion-prompt.service.ts | 128 +++++++++++++++ .../api/services/task-definition.service.ts | 1 + src/app/common/footer/footer.component.html | 13 ++ src/app/common/footer/footer.component.ts | 4 + src/app/doubtfire-angular.module.ts | 9 + .../discussion-prompts-view.component.html | 11 ++ .../discussion-prompts-view.component.scss | 0 .../discussion-prompts-view.component.ts | 11 ++ .../task-dashboard.component.html | 7 + .../states/dashboard/selected-task.service.ts | 5 + .../discussion-prompts.component.html | 21 +++ .../discussion-prompts.component.scss | 0 .../discussion-prompts.component.ts | 50 ++++++ .../tutor-discussion.component.html | 15 +- .../tutor-discussion.component.ts | 7 + ...finition-discussion-prompts.component.html | 107 ++++++++++++ ...finition-discussion-prompts.component.scss | 0 ...definition-discussion-prompts.component.ts | 155 ++++++++++++++++++ .../task-definition-editor.component.html | 12 ++ 21 files changed, 614 insertions(+), 6 deletions(-) create mode 100644 src/app/api/models/discussion-prompt.ts create mode 100644 src/app/api/services/discussion-prompt.service.ts create mode 100644 src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.html create mode 100644 src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.scss create mode 100644 src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.ts create mode 100644 src/app/projects/states/discussion-prompts/discussion-prompts.component.html create mode 100644 src/app/projects/states/discussion-prompts/discussion-prompts.component.scss create mode 100644 src/app/projects/states/discussion-prompts/discussion-prompts.component.ts create mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.html create mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.scss create mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.ts diff --git a/src/app/api/models/discussion-prompt.ts b/src/app/api/models/discussion-prompt.ts new file mode 100644 index 0000000000..e984847084 --- /dev/null +++ b/src/app/api/models/discussion-prompt.ts @@ -0,0 +1,59 @@ +import {Entity} from 'ngx-entity-service'; +import {AppInjector} from 'src/app/app-injector'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {DiscussionPromptService} from '../services/discussion-prompt.service'; +import {Project, TaskDefinition, Unit, User} from './doubtfire-model'; + +export class DiscussionPrompt extends Entity { + id: number; + unit: Unit; + taskDefinition: TaskDefinition; + project: Project | null; + createdBy: User; + content: string; + priority: number; + discussedAt: Date; + + public readonly PRIORITY = { + 1: 'Low', + 2: 'Medium', + 3: 'High', + } as const; + + constructor(data?: Project | TaskDefinition | Unit) { + super(); + if (data) { + if (data instanceof Project) { + this.project = data; + } else if (data instanceof TaskDefinition) { + this.taskDefinition = data; + } else if (data instanceof Unit) { + this.unit = data; + } + } else { + console.error('Failed to get project'); + } + } + + public get priorityLabel() { + return this.PRIORITY[this.priority] ?? this.priority; + } + + public delete() { + const discussionPromptService: DiscussionPromptService = + AppInjector.get(DiscussionPromptService); + discussionPromptService + .delete( + {task_definition_id: this.taskDefinition.id, id: this.id}, + {cache: this.taskDefinition.discussionPromptsCache}, + ) + .subscribe({ + next: (_response: object) => { + AppInjector.get(AlertService).success('Successfully deleted discussion note', 4000); + }, + error: (error: any) => { + AppInjector.get(AlertService).error(error?.message || error || 'Unknown error', 2000); + }, + }); + } +} diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index 814c7a9c53..d3aa2a4317 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -8,6 +8,7 @@ import {TaskDefinitionService} from '../services/task-definition.service'; import {Grade, GroupSet, LearningOutcome, Project, TutorialStream, Unit} from './doubtfire-model'; import {Task} from './doubtfire-model'; import {TaskPrerequisite} from './task-prerequisite'; +import {DiscussionPrompt} from './discussion-prompt'; export type UploadRequirement = { key: string; @@ -57,10 +58,14 @@ export class TaskDefinition extends Entity { assessInPortfolioOnly: boolean; useResourcesForJplagBaseCode: boolean; lockAssessmentsToTutorialStream: boolean; + discussionPromptsCount: number; public readonly taskPrerequisitesCache: EntityCache = new EntityCache(); + public readonly discussionPromptsCache: EntityCache = + new EntityCache(); + public readonly learningOutcomesCache: EntityCache = new EntityCache(); diff --git a/src/app/api/services/discussion-prompt.service.ts b/src/app/api/services/discussion-prompt.service.ts new file mode 100644 index 0000000000..c102fbcbf1 --- /dev/null +++ b/src/app/api/services/discussion-prompt.service.ts @@ -0,0 +1,128 @@ +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {CachedEntityService, RequestOptions} from 'ngx-entity-service'; +import {Observable} from 'rxjs'; +import { + Project, + ProjectService, + TaskDefinition, + Unit, + UserService, +} from 'src/app/api/models/doubtfire-model'; +import API_URL from 'src/app/config/constants/apiUrl'; +import {DiscussionPrompt} from '../models/discussion-prompt'; + +@Injectable() +export class DiscussionPromptService extends CachedEntityService { + protected readonly endpointFormat = + 'task_definitions/:task_definition_id:/discussion_prompts/:id:'; + + protected readonly projectEndpointFormat = 'projects/:projectId:/discussion_prompts'; + protected readonly taskDefinitionProjectEndpointFormat = + 'projects/:projectId:/discussion_prompts'; + protected readonly taskDefinitionEndpointFormat = + 'task_definitions/:taskDefinitionId:/discussion_prompts'; + + constructor( + httpClient: HttpClient, + private userService: UserService, + private projectService: ProjectService, + ) { + super(httpClient, API_URL); + + this.mapping.addKeys( + 'id', + 'content', + 'priority', + { + keys: ['createdBy', 'created_by_id'], + toEntityFn: (data: object, key: string, prompt: DiscussionPrompt) => { + if (prompt.project) { + return prompt.project.unit.staff.find((s) => s.user.id === data['created_by_id']).user; + } else if (prompt.taskDefinition) { + return prompt.taskDefinition.unit.staff.find((s) => s.user.id === data['created_by_id']) + ?.user; + } else if (prompt.unit) { + return prompt.unit.staff.find((s) => s.user.id === data['created_by_id'])?.user; + } + }, + }, + { + keys: ['taskDefinition', 'task_definition_id'], + toEntityFn: (data: object, key: string, entity: DiscussionPrompt) => { + if (entity.project) { + return entity.project.unit.taskDef(data[key]); + } else if (entity.unit) { + return entity.unit.taskDef(data[key]); + } + return entity.taskDefinition; + }, + toJsonFn: (entity: DiscussionPrompt, key: string) => { + return entity.taskDefinition?.id; + }, + }, + ); + + this.mapping.addJsonKey('project', 'taskDefinition', 'unit', 'createdBy'); + } + + public createInstanceFrom( + _json: object, + other?: Project | Unit | TaskDefinition, + ): DiscussionPrompt { + return new DiscussionPrompt(other); + } + + // TODO: loadDiscussionPromptsForProject and overload for loadTaskDefinitionDiscussionPrompts() + + public loadDiscussionPromptsForPoject(project: Project) { + const options: RequestOptions = { + endpointFormat: this.taskDefinitionProjectEndpointFormat, + cacheBehaviourOnGet: 'cacheQuery', + constructorParams: project, + }; + + return super.fetchAll( + { + projectId: project?.id, + }, + options, + ); + } + + public loadDiscussionPrompts( + project: Project, + taskDefinition?: TaskDefinition, + useFetch: boolean = true, + ): Observable { + const options: RequestOptions = { + endpointFormat: project + ? taskDefinition + ? this.taskDefinitionProjectEndpointFormat + : this.projectEndpointFormat + : this.taskDefinitionEndpointFormat, + cache: taskDefinition.discussionPromptsCache, + sourceCache: taskDefinition.discussionPromptsCache, + // cacheBehaviourOnGet: 'cacheQuery', + constructorParams: project ? project : taskDefinition, + }; + + if (useFetch) { + return super.fetchAll( + { + projectId: project?.id, + taskDefinitionId: taskDefinition?.id, + }, + options, + ); + } else { + return super.query( + { + projectId: project?.id, + taskDefinitionId: taskDefinition?.id, + }, + options, + ); + } + } +} diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index cb36d3cc32..59664c143a 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -125,6 +125,7 @@ export class TaskDefinitionService extends CachedEntityService { 'maxQualityPts', 'overseerImageId', 'assessmentEnabled', + 'discussionPromptsCount', { keys: 'ilos', toEntityOp: (data: object, key: string, taskDefinition: TaskDefinition) => { diff --git a/src/app/common/footer/footer.component.html b/src/app/common/footer/footer.component.html index 8fd9e50a33..e7d84ac5e5 100644 --- a/src/app/common/footer/footer.component.html +++ b/src/app/common/footer/footer.component.html @@ -128,6 +128,19 @@
    } @if (selectedTask?.project) { + @if (selectedTask?.definition.discussionPromptsCount) { + + } +
    @if (selectedTask.project.staffNoteCount > 0) {
    diff --git a/src/app/common/footer/footer.component.ts b/src/app/common/footer/footer.component.ts index 7e38438381..295b8612ee 100644 --- a/src/app/common/footer/footer.component.ts +++ b/src/app/common/footer/footer.component.ts @@ -107,6 +107,10 @@ export class FooterComponent implements OnInit { this.selectedTaskService.showStaffNotes(); } + viewDiscussionPrompts() { + this.selectedTaskService.showDiscussionPrompts(); + } + getJplagReport() { if (!this.selectedTask?.definition) { return; diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 318c601b35..7938a4e274 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -297,6 +297,10 @@ import {PortfolioIncludedTasksComponent} from './projects/states/portfolio/direc import {OverseerScriptEditorModalComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component'; import {CodeEditorModule} from '@ngstack/code-editor'; import {UploadGradesComponent} from './units/states/portfolios/upload-grades/upload-grades.component'; +import {DiscussionPromptService} from './api/services/discussion-prompt.service'; +import {DiscussionPromptsComponent} from './projects/states/discussion-prompts/discussion-prompts.component'; +import {TaskDefinitionDiscussionPromptsComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component'; +import {DiscussionPromptsViewComponent} from './projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.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 = { @@ -452,6 +456,10 @@ const MY_DATE_FORMAT = { AnalyticsTutorTimesComponent, PortfolioIncludedTasksComponent, OverseerScriptEditorModalComponent, + UploadGradesComponent, + DiscussionPromptsComponent, + TaskDefinitionDiscussionPromptsComponent, + DiscussionPromptsViewComponent, ], providers: [ // Services we provide @@ -541,6 +549,7 @@ const MY_DATE_FORMAT = { LtiService, TaskPrerequisiteService, MarkingSessionService, + DiscussionPromptService, ], imports: [ FlexLayoutModule, diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.html new file mode 100644 index 0000000000..8cc634a7fd --- /dev/null +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.html @@ -0,0 +1,11 @@ +
    +
    + comment +

    Discussion Prompts for {{ project?.student?.name }}

    +
    + + +
    diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.scss b/src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.ts new file mode 100644 index 0000000000..6156a4b191 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.ts @@ -0,0 +1,11 @@ +import {Component, Input} from '@angular/core'; + +@Component({ + selector: 'f-discussion-prompts-view', + templateUrl: './discussion-prompts-view.component.html', + styleUrls: ['./discussion-prompts-view.component.scss'], +}) +export class DiscussionPromptsViewComponent { + @Input() project; + @Input() taskDefinition; +} diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.html index 705ab5b6d3..df91328f92 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.html @@ -19,6 +19,13 @@ + + + + diff --git a/src/app/projects/states/dashboard/selected-task.service.ts b/src/app/projects/states/dashboard/selected-task.service.ts index 31b5dd8716..204714694e 100644 --- a/src/app/projects/states/dashboard/selected-task.service.ts +++ b/src/app/projects/states/dashboard/selected-task.service.ts @@ -9,6 +9,7 @@ export enum DashboardViews { task, similarity, staff_notes, + discussion_prompts, } @Injectable({ @@ -66,6 +67,10 @@ export class SelectedTaskService { this.currentView$.next(DashboardViews.staff_notes); } + public showDiscussionPrompts() { + this.currentView$.next(DashboardViews.discussion_prompts); + } + public showSubmission() { if (!this.task$.value) return; this.currentPdfUrl$.next(this.task$.value.submissionUrl(false)); diff --git a/src/app/projects/states/discussion-prompts/discussion-prompts.component.html b/src/app/projects/states/discussion-prompts/discussion-prompts.component.html new file mode 100644 index 0000000000..5dcbbf84ff --- /dev/null +++ b/src/app/projects/states/discussion-prompts/discussion-prompts.component.html @@ -0,0 +1,21 @@ +
    + @for (prompt of discussionPrompts; track prompt) { + + +
    + {{ prompt.taskDefinition.abbreviation }} +
    +
    + {{ prompt.content }} +
    +
    + {{ prompt.priorityLabel }} +
    +
    +
    + } +
    diff --git a/src/app/projects/states/discussion-prompts/discussion-prompts.component.scss b/src/app/projects/states/discussion-prompts/discussion-prompts.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/discussion-prompts/discussion-prompts.component.ts b/src/app/projects/states/discussion-prompts/discussion-prompts.component.ts new file mode 100644 index 0000000000..2e384498af --- /dev/null +++ b/src/app/projects/states/discussion-prompts/discussion-prompts.component.ts @@ -0,0 +1,50 @@ +import {Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core'; +import {DiscussionPrompt} from 'src/app/api/models/discussion-prompt'; +import {Project, TaskDefinition, UserService} from 'src/app/api/models/doubtfire-model'; +import {StaffNote} from 'src/app/api/models/staff-note'; +import {DiscussionPromptService} from 'src/app/api/services/discussion-prompt.service'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-discussion-prompts', + templateUrl: './discussion-prompts.component.html', + styleUrl: './discussion-prompts.component.scss', +}) +export class DiscussionPromptsComponent implements OnInit { + @ViewChild('staffNotesContainer') staffNotesContainer!: ElementRef; + @ViewChild('staffNoteEditor', {static: false}) staffNoteEditor!: ElementRef; + + @Input() project: Project; + @Input() taskDefinition: TaskDefinition; + + loadingStaffNotes: boolean = true; + + noteText: string = ''; + + editingNote?: StaffNote; + editingNoteText?: string = ''; + + replyingToNote?: StaffNote; + + hoveredNoteId: number | null = null; + + discussionPrompts: DiscussionPrompt[] = []; + + constructor( + private userService: UserService, + private discussionPromptService: DiscussionPromptService, + private alertService: AlertService, + private confirmationModalService: ConfirmationModalService, + ) {} + ngOnInit(): void { + console.log('task def?', this.taskDefinition); + this.loadingStaffNotes = true; + this.discussionPromptService + .loadDiscussionPromptsForPoject(this.project) + .subscribe((prompts) => { + console.log(prompts); + this.discussionPrompts = prompts; + }); + } +} diff --git a/src/app/projects/states/tutor-discussion/tutor-discussion.component.html b/src/app/projects/states/tutor-discussion/tutor-discussion.component.html index 89cf35865f..f38d42764b 100644 --- a/src/app/projects/states/tutor-discussion/tutor-discussion.component.html +++ b/src/app/projects/states/tutor-discussion/tutor-discussion.component.html @@ -79,7 +79,7 @@ -
    -

    {{ task.definition.name }}

    -
    +
    +

    {{ task.definition.name }}

    + {{ task.definition.abbreviation }} - {{ getTargetTradeString(task.definition.targetGrade) }} Task -
    +
    @if (task.hasGrade()) {
    @@ -215,7 +215,8 @@

    {{ task.definition.name }}

    - + + @if (footerTabView === TutorDiscussionTabView.SHOW_COMMENTS) { @@ -235,6 +236,8 @@

    {{ task.definition.name }}

    } @else if (footerTabView === TutorDiscussionTabView.SHOW_STAFF_NOTES) { + } @else if (footerTabView === TutorDiscussionTabView.SHOW_DISCUSSION_PROMPTS) { + } }
    diff --git a/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts b/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts index 567988d53d..801482ec4d 100644 --- a/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts +++ b/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts @@ -23,6 +23,7 @@ import {GradeService} from 'src/app/common/services/grade.service'; enum TutorDiscussionTabView { SHOW_COMMENTS, SHOW_STAFF_NOTES, + SHOW_DISCUSSION_PROMPTS, } @Component({ selector: 'f-tutor-discussion', @@ -88,6 +89,8 @@ export class TutorDiscussionComponent implements AfterViewInit { this.showComments(); } else if (event.index === 1) { this.showStaffNotes(); + } else if (event.index === 2) { + this.showDiscussionPrompts(); } } @@ -99,6 +102,10 @@ export class TutorDiscussionComponent implements AfterViewInit { this.footerTabView = TutorDiscussionTabView.SHOW_STAFF_NOTES; } + public showDiscussionPrompts() { + this.footerTabView = TutorDiscussionTabView.SHOW_DISCUSSION_PROMPTS; + } + public ngAfterViewInit(): void { this.authService.afterAuthCall((result) => { if (!result) { diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.html new file mode 100644 index 0000000000..86d762974e --- /dev/null +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.html @@ -0,0 +1,107 @@ +
    +
    + + + + + + + + + + + + + + + + + + +
    Discussion Prompt + @if (!editing(prompt)) { + {{ prompt.content }} + } @else { + + Discussion Prompt + + + } + Priority + @if (!editing(prompt)) { + {{ prompt.priorityLabel }} + } @else { +
    + + Priority + + + High + Medium + Low + + +
    + } +
    Actions +
    + @if (editing(prompt)) { + + + } @else { + + + } +
    +
    + @if (!dataSource.data.length) { +
    No discussion prompts
    + } +
    + @if (!creatingNewDiscussionPrompt) { +
    + +
    + } + + @if (creatingNewDiscussionPrompt) { + + +
    + + Discussion Prompt + + + + Priority + + + High + Medium + Low + + +
    +
    + + +
    +
    +
    + } +
    diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.scss b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.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-discussion-prompts/task-definition-discussion-prompts.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.ts new file mode 100644 index 0000000000..149682d55d --- /dev/null +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.ts @@ -0,0 +1,155 @@ +import {Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core'; +import {UntypedFormControl, Validators} from '@angular/forms'; +import {MatTableDataSource} from '@angular/material/table'; +import {Observable, Subscription} from 'rxjs'; +import {DiscussionPrompt} from 'src/app/api/models/discussion-prompt'; +import {Task} from 'src/app/api/models/task'; +import {TaskDefinition} from 'src/app/api/models/task-definition'; +import {TaskPrerequisite} from 'src/app/api/models/task-prerequisite'; +import {Unit} from 'src/app/api/models/unit'; +import {DiscussionPromptService} from 'src/app/api/services/discussion-prompt.service'; +import {TaskDefinitionService} from 'src/app/api/services/task-definition.service'; +import {TaskPrerequisiteService} from 'src/app/api/services/task-prerequisite.service'; +import {EntityFormComponent} from 'src/app/common/entity-form/entity-form.component'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-task-definition-discussion-prompts', + templateUrl: 'task-definition-discussion-prompts.component.html', + styleUrls: ['task-definition-discussion-prompts.component.scss'], +}) +export class TaskDefinitionDiscussionPromptsComponent + extends EntityFormComponent + implements OnInit, OnChanges +{ + @Input() taskDefinition: TaskDefinition; + @Input() staffView: boolean; + @Input() task: Task; + + displayedColumns: string[] = ['content', 'priority', 'actions']; + + private prereqSub?: Subscription; + + public dataSource = new MatTableDataSource(); + + creatingNewDiscussionPrompt: boolean = false; + + newDiscussionPromptContent: string; + newDiscussionPromptWeight: number = 2; + + constructor( + private taskDefinitionService: TaskDefinitionService, + private alertService: AlertService, + private taskPrerequisiteService: TaskPrerequisiteService, + private discussionPromptService: DiscussionPromptService, + ) { + super( + { + content: new UntypedFormControl('', [Validators.required]), + priority: new UntypedFormControl('', [Validators.required]), + }, + 'Discussion Prompt', + ); + } + public get unit(): Unit { + return this.taskDefinition?.unit; + } + + public get prerequisites(): Observable { + return this.taskDefinition.taskPrerequisitesCache.values; + } + + ngOnInit(): void { + this.prereqSub = this.taskDefinition.discussionPromptsCache.values.subscribe((values) => { + this.dataSource.data = values; + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if ( + changes.taskDefinition && + changes.taskDefinition.previousValue?.id !== changes.taskDefinition.currentValue?.id + ) { + this.prereqSub?.unsubscribe(); + this.prereqSub = this.taskDefinition.discussionPromptsCache.values.subscribe((values) => { + this.dataSource.data = values; + }); + this.fetchDiscussionPrompts(); + } + } + + private fetchDiscussionPrompts() { + const taskDefinition = this.taskDefinition; + this.discussionPromptService.loadDiscussionPrompts(null, taskDefinition).subscribe({ + next: (data) => { + this.dataSource.data = data; + }, + error: (error) => { + this.alertService.error(`Failed to load discussion promnpts: ${error}`); + }, + }); + } + + public addNewPrompt() { + const content = this.newDiscussionPromptContent; + const priority = this.newDiscussionPromptWeight; + this.discussionPromptService + .create( + { + task_definition_id: this.taskDefinition.id, + content: content, + priority: priority, + }, + { + cache: this.taskDefinition.discussionPromptsCache, + constructorParams: this.taskDefinition, + }, + ) + .subscribe({ + next: (_result) => { + this.cancelNewDiscussionPrompt(); + this.prereqSub?.unsubscribe(); + this.prereqSub = this.taskDefinition.discussionPromptsCache.values.subscribe((values) => { + this.dataSource.data = values; + }); + this.alertService.success(`Succesfully created prompt`, 3000); + }, + error: (error) => { + this.alertService.error(`Failed to create prompt: ${error}`, 6000); + }, + }); + } + + public deletePrompt(prompt: DiscussionPrompt) { + prompt.delete(); + } + + createNewDiscussionPrompt() { + this.creatingNewDiscussionPrompt = true; + } + + cancelNewDiscussionPrompt() { + this.creatingNewDiscussionPrompt = false; + this.newDiscussionPromptContent = ''; + this.newDiscussionPromptWeight = 2; + } + + submit() { + this.discussionPromptService + .put({ + id: this.selected.id, + task_definition_id: this.taskDefinition.id, + content: this.selected.content, + priority: this.selected.priority, + }) + .subscribe({ + next: (_response) => { + this.cancelEdit(); + this.alertService.success('Successfully saved prompt', 3000); + }, + error: (error) => { + this.alertService.error(`Failed to update prompt: ${error}`, 6000); + }, + }); + } +} 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 7744269ca5..a13d096675 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 @@ -68,6 +68,18 @@

    Details for {{ taskDefinition.abbreviation }} - {{ taskDefinition.name }} + + Discussion Prompts +
    +

    + Discussion prompts for tutors to use when discussing student tasks in class +

    + + + +
    +
    + @if (overseerEnabled) { Task Assessment Automation From 95480e1cd2c128c3a90a8beac5d575bdef590f1d Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 11:55:01 +1100 Subject: [PATCH 653/776] refactor: require confirmation for deleting comment (#1059) --- .../comment-bubble-action.component.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.ts b/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.ts index 4c53949ae3..5b62675292 100644 --- a/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.ts +++ b/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.ts @@ -1,6 +1,7 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { TaskComment } from 'src/app/api/models/doubtfire-model'; -import { TaskCommentComposerData } from '../../task-comment-composer/task-comment-composer.component'; +import {Component, OnInit, Input} from '@angular/core'; +import {TaskComment} from 'src/app/api/models/doubtfire-model'; +import {TaskCommentComposerData} from '../../task-comment-composer/task-comment-composer.component'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; @Component({ selector: 'comment-bubble-action', @@ -11,7 +12,7 @@ export class CommentBubbleActionComponent implements OnInit { @Input() comment: TaskComment; @Input() sharedData: TaskCommentComposerData; - constructor() {} + constructor(private confirmationModalService: ConfirmationModalService) {} ngOnInit() {} reply() { @@ -19,6 +20,12 @@ export class CommentBubbleActionComponent implements OnInit { } delete() { - this.comment.delete(); + this.confirmationModalService.show( + `Delete comment`, + `Are you sure you want to delete this comment?`, + () => { + this.comment.delete(); + }, + ); } } From 6538d8612e756cdb099874ec55208837731e92e2 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 11:55:44 +1100 Subject: [PATCH 654/776] refactor: render overseer comments as is (#1058) --- .../task-assessment-comment.component.html | 18 +++++++++--------- .../task-assessment-comment.component.ts | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.html b/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.html index 7cdf8e89ee..d6d344fc06 100644 --- a/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.html +++ b/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.html @@ -1,15 +1,15 @@
    - @if (comment.assessment_result && comment.assessment_result.is_successful) { -
    -
    -
    {{ comment.text }}
    + +
    +
    +
    {{ comment.text }}
    - -
    -
    - } @else { + +
    +
    +
    diff --git a/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.ts b/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.ts index 44e22eea8b..ed2e404b26 100644 --- a/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.ts +++ b/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.ts @@ -46,7 +46,7 @@ export class TaskAssessmentCommentComponent implements OnInit { } ngOnInit() { - this.update(); + // this.update(); } get message() { From e454166455d0af105bd9ef037ff7a19c2180e915 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 11:56:10 +1100 Subject: [PATCH 655/776] chore(release): 10.0.0-66 --- CHANGELOG.md | 12 ++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6935c31476..aa893a612a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-66](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-65...v10.0.0-66) (2025-12-03) + + +### Features + +* discussion prompts ([#1042](https://github.com/b0ink/doubtfire-deploy/issues/1042)) ([730ffd0](https://github.com/b0ink/doubtfire-deploy/commit/730ffd0c3f79a5ad42c0c59c2f2c2a901ab0d702)) + + +### Bug Fixes + +* correctly set rollover end date ([0611be0](https://github.com/b0ink/doubtfire-deploy/commit/0611be04e8fe7e75ae07504945a2984839475e3a)) + ## [10.0.0-65](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-64...v10.0.0-65) (2025-11-25) diff --git a/package-lock.json b/package-lock.json index c30263893b..184b9874da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-65", + "version": "10.0.0-66", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-65", + "version": "10.0.0-66", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index fb458e4b24..307e43542b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-65", + "version": "10.0.0-66", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 6651279c49f431dbddf6fd5d810440ae7b3b9898 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:33:08 +1100 Subject: [PATCH 656/776] feat: allow sidekiq web access (#1053) --- ngsw-config.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ngsw-config.json b/ngsw-config.json index 8c80cd0891..5c8069284f 100644 --- a/ngsw-config.json +++ b/ngsw-config.json @@ -47,5 +47,14 @@ } } ], - "navigationUrls": ["/**", "!/**/*.*", "!/**/*__*", "!/**/*__*/**", "!/JPlag/**", "!/JPlag"] + "navigationUrls": [ + "/**", + "!/**/*.*", + "!/**/*__*", + "!/**/*__*/**", + "!/JPlag/**", + "!/JPlag", + "!/sidekiq/**", + "!/sidekiq" + ] } From 8db864661ec58c111bd611e9a33c65f11db59f3d Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:33:23 +1100 Subject: [PATCH 657/776] chore(release): 10.0.0-67 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa893a612a..eb4dacb6a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-67](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-66...v10.0.0-67) (2025-12-03) + + +### Features + +* allow sidekiq web access ([#1053](https://github.com/b0ink/doubtfire-deploy/issues/1053)) ([6651279](https://github.com/b0ink/doubtfire-deploy/commit/6651279c49f431dbddf6fd5d810440ae7b3b9898)) + ## [10.0.0-66](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-65...v10.0.0-66) (2025-12-03) diff --git a/package-lock.json b/package-lock.json index 184b9874da..452c2770cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-66", + "version": "10.0.0-67", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-66", + "version": "10.0.0-67", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 307e43542b..377855c416 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-66", + "version": "10.0.0-67", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From fbc63b0308d87d921e1d74788e8da6031443e57e Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:40:42 +1100 Subject: [PATCH 658/776] fix: only trim if valid --- src/app/api/models/user/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/models/user/user.ts b/src/app/api/models/user/user.ts index 03a281bf12..b11826505a 100644 --- a/src/app/api/models/user/user.ts +++ b/src/app/api/models/user/user.ts @@ -54,7 +54,7 @@ export class User extends Entity { } public get preferredName(): string { - const nickname = this.nickname.trim(); + const nickname = this.nickname?.trim(); const firstName = this.firstName.trim(); if (nickname) { return nickname; From 520be4a74735e9dd9fe9ab511208b48e2fcff152 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:43:11 +1100 Subject: [PATCH 659/776] chore(release): 10.0.0-68 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb4dacb6a1..6bf63acef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-68](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-67...v10.0.0-68) (2025-12-08) + + +### Bug Fixes + +* only trim if valid ([fbc63b0](https://github.com/b0ink/doubtfire-deploy/commit/fbc63b0308d87d921e1d74788e8da6031443e57e)) + ## [10.0.0-67](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-66...v10.0.0-67) (2025-12-03) diff --git a/package-lock.json b/package-lock.json index 452c2770cb..7d80fd42ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-67", + "version": "10.0.0-68", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-67", + "version": "10.0.0-68", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 377855c416..5bd6fc814f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-67", + "version": "10.0.0-68", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 2674d4e5ade26e5420b87518f4cd9da909ec7246 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Tue, 9 Dec 2025 11:20:45 +1100 Subject: [PATCH 660/776] feat: display number of stuff notes in tutor discussion --- .../states/tutor-discussion/tutor-discussion.component.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/projects/states/tutor-discussion/tutor-discussion.component.html b/src/app/projects/states/tutor-discussion/tutor-discussion.component.html index f38d42764b..87703c8b6b 100644 --- a/src/app/projects/states/tutor-discussion/tutor-discussion.component.html +++ b/src/app/projects/states/tutor-discussion/tutor-discussion.component.html @@ -215,7 +215,10 @@

    {{ task.definition.name }}

    - + From 15365ed4f42af790af9a9ae24dae42f041dff1df Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:26:45 +1100 Subject: [PATCH 661/776] feat: attention required task status (#1061) * feat: init discuss check task status * refactor: add discuss check status * refactor: add discuss and check marking button * refactor: add warning badge to discuss check tasks * refactor: update discuss check status description * refactor: update discuss check description * chore: ensure discuss check status does not unlock requisite tasks * refactor: rename to off track * refactor: rename to attention required * refactor: update status colors * refactor: rename to attention required internally * chore: update assess in portfolio color --- src/app/api/models/task-prerequisite.ts | 1 + src/app/api/models/task-status.ts | 84 +++++++++++++++---- src/app/common/footer/footer.component.html | 45 ++++++---- .../status-icon/status-icon.component.scss | 3 + .../tutor-discussion.component.html | 6 +- .../tutor-discussion.component.ts | 1 + ...task-definition-prerequisites.component.ts | 1 + src/styles/common/task-status-colors.scss | 12 ++- src/styles/mixins/task-list.scss | 6 ++ .../mixins/task-status-colors-generator.scss | 3 + src/styles/modules/project-task-bar.scss | 6 ++ src/styles/modules/task-status.scss | 14 ++++ 12 files changed, 144 insertions(+), 38 deletions(-) diff --git a/src/app/api/models/task-prerequisite.ts b/src/app/api/models/task-prerequisite.ts index 510d6ce3b6..9645adfe5b 100644 --- a/src/app/api/models/task-prerequisite.ts +++ b/src/app/api/models/task-prerequisite.ts @@ -23,6 +23,7 @@ export class TaskPrerequisite extends Entity { ready_for_feedback: 1, assess_in_portfolio: 1, discuss: 2, + attention_required: 0, demonstrate: 2, complete: 3, }; diff --git a/src/app/api/models/task-status.ts b/src/app/api/models/task-status.ts index 4bff84e86c..0933627924 100644 --- a/src/app/api/models/task-status.ts +++ b/src/app/api/models/task-status.ts @@ -1,4 +1,4 @@ -import { Task } from './task'; +import {Task} from './task'; export type TaskStatusEnum = | 'not_started' @@ -13,14 +13,15 @@ export type TaskStatusEnum = | 'complete' | 'fail' | 'time_exceeded' - | 'assess_in_portfolio'; + | 'assess_in_portfolio' + | 'attention_required'; export type TaskStatusUiData = { status: TaskStatusEnum; icon: string; label: string; class: string; - help: { detail: string; reason: string; action: string }; + help: {detail: string; reason: string; action: string}; }; export class TaskStatus { @@ -38,6 +39,7 @@ export class TaskStatus { 'fail', 'time_exceeded', 'assess_in_portfolio', + 'attention_required', ]; public static readonly VALID_TOP_TASKS: TaskStatusEnum[] = [ @@ -48,6 +50,7 @@ export class TaskStatus { 'fix_and_resubmit', 'ready_for_feedback', 'discuss', + 'attention_required', 'demonstrate', ]; @@ -60,6 +63,7 @@ export class TaskStatus { 'fail', 'time_exceeded', 'assess_in_portfolio', + 'attention_required', ]; public static readonly FINAL_STATUSES: TaskStatusEnum[] = [ @@ -70,11 +74,25 @@ export class TaskStatus { 'assess_in_portfolio', ]; - public static readonly GRADEABLE_STATUSES: TaskStatusEnum[] = ['fail', 'discuss', 'demonstrate', 'complete']; + public static readonly GRADEABLE_STATUSES: TaskStatusEnum[] = [ + 'fail', + 'discuss', + 'demonstrate', + 'complete', + ]; - public static readonly TO_BE_WORKED_ON: TaskStatusEnum[] = ['not_started', 'redo', 'need_help', 'working_on_it']; + public static readonly TO_BE_WORKED_ON: TaskStatusEnum[] = [ + 'not_started', + 'redo', + 'need_help', + 'working_on_it', + ]; - public static readonly DISCUSSION_STATES: TaskStatusEnum[] = ['discuss', 'demonstrate']; + public static readonly DISCUSSION_STATES: TaskStatusEnum[] = [ + 'discuss', + 'attention_required', + 'demonstrate', + ]; public static readonly STATE_THAT_ALLOWS_EXTENSION: TaskStatusEnum[] = [ 'not_started', @@ -112,6 +130,7 @@ export class TaskStatus { 'discuss', 'demonstrate', 'complete', + 'attention_required', ]; public static readonly FEEDBACK_TEMPLATE_STATUSES: TaskStatusEnum[] = [ @@ -120,9 +139,13 @@ export class TaskStatus { 'fix_and_resubmit', 'redo', 'feedback_exceeded', + 'attention_required', ]; - public static readonly LEARNING_WEIGHT: Map = new Map([ + public static readonly LEARNING_WEIGHT: Map = new Map< + TaskStatusEnum, + number + >([ ['fail', 0.0], ['not_started', 0.0], ['working_on_it', 0.0], @@ -135,10 +158,14 @@ export class TaskStatus { ['demonstrate', 0.8], ['complete', 1.0], ['time_exceeded', 0.3], - ['assess_in_portfolio', 0.0], + ['assess_in_portfolio', 1.0], + ['attention_required', 0.1], ]); - public static readonly STATUS_ACRONYM: Map = new Map([ + public static readonly STATUS_ACRONYM: Map = new Map< + TaskStatusEnum, + string + >([ ['ready_for_feedback', 'RFF'], ['not_started', 'NOS'], ['working_on_it', 'WRK'], @@ -152,6 +179,7 @@ export class TaskStatus { ['fail', 'FAL'], ['time_exceeded', 'TIE'], ['assess_in_portfolio', 'AIP'], + ['attention_required', 'AR'], ]); // Which status should not show up in the task status drop down... for students @@ -172,6 +200,7 @@ export class TaskStatus { ['time_exceeded', ['ready_for_feedback', 'not_started', 'working_on_it', 'need_help']], ['fail', ['ready_for_feedback', 'not_started', 'working_on_it', 'need_help']], ['assess_in_portfolio', ['not_started']], + ['attention_required', ['ready_for_feedback', 'not_started', 'working_on_it', 'need_help']], ]); public static readonly STATUS_LABELS = new Map([ @@ -188,6 +217,7 @@ export class TaskStatus { ['fail', 'Fail'], ['time_exceeded', 'Time Exceeded'], ['assess_in_portfolio', 'Assess in Portfolio'], + ['attention_required', 'Attention Required'], ]); public static readonly STATUS_ICONS = new Map([ @@ -204,6 +234,7 @@ export class TaskStatus { ['fail', 'fa fa-times'], ['time_exceeded', 'fa fa-clock-o'], ['assess_in_portfolio', 'fa fa-folder-open'], + ['attention_required', 'fa fa-commenting'], ]); // Please make sure this matches task-status-colors.less @@ -220,7 +251,8 @@ export class TaskStatus { ['complete', '#5BB75B'], ['fail', '#d93713'], ['time_exceeded', '#d93713'], - ['assess_in_portfolio', '#91b891'], + ['assess_in_portfolio', '#f2d85c'], + ['attention_required', '#f1814d'], ]); public static readonly STATUS_SEQ = new Map([ @@ -237,6 +269,7 @@ export class TaskStatus { ['demonstrate', 11], ['complete', 12], ['assess_in_portfolio', 13], + ['attention_required', 14], ]); public static readonly SWITCHABLE_STATES = { @@ -244,6 +277,7 @@ export class TaskStatus { tutor: [ 'complete', 'discuss', + 'attention_required', 'demonstrate', 'fix_and_resubmit', 'redo', @@ -258,14 +292,16 @@ export class TaskStatus { // action = action student can take public static readonly HELP_DESCRIPTIONS = new Map< TaskStatusEnum, - { detail: string; reason: string; action: string } + {detail: string; reason: string; action: string} >([ [ 'ready_for_feedback', { detail: 'Submitted this task for feedback', - reason: 'You have finished working on the task and have uploaded it for your tutor to assess.', - action: 'No further action is required. Your tutor will change this task status once they have assessed it.', + reason: + 'You have finished working on the task and have uploaded it for your tutor to assess.', + action: + 'No further action is required. Your tutor will change this task status once they have assessed it.', }, ], [ @@ -289,7 +325,8 @@ export class TaskStatus { { detail: 'Need help for the task', reason: 'You are working on the task but would like some help to get it complete.', - action: 'Upload the task with what you have completed so far and add a comment on what you would like help on.', + action: + 'Upload the task with what you have completed so far and add a comment on what you would like help on.', }, ], [ @@ -306,7 +343,8 @@ export class TaskStatus { 'feedback_exceeded', { detail: 'Feedback will no longer be given', - reason: 'This work is not complete to an acceptable standard and your tutor will not reassess it again.', + reason: + 'This work is not complete to an acceptable standard and your tutor will not reassess it again.', action: "It is now your responsibility to ensure this task is at an adequate standard in your portfolio. You should fix your work according to your tutor's prior feedback and include a corrected version in your portfolio.", }, @@ -329,6 +367,16 @@ export class TaskStatus { action: 'For this to be marked as complete, attend class and discuss it with your tutor.', }, ], + [ + 'attention_required', + { + detail: 'Your work is off track and needs focused discussion.', + reason: + 'It seems you have misunderstood some key requirements for this task. Previous feedback has not led to sufficient progress.', + action: + 'Attend class so your tutor can go through your work in detail and help you get back on track.', + }, + ], [ 'demonstrate', { @@ -343,7 +391,8 @@ export class TaskStatus { { detail: 'You are finished with this task 🎉', reason: 'Your tutor is happy with your work and it has been discussed with them.', - action: 'No further action required. Move onto the next task, or go party if everything is done.', + action: + 'No further action required. Move onto the next task, or go party if everything is done.', }, ], [ @@ -359,7 +408,8 @@ export class TaskStatus { 'time_exceeded', { detail: 'Time limit exceeded', - reason: 'This work was submitted after the deadline, having missed both the target date and deadline.', + reason: + 'This work was submitted after the deadline, having missed both the target date and deadline.', action: 'Work submitted after the feedback deadline will not be checked by tutors prior to the portfolio assessment. You will need to ensure this task is at an adequate standard in your portfolio.', }, diff --git a/src/app/common/footer/footer.component.html b/src/app/common/footer/footer.component.html index e7d84ac5e5..a31bf9fac8 100644 --- a/src/app/common/footer/footer.component.html +++ b/src/app/common/footer/footer.component.html @@ -14,18 +14,32 @@
    - - + +
    + + + +
    - + @if (selectedTask && selectedTask.suggestedTaskStatus) { - } @if (selectedTask?.definition?.assessInPortfolioOnly) { -
    + + +

    } diff --git a/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts b/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts index 801482ec4d..f08edf198c 100644 --- a/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts +++ b/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts @@ -352,6 +352,7 @@ export class TutorDiscussionComponent implements AfterViewInit { 'demonstrate', 'ready_for_feedback', 'discuss', + 'attention_required', 'need_help', // 'complete', 'fix_and_resubmit', diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-prerequisites/task-definition-prerequisites.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-prerequisites/task-definition-prerequisites.component.ts index eff30b4bd3..2528ffcd6d 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-prerequisites/task-definition-prerequisites.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-prerequisites/task-definition-prerequisites.component.ts @@ -37,6 +37,7 @@ export class TaskDefinitionPrerequisitesComponent implements OnInit, OnChanges { ready_for_feedback: 1, assess_in_portfolio: 1, discuss: 2, + attention_required: 0, demonstrate: 2, complete: 3, }; diff --git a/src/styles/common/task-status-colors.scss b/src/styles/common/task-status-colors.scss index 563e2cf038..3e045a4406 100644 --- a/src/styles/common/task-status-colors.scss +++ b/src/styles/common/task-status-colors.scss @@ -85,9 +85,15 @@ $task-status-colors: ( fore: $task-status-foreground-color-light, ), assess-in-portfolio: ( - base: #91b891, - dark: darken(#91b891, 15%), - light: lighten(#91b891, 15%), + base: #f2d85c, + dark: darken(#f2d85c, 15%), + light: lighten(#f2d85c, 15%), + fore: $task-status-foreground-color-dark, + ), + attention-required: ( + base: #f1814d, + dark: darken(#f1814d, 15%), + light: lighten(#f1814d, 15%), fore: $task-status-foreground-color-light, ), ); diff --git a/src/styles/mixins/task-list.scss b/src/styles/mixins/task-list.scss index 5684822871..63af66d655 100644 --- a/src/styles/mixins/task-list.scss +++ b/src/styles/mixins/task-list.scss @@ -81,6 +81,9 @@ &.assess-in-portfolio { @include custom-box-shadow(lighten(task-status-color('assess-in-portfolio'), 15%)); } + &.attention-required { + @include custom-box-shadow(lighten(task-status-color('attention-required'), 15%)); + } } &.selected { &.ready-for-feedback { @@ -122,6 +125,9 @@ &.assess-in-portfolio { @include custom-box-shadow(task-status-color('assess-in-portfolio')); } + &.attention-required { + @include custom-box-shadow(task-status-color('attention-required')); + } } .task-badges { width: 50px; diff --git a/src/styles/mixins/task-status-colors-generator.scss b/src/styles/mixins/task-status-colors-generator.scss index 4d0646ecd6..6f2b97e2c5 100644 --- a/src/styles/mixins/task-status-colors-generator.scss +++ b/src/styles/mixins/task-status-colors-generator.scss @@ -62,3 +62,6 @@ @mixin task-status-color-assess-in-portfolio { @include task-status-color('assess-in-portfolio'); } +@mixin task-status-color-attention-required { + @include task-status-color('attention-required'); +} diff --git a/src/styles/modules/project-task-bar.scss b/src/styles/modules/project-task-bar.scss index 78d15e07f6..d3af788607 100644 --- a/src/styles/modules/project-task-bar.scss +++ b/src/styles/modules/project-task-bar.scss @@ -76,6 +76,12 @@ .progress-bar-complete { @include task-status-color-complete; } +.progress-bar-assess-in-portfolio { + @include task-status-color-assess-in-portfolio; +} +.progress-bar-attention-required { + @include task-status-color-attention-required; +} .progress-bar-not-started { @include task-status-color-not-started; diff --git a/src/styles/modules/task-status.scss b/src/styles/modules/task-status.scss index ba7252f899..6ed7ea0f57 100644 --- a/src/styles/modules/task-status.scss +++ b/src/styles/modules/task-status.scss @@ -41,6 +41,9 @@ &.assess-in-portfolio { @include task-status-color-assess-in-portfolio; } + &.attention-required { + @include task-status-color-attention-required; + } &.ready-for-feedback:hover { background-color: task-status-color('ready-for-feedback'); @@ -81,6 +84,9 @@ &.assess-in-portfolio:hover { background-color: task-status-color('assess-in-portfolio'); } + &.attention-required:hover { + background-color: task-status-color('attention-required'); + } } .task-status > .btn-default { @@ -149,6 +155,11 @@ &.assess-in-portfolio:hover { @include task-status-color-assess-in-portfolio; } + &.attention-required, + &.attention-required.active, + &.attention-required:hover { + @include task-status-color-attention-required; + } &.ready-for-feedback { background-color: lighten(task-status-color('ready-for-feedback'), 15%); @@ -189,6 +200,9 @@ &.assess-in-portfolio { background-color: lighten(task-status-color('assess-in-portfolio'), 15%); } + &.attention-required { + background-color: lighten(task-status-color('attention-required'), 15%); + } } i.task-status-icon { From 7cb8fc7471f0402c562fb8a0b87df672ceb22ea3 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:01:53 +1100 Subject: [PATCH 662/776] feat: overseer pipeline (#1064) * feat: init overseer pipeline ui * chore: init drag and drop steps panel * feat: init overseer-step crud * feat: use new monaco editor package * refactor: clean up ui * feat: get list of overseer resource files * chore: allow no input file * feat: allow partial input output diff check * feat: allow deleting of overseer steps * feat: overseer step result model * refactor: improve overseer report view * refactor: organise imports * refactor: retrieve feedback message * refactor: expose overseer assessment details to task comment * feat: dropdown for command language * fix: set fixed height size * chore: show overseer reports to students * build: ensure monaco editor is packed correctly * refactor: lazy load step results for overseer assessments * refactor: align default feedback messages with the backend * refactor: improve assessment comment ui * chore: ignore old assessment script * refactor: hide test result until report is ready * fix: use new overseer report component for test submission history * chore: allow for overseer report refresh * refactor: reduce icon size * refactor: auto expand selected overseer report * chore: enable submission history if project is valid * chore: delete new step * chore: set correct overseer steps when changing task definition * feat: add overseer task status result * chore: add hint explaining test submissions can be used before enabling overseer * refactor: hide overseer steps until docker image has been selected * chore: move overseer editor to original place * refactor: ensure task definition is saved before adding overseer steps * chore: show delete and save buttons when step is selected * refactor: use seconds for overseer timeout * refactor: show status dropdown only when halting step * refactor: cleanup * chore: render diff if overseer step is null * refactor: remove debug * chore: show empty overseer reports --- angular.json | 8 + package-lock.json | 47 +-- package.json | 3 +- .../models/overseer/overseer-assessment.ts | 23 +- .../models/overseer/overseer-step-result.ts | 35 ++ src/app/api/models/overseer/overseer-step.ts | 75 ++++ src/app/api/models/task-definition.ts | 4 + src/app/api/models/task.ts | 6 +- .../services/overseer-assessment.service.ts | 28 +- .../services/overseer-step-result.service.ts | 52 +++ src/app/api/services/overseer-step.service.ts | 47 +++ src/app/api/services/task-comment.service.ts | 5 + .../api/services/task-definition.service.ts | 18 + src/app/common/footer/footer.component.ts | 6 +- .../task-assessment-modal.component.html | 16 +- .../task-assessment-modal.component.ts | 7 +- .../task-assessment-modal.service.ts | 35 +- src/app/doubtfire-angular.module.ts | 13 +- src/app/doubtfire-angularjs.module.ts | 6 + .../task-overseer-report.component.html | 167 ++++++++ .../task-overseer-report.component.scss | 0 .../task-overseer-report.component.ts | 145 +++++++ .../task-dashboard/task-dashboard.coffee | 2 +- .../task-dashboard.component.html | 4 + .../task-dashboard/task-dashboard.tpl.html | 14 +- .../states/dashboard/selected-task.service.ts | 5 + .../task-assessment-comment.component.html | 58 ++- .../task-assessment-comment.component.scss | 56 --- .../task-assessment-comment.component.ts | 24 +- .../task-definition-overseer.component.html | 376 +++++++++++++++++- .../task-definition-overseer.component.scss | 50 +++ .../task-definition-overseer.component.ts | 206 +++++++++- 32 files changed, 1383 insertions(+), 158 deletions(-) create mode 100644 src/app/api/models/overseer/overseer-step-result.ts create mode 100644 src/app/api/models/overseer/overseer-step.ts create mode 100644 src/app/api/services/overseer-step-result.service.ts create mode 100644 src/app/api/services/overseer-step.service.ts create mode 100644 src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.html create mode 100644 src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.scss create mode 100644 src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.ts diff --git a/angular.json b/angular.json index cf6bc6bd32..0d892151bd 100644 --- a/angular.json +++ b/angular.json @@ -29,8 +29,16 @@ "input": "./JPlag-Report-Viewer", "output": "/JPlag", "glob": "**/!(*.gitignore|README.md|.git)" + }, + { + "glob": "**/*", + "input": "./node_modules/monaco-editor/min", + "output": "/assets/monaco/min/" } ], + "loader": { + ".ttf": "binary" + }, "styles": [ "src/theme.scss", "src/styles.scss", diff --git a/package-lock.json b/package-lock.json index 7d80fd42ec..05dae886a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,7 @@ "lottie-web": "^5.12.2", "marked": "^11.1.0", "moment": "^2.29.4", - "monaco-editor": "^0.54.0", + "monaco-editor": "^0.44.0", "ng-csv": "0.2.3", "ng-file-upload": "~5.0.9", "ng-flex-layout": "^17.3.7-beta.1", @@ -69,6 +69,7 @@ "ngx-bootstrap": "^6.1.0", "ngx-entity-service": "^0.0.41", "ngx-lottie": "^11.0.2", + "ngx-monaco-editor-v2": "^17.0.1", "nvd3": "1.8.6", "qrcode": "^1.5.4", "rxjs": "~7.4.0", @@ -9254,12 +9255,6 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/dompurify": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", - "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", - "license": "(MPL-2.0 OR Apache-2.0)" - }, "node_modules/domutils": { "version": "3.1.0", "dev": true, @@ -15766,26 +15761,10 @@ } }, "node_modules/monaco-editor": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz", - "integrity": "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==", - "license": "MIT", - "dependencies": { - "dompurify": "3.1.7", - "marked": "14.0.0" - } - }, - "node_modules/monaco-editor/node_modules/marked": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", - "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.44.0.tgz", + "integrity": "sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==", + "license": "MIT" }, "node_modules/morgan": { "version": "1.10.0", @@ -16126,6 +16105,20 @@ "lottie-web": ">=5.9.2" } }, + "node_modules/ngx-monaco-editor-v2": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/ngx-monaco-editor-v2/-/ngx-monaco-editor-v2-17.0.1.tgz", + "integrity": "sha512-GP+Ni6zKFQjF/ve5ZQtfE9eRLKL4GxMvdmDTrla1x6F5pSIcYGCcjZ4gQ1/AHMa5dgarfs+Et+1bBtAOJtI6KA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@angular/common": "^17.0.3", + "@angular/core": "^17.0.3", + "monaco-editor": "^0.44.0" + } + }, "node_modules/nice-napi": { "version": "1.0.2", "dev": true, diff --git a/package.json b/package.json index 5bd6fc814f..d3eeccad5d 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "lottie-web": "^5.12.2", "marked": "^11.1.0", "moment": "^2.29.4", - "monaco-editor": "^0.54.0", + "monaco-editor": "^0.44.0", "ng-csv": "0.2.3", "ng-file-upload": "~5.0.9", "ng-flex-layout": "^17.3.7-beta.1", @@ -88,6 +88,7 @@ "ngx-bootstrap": "^6.1.0", "ngx-entity-service": "^0.0.41", "ngx-lottie": "^11.0.2", + "ngx-monaco-editor-v2": "^17.0.1", "nvd3": "1.8.6", "qrcode": "^1.5.4", "rxjs": "~7.4.0", diff --git a/src/app/api/models/overseer/overseer-assessment.ts b/src/app/api/models/overseer/overseer-assessment.ts index dcffd8fd4f..005b6be09f 100644 --- a/src/app/api/models/overseer/overseer-assessment.ts +++ b/src/app/api/models/overseer/overseer-assessment.ts @@ -1,20 +1,28 @@ -import {Entity, EntityMapping} from 'ngx-entity-service'; +import {Entity, EntityCache, EntityMapping} from 'ngx-entity-service'; import {Task} from '../doubtfire-model'; +import {OverseerStepResult} from './overseer-step-result'; export class OverseerAssessment extends Entity { id: number; + // overseerStepId: number; timestamp: Date; timestampString: string; content?: [{label: string; result: string}]; task?: Task; taskStatus?: string; - submissionStatus?: string; + submissionStatus?: 'queued' | 'executing' | 'passed' | 'failed' | 'error'; createdAt?: Date; updatedAt?: Date; taskId?: number; + totalSteps: number; + passedSteps: number; + label: string; + public readonly stepResultsCache: EntityCache = + new EntityCache(); + constructor(task?: Task) { super(); @@ -31,4 +39,15 @@ export class OverseerAssessment extends Entity { overseer_assessment: super.toJson(mappingData, ignoreKeys), }; } + + public get stepsSkipped() { + return this.task?.definition.overseerStepsCache.currentValues.filter( + (step) => + !this.stepResultsCache.currentValues.find((result) => result.overseerStepId === step.id), + ); + } + + public get reportReady() { + return this.submissionStatus === 'passed' || this.submissionStatus === 'failed'; + } } diff --git a/src/app/api/models/overseer/overseer-step-result.ts b/src/app/api/models/overseer/overseer-step-result.ts new file mode 100644 index 0000000000..2a44371825 --- /dev/null +++ b/src/app/api/models/overseer/overseer-step-result.ts @@ -0,0 +1,35 @@ +import {Entity, EntityMapping} from 'ngx-entity-service'; +import {OverseerAssessment} from './overseer-assessment'; +import {OverseerStep} from './overseer-step'; + +export class OverseerStepResult extends Entity { + id: number; + overseerAssessment: OverseerAssessment; + overseerStep: OverseerStep; + overseerStepId: number; + + exitStatus: number; + pass: boolean; + stdout: string; + stdin: string; + expectedOutput: string; + stdoutSha256: string; + stdinSha256: string; + expectedOutputSha256: string; + feedbackMessage: string; + + constructor(oa?: OverseerAssessment, os?: OverseerStep) { + super(); + this.overseerAssessment = oa; + // this.overseerStep = os; + } + + public override toJson( + mappingData: EntityMapping, + ignoreKeys?: string[], + ): object { + return { + overseer_step_result: super.toJson(mappingData, ignoreKeys), + }; + } +} diff --git a/src/app/api/models/overseer/overseer-step.ts b/src/app/api/models/overseer/overseer-step.ts new file mode 100644 index 0000000000..5d2a03ee28 --- /dev/null +++ b/src/app/api/models/overseer/overseer-step.ts @@ -0,0 +1,75 @@ +import {Entity, EntityMapping} from 'ngx-entity-service'; +import {TaskDefinition} from '../task-definition'; +import {TaskStatus, TaskStatusEnum} from '../task-status'; +import {OverseerStepService} from '../../services/overseer-step.service'; +import {AppInjector} from 'src/app/app-injector'; +import {AlertService} from 'src/app/common/services/alert.service'; + +export class OverseerStep extends Entity { + id: number; + taskDefinition: TaskDefinition; + + name: string; + description: string; + + // Shown to the student's + displayName: string; + displayDescription: string; + + runCommand: string; + commandLanguage: string; + timeout: number; + sortOrder: number; + stepType: 'status_check' | 'output_diff'; + partialOutputDiff: boolean; + stdinInputFile: string; + expectedOutputFile: string; + + feedbackMessage: string; + statusOnSuccess: TaskStatusEnum | 'no_change'; + statusOnFailure: TaskStatusEnum | 'no_change'; + + haltOnSuccess: boolean; + haltOnFailure: boolean; + + showExpectedOutput: boolean; + showStdin: boolean; + showStdout: boolean; + + enabled: boolean; + + // showStdOutToStudent: boolean + + constructor(td?: TaskDefinition) { + super(); + this.taskDefinition = td; + } + + public override toJson( + mappingData: EntityMapping, + ignoreKeys?: string[], + ): object { + return { + overseer_step: super.toJson(mappingData, ignoreKeys), + }; + } + + public delete() { + const overseerStepService: OverseerStepService = AppInjector.get(OverseerStepService); + overseerStepService + .delete( + { + id: this.id, + }, + {cache: this.taskDefinition.overseerStepsCache, endpointFormat: 'overseer_steps/:id:'}, + ) + .subscribe({ + next: (_response: object) => { + AppInjector.get(AlertService).success('Successfully deleted overseer step', 4000); + }, + error: (error) => { + AppInjector.get(AlertService).error(error?.message || error || 'Unknown error', 2000); + }, + }); + } +} diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index d3aa2a4317..4a47a7ab96 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -9,6 +9,7 @@ import {Grade, GroupSet, LearningOutcome, Project, TutorialStream, Unit} from '. import {Task} from './doubtfire-model'; import {TaskPrerequisite} from './task-prerequisite'; import {DiscussionPrompt} from './discussion-prompt'; +import {OverseerStep} from './overseer/overseer-step'; export type UploadRequirement = { key: string; @@ -59,6 +60,7 @@ export class TaskDefinition extends Entity { useResourcesForJplagBaseCode: boolean; lockAssessmentsToTutorialStream: boolean; discussionPromptsCount: number; + overseerResourceFiles: string[] = []; public readonly taskPrerequisitesCache: EntityCache = new EntityCache(); @@ -69,6 +71,8 @@ export class TaskDefinition extends Entity { public readonly learningOutcomesCache: EntityCache = new EntityCache(); + public readonly overseerStepsCache: EntityCache = new EntityCache(); + readonly unit: Unit; constructor(unit: Unit) { diff --git a/src/app/api/models/task.ts b/src/app/api/models/task.ts index c920e985a7..d624a8285d 100644 --- a/src/app/api/models/task.ts +++ b/src/app/api/models/task.ts @@ -579,11 +579,7 @@ export class Task extends Entity { } public get overseerEnabled(): boolean { - return ( - this.unit.overseerEnabled && - this.definition.assessmentEnabled && - this.definition.hasTaskAssessmentResources - ); + return this.unit.overseerEnabled && this.definition.assessmentEnabled; } public get scormEnabled(): boolean { diff --git a/src/app/api/services/overseer-assessment.service.ts b/src/app/api/services/overseer-assessment.service.ts index 2226887caf..df897e43be 100644 --- a/src/app/api/services/overseer-assessment.service.ts +++ b/src/app/api/services/overseer-assessment.service.ts @@ -5,6 +5,7 @@ import {HttpClient} from '@angular/common/http'; import API_URL from 'src/app/config/constants/apiUrl'; import {OverseerAssessment} from '../models/overseer/overseer-assessment'; import {Task} from '../models/doubtfire-model'; +import {OverseerStepResultService} from './overseer-step-result.service'; @Injectable() export class OverseerAssessmentService extends EntityService { @@ -13,7 +14,10 @@ export class OverseerAssessmentService extends EntityService protected readonly triggerEndpointFormat = 'projects/:project_id:/task_def_id/:td_id:/overseer_assessment/:id:/trigger'; - constructor(httpClient: HttpClient) { + constructor( + httpClient: HttpClient, + private overseerStepResultService: OverseerStepResultService, + ) { super(httpClient, API_URL); this.mapping.addKeys( @@ -31,6 +35,24 @@ export class OverseerAssessmentService extends EntityService }, }, ['timestampString', 'submission_timestamp'], + { + keys: 'overseerStepResults', + toEntityOp: (data: object, key: string, overseerAssesment: OverseerAssessment) => { + data[key]?.forEach((overseerStep) => { + overseerAssesment.stepResultsCache.getOrCreate( + overseerStep['id'], + this.overseerStepResultService, + overseerStep, + { + constructorParams: overseerAssesment, + }, + ); + }); + }, + }, + 'overseerStepId', + 'totalSteps', + 'passedSteps', ); } @@ -44,7 +66,9 @@ export class OverseerAssessmentService extends EntityService td_id: task.definition.id, }; - return this.query(pathIds); + return this.query(pathIds, { + constructorParams: task, + }); } public triggerOverseer(assessment: OverseerAssessment): Observable { diff --git a/src/app/api/services/overseer-step-result.service.ts b/src/app/api/services/overseer-step-result.service.ts new file mode 100644 index 0000000000..f7e873281b --- /dev/null +++ b/src/app/api/services/overseer-step-result.service.ts @@ -0,0 +1,52 @@ +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {CachedEntityService} from 'ngx-entity-service'; +import API_URL from 'src/app/config/constants/apiUrl'; +import {OverseerAssessment} from '../models/doubtfire-model'; +import {OverseerStepResult} from '../models/overseer/overseer-step-result'; +import {Observable} from 'rxjs'; + +@Injectable() +export class OverseerStepResultService extends CachedEntityService { + protected readonly endpointFormat = + 'units/:unitId:/task_definitions/:taskDefId:/overseer_step_results/:id:'; + + constructor(httpClient: HttpClient) { + super(httpClient, API_URL); + + this.mapping.addKeys( + 'id', + 'overseerStepId', + 'exitStatus', + 'pass', + 'feedbackMessage', + 'stdout', + 'stdin', + 'expectedOutput', + 'stdoutSha256', + 'stdinSha256', + 'expectedOutputSha256', + ); + + this.mapping.mapAllKeysToJsonExcept('id'); + } + + public createInstanceFrom(json: object, other?: any): OverseerStepResult { + return new OverseerStepResult(other as OverseerAssessment); + } + + public getOverseerStepResults(assessment: OverseerAssessment): Observable { + const pathIds = { + projectId: assessment.task.project.id, + taskDefId: assessment.task.definition.id, + id: assessment.id, + }; + + return this.query(pathIds, { + endpointFormat: + 'projects/:projectId:/task_definitions/:taskDefId:/overseer_assessments_results/:id:', + constructorParams: assessment.task, + cache: assessment.stepResultsCache, + }); + } +} diff --git a/src/app/api/services/overseer-step.service.ts b/src/app/api/services/overseer-step.service.ts new file mode 100644 index 0000000000..6df6323294 --- /dev/null +++ b/src/app/api/services/overseer-step.service.ts @@ -0,0 +1,47 @@ +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {CachedEntityService} from 'ngx-entity-service'; +import API_URL from 'src/app/config/constants/apiUrl'; +import {OverseerStep} from '../models/overseer/overseer-step'; +import {TaskDefinition} from '../models/task-definition'; + +@Injectable() +export class OverseerStepService extends CachedEntityService { + protected readonly endpointFormat = + 'units/:unitId:/task_definitions/:taskDefId:/overseer_steps/:id:'; + + constructor(httpClient: HttpClient) { + super(httpClient, API_URL); + + this.mapping.addKeys( + 'id', + // 'taskDefinition', + 'name', + 'description', + 'displayName', + 'displayDescription', + 'runCommand', + 'timeout', + 'sortOrder', + 'stepType', + 'partialOutputDiff', + 'stdinInputFile', + 'expectedOutputFile', + 'feedbackMessage', + 'statusOnSuccess', + 'statusOnFailure', + 'haltOnSuccess', + 'haltOnFailure', + 'showExpectedOutput', + 'showStdin', + 'showStdout', + 'enabled', + ); + + this.mapping.mapAllKeysToJsonExcept('id'); + } + + public createInstanceFrom(json: object, other?: any): OverseerStep { + return new OverseerStep(other as TaskDefinition); + } +} diff --git a/src/app/api/services/task-comment.service.ts b/src/app/api/services/task-comment.service.ts index b804b164b4..6761bc715c 100644 --- a/src/app/api/services/task-comment.service.ts +++ b/src/app/api/services/task-comment.service.ts @@ -120,6 +120,11 @@ export class TaskCommentService extends CachedEntityService { // Scorm Extension Comments ['taskScormExtensions', 'scorm_extensions'], + 'overseerAssessmentId', + 'overseerPassedSteps', + 'overseerTotalSteps', + 'overseerInProgress', + 'overseerStatus', ); this.mapping.addJsonKey('granted'); diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index 59664c143a..8e98e8175d 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -15,6 +15,7 @@ import {TaskPrerequisiteService} from './task-prerequisite.service'; import {TaskPrerequisite} from '../models/task-prerequisite'; import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; import {SidekiqJob} from '../models/sidekiq-job'; +import {OverseerStepService} from './overseer-step.service'; @Injectable() export class TaskDefinitionService extends CachedEntityService { @@ -24,6 +25,7 @@ export class TaskDefinitionService extends CachedEntityService { httpClient: HttpClient, private learningOutcomeService: LearningOutcomeService, private taskPrerequisiteService: TaskPrerequisiteService, + private overseerStepService: OverseerStepService, ) { super(httpClient, API_URL); @@ -140,6 +142,22 @@ export class TaskDefinitionService extends CachedEntityService { }, 'useResourcesForJplagBaseCode', 'lockAssessmentsToTutorialStream', + { + keys: 'overseerSteps', + toEntityOp: (data: object, key: string, taskDefinition: TaskDefinition) => { + data[key]?.forEach((overseerStep) => { + taskDefinition.overseerStepsCache.getOrCreate( + overseerStep['id'], + this.overseerStepService, + overseerStep, + { + constructorParams: taskDefinition, + }, + ); + }); + }, + }, + 'overseerResourceFiles', ); this.mapping.mapAllKeysToJsonExcept( diff --git a/src/app/common/footer/footer.component.ts b/src/app/common/footer/footer.component.ts index 295b8612ee..fa44839024 100644 --- a/src/app/common/footer/footer.component.ts +++ b/src/app/common/footer/footer.component.ts @@ -99,8 +99,12 @@ export class FooterComponent implements OnInit { this.selectedTaskService.showSimilarity(); } + // viewOverseer() { + // this.taskAssessmentModal.show(this.selectedTask); + // } + viewOverseer() { - this.taskAssessmentModal.show(this.selectedTask); + this.selectedTaskService.showOverseerReports(); } viewStaffNotes() { diff --git a/src/app/common/modals/task-assessment-modal/task-assessment-modal.component.html b/src/app/common/modals/task-assessment-modal/task-assessment-modal.component.html index 080a65cfd6..4ac4445bfb 100644 --- a/src/app/common/modals/task-assessment-modal/task-assessment-modal.component.html +++ b/src/app/common/modals/task-assessment-modal/task-assessment-modal.component.html @@ -1,14 +1,20 @@

    Overseer Assessment

    - - + +
    @if (noDataFlag) { - + }
    diff --git a/src/app/common/modals/task-assessment-modal/task-assessment-modal.component.ts b/src/app/common/modals/task-assessment-modal/task-assessment-modal.component.ts index 06c8fb3246..13bd6e9236 100644 --- a/src/app/common/modals/task-assessment-modal/task-assessment-modal.component.ts +++ b/src/app/common/modals/task-assessment-modal/task-assessment-modal.component.ts @@ -2,6 +2,7 @@ import {Component, OnInit, Inject, Input} from '@angular/core'; import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; import {Subject} from 'rxjs'; import {Task} from 'src/app/api/models/doubtfire-model'; +import {TaskAssessmentModalData} from './task-assessment-modal.service'; @Component({ selector: 'task-assessment-modal', @@ -10,16 +11,18 @@ import {Task} from 'src/app/api/models/doubtfire-model'; }) export class TaskAssessmentModalComponent implements OnInit { @Input() task: Task; + @Input() overseerAssessmentId?: number; noDataFlag: boolean; refreshTrigger: Subject = new Subject(); constructor( public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: any, + @Inject(MAT_DIALOG_DATA) public data: TaskAssessmentModalData, ) {} ngOnInit() { - this.task = this.data; + this.task = this.data.task; + this.overseerAssessmentId = this.data.overseerAssessmentId; } setNoDataFlag($event) { diff --git a/src/app/common/modals/task-assessment-modal/task-assessment-modal.service.ts b/src/app/common/modals/task-assessment-modal/task-assessment-modal.service.ts index 927c861a04..31e9bc54c9 100644 --- a/src/app/common/modals/task-assessment-modal/task-assessment-modal.service.ts +++ b/src/app/common/modals/task-assessment-modal/task-assessment-modal.service.ts @@ -1,21 +1,30 @@ -import { Injectable } from '@angular/core'; -import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog'; +import {Injectable} from '@angular/core'; +import {MatDialogRef, MAT_DIALOG_DATA, MatDialog} from '@angular/material/dialog'; import {TaskAssessmentModalComponent} from './task-assessment-modal.component'; +import {Task} from 'src/app/api/models/task'; + +export interface TaskAssessmentModalData { + task: Task; + overseerAssessmentId: number; +} @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TaskAssessmentModalService { - constructor( - public dialog: MatDialog, - ) { } + constructor(public dialog: MatDialog) {} - public show(task: any) { - let dialogRef: MatDialogRef; - dialogRef = this.dialog.open(TaskAssessmentModalComponent, { - data: task, - width: '80%', - panelClass: 'submission-history-modal' - }); + public show(task: Task, overseerAssessmentId?: number) { + this.dialog.open( + TaskAssessmentModalComponent, + { + data: { + task: task, + overseerAssessmentId, + }, + width: '80%', + panelClass: 'submission-history-modal', + }, + ); } } diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 7938a4e274..86b5fc459d 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -43,9 +43,13 @@ import {UIRouterUpgradeModule} from '@uirouter/angular-hybrid'; import {MatDialogModule as MatDialogModuleNew} from '@angular/material/dialog'; import {AlertService} from 'src/app/common/services/alert.service'; import {AlertComponent} from 'src/app/common/services/alert.service'; +import {MatSidenavModule} from '@angular/material/sidenav'; import {setTheme} from 'ngx-bootstrap/utils'; +import {CodeEditorModule} from '@ngstack/code-editor'; +import {MonacoEditorModule} from 'ngx-monaco-editor-v2'; + import {AboutDoubtfireModalService} from 'src/app/common/modals/about-doubtfire-modal/about-doubtfire-modal.service'; import { D2lUnitDetailsFormComponent, @@ -295,12 +299,14 @@ import {AnalyticsTutorTimesComponent} from './units/states/analytics/directives/ import {MarkingSessionService} from './api/services/marking-session.service'; import {PortfolioIncludedTasksComponent} from './projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component'; import {OverseerScriptEditorModalComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component'; -import {CodeEditorModule} from '@ngstack/code-editor'; import {UploadGradesComponent} from './units/states/portfolios/upload-grades/upload-grades.component'; import {DiscussionPromptService} from './api/services/discussion-prompt.service'; import {DiscussionPromptsComponent} from './projects/states/discussion-prompts/discussion-prompts.component'; import {TaskDefinitionDiscussionPromptsComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component'; import {DiscussionPromptsViewComponent} from './projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component'; +import {OverseerStepService} from './api/services/overseer-step.service'; +import {TaskOverseerReportComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component'; +import {OverseerStepResultService} from './api/services/overseer-step-result.service'; // 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 = { @@ -460,6 +466,7 @@ const MY_DATE_FORMAT = { DiscussionPromptsComponent, TaskDefinitionDiscussionPromptsComponent, DiscussionPromptsViewComponent, + TaskOverseerReportComponent, ], providers: [ // Services we provide @@ -550,6 +557,8 @@ const MY_DATE_FORMAT = { TaskPrerequisiteService, MarkingSessionService, DiscussionPromptService, + OverseerStepService, + OverseerStepResultService, ], imports: [ FlexLayoutModule, @@ -613,6 +622,8 @@ const MY_DATE_FORMAT = { MatDialogModuleNew, CalendarModule.forRoot({provide: CalendarDateAdapter, useFactory: adapterFactory}), CodeEditorModule.forRoot(), + MatSidenavModule, + MonacoEditorModule.forRoot(), ], }) diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 8187457400..8d0fee5456 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -233,6 +233,7 @@ import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/dir import {PortfolioIncludedTasksComponent} from './projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component'; import {TaskSimilarityViewComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component'; import {UploadGradesComponent} from './units/states/portfolios/upload-grades/upload-grades.component'; +import {TaskOverseerReportComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component'; export const DoubtfireAngularJSModule = angular .module('doubtfire', [ @@ -575,3 +576,8 @@ DoubtfireAngularJSModule.directive( 'fUploadGrades', downgradeComponent({component: UploadGradesComponent}), ); + +DoubtfireAngularJSModule.directive( + 'fTaskOverseerReport', + downgradeComponent({component: TaskOverseerReportComponent}), +); diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.html new file mode 100644 index 0000000000..5609881791 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.html @@ -0,0 +1,167 @@ +
    + +
    + + @for (oa of overseerAssessments; track oa; let idx = $index) { + + + + + Submission {{ idx + 1 }}: {{ oa.timestamp | humanizedDate }} + @if (idx === 0) { + (Most recent) + } + + @if (oa.reportReady) { +
    + {{ oa.passedSteps }} / {{ oa.totalSteps }} + @if (oa.passedSteps === oa.totalSteps) { + done + } @else { + cancel + } +
    + } @else { +
    + Tests In Progress + +
    + } +
    + + + @for (result of oa.stepResultsCache.values | async; track result.id; let idx = $index) { + + + + Step {{ idx + 1 }}: {{ result.overseerStep?.displayName }} + @if (result.pass) { + done + } @else { + cancel + } + + + + + + @if (!result.pass) { +

    + {{ result.feedbackMessage }} +

    + } + + @if ( + result.expectedOutput && + result.expectedOutput !== result.stdout && + (result.overseerStep?.stepType === 'output_diff' || !result.overseerStep) + ) { +
    + + + + + + +
    + @if (viewOutput === 'diff' || viewOutput === 'split_diff') { + @if (result.expectedOutput !== result.stdout) { + + } + } @else if (viewOutput === 'your_output') { + + } @else if (viewOutput === 'expected_output') { + + } + } @else if (result.stdout) { +
    {{result.stdout}}
    + } @else if (result.pass) { +
    SUCCESS
    +
    + (No Output) +
    + } +
    + } + @if (loadingAssessments.has(oa.id)) { +
    + +
    + } @else { + @for (skipped of oa.stepsSkipped; track skipped.id; let idx = $index) { + + + + + + Step {{ oa.stepResultsCache.currentValues.length + idx + 1 }}: + {{ skipped?.displayName ?? '-' }} + (Skipped) + + + pause + + + } + } +
    +
    + } @empty { +
    + subtitles_off +
    No submission reports for this task.
    +
    + } +
    diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.scss b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.ts new file mode 100644 index 0000000000..cf4c5a86ed --- /dev/null +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.ts @@ -0,0 +1,145 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {OverseerAssessment} from 'src/app/api/models/doubtfire-model'; +import {Task} from 'src/app/api/models/task'; +import {OverseerAssessmentService} from 'src/app/api/services/overseer-assessment.service'; +import {OverseerStepResultService} from 'src/app/api/services/overseer-step-result.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {TaskSubmissionService} from 'src/app/common/services/task-submission.service'; + +@Component({ + selector: 'f-task-overseer-report', + templateUrl: './task-overseer-report.component.html', + styleUrl: './task-overseer-report.component.scss', +}) +export class TaskOverseerReportComponent implements OnInit { + @Input() task: Task; + @Input() loadOverseerAssessmentId?: number; + + constructor( + private alerts: AlertService, + private submissions: TaskSubmissionService, + private overseerAssessmentService: OverseerAssessmentService, + private overseerStepResultsService: OverseerStepResultService, + ) {} + + public viewOutput: 'your_output' | 'expected_output' | 'diff' | 'split_diff' = 'your_output'; + + stdoutOptions = { + theme: 'vs-dark', + language: 'plaintext', + renderMinimap: false, + lineNumbers: false, + + minimap: { + enabled: false, + }, + }; + editorOptions = { + theme: 'vs', + language: 'text', + renderMinimap: false, + minimap: { + enabled: false, + }, + readOnly: true, + }; + + diffEditorOptions = { + theme: 'vs', + language: 'plaintext', + renderMinimap: false, + readOnly: true, + domReadOnly: true, + renderMarginRevertIcon: false, + enableSplitViewResizing: false, + useInlineViewWhenSpaceIsLimited: false, + renderSideBySideInlineBreakpoint: 1000, + renderSideBySide: true, + compactMode: true, + minimap: { + enabled: false, + }, + lineNumbers: 'off', + }; + + diff() { + this.diffEditorOptions.renderSideBySide = false; + this.diffEditorOptions.compactMode = true; + this.diffEditorOptions = {...this.diffEditorOptions}; + this.viewOutput = 'diff'; + } + + splitDiff() { + this.diffEditorOptions.renderSideBySide = true; + this.diffEditorOptions.compactMode = false; + this.diffEditorOptions = {...this.diffEditorOptions}; + setTimeout(() => { + this.viewOutput = 'split_diff'; + }, 100); + } + + submissionOutput() { + this.viewOutput = 'your_output'; + } + + expectedOutput() { + this.viewOutput = 'expected_output'; + } + + public overseerAssessments: OverseerAssessment[] = []; + + ngOnInit(): void { + this.loadAssessments(); + } + + loadAssessments(isRefresh: boolean = false) { + if (isRefresh) { + this.loadOverseerAssessmentId = null; + } + this.overseerAssessmentService.queryForTask(this.task).subscribe({ + next: (assessments) => { + this.overseerAssessments = assessments; + for (const oa of this.overseerAssessments) { + for (const result of oa.stepResultsCache.currentValues) { + result.overseerStep = this.task.definition.overseerStepsCache.currentValues.find( + (step) => step.id === result.overseerStepId, + ); + } + } + }, + error: (error) => { + this.alerts.error(`Failed to load overseer reports: ${error}`, 6000); + }, + }); + } + + loadingAssessments = new Set(); + + onAssessmentOpen(overseerAssesment: OverseerAssessment) { + if (this.loadOverseerAssessmentId === overseerAssesment.id) { + setTimeout(() => { + const el = document.getElementById(`oa-panel-${overseerAssesment.id}`); + el?.scrollIntoView({behavior: 'smooth', block: 'start'}); + }, 250); + } + + this.loadingAssessments.add(overseerAssesment.id); + + this.overseerStepResultsService.getOverseerStepResults(overseerAssesment).subscribe({ + next: () => { + for (const oa of this.overseerAssessments) { + for (const result of oa.stepResultsCache.currentValues) { + result.overseerStep = this.task.definition.overseerStepsCache.currentValues.find( + (step) => step.id === result.overseerStepId, + ); + } + } + this.loadingAssessments.delete(overseerAssesment.id); + }, + error: (error) => { + console.error(error); + this.loadingAssessments.delete(overseerAssesment.id); + }, + }); + } +} diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.coffee b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.coffee index f70113800c..a5d9df4511 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.coffee +++ b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.coffee @@ -25,7 +25,7 @@ angular.module('doubtfire.projects.states.dashboard.directives.task-dashboard', # Is the current user a tutor? $scope.tutor = $stateParams.tutor # the ways in which the dashboard can be viewed - $scope.dashboardViews = ["details", "submission", "task", "similarities"] + $scope.dashboardViews = ["details", "submission", "task", "similarities", "overseer"] # set the current dashboard view to details by default updateCurrentView = -> diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.html index df91328f92..fc861e5b34 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.html @@ -16,6 +16,10 @@ + + + + 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 16d998bc0a..45893297a0 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 @@ -2,7 +2,9 @@
    {{task.definition.name}} - {{task.definition.name}} + {{task.definition.name}} + @@ -94,6 +99,9 @@
    +
    + +
    diff --git a/src/app/projects/states/dashboard/selected-task.service.ts b/src/app/projects/states/dashboard/selected-task.service.ts index 204714694e..3385019a77 100644 --- a/src/app/projects/states/dashboard/selected-task.service.ts +++ b/src/app/projects/states/dashboard/selected-task.service.ts @@ -10,6 +10,7 @@ export enum DashboardViews { similarity, staff_notes, discussion_prompts, + overseer, } @Injectable({ @@ -67,6 +68,10 @@ export class SelectedTaskService { this.currentView$.next(DashboardViews.staff_notes); } + public showOverseerReports() { + this.currentView$.next(DashboardViews.overseer); + } + public showDiscussionPrompts() { this.currentView$.next(DashboardViews.discussion_prompts); } diff --git a/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.html b/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.html index d6d344fc06..4feda189a2 100644 --- a/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.html +++ b/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.html @@ -1,15 +1,49 @@ -
    -
    - -
    -
    -
    {{ comment.text }}
    - - -
    +
    + +
    +
    + +
    + @if (comment.overseerStatus === 'pre_queued') { + Tests In Progress + } @else if (comment.overseerStatus === 'passed') { + Tests Passed {{ comment.overseerPassedSteps }} / {{ comment.overseerTotalSteps }} + } @else if (comment.overseerStatus === 'failed') { + Tests Failed {{ comment.overseerPassedSteps }} / {{ comment.overseerTotalSteps }} + }
    - + +
    + + @if (comment.overseerStatus === 'passed') { + check_circle + } @else if (comment.overseerStatus === 'failed') { + highlight_off_outline + } @else if (comment.overseerStatus === 'pre_queued') { + + } + + @if (comment.overseerStatus !== 'pre_queued') { + + }
    diff --git a/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.scss b/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.scss index 816d3724e7..e69de29bb2 100644 --- a/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.scss +++ b/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.scss @@ -1,56 +0,0 @@ -div { - width: 100%; -} - -p { - color: #2c2c2c; - text-align: center; -} - -hr { - width: 100%; -} - -.hr-fade { - background: linear-gradient(to right, transparent, #9696969d, transparent); - width: 100%; - margin-top: 1px; -} - -.fade-text { - color: #9696969d; - opacity: 0.8; -} - -.hr-text { - margin: 0; - line-height: 1em; - position: relative; - outline: 0; - border: 0; - color: black; - text-align: center; - height: 1.5em; - opacity: 0.8; - &:before { - content: ""; - background: linear-gradient(to right, transparent, #9696969d, transparent); - position: absolute; - left: 0; - top: 50%; - width: 100%; - height: 1px; - } - &:after { - content: attr(data-content); - position: relative; - display: inline-block; - color: black; - - padding: 0 0.5em; - line-height: 1.5em; - - color: #9696969d; - background-color: #fff; - } -} diff --git a/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.ts b/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.ts index ed2e404b26..a57f866649 100644 --- a/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.ts +++ b/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.ts @@ -1,8 +1,11 @@ -import { Component, OnInit, Input, Inject } from '@angular/core'; -import { TaskSubmissionService, TaskAssessmentResult } from 'src/app/common/services/task-submission.service'; -import { TaskAssessmentModalService } from 'src/app/common/modals/task-assessment-modal/task-assessment-modal.service'; -import { Task } from 'src/app/api/models/doubtfire-model'; -import { AlertService } from 'src/app/common/services/alert.service'; +import {Component, OnInit, Input, Inject} from '@angular/core'; +import { + TaskSubmissionService, + TaskAssessmentResult, +} from 'src/app/common/services/task-submission.service'; +import {TaskAssessmentModalService} from 'src/app/common/modals/task-assessment-modal/task-assessment-modal.service'; +import {Task} from 'src/app/api/models/doubtfire-model'; +import {AlertService} from 'src/app/common/services/alert.service'; export interface User { id: number; @@ -23,6 +26,11 @@ export interface TaskAssessmentComment { recipient_read_time?: Date; // new fields that extend regular Comment Interface. TODO: create a separate Comment entity and extend it. assessment_result?: TaskAssessmentResult; + overseerAssessmentId: number; + overseerPassedSteps: number; + overseerTotalSteps: number; + overseerInProgress: boolean; + overseerStatus: string; } @Component({ @@ -53,12 +61,12 @@ export class TaskAssessmentCommentComponent implements OnInit { return this.comment.assessment_result.assessment_output; } - showTaskAssessmentResult() { - this.modalService.show(this.task); + showTaskAssessmentResult(overseerAssessmentId?: number) { + this.modalService.show(this.task, overseerAssessmentId); } scroll(el: HTMLElement) { - el.scrollIntoView({ behavior: 'smooth' }); + el.scrollIntoView({behavior: 'smooth'}); } update(): void { diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html index 0e5c521bcb..fb78832932 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html @@ -1,12 +1,19 @@
    - - Automation Enabled - +
    + + Automation Enabled + +
    + + You can add or modify Overseer steps while keeping this unchecked and use the test submission + feature. Once everything is ready, enable automation to apply it to all student submissions. + +
    Docker Image @@ -18,10 +25,6 @@ Docker image for Overseer - - upload_file Test Submission -
    + +
    + +@if (taskDefinition.overseerImageId) { + @if (!taskDefinitionHasChanges()) { +
    +
    + @if (this.selectedOverseerStep) { + + + } +
    +
    + + + +
    +
    Overseer Steps
    + @if (!newOverseerStep) { + + } +
    +
    + @for (step of overseerSteps; track step.id) { +
    + drag_indicator + {{ step.sortOrder }}. {{ step.name ?? 'Untitled Step' }} +
    + } + @if (newOverseerStep) { +
    + + {{ newOverseerStep.sortOrder }}. {{ newOverseerStep.name || 'Untitled Step' }} +
    + } + + +
    +
    + + @if (selectedOverseerStep) { + + + +
    + + Step Name + + Visible to staff only + + + Description + + Visible to staff only + +
    + + + Step Type + + Custom Script + Input/Output + + + + @if (selectedOverseerStep.stepType === 'output_diff') { +
    +
    + + Script Input File + + + (none) + @for (file of taskDefinition.overseerResourceFiles; track file) { + {{ file }} + } + + The selected file will be passed as standard input to the program when it + runs. + + + + Show input file to student + +
    +
    + + Expected Output File + + @for (file of taskDefinition.overseerResourceFiles; track file) { + {{ file }} + } + + Program output must exactly match this file to pass. + + + Show expected output file to student + +
    + Partial Output Comparison +
    + + If enabled, test passes if the expected output appears anywhere in the + student's output. Otherwise, only exact matches pass. + +
    +
    + + } + +
    +
    Execution Script
    + + Language + + @for (language of getLanguages; track language) { + {{ + language.id ?? language.aliases?.[0] ?? language.id + }} + } + + Visual use only + +
    + + + +
    + + + @if (selectedOverseerStep.stepType === 'status_check') { + This step passes only if the script exits with status 0. Any non-zero exit code + marks the step as failed. + } @else if (selectedOverseerStep.stepType === 'output_diff') { + The script output is compared against the expected output. If noisy warnings + cause failures, move that logic into a separate step. + } + +
    + + Time Limit (s) + + How long the code can execute for before overseer automatically kills the + process. A timeout will result in a failed test (Exit status 124) + +
    +
    + +
    + Halt on success + + @if (selectedOverseerStep.haltOnSuccess) { + + Status on Success + + No Change + @for (status of statusKeys; track status) { + {{ statusName(status) }} + } + + + } +
    +
    + +
    + +
    + + Halt on failure + + @if (selectedOverseerStep.haltOnFailure) { + + + Status on Fail + + No Change + @for (status of statusKeys; track status) { + {{ statusName(status) }} + } + + + } +
    +
    +
    + + +
    + + + Test Name + + Shown to student in overseer report + + + Description + + Shown to student in overseer report + +
    + + Feedback message + + Feedback to provide to the student if the task failed. You can leave this blank + and a default will message will be used instead. + + + +
    + Enabled +
    + + If disabled, this step will not run and will not appear in the Overseer report for + students. + +
    + +
    + Show output +
    + + When enabled, students can see this step’s standard output, including script + output, program output, and compile errors. + +
    +
    +
    + } @else { +
    + No steps selected + tab_unselected +
    + } +
    +
    + } @else { +
    + Please save the task definition before configuring the overseer steps. +
    + } +} diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.scss b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.scss index e69de29bb2..ff10c528b0 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.scss +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.scss @@ -0,0 +1,50 @@ +.example-list { + width: 300px; + max-width: 100%; + border: solid 1px #ccc; + min-height: 50px; + height: 100%; + display: block; + background: white; + // border-radius: 4px; + overflow: hidden; +} + +.example-box { + padding: 20px 10px; + border-bottom: solid 1px #ccc; + height: 50px; + color: rgba(0, 0, 0, 0.87); + display: flex; + flex-direction: row; + align-items: center; + // justify-content: space-between; + box-sizing: border-box; + cursor: pointer; + background: white; + font-size: 14px; + font-family: sans-serif; + user-select: none; +} + +.cdk-drag-preview { + border: none; + box-sizing: border-box; + border-radius: 4px; + box-shadow: + 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12); +} +.cdk-drag-placeholder { + opacity: 0; +} +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} +.example-box:last-child { + border: none; +} +.example-list.cdk-drop-list-dragging .example-box:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.ts index ea0f92ba89..07f7eb597c 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.ts @@ -1,32 +1,52 @@ -import {Component, Input, OnChanges} from '@angular/core'; +import {CdkDragDrop, moveItemInArray} from '@angular/cdk/drag-drop'; +import {Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild} from '@angular/core'; import {Observable} from 'rxjs'; import { OverseerAssessment, OverseerImage, OverseerImageService, Task, + TaskService, + TaskStatusEnum, User, UserService, } from 'src/app/api/models/doubtfire-model'; +import {OverseerStep} from 'src/app/api/models/overseer/overseer-step'; import {TaskDefinition} from 'src/app/api/models/task-definition'; import {Unit} from 'src/app/api/models/unit'; +import {OverseerStepService} from 'src/app/api/services/overseer-step.service'; import {TaskDefinitionService} from 'src/app/api/services/task-definition.service'; import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; import {TaskAssessmentModalService} from 'src/app/common/modals/task-assessment-modal/task-assessment-modal.service'; import {AlertService} from 'src/app/common/services/alert.service'; import {TaskSubmissionService} from 'src/app/common/services/task-submission.service'; import {OverseerScriptEditorModalService} from './overseer-script-editor-modal/overseer-script-editor-modal.service'; - +import * as monaco from 'monaco-editor'; +import {MatSelectChange} from '@angular/material/select'; @Component({ selector: 'f-task-definition-overseer', templateUrl: 'task-definition-overseer.component.html', styleUrls: ['task-definition-overseer.component.scss'], }) -export class TaskDefinitionOverseerComponent implements OnChanges { +export class TaskDefinitionOverseerComponent implements OnChanges, OnInit { @Input() taskDefinition: TaskDefinition; + @ViewChild('editor') editorComponent; + public currentUserTask: Task; + editorOptions = { + theme: 'vs', + language: 'shell', + renderMinimap: false, + + minimap: { + enabled: false, + }, + }; + + public stepType: 'status_check' | 'output_diff' = 'status_check'; + public visibility = 'public'; constructor( private alerts: AlertService, private overseerImageService: OverseerImageService, @@ -36,8 +56,175 @@ export class TaskDefinitionOverseerComponent implements OnChanges { private taskDefinitionService: TaskDefinitionService, private fileDownloaderService: FileDownloaderService, private overseerScriptEditorModal: OverseerScriptEditorModalService, + private overseerStepService: OverseerStepService, + private taskService: TaskService, ) {} + public get statusKeys() { + return this.taskService.statusKeys; + } + + public statusName(status: TaskStatusEnum) { + return this.taskService.statusLabels.get(status); + } + + public selectedOverseerStep: OverseerStep = null; + public newOverseerStep: OverseerStep = null; + + public overseerSteps: OverseerStep[] = []; + + selectStep(step: OverseerStep) { + this.selectedOverseerStep = step; + setTimeout(() => { + const editor = this.editorComponent?._editor; + if (editor) { + editor.revealLine(1); + editor.setScrollPosition({scrollTop: 0}); + } + }); + } + + addStep() { + this.newOverseerStep = new OverseerStep(this.taskDefinition); + this.newOverseerStep.stepType = 'status_check'; + this.newOverseerStep.timeout = 30; + this.newOverseerStep.enabled = true; + this.newOverseerStep.showStdout = true; + this.newOverseerStep.statusOnFailure = 'no_change'; + this.newOverseerStep.statusOnSuccess = 'no_change'; + this.newOverseerStep.commandLanguage = 'shell'; + this.newOverseerStep.runCommand = '#!/bin/bash\n\n'; + this.newOverseerStep.showExpectedOutput = true; + + this.newOverseerStep.sortOrder = this.taskDefinition.overseerStepsCache.currentValues.length; + this.selectedOverseerStep = this.newOverseerStep; + } + + getFeedbackMessagePlaceholder() { + // If the feedback message is blank, this is what will automatically be used + // (If this changes, ensure to update it in AcceptOverseerJob) + switch (this.selectedOverseerStep.stepType) { + case 'status_check': + return 'This test did not complete successfully. Check the output for any errors.'; + case 'output_diff': + return 'Your output did not match the expected result.'; + } + } + + ngOnInit(): void { + this.taskDefinition.overseerStepsCache.values.subscribe((steps) => { + this.overseerSteps = [...steps]; + }); + } + + public get getLanguages() { + return monaco?.languages.getLanguages() ?? []; + } + + onLanguageChange(event: MatSelectChange) { + const value = event.value; + this.editorOptions.language = value; + this.editorOptions = {...this.editorOptions}; + } + + drop(event: CdkDragDrop) { + if (this.newOverseerStep) { + this.alerts.error('Please save changes before re-ordering steps', 3000); + return; + } + moveItemInArray(this.overseerSteps, event.previousIndex, event.currentIndex); + // TODO: open endpoint to update sort orders in a single request + for (let i = 0; i < this.overseerSteps.length; i++) { + const step = this.taskDefinition.overseerStepsCache.get(this.overseerSteps[i].id); + if (step.sortOrder === i) { + // Ignore if no change + continue; + } + step.sortOrder = i; + this.overseerStepService + .update( + { + id: step.id, + unitId: this.unit.id, + taskDefId: this.taskDefinition.id, + }, + { + entity: step, + constructorParams: this.taskDefinition, + }, + ) + .subscribe({ + next: () => { + // console.log('updated!'); + }, + error: (error) => { + this.alerts.error(`Failed to update order of steps: ${error}`, 6000); + }, + }); + } + } + + deleteStep() { + if (this.selectedOverseerStep && this.selectedOverseerStep === this.newOverseerStep) { + this.newOverseerStep = null; + this.selectedOverseerStep = null; + return; + } + this.selectedOverseerStep?.delete(); + this.selectedOverseerStep = null; + } + + saveStep() { + if (!this.selectedOverseerStep.id) { + // this.newOverseerStep.runCommand = this.model.value; + this.overseerStepService + .create( + { + unitId: this.unit.id, + taskDefId: this.taskDefinition.id, + }, + { + entity: this.newOverseerStep, + }, + ) + .subscribe({ + next: (result) => { + this.alerts.success('Added overseer step', 3000); + result.taskDefinition = this.taskDefinition; + this.taskDefinition.overseerStepsCache.add(result); + this.selectStep(result); + this.newOverseerStep = null; + }, + error: (error) => { + console.error(error); + this.alerts.error(error, 3000); + }, + }); + } else { + this.overseerStepService + .update( + { + id: this.selectedOverseerStep.id, + unitId: this.unit.id, + taskDefId: this.taskDefinition.id, + }, + { + entity: this.selectedOverseerStep, + cache: this.taskDefinition.overseerStepsCache, + }, + ) + .subscribe({ + next: (result) => { + this.alerts.success('Saved overseer step', 3000); + }, + error: (error) => { + console.error(error); + this.alerts.error(error, 3000); + }, + }); + } + } + public get overseerEnabled(): boolean { return this.unit.overseerEnabled; } @@ -54,12 +241,21 @@ export class TaskDefinitionOverseerComponent implements OnChanges { return this.userService.currentUser; } - public ngOnChanges() { + public taskDefinitionHasChanges(): boolean { + return this.taskDefinition.hasChanges(this.taskDefinitionService.mapping); + } + + public ngOnChanges(changes: SimpleChanges) { const proj = this.unit.findProjectForUsername(this.currentUser.username); if (proj) { this.currentUserTask = proj.findTaskForDefinition(this.taskDefinition.id); this.hasAnySubmissions(); } + if (changes['taskDefinition']) { + this.taskDefinition.overseerStepsCache.values.subscribe((steps) => { + this.overseerSteps = [...steps]; + }); + } } testSubmission() { @@ -69,7 +265,7 @@ export class TaskDefinitionOverseerComponent implements OnChanges { this.currentUserTask.definition = this.taskDefinition; this.currentUserTask.status = 'ready_for_feedback'; this.currentUserTask.id = this.taskDefinition.id; // set a default id... - this.hasAnySubmissions(); + // this.hasAnySubmissions(); } this.currentUserTask.presentTaskSubmissionModal(this.currentUserTask.status, false, true); From 4f607926e3ac305316a05607fda7f7bff7faec00 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:42:12 +1100 Subject: [PATCH 663/776] feat: project task planner gantt chart (#1051) * feat: init project plan gatt chart * chore: set compatible version * chore: update gantt chart package * refactor: display task items and map prerequisites * refactor: add task target start and due date * refactor: show prerequisite for tasks in tooltip * refactor: save target and due dates * feat: display task definition dates as baseline items * chore: remove extension * fix: create unique baseline item * refactor: fix timezone issues * refactor: clean up ui buttons * refactor: ensure timeline view shows the earliest task * chore: always show task planner * refactor: rename project plan * refactor: minify ui * chore: confirm changes * feat: allow target grade changes * refactor: display tasks target grade * feat: link to task details * refactor: remove debug * chore: remove old task sliders * refactor: display show date toggle * chore: add curved links * fix: save target dates * chore: reverse prerequisite links * refactor: reset target dates if task is prerequisite for another task * chore: fix gantt chart config * refactor: display warning if prerequisite task ends after dependent task starts * fix: return false if no linked item * refactor: highlight prerequisite conflicts * refactor: move task planner into its own component * refactor: highlight prerequisites on task hover * chore: fix prerequisite conflicts * feat: add status icon to task list * chore: update bar color * feat: display task prerequisites modal * refactor: display dependent tasks * fix: only use target start and end date if unit allows flexible dates * refactor: ensure prerequisites are mapped * chore: only update z-index of lines when hovering over * feat: task planner card for project dashboard * chore: add more details * chore: add tooltip delay * chore: add description heading * chore: add back watch command * refactor: use single endpoint to reset all target dates * chore: add button to redirect to task planner * chore: only show dates in tooltip * refactor: set gantt view dates to latest possible end date * feat: scroll to and highlight selected task item * refactor: remove unused code * refactor: remove unused code * chore: format * refactor: assign gantt config variable * refactor: add oop logic + cleanup duplicated code * fix: convert correct deadline date string * chore: import task planner card --- angular.json | 3 +- package-lock.json | 68 +++ package.json | 2 + src/app/api/models/project.ts | 13 + src/app/api/models/task.ts | 30 +- src/app/api/models/unit.ts | 10 + .../api/services/task-prerequisite.service.ts | 13 + src/app/api/services/task.service.ts | 8 + .../task-dropdown.component.html | 12 +- .../task-date-slider.component.html | 13 +- src/app/doubtfire-angular.module.ts | 36 ++ src/app/doubtfire-angularjs.module.ts | 18 + src/app/doubtfire.states.ts | 5 +- .../progress-dashboard.tpl.html | 5 + .../task-planner-card.component.html | 28 + .../task-planner-card.component.scss | 0 .../task-planner-card.component.ts | 14 + .../states/plan/project-plan.component.html | 35 +- .../states/plan/project-plan.component.scss | 0 .../states/plan/project-plan.component.ts | 63 +- ...planner-prerequisites-modal.component.html | 79 +++ ...planner-prerequisites-modal.component.scss | 0 ...k-planner-prerequisites-modal.component.ts | 39 ++ ...ask-planner-prerequisites-modal.service.ts | 27 + .../task-planner/task-planner.component.html | 129 +++++ .../task-planner/task-planner.component.scss | 18 + .../task-planner/task-planner.component.ts | 544 ++++++++++++++++++ src/styles.scss | 1 + tsconfig.json | 3 +- 29 files changed, 1184 insertions(+), 32 deletions(-) create mode 100644 src/app/projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component.html create mode 100644 src/app/projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component.scss create mode 100644 src/app/projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component.ts create mode 100644 src/app/projects/states/plan/project-plan.component.scss create mode 100644 src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.html create mode 100644 src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.scss create mode 100644 src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.ts create mode 100644 src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.service.ts create mode 100644 src/app/projects/states/plan/task-planner/task-planner.component.html create mode 100644 src/app/projects/states/plan/task-planner/task-planner.component.scss create mode 100644 src/app/projects/states/plan/task-planner/task-planner.component.ts diff --git a/angular.json b/angular.json index 0d892151bd..6a528bd8c1 100644 --- a/angular.json +++ b/angular.json @@ -49,7 +49,8 @@ "./build/assets/node_modules/codemirror/lib/codemirror.css", "./build/assets/node_modules/codemirror/theme/xq-light.css", "./build/assets/node_modules/nvd3/build/nv.d3.css", - "node_modules/@ctrl/ngx-emoji-mart/picker.css" + "node_modules/@ctrl/ngx-emoji-mart/picker.css", + "node_modules/@worktile/gantt/styles/index.scss" ], "scripts": [ "node_modules/moment/moment.js", diff --git a/package-lock.json b/package-lock.json index 05dae886a7..53f1d09af5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@uirouter/angularjs": "^1.0.30", "@uirouter/core": "^6.1.0", "@uirouter/rx": "^1.0.0", + "@worktile/gantt": "^18.0.5", "angular": "1.5.11", "angular-calendar": "^0.31.1", "angular-filter": "0.5.17", @@ -55,6 +56,7 @@ "es5-shim": "^4.5.12", "file-saver": "^2.0.5", "font-awesome": "~4.7.0", + "html2canvas": "^1.4.1", "html5-qrcode": "^2.3.8", "jquery": "2.1.4", "lodash": "~4.17", @@ -6186,6 +6188,22 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@worktile/gantt": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/@worktile/gantt/-/gantt-18.0.5.tgz", + "integrity": "sha512-LCcWaFBmeg5u9cVDEmREHdR+qJJHE3Ld4VxdoJpTXfhDkx2f19tp0wMR9MkwOLRwwTCx/5gGJ1kTbz4P0Zfc1Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": ">=17.0.0", + "@angular/common": ">=17.0.0", + "@angular/core": ">=17.0.0", + "date-fns": ">=2.0.0", + "rxjs": "^6.5.0 || ^7.0.0" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "dev": true, @@ -7235,6 +7253,15 @@ "node": ">= 0.4" } }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "funding": [ @@ -8704,6 +8731,15 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-loader": { "version": "6.10.0", "dev": true, @@ -8860,6 +8896,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -12848,6 +12885,19 @@ "dev": true, "license": "MIT" }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/html5-qrcode": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz", @@ -21515,6 +21565,15 @@ "node": ">=0.10" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "dev": true, @@ -22306,6 +22365,15 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "3.4.0", "dev": true, diff --git a/package.json b/package.json index d3eeccad5d..4b9234d2b7 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@uirouter/angularjs": "^1.0.30", "@uirouter/core": "^6.1.0", "@uirouter/rx": "^1.0.0", + "@worktile/gantt": "^18.0.5", "angular": "1.5.11", "angular-calendar": "^0.31.1", "angular-filter": "0.5.17", @@ -74,6 +75,7 @@ "es5-shim": "^4.5.12", "file-saver": "^2.0.5", "font-awesome": "~4.7.0", + "html2canvas": "^1.4.1", "html5-qrcode": "^2.3.8", "jquery": "2.1.4", "lodash": "~4.17", diff --git a/src/app/api/models/project.ts b/src/app/api/models/project.ts index df3c3cda26..2afef9ffa2 100644 --- a/src/app/api/models/project.ts +++ b/src/app/api/models/project.ts @@ -537,4 +537,17 @@ export class Project extends Entity { const httpClient = AppInjector.get(HttpClient); return httpClient.get(this.tasksIncludedInPortfolioUrl()); } + + public resetTargetDates(): Observable { + const projectService: ProjectService = AppInjector.get(ProjectService); + return projectService.update( + { + projectId: this.id, + }, + { + endpointFormat: '/projects/:projectId:/reset_target_dates', + entity: this, + }, + ); + } } diff --git a/src/app/api/models/task.ts b/src/app/api/models/task.ts index d624a8285d..e2d85bf61b 100644 --- a/src/app/api/models/task.ts +++ b/src/app/api/models/task.ts @@ -53,6 +53,9 @@ export class Task extends Entity { pinned: boolean = false; + targetStartDate: Date; + targetDueDate: Date; + public topWeight: number = 0; public readonly commentCache: EntityCache = new EntityCache(); @@ -167,7 +170,9 @@ export class Task extends Entity { } public localDueDate(): Date { - if (this.dueDate) { + if (this.targetDueDate && this.unit.allowFlexibleDates) { + return this.targetDueDate; + } else if (this.dueDate) { return this.dueDate; } else { return this.definition.localDueDate(); @@ -236,6 +241,25 @@ export class Task extends Entity { ); } + public saveTargetDates(startDate: Date | string, dueDate: Date | string): Observable { + const taskService: TaskService = AppInjector.get(TaskService); + + return taskService.update( + { + projectId: this.project.id, + taskDefId: this.definition.id, + }, + { + endpointFormat: '/projects/:projectId:/task_def_id/:taskDefId:/target_dates', + entity: this, + body: { + target_start_date: startDate, + target_due_date: dueDate, + }, + }, + ); + } + /** * Calculate the time between two dates * @@ -297,7 +321,9 @@ export class Task extends Entity { } public get startDate(): Date { - if (this.extensions < 0) { + if (this.targetStartDate && this.unit.allowFlexibleDates) { + return this.targetStartDate; + } else if (this.extensions < 0) { // If the task has an extension, the start date is the due date minus the extension return MappingFunctions.addWeeks(this.definition.startDate, this.extensions); } else { diff --git a/src/app/api/models/unit.ts b/src/app/api/models/unit.ts index 0caa668085..452bbceb98 100644 --- a/src/app/api/models/unit.ts +++ b/src/app/api/models/unit.ts @@ -34,6 +34,7 @@ import {HttpClient, HttpParams} from '@angular/common/http'; import {TaskPrerequisiteService} from '../services/task-prerequisite.service'; import {MarkingSession} from './marking-session'; import {MarkingSessionService} from '../services/marking-session.service'; +import {TaskPrerequisite} from './task-prerequisite'; export class Unit extends Entity { id: number; @@ -721,4 +722,13 @@ export class Unit extends Entity { }), ); } + + public get taskDefinitionsPrerequisitesUrl(): string { + return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.id}/task_prerequisites`; + } + + public getTaskPrerequisites(): Observable { + const prerequisiteService = AppInjector.get(TaskPrerequisiteService); + return prerequisiteService.getUnitPrerequisites(this.id); + } } diff --git a/src/app/api/services/task-prerequisite.service.ts b/src/app/api/services/task-prerequisite.service.ts index d3906860f8..ffb323b0d1 100644 --- a/src/app/api/services/task-prerequisite.service.ts +++ b/src/app/api/services/task-prerequisite.service.ts @@ -9,6 +9,8 @@ export class TaskPrerequisiteService extends CachedEntityService { keys: 'dueDate', toEntityFn: MappingFunctions.mapDateToEndOfDay, }, + { + keys: 'targetStartDate', + toEntityFn: MappingFunctions.mapDateToEndOfDay, + }, + { + keys: 'targetDueDate', + toEntityFn: MappingFunctions.mapDateToEndOfDay, + }, 'extensions', 'scormExtensions', { diff --git a/src/app/common/header/task-dropdown/task-dropdown.component.html b/src/app/common/header/task-dropdown/task-dropdown.component.html index c5d1d763d4..cc8cec968f 100644 --- a/src/app/common/header/task-dropdown/task-dropdown.component.html +++ b/src/app/common/header/task-dropdown/task-dropdown.component.html @@ -63,13 +63,11 @@ > Dashboard - @if (currentProject.unit.allowFlexibleDates) { - - - } + + + } +
    @if (editMode) { diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 86b5fc459d..33c8db369b 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -300,10 +300,15 @@ import {MarkingSessionService} from './api/services/marking-session.service'; import {PortfolioIncludedTasksComponent} from './projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component'; import {OverseerScriptEditorModalComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component'; import {UploadGradesComponent} from './units/states/portfolios/upload-grades/upload-grades.component'; +import {GANTT_GLOBAL_CONFIG, GanttLinkLineType, NgxGanttModule} from '@worktile/gantt'; import {DiscussionPromptService} from './api/services/discussion-prompt.service'; import {DiscussionPromptsComponent} from './projects/states/discussion-prompts/discussion-prompts.component'; import {TaskDefinitionDiscussionPromptsComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component'; import {DiscussionPromptsViewComponent} from './projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component'; +import {TaskPlannerComponent} from './projects/states/plan/task-planner/task-planner.component'; +import {TaskPlannerPrerequisitesModalComponent} from './projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component'; +import {TaskPlannerPrerequisitesModalService} from './projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.service'; +import {TaskPlannerCardComponent} from './projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component'; import {OverseerStepService} from './api/services/overseer-step.service'; import {TaskOverseerReportComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component'; import {OverseerStepResultService} from './api/services/overseer-step-result.service'; @@ -321,6 +326,31 @@ const MY_DATE_FORMAT = { }, }; +const GANTT_CHART_CONFIG = { + provide: GANTT_GLOBAL_CONFIG, + useValue: { + // locale: 'en-US', + dateFormat: { + // timeZone: 'UTC', + weekStartsOn: 1, + week: 'w', + year: 'yyyy', + month: 'MMMM', + yearMonth: 'yyyy MMM', + yearQuarter: 'yyyy', + }, + linkOptions: { + showArrow: true, + lineType: GanttLinkLineType.curve, + }, + styleOptions: { + // lineHeight: '25', + // barHeight: '23', + // headerHeight: '50px', + }, + }, +}; + @NgModule({ // Components we declare declarations: [ @@ -466,6 +496,9 @@ const MY_DATE_FORMAT = { DiscussionPromptsComponent, TaskDefinitionDiscussionPromptsComponent, DiscussionPromptsViewComponent, + TaskPlannerComponent, + TaskPlannerCardComponent, + TaskPlannerPrerequisitesModalComponent, TaskOverseerReportComponent, ], providers: [ @@ -557,6 +590,8 @@ const MY_DATE_FORMAT = { TaskPrerequisiteService, MarkingSessionService, DiscussionPromptService, + GANTT_CHART_CONFIG, + TaskPlannerPrerequisitesModalService, OverseerStepService, OverseerStepResultService, ], @@ -622,6 +657,7 @@ const MY_DATE_FORMAT = { MatDialogModuleNew, CalendarModule.forRoot({provide: CalendarDateAdapter, useFactory: adapterFactory}), CodeEditorModule.forRoot(), + NgxGanttModule, MatSidenavModule, MonacoEditorModule.forRoot(), ], diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 8d0fee5456..40e8930490 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -233,6 +233,9 @@ import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/dir import {PortfolioIncludedTasksComponent} from './projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component'; import {TaskSimilarityViewComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component'; import {UploadGradesComponent} from './units/states/portfolios/upload-grades/upload-grades.component'; +import {ProjectPlanComponent} from './projects/states/plan/project-plan.component'; +import {TaskPlannerComponent} from './projects/states/plan/task-planner/task-planner.component'; +import {TaskPlannerCardComponent} from './projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component'; import {TaskOverseerReportComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component'; export const DoubtfireAngularJSModule = angular @@ -577,6 +580,21 @@ DoubtfireAngularJSModule.directive( downgradeComponent({component: UploadGradesComponent}), ); +DoubtfireAngularJSModule.directive( + 'fProjectPlan', + downgradeComponent({component: ProjectPlanComponent}), +); + +DoubtfireAngularJSModule.directive( + 'fTaskPlanner', + downgradeComponent({component: TaskPlannerComponent}), +); + +DoubtfireAngularJSModule.directive( + 'fTaskPlannerCard', + downgradeComponent({component: TaskPlannerCardComponent}), +); + DoubtfireAngularJSModule.directive( 'fTaskOverseerReport', downgradeComponent({component: TaskOverseerReportComponent}), diff --git a/src/app/doubtfire.states.ts b/src/app/doubtfire.states.ts index 01b722864a..f1710f6b54 100644 --- a/src/app/doubtfire.states.ts +++ b/src/app/doubtfire.states.ts @@ -429,8 +429,11 @@ const SuccessCloseState: NgHybridStateDeclaration = { const projectPlanState: NgHybridStateDeclaration = { name: 'project/plan', parent: 'projects/index', - url: '/plan', + url: '/plan?:taskDef?', component: ProjectPlanComponent, + params: { + taskDef: {value: null, squash: true, dynamic: true}, + }, // views: { // main: { // // Main body links to angular component diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html index 93069ef71b..b5859c8b2a 100644 --- a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html @@ -5,6 +5,11 @@

    +
    +
    + +
    +
    diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component.html b/src/app/projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component.html new file mode 100644 index 0000000000..da584e5670 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component.html @@ -0,0 +1,28 @@ + + + Plan Your Tasks + +

    + The Task Planner shows a timeline of your tasks, their due dates, and prerequisite + relationships. Use it to plan when to start and submit tasks, ensuring prerequisites are + completed early so you have time for feedback. +

    + @if (unit.allowFlexibleDates) { +

    + You can set your own start and target dates. Be mindful of each task’s + Feedback Deadline — submissions after this date will not be checked. Aim + to finish at least one week before the deadline, especially if you are targeting a higher + grade. +

    + } +
    +
    + + + +
    diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component.scss b/src/app/projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component.ts b/src/app/projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component.ts new file mode 100644 index 0000000000..3af5379eec --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component.ts @@ -0,0 +1,14 @@ +import {Component, Input} from '@angular/core'; +import {Project} from 'src/app/api/models/project'; + +@Component({ + selector: 'f-task-planner-card', + templateUrl: './task-planner-card.component.html', + styleUrl: './task-planner-card.component.scss', +}) +export class TaskPlannerCardComponent { + @Input() project: Project; + public get unit() { + return this.project?.unit; + } +} diff --git a/src/app/projects/states/plan/project-plan.component.html b/src/app/projects/states/plan/project-plan.component.html index d8f43331cb..cf49f02ec0 100644 --- a/src/app/projects/states/plan/project-plan.component.html +++ b/src/app/projects/states/plan/project-plan.component.html @@ -1,13 +1,34 @@
    -

    Plan Your Tasks

    +

    Task Planner

    - View and adjust the due dates of tasks. Remember to leave time to get and respond to feedback. + @if (unit.allowFlexibleDates) { + View and adjust the due dates for your tasks. Remember to leave time to get and respond to + feedback. + } @else { + View the task deadlines for your project, so you can organise your work and ensure you meet + all submission requirements on time. + } +

    +

    + Click on a task in the timeline to see how it connects to other tasks. This will show you which + tasks must be completed before it, and which tasks depend on it being completed first.

    - +
    -
    - @for (taskDef of taskDefs(); track $index) { - - } +
    +
    + + Target Grade + + @for (grade of gradeValues; track grade) { + {{ gradeString(grade) }} + } + +
    +
    diff --git a/src/app/projects/states/plan/project-plan.component.scss b/src/app/projects/states/plan/project-plan.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/plan/project-plan.component.ts b/src/app/projects/states/plan/project-plan.component.ts index c3002f3026..d1a9194f75 100644 --- a/src/app/projects/states/plan/project-plan.component.ts +++ b/src/app/projects/states/plan/project-plan.component.ts @@ -1,16 +1,44 @@ -import {Component} from '@angular/core'; +import {Component, OnInit, ViewChild} from '@angular/core'; +import {MatSelectChange} from '@angular/material/select'; +import {Project, ProjectService} from 'src/app/api/models/doubtfire-model'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {GradeService} from 'src/app/common/services/grade.service'; import {GlobalStateService} from '../index/global-state.service'; -import { Project, TaskDefinition } from 'src/app/api/models/doubtfire-model'; +import {TaskPlannerComponent} from './task-planner/task-planner.component'; @Component({ selector: 'f-project-plan', templateUrl: 'project-plan.component.html', - // styleUrls: ['project-plan.component.scss'] + styleUrls: ['project-plan.component.scss'], }) -export class ProjectPlanComponent { +export class ProjectPlanComponent implements OnInit { public project: Project; - constructor(private globalStateService: GlobalStateService) { + @ViewChild(TaskPlannerComponent) planner!: TaskPlannerComponent; + + public get unit() { + return this.project?.unit; + } + + public get gradeValues() { + return this.gradeService.gradeValues; + } + + public get gradeAcronyms() { + return this.gradeService.gradeAcronyms; + } + + public gradeString(grade: number) { + return this.gradeService.grades[grade]; + } + + constructor( + private globalStateService: GlobalStateService, + private gradeService: GradeService, + private projectService: ProjectService, + private alertService: AlertService, + ) { this.globalStateService.currentViewAndEntitySubject$.subscribe((viewAndEntity) => { if (viewAndEntity.viewType === 'PROJECT' && viewAndEntity.entity) { this.project = viewAndEntity.entity as Project; @@ -18,13 +46,26 @@ export class ProjectPlanComponent { }); } - public taskDefs(): TaskDefinition[] { - if (!this.project || !this.project.unit.taskDefinitions) { - return []; - } + public selectedTargetGrade: number; + + ngOnInit(): void { + this.selectedTargetGrade = this.project.targetGrade; + } + + onTargetGradeChange(event: MatSelectChange) { + const previousTargetGrade = this.project.targetGrade; + this.project.targetGrade = event.value; - return this.project.unit.taskDefinitions.filter((taskDef) => { - return taskDef.targetGrade <= this.project.targetGrade; + this.projectService.update(this.project).subscribe({ + next: () => { + this.alertService.success(`Succesfully updated target grade`, 2000); + this.planner.refreshItems(); + }, + error: (error) => { + this.project.targetGrade = previousTargetGrade; + this.selectedTargetGrade = previousTargetGrade; + this.alertService.error(`Failed to update target grade: ${error}`, 6000); + }, }); } } diff --git a/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.html b/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.html new file mode 100644 index 0000000000..5ea71774fd --- /dev/null +++ b/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.html @@ -0,0 +1,79 @@ + + + + {{ taskDefinition.abbreviation }} {{ taskDefinition.name }} + + + + + Task Description: +

    {{ taskDefinition.description }}

    + + @if (!task.hasPrerequisiteTasks()) { +
    This task has no prerequisites.
    + } + + @if (dependents.length) { + + + Required by + + + + + {{ taskDefinition.abbreviation }} {{ taskDefinition.name }} is a + prerequisite for the following tasks. In some cases, + {{ taskDefinition.abbreviation }} {{ taskDefinition.name }} needs to + reach the Discuss or Complete status, which requires + tutor feedback. Plan ahead to avoid being locked out of submission. + + + + + + + + + + + + + + + + + + +
    Task + {{ link.taskDefinition?.abbreviation }} {{ link.taskDefinition?.name }} + Submission Open + @if (link.taskDefinition.projectTask(project).blockedByPrerequisiteTasks()) { + block_outlined + } @else { + check_circle + } + + Required Status + +
    +
    +
    + } @else { +
    + This task is not a prerequisite for any other tasks. +
    + } +
    +
    diff --git a/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.scss b/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.ts b/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.ts new file mode 100644 index 0000000000..54f2aaadf0 --- /dev/null +++ b/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.ts @@ -0,0 +1,39 @@ +import {Component, Inject, Input, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA} from '@angular/material/dialog'; +import {MatTableDataSource} from '@angular/material/table'; +import {Project} from 'src/app/api/models/project'; +import {TaskDefinition} from 'src/app/api/models/task-definition'; +import {TaskPrerequisite} from 'src/app/api/models/task-prerequisite'; + +export interface TaskPlannerPrerequisitesModalData { + taskDefinition: TaskDefinition; + project: Project; + dependents: TaskPrerequisite[]; +} + +@Component({ + selector: 'f-task-planner-prerequisites-modal', + templateUrl: './task-planner-prerequisites-modal.component.html', + styleUrl: './task-planner-prerequisites-modal.component.scss', +}) +export class TaskPlannerPrerequisitesModalComponent implements OnInit { + @Input() taskDefinition: TaskDefinition; + @Input() project: Project; + @Input() dependents: TaskPrerequisite[]; + + public dataSource = new MatTableDataSource(); + public displayedColumns: string[] = ['task-definition', 'current-status', 'required-status']; + + public get task() { + return this.project?.findTaskForDefinition(this.taskDefinition?.id); + } + + constructor(@Inject(MAT_DIALOG_DATA) public data: TaskPlannerPrerequisitesModalData) {} + + ngOnInit(): void { + this.taskDefinition = this.data.taskDefinition; + this.project = this.data.project; + this.dependents = this.data.dependents; + this.dataSource.data = this.dependents; + } +} diff --git a/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.service.ts b/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.service.ts new file mode 100644 index 0000000000..4f12f4fd1f --- /dev/null +++ b/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.service.ts @@ -0,0 +1,27 @@ +import {Injectable} from '@angular/core'; +import {MatDialog} from '@angular/material/dialog'; +import {Project, TaskDefinition} from 'src/app/api/models/doubtfire-model'; +import { + TaskPlannerPrerequisitesModalComponent, + TaskPlannerPrerequisitesModalData, +} from './task-planner-prerequisites-modal.component'; +import {TaskPrerequisite} from 'src/app/api/models/task-prerequisite'; + +@Injectable({ + providedIn: 'root', +}) +export class TaskPlannerPrerequisitesModalService { + constructor(public dialog: MatDialog) {} + + public show(project: Project, taskDefinition: TaskDefinition, dependents: TaskPrerequisite[]) { + this.dialog.open( + TaskPlannerPrerequisitesModalComponent, + { + data: {taskDefinition, project, dependents}, + width: '100%', + maxWidth: '900px', + panelClass: 'overflow-y-auto', + }, + ); + } +} diff --git a/src/app/projects/states/plan/task-planner/task-planner.component.html b/src/app/projects/states/plan/task-planner/task-planner.component.html new file mode 100644 index 0000000000..025da92aa5 --- /dev/null +++ b/src/app/projects/states/plan/task-planner/task-planner.component.html @@ -0,0 +1,129 @@ +
    +
    + Show Task Dates +
    + @if (unit.allowFlexibleDates) { +
    + + +
    + } +
    + + + + +
    +
    + +
    + {{ item.title }} +
    +
    + +
    +
    +
    + + @if (showDatesColumn) { + + + {{ toDateString(item.start) }} + + + + + + {{ toDateString(item.end) }} + + + + + + {{ item.task.localDeadlineDate() ? toDateString(item.task.localDeadlineDate()) : 'N/A' }} + + + } + + +
    +
    + @if (unsavedChanges(item)) { + change_circle + } + @if (prerequisiteConflict(item)) { + warning + } @else if (isBlockedByPrerequisite(item)) { + warning + } + @if (isPastFeedbackDeadline(item)) { + dangerous + } + @if (isCloseToFeedbackDeadline(item)) { + query_builder + } +
    + +   {{ item.title }} +
    +
    +
    +
    +
    diff --git a/src/app/projects/states/plan/task-planner/task-planner.component.scss b/src/app/projects/states/plan/task-planner/task-planner.component.scss new file mode 100644 index 0000000000..ddf668e57b --- /dev/null +++ b/src/app/projects/states/plan/task-planner/task-planner.component.scss @@ -0,0 +1,18 @@ +:host ::ng-deep .flexible-dates .gantt-links-overlay svg { + z-index: 999 !important; + pointer-events: none; +} + +:host ::ng-deep .flexible-dates .gantt-links-overlay-main { + height: 1px; + overflow: visible !important; + pointer-events: none; +} + +.gantt-bar { + background-color: var(--bar-bg); +} + +.flash { + transition: background-color 500ms ease-in-out; +} diff --git a/src/app/projects/states/plan/task-planner/task-planner.component.ts b/src/app/projects/states/plan/task-planner/task-planner.component.ts new file mode 100644 index 0000000000..097b0e6b92 --- /dev/null +++ b/src/app/projects/states/plan/task-planner/task-planner.component.ts @@ -0,0 +1,544 @@ +import {Component, Input, OnInit, ViewChild} from '@angular/core'; +import {UIRouter} from '@uirouter/core'; +import { + GanttBaselineItem, + GanttDate, + GanttItem, + GanttLink, + GanttLinkType, + GanttViewOptions, + GanttViewType, + NgxGanttComponent, +} from '@worktile/gantt'; +import {Project} from 'src/app/api/models/project'; +import {Task} from 'src/app/api/models/task'; +import {TaskDefinition} from 'src/app/api/models/task-definition'; +import {TaskPrerequisite} from 'src/app/api/models/task-prerequisite'; +import {TaskPrerequisiteService} from 'src/app/api/services/task-prerequisite.service'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {GradeService} from 'src/app/common/services/grade.service'; +import {TaskPlannerPrerequisitesModalService} from './task-planner-prerequisites-modal/task-planner-prerequisites-modal.service'; + +interface TaskGanttItem extends GanttItem { + highlighted?: boolean; + taskDefinition: TaskDefinition; + task: Task; + originalLinks: GanttLink[]; +} + +@Component({ + selector: 'f-task-planner', + templateUrl: './task-planner.component.html', + styleUrl: './task-planner.component.scss', +}) +export class TaskPlannerComponent implements OnInit { + // Show a warning if the task's target end date is within this many days of the feedback deadline + public readonly CLOSE_TO_FEEDBACK_DEADLINE_THRESHOLD = 7; + + @Input() project: Project; + @ViewChild('gantt') ganttComponent: NgxGanttComponent; + + public viewType: GanttViewType = GanttViewType.day; + public viewOptions: GanttViewOptions; + + public allTaskPrerequisites: TaskPrerequisite[]; + public taskPrerequisites: TaskPrerequisite[]; + + public items: TaskGanttItem[] = []; + + // TaskDefinition default dates for reference + public baselineItems: GanttBaselineItem[] = []; + + public animateBackground: boolean = false; + public showDatesColumn: boolean = false; + public overlayLines: boolean = false; + + public get unit() { + return this.project?.unit; + } + + constructor( + private gradeService: GradeService, + private alertService: AlertService, + private confirmationModalService: ConfirmationModalService, + private taskPlannerPrerequisitesModal: TaskPlannerPrerequisitesModalService, + private taskPrerequisiteService: TaskPrerequisiteService, + private router: UIRouter, + ) {} + + public get gradeValues() { + return this.gradeService.gradeValues; + } + + public get gradeAcronyms() { + return this.gradeService.gradeAcronyms; + } + + public gradeString(grade: number) { + return this.gradeService.grades[grade]; + } + + onBarHover(item: TaskGanttItem) { + this.setLinkColors(item, true); + } + + onBarLeave(item: TaskGanttItem) { + this.setLinkColors(item, false); + } + + setLinkColors(item: TaskGanttItem, active: boolean) { + this.overlayLines = active; + + const ganttItem = this.items.find((i) => i.id === item.id); + ganttItem.links.forEach((linkItem) => { + const link = linkItem as GanttLink; + this.toggleLinkOpacity(link, active); + }); + + const prerequisites = this.items.filter((i) => { + const links = i.links; + if (typeof links === 'string') { + return false; + } + + return links.some((l) => typeof l !== 'string' && l.link === item.id); + }); + + prerequisites.forEach((prereq) => { + prereq.links.forEach((linkItem) => { + const link = linkItem as GanttLink; + if (link.link == item.id) { + this.toggleLinkOpacity(link, active); + } + }); + }); + + this.items = [...this.items]; + } + + private toggleLinkOpacity(link: GanttLink, active: boolean) { + const color = link.color as {active: string; default: string}; + + if (!active && !color.default.endsWith('0.1)')) { + // Dim link by lowering alpha + color.default = color.default.slice(0, -2) + '0.1)'; + } else if (active && color.default.endsWith('0.1)')) { + // Restore full opacity + color.default = color.default.slice(0, -4) + '1)'; + } + } + + barClick(item: TaskGanttItem) { + const td = item.taskDefinition; + const prereqs = this.taskPrerequisites.filter((p) => p.prerequisiteId === td.id); + this.taskPlannerPrerequisitesModal.show(this.project, td, prereqs); + } + + private mapPrerequisites() { + for (const prerequisite of this.allTaskPrerequisites) { + prerequisite.taskDefinition = this.unit.taskDefinitions.find( + (td) => td.id === prerequisite.taskDefinitionId, + ); + prerequisite.prerequisite = this.unit.taskDefinitions.find( + (td) => td.id === prerequisite.prerequisiteId, + ); + } + this.allTaskPrerequisites = [...this.allTaskPrerequisites]; + } + + public blockedDependents: Map = new Map(); + + // Check to see if this task is a prerequisite for another task + // If it is, ensure the end date on the task is before the start of its dependent task + prerequisiteConflict(item: TaskGanttItem) { + if (!item.links.length) { + return false; + } + + let isAfterDependentStartDate: boolean = false; + for (const ganttLink of item.links) { + if (typeof ganttLink === 'string') { + continue; + } + + const ganttItem = this.items.find((i) => i.id === ganttLink.link); + if (!ganttItem) { + return false; + } + const diff = this.normalizeDateUTC(item.end) - this.normalizeDateUTC(ganttItem.end); + const color = typeof ganttLink.color === 'string' ? ganttLink.color : ganttLink.color.default; + + if (diff > 0) { + isAfterDependentStartDate = true; + } + + continue; + + if (color === '#0079D8') { + // Ready for feedback + if (diff > 0) { + isAfterDependentStartDate = true; + } + } else if (color === '#31b0d5' || color === '#5BB75B') { + // Discuss or Complete + if (diff >= -7 * 24 * 60 * 60) { + // We need to ensure this task is submitted a week earlier than its dependent so get it in a Discuss state + isAfterDependentStartDate = true; + } + } + } + + return isAfterDependentStartDate; + } + + isBlockedByPrerequisite(item: TaskGanttItem) { + const prerequisites = this.items.filter((i) => { + const links = i.links; + if (typeof links === 'string') { + return false; + } + + return links.some((l) => typeof l !== 'string' && l.link === item.id); + }); + + for (const link of prerequisites) { + if (this.prerequisiteConflict(link)) { + const diff = this.normalizeDateUTC(link.end) - this.normalizeDateUTC(item.end); + if (diff > 0) { + return true; + } + } + } + + return false; + } + + getItemClasses(item: TaskGanttItem): string[] { + const classes: string[] = ['gantt-bar']; + if (this.animateBackground) { + classes.push('flash'); + } + if (item.highlighted) { + classes.push('[--bar-bg:#03c6fc]'); + } else if (this.isPastFeedbackDeadline(item)) { + classes.push('[--bar-bg:#cd3704]', 'text-white'); + } else if (this.isBlockedByPrerequisite(item)) { + classes.push('[--bar-bg:#e88307]', 'text-black'); + } else if (this.isCloseToFeedbackDeadline(item)) { + classes.push('[--bar-bg:#ffc53d]', 'text-black'); + } else { + classes.push('[--bar-bg:#0e467b]', 'text-white'); + } + + return classes; + } + + isPastFeedbackDeadline(item: TaskGanttItem) { + return item.end > item.task.localDeadlineDate().getTime() / 1000; + } + + isCloseToFeedbackDeadline(item: TaskGanttItem) { + if (!this.unit.allowFlexibleDates) { + return false; + } + + const task = item.task; + const diff = + this.normalizeDateUTC(task.localDeadlineDate().getTime() / 1000) - + this.normalizeDateUTC(item.end); + + return diff >= 0 && diff <= this.CLOSE_TO_FEEDBACK_DEADLINE_THRESHOLD * 24 * 60 * 60; + } + + toDateStr = (timestamp: number) => { + const d = new Date(timestamp * 1000); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + }; + + saveTargetDates() { + for (const item of this.items) { + const td = item.taskDefinition; + if (!td) { + continue; + } + if (this.unsavedChanges(item)) { + this.saveTargetDate(item); + } + } + } + + saveTargetDate(item: TaskGanttItem) { + const td = item.taskDefinition; + const task = item.task; + + task.saveTargetDates(this.toDateStr(item.start), this.toDateStr(item.end)).subscribe({ + next: (data) => { + task.targetDueDate = data.targetDueDate; + task.targetStartDate = data.targetStartDate; + item.start = this.normalizeDateUTC(data.targetStartDate.getTime() / 1000); + item.end = this.normalizeDateUTC(data.targetDueDate.getTime() / 1000); + this.items = [...this.items]; + }, + error: (error) => { + this.alertService.error( + `Failed to save target date for ${td.abbreviation}: ${error}`, + 6000, + ); + }, + }); + } + + anyUnsavedChanges() { + return this.items.some((i) => this.unsavedChanges(i)); + } + + confirmSaveTargetDates() { + this.confirmationModalService.show( + 'Save Task Dates?', + `Do you want to save these new target dates for your tasks? You can always reset them to the unit's default later.`, + () => { + this.saveTargetDates(); + }, + ); + } + + confirmResetTargetDates() { + this.confirmationModalService.show( + 'Reset Task Dates?', + `Are you sure you want to reset all target dates to the unit's default? All modified dates will be reset.`, + () => { + this.project.resetTargetDates().subscribe({ + next: (_project) => { + for (const task of this.project.tasks) { + task.targetDueDate = null; + task.targetStartDate = null; + + const item = this.items.find((item) => item.id === task.definition.id.toString()); + + item.start = this.normalizeDateUTC(task.startDate.getTime() / 1000); + item.end = this.normalizeDateUTC(task.localDueDate().getTime() / 1000); + } + this.items = [...this.items]; + }, + error: (error) => { + this.alertService.error(`Failed to reset target dates: ${error}`, 6000); + }, + }); + }, + ); + } + + normalizeDateUTC = (ts: number) => { + const d = new GanttDate(ts * 1000); + // const utc = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 0, 0, 0, 0); + return Math.floor(d.getUnixTime()); + }; + + toDateString(timestamp: number | Date) { + const date = timestamp instanceof Date ? timestamp : new Date(timestamp * 1000); + return date.toLocaleDateString('en-AU', { + month: 'short', + day: 'numeric', + // year: '2-digit', + }); + } + + unsavedChanges(item: TaskGanttItem) { + const task = item.task; + const start = this.normalizeDateUTC(task.startDate.getTime() / 1000); + const end = this.normalizeDateUTC(task.localDueDate().getTime() / 1000); + return start !== this.normalizeDateUTC(item.start) || end !== this.normalizeDateUTC(item.end); + } + + getTooltip(item: TaskGanttItem) { + return `${this.toDateString(item.start)} — ${this.toDateString(item.end)}`; + } + + public get earliestStartDate() { + const earliestTaskStartDate = Math.min( + ...this.taskDefs().map((t) => t.startDate.getTime() / 1000), + ); + return Math.floor(Math.min(this.unit.startDate.getTime() / 1000, earliestTaskStartDate)); + } + + public get latestEndDate() { + const latestTaskEndDate = Math.max(...this.taskDefs().map((t) => t.dueDate.getTime() / 1000)); + return Math.floor(Math.max(this.unit.endDate.getTime() / 1000, latestTaskEndDate)); + } + + ngOnInit(): void { + this.viewOptions = { + datePrecisionUnit: 'day', + start: new GanttDate(this.earliestStartDate), + end: new GanttDate(this.latestEndDate), + dragPreviewDateFormat: 'MMM dd', + }; + + this.unit.getTaskPrerequisites().subscribe({ + next: (prereqs) => { + this.allTaskPrerequisites = prereqs; + this.mapPrerequisites(); + for (const prerequisite of this.allTaskPrerequisites) { + prerequisite.taskDefinition.taskPrerequisitesCache.getOrCreate( + prerequisite.id, + this.taskPrerequisiteService, + prerequisite, + ); + } + + for (const td of this.unit.taskDefinitions) { + const prerequisites = td.taskPrerequisitesCache.currentValues; + const definitions = this.unit.taskDefinitions; + for (const prerequisite of prerequisites) { + prerequisite.taskDefinition = definitions.find( + (td) => td.id === prerequisite.taskDefinitionId, + ); + prerequisite.prerequisite = definitions.find( + (td) => td.id === prerequisite.prerequisiteId, + ); + } + } + + this.refreshItems(); + }, + error: (error) => { + this.alertService.error(`Failed to get task prerequisites: ${error}`, 6000); + }, + }); + } + + refreshItems() { + this.taskPrerequisites = this.allTaskPrerequisites.filter((pre) => + this.taskDefs().find((td) => td.id === pre.taskDefinitionId), + ); + + const taskDefinitions = this.taskDefs(); + this.items = []; + + for (const td of taskDefinitions) { + const task = this.project.findTaskForDefinition(td.id); + + const item: TaskGanttItem = { + id: td.id.toString(), + title: `${td.abbreviation} ${td.name}`, + start: this.normalizeDateUTC(task.startDate.getTime() / 1000), + end: this.normalizeDateUTC(task.localDueDate().getTime() / 1000), + expandable: false, + draggable: this.project.unit.allowFlexibleDates, + // color: this.gradeService.gradeColors[td.targetGrade], + expanded: false, + color: '#3333ff', + taskDefinition: td, + task: task, + // progress: 0.5, + originalLinks: [], + links: this.taskPrerequisites + .filter((p) => p.prerequisiteId === td.id) + // .filter((p) => p.taskDefinitionId === td.id) + .map((p) => { + let color: string; + + switch (p.taskStatus) { + case 'ready_for_feedback': + color = 'rgba(0, 121, 216, 0.1)'; + break; + case 'complete': + color = 'rgba(91, 183, 91, 0.1)'; + break; + case 'discuss': + color = 'rgba(49, 176, 213, 0.1)'; + break; + case 'demonstrate': + color = 'rgba(49, 176, 213, 0.1)'; + break; + default: + color = 'gray'; + } + const link: GanttLink = { + type: GanttLinkType.fs, + link: p.taskDefinitionId.toString(), + // link: p.prerequisiteId.toString(), + color: { + default: color, + active: color, + }, + }; + + return link; + }), + }; + + // if ( + // item.links.length && + // (this.isCloseToFeedbackDeadline(item) || this.isPastFeedbackDeadline(item)) + // ) { + // const task = this.project.findTaskForDefinition(td.id); + + // item.start = this.normalizeDateUTC(task.startDate.getTime() / 1000); + // item.end = this.normalizeDateUTC(task.localDueDate().getTime() / 1000); + + // // If the task defaults are still invalid, reset them to the task definition default + // if (this.isCloseToFeedbackDeadline(item) || this.isPastFeedbackDeadline(item)) { + // item.start = this.normalizeDateUTC(td.startDate.getTime() / 1000); + // item.end = this.normalizeDateUTC(td.localDueDate().getTime() / 1000); + // } + // } + + const originalItem = {...item}; + item.originalLinks = [...(originalItem.links as GanttLink[])]; + + this.items.push(item); + this.items = [...this.items]; + + // Create baseline item + const baselineItem = {...item}; + baselineItem.start = this.normalizeDateUTC(td.startDate.getTime() / 1000); + baselineItem.end = this.normalizeDateUTC(td.targetDate.getTime() / 1000); + this.baselineItems.push(baselineItem); + this.baselineItems = [...this.baselineItems]; + + // if (this.unsavedChanges(item)) { + // this.saveTargetDate(item); + // } + } + + this.ganttComponent.scrollToToday(); + + if (this.router.globals.params.taskDef) { + const taskItem = this.items.find((item) => item.id === this.router.globals.params.taskDef); + if (taskItem) { + this.ganttComponent.scrollToDate(taskItem.start); + taskItem.highlighted = true; + this.animateBackground = true; + + setTimeout(() => { + const el = document.querySelector(`[data-gantt-id="${taskItem.id}"]`) as HTMLElement; + + el?.scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'nearest', + }); + }); + setTimeout(() => (taskItem.highlighted = false), 1000); + setTimeout(() => (this.animateBackground = false), 2000); + } + this.router.stateService.go( + this.router.globals.current.name, + {taskDef: null}, + {location: 'replace', notify: false, reload: false}, + ); + } + } + + public taskDefs(): TaskDefinition[] { + if (!this.project || !this.project.unit.taskDefinitions) { + return []; + } + + return this.project.unit.taskDefinitions.filter((taskDef) => { + return taskDef.targetGrade <= this.project.targetGrade; + }); + } +} diff --git a/src/styles.scss b/src/styles.scss index 7eec75221c..8b8db3480d 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,5 +1,6 @@ // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; +@use '@worktile/gantt/styles/index'; @include mat.core(); diff --git a/tsconfig.json b/tsconfig.json index c1f136d7b7..f062ffbb0f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,8 @@ "typeRoots": ["node_modules/@types"], "lib": ["es2020", "dom", "ES2021.String"], "useDefineForClassFields": false, - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "skipLibCheck": true }, "angularCompilerOptions": { "strictTemplates": false From 6b616fc6de2f58197988c7999005a9abaa79864b Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:45:22 +1100 Subject: [PATCH 664/776] chore(release): 10.0.0-69 --- CHANGELOG.md | 10 ++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bf63acef3..51a2cd51da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-69](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-68...v10.0.0-69) (2026-01-05) + + +### Features + +* attention required task status ([#1061](https://github.com/b0ink/doubtfire-deploy/issues/1061)) ([15365ed](https://github.com/b0ink/doubtfire-deploy/commit/15365ed4f42af790af9a9ae24dae42f041dff1df)) +* display number of stuff notes in tutor discussion ([2674d4e](https://github.com/b0ink/doubtfire-deploy/commit/2674d4e5ade26e5420b87518f4cd9da909ec7246)) +* overseer pipeline ([#1064](https://github.com/b0ink/doubtfire-deploy/issues/1064)) ([7cb8fc7](https://github.com/b0ink/doubtfire-deploy/commit/7cb8fc7471f0402c562fb8a0b87df672ceb22ea3)) +* project task planner gantt chart ([#1051](https://github.com/b0ink/doubtfire-deploy/issues/1051)) ([4f60792](https://github.com/b0ink/doubtfire-deploy/commit/4f607926e3ac305316a05607fda7f7bff7faec00)) + ## [10.0.0-68](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-67...v10.0.0-68) (2025-12-08) diff --git a/package-lock.json b/package-lock.json index 53f1d09af5..ae33639815 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-68", + "version": "10.0.0-69", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-68", + "version": "10.0.0-69", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 4b9234d2b7..33f6f2b521 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-68", + "version": "10.0.0-69", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 6c6c7f9cd1297d0671231c80fbad77b900be5cba Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 5 Jan 2026 16:44:33 +1100 Subject: [PATCH 665/776] chore: allow no expected output file --- .../task-definition-overseer.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html index fb78832932..cde5ed4e7e 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html @@ -194,6 +194,7 @@ Expected Output File + (none) @for (file of taskDefinition.overseerResourceFiles; track file) { {{ file }} } From 7ff9fa3380b58320ed1819f04ac8a258c72ada39 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Tue, 6 Jan 2026 08:08:24 +1100 Subject: [PATCH 666/776] chore: render correct error --- src/app/home/states/lti-dashboard/lti-dashboard.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/home/states/lti-dashboard/lti-dashboard.component.ts b/src/app/home/states/lti-dashboard/lti-dashboard.component.ts index db081c7375..0c0ad93d57 100644 --- a/src/app/home/states/lti-dashboard/lti-dashboard.component.ts +++ b/src/app/home/states/lti-dashboard/lti-dashboard.component.ts @@ -78,7 +78,7 @@ export class LtiDashboardComponent implements AfterViewInit { this.isLoading = false; }, error: (error) => { - this.alertsService.error(error.error, 6000); + this.alertsService.error(error.error || error, 6000); this.isLoading = false; }, }); From a6bdd22ff864e72c679fae1844345963447fdf08 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Tue, 6 Jan 2026 09:02:27 +1100 Subject: [PATCH 667/776] fix: skip query for unsaved task definition --- .../task-definition-discussion-prompts.component.ts | 5 ++++- .../task-definition-prerequisites.component.ts | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.ts index 149682d55d..e22072d433 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.ts @@ -80,12 +80,15 @@ export class TaskDefinitionDiscussionPromptsComponent private fetchDiscussionPrompts() { const taskDefinition = this.taskDefinition; + if (!taskDefinition.id) { + return; + } this.discussionPromptService.loadDiscussionPrompts(null, taskDefinition).subscribe({ next: (data) => { this.dataSource.data = data; }, error: (error) => { - this.alertService.error(`Failed to load discussion promnpts: ${error}`); + this.alertService.error(`Failed to load discussion prompts: ${error}`, 6000); }, }); } diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-prerequisites/task-definition-prerequisites.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-prerequisites/task-definition-prerequisites.component.ts index 2528ffcd6d..201e8a35ab 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-prerequisites/task-definition-prerequisites.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-prerequisites/task-definition-prerequisites.component.ts @@ -101,6 +101,9 @@ export class TaskDefinitionPrerequisitesComponent implements OnInit, OnChanges { private fetchTaskPrerequisites() { const taskDefinition = this.taskDefinition; + if (!taskDefinition.id) { + return; + } this.taskPrerequisiteService .query( { @@ -127,7 +130,10 @@ export class TaskDefinitionPrerequisitesComponent implements OnInit, OnChanges { this.filterTaskDefs(this.searchCtrl.value ?? ''); }, error: (error) => { - console.error(error); + this.alertService.error( + `Failed to fetch prerequisites for task definition: ${error}`, + 6000, + ); }, }); } From 9317a5477bb27ed59d9c2e29af9e4538c253816c Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Tue, 6 Jan 2026 09:13:21 +1100 Subject: [PATCH 668/776] chore: remove required fields --- .../task-definition-overseer.component.html | 45 ++----------------- 1 file changed, 3 insertions(+), 42 deletions(-) diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html index cde5ed4e7e..87e44b0754 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.html @@ -113,35 +113,14 @@ cdkDragDisabled (click)="selectStep(newOverseerStep)" > - {{ newOverseerStep.sortOrder }}. {{ newOverseerStep.name || 'Untitled Step' }}
    } - -
    @if (selectedOverseerStep) { -
    @@ -193,7 +172,7 @@
    Expected Output File - + (none) @for (file of taskDefinition.overseerResourceFiles; track file) { {{ file }} @@ -220,9 +199,6 @@
    - }
    @@ -231,7 +207,6 @@ Language @for (language of getLanguages; track language) { @@ -244,11 +219,6 @@
    - -
    Status on Success - + No Change @for (status of statusKeys; track status) { {{ statusName(status) }} @@ -318,7 +288,7 @@ Status on Fail - + No Change @for (status of statusKeys; track status) { {{ statusName(status) }} @@ -361,15 +331,6 @@ and a default will message will be used instead. -
    Date: Tue, 6 Jan 2026 09:26:42 +1100 Subject: [PATCH 669/776] chore(release): 10.0.0-70 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51a2cd51da..6121dce31b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-70](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-69...v10.0.0-70) (2026-01-05) + + +### Bug Fixes + +* skip query for unsaved task definition ([a6bdd22](https://github.com/b0ink/doubtfire-deploy/commit/a6bdd22ff864e72c679fae1844345963447fdf08)) + ## [10.0.0-69](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-68...v10.0.0-69) (2026-01-05) diff --git a/package-lock.json b/package-lock.json index ae33639815..2eb60f3bef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.0-69", + "version": "10.0.0-70", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.0-69", + "version": "10.0.0-70", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 33f6f2b521..6b4ad8bb97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.0-69", + "version": "10.0.0-70", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 6dcfe952f0415fb1327b738137f817a0fcfa0488 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:06:34 +1100 Subject: [PATCH 670/776] feat: download staff notes csv (#1062) * feat: download staff notes csv * chore: restore imports --- src/app/api/models/unit.ts | 11 +++++++++ src/app/doubtfire-angular.module.ts | 2 ++ src/app/doubtfire-angularjs.module.ts | 6 +++++ .../download-staff-notes.component.html | 3 +++ .../download-staff-notes.component.scss | 0 .../download-staff-notes.component.ts | 24 +++++++++++++++++++ .../states/portfolios/portfolios.tpl.html | 1 + 7 files changed, 47 insertions(+) create mode 100644 src/app/units/states/portfolios/download-staff-notes/download-staff-notes.component.html create mode 100644 src/app/units/states/portfolios/download-staff-notes/download-staff-notes.component.scss create mode 100644 src/app/units/states/portfolios/download-staff-notes/download-staff-notes.component.ts diff --git a/src/app/api/models/unit.ts b/src/app/api/models/unit.ts index 452bbceb98..0415a8bda2 100644 --- a/src/app/api/models/unit.ts +++ b/src/app/api/models/unit.ts @@ -723,6 +723,17 @@ export class Unit extends Entity { ); } + public get staffNotesCsvDownloadUrl(): string { + return `${AppInjector.get(DoubtfireConstants).API_URL}/csv/units/${this.id}/staff_notes`; + } + + public downloadStaffNotesCsv(): void { + AppInjector.get(FileDownloaderService).downloadFile( + `${AppInjector.get(DoubtfireConstants).API_URL}/csv/units/${this.id}/staff_notes`, + `${this.name}-StaffNotes.csv`, + ); + } + public get taskDefinitionsPrerequisitesUrl(): string { return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.id}/task_prerequisites`; } diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 33c8db369b..16e6c15557 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -305,6 +305,7 @@ import {DiscussionPromptService} from './api/services/discussion-prompt.service' import {DiscussionPromptsComponent} from './projects/states/discussion-prompts/discussion-prompts.component'; import {TaskDefinitionDiscussionPromptsComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component'; import {DiscussionPromptsViewComponent} from './projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component'; +import {DownloadStaffNotesComponent} from './units/states/portfolios/download-staff-notes/download-staff-notes.component'; import {TaskPlannerComponent} from './projects/states/plan/task-planner/task-planner.component'; import {TaskPlannerPrerequisitesModalComponent} from './projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component'; import {TaskPlannerPrerequisitesModalService} from './projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.service'; @@ -497,6 +498,7 @@ const GANTT_CHART_CONFIG = { TaskDefinitionDiscussionPromptsComponent, DiscussionPromptsViewComponent, TaskPlannerComponent, + DownloadStaffNotesComponent, TaskPlannerCardComponent, TaskPlannerPrerequisitesModalComponent, TaskOverseerReportComponent, diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 40e8930490..1fb4868e3c 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -233,6 +233,7 @@ import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/dir import {PortfolioIncludedTasksComponent} from './projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component'; import {TaskSimilarityViewComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component'; import {UploadGradesComponent} from './units/states/portfolios/upload-grades/upload-grades.component'; +import {DownloadStaffNotesComponent} from './units/states/portfolios/download-staff-notes/download-staff-notes.component'; import {ProjectPlanComponent} from './projects/states/plan/project-plan.component'; import {TaskPlannerComponent} from './projects/states/plan/task-planner/task-planner.component'; import {TaskPlannerCardComponent} from './projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component'; @@ -580,6 +581,11 @@ DoubtfireAngularJSModule.directive( downgradeComponent({component: UploadGradesComponent}), ); +DoubtfireAngularJSModule.directive( + 'fDownloadStaffNotes', + downgradeComponent({component: DownloadStaffNotesComponent}), +); + DoubtfireAngularJSModule.directive( 'fProjectPlan', downgradeComponent({component: ProjectPlanComponent}), diff --git a/src/app/units/states/portfolios/download-staff-notes/download-staff-notes.component.html b/src/app/units/states/portfolios/download-staff-notes/download-staff-notes.component.html new file mode 100644 index 0000000000..529708f97c --- /dev/null +++ b/src/app/units/states/portfolios/download-staff-notes/download-staff-notes.component.html @@ -0,0 +1,3 @@ + diff --git a/src/app/units/states/portfolios/download-staff-notes/download-staff-notes.component.scss b/src/app/units/states/portfolios/download-staff-notes/download-staff-notes.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/units/states/portfolios/download-staff-notes/download-staff-notes.component.ts b/src/app/units/states/portfolios/download-staff-notes/download-staff-notes.component.ts new file mode 100644 index 0000000000..eb3e1501bd --- /dev/null +++ b/src/app/units/states/portfolios/download-staff-notes/download-staff-notes.component.ts @@ -0,0 +1,24 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {Unit} from 'src/app/api/models/unit'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-download-staff-notes', + templateUrl: 'download-staff-notes.component.html', + styleUrl: 'download-staff-notes.component.scss', +}) +export class DownloadStaffNotesComponent implements OnInit { + @Input() unit: Unit; + + constructor(private alertService: AlertService) {} + + public ngOnInit(): void { + if (!this.unit) { + return console.error(`Invalid unit`); + } + } + + public downloadStaffNotesCsv() { + this.unit.downloadStaffNotesCsv(); + } +} diff --git a/src/app/units/states/portfolios/portfolios.tpl.html b/src/app/units/states/portfolios/portfolios.tpl.html index 34d00d5d7f..eaff240d73 100644 --- a/src/app/units/states/portfolios/portfolios.tpl.html +++ b/src/app/units/states/portfolios/portfolios.tpl.html @@ -236,6 +236,7 @@

    Mark Portfolios

    rotate="false" >
    +