diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a119c3b7d..9f6d6fd1f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [10.0.1-26](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.1-25...v10.0.1-26) (2026-03-26) + +### [10.0.1-25](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.1-24...v10.0.1-25) (2026-03-26) + + +### Features + +* display sso redirecting state ([248c992](https://github.com/b0ink/doubtfire-deploy/commit/248c992f46488f61916e00a87ca32ed987721f31)) +* pause feedback threshold during teaching period breaks ([#1138](https://github.com/b0ink/doubtfire-deploy/issues/1138)) ([12fbf81](https://github.com/b0ink/doubtfire-deploy/commit/12fbf8147107dc185a9a9cbea940efac93a0f316)) + +### [10.0.1-24](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.1-23...v10.0.1-24) (2026-03-24) + + +### Features + +* confirm recursive fix in mobile tutor view ([b64601a](https://github.com/b0ink/doubtfire-deploy/commit/b64601a425c7a64bccfaf47c1e4fccb0343b1589)) + +### [10.0.1-23](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.1-22...v10.0.1-23) (2026-03-24) + ### [10.0.1-22](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.1-21...v10.0.1-22) (2026-03-23) diff --git a/package-lock.json b/package-lock.json index 19291ceb8e..169f01ca68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "doubtfire", - "version": "10.0.1-22", + "version": "10.0.1-26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.1-22", + "version": "10.0.1-26", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "^17.3.6", @@ -40,7 +40,7 @@ "angular-mocks": "1.8.3", "angular-nvd3": "1.0.9", "angular-resource": "1.5.11", - "angular-sanitize": "1.5.11", + "angular-sanitize": "1.8.3", "angular-ui-bootstrap": "0.13.4", "angular-ui-codemirror": "0.3.0", "angular-xeditable": "0.9.0", @@ -3413,7 +3413,9 @@ "license": "Python-2.0" }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -3495,7 +3497,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -6611,7 +6615,10 @@ "license": "MIT" }, "node_modules/angular-sanitize": { - "version": "1.5.11", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/angular-sanitize/-/angular-sanitize-1.8.3.tgz", + "integrity": "sha512-2rxdqzlUVafUeWOwvY/FtyWk1pFTyCtzreeiTytG9m4smpuAEKaIJAjYeVwWsoV+nlTOcgpwV4W1OCmR+BQbUg==", + "deprecated": "For the actively supported Angular, see https://www.npmjs.com/package/@angular/core. AngularJS support has officially ended. For extended AngularJS support options, see https://goo.gle/angularjs-path-forward.", "license": "MIT" }, "node_modules/angular-ui-bootstrap": { @@ -7580,7 +7587,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.1", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -10081,7 +10090,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -10253,7 +10264,9 @@ "license": "Python-2.0" }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -11200,7 +11213,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -11446,7 +11458,9 @@ "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "devOptional": true, "license": "MIT", "dependencies": { @@ -11591,7 +11605,9 @@ } }, "node_modules/globule/node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -12761,7 +12777,9 @@ } }, "node_modules/grunt/node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -14232,7 +14250,9 @@ "license": "MIT" }, "node_modules/jake/node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -14550,7 +14570,9 @@ } }, "node_modules/jshint/node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -14867,7 +14889,9 @@ } }, "node_modules/karma-coverage-istanbul-reporter/node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -14938,7 +14962,9 @@ } }, "node_modules/karma/node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -16062,7 +16088,9 @@ } }, "node_modules/multimatch/node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -16396,9 +16424,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "dev": true, "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { @@ -21543,7 +21571,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 6a353dbcaa..03f925ec90 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doubtfire", - "version": "10.0.1-22", + "version": "10.0.1-26", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", @@ -59,7 +59,7 @@ "angular-mocks": "1.8.3", "angular-nvd3": "1.0.9", "angular-resource": "1.5.11", - "angular-sanitize": "1.5.11", + "angular-sanitize": "1.8.3", "angular-ui-bootstrap": "0.13.4", "angular-ui-codemirror": "0.3.0", "angular-xeditable": "0.9.0", diff --git a/src/app/api/models/task.ts b/src/app/api/models/task.ts index 2d7d995792..26488452d3 100644 --- a/src/app/api/models/task.ts +++ b/src/app/api/models/task.ts @@ -24,10 +24,11 @@ import { import {Grade} from './grade'; import {LOCALE_ID} from '@angular/core'; import {HttpClient} from '@angular/common/http'; -import {Observable, map} from 'rxjs'; +import {Observable, firstValueFrom, map} from 'rxjs'; import {gradeTaskModal, uploadSubmissionModal} from 'src/app/ajs-upgraded-providers'; import {AlertService} from 'src/app/common/services/alert.service'; import {MappingFunctions} from '../services/mapping-fn'; +import {TaskPrerequisite} from './task-prerequisite'; export const FeedbackModerationAction = { ShowMore: 'show_more', @@ -151,6 +152,49 @@ export class Task extends Entity { return this.project.unit; } + private getBreakOverlapMilliseconds( + startTime: number, + endTime: number, + breaks: readonly {startDate: Date; numberOfWeeks: number}[], + ): number { + const millisecondsPerDay = 1000 * 60 * 60 * 24; + + return breaks.reduce((overlap, teachingBreak) => { + const breakStart = new Date(teachingBreak.startDate).getTime(); + const breakDuration = (teachingBreak.numberOfWeeks ?? 0) * 7 * millisecondsPerDay; + const breakEnd = breakStart + breakDuration; + + if (!Number.isFinite(breakStart) || breakDuration <= 0) { + return overlap; + } + + const overlapStart = Math.max(startTime, breakStart); + const overlapEnd = Math.min(endTime, breakEnd); + + return overlap + Math.max(0, overlapEnd - overlapStart); + }, 0); + } + + public daysSinceSubmission(nowTime = Date.now()): number { + const millisecondsPerDay = 1000 * 60 * 60 * 24; + const submissionTime = new Date(this.submissionDate).getTime(); + + if (!Number.isFinite(submissionTime) || nowTime <= submissionTime) { + return 0; + } + + const teachingBreaks = this.unit.teachingPeriod?.breaks ?? []; + const pausedMilliseconds = this.getBreakOverlapMilliseconds( + submissionTime, + nowTime, + teachingBreaks, + ); + + return Math.floor( + Math.max(0, nowTime - submissionTime - pausedMilliseconds) / millisecondsPerDay, + ); + } + /** * Determine if a task matches a given search text. * @@ -681,6 +725,58 @@ export class Task extends Entity { ); } + private mapUnitTaskPrerequisites(prerequisites: TaskPrerequisite[]): TaskPrerequisite[] { + const definitions = this.unit.taskDefinitions; + + return prerequisites.map((prerequisite) => { + prerequisite.taskDefinition = definitions.find( + (td) => td.id === prerequisite.taskDefinitionId, + ); + prerequisite.prerequisite = definitions.find((td) => td.id === prerequisite.prerequisiteId); + return prerequisite; + }); + } + + private buildProjectTaskForDefinition(definition: TaskDefinition): Task { + const dependentTask = new Task(this.project); + dependentTask.project = this.project; + dependentTask.definition = definition; + return dependentTask; + } + + private async dependentTaskNeedsRecursiveFix(definition: TaskDefinition): Promise { + const cachedTask = this.project.findTaskForDefinition(definition.id); + if (cachedTask) { + return cachedTask.status === 'ready_for_feedback'; + } + + const dependentTask = this.buildProjectTaskForDefinition(definition); + const taskWithSubmissionDetails = await firstValueFrom(dependentTask.getSubmissionDetails()); + return taskWithSubmissionDetails.status === 'ready_for_feedback'; + } + + public async hasReadyForFeedbackDependents(): Promise { + const allPrerequisites = await firstValueFrom(this.unit.getTaskPrerequisites()); + const dependentPrerequisites = this.mapUnitTaskPrerequisites(allPrerequisites).filter( + (prerequisite) => prerequisite.prerequisiteId === this.definition.id, + ); + + for (const prerequisite of dependentPrerequisites) { + if (!prerequisite.taskDefinition) { + continue; + } + + const shouldTriggerRecursiveFix = await this.dependentTaskNeedsRecursiveFix( + prerequisite.taskDefinition, + ); + if (shouldTriggerRecursiveFix) { + return true; + } + } + + return false; + } + public get overseerEnabled(): boolean { return this.unit.overseerEnabled && this.definition.assessmentEnabled; } @@ -826,7 +922,11 @@ export class Task extends Entity { }); } - public updateTaskStatus(status: TaskStatusEnum, markAsDiscussed?: boolean) { + public updateTaskStatus( + status: TaskStatusEnum, + markAsDiscussed?: boolean, + triggerRecursiveFix?: boolean, + ) { const oldStatus = this.status; const alerts: AlertService = AppInjector.get(AlertService); @@ -851,6 +951,10 @@ export class Task extends Entity { options.body['discussed'] = true; } + if (triggerRecursiveFix === true) { + options.body['trigger_recursive_fix'] = true; + } + const hasId: boolean = this.id > 0; taskService diff --git a/src/app/api/services/task.service.ts b/src/app/api/services/task.service.ts index 8aaa3805a2..d3d517fe1e 100644 --- a/src/app/api/services/task.service.ts +++ b/src/app/api/services/task.service.ts @@ -188,6 +188,9 @@ export class TaskService extends CachedEntityService { constructorParams: unit, }, ).pipe( + map((tasks: Task[]) => + tasks.filter((t) => t.daysSinceSubmission() >= t.unit.feedbackOverflowThresholdDays), + ), tap((tasks: Task[]) => { unit.incorporateTasks(tasks); }), diff --git a/src/app/common/footer/footer.component.html b/src/app/common/footer/footer.component.html index cb6e238c38..fb9eca0065 100644 --- a/src/app/common/footer/footer.component.html +++ b/src/app/common/footer/footer.component.html @@ -45,7 +45,7 @@ matTooltipPosition="above" aria-label="" class="button large-button" - (click)="selectedTask?.updateTaskStatus('fix')" + (click)="markAsResubmit(selectedTask)" > construction diff --git a/src/app/common/footer/footer.component.ts b/src/app/common/footer/footer.component.ts index 7626b63a6d..aab80e6fd5 100644 --- a/src/app/common/footer/footer.component.ts +++ b/src/app/common/footer/footer.component.ts @@ -7,6 +7,9 @@ import {FileDownloaderService} from '../file-downloader/file-downloader.service' import {TaskAssessmentModalService} from '../modals/task-assessment-modal/task-assessment-modal.service'; import {UnitRole} from 'src/app/api/models/unit-role'; import {UserService} from 'src/app/api/services/user.service'; +import {ProjectService} from 'src/app/api/services/project.service'; +import {ConfirmationModalService} from '../modals/confirmation-modal/confirmation-modal.service'; +import {AlertService} from '../services/alert.service'; @Component({ selector: 'f-footer', @@ -20,6 +23,9 @@ export class FooterComponent implements OnInit { private fileDownloader: FileDownloaderService, private taskAssessmentModal: TaskAssessmentModalService, private userService: UserService, + private projectService: ProjectService, + private confirmationModalService: ConfirmationModalService, + private alertService: AlertService, ) {} @Input() viewType: 'inbox' | 'explorer' | 'moderation' | 'overflow'; @@ -191,4 +197,34 @@ export class FooterComponent implements OnInit { public toggleModerationStatusButtons() { this.showModerationStatusButtons = !this.showModerationStatusButtons; } + + async markAsResubmit(task: Task) { + if (!task?.definition || !task?.project) { + return; + } + + try { + const hasReadyDependents = await task.hasReadyForFeedbackDependents(); + if (!hasReadyDependents) { + task.updateTaskStatus('fix_and_resubmit'); + return; + } + + this.confirmationModalService.show( + 'Move dependent tasks to Fix and Resubmit?', + 'This task is a prerequisite for one or more other tasks submitted by this student that are Ready for Feedback. Do you want to move those tasks to Fix and Resubmit as well?', + () => { + task.updateTaskStatus('fix_and_resubmit', false, true); + }, + () => { + task.updateTaskStatus('fix_and_resubmit'); + }, + 'Yes, update dependent tasks', + 'No, just this task', + ); + } catch (error) { + this.alertService.error(`Failed to check dependent task statuses: ${error}`, 6000); + task.updateTaskStatus('fix_and_resubmit'); + } + } } diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.component.html b/src/app/common/modals/confirmation-modal/confirmation-modal.component.html index 3ec07fc729..c976c7c75a 100644 --- a/src/app/common/modals/confirmation-modal/confirmation-modal.component.html +++ b/src/app/common/modals/confirmation-modal/confirmation-modal.component.html @@ -13,8 +13,8 @@

- + diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.component.ts b/src/app/common/modals/confirmation-modal/confirmation-modal.component.ts index f9506e5b8b..d0ef5a3691 100644 --- a/src/app/common/modals/confirmation-modal/confirmation-modal.component.ts +++ b/src/app/common/modals/confirmation-modal/confirmation-modal.component.ts @@ -5,7 +5,10 @@ import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; export interface ConfirmationModalData { title: string; message: string; - action?: any; + action?: () => void; + cancelAction?: () => void; + confirmText?: string; + cancelText?: string; } @Component({ @@ -17,6 +20,9 @@ export class ConfirmationModalComponent implements OnInit { @Input() title: string; @Input() message: string; @Input() action: () => void; + @Input() cancelActionFn: () => void; + @Input() confirmText: string; + @Input() cancelText: string; constructor( @Inject(AlertService) private alertService: AlertService, @@ -29,6 +35,9 @@ export class ConfirmationModalComponent implements OnInit { this.title = this.data.title; this.message = this.data.message; this.action = this.data.action; + this.cancelActionFn = this.data.cancelAction; + this.confirmText = this.data.confirmText ?? 'Confirm'; + this.cancelText = this.data.cancelText ?? 'Cancel'; } public confirmAction() { @@ -41,7 +50,11 @@ export class ConfirmationModalComponent implements OnInit { } public cancelAction() { - this.alertService.success(`${this.title} action cancelled.`); + if (typeof this.cancelActionFn === 'function') { + this.cancelActionFn(); + } else { + this.alertService.success(`${this.title} action cancelled.`); + } this.dialogRef.close(); } } diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts b/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts index 660ddec60c..b14d252ebb 100644 --- a/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts +++ b/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts @@ -11,7 +11,10 @@ export class ConfirmationModalService { public show( title: string, message: string, - action?: any, + action?: () => void, + cancelAction?: () => void, + confirmText?: string, + cancelText?: string, ): MatDialogRef { return this.dialog.open( ConfirmationModalComponent, @@ -20,6 +23,9 @@ export class ConfirmationModalService { title, message, action, + cancelAction, + confirmText, + cancelText, }, position: {top: '2.5%'}, width: '100%', diff --git a/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts b/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts index 43416b60c1..84095a5436 100644 --- a/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts +++ b/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts @@ -17,6 +17,7 @@ import { UnitService, UserService, } from 'src/app/api/models/doubtfire-model'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; import {AlertService} from 'src/app/common/services/alert.service'; import {GradeService} from 'src/app/common/services/grade.service'; @@ -66,6 +67,7 @@ export class TutorDiscussionComponent implements AfterViewInit { private gradeService: GradeService, private state: StateService, private alertService: AlertService, + private confirmationModalService: ConfirmationModalService, private route: UIRouter, private taskCommentService: TaskCommentService, private taskService: TaskService, @@ -262,7 +264,7 @@ export class TutorDiscussionComponent implements AfterViewInit { this.selectedTask = task; } - public setSelectedTasksStatus(status: TaskStatusEnum) { + public async setSelectedTasksStatus(status: TaskStatusEnum) { const selectedTasks = this.tasksList.selectedOptions.selected.map((taskOption) => { return taskOption.value as Task; }); @@ -279,6 +281,44 @@ export class TutorDiscussionComponent implements AfterViewInit { } } + if (status === 'fix_and_resubmit') { + try { + const hasReadyDependents = ( + await Promise.all( + selectedTasks.map((task) => + task?.definition && task?.project ? task.hasReadyForFeedbackDependents() : false, + ), + ) + ).some(Boolean); + + if (hasReadyDependents) { + this.confirmationModalService.show( + 'Move dependent tasks to Fix and Resubmit?', + 'One or more selected tasks are prerequisites for other tasks submitted by this student that are Ready for Feedback. Do you want to move those tasks to Fix and Resubmit as well?', + () => { + this.updateSelectedTasksStatus(selectedTasks, status, true); + }, + () => { + this.updateSelectedTasksStatus(selectedTasks, status, false); + }, + 'Yes, update dependent tasks', + 'No, just selected tasks', + ); + return; + } + } catch (error) { + this.alertService.error(`Failed to check dependent task statuses: ${error}`, 6000); + } + } + + this.updateSelectedTasksStatus(selectedTasks, status, false); + } + + private updateSelectedTasksStatus( + selectedTasks: Task[], + status: TaskStatusEnum, + moveDependentTasks: boolean, + ) { for (const task of selectedTasks) { if ( status === 'complete' && @@ -290,6 +330,8 @@ export class TutorDiscussionComponent implements AfterViewInit { if (task.definition.assessInPortfolioOnly) { task.updateTaskStatus(status === 'complete' ? 'working_on_it' : status, true); + } else if (status === 'fix_and_resubmit') { + task.updateTaskStatus(status, true, moveDependentTasks); } else { task.updateTaskStatus(status, true); } diff --git a/src/app/sessions/states/sign-in/sign-in.component.html b/src/app/sessions/states/sign-in/sign-in.component.html index 74fd6b7b99..29b7743d41 100644 --- a/src/app/sessions/states/sign-in/sign-in.component.html +++ b/src/app/sessions/states/sign-in/sign-in.component.html @@ -57,7 +57,14 @@

type="form" [disabled]="form.invalid" > - Sign In +
+ @if (redirectingSSO) { + Signing In... + + } @else { + Sign In + } +
diff --git a/src/app/sessions/states/sign-in/sign-in.component.ts b/src/app/sessions/states/sign-in/sign-in.component.ts index 8f047489d4..2196ee83c9 100644 --- a/src/app/sessions/states/sign-in/sign-in.component.ts +++ b/src/app/sessions/states/sign-in/sign-in.component.ts @@ -54,6 +54,8 @@ export class SignInComponent implements OnInit { public isLoading: boolean = true; public authMethodFailed: boolean = false; + public redirectingSSO: boolean = false; + // Get query params from the resolve in the router state @Input() username: string; @Input() authToken: string; @@ -163,10 +165,13 @@ export class SignInComponent implements OnInit { }); } else if (this.SSOLoginUrl) { if (this.autoLogin) { + this.redirectingSSO = true; return wait.then(() => { // Double check in case changed in the meantime if (this.autoLogin) { this.redirectToSSO(); + } else { + this.redirectingSSO = false; } }); } else { diff --git a/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.html b/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.html index 5912585804..2f96b676bb 100644 --- a/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.html +++ b/src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.html @@ -269,7 +269,11 @@

} @else if (getWarningIcon(task) === 'overflow') { watch_later 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 1d3c166609..3b65b4247e 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 @@ -572,10 +572,7 @@ export class StaffTaskListComponent implements OnInit, OnChanges, OnDestroy { return null; } - const now = Date.now(); - const submission = new Date(task.submissionDate).getTime(); - - const daysSinceSubmission = (now - submission) / (1000 * 60 * 60 * 24); + const daysSinceSubmission = task.daysSinceSubmission(); if (daysSinceSubmission >= task.unit.feedbackOverflowThresholdDays) { return 'overflow'; diff --git a/src/app/units/states/tasks/viewer/directives/unit-task-list/unit-task-list.coffee b/src/app/units/states/tasks/viewer/directives/unit-task-list/unit-task-list.coffee deleted file mode 100644 index aa30c611e4..0000000000 --- a/src/app/units/states/tasks/viewer/directives/unit-task-list/unit-task-list.coffee +++ /dev/null @@ -1,41 +0,0 @@ -angular.module('doubtfire.units.states.tasks.viewer.directives.unit-task-list', []) - -.directive('unitTaskList', -> - restrict: 'E' - templateUrl: 'units/states/tasks/viewer/directives/unit-task-list/unit-task-list.tpl.html' - scope: - unit: '=' - # Function to invoke to refresh tasks - refreshTasks: '=?' - unitTasks: '=' - selectedTaskDef: '=' - controller: ($scope, $timeout, $filter, gradeService, listenerService) -> - listeners = listenerService.listenTo($scope) - # Set up initial filtered tasks - $scope.filteredTasks = [] - # Set up filters - $scope.filters = { - taskSearch:null - } - # Sets new filteredTasks variable - applyFilters = -> - filteredTasks = $filter('taskDefinitionName')($scope.unitTasks, $scope.filters.taskSearch) - filteredTasks = $filter('orderBy')(filteredTasks, 'task.seq') - $scope.filteredTasks = filteredTasks - # Apply filters first-time - applyFilters() - # When refreshing tasks, we are just reloading the active tasks - $scope.refreshTasks = applyFilters - # Expose grade service names - $scope.gradeNames = gradeService.grades - # On task name change, reapply filters - $scope.taskNameChanged = applyFilters - # UI call to change currently selected task - $scope.setSelectedTask = (task) -> - $scope.selectedTaskDef = task - - - $scope.isSelectedTask = (task) -> - # Compare by definition - task.id == $scope.selectedTaskDef?.id -) diff --git a/src/app/units/states/tasks/viewer/directives/unit-task-list/unit-task-list.scss b/src/app/units/states/tasks/viewer/directives/unit-task-list/unit-task-list.scss deleted file mode 100644 index b17c5d3b7b..0000000000 --- a/src/app/units/states/tasks/viewer/directives/unit-task-list/unit-task-list.scss +++ /dev/null @@ -1,3 +0,0 @@ -unit-task-list { - @include task-list(); -} diff --git a/src/app/units/states/tasks/viewer/directives/unit-task-list/unit-task-list.tpl.html b/src/app/units/states/tasks/viewer/directives/unit-task-list/unit-task-list.tpl.html deleted file mode 100644 index 4a0db812c4..0000000000 --- a/src/app/units/states/tasks/viewer/directives/unit-task-list/unit-task-list.tpl.html +++ /dev/null @@ -1,30 +0,0 @@ -
-
- -
-
    -
  • -
    -

    {{task.name}}

    -

    - {{task.abbreviation}} - {{gradeNames[task.targetGrade]}} Task -

    -
    - -
  • - -
  • No tasks to display.
  • -
-