From ec7524aaae040794de76521363c2d885a0425db0 Mon Sep 17 00:00:00 2001 From: Bahnschrift Date: Fri, 31 May 2024 12:56:24 +1000 Subject: [PATCH 001/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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/155] 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 b67106589b71660bdfe01151deb68d3092b9091f Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Wed, 24 Jul 2024 21:57:49 +1000 Subject: [PATCH 110/155] fix: ensure groups can be locked --- src/app/groups/group-selector/group-selector.coffee | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/groups/group-selector/group-selector.coffee b/src/app/groups/group-selector/group-selector.coffee index 0c79db4f74..f6e0a56745 100644 --- a/src/app/groups/group-selector/group-selector.coffee +++ b/src/app/groups/group-selector/group-selector.coffee @@ -195,11 +195,12 @@ angular.module('doubtfire.groups.group-selector', []) # Toggle lockable group $scope.toggleLocked = (group) -> group.locked = !group.locked - $scope.unit.updateGroup(group, - (success) -> + newGroupService.update(group).subscribe({ + next: (success) -> group.locked = success.locked alertService.success( "Group updated", 2000) - ) + error: () -> alertService.error( "Failed to lock group. #{message}", 6000) + }) # Watch selected group set changes listeners.push $scope.$on 'UnitGroupSetEditor/SelectedGroupSetChanged', (evt, args) -> From e00479271fbd70e6ac75bbee63fac559a94c5490 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Wed, 24 Jul 2024 21:58:31 +1000 Subject: [PATCH 111/155] chore(release): 8.0.21 --- 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 a5fe9f894e..70004dd00f 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.21](https://github.com/macite/doubtfire-deploy/compare/v8.0.20...v8.0.21) (2024-07-24) + + +### Bug Fixes + +* ensure groups can be locked ([b671065](https://github.com/macite/doubtfire-deploy/commit/b67106589b71660bdfe01151deb68d3092b9091f)) + ### [8.0.20](https://github.com/macite/doubtfire-deploy/compare/v8.0.19...v8.0.20) (2024-07-15) diff --git a/package-lock.json b/package-lock.json index d176f2345c..86fdc3b04b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.20", + "version": "8.0.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.20", + "version": "8.0.21", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 762969722e..265a5a30bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.20", + "version": "8.0.21", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From dca03a41f502947136e301e51242570169826dd9 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 26 Jul 2024 15:01:21 +1000 Subject: [PATCH 112/155] fix: correct duplicate shortcut registration --- src/app/units/states/tasks/inbox/inbox.component.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/app/units/states/tasks/inbox/inbox.component.ts b/src/app/units/states/tasks/inbox/inbox.component.ts index 41716a3e40..568df3744d 100644 --- a/src/app/units/states/tasks/inbox/inbox.component.ts +++ b/src/app/units/states/tasks/inbox/inbox.component.ts @@ -25,7 +25,7 @@ import {UserService} from 'src/app/api/services/user.service'; templateUrl: './inbox.component.html', styleUrls: ['./inbox.component.scss'], }) -export class InboxComponent implements OnInit, AfterViewInit { +export class InboxComponent implements OnInit { @Input() unit: Unit; @Input() unitRole: UnitRole; @Input() taskData: {selectedTask: Task; any}; @@ -67,11 +67,11 @@ export class InboxComponent implements OnInit, AfterViewInit { this.taskSelected = task != null; }); } - ngAfterViewInit(): void { - console.log('ngAfterViewInit'); - const markers = ['Admin', 'Convenor', 'Tutor', 'Student']; - if (markers.includes(this.userService.currentUser?.role)) { + ngOnInit(): void { + const registeredHotkeys = this.hotkeys.getHotkeys().map((hotkey) => hotkey.keys); + + if (!registeredHotkeys.includes('shift.?')) { this.hotkeys.registerHelpModal(() => { const ref = this.dialog.open(HotkeysHelpComponent, { // width: '250px', @@ -80,9 +80,7 @@ export class InboxComponent implements OnInit, AfterViewInit { ref.componentInstance.dismiss.subscribe(() => ref.close()); }); } - } - ngOnInit(): void { // this.hotkeys // .addShortcut({ // keys: 'control.c', From 00d10e1328fff75140525c7a80d3d8be2e39b049 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 26 Jul 2024 17:20:34 +1000 Subject: [PATCH 113/155] chore(release): 8.0.22 --- 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 70004dd00f..e6f408ac0b 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.22](https://github.com/macite/doubtfire-deploy/compare/v8.0.21...v8.0.22) (2024-07-26) + + +### Bug Fixes + +* correct duplicate shortcut registration ([dca03a4](https://github.com/macite/doubtfire-deploy/commit/dca03a41f502947136e301e51242570169826dd9)) + ### [8.0.21](https://github.com/macite/doubtfire-deploy/compare/v8.0.20...v8.0.21) (2024-07-24) diff --git a/package-lock.json b/package-lock.json index 86fdc3b04b..610086b7e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.21", + "version": "8.0.22", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.21", + "version": "8.0.22", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 265a5a30bd..5959c70f0e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.21", + "version": "8.0.22", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 16ac9c8bc488607025ee95dbb4b41134a5346b00 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 2 Aug 2024 16:45:04 +1000 Subject: [PATCH 114/155] feat: allow pdf download and switch to native viewer --- .../file-downloader.service.ts | 34 ++++-- .../pdf-viewer/pdf-viewer.component.html | 101 +++++++++++------- .../pdf-viewer/pdf-viewer.component.scss | 24 +++++ .../common/pdf-viewer/pdf-viewer.component.ts | 23 ++-- 4 files changed, 125 insertions(+), 57 deletions(-) diff --git a/src/app/common/file-downloader/file-downloader.service.ts b/src/app/common/file-downloader/file-downloader.service.ts index 681e4bde8e..57b8fff783 100644 --- a/src/app/common/file-downloader/file-downloader.service.ts +++ b/src/app/common/file-downloader/file-downloader.service.ts @@ -125,26 +125,40 @@ export class FileDownloaderService { window.URL.revokeObjectURL(url); } + /** + * Download or save a blob to a file. This will trigger the user to "download" + * the blob, with the suggested filename. + * + * @param blobUrl the url of the blob to download/save to file + * @param filename the name of the file + */ + public downloadBlobToFile(blobUrl: string, filename: string): void { + const downloadLink = document.createElement('a'); + downloadLink.href = blobUrl; + downloadLink.target = '_blank'; + downloadLink.setAttribute('download', filename); + document.body.appendChild(downloadLink); + + downloadLink.click(); + downloadLink.parentNode.removeChild(downloadLink); + } + public downloadFile(url: string, defaultFilename: string) { this.downloadBlob( url, (resourceUrl: string, response: HttpResponse) => { - const downloadLink = document.createElement('a'); - downloadLink.href = resourceUrl; - downloadLink.target = '_blank'; const filenameRegex = /filename[^;=\n]*=((['']).*?\2|[^;\n]*)/; + const matches = filenameRegex.exec(response.headers.get('Content-Disposition')); + let filename: string; + if (matches != null && matches[1]) { - const filename = matches[1].replace(/['']/g, ''); - downloadLink.setAttribute('download', filename); + filename = matches[1].replace(/['']/g, ''); } else { - downloadLink.setAttribute('download', defaultFilename); + filename = defaultFilename; } - document.body.appendChild(downloadLink); - - downloadLink.click(); - downloadLink.parentNode.removeChild(downloadLink); + this.downloadBlobToFile(resourceUrl, filename); }, (error) => { this.alerts.error(`Error downloading file - ${error}`); diff --git a/src/app/common/pdf-viewer/pdf-viewer.component.html b/src/app/common/pdf-viewer/pdf-viewer.component.html index 87dc97c996..b060046125 100644 --- a/src/app/common/pdf-viewer/pdf-viewer.component.html +++ b/src/app/common/pdf-viewer/pdf-viewer.component.html @@ -1,46 +1,67 @@
-
-
- - search - +
+
+ + +
+
+ + + search + - - - - + + + + +
+ @if (pdfBlobUrl) { - + @if (useNativePdfViewer) { + + } @else { + + } + } @else { + }
diff --git a/src/app/common/pdf-viewer/pdf-viewer.component.scss b/src/app/common/pdf-viewer/pdf-viewer.component.scss index 6942d03e8b..da9fc20f12 100644 --- a/src/app/common/pdf-viewer/pdf-viewer.component.scss +++ b/src/app/common/pdf-viewer/pdf-viewer.component.scss @@ -12,6 +12,30 @@ align-items: center; } +#actionBar { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; +} + +object { + height: 100%; + min-width: 100%; + width: 100%; +} + +#pdfViewActions { + padding-top: 1rem; + padding-left: 1rem; + margin-right: auto; + + button { + margin-bottom: 28px; + } +} + #pdfActions { padding-top: 1rem; padding-right: 1rem; diff --git a/src/app/common/pdf-viewer/pdf-viewer.component.ts b/src/app/common/pdf-viewer/pdf-viewer.component.ts index 5cd893b5a9..8f21f8e47c 100644 --- a/src/app/common/pdf-viewer/pdf-viewer.component.ts +++ b/src/app/common/pdf-viewer/pdf-viewer.component.ts @@ -7,7 +7,6 @@ import { SimpleChanges, OnChanges, ViewChild, - OnInit, AfterViewInit, } from '@angular/core'; import {PdfViewerComponent} from 'ng2-pdf-viewer'; @@ -22,6 +21,8 @@ import {AlertService} from '../services/alert.service'; export class fPdfViewerComponent implements OnDestroy, OnChanges, AfterViewInit { private _pdfUrl: string; public pdfBlobUrl: string; + public useNativePdfViewer = false; + @Input() pdfUrl: string; @ViewChild(PdfViewerComponent) private pdfComponent: PdfViewerComponent; pdfSearchString: string; @@ -41,8 +42,7 @@ export class fPdfViewerComponent implements OnDestroy, OnChanges, AfterViewInit } ngAfterViewInit(): void { - console.log("pdfUrl"); - console.log(this.pdfUrl); + this.useNativePdfViewer = localStorage.getItem('useNativePdfViewer') === 'true'; } ngOnChanges(changes: SimpleChanges): void { @@ -75,24 +75,33 @@ export class fPdfViewerComponent implements OnDestroy, OnChanges, AfterViewInit }); } - zoomIn() { + public zoomIn() { if (this.zoomValue < 2.5) { this.zoomValue += 0.1; } } - zoomOut() { + public zoomOut() { if (this.zoomValue > 0.5) { this.zoomValue -= 0.1; } } + public downloadPdf() { + this.fileDownloader.downloadBlobToFile(this.pdfBlobUrl, 'displayed-pdf.pdf'); + } + + public toggleNativePdfViewer() { + this.useNativePdfViewer = !this.useNativePdfViewer; + localStorage.setItem('useNativePdfViewer', this.useNativePdfViewer.toString()); + } + private downloadBlob(downloadUrl: string): void { this.fileDownloader.downloadBlob( downloadUrl, - (url: string, response: HttpResponse) => { + (url: string, _response: HttpResponse) => { this.pdfBlobUrl = url; }, - (error: any) => { + (error: unknown) => { this.alerts.error(`Error downloading PDF. ${error}`, 6000); }, ); From c04e9fe3a824e316a632b3f7b53b08887e57c831 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 2 Aug 2024 16:54:30 +1000 Subject: [PATCH 115/155] fix: ensure overseer menu only shows when active for task --- .../directives/task-dashboard/task-dashboard.coffee | 2 +- .../directives/task-dashboard/task-dashboard.tpl.html | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) 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 62292ad919..b36e94e005 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 @@ -10,7 +10,7 @@ angular.module('doubtfire.projects.states.dashboard.directives.task-dashboard', showFooter: '@?' showSubmission: '@?' controller: ($scope, $stateParams, listenerService, newTaskService, DoubtfireConstants, TaskAssessmentModal, fileDownloaderService) -> - $scope.overseerEnabled = DoubtfireConstants.IsOverseerEnabled + # $scope.overseerEnabled = DoubtfireConstants.IsOverseerEnabled $scope.overseerEnabled = () -> DoubtfireConstants.IsOverseerEnabled.value && $scope.task?.overseerEnabled 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..84da532b5b 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 @@ -3,11 +3,10 @@
{{task.definition.name}} {{task.definition.name}} - From 2965b680772b138656bf58ff8bd697e0d68e5a12 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 2 Aug 2024 16:54:41 +1000 Subject: [PATCH 116/155] chore(release): 8.0.23 --- 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 e6f408ac0b..1839482f34 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. +### [8.0.23](https://github.com/macite/doubtfire-deploy/compare/v8.0.22...v8.0.23) (2024-08-02) + + +### Features + +* allow pdf download and switch to native viewer ([16ac9c8](https://github.com/macite/doubtfire-deploy/commit/16ac9c8bc488607025ee95dbb4b41134a5346b00)) + + +### Bug Fixes + +* ensure overseer menu only shows when active for task ([c04e9fe](https://github.com/macite/doubtfire-deploy/commit/c04e9fe3a824e316a632b3f7b53b08887e57c831)) + ### [8.0.22](https://github.com/macite/doubtfire-deploy/compare/v8.0.21...v8.0.22) (2024-07-26) diff --git a/package-lock.json b/package-lock.json index 610086b7e2..391ed15dac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.22", + "version": "8.0.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.22", + "version": "8.0.23", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 5959c70f0e..e90b3ba27c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.22", + "version": "8.0.23", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From bb78f23bfdd99521f9907697e1763e2f576cd0a4 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 8 Aug 2024 15:05:58 +1000 Subject: [PATCH 117/155] chore: switch to port 4200 for api with angular proxy --- docker-compose.yml | 6 +++--- package.json | 2 +- proxy.conf.json | 6 ++++++ src/app/config/constants/apiURL.ts | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 proxy.conf.json diff --git a/docker-compose.yml b/docker-compose.yml index b97ff4e52c..9019dbcc4b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: RAILS_ENV: 'development' DF_STUDENT_WORK_DIR: /student-work - DF_INSTITUTION_HOST: http://localhost:3000 + DF_INSTITUTION_HOST: http://localhost:4200 DF_INSTITUTION_PRODUCT_NAME: OnTrack DF_SECRET_KEY_BASE: test-secret-key-test-secret-key! @@ -24,8 +24,8 @@ services: # Authentication method - can set to AAF or ldap DF_AUTH_METHOD: database DF_AAF_ISSUER_URL: https://rapid.test.aaf.edu.au - DF_AAF_AUDIENCE_URL: http://localhost:3000 - DF_AAF_CALLBACK_URL: http://localhost:3000/api/auth/jwt + DF_AAF_AUDIENCE_URL: http://localhost:4200 + DF_AAF_CALLBACK_URL: http://localhost:4200/api/auth/jwt DF_AAF_IDENTITY_PROVIDER_URL: https://signon-uat.deakin.edu.au/idp/shibboleth DF_AAF_UNIQUE_URL: https://rapid.test.aaf.edu.au/jwt/authnrequest/research/Ag4EJJhjf0zXHqlKvKZEbg DF_AAF_AUTH_SIGNOUT_URL: https://sync-uat.deakin.edu.au/auth/logout diff --git a/package.json b/package.json index e90b3ba27c..691c8a62fb 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build:angular17": "ng build", "lint:fix": "ng lint --fix", "lint": "ng lint", - "serve:angular17": "export NODE_OPTIONS=--max_old_space_size=4096 && ng serve --configuration $NODE_ENV", + "serve:angular17": "export NODE_OPTIONS=--max_old_space_size=4096 && ng serve --configuration $NODE_ENV --proxy-config proxy.conf.json", "start": "npm-run-all -l -s build:angular1 -p watch:angular1 serve:angular17", "watch:angular1": "grunt delta", "deploy:build2api": "ng build --delete-output-path=true --optimization=true --configuration production --output-path dist", diff --git a/proxy.conf.json b/proxy.conf.json new file mode 100644 index 0000000000..63dd627501 --- /dev/null +++ b/proxy.conf.json @@ -0,0 +1,6 @@ +{ + "/api": { + "target": "http://localhost:3000", + "secure": false + } +} diff --git a/src/app/config/constants/apiURL.ts b/src/app/config/constants/apiURL.ts index 737aad40bd..a4d39f05f2 100644 --- a/src/app/config/constants/apiURL.ts +++ b/src/app/config/constants/apiURL.ts @@ -1,4 +1,4 @@ let API_URL: string; -API_URL = `${window.location.protocol}//${window.location.hostname}:3000/api`; +API_URL = `${window.location.protocol}//${window.location.hostname}:4200/api`; export default API_URL; From c8fc702bde6f1c448fc585042e5265625552bae8 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 8 Aug 2024 20:11:39 +1000 Subject: [PATCH 118/155] fix: remove saved scorm token and always get --- src/app/api/models/user/user.ts | 1 - src/app/api/services/authentication.service.ts | 14 ++------------ .../common/scorm-player/scorm-player.component.ts | 11 +++-------- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/src/app/api/models/user/user.ts b/src/app/api/models/user/user.ts index 552066cfd1..10357a1898 100644 --- a/src/app/api/models/user/user.ts +++ b/src/app/api/models/user/user.ts @@ -22,7 +22,6 @@ 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 fe2b8d0b1c..a83637b236 100644 --- a/src/app/api/services/authentication.service.ts +++ b/src/app/api/services/authentication.service.ts @@ -192,20 +192,10 @@ export class AuthenticationService { } } - public getScormToken(): Observable { + 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, - ); + return response['scorm_auth_token']; }), ); } diff --git a/src/app/common/scorm-player/scorm-player.component.ts b/src/app/common/scorm-player/scorm-player.component.ts index a7de380195..f63a64acfc 100644 --- a/src/app/common/scorm-player/scorm-player.component.ts +++ b/src/app/common/scorm-player/scorm-player.component.ts @@ -49,15 +49,10 @@ export class ScormPlayerComponent implements OnInit { ngOnInit(): void { this.globalState.setView(ViewType.OTHER); this.globalState.hideHeader(); - - if (this.userService.currentUser.scormAuthenticationToken) { - this.setupScorm(); - } else { - this.authService.getScormToken().subscribe(() => this.setupScorm()); - } + this.authService.getScormToken().subscribe((value: string) => this.setupScorm(value)); } - setupScorm(): void { + private setupScorm(token: string): void { this.scormAdapter.mode = this.mode; if (this.mode === 'normal') { this.scormAdapter.projectId = this.projectId; @@ -78,7 +73,7 @@ export class ScormPlayerComponent implements OnInit { }; this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl( - `${AppInjector.get(DoubtfireConstants).API_URL}/scorm/${this.taskDefId}/${this.userService.currentUser.username}/${this.userService.currentUser.scormAuthenticationToken}/index.html`, + `${AppInjector.get(DoubtfireConstants).API_URL}/scorm/${this.taskDefId}/${this.userService.currentUser.username}/${token}/index.html`, ); } From 6ae42954162996acf145cef211981153381ff49d Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 9 Aug 2024 11:54:52 +1000 Subject: [PATCH 119/155] fix: ensure scorm frame loads when src is available --- src/app/common/scorm-player/scorm-player.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/common/scorm-player/scorm-player.component.html b/src/app/common/scorm-player/scorm-player.component.html index 4855cf4d2b..990a9ef4a7 100644 --- a/src/app/common/scorm-player/scorm-player.component.html +++ b/src/app/common/scorm-player/scorm-player.component.html @@ -1 +1 @@ - + From 7ffbea9f56c393d44571c18f30735e090701b554 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 9 Aug 2024 12:48:56 +1000 Subject: [PATCH 120/155] feat: add ability to preview scorm test --- src/app/api/models/scorm-player-context.ts | 2 +- src/app/api/models/task-definition.ts | 7 ++++ src/app/api/models/task.ts | 8 ++++ src/app/api/models/test-attempt.ts | 9 ++++ src/app/api/services/scorm-adapter.service.ts | 2 +- src/app/api/services/task-comment.service.ts | 3 ++ .../scorm-player/scorm-player.component.ts | 41 ++++++++++++------- src/app/doubtfire.states.ts | 39 +++++++++++++++--- .../task-scorm-card.component.ts | 10 +---- .../scorm-comment/scorm-comment.component.ts | 5 +-- .../task-definition-scorm.component.html | 40 +++++++++--------- .../task-definition-scorm.component.ts | 9 +++- 12 files changed, 120 insertions(+), 55 deletions(-) diff --git a/src/app/api/models/scorm-player-context.ts b/src/app/api/models/scorm-player-context.ts index 195bc3cd4c..e8eed9b12e 100644 --- a/src/app/api/models/scorm-player-context.ts +++ b/src/app/api/models/scorm-player-context.ts @@ -33,7 +33,7 @@ const CMIErrorCodes: DataModelError = { }; export class ScormPlayerContext { - mode: 'browse' | 'normal' | 'review'; + mode: 'browse' | 'normal' | 'review' | 'preview'; state: DataModelState; private _errorCode: number; diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index ff679f140e..b848686768 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -165,6 +165,13 @@ export class TaskDefinition extends Entity { }`; } + /** + * Open the SCORM test in a new tab - using preview mode. + */ + public previewScormTest(): void { + window.open(`#/task_def_id/${this.id}/preview-scorm`, '_blank'); + } + public get targetGradeText(): string { return Grade.GRADES[this.targetGrade]; } diff --git a/src/app/api/models/task.ts b/src/app/api/models/task.ts index 0643f99732..4283e3d25f 100644 --- a/src/app/api/models/task.ts +++ b/src/app/api/models/task.ts @@ -531,6 +531,14 @@ export class Task extends Entity { return false; } + /** + * Launch the SCORM player for this task in a new window. + */ + public launchScormPlayer(): void { + const url = `#/projects/${this.project.id}/task_def_id/${this.taskDefId}/scorm-player/normal`; + window.open(url, '_blank'); + } + public get isReadyForUpload(): boolean { return !this.scormEnabled || this.definition.scormBypassTest || this.scormPassed; } diff --git a/src/app/api/models/test-attempt.ts b/src/app/api/models/test-attempt.ts index 646b9ed64f..4497028a75 100644 --- a/src/app/api/models/test-attempt.ts +++ b/src/app/api/models/test-attempt.ts @@ -16,4 +16,13 @@ export class TestAttempt extends Entity { super(); this.task = task; } + + /** + * Open a test attempt window in review mode + */ + public review() { + const url = `#/projects/${this.task.project.id}/task_def_id/${this.task.taskDefId}/scorm-player/review/${this.id}`; + + window.open(url, '_blank'); + } } diff --git a/src/app/api/services/scorm-adapter.service.ts b/src/app/api/services/scorm-adapter.service.ts index 702fc72f40..0f1af9889c 100644 --- a/src/app/api/services/scorm-adapter.service.ts +++ b/src/app/api/services/scorm-adapter.service.ts @@ -25,7 +25,7 @@ export class ScormAdapterService { this.context.taskDefId = taskDefId; } - set mode(mode: 'browse' | 'normal' | 'review') { + set mode(mode: 'browse' | 'normal' | 'review' | 'preview') { this.context.mode = mode; } diff --git a/src/app/api/services/task-comment.service.ts b/src/app/api/services/task-comment.service.ts index a33389efc3..4f178ce771 100644 --- a/src/app/api/services/task-comment.service.ts +++ b/src/app/api/services/task-comment.service.ts @@ -101,6 +101,9 @@ export class TaskCommentService extends CachedEntityService { data[key].id, testAttemptService, data[key], + { + constructorParams: comment.task, + }, ); return testAttempt; }, diff --git a/src/app/common/scorm-player/scorm-player.component.ts b/src/app/common/scorm-player/scorm-player.component.ts index f63a64acfc..d20b76f00f 100644 --- a/src/app/common/scorm-player/scorm-player.component.ts +++ b/src/app/common/scorm-player/scorm-player.component.ts @@ -12,7 +12,16 @@ import {GlobalStateService, ViewType} from 'src/app/projects/states/index/global declare global { interface Window { - API_1484_11: any; + API_1484_11: { + Initialize: () => void; + Terminate: () => void; + GetValue: (element: string) => string; + SetValue: (element: string, value: string) => void; + Commit: () => void; + GetLastError: () => string; + GetErrorString: (errorCode: string) => string; + GetDiagnostic: (errorCode: string) => string; + }; } } @@ -31,7 +40,7 @@ export class ScormPlayerComponent implements OnInit { taskDefId: number; @Input() - mode: 'browse' | 'normal' | 'review'; + mode: 'browse' | 'normal' | 'review' | 'preview'; @Input() testAttemptId: number; @@ -61,16 +70,20 @@ export class ScormPlayerComponent implements OnInit { this.scormAdapter.testAttemptId = this.testAttemptId; } - window.API_1484_11 = { - 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), - }; + if (this.mode !== 'preview') { + window.API_1484_11 = { + 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), + }; + } else { + window.API_1484_11 = undefined; + } this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl( `${AppInjector.get(DoubtfireConstants).API_URL}/scorm/${this.taskDefId}/${this.userService.currentUser.username}/${token}/index.html`, @@ -78,7 +91,7 @@ export class ScormPlayerComponent implements OnInit { } @HostListener('window:beforeunload', ['$event']) - beforeUnload($event: any): void { + beforeUnload(_event: Event): void { if (this.scormAdapter.state == 'Initialized') { // console.log('SCORM player closing during an initialized session, commiting DataModel'); this.scormAdapter.Commit(); @@ -86,7 +99,7 @@ export class ScormPlayerComponent implements OnInit { } @HostListener('window:unload', ['$event']) - onUnload($event: any): void { + onUnload(_event: Event): void { this.scormAdapter.destroy(); } } diff --git a/src/app/doubtfire.states.ts b/src/app/doubtfire.states.ts index ce4dd84210..7f95af1f04 100644 --- a/src/app/doubtfire.states.ts +++ b/src/app/doubtfire.states.ts @@ -299,10 +299,10 @@ const ScormPlayerNormalState: NgHybridStateDeclaration = { name: 'scorm-player-normal', url: '/projects/:project_id/task_def_id/:task_definition_id/scorm-player/normal', resolve: { - projectId: function ($stateParams) { + projectId: function ($stateParams: {project_id: number}) { return $stateParams.project_id; }, - taskDefId: function ($stateParams) { + taskDefId: function ($stateParams: {task_definition_id: number}) { return $stateParams.task_definition_id; }, mode: function () { @@ -320,10 +320,16 @@ const ScormPlayerNormalState: NgHybridStateDeclaration = { }, }; -const ScormPlayerReviewState: NgHybridStateDeclaration = { - name: 'scorm-player-review', - url: '/task_def_id/:task_definition_id/scorm-player/review/:test_attempt_id', +/** + * Define the SCORM Player state. + */ +const ScormPlayerStudentReviewState: NgHybridStateDeclaration = { + name: 'scorm-player-student-review', + url: '/projects/:project_id/task_def_id/:task_definition_id/scorm-player/review/:test_attempt_id', resolve: { + projectId: function ($stateParams) { + return $stateParams.project_id; + }, taskDefId: function ($stateParams) { return $stateParams.task_definition_id; }, @@ -345,6 +351,28 @@ const ScormPlayerReviewState: NgHybridStateDeclaration = { }, }; +const ScormPlayerReviewState: NgHybridStateDeclaration = { + name: 'scorm-preview', + url: '/task_def_id/:task_definition_id/preview-scorm', + resolve: { + taskDefId: function ($stateParams) { + return $stateParams.task_definition_id; + }, + mode: function () { + return 'preview'; + }, + }, + views: { + main: { + component: ScormPlayerComponent, + }, + }, + data: { + pageTitle: 'Preview Scorm Test', + roleWhitelist: ['Tutor', 'Convenor', 'Admin'], + }, +}; + /** * Export the list of states we have created in angular */ @@ -362,4 +390,5 @@ export const doubtfireStates = [ AdministerUnits, ScormPlayerNormalState, ScormPlayerReviewState, + ScormPlayerStudentReviewState, ]; 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 9860d47741..377b89c2f4 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 @@ -44,17 +44,11 @@ export class TaskScormCardComponent implements OnChanges { } launchScormPlayer(): void { - window.open( - `#/projects/${this.task.project.id}/task_def_id/${this.task.taskDefId}/scorm-player/normal`, - '_blank', - ); + this.task.launchScormPlayer(); } reviewLatestCompletedAttempt(): void { - window.open( - `#/task_def_id/${this.task.taskDefId}/scorm-player/review/${this.task.latestCompletedTestAttempt.id}`, - '_blank', - ); + this.task.latestCompletedTestAttempt.review(); } requestExtraAttempt(): 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 d26b914f7e..869b79aa51 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 @@ -28,10 +28,7 @@ export class ScormCommentComponent { } reviewScormTest() { - window.open( - `#/task_def_id/${this.task.taskDefId}/scorm-player/review/${this.comment.testAttempt.id}`, - '_blank', - ); + this.comment.testAttempt.review(); } passScormAttempt() { 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 c2b89f9d7b..fb02d119a2 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 @@ -4,24 +4,25 @@ @if (taskDefinition.scormEnabled) { -
- - @if (taskDefinition.hasScormData) { -
- - -
- } -
+ + @if (taskDefinition.hasScormData) { +
+ + + +
+ }
@@ -33,14 +34,13 @@
- Attempt limit + Limit to this number of attempts - 0 is unlimited
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 6a8708d25d..8dee18a297 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 @@ -20,12 +20,17 @@ export class TaskDefinitionScormComponent { private taskDefinitionService: TaskDefinitionService, ) {} - public attemptLimitControl = new FormControl('', [Validators.max(100), Validators.min(0)]); - public get unit(): Unit { return this.taskDefinition?.unit; } + /** + * Open the SCORM test in a new tab - using preview mode. + */ + public previewScormTest() { + this.taskDefinition.previewScormTest(); + } + public downloadScormData() { this.fileDownloaderService.downloadFile( this.taskDefinition.getScormDataUrl(true), From 1e056037562753dd8628b1df83f913ca06af2dff Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 9 Aug 2024 13:04:07 +1000 Subject: [PATCH 121/155] chore(release): 8.0.24 --- CHANGELOG.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 4 +-- package.json | 2 +- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1839482f34..830c69392a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,68 @@ 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.24](https://github.com/doubtfire-lms/doubtfire-deploy/compare/v8.0.23...v8.0.24) (2024-08-09) + + +### Features + +* add ability to preview scorm test ([7ffbea9](https://github.com/doubtfire-lms/doubtfire-deploy/commit/7ffbea9f56c393d44571c18f30735e090701b554)) +* add new Numbas Feature ([6f1ac4e](https://github.com/doubtfire-lms/doubtfire-deploy/commit/6f1ac4e4c1a78115c426838bce350bce286e44f0)) +* add new Numbas Feature ([8eba913](https://github.com/doubtfire-lms/doubtfire-deploy/commit/8eba913ec4462e1c252a8a698b4b6de67c4ec25f)) +* add numbas component ([7c1734b](https://github.com/doubtfire-lms/doubtfire-deploy/commit/7c1734b137ac369b3b605b38749c845df38b9a78)) +* add Numbas config options to task def service keys ([ff28e48](https://github.com/doubtfire-lms/doubtfire-deploy/commit/ff28e4802b86dc22be339cc196ef82ade67934f7)) +* add Numbas test attempt model ([097df79](https://github.com/doubtfire-lms/doubtfire-deploy/commit/097df7960d34a8c905ee495b8c6c8b8843e43b36)) +* add Numbas test section on ready for feedback ([e61295c](https://github.com/doubtfire-lms/doubtfire-deploy/commit/e61295c50006dbc89cfc0639bb3c68a09a3cda9d)) +* add Numbas test upload section and reorder editor sections ([edbd536](https://github.com/doubtfire-lms/doubtfire-deploy/commit/edbd536e73ace8b6b4cced1481c809fd36524dce)) +* add Numbas upload component and related functions to task-definition model ([4ecaee8](https://github.com/doubtfire-lms/doubtfire-deploy/commit/4ecaee8ad1c0caed9a5d848066a173000989f79b)) +* add test attempt service ([a576f48](https://github.com/doubtfire-lms/doubtfire-deploy/commit/a576f484bc434728eab9632c022bccf1ed26cb01)) +* add test attempt service and minor numbas related changes ([0652b56](https://github.com/doubtfire-lms/doubtfire-deploy/commit/0652b56f69eb850c2dfb43be54d07beb4c4eb469)) +* added numbas-lms service code ([471d344](https://github.com/doubtfire-lms/doubtfire-deploy/commit/471d34486f2582e95aac84469187f087277cc079)) +* allow changing scorm review config and add minor UI changes ([fc023af](https://github.com/doubtfire-lms/doubtfire-deploy/commit/fc023af462656e3557e2970f211fa4d59ee1e3d5)) +* change Numbas time delay config to enable incremental delays ([0afa719](https://github.com/doubtfire-lms/doubtfire-deploy/commit/0afa7197293c90a154c1716db91ecadae3677d53)) +* disable attempt button if passed and add button to review latest attempt in card ([703563c](https://github.com/doubtfire-lms/doubtfire-deploy/commit/703563c86253c60ad30d451ee2d8e0fa7ebbfabb)) +* display numbas task comments ([48a31da](https://github.com/doubtfire-lms/doubtfire-deploy/commit/48a31da1442f1e14f497c614053d9002c9f2631b)) +* enable reviewing, passing, deleting test attempts and add test attempt model and service ([561b924](https://github.com/doubtfire-lms/doubtfire-deploy/commit/561b9241c2f44fd69d3f09c656a025f514bbaf3a)) +* enable students to request extra scorm attempt ([d904ffd](https://github.com/doubtfire-lms/doubtfire-deploy/commit/d904ffd6fd674f6e61894d517f5aa147d1db6d29)) +* get unique token for scorm asset retrieval ([8fe2f9e](https://github.com/doubtfire-lms/doubtfire-deploy/commit/8fe2f9e9ca60c02e55c24d0f2bb3eb6dbea8217c)) +* implement numbas test data upload in task definition service ([2c7dab5](https://github.com/doubtfire-lms/doubtfire-deploy/commit/2c7dab555fc9a226b980a8dae96f5738bf517b1b)) +* insert Numbas test rules options in the task editor ([7e52ad5](https://github.com/doubtfire-lms/doubtfire-deploy/commit/7e52ad5e5759291d7d61805070ec482eb49c90be)) +* numbas-test-numbas-service ([cee13b7](https://github.com/doubtfire-lms/doubtfire-deploy/commit/cee13b727b35f804fc16070885f0e57c422c9982)) +* prevent uploading files until scorm passed ([ec86e4e](https://github.com/doubtfire-lms/doubtfire-deploy/commit/ec86e4eca8e9c50ebdaf54c34c302da88dd44b31)) +* show banner based on scorm success status ([db16172](https://github.com/doubtfire-lms/doubtfire-deploy/commit/db161721fab1359694c44d8df7237cd01892938b)) +* show launch button on ready for feedback if Numbas test is enabled for the task ([17af5b7](https://github.com/doubtfire-lms/doubtfire-deploy/commit/17af5b7fe9e1fbc8e5f18e2fece6e029b017408c)) +* use confirmation modal when passing or deleting test attempts ([3fb25bb](https://github.com/doubtfire-lms/doubtfire-deploy/commit/3fb25bb373acab837a3ef787620dd654bfd6803f)) + + +### Bug Fixes + +* add accepted Numbas file types ([bcaa8af](https://github.com/doubtfire-lms/doubtfire-deploy/commit/bcaa8af150aaf68b5cf5fb07ad9cb72037212d5d)) +* add auth headers to scorm adapter xhr requests ([97e1ea1](https://github.com/doubtfire-lms/doubtfire-deploy/commit/97e1ea187b769a51ebb66b82d76f21193bb29386)) +* adjusted edit profile accidental change ([316abc7](https://github.com/doubtfire-lms/doubtfire-deploy/commit/316abc775eeba6ca846c7c742bc88bec61d05a5e)) +* change success status descriptions ([20da042](https://github.com/doubtfire-lms/doubtfire-deploy/commit/20da0423450fe3b9b8006660bf6f5e06670f520c)) +* delete comment as well as test attempt ([b6887e8](https://github.com/doubtfire-lms/doubtfire-deploy/commit/b6887e85b7a06b166519924dd682ef57650a4e32)) +* disable launch scorm test button if user is staff ([9bc48b0](https://github.com/doubtfire-lms/doubtfire-deploy/commit/9bc48b03905c0a839d78ea13be9817ca73ba54cf)) +* ensure counters are incremented after object creation ([2b1dcfc](https://github.com/doubtfire-lms/doubtfire-deploy/commit/2b1dcfc717eb770dd623c62ac99d8d961d1c3124)) +* ensure datamodel is updated on termination ([2ac487f](https://github.com/doubtfire-lms/doubtfire-deploy/commit/2ac487f13c3743f2b5b805521964bdcbca7cdc15)) +* ensure scorm frame loads when src is available ([6ae4295](https://github.com/doubtfire-lms/doubtfire-deploy/commit/6ae42954162996acf145cef211981153381ff49d)) +* hide config options if numbas test is disabled ([0b15b1c](https://github.com/doubtfire-lms/doubtfire-deploy/commit/0b15b1c38b8f09a30f53bb42dccd1971109bce01)) +* indicate task def has scorm data when zip is uploaded ([4b983ba](https://github.com/doubtfire-lms/doubtfire-deploy/commit/4b983bae1f522013f750b8c36d5e1a52624abbbc)) +* initialise SCORM API wrapper before iframe loads ([5d0606c](https://github.com/doubtfire-lms/doubtfire-deploy/commit/5d0606c56eaeb929d5873cae7038ce729581512c)) +* integrate Numbas services well with the existing system ([d47b8ed](https://github.com/doubtfire-lms/doubtfire-deploy/commit/d47b8edc745660801984346c4fa67d6bb371f4cb)) +* remove attempt number field ([f0ff40b](https://github.com/doubtfire-lms/doubtfire-deploy/commit/f0ff40bfd7e86b904adae94bf8cbe5d56371e011)) +* remove saved scorm token and always get ([c8fc702](https://github.com/doubtfire-lms/doubtfire-deploy/commit/c8fc702bde6f1c448fc585042e5265625552bae8)) +* retrieve test attempt data correctly ([2810ce6](https://github.com/doubtfire-lms/doubtfire-deploy/commit/2810ce65d423a137600d621a98bf27dbc221921c)) +* send task id with numbas completed attempt data ([e46214d](https://github.com/doubtfire-lms/doubtfire-deploy/commit/e46214dd59d86014bb04d1c31f3a3bf21240e32b)) +* show correct attempts left and allow tutor to review attempt always ([58c24c3](https://github.com/doubtfire-lms/doubtfire-deploy/commit/58c24c3a6760af168a918bc099e755f475eac5f0)) +* show correct Numbas test from the task def with all assets loaded ([bee0a0b](https://github.com/doubtfire-lms/doubtfire-deploy/commit/bee0a0bb1eb905c964caf7888419effe70553520)) +* show delete and download buttons in editor when Numbas test exists ([821feb9](https://github.com/doubtfire-lms/doubtfire-deploy/commit/821feb93a8a40d095ef4f50adedbfd9216278fa9)) +* show Numbas button component and modify iframe request ([c9c2fbe](https://github.com/doubtfire-lms/doubtfire-deploy/commit/c9c2fbe10db156512946355f3cabfe54461f0eba)) +* show Numbas iframe on top of other elements ([f53befd](https://github.com/doubtfire-lms/doubtfire-deploy/commit/f53befd82b52bba24bc8c593a9266c11f9155d32)) +* show previously configured Numbas attempt limit ([56e1a5d](https://github.com/doubtfire-lms/doubtfire-deploy/commit/56e1a5de44ee275f9544b77909b7472f1020c2c3)) +* update numbas api path ([3df59dc](https://github.com/doubtfire-lms/doubtfire-deploy/commit/3df59dcc58f021ef16d81938d1d05473818bc75e)) +* use modal for Numbas and enable authentication ([64b1bfb](https://github.com/doubtfire-lms/doubtfire-deploy/commit/64b1bfb2918993e58a6659948d9621fc7d0b8ba4)) +* use nullish coalescing when retrieving data from the datamodel ([226d919](https://github.com/doubtfire-lms/doubtfire-deploy/commit/226d9193251fb675c92bec8265508477857a4ec9)) + ### [8.0.23](https://github.com/macite/doubtfire-deploy/compare/v8.0.22...v8.0.23) (2024-08-02) diff --git a/package-lock.json b/package-lock.json index 391ed15dac..89ded24609 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.23", + "version": "8.0.24", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.23", + "version": "8.0.24", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 691c8a62fb..ed0887c53a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.23", + "version": "8.0.24", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From a0c1185bf4af3acb8f58c97cc27021c952861603 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 9 Aug 2024 14:15:19 +1000 Subject: [PATCH 122/155] fix: switch to control shift alt shift fails to be registered on macos... --- .../staff-task-list.component.ts | 16 ++-- .../states/tasks/inbox/inbox.component.ts | 74 +++++++++++-------- 2 files changed, 54 insertions(+), 36 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 5e427de56e..db6049a027 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 @@ -122,27 +122,28 @@ export class StaffTaskListComponent implements OnInit, OnChanges, OnDestroy { this.refreshData(); } } + ngOnDestroy(): void { - this.hotkeys.removeShortcuts('alt.shift.arrowdown'); - this.hotkeys.removeShortcuts('alt.shift.arrowup'); + this.hotkeys.removeShortcuts('control.shift.arrowdown'); + this.hotkeys.removeShortcuts('control.shift.arrowup'); } ngOnInit(): void { const registeredHotkeys = this.hotkeys.getHotkeys().map((hotkey) => hotkey.keys); - if (!registeredHotkeys.includes('alt.shift..arrowdown')) { + if (!registeredHotkeys.includes('control.shift.arrowdown')) { this.hotkeys .addShortcut({ - keys: 'alt.shift.arrowdown', + keys: 'control.shift.arrowdown', description: 'Select next task', }) .subscribe(() => this.nextTask()); } - if (!registeredHotkeys.includes('alt.shift.arrowup')) { + if (!registeredHotkeys.includes('control.shift.arrowup')) { this.hotkeys .addShortcut({ - keys: 'alt.shift.arrowup', + keys: 'control.shift.arrowup', description: 'Select previous task', }) .subscribe(() => this.previousTask()); @@ -385,6 +386,9 @@ export class StaffTaskListComponent implements OnInit, OnChanges, OnDestroy { } nextTask(): void { + if (!this.filteredTasks) { + return; + } const currentTaskIndex = this.filteredTasks.findIndex((task) => this.isSelectedTask(task)); if (currentTaskIndex >= this.filteredTasks.length) { return; diff --git a/src/app/units/states/tasks/inbox/inbox.component.ts b/src/app/units/states/tasks/inbox/inbox.component.ts index ce178918da..d09d725176 100644 --- a/src/app/units/states/tasks/inbox/inbox.component.ts +++ b/src/app/units/states/tasks/inbox/inbox.component.ts @@ -1,6 +1,5 @@ import {CdkDragEnd, CdkDragStart, CdkDragMove} from '@angular/cdk/drag-drop'; import { - AfterViewInit, Component, ElementRef, Input, @@ -26,7 +25,7 @@ import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; templateUrl: './inbox.component.html', styleUrls: ['./inbox.component.scss'], }) -export class InboxComponent implements OnInit { +export class InboxComponent implements OnInit, OnDestroy { @Input() unit: Unit; @Input() unitRole: UnitRole; @Input() taskData: {selectedTask: Task; any}; @@ -78,38 +77,48 @@ export class InboxComponent implements OnInit { const ref = this.dialog.open(HotkeysHelpComponent, { // width: '250px', }); - ref.componentInstance.title = `${this.constants.ExternalName.value} Marking Shortcuts`; + ref.componentInstance.title = `${this.constants.ExternalName.value} Feedback Shortcuts`; ref.componentInstance.dismiss.subscribe(() => ref.close()); }); } - this.hotkeys - .addShortcut({ - keys: 'alt.shift.r', - description: 'Mark selected task as redo', - }) - .subscribe(() => this.selectedTask.selectedTask?.updateTaskStatus('redo')); - - this.hotkeys - .addShortcut({ - 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')); + if (!registeredHotkeys.includes('control.shift.r')) { + this.hotkeys + .addShortcut({ + keys: 'control.shift.r', + description: 'Mark selected task as redo', + }) + .subscribe(() => this.selectedTask.selectedTask?.updateTaskStatus('redo')); + } + + if (!registeredHotkeys.includes('control.shift.f')) { + this.hotkeys + .addShortcut({ + keys: 'control.shift.f', + description: 'Mark selected task as fix', + }) + .subscribe(() => this.selectedTask.selectedTask?.updateTaskStatus('fix_and_resubmit')); + } + + if (!registeredHotkeys.includes('altleft.shift.c')) { + this.hotkeys + .addShortcut({ + keys: 'control.Shift.c', + description: 'Mark selected task as complete', + }) + .subscribe(() => + this.selectedTask.selectedTask?.updateTaskStatus('complete') + ); + } + + if (!registeredHotkeys.includes('control.shift.d')) { + this.hotkeys + .addShortcut({ + keys: 'control.shift.d', + description: 'Mark selected task as discuss', + }) + .subscribe(() => this.selectedTask.selectedTask?.updateTaskStatus('discuss')); + } this.dragMoveAudited$ = this.dragMove$.pipe( withLatestFrom(this.inboxStartSize$), @@ -142,6 +151,11 @@ export class InboxComponent implements OnInit { window.dispatchEvent(new Event('resize')); } + ngOnDestroy(): void { + this.hotkeys.removeShortcuts('control.shift.arrowdown'); + this.hotkeys.removeShortcuts('control.shift.arrowup'); + } + startedDragging(event: CdkDragStart, div: HTMLDivElement) { event.source.element.nativeElement.classList.add('hovering'); const w = div.getBoundingClientRect().width; From 8a2ce3bfe32d99be1bc0667ca1aba55d36f987d2 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 9 Aug 2024 16:13:53 +1000 Subject: [PATCH 123/155] fix: adjust shortcuts and unregister on inbox destroy --- .../units/states/tasks/inbox/inbox.component.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/app/units/states/tasks/inbox/inbox.component.ts b/src/app/units/states/tasks/inbox/inbox.component.ts index d09d725176..7e73fcccb1 100644 --- a/src/app/units/states/tasks/inbox/inbox.component.ts +++ b/src/app/units/states/tasks/inbox/inbox.component.ts @@ -82,15 +82,6 @@ export class InboxComponent implements OnInit, OnDestroy { }); } - if (!registeredHotkeys.includes('control.shift.r')) { - this.hotkeys - .addShortcut({ - keys: 'control.shift.r', - description: 'Mark selected task as redo', - }) - .subscribe(() => this.selectedTask.selectedTask?.updateTaskStatus('redo')); - } - if (!registeredHotkeys.includes('control.shift.f')) { this.hotkeys .addShortcut({ @@ -100,7 +91,7 @@ export class InboxComponent implements OnInit, OnDestroy { .subscribe(() => this.selectedTask.selectedTask?.updateTaskStatus('fix_and_resubmit')); } - if (!registeredHotkeys.includes('altleft.shift.c')) { + if (!registeredHotkeys.includes('control.shift.c')) { this.hotkeys .addShortcut({ keys: 'control.Shift.c', @@ -152,8 +143,10 @@ export class InboxComponent implements OnInit, OnDestroy { } ngOnDestroy(): void { - this.hotkeys.removeShortcuts('control.shift.arrowdown'); - this.hotkeys.removeShortcuts('control.shift.arrowup'); + this.hotkeys.removeShortcuts('control.shift.d'); + this.hotkeys.removeShortcuts('control.shift.f'); + this.hotkeys.removeShortcuts('control.shift.c'); + this.hotkeys.removeShortcuts('shift.?'); } startedDragging(event: CdkDragStart, div: HTMLDivElement) { From fe94f0440a2c1ce457413536481cd2ffc1717247 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 9 Aug 2024 16:22:46 +1000 Subject: [PATCH 124/155] chore(release): 8.0.25 --- 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 830c69392a..4f765d1e4a 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.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)) +* marking shortcuts no longer conflict with common browser shortcuts ([ec7524a](https://github.com/macite/doubtfire-deploy/commit/ec7524aaae040794de76521363c2d885a0425db0)) +* switch to control shift ([a0c1185](https://github.com/macite/doubtfire-deploy/commit/a0c1185bf4af3acb8f58c97cc27021c952861603)) + ### [8.0.24](https://github.com/doubtfire-lms/doubtfire-deploy/compare/v8.0.23...v8.0.24) (2024-08-09) diff --git a/package-lock.json b/package-lock.json index 89ded24609..a7032b1481 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.24", + "version": "8.0.25", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.24", + "version": "8.0.25", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index ed0887c53a..031c7b1a2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.24", + "version": "8.0.25", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 64e41f4aedcf519a2786d7fe4929900f86731896 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 9 Aug 2024 23:06:22 +1000 Subject: [PATCH 125/155] fix: register service names in state resolve functions --- src/app/doubtfire.states.ts | 55 ++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/src/app/doubtfire.states.ts b/src/app/doubtfire.states.ts index 7f95af1f04..86b9f2b4d8 100644 --- a/src/app/doubtfire.states.ts +++ b/src/app/doubtfire.states.ts @@ -270,7 +270,6 @@ const AdministerUnits: NgHybridStateDeclaration = { }, }; - const ViewAllUnits: NgHybridStateDeclaration = { name: 'view-all-units', url: '/view-all-units', @@ -299,12 +298,18 @@ const ScormPlayerNormalState: NgHybridStateDeclaration = { name: 'scorm-player-normal', url: '/projects/:project_id/task_def_id/:task_definition_id/scorm-player/normal', resolve: { - projectId: function ($stateParams: {project_id: number}) { - return $stateParams.project_id; - }, - taskDefId: function ($stateParams: {task_definition_id: number}) { - return $stateParams.task_definition_id; - }, + projectId: [ + '$stateParams', + function ($stateParams: {project_id: number}) { + return $stateParams.project_id; + } + ], + taskDefId: [ + '$stateParams', + function ($stateParams: {task_definition_id: number}) { + return $stateParams.task_definition_id; + } + ], mode: function () { return 'normal'; }, @@ -327,15 +332,24 @@ const ScormPlayerStudentReviewState: NgHybridStateDeclaration = { name: 'scorm-player-student-review', url: '/projects/:project_id/task_def_id/:task_definition_id/scorm-player/review/:test_attempt_id', resolve: { - projectId: function ($stateParams) { - return $stateParams.project_id; - }, - taskDefId: function ($stateParams) { - return $stateParams.task_definition_id; - }, - testAttemptId: function ($stateParams) { - return $stateParams.test_attempt_id; - }, + projectId: [ + '$stateParams', + function ($stateParams) { + return $stateParams.project_id; + } + ], + taskDefId: [ + '$stateParams', + function ($stateParams) { + return $stateParams.task_definition_id; + } + ], + testAttemptId: [ + '$stateParams', + function ($stateParams) { + return $stateParams.test_attempt_id; + } + ], mode: function () { return 'review'; }, @@ -355,9 +369,12 @@ const ScormPlayerReviewState: NgHybridStateDeclaration = { name: 'scorm-preview', url: '/task_def_id/:task_definition_id/preview-scorm', resolve: { - taskDefId: function ($stateParams) { - return $stateParams.task_definition_id; - }, + taskDefId: [ + '$stateParams', + function ($stateParams) { + return $stateParams.task_definition_id; + } + ], mode: function () { return 'preview'; }, From 349730ea9d4998ed2d87f7ec6c88daae6b336a2e Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 9 Aug 2024 23:06:41 +1000 Subject: [PATCH 126/155] chore(release): 8.0.26 --- 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 4f765d1e4a..c0f61e572b 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.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) diff --git a/package-lock.json b/package-lock.json index a7032b1481..895dc9d7c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.25", + "version": "8.0.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.25", + "version": "8.0.26", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index 031c7b1a2a..bcfbae280b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.25", + "version": "8.0.26", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 1ec175419ab3ef80f22ac8a6384f6ed40a17c217 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Sat, 10 Aug 2024 00:27:07 +1000 Subject: [PATCH 127/155] fix: encode . in the username in scorm player --- src/app/common/scorm-player/scorm-player.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/common/scorm-player/scorm-player.component.ts b/src/app/common/scorm-player/scorm-player.component.ts index d20b76f00f..6c2bdadd20 100644 --- a/src/app/common/scorm-player/scorm-player.component.ts +++ b/src/app/common/scorm-player/scorm-player.component.ts @@ -85,8 +85,11 @@ export class ScormPlayerComponent implements OnInit { window.API_1484_11 = undefined; } + // Encode . as %2e to avoid issue with grape treating the username as a file extension + const username = this.userService.currentUser.username.replaceAll('.', '%2e'); + this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl( - `${AppInjector.get(DoubtfireConstants).API_URL}/scorm/${this.taskDefId}/${this.userService.currentUser.username}/${token}/index.html`, + `${AppInjector.get(DoubtfireConstants).API_URL}/scorm/${this.taskDefId}/${username}/${token}/index.html`, ); } From 57359b34686de49bc36b4f97ae35cea085b6e9cc Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Sat, 10 Aug 2024 00:27:25 +1000 Subject: [PATCH 128/155] chore(release): 8.0.27 --- 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 c0f61e572b..4dc8e1bc57 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.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) diff --git a/package-lock.json b/package-lock.json index 895dc9d7c8..55ece4bb8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.26", + "version": "8.0.27", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.26", + "version": "8.0.27", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index bcfbae280b..d39e2bd7f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.26", + "version": "8.0.27", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 151ca04ca8da9059890d7846293bf34ae67f05c5 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Wed, 4 Sep 2024 16:20:24 +1000 Subject: [PATCH 129/155] feat: add support for the view language --- 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 9f8dac5ff9..c492dac115 100644 --- a/src/app/common/file-uploader/file-uploader.coffee +++ b/src/app/common/file-uploader/file-uploader.coffee @@ -68,7 +68,7 @@ angular.module('doubtfire.common.file-uploader', ["ngFileUpload"]) extensions: ['pas', 'cpp', 'c', 'cs', 'csv', 'h', 'hpp', 'java', 'py', 'js', 'html', 'coffee', 'rb', 'css', 'scss', 'yaml', 'yml', 'xml', 'json', 'ts', 'r', 'rmd', 'rnw', 'rhtml', 'rpres', 'tex', 'vb', 'sql', 'txt', 'md', 'jack', 'hack', 'asm', 'hdl', 'tst', 'out', 'cmp', 'vm', 'sh', 'bat', - 'dat', 'ipynb', 'pml'] + 'dat', 'ipynb', 'pml', 'vue'] icon: 'fa-file-code-o' name: 'code' image: From b4b36541943232f7df3ee4888bf4ca46c4c04018 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Wed, 4 Sep 2024 16:20:24 +1000 Subject: [PATCH 130/155] feat: add support for the view language --- 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 9f8dac5ff9..c492dac115 100644 --- a/src/app/common/file-uploader/file-uploader.coffee +++ b/src/app/common/file-uploader/file-uploader.coffee @@ -68,7 +68,7 @@ angular.module('doubtfire.common.file-uploader', ["ngFileUpload"]) extensions: ['pas', 'cpp', 'c', 'cs', 'csv', 'h', 'hpp', 'java', 'py', 'js', 'html', 'coffee', 'rb', 'css', 'scss', 'yaml', 'yml', 'xml', 'json', 'ts', 'r', 'rmd', 'rnw', 'rhtml', 'rpres', 'tex', 'vb', 'sql', 'txt', 'md', 'jack', 'hack', 'asm', 'hdl', 'tst', 'out', 'cmp', 'vm', 'sh', 'bat', - 'dat', 'ipynb', 'pml'] + 'dat', 'ipynb', 'pml', 'vue'] icon: 'fa-file-code-o' name: 'code' image: From 3f1a75207674b42e32c4ed7ddb1056b0dbab3407 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 5 Sep 2024 15:37:30 +1000 Subject: [PATCH 131/155] chore(release): 8.0.28 --- 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 1839482f34..67a49d5d89 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.28](https://github.com/macite/doubtfire-deploy/compare/v8.0.23...v8.0.28) (2024-09-05) + + +### Features + +* add support for the view language ([b4b3654](https://github.com/macite/doubtfire-deploy/commit/b4b36541943232f7df3ee4888bf4ca46c4c04018)) + ### [8.0.23](https://github.com/macite/doubtfire-deploy/compare/v8.0.22...v8.0.23) (2024-08-02) diff --git a/package-lock.json b/package-lock.json index 391ed15dac..a02d52eb80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "8.0.23", + "version": "8.0.28", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "8.0.23", + "version": "8.0.28", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", diff --git a/package.json b/package.json index e90b3ba27c..15405c0da0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "8.0.23", + "version": "8.0.28", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", From 7984b393752381e0a219dbbdafc0e7b44c3fcdb3 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 18 Oct 2024 13:59:51 +1100 Subject: [PATCH 132/155] fix: correct task scroll into view --- src/app/api/models/task.ts | 2 +- .../directives/student-task-list/student-task-list.coffee | 2 +- .../directives/staff-task-list/staff-task-list.component.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/api/models/task.ts b/src/app/api/models/task.ts index 2aa167adfd..ee5d521d2b 100644 --- a/src/app/api/models/task.ts +++ b/src/app/api/models/task.ts @@ -396,7 +396,7 @@ export class Task extends Entity { public taskKeyToIdString(): string { const key = this.taskKey(); - return `task-key-${key.studentId}-${key.taskDefAbbr}`.replace(/[.#]/g, '-'); + return `task-key-${key.studentId}-${key.taskDefAbbr}`.replace(/[.# ]/g, '-'); } public get similaritiesDetected(): boolean { diff --git a/src/app/projects/states/dashboard/directives/student-task-list/student-task-list.coffee b/src/app/projects/states/dashboard/directives/student-task-list/student-task-list.coffee index a504f3f1c6..b4212fa09b 100644 --- a/src/app/projects/states/dashboard/directives/student-task-list/student-task-list.coffee +++ b/src/app/projects/states/dashboard/directives/student-task-list/student-task-list.coffee @@ -44,7 +44,7 @@ angular.module('doubtfire.projects.states.dashboard.directives.student-task-list $scope.taskData.onSelectedTaskChange?(task) scrollToTaskInList(task) if task? scrollToTaskInList = (task) -> - taskEl = document.querySelector("student-task-list-#{task.taskKeyToIdString()}") + taskEl = document.querySelector("##{task.taskKeyToIdString()}") return unless taskEl? funcName = if taskEl.scrollIntoViewIfNeeded? then 'scrollIntoViewIfNeeded' else if taskEl.scrollIntoView? then 'scrollIntoView' return unless funcName? 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..e40367f623 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 @@ -363,7 +363,7 @@ export class StaffTaskListComponent implements OnInit, OnChanges, OnDestroy { } private scrollToTaskInList(task) { - const taskEl = document.querySelector(`staff-task-list #${task.taskKeyToIdString()}`) as any; + const taskEl = document.querySelector(`#${task.taskKeyToIdString()}`) as any; if (!taskEl) { return; } From a5316c47842a80f4e7bb0bbf1a84e27ccdc24e2d Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 25 Oct 2024 21:14:39 +1100 Subject: [PATCH 133/155] 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 9d017fbfaa1d5bae7f43da37ec7ae43e1dfafa8f Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Fri, 6 Dec 2024 10:25:51 +1100 Subject: [PATCH 134/155] 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 @@ +