MOBILE-2354 workshop: Assessment strategy component
parent
7e80c5c77b
commit
f0c80085d8
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,12 +23,14 @@ import { CoreCourseComponentsModule } from '@core/course/components/components.m
|
||||||
import { AddonModWorkshopIndexComponent } from './index/index';
|
import { AddonModWorkshopIndexComponent } from './index/index';
|
||||||
import { AddonModWorkshopSubmissionComponent } from './submission/submission';
|
import { AddonModWorkshopSubmissionComponent } from './submission/submission';
|
||||||
import { AddonModWorkshopAssessmentComponent } from './assessment/assessment';
|
import { AddonModWorkshopAssessmentComponent } from './assessment/assessment';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyComponent } from './assessment-strategy/assessment-strategy';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AddonModWorkshopIndexComponent,
|
AddonModWorkshopIndexComponent,
|
||||||
AddonModWorkshopSubmissionComponent,
|
AddonModWorkshopSubmissionComponent,
|
||||||
AddonModWorkshopAssessmentComponent
|
AddonModWorkshopAssessmentComponent,
|
||||||
|
AddonModWorkshopAssessmentStrategyComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
@ -44,7 +46,8 @@ import { AddonModWorkshopAssessmentComponent } from './assessment/assessment';
|
||||||
exports: [
|
exports: [
|
||||||
AddonModWorkshopIndexComponent,
|
AddonModWorkshopIndexComponent,
|
||||||
AddonModWorkshopSubmissionComponent,
|
AddonModWorkshopSubmissionComponent,
|
||||||
AddonModWorkshopAssessmentComponent
|
AddonModWorkshopAssessmentComponent,
|
||||||
|
AddonModWorkshopAssessmentStrategyComponent
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
AddonModWorkshopIndexComponent
|
AddonModWorkshopIndexComponent
|
||||||
|
|
Loading…
Reference in New Issue