From 2aa4a55d172f95a50a38dd6db29ec489a1e9db96 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 17 Apr 2018 13:16:22 +0200 Subject: [PATCH] MOBILE-2334 assign: Implement submission list and review pages --- .../submission-list/submission-list.html | 48 +++ .../submission-list/submission-list.module.ts | 33 +++ .../pages/submission-list/submission-list.ts | 273 ++++++++++++++++++ .../submission-review/submission-review.html | 22 ++ .../submission-review.module.ts | 35 +++ .../submission-review/submission-review.ts | 154 ++++++++++ 6 files changed, 565 insertions(+) create mode 100644 src/addon/mod/assign/pages/submission-list/submission-list.html create mode 100644 src/addon/mod/assign/pages/submission-list/submission-list.module.ts create mode 100644 src/addon/mod/assign/pages/submission-list/submission-list.ts create mode 100644 src/addon/mod/assign/pages/submission-review/submission-review.html create mode 100644 src/addon/mod/assign/pages/submission-review/submission-review.module.ts create mode 100644 src/addon/mod/assign/pages/submission-review/submission-review.ts diff --git a/src/addon/mod/assign/pages/submission-list/submission-list.html b/src/addon/mod/assign/pages/submission-list/submission-list.html new file mode 100644 index 000000000..5e33c96a7 --- /dev/null +++ b/src/addon/mod/assign/pages/submission-list/submission-list.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + +

{{submission.userfullname}}

+

{{ 'addon.mod_assign.hiddenuser' | translate }}{{submission.blindid}}

+

+ {{submission.groupname}} + {{ 'addon.mod_assign.noteam' | translate }} + {{ 'addon.mod_assign.multipleteams' | translate }} + {{ 'addon.mod_assign.defaultteam' | translate }} +

