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 { 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
|
||||
|
|
Loading…
Reference in New Issue