MOBILE-2334 assign: Implement submission list and review pages

main
Dani Palou 2018-04-17 13:16:22 +02:00
parent bb6f4b21e7
commit 2aa4a55d17
6 changed files with 565 additions and 0 deletions

View File

@ -0,0 +1,48 @@
<ion-header>
<ion-navbar>
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
<ion-buttons end></ion-buttons>
</ion-navbar>
</ion-header>
<core-split-view>
<ion-content>
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshList($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<core-empty-box *ngIf="!submissions || submissions.length == 0" icon="paper" [message]="'addon.mod_assign.submissionstatus_' | translate">
</core-empty-box>
<ion-list>
<!-- List of submissions. -->
<ng-container *ngFor="let submission of submissions">
<a ion-item text-wrap (click)="loadSubmission(submission)" [class.core-split-item-selected]="submission.id == selectedSubmissionId">
<ion-avatar item-start *ngIf="submission.userprofileimageurl">
<img [src]="submission.userprofileimageurl" [alt]="'core.pictureof' | translate:{$a: submission.userfullname}" core-external-content role="presentation" onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<h2 *ngIf="submission.userfullname">{{submission.userfullname}}</h2>
<h2 *ngIf="!submission.userfullname">{{ 'addon.mod_assign.hiddenuser' | translate }}{{submission.blindid}}</h2>
<p *ngIf="assign.teamsubmission">
<span *ngIf="submission.groupname">{{submission.groupname}}</span>
<span *ngIf="assign.preventsubmissionnotingroup && !submission.groupname && !submission.manyGroups && !submission.blindid">{{ 'addon.mod_assign.noteam' | translate }}</span>
<span *ngIf="assign.preventsubmissionnotingroup && !submission.groupname && submission.manyGroups && !submission.blindid">{{ 'addon.mod_assign.multipleteams' | translate }}</span>
<span *ngIf="!assign.preventsubmissionnotingroup && !submission.groupname">{{ 'addon.mod_assign.defaultteam' | translate }}</span>
</p>
<ion-badge text-center [color]="submission.statusColor" *ngIf="submission.statusTranslated">
{{ submission.statusTranslated }}
</ion-badge>
<ion-badge text-center [color]="submission.gradingColor" *ngIf="submission.gradingStatusTranslationId">
{{ submission.gradingStatusTranslationId | translate }}
</ion-badge>
</a>
</ng-container>
<ion-item text-wrap class="core-warning-card" *ngIf="!haveAllParticipants" icon-start>
<ion-icon name="warning"></ion-icon>
{{ 'addon.mod_assign.notallparticipantsareshown' | translate }}
</ion-item>
</ion-list>
</core-loading>
</ion-content>
</core-split-view>

View File

@ -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 {}

View File

@ -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<void>} Resolved if we can leave it, rejected if not.
*/
ionViewCanLeave(): boolean | Promise<void> {
// 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<any>} Promise resolved when done.
*/
protected fetchAssignment(): Promise<any> {
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<any>} Promise resolved when done.
*/
protected refreshAllData(): Promise<any> {
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();
}
}

View File

@ -0,0 +1,22 @@
<ion-header>
<ion-navbar>
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
<ion-buttons end></ion-buttons>
</ion-navbar>
<core-navbar-buttons end>
<button [hidden]="!canSaveGrades" ion-button button-clear (click)="submitGrade()" [attr.aria-label]="'core.done' | translate">
{{ 'core.done' | translate }}
</button>
</core-navbar-buttons>
</ion-header>
<ion-content>
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshSubmission($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<addon-mod-assign-submission [courseId]="courseId" [moduleId]="moduleId" [submitId]="submitId" [blindId]="blindId" [showGrade]="showGrade"></addon-mod-assign-submission>
</core-loading>
</ion-content>

View File

@ -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 {}

View File

@ -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<void>} Resolved if we can leave it, rejected if not.
*/
ionViewCanLeave(): boolean | Promise<void> {
if (!this.submissionComponent || this.forceLeave) {
return true;
}
// Check if data has changed.
return this.submissionComponent.canLeave();
}
/**
* Get the submission.
*
* @return {Promise<any>} Promise resolved when done.
*/
protected fetchSubmission(): Promise<any> {
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<any>} Promise resolved when done.
*/
protected refreshAllData(): Promise<any> {
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();
}
});
}
}
}