+ + {{ submission.statusTranslated }} + + + {{ submission.gradingStatusTranslationId | translate }} + +
+
+ + + + {{ 'addon.mod_assign.notallparticipantsareshown' | translate }} + +
+
+
+
diff --git a/src/addon/mod/assign/pages/submission-list/submission-list.module.ts b/src/addon/mod/assign/pages/submission-list/submission-list.module.ts new file mode 100644 index 000000000..19a54ad06 --- /dev/null +++ b/src/addon/mod/assign/pages/submission-list/submission-list.module.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonModAssignSubmissionListPage } from './submission-list'; + +@NgModule({ + declarations: [ + AddonModAssignSubmissionListPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(AddonModAssignSubmissionListPage), + TranslateModule.forChild() + ], +}) +export class AddonModAssignSubmissionListPageModule {} diff --git a/src/addon/mod/assign/pages/submission-list/submission-list.ts b/src/addon/mod/assign/pages/submission-list/submission-list.ts new file mode 100644 index 000000000..5f947416d --- /dev/null +++ b/src/addon/mod/assign/pages/submission-list/submission-list.ts @@ -0,0 +1,273 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { AddonModAssignProvider } from '../../providers/assign'; +import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; +import { AddonModAssignHelperProvider } from '../../providers/helper'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; + +/** + * Page that displays a list of submissions of an assignment. + */ +@IonicPage({ segment: 'addon-mod-assign-submission-list' }) +@Component({ + selector: 'page-addon-mod-assign-submission-list', + templateUrl: 'submission-list.html', +}) +export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { + @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; + + title: string; // Title to display. + assign: any; // Assignment. + submissions: any[]; // List of submissions + loaded: boolean; // Whether data has been loaded. + haveAllParticipants: boolean; // Whether all participants have been loaded. + selectedSubmissionId: number; // Selected submission ID. + + protected moduleId: number; // Module ID the submission belongs to. + protected courseId: number; // Course ID the assignment belongs to. + protected selectedStatus: string; // The status to see. + protected gradedObserver; // Observer to refresh data when a grade changes. + + constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, + protected domUtils: CoreDomUtilsProvider, protected translate: TranslateService, + protected assignProvider: AddonModAssignProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider, + protected assignHelper: AddonModAssignHelperProvider) { + + this.moduleId = navParams.get('moduleId'); + this.courseId = navParams.get('courseId'); + this.selectedStatus = navParams.get('status'); + + if (this.selectedStatus) { + if (this.selectedStatus == AddonModAssignProvider.NEED_GRADING) { + this.title = this.translate.instant('addon.mod_assign.numberofsubmissionsneedgrading'); + } else { + this.title = this.translate.instant('addon.mod_assign.submissionstatus_' + this.selectedStatus); + } + } else { + this.title = this.translate.instant('addon.mod_assign.numberofparticipants'); + } + + // Update data if some grade changes. + this.gradedObserver = eventsProvider.on(AddonModAssignProvider.GRADED_EVENT, (data) => { + if (this.assign && data.assignmentId == this.assign.id && data.userId == sitesProvider.getCurrentSiteUserId()) { + // Grade changed, refresh the data. + this.loaded = false; + + this.refreshAllData().finally(() => { + this.loaded = true; + }); + } + }, sitesProvider.getCurrentSiteId()); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.fetchAssignment().finally(() => { + if (!this.selectedSubmissionId && this.splitviewCtrl.isOn() && this.submissions.length > 0) { + // Take first and load it. + this.loadSubmission(this.submissions[0]); + } + + this.loaded = true; + }); + } + + /** + * Check if we can leave the page or not. + * + * @return {boolean|Promise} Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): boolean | Promise { + // If split view is enabled, check if we can leave the details page. + if (this.splitviewCtrl.isOn()) { + const detailsPage = this.splitviewCtrl.getDetailsNav().getActive().instance; + if (detailsPage && detailsPage.ionViewCanLeave) { + return detailsPage.ionViewCanLeave(); + } + } + + return true; + } + + /** + * Fetch assignment data. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchAssignment(): Promise { + let participants, + submissionsData; + + // Get assignment data. + return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assign) => { + this.title = assign.name || this.title; + this.assign = assign; + this.haveAllParticipants = true; + + // Get assignment submissions. + return this.assignProvider.getSubmissions(assign.id); + }).then((data) => { + if (!data.canviewsubmissions) { + // User shouldn't be able to reach here. + return Promise.reject(null); + } + + submissionsData = data; + + // Get the participants. + return this.assignHelper.getParticipants(this.assign).then((parts) => { + this.haveAllParticipants = true; + participants = parts; + }).catch(() => { + this.haveAllParticipants = false; + }); + }).then(() => { + // We want to show the user data on each submission. + return this.assignProvider.getSubmissionsUserData(submissionsData.submissions, this.courseId, this.assign.id, + this.assign.blindmarking && !this.assign.revealidentities, participants); + }).then((submissions) => { + + // Filter the submissions to get only the ones with the right status and add some extra data. + const getNeedGrading = this.selectedStatus == AddonModAssignProvider.NEED_GRADING, + searchStatus = getNeedGrading ? AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED : this.selectedStatus, + promises = []; + + this.submissions = []; + submissions.forEach((submission) => { + if (!searchStatus || searchStatus == submission.status) { + promises.push(this.assignOfflineProvider.getSubmissionGrade(this.assign.id, submission.userid).catch(() => { + // Ignore errors. + }).then((data) => { + let promise, + notSynced = false; + + // Load offline grades. + if (data && submission.timemodified < data.timemodified) { + notSynced = true; + } + + if (getNeedGrading) { + // Only show the submissions that need to be graded. + promise = this.assignProvider.needsSubmissionToBeGraded(submission, this.assign.id); + } else { + promise = Promise.resolve(true); + } + + return promise.then((add) => { + if (!add) { + return; + } + + submission.statusColor = this.assignProvider.getSubmissionStatusColor(submission.status); + submission.gradingColor = this.assignProvider.getSubmissionGradingStatusColor(submission.gradingstatus); + + // Show submission status if not submitted for grading. + if (submission.statusColor != 'success' || !submission.gradingstatus) { + submission.statusTranslated = this.translate.instant('addon.mod_assign.submissionstatus_' + + submission.status); + } else { + submission.statusTranslated = false; + } + + if (notSynced) { + submission.gradingStatusTranslationId = 'addon.mod_assign.gradenotsynced'; + submission.gradingColor = ''; + } else if (submission.statusColor != 'danger' || submission.gradingColor != 'danger') { + // Show grading status if one of the statuses is not done. + submission.gradingStatusTranslationId = + this.assignProvider.getSubmissionGradingStatusTranslationId(submission.gradingstatus); + } else { + submission.gradingStatusTranslationId = false; + } + + this.submissions.push(submission); + }); + })); + } + }); + + return Promise.all(promises); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.'); + }); + } + + /** + * Load a certain submission. + * + * @param {any} submission The submission to load. + */ + loadSubmission(submission: any): void { + if (this.selectedSubmissionId === submission.id) { + // Already selected. + return; + } + + this.selectedSubmissionId = submission.id; + + this.splitviewCtrl.push('AddonModAssignSubmissionReviewPage', { + courseId: this.courseId, + moduleId: this.moduleId, + submitId: submission.submitid, + blindId: submission.blindid + }); + } + + /** + * Refresh all the data. + * + * @return {Promise} Promise resolved when done. + */ + protected refreshAllData(): Promise { + const promises = []; + + promises.push(this.assignProvider.invalidateAssignmentData(this.courseId)); + if (this.assign) { + promises.push(this.assignProvider.invalidateAllSubmissionData(this.assign.id)); + promises.push(this.assignProvider.invalidateAssignmentUserMappingsData(this.assign.id)); + promises.push(this.assignProvider.invalidateListParticipantsData(this.assign.id)); + } + + return Promise.all(promises).finally(() => { + return this.fetchAssignment(); + }); + } + + /** + * Refresh the list. + * + * @param {any} refresher Refresher. + */ + refreshList(refresher: any): void { + this.refreshAllData().finally(() => { + refresher.complete(); + }); + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + this.gradedObserver && this.gradedObserver.off(); + } +} diff --git a/src/addon/mod/assign/pages/submission-review/submission-review.html b/src/addon/mod/assign/pages/submission-review/submission-review.html new file mode 100644 index 000000000..0deb67f29 --- /dev/null +++ b/src/addon/mod/assign/pages/submission-review/submission-review.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/addon/mod/assign/pages/submission-review/submission-review.module.ts b/src/addon/mod/assign/pages/submission-review/submission-review.module.ts new file mode 100644 index 000000000..788b4f527 --- /dev/null +++ b/src/addon/mod/assign/pages/submission-review/submission-review.module.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonModAssignComponentsModule } from '../../components/components.module'; +import { AddonModAssignSubmissionReviewPage } from './submission-review'; + +@NgModule({ + declarations: [ + AddonModAssignSubmissionReviewPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + AddonModAssignComponentsModule, + IonicPageModule.forChild(AddonModAssignSubmissionReviewPage), + TranslateModule.forChild() + ], +}) +export class AddonModAssignSubmissionReviewPageModule {} diff --git a/src/addon/mod/assign/pages/submission-review/submission-review.ts b/src/addon/mod/assign/pages/submission-review/submission-review.ts new file mode 100644 index 000000000..8fe64533d --- /dev/null +++ b/src/addon/mod/assign/pages/submission-review/submission-review.ts @@ -0,0 +1,154 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, ViewChild } from '@angular/core'; +import { IonicPage, NavController, NavParams } from 'ionic-angular'; +import { CoreAppProvider } from '@providers/app'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { AddonModAssignProvider } from '../../providers/assign'; +import { AddonModAssignSubmissionComponent } from '../../components/submission/submission'; + +/** + * Page that displays a submission. + */ +@IonicPage({ segment: 'addon-mod-assign-submission-review' }) +@Component({ + selector: 'page-addon-mod-assign-submission-review', + templateUrl: 'submission-review.html', +}) +export class AddonModAssignSubmissionReviewPage implements OnInit { + @ViewChild(AddonModAssignSubmissionComponent) submissionComponent: AddonModAssignSubmissionComponent; + + title: string; // Title to display. + moduleId: number; // Module ID the submission belongs to. + courseId: number; // Course ID the assignment belongs to. + submitId: number; // User that did the submission. + blindId: number; // Blinded user ID (if it's blinded). + showGrade: boolean; // Whether to display the grade at start. + loaded: boolean; // Whether data has been loaded. + canSaveGrades: boolean; // Whether the user can save grades. + + protected assign: any; // The assignment the submission belongs to. + protected blindMarking: boolean; // Whether it uses blind marking. + protected forceLeave = false; // To allow leaving the page without checking for changes. + + constructor(navParams: NavParams, protected navCtrl: NavController, protected courseProvider: CoreCourseProvider, + protected appProvider: CoreAppProvider, protected assignProvider: AddonModAssignProvider) { + + this.moduleId = navParams.get('moduleId'); + this.courseId = navParams.get('courseId'); + this.submitId = navParams.get('submitId'); + this.blindId = navParams.get('blindId'); + this.showGrade = !!navParams.get('showGrade'); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.fetchSubmission().finally(() => { + this.loaded = true; + }); + } + + /** + * Check if we can leave the page or not. + * + * @return {boolean|Promise} Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): boolean | Promise { + if (!this.submissionComponent || this.forceLeave) { + return true; + } + + // Check if data has changed. + return this.submissionComponent.canLeave(); + } + + /** + * Get the submission. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchSubmission(): Promise { + return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assignment) => { + this.assign = assignment; + this.title = this.assign.name; + + this.blindMarking = this.assign.blindmarking && !this.assign.revealidentities; + + return this.courseProvider.getModuleBasicGradeInfo(this.moduleId).then((gradeInfo) => { + if (gradeInfo) { + // Grades can be saved if simple grading. + if (gradeInfo.advancedgrading && gradeInfo.advancedgrading[0] && + typeof gradeInfo.advancedgrading[0].method != 'undefined') { + + const method = gradeInfo.advancedgrading[0].method || 'simple'; + this.canSaveGrades = method == 'simple'; + } else { + this.canSaveGrades = true; + } + } + }); + }); + } + + /** + * Refresh all the data. + * + * @return {Promise} Promise resolved when done. + */ + protected refreshAllData(): Promise { + const promises = []; + + promises.push(this.assignProvider.invalidateAssignmentData(this.courseId)); + if (this.assign) { + promises.push(this.assignProvider.invalidateSubmissionData(this.assign.id)); + promises.push(this.assignProvider.invalidateAssignmentUserMappingsData(this.assign.id)); + promises.push(this.assignProvider.invalidateSubmissionStatusData(this.assign.id, this.submitId, this.blindMarking)); + } + + return Promise.all(promises).finally(() => { + this.submissionComponent && this.submissionComponent.invalidateAndRefresh(); + + return this.fetchSubmission(); + }); + } + + /** + * Refresh the data. + * + * @param {any} refresher Refresher. + */ + refreshSubmission(refresher: any): void { + this.refreshAllData().finally(() => { + refresher.complete(); + }); + } + + /** + * Submit a grade and feedback. + */ + submitGrade(): void { + if (this.submissionComponent) { + this.submissionComponent.submitGrade().then(() => { + // Grade submitted, leave the view if not in tablet. + if (!this.appProvider.isWide()) { + this.forceLeave = true; + this.navCtrl.pop(); + } + }); + } + } +}