MOBILE-2354 workshop: Assessment strategy component

main
Albert Gasset 2018-06-12 10:18:12 +02:00
parent 7e80c5c77b
commit f0c80085d8
3 changed files with 412 additions and 2 deletions

View File

@ -0,0 +1,46 @@
<h3 padding>{{ 'addon.mod_workshop.assessmentform' | translate }}</h3>
<form name="mma-mod_workshop-assessment-form">
<core-loading [hideUntil]="assessmentStrategyLoaded">
<ng-container *ngIf="componentClass && assessmentStrategyLoaded">
<core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component>
</ng-container>
<!-- This content will be replaced by the directive if any is applied. -->
<div class="core-info-card" *ngIf="notSupported">
{{ 'addon.mod_workshop.assessmentstrategynotsupported' | translate:{$a: strategy} }}
</div>
<ion-card *ngIf="assessmentStrategyLoaded && overallFeedkback && (edit || data.assessment.feedbackauthor || data.assessment.feedbackattachmentfiles.length) ">
<ion-item text-wrap>
<h2>{{ 'addon.mod_workshop.overallfeedback' | translate }}</h2>
</ion-item>
<ion-item stacked *ngIf="edit">
<ion-label stacked [core-mark-required]="overallFeedkbackRequired">{{ 'addon.mod_workshop.feedbackauthor' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="feedbackControl" (contentChanged)="onFeedbackChange($event)"></core-rich-text-editor>
<!-- @todo: Attributes that were passed to RTE in Ionic 1 but now they aren't supported yet:
[component]="component" [componentId]="workshop.coursemodule" -->
<core-input-errors item-content *ngIf="overallFeedkbackRequired && fieldErrors['feedbackauthor']" [errorText]="fieldErrors['feedbackauthor']"></core-input-errors>
</ion-item>
<core-attachments *ngIf="edit && workshop.overallfeedbackfiles" [files]="data.assessment.feedbackattachmentfiles" [maxSize]="workshop.overallfeedbackmaxbytes"
[maxSubmissions]="workshop.overallfeedbackfiles" [component]="component" [componentId]="componentId" [allowOffline]="true"></core-attachments>
<ion-item *ngIf="edit && access && access.canallocate">
<ion-label stacked [core-mark-required]="true">{{ 'addon.mod_workshop.assessmentweight' | translate }}</ion-label>
<ion-select [(ngModel)]="weight">
<ion-option *ngFor="let w of weights" [value]="w">{{w}}</ion-option>
</ion-select>
</ion-item>
<ion-item text-wrap *ngIf="!edit && data.assessment.feedbackauthor">
<core-format-text [component]="component" [componentId]="componentId" [text]="data.assessment.feedbackauthor"></core-format-text>
</ion-item>
<ion-item *ngIf="!edit && workshop.overallfeedbackfiles && data.assessment.feedbackattachmentfiles && data.assessment.feedbackattachmentfiles.length">
<ng-container *ngFor="let attachment of data.assessment.feedbackattachmentfiles">
<!-- Files already attached to the submission. -->
<core-file *ngIf="!attachment.name" [file]="attachment" [component]="component" [componentId]="componentId"></core-file>
<!-- Files stored in offline to be sent later. -->
<core-local-file *ngIf="attachment.name" [file]="attachment"></core-local-file>
</ng-container>
</ion-item>
</ion-card>
</core-loading>
</form>

View File

@ -0,0 +1,361 @@
// (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, Input, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { CoreSyncProvider } from '@providers/sync';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreEventsProvider } from '@providers/events';
import { CoreFileSessionProvider } from '@providers/file-session';
import { CoreSitesProvider } from '@providers/sites';
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
import { AddonModWorkshopProvider } from '../../providers/workshop';
import { AddonModWorkshopHelperProvider } from '../../providers/helper';
import { AddonModWorkshopOfflineProvider } from '../../providers/offline';
import { AddonWorkshopAssessmentStrategyDelegate } from '../../providers/assessment-strategy-delegate';
/**
* Component that displays workshop assessment strategy form.
*/
@Component({
selector: 'addon-mod-workshop-assessment-strategy',
templateUrl: 'assessment-strategy.html',
})
export class AddonModWorkshopAssessmentStrategyComponent implements OnInit {
@Input() workshop: any;
@Input() access: any;
@Input() assessmentId: number;
@Input() userId: number;
@Input() strategy: string;
@Input() edit?: boolean;
componentClass: any;
data = {
workshopId: 0,
assessment: null,
edit: false,
selectedValues: [],
fieldErrors: {},
};
assessmentStrategyLoaded = false;
notSupported = false;
feedbackText = '';
feedbackControl = new FormControl();
overallFeedkback = false;
overallFeedkbackRequired = false;
component = AddonModWorkshopProvider.COMPONENT;
componentId: number;
weights: any[];
weight: number;
protected rteEnabled: boolean;
protected obsInvalidated: any;
protected hasOffline: boolean;
protected originalData = {
text: '',
files: [],
weight: 1,
selectedValues: []
};
constructor(private translate: TranslateService,
private eventsProvider: CoreEventsProvider,
private fileSessionProvider: CoreFileSessionProvider,
private syncProvider: CoreSyncProvider,
private domUtils: CoreDomUtilsProvider,
private textUtils: CoreTextUtilsProvider,
private utils: CoreUtilsProvider,
private sitesProvider: CoreSitesProvider,
private uploaderProvider: CoreFileUploaderProvider,
private workshopProvider: AddonModWorkshopProvider,
private workshopHelper: AddonModWorkshopHelperProvider,
private workshopOffline: AddonModWorkshopOfflineProvider,
private strategyDelegate: AddonWorkshopAssessmentStrategyDelegate) {}
/**
* Component being initialized.
*/
ngOnInit(): void {
if (!this.assessmentId || !this.strategy) {
this.assessmentStrategyLoaded = true;
return;
}
this.data.workshopId = this.workshop.id;
this.data.edit = this.edit;
this.componentClass = this.strategyDelegate.getComponentForPlugin(this.strategy);
if (this.componentClass) {
this.overallFeedkback = !!this.workshop.overallfeedbackmode;
this.overallFeedkbackRequired = this.workshop.overallfeedbackmode == 2;
this.componentId = this.workshop.coursemodule;
// Load Weights selector.
if (this.edit && this.access.canallocate) {
this.weights = [];
for (let i = 16; i >= 0; i--) {
this.weights[i] = i;
}
}
let promise;
// Check if rich text editor is enabled.
if (this.edit) {
// Block the workshop.
this.syncProvider.blockOperation(AddonModWorkshopProvider.COMPONENT, this.workshop.id);
promise = this.domUtils.isRichTextEditorEnabled();
} else {
// We aren't editing, so no rich text editor.
promise = Promise.resolve(false);
}
promise.then((enabled) => {
this.rteEnabled = enabled;
return this.load();
}).then(() => {
this.obsInvalidated = this.eventsProvider.on(AddonModWorkshopProvider.ASSESSMENT_INVALIDATED,
this.load.bind(this), this.sitesProvider.getCurrentSiteId());
}).finally(() => {
this.assessmentStrategyLoaded = true;
});
} else {
// Helper data and fallback.
this.notSupported = !this.strategyDelegate.isPluginSupported(this.strategy);
this.assessmentStrategyLoaded = true;
}
}
/**
* Convenience function to load the assessment data.
*
* @return {Promise<any>} Promised resvoled when data is loaded.
*/
protected load(): Promise<any> {
return this.workshopHelper.getReviewerAssessmentById(this.workshop.id, this.assessmentId, this.userId)
.then((assessmentData) => {
this.data.assessment = assessmentData;
let promise;
if (this.edit) {
promise = this.workshopOffline.getAssessment(this.workshop.id, this.assessmentId).then((offlineAssessment) => {
const offlineData = offlineAssessment.inputdata;
this.hasOffline = true;
assessmentData.feedbackauthor = offlineData.feedbackauthor;
if (this.access.canallocate) {
assessmentData.weight = offlineData.weight;
}
// Override assessment plugins values.
assessmentData.form.current = this.workshopProvider.parseFields(
this.utils.objectToArrayOfObjects(offlineData, 'name', 'value'));
// Override offline files.
if (offlineData) {
return this.workshopHelper.getAssessmentFilesFromOfflineFilesObject(
offlineData.feedbackauthorattachmentsid, this.workshop.id, this.assessmentId)
.then((files) => {
assessmentData.feedbackattachmentfiles = files;
});
}
}).catch(() => {
this.hasOffline = false;
// Ignore errors.
}).finally(() => {
this.feedbackText = assessmentData.feedbackauthor;
this.feedbackControl.setValue(this.feedbackText);
this.originalData.text = this.data.assessment.feedbackauthor;
if (this.access.canallocate) {
this.originalData.weight = assessmentData.weight;
}
this.originalData.files = [];
assessmentData.feedbackattachmentfiles.forEach((file) => {
let filename;
if (file.filename) {
filename = file.filename;
} else {
// We don't have filename, extract it from the path.
filename = file.filepath[0] == '/' ? file.filepath.substr(1) : file.filepath;
}
this.originalData.files.push({
filename : filename,
fileurl: file.fileurl
});
});
});
} else {
promise = Promise.resolve();
}
return promise.then(() => {
return this.strategyDelegate.getOriginalValues(this.strategy, assessmentData.form, this.workshop.id)
.then((values) => {
this.data.selectedValues = values;
}).finally(() => {
this.originalData.selectedValues = this.utils.clone(this.data.selectedValues);
if (this.edit) {
this.fileSessionProvider.setFiles(AddonModWorkshopProvider.COMPONENT,
this.workshop.id + '_' + this.assessmentId, assessmentData.feedbackattachmentfiles);
if (this.access.canallocate) {
this.weight = assessmentData.weight;
}
}
});
});
});
}
/**
* Check if data has changed.
*
* @return {boolean} True if data has changed.
*/
hasDataChanged(): boolean {
if (!this.assessmentStrategyLoaded) {
return false;
}
// Compare feedback text.
const text = this.textUtils.restorePluginfileUrls(this.feedbackText, this.data.assessment.feedbackcontentfiles || []);
if (this.originalData.text != text) {
return true;
}
if (this.access.canallocate && this.originalData.weight != this.weight) {
return true;
}
// Compare feedback files.
const files = this.fileSessionProvider.getFiles(AddonModWorkshopProvider.COMPONENT,
this.workshop.id + '_' + this.assessmentId) || [];
if (this.uploaderProvider.areFileListDifferent(files, this.originalData.files)) {
return true;
}
return this.strategyDelegate.hasDataChanged(this.workshop, this.originalData.selectedValues, this.data.selectedValues);
}
/**
* Save the assessment.
*
* @return {Promise<any>} Promise resolved when done.
*/
saveAssessment(): Promise<any> {
const files = this.fileSessionProvider.getFiles(AddonModWorkshopProvider.COMPONENT,
this.workshop.id + '_' + this.assessmentId) || [];
let saveOffline = false;
let allowOffline = !files.length;
const modal = this.domUtils.showModalLoading('core.sending', true);
this.data.fieldErrors = {};
// Upload attachments first if any.
return this.workshopHelper.uploadOrStoreAssessmentFiles(this.workshop.id, this.assessmentId, files,
saveOffline).catch(() => {
// Cannot upload them in online, save them in offline.
saveOffline = true;
allowOffline = true;
return this.workshopHelper.uploadOrStoreAssessmentFiles(this.workshop.id, this.assessmentId, files, saveOffline);
}).then((attachmentsId) => {
const text = this.textUtils.restorePluginfileUrls(this.feedbackText, this.data.assessment.feedbackcontentfiles || []);
return this.workshopHelper.prepareAssessmentData(this.workshop, this.data.selectedValues, text, files,
this.data.assessment.form, attachmentsId).catch((errors) => {
this.data.fieldErrors = errors;
return Promise.reject(this.translate.instant('core.errorinvalidform'));
});
}).then((assessmentData) => {
if (saveOffline) {
// Save assessment in offline.
return this.workshopOffline.saveAssessment(this.workshop.id, this.assessmentId, this.workshop.course,
assessmentData).then(() => {
// Don't return anything.
});
}
// Try to send it to server.
// Don't allow offline if there are attachments since they were uploaded fine.
return this.workshopProvider.updateAssessment(this.workshop.id, this.assessmentId, this.workshop.course,
assessmentData, false, allowOffline);
}).then((grade) => {
const promises = [];
// If sent to the server, invalidate and clean.
if (grade) {
promises.push(this.workshopHelper.deleteAssessmentStoredFiles(this.workshop.id, this.assessmentId));
promises.push(this.workshopProvider.invalidateAssessmentFormData(this.workshop.id, this.assessmentId));
promises.push(this.workshopProvider.invalidateAssessmentData(this.workshop.id, this.assessmentId));
}
return Promise.all(promises).catch(() => {
// Ignore errors.
}).finally(() => {
this.eventsProvider.trigger(AddonModWorkshopProvider.ASSESSMENT_SAVED, {
workshopId: this.workshop.id,
assessmentId: this.assessmentId,
userId: this.sitesProvider.getCurrentSiteUserId(),
}, this.sitesProvider.getCurrentSiteId());
if (files) {
// Delete the local files from the tmp folder.
this.uploaderProvider.clearTmpFiles(files);
}
});
}).catch((message) => {
this.domUtils.showErrorModalDefault(message, 'Error saving assessment.');
return Promise.reject(null);
}).finally(() => {
modal.dismiss();
});
}
/**
* Feedback text changed.
*
* @param {string} text The new text.
*/
onFeedbackChange(text: string): void {
this.feedbackText = text;
}
/**
* Component destroyed.
*/
ngOnDestroy(): void {
this.obsInvalidated && this.obsInvalidated.off();
if (this.data.assessment.feedbackattachmentfiles) {
// Delete the local files from the tmp folder.
this.uploaderProvider.clearTmpFiles(this.data.assessment.feedbackattachmentfiles);
}
}
}

View File

@ -23,12 +23,14 @@ import { CoreCourseComponentsModule } from '@core/course/components/components.m
import { AddonModWorkshopIndexComponent } from './index/index';
import { AddonModWorkshopSubmissionComponent } from './submission/submission';
import { AddonModWorkshopAssessmentComponent } from './assessment/assessment';
import { AddonModWorkshopAssessmentStrategyComponent } from './assessment-strategy/assessment-strategy';
@NgModule({
declarations: [
AddonModWorkshopIndexComponent,
AddonModWorkshopSubmissionComponent,
AddonModWorkshopAssessmentComponent
AddonModWorkshopAssessmentComponent,
AddonModWorkshopAssessmentStrategyComponent
],
imports: [
CommonModule,
@ -44,7 +46,8 @@ import { AddonModWorkshopAssessmentComponent } from './assessment/assessment';
exports: [
AddonModWorkshopIndexComponent,
AddonModWorkshopSubmissionComponent,
AddonModWorkshopAssessmentComponent
AddonModWorkshopAssessmentComponent,
AddonModWorkshopAssessmentStrategyComponent
],
entryComponents: [
AddonModWorkshopIndexComponent