diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index ae030bcc3e..4df00b3cd4 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -36,7 +36,7 @@ import {MatSnackBarModule} from '@angular/material/snack-bar'; import {MatPaginatorModule} from '@angular/material/paginator'; import {MatTooltipModule} from '@angular/material/tooltip'; import {MatSlideToggleModule} from '@angular/material/slide-toggle'; -import {MatChipListbox, MatChipsModule} from '@angular/material/chips'; +import {MatChipsModule} from '@angular/material/chips'; import {MatGridListModule} from '@angular/material/grid-list'; import {PdfViewerModule} from 'ng2-pdf-viewer'; import {UIRouterUpgradeModule} from '@uirouter/angular-hybrid'; @@ -213,6 +213,8 @@ import {TaskAssessmentCardComponent} from './projects/states/dashboard/directive import {TaskSubmissionCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component'; import {TaskDashboardComponent} from './projects/states/dashboard/directives/task-dashboard/task-dashboard.component'; import {InboxComponent} from './units/states/tasks/inbox/inbox.component'; +import {PortfoliosComponent} from './units/states/portfolios/portfolios.component'; +import {AjsProjectProgressDashboardComponent} from './units/states/portfolios/ajs-project-progress-dashboard.component'; import {ProjectProgressBarComponent} from './common/project-progress-bar/project-progress-bar.component'; import {TeachingPeriodListComponent} from './admin/states/teaching-periods/teaching-period-list/teaching-period-list.component'; import {FChipComponent} from './common/f-chip/f-chip.component'; @@ -454,6 +456,8 @@ const GANTT_CHART_CONFIG = { TaskSubmissionCardComponent, TaskDashboardComponent, InboxComponent, + PortfoliosComponent, + AjsProjectProgressDashboardComponent, ProjectProgressBarComponent, TeachingPeriodListComponent, CreateNewUnitModal, @@ -683,7 +687,6 @@ const GANTT_CHART_CONFIG = { NgxGanttModule, MatSidenavModule, MonacoEditorModule.forRoot(), - MatChipListbox, ], }) diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index ca57426fd2..637af2ed6c 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -203,6 +203,7 @@ import {FooterComponent} from './common/footer/footer.component'; import {TaskAssessmentCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component'; import {TaskSubmissionCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component'; import {InboxComponent} from './units/states/tasks/inbox/inbox.component'; +import {PortfoliosComponent} from './units/states/portfolios/portfolios.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'; @@ -417,6 +418,7 @@ DoubtfireAngularJSModule.directive( downgradeComponent({component: TasksViewerComponent}), ); DoubtfireAngularJSModule.directive('fInbox', downgradeComponent({component: InboxComponent})); +DoubtfireAngularJSModule.directive('fPortfolios', downgradeComponent({component: PortfoliosComponent})); DoubtfireAngularJSModule.directive( 'fTaskDueCard', downgradeComponent({component: TaskDueCardComponent}), diff --git a/src/app/projects/project-progress-dashboard/project-progress-dashboard.coffee b/src/app/projects/project-progress-dashboard/project-progress-dashboard.coffee index d28a0d31b5..fcf826733d 100644 --- a/src/app/projects/project-progress-dashboard/project-progress-dashboard.coffee +++ b/src/app/projects/project-progress-dashboard/project-progress-dashboard.coffee @@ -10,17 +10,31 @@ angular.module('doubtfire.projects.project-progress-dashboard',[]) .directive('projectProgressDashboard', -> restrict: 'E' templateUrl: 'projects/project-progress-dashboard/project-progress-dashboard.tpl.html' + # Explicit bindings for use inside upgraded Angular hosts (f-portfolios). Link copies from + # $parent when attributes are omitted so legacy templates without project="..." still work. + scope: + project: '<' + unit: '<' controller: ($scope, $state, $rootScope, $stateParams, newProjectService, alertService, gradeService, newTaskService, listenerService) -> - if $stateParams.projectId? - $scope.studentProjectId = $stateParams.projectId - else if $scope.project? - $scope.studentProjectId = $scope.project.id - $scope.grades = gradeService.grades $scope.currentVisualisation = 'burndown' + $scope.taskStats = {} + + updateTaskCompletionStats = -> + return unless $scope.project? + $scope.taskStats.numberOfTasksCompleted = $scope.project.tasksByStatus(newTaskService.completeStatus).length + $scope.taskStats.numberOfTasksRemaining = $scope.project.activeTasks().length - $scope.taskStats.numberOfTasksCompleted + + syncStudentProjectId = -> + if $stateParams.projectId? + $scope.studentProjectId = $stateParams.projectId + else if $scope.project? + $scope.studentProjectId = $scope.project.id + $scope.chooseGrade = (idx) -> + return unless $scope.project? $scope.project.targetGrade = idx newProjectService.update($scope.project).subscribe( (response) -> @@ -29,18 +43,19 @@ angular.module('doubtfire.projects.project-progress-dashboard',[]) updateTaskCompletionStats() $scope.taskCount = -> - $scope.unit.taskDefinitionCount - - $scope.taskStats = {} - - # Update move to task and project... - updateTaskCompletionStats = -> - $scope.taskStats.numberOfTasksCompleted = $scope.project.tasksByStatus(newTaskService.completeStatus).length - $scope.taskStats.numberOfTasksRemaining = $scope.project.activeTasks().length - $scope.taskStats.numberOfTasksCompleted + $scope.unit?.taskDefinitionCount $scope.$on 'TaskStatusUpdated', -> updateTaskCompletionStats() + $scope.$watch 'project', (project) -> + return unless project? + syncStudentProjectId() + updateTaskCompletionStats() - updateTaskCompletionStats() + link: (scope, _el, _attrs) -> + unless scope.project? + scope.project = scope.$parent.project if scope.$parent? + unless scope.unit? + scope.unit = scope.$parent.unit if scope.$parent? ) diff --git a/src/app/projects/states/index/global-state.service.ts b/src/app/projects/states/index/global-state.service.ts index 6d3cdb12d2..c28824121b 100644 --- a/src/app/projects/states/index/global-state.service.ts +++ b/src/app/projects/states/index/global-state.service.ts @@ -2,7 +2,7 @@ import {Inject, Injectable, OnDestroy} from '@angular/core'; import {MediaObserver} from 'ng-flex-layout'; import {UIRouter} from '@uirouter/angular'; import {EntityCache} from 'ngx-entity-service'; -import {BehaviorSubject, Observable, Subject, skip, take} from 'rxjs'; +import {BehaviorSubject, Observable, Subject, catchError, of, skip, take} from 'rxjs'; import { CampusService, LearningOutcomeService, @@ -122,9 +122,7 @@ export class GlobalStateService implements OnDestroy { setTimeout(() => { // Try to login using the refresh token this.authenticationService.attemptLoginUsingRefreshToken((result: boolean) => { - if (result) { - this.loadGlobals(); - } else { + if (!result) { // Loading is finshed... this.isLoadingSubject.next(false); @@ -133,6 +131,7 @@ export class GlobalStateService implements OnDestroy { this.router.stateService.go('sign_in'); } } + // On success, loadGlobals() runs from AuthenticationService.setupUserFromResponse }); }, 100); @@ -238,29 +237,35 @@ export class GlobalStateService implements OnDestroy { }); if (this.userService.currentUser.isStaff) { + // Global GLO / feedback-chip endpoints are optional: many API builds do not mount + // GET /api/global/outcomes or /api/global/feedback_chips yet. Failing here should not + // block login or spam error toasts; task-level data still loads from unit routes. this.learningOutcomeService .query({}, {endpointFormat: LearningOutcomeService.globalEndpoint}) + .pipe( + catchError((err: unknown) => { + console.warn('Global learning outcomes (GLO) bootstrap skipped:', err); + return of([]); + }), + ) .subscribe({ next: (_response) => { subscriber.next(true); }, - error: (_response) => { - this.alerts.error('Unable to access service. Failed loading GLOs.', 6000); - }, }); this.feedbackTemplateService .query({}, {endpointFormat: FeedbackTemplateService.globalEndpoint}) + .pipe( + catchError((err: unknown) => { + console.warn('Global GLO feedback templates bootstrap skipped:', err); + return of([]); + }), + ) .subscribe({ next: (_response) => { subscriber.next(true); }, - error: (_response) => { - this.alerts.error( - 'Unable to access service. Failed loading GLO feedback templates.', - 6000, - ); - }, }); } 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..91c6de68cc 100644 --- a/src/app/sessions/states/sign-in/sign-in.component.ts +++ b/src/app/sessions/states/sign-in/sign-in.component.ts @@ -206,7 +206,7 @@ export class SignInComponent implements OnInit { * Perform the actions needed when the user successfully signs in. */ private actionSignInSuccess(): void { - this.globalState.loadGlobals(); + // loadGlobals() already runs inside AuthenticationService.setupUserFromResponse after sign-in this.state.go('welcome'); } @@ -246,7 +246,6 @@ export class SignInComponent implements OnInit { this.authService.signIn(signInCredentials).subscribe({ next: () => { if (this.isLtiLogin) { - this.globalState.loadGlobals(); const params = getUrlParams(document.location.href); this.state.go('lti', { ltik: params.ltik, diff --git a/src/app/units/states/portfolios/ajs-project-progress-dashboard.component.ts b/src/app/units/states/portfolios/ajs-project-progress-dashboard.component.ts new file mode 100644 index 0000000000..e71e55855e --- /dev/null +++ b/src/app/units/states/portfolios/ajs-project-progress-dashboard.component.ts @@ -0,0 +1,32 @@ +import { Component, ElementRef, Inject, Input, Injector, OnInit, Optional } from '@angular/core'; +import { UpgradeComponent } from '@angular/upgrade/static'; +import { Project } from 'src/app/api/models/project'; +import { Unit } from 'src/app/api/models/unit'; +import { visualisations } from 'src/app/ajs-upgraded-providers'; + +/** + * Hosts the AngularJS `projectProgressDashboard` directive inside Angular templates + * (same charts and target-grade UI as the legacy portfolios “View Progress” tab). + */ +@Component({ + selector: 'f-ajs-project-progress-dashboard', + template: '', +}) +export class AjsProjectProgressDashboardComponent extends UpgradeComponent implements OnInit { + @Input() project: Project; + @Input() unit: Unit; + + constructor( + elementRef: ElementRef, + injector: Injector, + @Optional() @Inject(visualisations) private readonly visualisationApi: { refreshAll?: () => void } | null, + ) { + super('projectProgressDashboard', elementRef, injector); + } + + override ngOnInit(): void { + super.ngOnInit(); + // Burndown / pie use legacy visualisation lifecycle; match UnitPortfoliosStateCtrl.refreshCharts. + setTimeout(() => this.visualisationApi?.refreshAll?.(), 0); + } +} diff --git a/src/app/units/states/portfolios/portfolios.coffee b/src/app/units/states/portfolios/portfolios.coffee index 62c70a04f3..86193d5628 100644 --- a/src/app/units/states/portfolios/portfolios.coffee +++ b/src/app/units/states/portfolios/portfolios.coffee @@ -7,154 +7,9 @@ angular.module('doubtfire.units.states.portfolios', []) parent: 'units/index' url: '/students/portfolios' templateUrl: "units/states/portfolios/portfolios.tpl.html" - controller: "UnitPortfoliosStateCtrl" data: task: "Student Portfolios" pageTitle: "_Home_" roleWhitelist: ['Tutor', 'Convenor', 'Admin', 'Auditor'] } ) -.controller("UnitPortfoliosStateCtrl", ($scope, alertService, analyticsService, gradeService, newProjectService, Visualisation, newTaskService, fileDownloaderService, newUserService, D2lTransferModal, newUnitService, sidekiqProgressModalService) -> - # TODO: (@alexcu) Break this down into smaller directives/substates - - $scope.unit.loadD2lMapping().subscribe() - - $scope.downloadGrades = -> fileDownloaderService.downloadFile($scope.unit.gradesUrl, "#{$scope.unit.code}-grades.csv") - - $scope.downloadPortfolios = -> - newUnitService.zipPortfolios($scope.unit).subscribe({ - next: (newJob) -> - sidekiqProgressModalService.show("Downloading Portfolios: " + $scope.unit.code, newJob.id).subscribe({ - next: (job) -> - fileDownloaderService.downloadFile($scope.unit.portfoliosUrl, "#{$scope.unit.code}-portfolios.zip") - error: (message) -> alertService.error(message, 6000) - }) - error: (message) -> alertService.error(message, 6000) - }) - - - $scope.studentFilter = 'allStudents' - $scope.portfolioFilter = 'withPortfolio' - - $scope.statusClass = newTaskService.statusClass - $scope.statusText = newTaskService.statusText - - refreshCharts = Visualisation.refreshAll - - # - # Sets the active tab - # - $scope.setActiveTab = (tab) -> - # Do nothing if we're switching to the same tab - return if tab is $scope.activeTab - $scope.activeTab?.active = false - $scope.activeTab = tab - $scope.activeTab.active = true - - if $scope.activeTab == $scope.tabs.viewProgress - refreshCharts() - - # - # Active task tab group - # - $scope.tabs = - selectStudent: - title: "Select Student" - subtitle: "Select the student to assess" - seq: 0 - viewProgress: - title: "View Progress" - subtitle: "See the progress of the student" - seq: 1 - viewStaffNotes: - title: "View Staff Notes" - subtitle: "See notes of the student add by staff" - seq: 2 - viewPortfolio: - title: "View Portfolio" - subtitle: "See the portfolio of the student" - seq: 3 - assessPortfolio: - title: "Assess Portfolio" - subtitle: "Enter a grade for the student" - seq: 4 - - $scope.setActiveTab($scope.tabs.selectStudent) - - $scope.tutor = newUserService.currentUser - - $scope.search = "" - - # Pagination details - $scope.currentPage = 1 - $scope.maxSize = 5 - $scope.pageSize = 10 - - $scope.filterOptions = {selectedGrade: -1} - $scope.gradeValues = gradeService.gradeValues - $scope.grades = gradeService.grades - $scope.gradeAcronyms = gradeService.gradeAcronyms - - $scope.selectedStudent = null - - $scope.gradeResults = [ - { - name: 'Fail', - scores: [ 0, 10, 20, 30, 40, 44 ] - } - { - name: 'Pass', - scores: [ 50, 53, 55, 57 ] - } - { - name: 'Credit', - scores: [ 60, 63, 65, 67 ] - } - { - name: 'Distinction', - scores: [ 70, 73, 75, 77 ] - } - { - name: 'High Distinction', - scores: [ 80, 83, 85, 87 ] - } - { - name: 'High Distinction', - scores: [ 90, 93, 95, 97, 100 ] - } - ] - - $scope.editingRationale = false - - $scope.toggleEditRationale = -> - $scope.editingRationale = !$scope.editingRationale - - - analyticsService.watchEvent $scope, 'studentFilter', 'Teacher View - Grading Tab' - analyticsService.watchEvent $scope, 'sortOrder', 'Teacher View - Grading Tab' - analyticsService.watchEvent $scope, 'currentPage', 'Teacher View - Grading Tab', 'Selected Page' - - $scope.selectStudent = (student) -> - $scope.selectedStudent = student - $scope.project = null - newProjectService.loadProject(student, $scope.unit).subscribe({ - next: (project) -> - $scope.project = project - $scope.project.preloadedUrl = $scope.project.portfolioUrl() - error: (message) -> alertService.error( message, 6000) - }) - - $scope.hasD2lMapping = -> - $scope.unit.hasD2lMapping() - - $scope.transferToD2L = -> - D2lTransferModal.open($scope.unit) - - $scope.openProject = ($event, project) -> - $event.stopPropagation() - # HACK: avoids using window.open() to prevent AngularJS error - link = document.createElement('a') - link.href = "/projects/#{project.id}/dashboard/?tutor=true" - link.target = '_blank' - link.click() -) diff --git a/src/app/units/states/portfolios/portfolios.component.html b/src/app/units/states/portfolios/portfolios.component.html new file mode 100644 index 0000000000..71316507f2 --- /dev/null +++ b/src/app/units/states/portfolios/portfolios.component.html @@ -0,0 +1,339 @@ +
No portfolios found
++ Showing {{ totalItems }} of {{ totalEnrolledStudents }} students enrolled. +
+ +| + Student + {{ sortReverse ? '↓' : '↑' }} + | ++ {{ row.student.studentId || row.student.username }} + | ++ Name + {{ sortReverse ? '↓' : '↑' }} + | ++ {{ row.student.name }} + | ++ Tutor + | ++ {{ row.tutorNames() }} + | ++ Tutorial + | ++ {{ row.shortTutorialDescription() }} + | ++ Target + | ++ + | ++ Submitted as + | ++ + | ++ Stats + | +
+
+
+
+ |
+ Portfolio? | ++ {{ (row.hasPortfolio || (row.portfolioStatus ?? 0) > 0) ? 'Yes' : 'No' }} + | +Grade | ++ {{ row.grade ?? '-' }} + | +
|---|
Loading student project…
+Progress through the unit's tasks.
+Loading student project…
+View internal notes recorded by staff about the student.
+Loading student project…
+View or download portfolio for assessment.
+No Portfolio Submitted
+Loading student project…
+Assign grade and rationale for this work.
+Select student to view portfolio and assign grade.
+No portfolios found
-| - - Student - - - | -- - Name - - - | -- - Tutor - - - | -- - Tutorial - - - | -- - Target - - - | -- - Submitted as - - - | -- - Submission Date - - - | -- - Stats - - | -- - Portfolio? - - - | -- - Grade - - - | -|
|---|---|---|---|---|---|---|---|---|---|---|
| {{student.student.studentId || student.student.username}} | -{{student.student.name}} | -{{student.tutorNames()}} | -{{student.shortTutorialDescription()}} | -
- |
-
- |
- {{student.portfolioSubmissionDate | date: 'EEE d MMM y, h:mm a'}} | -- - | -- {{student.hasPortfolio ? "Yes" : "No"}} - | -{{student.grade}} | -- - | -
Select student to view portfolio and assign grade
-No Portfolio Submitted
-- -
-