diff --git a/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.html b/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.html
new file mode 100644
index 000000000..68bb847bb
--- /dev/null
+++ b/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.html
@@ -0,0 +1,46 @@
+
{{ 'addon.mod_workshop.assessmentform' | translate }}
+
+
\ No newline at end of file
diff --git a/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts b/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts
new file mode 100644
index 000000000..6ce7c2a65
--- /dev/null
+++ b/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts
@@ -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} Promised resvoled when data is loaded.
+ */
+ protected load(): Promise {
+ 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} Promise resolved when done.
+ */
+ saveAssessment(): Promise {
+ 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);
+ }
+ }
+}
diff --git a/src/addon/mod/workshop/components/components.module.ts b/src/addon/mod/workshop/components/components.module.ts
index 0c1e07224..f8e20d0ad 100644
--- a/src/addon/mod/workshop/components/components.module.ts
+++ b/src/addon/mod/workshop/components/components.module.ts
@@ -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