From a9b1b562a0b06ba9460392ef29955fe837338d2d Mon Sep 17 00:00:00 2001 From: "josh.talev" Date: Thu, 26 Mar 2026 23:03:50 +1100 Subject: [PATCH 1/2] adds estimated days/hours to task definitions --- src/app/api/models/task-definition.ts | 416 +++++++++--------- .../task-definition-general.component.html | 28 ++ 2 files changed, 237 insertions(+), 207 deletions(-) diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index 1b49a2e856..57ed38b611 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -1,207 +1,209 @@ -import { HttpClient } from '@angular/common/http'; -import { Entity, EntityMapping } from 'ngx-entity-service'; -import { Observable, tap } from 'rxjs'; -import { AppInjector } from 'src/app/app-injector'; -import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; -import { Grade, GroupSet, TutorialStream, Unit } from './doubtfire-model'; -import { TaskDefinitionService } from '../services/task-definition.service'; - -export type UploadRequirement = { key: string; name: string; type: string; tiiCheck?: boolean; tiiPct?: number }; - -export type SimilarityCheck = { key: string; type: string; pattern: string }; - -export class TaskDefinition extends Entity { - id: number; - seq: number; - abbreviation: string; - name: string; - description: string; - weighting: number; - targetGrade: number; - targetDate: Date; - dueDate: Date; - startDate: Date; - uploadRequirements: UploadRequirement[]; - tutorialStream: TutorialStream = null; - plagiarismChecks: SimilarityCheck[] = []; - plagiarismReportUrl: string; - plagiarismWarnPct: number; - restrictStatusUpdates: boolean; - // groupSetId: number; - groupSet: GroupSet = null; - hasTaskSheet: boolean; - hasTaskResources: boolean; - hasTaskAssessmentResources: boolean; - isGraded: boolean; - maxQualityPts: number; - overseerImageId: number; - assessmentEnabled: boolean; - mossLanguage: string = 'moss c'; - - readonly unit: Unit; - - constructor(unit: Unit) { - super(); - this.unit = unit; - } - - public toJson(mappingData: EntityMapping, ignoreKeys?: string[]): object { - return { - task_def: super.toJson(mappingData, ignoreKeys), - }; - } - - /** - * Save the task definition - */ - public save(): Observable { - const svc = AppInjector.get(TaskDefinitionService); - - if (this.isNew) { - // TODO: add progress modal - return svc.create( - { - unitId: this.unit.id, - }, - { - entity: this, - cache: this.unit.taskDefinitionCache, - constructorParams: this.unit, - } - ); - } else { - return svc.update( - { - unitId: this.unit.id, - id: this.id, - }, - { entity: this } - ); - } - } - - private originalSaveData: string; - - public get hasOriginalSaveData(): boolean { - return this.originalSaveData !== undefined && this.originalSaveData !== null; - } - - /** - * To check if things have changed, we need to get the initial save data... as it - * isn't empty by default. We can then use - * this to check if there are changes. - * - * @param mapping the mapping to get changes - */ - public setOriginalSaveData(mapping: EntityMapping) { - this.originalSaveData = JSON.stringify(this.toJson(mapping)); - } - - public hasChanges(mapping: EntityMapping): boolean { - if (!this.originalSaveData) { - return false; - } - - return this.originalSaveData != JSON.stringify(this.toJson(mapping)); - } - - public get isNew(): boolean { - return !this.id; - } - - public get unitId(): number { - return this.unit.id; - } - - public localDueDate(): Date { - return this.targetDate; - } - - public localDeadlineDate(): Date { - return this.dueDate; - } - - public matches(text: string): boolean { - return this.abbreviation.toLowerCase().indexOf(text) !== -1 || this.name.toLowerCase().indexOf(text) !== -1; - } - - /** - * The final deadline for task submission. - * - * @returns the final due date - */ - public finalDeadlineDate(): Date { - return this.dueDate; // now in due date - } - - public isGroupTask(): boolean { - return this.groupSet !== null && this.groupSet !== undefined; - } - - public getTaskPDFUrl(asAttachment: boolean = false): string { - const constants = AppInjector.get(DoubtfireConstants); - return `${constants.API_URL}/units/${this.unit.id}/task_definitions/${this.id}/task_pdf.json${ - asAttachment ? '?as_attachment=true' : '' - }`; - } - - public getTaskResourcesUrl(asAttachment: boolean = false) { - const constants = AppInjector.get(DoubtfireConstants); - return `${constants.API_URL}/units/${this.unit.id}/task_definitions/${this.id}/task_resources.json${ - asAttachment ? '?as_attachment=true' : '' - }`; - } - - public get targetGradeText(): string { - return Grade.GRADES[this.targetGrade]; - } - - public hasPlagiarismCheck(): boolean { - return this.plagiarismChecks?.length > 0; - } - - public get needsMoss(): boolean { - return this.uploadRequirements.some((upreq) => upreq.type === 'code' && upreq.tiiCheck); - } - - public get taskSheetUploadUrl(): string { - return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.unit.id}/task_definitions/${ - this.id - }/task_sheet`; - } - - public get taskResourcesUploadUrl(): string { - return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.unit.id}/task_definitions/${ - this.id - }/task_resources`; - } - - public get taskAssessmentResourcesUploadUrl(): string { - return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.unit.id}/task_definitions/${ - this.id - }/task_assessment_resources`; - } - - public getTaskAssessmentResourcesUrl(): string { - return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.unit.id}/task_definitions/${ - this.id - }/task_assessment_resources.json`; - } - - public deleteTaskSheet(): Observable { - const httpClient = AppInjector.get(HttpClient); - return httpClient.delete(this.taskSheetUploadUrl).pipe(tap(() => (this.hasTaskSheet = false))); - } - - public deleteTaskResources(): Observable { - const httpClient = AppInjector.get(HttpClient); - return httpClient.delete(this.taskResourcesUploadUrl).pipe(tap(() => (this.hasTaskResources = false))); - } - - public deleteTaskAssessmentResources(): Observable { - const httpClient = AppInjector.get(HttpClient); - return httpClient - .delete(this.taskAssessmentResourcesUploadUrl) - .pipe(tap(() => (this.hasTaskAssessmentResources = false))); - } -} +import { HttpClient } from '@angular/common/http'; +import { Entity, EntityMapping } from 'ngx-entity-service'; +import { Observable, tap } from 'rxjs'; +import { AppInjector } from 'src/app/app-injector'; +import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; +import { Grade, GroupSet, TutorialStream, Unit } from './doubtfire-model'; +import { TaskDefinitionService } from '../services/task-definition.service'; + +export type UploadRequirement = { key: string; name: string; type: string; tiiCheck?: boolean; tiiPct?: number }; + +export type SimilarityCheck = { key: string; type: string; pattern: string }; + +export class TaskDefinition extends Entity { + id: number; + seq: number; + abbreviation: string; + name: string; + description: string; + weighting: number; + estimated_days: number = null; + estimated_hours: number = null; + targetGrade: number; + targetDate: Date; + dueDate: Date; + startDate: Date; + uploadRequirements: UploadRequirement[]; + tutorialStream: TutorialStream = null; + plagiarismChecks: SimilarityCheck[] = []; + plagiarismReportUrl: string; + plagiarismWarnPct: number; + restrictStatusUpdates: boolean; + // groupSetId: number; + groupSet: GroupSet = null; + hasTaskSheet: boolean; + hasTaskResources: boolean; + hasTaskAssessmentResources: boolean; + isGraded: boolean; + maxQualityPts: number; + overseerImageId: number; + assessmentEnabled: boolean; + mossLanguage: string = 'moss c'; + + readonly unit: Unit; + + constructor(unit: Unit) { + super(); + this.unit = unit; + } + + public toJson(mappingData: EntityMapping, ignoreKeys?: string[]): object { + return { + task_def: super.toJson(mappingData, ignoreKeys), + }; + } + + /** + * Save the task definition + */ + public save(): Observable { + const svc = AppInjector.get(TaskDefinitionService); + console.log(this); + if (this.isNew) { + // TODO: add progress modal + return svc.create( + { + unitId: this.unit.id, + }, + { + entity: this, + cache: this.unit.taskDefinitionCache, + constructorParams: this.unit, + } + ); + } else { + return svc.update( + { + unitId: this.unit.id, + id: this.id, + }, + { entity: this } + ); + } + } + + private originalSaveData: string; + + public get hasOriginalSaveData(): boolean { + return this.originalSaveData !== undefined && this.originalSaveData !== null; + } + + /** + * To check if things have changed, we need to get the initial save data... as it + * isn't empty by default. We can then use + * this to check if there are changes. + * + * @param mapping the mapping to get changes + */ + public setOriginalSaveData(mapping: EntityMapping) { + this.originalSaveData = JSON.stringify(this.toJson(mapping)); + } + + public hasChanges(mapping: EntityMapping): boolean { + if (!this.originalSaveData) { + return false; + } + + return this.originalSaveData != JSON.stringify(this.toJson(mapping)); + } + + public get isNew(): boolean { + return !this.id; + } + + public get unitId(): number { + return this.unit.id; + } + + public localDueDate(): Date { + return this.targetDate; + } + + public localDeadlineDate(): Date { + return this.dueDate; + } + + public matches(text: string): boolean { + return this.abbreviation.toLowerCase().indexOf(text) !== -1 || this.name.toLowerCase().indexOf(text) !== -1; + } + + /** + * The final deadline for task submission. + * + * @returns the final due date + */ + public finalDeadlineDate(): Date { + return this.dueDate; // now in due date + } + + public isGroupTask(): boolean { + return this.groupSet !== null && this.groupSet !== undefined; + } + + public getTaskPDFUrl(asAttachment: boolean = false): string { + const constants = AppInjector.get(DoubtfireConstants); + return `${constants.API_URL}/units/${this.unit.id}/task_definitions/${this.id}/task_pdf.json${ + asAttachment ? '?as_attachment=true' : '' + }`; + } + + public getTaskResourcesUrl(asAttachment: boolean = false) { + const constants = AppInjector.get(DoubtfireConstants); + return `${constants.API_URL}/units/${this.unit.id}/task_definitions/${this.id}/task_resources.json${ + asAttachment ? '?as_attachment=true' : '' + }`; + } + + public get targetGradeText(): string { + return Grade.GRADES[this.targetGrade]; + } + + public hasPlagiarismCheck(): boolean { + return this.plagiarismChecks?.length > 0; + } + + public get needsMoss(): boolean { + return this.uploadRequirements.some((upreq) => upreq.type === 'code' && upreq.tiiCheck); + } + + public get taskSheetUploadUrl(): string { + return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.unit.id}/task_definitions/${ + this.id + }/task_sheet`; + } + + public get taskResourcesUploadUrl(): string { + return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.unit.id}/task_definitions/${ + this.id + }/task_resources`; + } + + public get taskAssessmentResourcesUploadUrl(): string { + return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.unit.id}/task_definitions/${ + this.id + }/task_assessment_resources`; + } + + public getTaskAssessmentResourcesUrl(): string { + return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.unit.id}/task_definitions/${ + this.id + }/task_assessment_resources.json`; + } + + public deleteTaskSheet(): Observable { + const httpClient = AppInjector.get(HttpClient); + return httpClient.delete(this.taskSheetUploadUrl).pipe(tap(() => (this.hasTaskSheet = false))); + } + + public deleteTaskResources(): Observable { + const httpClient = AppInjector.get(HttpClient); + return httpClient.delete(this.taskResourcesUploadUrl).pipe(tap(() => (this.hasTaskResources = false))); + } + + public deleteTaskAssessmentResources(): Observable { + const httpClient = AppInjector.get(HttpClient); + return httpClient + .delete(this.taskAssessmentResourcesUploadUrl) + .pipe(tap(() => (this.hasTaskAssessmentResources = false))); + } +} diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-general/task-definition-general.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-general/task-definition-general.component.html index 5ec2551e03..8413f48bd6 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-general/task-definition-general.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-general/task-definition-general.component.html @@ -28,3 +28,31 @@ Task description + +
+
Expected duration
+
+ + Days + + + + + Hours + + +
+
From 43b794060d1c598fd24d83e6b9e9ff80188673ce Mon Sep 17 00:00:00 2001 From: "josh.talev" Date: Mon, 30 Mar 2026 21:33:56 +1100 Subject: [PATCH 2/2] update key mappings --- src/app/api/models/task-definition.ts | 1 - .../api/services/task-definition.service.ts | 264 +++++++++--------- 2 files changed, 133 insertions(+), 132 deletions(-) diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index 57ed38b611..704884b229 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -58,7 +58,6 @@ export class TaskDefinition extends Entity { */ public save(): Observable { const svc = AppInjector.get(TaskDefinitionService); - console.log(this); if (this.isNew) { // TODO: add progress modal return svc.create( diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index 13a1dd2797..ac6fb27a6f 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -1,131 +1,133 @@ -import { CachedEntityService } from 'ngx-entity-service'; -import { TaskDefinition, Unit } from 'src/app/api/models/doubtfire-model'; -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import API_URL from 'src/app/config/constants/apiURL'; -import { MappingFunctions } from './mapping-fn'; -import { AppInjector } from 'src/app/app-injector'; -import { Observable } from 'rxjs'; - -@Injectable() -export class TaskDefinitionService extends CachedEntityService { - protected readonly endpointFormat = 'units/:unitId:/task_definitions/:id:'; - - constructor(httpClient: HttpClient) { - super(httpClient, API_URL); - - this.mapping.addKeys( - 'id', - 'abbreviation', - 'name', - 'description', - 'weighting', - 'targetGrade', - 'mossLanguage', - { - keys: 'targetDate', - toEntityFn: MappingFunctions.mapDateToEndOfDay, - toJsonFn: MappingFunctions.mapDayToJson, - }, - { - keys: 'dueDate', - toEntityFn: MappingFunctions.mapDateToEndOfDay, - toJsonFn: MappingFunctions.mapDayToJson, - }, - { - keys: 'startDate', - toEntityFn: MappingFunctions.mapDateToDay, - toJsonFn: MappingFunctions.mapDayToJson, - }, - { - keys: 'uploadRequirements', - toJsonFn: (taskDef: TaskDefinition, key: string) => { - return JSON.stringify( - taskDef.uploadRequirements.map((upreq) => { - return { - key: upreq.key, - name: upreq.name, - type: upreq.type, - 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) => { - return { - key: upreq.key, - name: upreq.name, - type: upreq.type, - tiiCheck: upreq.tii_check, - tiiPct: upreq.tii_pct, - }; - }); - }, - }, - { - keys: ['tutorialStream', 'tutorial_stream_abbr'], - toEntityFn: (data: object, key: string, taskDef: TaskDefinition, params?: any) => { - return taskDef.unit.tutorialStreamsCache.get(data[key]); - }, - toJsonFn: (taskDef: TaskDefinition, key: string) => { - return taskDef.tutorialStream?.abbreviation; - }, - }, - 'plagiarismWarnPct', - 'restrictStatusUpdates', - { - keys: ['groupSet', 'group_set_id'], - toEntityFn: (data: object, key: string, taskDef: TaskDefinition, params?: any) => { - if (data[key]) { - return taskDef.unit.groupSetsCache.get(data[key]); - } else { - return data[key]; - } - }, - toJsonFn: (taskDef: TaskDefinition, key: string) => { - return taskDef.groupSet?.id; - }, - }, - 'hasTaskSheet', - 'hasTaskResources', - 'hasTaskAssessmentResources', - 'isGraded', - 'maxQualityPts', - 'overseerImageId', - 'assessmentEnabled' - ); - - this.mapping.mapAllKeysToJsonExcept( - 'id', - 'hasTaskSheet', - 'hasTaskResources', - 'hasTaskAssessmentResources' - ); - } - - public override createInstanceFrom(json: object, other?: any): TaskDefinition { - return new TaskDefinition(other as Unit); - } - - public uploadTaskSheet(taskDefinition: TaskDefinition, file: File): Observable { - const formData = new FormData(); - formData.append('file', file); - return AppInjector.get(HttpClient).post(taskDefinition.taskSheetUploadUrl, formData); - } - - public uploadTaskResources(taskDefinition: TaskDefinition, file: File): Observable { - const formData = new FormData(); - formData.append('file', file); - return AppInjector.get(HttpClient).post(taskDefinition.taskResourcesUploadUrl, formData); - } - - public uploadOverseerResources(taskDefinition: TaskDefinition, file: File): Observable { - const formData = new FormData(); - formData.append('file', file); - return AppInjector.get(HttpClient).post(taskDefinition.taskAssessmentResourcesUploadUrl, formData); - } -} +import { CachedEntityService } from 'ngx-entity-service'; +import { TaskDefinition, Unit } from 'src/app/api/models/doubtfire-model'; +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import API_URL from 'src/app/config/constants/apiURL'; +import { MappingFunctions } from './mapping-fn'; +import { AppInjector } from 'src/app/app-injector'; +import { Observable } from 'rxjs'; + +@Injectable() +export class TaskDefinitionService extends CachedEntityService { + protected readonly endpointFormat = 'units/:unitId:/task_definitions/:id:'; + + constructor(httpClient: HttpClient) { + super(httpClient, API_URL); + + this.mapping.addKeys( + 'id', + 'abbreviation', + 'name', + 'description', + 'weighting', + 'estimated_days', + 'estimated_hours', + 'targetGrade', + 'mossLanguage', + { + keys: 'targetDate', + toEntityFn: MappingFunctions.mapDateToEndOfDay, + toJsonFn: MappingFunctions.mapDayToJson, + }, + { + keys: 'dueDate', + toEntityFn: MappingFunctions.mapDateToEndOfDay, + toJsonFn: MappingFunctions.mapDayToJson, + }, + { + keys: 'startDate', + toEntityFn: MappingFunctions.mapDateToDay, + toJsonFn: MappingFunctions.mapDayToJson, + }, + { + keys: 'uploadRequirements', + toJsonFn: (taskDef: TaskDefinition, key: string) => { + return JSON.stringify( + taskDef.uploadRequirements.map((upreq) => { + return { + key: upreq.key, + name: upreq.name, + type: upreq.type, + 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) => { + return { + key: upreq.key, + name: upreq.name, + type: upreq.type, + tiiCheck: upreq.tii_check, + tiiPct: upreq.tii_pct, + }; + }); + }, + }, + { + keys: ['tutorialStream', 'tutorial_stream_abbr'], + toEntityFn: (data: object, key: string, taskDef: TaskDefinition, params?: any) => { + return taskDef.unit.tutorialStreamsCache.get(data[key]); + }, + toJsonFn: (taskDef: TaskDefinition, key: string) => { + return taskDef.tutorialStream?.abbreviation; + }, + }, + 'plagiarismWarnPct', + 'restrictStatusUpdates', + { + keys: ['groupSet', 'group_set_id'], + toEntityFn: (data: object, key: string, taskDef: TaskDefinition, params?: any) => { + if (data[key]) { + return taskDef.unit.groupSetsCache.get(data[key]); + } else { + return data[key]; + } + }, + toJsonFn: (taskDef: TaskDefinition, key: string) => { + return taskDef.groupSet?.id; + }, + }, + 'hasTaskSheet', + 'hasTaskResources', + 'hasTaskAssessmentResources', + 'isGraded', + 'maxQualityPts', + 'overseerImageId', + 'assessmentEnabled' + ); + + this.mapping.mapAllKeysToJsonExcept( + 'id', + 'hasTaskSheet', + 'hasTaskResources', + 'hasTaskAssessmentResources' + ); + } + + public override createInstanceFrom(json: object, other?: any): TaskDefinition { + return new TaskDefinition(other as Unit); + } + + public uploadTaskSheet(taskDefinition: TaskDefinition, file: File): Observable { + const formData = new FormData(); + formData.append('file', file); + return AppInjector.get(HttpClient).post(taskDefinition.taskSheetUploadUrl, formData); + } + + public uploadTaskResources(taskDefinition: TaskDefinition, file: File): Observable { + const formData = new FormData(); + formData.append('file', file); + return AppInjector.get(HttpClient).post(taskDefinition.taskResourcesUploadUrl, formData); + } + + public uploadOverseerResources(taskDefinition: TaskDefinition, file: File): Observable { + const formData = new FormData(); + formData.append('file', file); + return AppInjector.get(HttpClient).post(taskDefinition.taskAssessmentResourcesUploadUrl, formData); + } +}