From 349df95d7ee38f3896f734fa4ac78ef1c5166ed6 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 30 May 2018 11:32:54 +0200 Subject: [PATCH 01/20] MOBILE-2354: Fix missing Promise.reject when using createFakeWSError --- src/addon/messages/providers/messages.ts | 2 +- src/addon/mod/feedback/providers/feedback.ts | 2 +- src/addon/mod/forum/providers/forum.ts | 4 ++-- src/addon/mod/glossary/providers/glossary.ts | 2 +- src/addon/mod/survey/providers/survey.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/addon/messages/providers/messages.ts b/src/addon/messages/providers/messages.ts index 19ece296b..4957047d4 100644 --- a/src/addon/messages/providers/messages.ts +++ b/src/addon/messages/providers/messages.ts @@ -1033,7 +1033,7 @@ export class AddonMessagesProvider { return this.sendMessagesOnline(messages, siteId).then((response) => { if (response && response[0] && response[0].msgid === -1) { // There was an error, and it should be translated already. - return this.utils.createFakeWSError(response[0].errormessage); + return Promise.reject(this.utils.createFakeWSError(response[0].errormessage)); } return this.invalidateDiscussionCache(toUserId, siteId).catch(() => { diff --git a/src/addon/mod/feedback/providers/feedback.ts b/src/addon/mod/feedback/providers/feedback.ts index c28d69193..3fcb0099e 100644 --- a/src/addon/mod/feedback/providers/feedback.ts +++ b/src/addon/mod/feedback/providers/feedback.ts @@ -1189,7 +1189,7 @@ export class AddonModFeedbackProvider { }; return site.write('mod_feedback_process_page', params).catch((error) => { - return this.utils.createFakeWSError(error); + return Promise.reject(this.utils.createFakeWSError(error)); }).then((response) => { // Invalidate and update current values because they will change. return this.invalidateCurrentValuesData(feedbackId, site.getId()).then(() => { diff --git a/src/addon/mod/forum/providers/forum.ts b/src/addon/mod/forum/providers/forum.ts index 37f36b139..cdbe517d3 100644 --- a/src/addon/mod/forum/providers/forum.ts +++ b/src/addon/mod/forum/providers/forum.ts @@ -179,7 +179,7 @@ export class AddonModForumProvider { return site.write('mod_forum_add_discussion', params).then((response) => { // Other errors ocurring. if (!response || !response.discussionid) { - return this.utils.createFakeWSError(''); + return Promise.reject(this.utils.createFakeWSError('')); } else { return response.discussionid; } @@ -694,7 +694,7 @@ export class AddonModForumProvider { return site.write('mod_forum_add_discussion_post', params).then((response) => { if (!response || !response.postid) { - return this.utils.createFakeWSError(''); + return Promise.reject(this.utils.createFakeWSError('')); } else { return response.postid; } diff --git a/src/addon/mod/glossary/providers/glossary.ts b/src/addon/mod/glossary/providers/glossary.ts index 943e91532..990f33cdd 100644 --- a/src/addon/mod/glossary/providers/glossary.ts +++ b/src/addon/mod/glossary/providers/glossary.ts @@ -812,7 +812,7 @@ export class AddonModGlossaryProvider { return response.entryid; } - return this.utils.createFakeWSError(''); + return Promise.reject(this.utils.createFakeWSError('')); }); }); } diff --git a/src/addon/mod/survey/providers/survey.ts b/src/addon/mod/survey/providers/survey.ts index 70c92dd03..a7ae0337d 100644 --- a/src/addon/mod/survey/providers/survey.ts +++ b/src/addon/mod/survey/providers/survey.ts @@ -268,7 +268,7 @@ export class AddonModSurveyProvider { return site.write('mod_survey_submit_answers', params).then((response) => { if (!response.status) { - return this.utils.createFakeWSError(''); + return Promise.reject(this.utils.createFakeWSError('')); } }); }); From c580f86bc1c8950299091f17265c0be1e280d14e Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 11 Jun 2018 15:05:35 +0200 Subject: [PATCH 02/20] MOBILE-2354 core: Fixed initial textarea resize in auto-rows directive --- src/directives/auto-rows.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/directives/auto-rows.ts b/src/directives/auto-rows.ts index 3e7a24291..3160c00f1 100644 --- a/src/directives/auto-rows.ts +++ b/src/directives/auto-rows.ts @@ -47,8 +47,11 @@ export class CoreAutoRowsDirective { /** * Resize after content. */ - ngAfterViewContent(): void { - this.resize(); + ngAfterViewInit(): void { + // Wait for rendering of child views. + setTimeout(() => { + this.resize(); + }, 300); } /** From daeb78489f1b66e5246126f1d6e120e395bd12c3 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 1 Jun 2018 15:47:46 +0200 Subject: [PATCH 03/20] MOBILE-2354 core: Implement makeGradesMenu method in grades helper --- src/core/grades/providers/helper.ts | 54 ++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/core/grades/providers/helper.ts b/src/core/grades/providers/helper.ts index 4f40b6caf..c7c7a62a1 100644 --- a/src/core/grades/providers/helper.ts +++ b/src/core/grades/providers/helper.ts @@ -20,6 +20,7 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreGradesProvider } from './grades'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; /** @@ -32,7 +33,7 @@ export class CoreGradesHelperProvider { constructor(logger: CoreLoggerProvider, private coursesProvider: CoreCoursesProvider, private gradesProvider: CoreGradesProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, private courseProvider: CoreCourseProvider, - private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider) { + private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private utils: CoreUtilsProvider) { this.logger = logger.getInstance('CoreGradesHelperProvider'); } @@ -446,4 +447,55 @@ export class CoreGradesHelperProvider { return row; } + + /** + * Creates an array that represents all the current grades that can be chosen using the given grading type. + * Negative numbers are scales, zero is no grade, and positive numbers are maximum grades. + * + * Taken from make_grades_menu on moodlelib.php + * + * @param {number} gradingType If positive, max grade you can provide. If negative, scale Id. + * @param {number} moduleId Module Id needed to retrieve the scale. + * @param {string} [defaultLabel] Element that will become default option, if not defined, it won't be added. + * @param {any} [defaultValue] Element that will become default option value. Default ''. + * @param {string} [scale] Scale csv list String. If not provided, it will take it from the module grade info. + * @return {Promise} Array with objects with value and label to create a propper HTML select. + */ + makeGradesMenu(gradingType: number, moduleId: number, defaultLabel: string = '', defaultValue: any = '', scale?: string): + Promise { + if (gradingType < 0) { + if (scale) { + return Promise.resolve(this.utils.makeMenuFromList(scale, defaultLabel, undefined, defaultValue)); + } else { + return this.courseProvider.getModuleBasicGradeInfo(moduleId).then((gradeInfo) => { + if (gradeInfo.scale) { + return this.utils.makeMenuFromList(gradeInfo.scale, defaultLabel, undefined, defaultValue); + } + + return []; + }); + } + } + + if (gradingType > 0) { + const grades = []; + if (defaultLabel) { + // Key as string to avoid resorting of the object. + grades.push({ + label: defaultLabel, + value: defaultValue + }); + } + for (let i = gradingType; i >= 0; i--) { + grades.push({ + label: i + ' / ' + gradingType, + value: i + }); + } + + return Promise.resolve(grades); + } + + return Promise.resolve([]); + } } From f90e699ac31b213bc716a84fc6a5b912415ab88e Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 30 May 2018 11:33:29 +0200 Subject: [PATCH 04/20] MOBILE-2354 workshop: Workshop and offline providers --- src/addon/mod/workshop/providers/offline.ts | 790 ++++++++++ src/addon/mod/workshop/providers/workshop.ts | 1378 ++++++++++++++++++ 2 files changed, 2168 insertions(+) create mode 100644 src/addon/mod/workshop/providers/offline.ts create mode 100644 src/addon/mod/workshop/providers/workshop.ts diff --git a/src/addon/mod/workshop/providers/offline.ts b/src/addon/mod/workshop/providers/offline.ts new file mode 100644 index 000000000..6ac09becb --- /dev/null +++ b/src/addon/mod/workshop/providers/offline.ts @@ -0,0 +1,790 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreFileProvider } from '@providers/file'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; + +/** + * Service to handle offline workshop. + */ +@Injectable() +export class AddonModWorkshopOfflineProvider { + + // Variables for database. + protected SUBMISSIONS_TABLE = 'addon_mod_workshop_submissions'; + protected ASSESSMENTS_TABLE = 'addon_mod_workshop_assessments'; + protected EVALUATE_SUBMISSIONS_TABLE = 'addon_mod_workshop_evaluate_submissions'; + protected EVALUATE_ASSESSMENTS_TABLE = 'addon_mod_workshop_evaluate_assessments'; + + protected tablesSchema = [ + { + name: this.SUBMISSIONS_TABLE, + columns: [ + { + name: 'workshopid', + type: 'INTEGER', + }, + { + name: 'submissionid', + type: 'INTEGER', + }, + { + name: 'action', + type: 'TEXT', + }, + { + name: 'courseid', + type: 'INTEGER', + }, + { + name: 'title', + type: 'TEXT', + }, + { + name: 'content', + type: 'TEXT', + }, + { + name: 'attachmentsid', + type: 'TEXT', + }, + { + name: 'timemodified', + type: 'INTEGER', + } + ], + primaryKeys: ['workshopid', 'submissionid', 'action'] + }, + { + name: this.ASSESSMENTS_TABLE, + columns: [ + { + name: 'workshopid', + type: 'INTEGER', + }, + { + name: 'assessmentid', + type: 'INTEGER', + }, + { + name: 'courseid', + type: 'INTEGER', + }, + { + name: 'inputdata', + type: 'TEXT', + }, + { + name: 'timemodified', + type: 'INTEGER', + }, + ], + primaryKeys: ['workshopid', 'assessmentid'] + }, + { + name: this.EVALUATE_SUBMISSIONS_TABLE, + columns: [ + { + name: 'workshopid', + type: 'INTEGER', + }, + { + name: 'submissionid', + type: 'INTEGER', + }, + { + name: 'courseid', + type: 'INTEGER', + }, + { + name: 'timemodified', + type: 'INTEGER', + }, + { + name: 'feedbacktext', + type: 'TEXT', + }, + { + name: 'published', + type: 'INTEGER', + }, + { + name: 'gradeover', + type: 'TEXT', + }, + ], + primaryKeys: ['workshopid', 'submissionid'] + }, + { + name: this.EVALUATE_ASSESSMENTS_TABLE, + columns: [ + { + name: 'workshopid', + type: 'INTEGER', + }, + { + name: 'assessmentid', + type: 'INTEGER', + }, + { + name: 'courseid', + type: 'INTEGER', + }, + { + name: 'timemodified', + type: 'INTEGER', + }, + { + name: 'feedbacktext', + type: 'TEXT', + }, + { + name: 'weight', + type: 'INTEGER', + }, + { + name: 'gradinggradeover', + type: 'TEXT', + }, + ], + primaryKeys: ['workshopid', 'assessmentid'] + } + ]; + + constructor(private fileProvider: CoreFileProvider, + private sitesProvider: CoreSitesProvider, + private textUtils: CoreTextUtilsProvider, + private timeUtils: CoreTimeUtilsProvider) { + this.sitesProvider.createTablesFromSchema(this.tablesSchema); + } + + /** + * Get all the workshops ids that have something to be synced. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with workshops id that have something to be synced. + */ + getAllWorkshops(siteId?: string): Promise { + const promises = [ + this.getAllSubmissions(siteId), + this.getAllAssessments(siteId), + this.getAllEvaluateSubmissions(siteId), + this.getAllEvaluateAssessments(siteId) + ]; + + return Promise.all(promises).then((promiseResults) => { + const workshopIds = {}; + + // Get workshops from any offline object all should have workshopid. + promiseResults.forEach((offlineObjects) => { + offlineObjects.forEach((offlineObject) => { + workshopIds[offlineObject.workshopid] = true; + }); + }); + + return Object.keys(workshopIds).map((workshopId) => parseInt(workshopId, 10)); + }); + } + + /** + * Check if there is an offline data to be synced. + * + * @param {number} workshopId Workshop ID to remove. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if has offline data, false otherwise. + */ + hasWorkshopOfflineData(workshopId: number, siteId?: string): Promise { + const promises = [ + this.getSubmissions(workshopId, siteId), + this.getAssessments(workshopId, siteId), + this.getEvaluateSubmissions(workshopId, siteId), + this.getEvaluateAssessments(workshopId, siteId) + ]; + + return Promise.all(promises).then((results) => { + return results.some((result) => result && result.length); + }).catch(() => { + // No offline data found. + return false; + }); + } + + /** + * Delete workshop submission action. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId Submission ID. + * @param {string} action Action to be done. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + deleteSubmissionAction(workshopId: number, submissionId: number, action: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + workshopid: workshopId, + submissionid: submissionId, + action: action + }; + + return site.getDb().deleteRecords(this.SUBMISSIONS_TABLE, conditions); + }); + } + + /** + * Delete all workshop submission actions. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId Submission ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + deleteAllSubmissionActions(workshopId: number, submissionId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + workshopid: workshopId, + submissionid: submissionId, + }; + + return site.getDb().deleteRecords(this.SUBMISSIONS_TABLE, conditions); + }); + } + + /** + * Get the all the submissions to be synced. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the objects to be synced. + */ + getAllSubmissions(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(this.SUBMISSIONS_TABLE).then((records) => { + records.forEach(this.parseSubmissionRecord.bind(this)); + + return records; + }); + }); + } + + /** + * Get the submissions of a workshop to be synced. + * + * @param {number} workshopId ID of the workshop. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the object to be synced. + */ + getSubmissions(workshopId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + workshopid: workshopId + }; + + return site.getDb().getRecords(this.SUBMISSIONS_TABLE, conditions).then((records) => { + records.forEach(this.parseSubmissionRecord.bind(this)); + + return records; + }); + }); + } + + /** + * Get all actions of a submission of a workshop to be synced. + * + * @param {number} workshopId ID of the workshop. + * @param {number} submissionId ID of the submission. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the object to be synced. + */ + getSubmissionActions(workshopId: number, submissionId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + workshopid: workshopId, + submissionid: submissionId + }; + + return site.getDb().getRecords(this.SUBMISSIONS_TABLE, conditions).then((records) => { + records.forEach(this.parseSubmissionRecord.bind(this)); + + return records; + }); + }); + } + + /** + * Get an specific action of a submission of a workshop to be synced. + * + * @param {number} workshopId ID of the workshop. + * @param {number} submissionId ID of the submission. + * @param {string} action Action to be done. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the object to be synced. + */ + getSubmissionAction(workshopId: number, submissionId: number, action: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + workshopid: workshopId, + submissionid: submissionId, + action: action + }; + + return site.getDb().getRecord(this.SUBMISSIONS_TABLE, conditions).then((record) => { + this.parseSubmissionRecord(record); + + return record; + }); + }); + } + + /** + * Offline version for adding a submission action to a workshop. + * + * @param {number} workshopId Workshop ID. + * @param {number} courseId Course ID the workshop belongs to. + * @param {string} title The submission title. + * @param {string} content The submission text content. + * @param {any} attachmentsId Stored attachments. + * @param {number} submissionId Submission Id, if action is add, the time the submission was created. + * If set to 0, current time is used. + * @param {string} action Action to be done. ['add', 'update', 'delete'] + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when submission action is successfully saved. + */ + saveSubmission(workshopId: number, courseId: number, title: string, content: string, attachmentsId: any, + submissionId: number, action: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const timemodified = this.timeUtils.timestamp(); + const assessment = { + workshopid: workshopId, + courseid: courseId, + title: title, + content: content, + attachmentsid: JSON.stringify(attachmentsId), + action: action, + submissionid: submissionId ? submissionId : -timemodified, + timemodified: timemodified + }; + + return site.getDb().insertRecord(this.SUBMISSIONS_TABLE, assessment); + }); + } + + /** + * Parse "attachments" column of a submission record. + * + * @param {any} record Submission record, modified in place. + */ + protected parseSubmissionRecord(record: any): void { + record.attachmentsid = this.textUtils.parseJSON(record.attachmentsid); + } + + /** + * Delete workshop assessment. + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId Assessment ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + deleteAssessment(workshopId: number, assessmentId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + workshopid: workshopId, + assessmentid: assessmentId + }; + + return site.getDb().deleteRecords(this.ASSESSMENTS_TABLE, conditions); + }); + } + + /** + * Get the all the assessments to be synced. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the objects to be synced. + */ + getAllAssessments(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(this.ASSESSMENTS_TABLE).then((records) => { + records.forEach(this.parseAssessnentRecord.bind(this)); + + return records; + }); + }); + } + + /** + * Get the assessments of a workshop to be synced. + * + * @param {number} workshopId ID of the workshop. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the object to be synced. + */ + getAssessments(workshopId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + workshopid: workshopId + }; + + return site.getDb().getRecords(this.ASSESSMENTS_TABLE, conditions).then((records) => { + records.forEach(this.parseAssessnentRecord.bind(this)); + + return records; + }); + }); + } + + /** + * Get an specific assessment of a workshop to be synced. + * + * @param {number} workshopId ID of the workshop. + * @param {number} assessmentId Assessment ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the object to be synced. + */ + getAssessment(workshopId: number, assessmentId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + workshopid: workshopId, + assessmentid: assessmentId + }; + + return site.getDb().getRecord(this.ASSESSMENTS_TABLE, conditions).then((record) => { + this.parseAssessnentRecord(record); + + return record; + }); + }); + } + + /** + * Offline version for adding an assessment to a workshop. + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId Assessment ID. + * @param {number} courseId Course ID the workshop belongs to. + * @param {any} inputData Assessment data. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when assessment is successfully saved. + */ + saveAssessment(workshopId: number, assessmentId: number, courseId: number, inputData: any, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const assessment = { + workshopid: workshopId, + courseid: courseId, + inputdata: JSON.stringify(inputData), + assessmentid: assessmentId, + timemodified: this.timeUtils.timestamp() + }; + + return site.getDb().insertRecord(this.ASSESSMENTS_TABLE, assessment); + }); + } + + /** + * Parse "inpudata" column of an assessment record. + * + * @param {any} record Assessnent record, modified in place. + */ + protected parseAssessnentRecord(record: any): void { + record.inputdata = this.textUtils.parseJSON(record.inputdata); + } + + /** + * Delete workshop evaluate submission. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId Submission ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + deleteEvaluateSubmission(workshopId: number, submissionId: number, siteId?: string): Promise { + const conditions = { + workshopid: workshopId, + submissionid: submissionId + }; + + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().deleteRecords(this.EVALUATE_SUBMISSIONS_TABLE, conditions); + }); + } + + /** + * Get the all the evaluate submissions to be synced. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the objects to be synced. + */ + getAllEvaluateSubmissions(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(this.EVALUATE_SUBMISSIONS_TABLE).then((records) => { + records.forEach(this.parseEvaluateSubmissionRecord.bind(this)); + + return records; + }); + }); + } + + /** + * Get the evaluate submissions of a workshop to be synced. + * + * @param {number} workshopId ID of the workshop. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the object to be synced. + */ + getEvaluateSubmissions(workshopId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + workshopid: workshopId + }; + + return site.getDb().getRecords(this.EVALUATE_SUBMISSIONS_TABLE, conditions).then((records) => { + records.forEach(this.parseEvaluateSubmissionRecord.bind(this)); + + return records; + }); + }); + } + + /** + * Get an specific evaluate submission of a workshop to be synced. + * + * @param {number} workshopId ID of the workshop. + * @param {number} submissionId Submission ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the object to be synced. + */ + getEvaluateSubmission(workshopId: number, submissionId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + workshopid: workshopId, + submissionid: submissionId + }; + + return site.getDb().getRecord(this.EVALUATE_SUBMISSIONS_TABLE, conditions).then((record) => { + this.parseEvaluateSubmissionRecord(record); + + return record; + }); + }); + } + + /** + * Offline version for evaluation a submission to a workshop. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId Submission ID. + * @param {number} courseId Course ID the workshop belongs to. + * @param {string} feedbackText The feedback for the author. + * @param {boolean} published Whether to publish the submission for other users. + * @param {any} gradeOver The new submission grade (empty for no overriding the grade). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when submission evaluation is successfully saved. + */ + saveEvaluateSubmission(workshopId: number, submissionId: number, courseId: number, feedbackText: string, published: boolean, + gradeOver: any, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const submission = { + workshopid: workshopId, + courseid: courseId, + submissionid: submissionId, + timemodified: this.timeUtils.timestamp(), + feedbacktext: feedbackText, + published: Number(published), + gradeover: JSON.stringify(gradeOver) + }; + + return site.getDb().insertRecord(this.EVALUATE_SUBMISSIONS_TABLE, submission); + }); + } + + /** + * Parse "published" and "gradeover" columns of an evaluate submission record. + * + * @param {any} record Evaluate submission record, modified in place. + */ + protected parseEvaluateSubmissionRecord(record: any): void { + record.published = Boolean(record.published); + record.gradeover = this.textUtils.parseJSON(record.gradeover); + } + + /** + * Delete workshop evaluate assessment. + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId Assessment ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + deleteEvaluateAssessment(workshopId: number, assessmentId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + workshopid: workshopId, + assessmentid: assessmentId + }; + + return site.getDb().deleteRecords(this.EVALUATE_ASSESSMENTS_TABLE, conditions); + }); + } + + /** + * Get the all the evaluate assessments to be synced. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the objects to be synced. + */ + getAllEvaluateAssessments(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(this.EVALUATE_ASSESSMENTS_TABLE).then((records) => { + records.forEach(this.parseEvaluateAssessmentRecord.bind(this)); + + return records; + }); + }); + } + + /** + * Get the evaluate assessments of a workshop to be synced. + * + * @param {number} workshopId ID of the workshop. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the object to be synced. + */ + getEvaluateAssessments(workshopId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + workshopid: workshopId + }; + + return site.getDb().getRecords(this.EVALUATE_ASSESSMENTS_TABLE, conditions).then((records) => { + records.forEach(this.parseEvaluateAssessmentRecord.bind(this)); + + return records; + }); + }); + } + + /** + * Get an specific evaluate assessment of a workshop to be synced. + * + * @param {number} workshopId ID of the workshop. + * @param {number} assessmentId Assessment ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the object to be synced. + */ + getEvaluateAssessment(workshopId: number, assessmentId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + workshopid: workshopId, + assessmentid: assessmentId + }; + + return site.getDb().getRecord(this.EVALUATE_ASSESSMENTS_TABLE, conditions).then((record) => { + this.parseEvaluateAssessmentRecord(record); + + return record; + }); + }); + } + + /** + * Offline version for evaluating an assessment to a workshop. + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId Assessment ID. + * @param {number} courseId Course ID the workshop belongs to. + * @param {string} feedbackText The feedback for the reviewer. + * @param {number} weight The new weight for the assessment. + * @param {any} gradingGradeOver The new grading grade (empty for no overriding the grade). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when assessment evaluation is successfully saved. + */ + saveEvaluateAssessment(workshopId: number, assessmentId: number, courseId: number, feedbackText: string, weight: number, + gradingGradeOver: any, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const assessment = { + workshopid: workshopId, + courseid: courseId, + assessmentid: assessmentId, + timemodified: this.timeUtils.timestamp(), + feedbacktext: feedbackText, + weight: weight, + gradinggradeover: JSON.stringify(gradingGradeOver) + }; + + return site.getDb().insertRecord(this.EVALUATE_ASSESSMENTS_TABLE, assessment); + }); + } + + /** + * Parse "gradinggradeover" column of an evaluate assessment record. + * + * @param {any} record Evaluate assessment record, modified in place. + */ + protected parseEvaluateAssessmentRecord(record: any): void { + record.gradinggradeover = this.textUtils.parseJSON(record.gradinggradeover); + } + + /** + * Get the path to the folder where to store files for offline attachments in a workshop. + * + * @param {number} workshopId Workshop ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the path. + */ + getWorkshopFolder(workshopId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + + const siteFolderPath = this.fileProvider.getSiteFolder(site.getId()); + const workshopFolderPath = 'offlineworkshop/' + workshopId + '/'; + + return this.textUtils.concatenatePaths(siteFolderPath, workshopFolderPath); + }); + } + + /** + * Get the path to the folder where to store files for offline submissions. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId If not editing, it will refer to timecreated. + * @param {boolean} editing If the submission is being edited or added otherwise. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the path. + */ + getSubmissionFolder(workshopId: number, submissionId: number, editing: boolean, siteId?: string): Promise { + return this.getWorkshopFolder(workshopId, siteId).then((folderPath) => { + folderPath += 'submission/'; + const folder = editing ? 'update_' + submissionId : 'add'; + + return this.textUtils.concatenatePaths(folderPath, folder); + }); + } + + /** + * Get the path to the folder where to store files for offline assessment. + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId Assessment ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the path. + */ + getAssessmentFolder(workshopId: number, assessmentId: number, siteId?: string): Promise { + return this.getWorkshopFolder(workshopId, siteId).then((folderPath) => { + folderPath += 'assessment/'; + + return this.textUtils.concatenatePaths(folderPath, String(assessmentId)); + }); + } +} diff --git a/src/addon/mod/workshop/providers/workshop.ts b/src/addon/mod/workshop/providers/workshop.ts new file mode 100644 index 000000000..5ce7d3a79 --- /dev/null +++ b/src/addon/mod/workshop/providers/workshop.ts @@ -0,0 +1,1378 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreAppProvider } from '@providers/app'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { AddonModWorkshopOfflineProvider } from './offline'; + +/** + * Service that provides some features for workshops. + */ +@Injectable() +export class AddonModWorkshopProvider { + static COMPONENT = 'mmaWorkshopUrl'; + static PER_PAGE = 10; + static PHASE_SETUP = 10; + static PHASE_SUBMISSION = 20; + static PHASE_ASSESSMENT = 30; + static PHASE_EVALUATION = 40; + static PHASE_CLOSED = 50; + static EXAMPLES_VOLUNTARY: 0; + static EXAMPLES_BEFORE_SUBMISSION: 1; + static EXAMPLES_BEFORE_ASSESSMENT: 2; + + protected ROOT_CACHE_KEY = 'mmaModWorkshop:'; + + constructor( + private appProvider: CoreAppProvider, + private filepoolProvider: CoreFilepoolProvider, + private sitesProvider: CoreSitesProvider, + private utils: CoreUtilsProvider, + private workshopOffline: AddonModWorkshopOfflineProvider) {} + + /** + * Get cache key for workshop data WS calls. + * + * @param {number} courseId Course ID. + * @return {string} Cache key. + */ + protected getWorkshopDataCacheKey(courseId: number): string { + return this.ROOT_CACHE_KEY + 'workshop:' + courseId; + } + + /** + * Get prefix cache key for all workshop activity data WS calls. + * + * @param {number} workshopId Workshop ID. + * @return {string} Cache key. + */ + protected getWorkshopDataPrefixCacheKey(workshopId: number): string { + return this.ROOT_CACHE_KEY + workshopId; + } + + /** + * Get cache key for workshop access information data WS calls. + * + * @param {number} workshopId Workshop ID. + * @return {string} Cache key. + */ + protected getWorkshopAccessInformationDataCacheKey(workshopId: number): string { + return this.getWorkshopDataPrefixCacheKey(workshopId) + ':access'; + } + + /** + * Get cache key for workshop user plan data WS calls. + * + * @param {number} workshopId Workshop ID. + * @return {string} Cache key. + */ + protected getUserPlanDataCacheKey(workshopId: number): string { + return this.getWorkshopDataPrefixCacheKey(workshopId) + ':userplan'; + } + + /** + * Get cache key for workshop submissions data WS calls. + * + * @param {number} workshopId Workshop ID. + * @param {number} [userId=0] User ID. + * @param {number} [groupId=0] Group ID. + * @return {string} Cache key. + */ + protected getSubmissionsDataCacheKey(workshopId: number, userId: number = 0, groupId: number = 0): string { + return this.getWorkshopDataPrefixCacheKey(workshopId) + ':submissions:' + userId + ':' + groupId; + } + + /** + * Get cache key for a workshop submission data WS calls. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId Submission ID. + * @return {string} Cache key. + */ + protected getSubmissionDataCacheKey(workshopId: number, submissionId: number): string { + return this.getWorkshopDataPrefixCacheKey(workshopId) + ':submission:' + submissionId; + } + + /** + * Get cache key for workshop grades data WS calls. + * + * @param {number} workshopId Workshop ID. + * @return {string} Cache key. + */ + protected getGradesDataCacheKey(workshopId: number): string { + return this.getWorkshopDataPrefixCacheKey(workshopId) + ':grades'; + } + + /** + * Get cache key for workshop grade report data WS calls. + * + * @param {number} workshopId Workshop ID. + * @param {number} [groupId=0] Group ID. + * @return {string} Cache key. + */ + protected getGradesReportDataCacheKey(workshopId: number, groupId: number = 0): string { + return this.getWorkshopDataPrefixCacheKey(workshopId) + ':report:' + groupId; + } + + /** + * Get cache key for workshop submission assessments data WS calls. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId Submission ID. + * @return {string} Cache key. + */ + protected getSubmissionAssessmentsDataCacheKey(workshopId: number, submissionId: number): string { + return this.getWorkshopDataPrefixCacheKey(workshopId) + ':assessments:' + submissionId; + } + + /** + * Get cache key for workshop reviewer assessments data WS calls. + * + * @param {number} workshopId Workshop ID. + * @param {number} [userId=0] User ID or current user. + * @return {string} Cache key. + */ + protected getReviewerAssessmentsDataCacheKey(workshopId: number, userId: number = 0): string { + return this.getWorkshopDataPrefixCacheKey(workshopId) + ':reviewerassessments:' + userId; + } + + /** + * Get cache key for a workshop assessment data WS calls. + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId Assessment ID. + * @return {string} Cache key. + */ + protected getAssessmentDataCacheKey(workshopId: number, assessmentId: number): string { + return this.getWorkshopDataPrefixCacheKey(workshopId) + ':assessment:' + assessmentId; + } + + /** + * Get cache key for workshop assessment form data WS calls. + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId Assessment ID. + * @param {string} [mode='assessment'] Mode assessment (default) or preview. + * @return {string} Cache key. + */ + protected getAssessmentFormDataCacheKey(workshopId: number, assessmentId: number, mode: string = 'assessment'): string { + return this.getWorkshopDataPrefixCacheKey(workshopId) + ':assessmentsform:' + assessmentId + ':' + mode; + } + + /** + * Return whether or not the plugin is enabled in a certain site. Plugin is enabled if the workshop WS are available. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise. + */ + isPluginEnabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.wsAvailable('mod_workshop_get_workshops_by_courses') && + site.wsAvailable('mod_workshop_get_workshop_access_information'); + }); + } + + /** + * Get a workshop with key=value. If more than one is found, only the first will be returned. + * + * @param {number} courseId Course ID. + * @param {string} key Name of the property to check. + * @param {any} value Value to search. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @return {Promise} Promise resolved when the workshop is retrieved. + */ + protected getWorkshopByKey(courseId: number, key: string, value: any, siteId?: string, forceCache: boolean = false): + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + courseids: [courseId] + }; + const preSets: any = { + cacheKey: this.getWorkshopDataCacheKey(courseId) + }; + + if (forceCache) { + preSets.omitExpires = true; + } + + return site.read('mod_workshop_get_workshops_by_courses', params, preSets).then((response) => { + if (response && response.workshops) { + for (const x in response.workshops) { + if (response.workshops[x][key] == value) { + return response.workshops[x]; + } + } + } + + return Promise.reject(null); + }); + }); + } + + /** + * Get a workshop by course module ID. + * + * @param {number} courseId Course ID. + * @param {number} cmId Course module ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @return {Promise} Promise resolved when the workshop is retrieved. + */ + getWorkshop(courseId: number, cmId: number, siteId?: string, forceCache: boolean = false): Promise { + return this.getWorkshopByKey(courseId, 'coursemodule', cmId, siteId, forceCache); + } + + /** + * Get a workshop by ID. + * + * @param {number} courseId Course ID. + * @param {number} id Workshop ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @return {Promise} Promise resolved when the workshop is retrieved. + */ + getWorkshopById(courseId: number, id: number, siteId?: string, forceCache: boolean = false): Promise { + return this.getWorkshopByKey(courseId, 'id', id, siteId, forceCache); + } + + /** + * Invalidates workshop data. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the workshop is invalidated. + */ + invalidateWorkshopData(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getWorkshopDataCacheKey(courseId)); + }); + } + + /** + * Invalidates workshop data except files and module info. + * + * @param {number} workshopId Workshop ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the workshop is invalidated. + */ + invalidateWorkshopWSData(workshopId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getWorkshopDataPrefixCacheKey(workshopId)); + }); + } + + /** + * Get access information for a given workshop. + * + * @param {number} workshopId Workshop ID. + * @param {boolean} [offline=false] True if it should return cached data. Has priority over ignoreCache. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the workshop is retrieved. + */ + getWorkshopAccessInformation(workshopId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + workshopid: workshopId + }; + const preSets: any = { + cacheKey: this.getWorkshopAccessInformationDataCacheKey(workshopId) + }; + + if (offline) { + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = 0; + preSets.emergencyCache = 0; + } + + return site.read('mod_workshop_get_workshop_access_information', params, preSets); + }); + } + + /** + * Invalidates workshop access information data. + * + * @param {number} workshopId Workshop ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateWorkshopAccessInformationData(workshopId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getWorkshopAccessInformationDataCacheKey(workshopId)); + }); + } + + /** + * Return the planner information for the given user. + * + * @param {number} workshopId Workshop ID. + * @param {boolean} [offline=false] True if it should return cached data. Has priority over ignoreCache. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the workshop data is retrieved. + */ + getUserPlanPhases(workshopId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + workshopid: workshopId + }; + const preSets: any = { + cacheKey: this.getUserPlanDataCacheKey(workshopId) + }; + + if (offline) { + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = 0; + preSets.emergencyCache = 0; + } + + return site.read('mod_workshop_get_user_plan', params, preSets).then((response) => { + if (response && response.userplan && response.userplan.phases) { + const phases = {}; + response.userplan.phases.forEach((phase) => { + phases[phase.code] = phase; + }); + + return phases; + } + + return Promise.reject(null); + }); + }); + } + + /** + * Invalidates workshop user plan data. + * + * @param {number} workshopId Workshop ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateUserPlanPhasesData(workshopId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getUserPlanDataCacheKey(workshopId)); + }); + } + + /** + * Retrieves all the workshop submissions visible by the current user or the one done by the given user. + * + * @param {number} workshopId Workshop ID. + * @param {number} [userId=0] User ID, 0 means the current user. + * @param {number} [groupId=0] Group id, 0 means that the function will determine the user group. + * @param {boolean} [offline=false] True if it should return cached data. Has priority over ignoreCache. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the workshop submissions are retrieved. + */ + getSubmissions(workshopId: number, userId: number = 0, groupId: number = 0, offline: boolean = false, + ignoreCache: boolean = false, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + workshopid: workshopId, + userid: userId, + groupid: groupId + }; + const preSets: any = { + cacheKey: this.getSubmissionsDataCacheKey(workshopId, userId, groupId) + }; + + if (offline) { + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = 0; + preSets.emergencyCache = 0; + } + + return site.read('mod_workshop_get_submissions', params, preSets).then((response) => { + if (response && response.submissions) { + return response.submissions; + } + + return Promise.reject(null); + }); + }); + } + + /** + * Invalidates workshop submissions data. + * + * @param {number} workshopId Workshop ID. + * @param {number} [userId=0] User ID. + * @param {number} [groupId=0] Group ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateSubmissionsData(workshopId: number, userId: number = 0, groupId: number = 0, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getSubmissionsDataCacheKey(workshopId, userId, groupId)); + }); + } + + /** + * Retrieves the given submission. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId Submission ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the workshop submission data is retrieved. + */ + getSubmission(workshopId: number, submissionId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + submissionid: submissionId + }; + const preSets = { + cacheKey: this.getSubmissionDataCacheKey(workshopId, submissionId) + }; + + return site.read('mod_workshop_get_submission', params, preSets).then((response) => { + if (response && response.submission) { + return response.submission; + } + + return Promise.reject(null); + }); + }); + } + + /** + * Invalidates workshop submission data. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId Submission ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateSubmissionData(workshopId: number, submissionId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getSubmissionDataCacheKey(workshopId, submissionId)); + }); + } + + /** + * Returns the grades information for the given workshop and user. + * + * @param {number} workshopId Workshop ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the workshop grades data is retrieved. + */ + getGrades(workshopId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + workshopid: workshopId + }; + const preSets = { + cacheKey: this.getGradesDataCacheKey(workshopId) + }; + + return site.read('mod_workshop_get_grades', params, preSets); + }); + } + + /** + * Invalidates workshop grades data. + * + * @param {number} workshopId Workshop ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateGradesData(workshopId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getGradesDataCacheKey(workshopId)); + }); + } + + /** + * Retrieves the assessment grades report. + * + * @param {number} workshopId Workshop ID. + * @param {number} [groupId] Group id, 0 means that the function will determine the user group. + * @param {number} [page=0] Page of records to return. Default 0. + * @param {number} [perPage=0] Records per page to return. Default AddonModWorkshopProvider.PER_PAGE. + * @param {boolean} [offline=false] True if it should return cached data. Has priority over ignoreCache. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the workshop data is retrieved. + */ + getGradesReport(workshopId: number, groupId: number = 0, page: number = 0, perPage: number = 0, offline: boolean = false, + ignoreCache: boolean = false, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + workshopid: workshopId, + groupid: groupId, + page: page, + perpage: perPage || AddonModWorkshopProvider.PER_PAGE + }; + const preSets: any = { + cacheKey: this.getGradesReportDataCacheKey(workshopId, groupId) + }; + + if (offline) { + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = 0; + preSets.emergencyCache = 0; + } + + return site.read('mod_workshop_get_grades_report', params, preSets).then((response) => { + if (response && response.report) { + return response.report; + } + + return Promise.reject(null); + }); + }); + } + + /** + * Performs the whole fetch of the grade reports in the workshop. + * + * @param {number} workshopId Workshop ID. + * @param {number} [groupId=0] Group ID. + * @param {number} [perPage=0] Records per page to fetch. It has to match with the prefetch. + * Default on AddonModWorkshopProvider.PER_PAGE. + * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + fetchAllGradeReports(workshopId: number, groupId: number = 0, perPage: number = 0, forceCache: boolean = false, + ignoreCache: boolean = false, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + perPage = perPage || AddonModWorkshopProvider.PER_PAGE; + + return this.fetchGradeReportsRecursive(workshopId, groupId, perPage, forceCache, ignoreCache, [], 0, siteId); + } + + /** + * Recursive call on fetch all grade reports. + * + * @param {number} workshopId Workshop ID. + * @param {number} groupId Group ID. + * @param {number} perPage Records per page to fetch. It has to match with the prefetch. + * @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false. + * @param {boolean} ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @param {any[]} grades Grades already fetched (just to concatenate them). + * @param {number} page Page of records to return. + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved when done. + */ + protected fetchGradeReportsRecursive(workshopId: number, groupId: number, perPage: number, forceCache: boolean, + ignoreCache: boolean, grades: any[], page: number, siteId: string): Promise { + return this.getGradesReport(workshopId, groupId, page, perPage, forceCache, ignoreCache, siteId).then((report) => { + Array.prototype.push.apply(grades, report.grades); + + const canLoadMore = ((page + 1) * perPage) < report.totalcount; + if (canLoadMore) { + return this.fetchGradeReportsRecursive( + workshopId, groupId, perPage, forceCache, ignoreCache, grades, page + 1, siteId); + } + + return grades; + }); + } + + /** + * Invalidates workshop grade report data. + * + * @param {number} workshopId Workshop ID. + * @param {number} [groupId=0] Group ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateGradeReportData(workshopId: number, groupId: number = 0, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getGradesReportDataCacheKey(workshopId, groupId)); + }); + } + + /** + * Retrieves the given submission assessment. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId Submission ID. + * @param {boolean} [offline=false] True if it should return cached data. Has priority over ignoreCache. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the workshop data is retrieved. + */ + getSubmissionAssessments(workshopId: number, submissionId: number, offline: boolean = false, ignoreCache: boolean = false, + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + submissionid: submissionId + }; + const preSets: any = { + cacheKey: this.getSubmissionAssessmentsDataCacheKey(workshopId, submissionId) + }; + + if (offline) { + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = 0; + preSets.emergencyCache = 0; + } + + return site.read('mod_workshop_get_submission_assessments', params, preSets).then((response) => { + if (response && response.assessments) { + return response.assessments; + } + + return Promise.reject(null); + }); + }); + } + + /** + * Invalidates workshop submission assessments data. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId Submission ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateSubmissionAssesmentsData(workshopId: number, submissionId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getSubmissionAssessmentsDataCacheKey(workshopId, submissionId)); + }); + } + + /** + * Add a new submission to a given workshop. + * + * @param {number} workshopId Workshop ID. + * @param {number} courseId Course ID the workshop belongs to. + * @param {string} title The submission title. + * @param {string} content The submission text content. + * @param {number} [attachmentsId] The draft file area id for attachments. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} [timecreated] The time the submission was created. Only used when editing an offline discussion. + * @param {boolean} [allowOffline=false] True if it can be stored in offline, false otherwise. + * @return {Promise} Promise resolved with submission ID if sent online or false if stored offline. + */ + addSubmission(workshopId: number, courseId: number, title: string, content: string, attachmentsId?: number, siteId?: string, + timecreated?: number, allowOffline: boolean = false): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a message to be synchronized later. + const storeOffline = (): Promise => { + return this.workshopOffline.saveSubmission(workshopId, courseId, title, content, {}, timecreated, 'add', + siteId).then(() => { + return false; + }); + }; + + // If we are editing an offline discussion, discard previous first. + const discardPromise = + timecreated ? this.workshopOffline.deleteSubmissionAction(workshopId, timecreated, 'add', siteId) : Promise.resolve(); + + return discardPromise.then(() => { + if (!this.appProvider.isOnline() && allowOffline) { + // App is offline, store the action. + return storeOffline(); + } + + return this.addSubmissionOnline(workshopId, title, content, attachmentsId, siteId).catch((error) => { + if (allowOffline && !this.utils.isWebServiceError(error)) { + // Couldn't connect to server, store in offline. + return storeOffline(); + } else { + // The WebService has thrown an error or offline not supported, reject. + return Promise.reject(error); + } + }); + }); + } + + /** + * Add a new submission to a given workshop. It will fail if offline or cannot connect. + * + * @param {number} workshopId Workshop ID. + * @param {string} title The submission title. + * @param {string} content The submission text content. + * @param {number} [attachmentsId] The draft file area id for attachments. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the submission is created. + */ + addSubmissionOnline(workshopId: number, title: string, content: string, attachmentsId: number, siteId?: string): + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + workshopid: workshopId, + title: title, + content: content, + attachmentsid: attachmentsId || 0 + }; + + return site.write('mod_workshop_add_submission', params).then((response) => { + // Other errors ocurring. + if (!response || !response.submissionid) { + return Promise.reject(this.utils.createFakeWSError('')); + } + + return response.submissionid; + }); + }); + } + + /** + * Updates the given submission. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId Submission ID. + * @param {number} courseId Course ID the workshop belongs to. + * @param {string} title The submission title. + * @param {string} content The submission text content. + * @param {number} [attachmentsId] The draft file area id for attachments. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [allowOffline=false] True if it can be stored in offline, false otherwise. + * @return {Promise} Promise resolved with submission ID if sent online or false if stored offline. + */ + updateSubmission(workshopId: number, submissionId: number, courseId: number, title: string, content: string, + attachmentsId?: number, siteId?: string, allowOffline: boolean = false): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a message to be synchronized later. + const storeOffline = (): Promise => { + return this.workshopOffline.saveSubmission(workshopId, courseId, title, content, attachmentsId, submissionId, 'update', + siteId).then(() => { + return false; + }); + }; + + // If we are editing an offline discussion, discard previous first. + return this.workshopOffline.deleteSubmissionAction(workshopId, submissionId, 'update', siteId).then(() => { + if (!this.appProvider.isOnline() && allowOffline) { + // App is offline, store the action. + return storeOffline(); + } + + return this.updateSubmissionOnline(submissionId, title, content, attachmentsId, siteId).catch((error) => { + if (allowOffline && !this.utils.isWebServiceError(error)) { + // Couldn't connect to server, store in offline. + return storeOffline(); + } else { + // The WebService has thrown an error or offline not supported, reject. + return Promise.reject(error); + } + }); + }); + } + + /** + * Updates the given submission. It will fail if offline or cannot connect. + * + * @param {number} submissionId Submission ID. + * @param {string} title The submission title. + * @param {string} content The submission text content. + * @param {number} [attachmentsId] The draft file area id for attachments. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the submission is updated. + */ + updateSubmissionOnline(submissionId: number, title: string, content: string, attachmentsId?: number, siteId?: string): + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + submissionid: submissionId, + title: title, + content: content, + attachmentsid: attachmentsId || 0 + }; + + return site.write('mod_workshop_update_submission', params).then((response) => { + // Other errors ocurring. + if (!response || !response.status) { + return Promise.reject(this.utils.createFakeWSError('')); + } + + // Return submissionId to be consistent with addSubmission. + return Promise.resolve(submissionId); + }); + }); + } + + /** + * Deletes the given submission. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId Submission ID. + * @param {number} courseId Course ID the workshop belongs to. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with submission ID if sent online, resolved with false if stored offline. + */ + deleteSubmission(workshopId: number, submissionId: number, courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a message to be synchronized later. + const storeOffline = (): Promise => { + return this.workshopOffline.saveSubmission(workshopId, courseId, '', '', 0, submissionId, 'delete', siteId).then(() => { + return false; + }); + }; + + // If we are editing an offline discussion, discard previous first. + return this.workshopOffline.deleteSubmissionAction(workshopId, submissionId, 'delete', siteId).then(() => { + if (!this.appProvider.isOnline()) { + // App is offline, store the action. + return storeOffline(); + } + + return this.deleteSubmissionOnline(submissionId, siteId).catch((error) => { + if (!this.utils.isWebServiceError(error)) { + // Couldn't connect to server, store in offline. + return storeOffline(); + } else { + // The WebService has thrown an error or offline not supported, reject. + return Promise.reject(error); + } + }); + }); + } + + /** + * Deletes the given submission. It will fail if offline or cannot connect. + * + * @param {number} submissionId Submission ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the submission is deleted. + */ + deleteSubmissionOnline(submissionId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + submissionid: submissionId + }; + + return site.write('mod_workshop_delete_submission', params).then((response) => { + // Other errors ocurring. + if (!response || !response.status) { + return Promise.reject(this.utils.createFakeWSError('')); + } + + // Return submissionId to be consistent with addSubmission. + return Promise.resolve(submissionId); + }); + }); + } + + /** + * Retrieves all the assessments reviewed by the given user. + * + * @param {number} workshopId Workshop ID. + * @param {number} [userId] User ID. If not defined, current user. + * @param {boolean} [offline=false] True if it should return cached data. Has priority over ignoreCache. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the workshop data is retrieved. + */ + getReviewerAssessments(workshopId: number, userId?: number, offline: boolean = false, ignoreCache: boolean = false, + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params: any = { + workshopid: workshopId + }; + const preSets: any = { + cacheKey: this.getReviewerAssessmentsDataCacheKey(workshopId, userId) + }; + + if (userId) { + params.userid = userId; + } + + if (offline) { + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = 0; + preSets.emergencyCache = 0; + } + + return site.read('mod_workshop_get_reviewer_assessments', params, preSets).then((response) => { + if (response && response.assessments) { + return response.assessments; + } + + return Promise.reject(null); + }); + }); + } + + /** + * Invalidates workshop user assessments data. + * + * @param {number} workshopId Workshop ID. + * @param {number} [userId] User ID. If not defined, current user. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateReviewerAssesmentsData(workshopId: number, userId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getReviewerAssessmentsDataCacheKey(workshopId, userId)); + }); + } + + /** + * Retrieves the given assessment. + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId Assessment ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the workshop data is retrieved. + */ + getAssessment(workshopId: number, assessmentId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + assessmentid: assessmentId + }; + const preSets = { + cacheKey: this.getAssessmentDataCacheKey(workshopId, assessmentId) + }; + + return site.read('mod_workshop_get_assessment', params, preSets).then((response) => { + if (response && response.assessment) { + return response.assessment; + } + + return Promise.reject(null); + }); + }); + } + + /** + * Invalidates workshop assessment data. + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId Assessment ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAssessmentData(workshopId: number, assessmentId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getAssessmentDataCacheKey(workshopId, assessmentId)); + }); + } + + /** + * Retrieves the assessment form definition (data required to be able to display the assessment form). + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId Assessment ID. + * @param {string} [mode='assessment'] Mode assessment (default) or preview. + * @param {boolean} [offline=false] True if it should return cached data. Has priority over ignoreCache. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the workshop data is retrieved. + */ + getAssessmentForm(workshopId: number, assessmentId: number, mode: string = 'assessment', offline: boolean = false, + ignoreCache: boolean = false, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + assessmentid: assessmentId, + mode: mode || 'assessment' + }; + const preSets: any = { + cacheKey: this.getAssessmentFormDataCacheKey(workshopId, assessmentId, mode) + }; + + if (offline) { + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = 0; + preSets.emergencyCache = 0; + } + + return site.read('mod_workshop_get_assessment_form_definition', params, preSets).then((response) => { + if (response) { + response.fields = this.parseFields(response.fields); + response.options = this.utils.objectToKeyValueMap(response.options, 'name', 'value'); + response.current = this.parseFields(response.current); + + return response; + } + + return Promise.reject(null); + }); + }); + } + + /** + * Parse fieldes into a more handful format. + * + * @param {any[]} fields Fields to parse + * @return {any[]} Parsed fields + */ + parseFields(fields: any[]): any[] { + const parsedFields = []; + + fields.forEach((field) => { + const args = field.name.split('_'); + const name = args[0]; + const idx = args[3]; + const idy = args[6] || false; + + if (parseInt(idx, 10) == idx) { + if (!parsedFields[idx]) { + parsedFields[idx] = { + number: parseInt(idx, 10) + 1 + }; + } + + if (idy && parseInt(idy, 10) == idy) { + if (!parsedFields[idx].fields) { + parsedFields[idx].fields = []; + } + if (!parsedFields[idx].fields[idy]) { + parsedFields[idx].fields[idy] = { + number: parseInt(idy, 10) + 1 + }; + } + parsedFields[idx].fields[idy][name] = field.value; + } else { + parsedFields[idx][name] = field.value; + } + } + }); + + return parsedFields; + } + + /** + * Invalidates workshop assessments form data. + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId Assessment ID. + * @param {string} [mode='assessment'] Mode assessment (default) or preview. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAssessmentFormData(workshopId: number, assessmentId: number, mode: string = 'assesssment', siteId?: string): + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getAssessmentFormDataCacheKey(workshopId, assessmentId, mode)); + }); + } + + /** + * Updates the given assessment. + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId Assessment ID. + * @param {number} courseId Course ID the workshop belongs to. + * @param {any} inputData Assessment data. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [allowOffline=false] True if it can be stored in offline, false otherwise. + * @return {Promise} Promise resolved with the grade of the submission if sent online, + * resolved with false if stored offline. + */ + updateAssessment(workshopId: number, assessmentId: number, courseId: number, inputData: any, siteId?: any, + allowOffline: boolean = false): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a message to be synchronized later. + const storeOffline = (): Promise => { + return this.workshopOffline.saveAssessment(workshopId, assessmentId, courseId, inputData, siteId).then(() => { + return false; + }); + }; + + // If we are editing an offline discussion, discard previous first. + return this.workshopOffline.deleteAssessment(workshopId, assessmentId, siteId).then(() => { + if (!this.appProvider.isOnline() && allowOffline) { + // App is offline, store the action. + return storeOffline(); + } + + return this.updateAssessmentOnline(assessmentId, inputData, siteId).catch((error) => { + if (allowOffline && !this.utils.isWebServiceError(error)) { + // Couldn't connect to server, store in offline. + return storeOffline(); + } else { + // The WebService has thrown an error or offline not supported, reject. + return Promise.reject(error); + } + }); + }); + } + + /** + * Updates the given assessment. It will fail if offline or cannot connect. + * + * @param {number} assessmentId Assessment ID. + * @param {any} inputData Assessment data. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the grade of the submission. + */ + updateAssessmentOnline(assessmentId: number, inputData: any, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + assessmentid: assessmentId, + data: this.utils.objectToArrayOfObjects(inputData, 'name', 'value') + }; + + return site.write('mod_workshop_update_assessment', params).then((response) => { + // Other errors ocurring. + if (!response || !response.status) { + return Promise.reject(this.utils.createFakeWSError('')); + } + + // Return rawgrade for submission + return response.rawgrade; + }); + }); + } + + /** + * Evaluates a submission (used by teachers for provide feedback or override the submission grade). + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId The submission id. + * @param {number} courseId Course ID the workshop belongs to. + * @param {string} feedbackText The feedback for the author. + * @param {boolean} published Whether to publish the submission for other users. + * @param {any} gradeOver The new submission grade (empty for no overriding the grade). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when submission is evaluated if sent online, + * resolved with false if stored offline. + */ + evaluateSubmission(workshopId: number, submissionId: number, courseId: number, feedbackText: string, published: boolean, + gradeOver: any, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a message to be synchronized later. + const storeOffline = (): Promise => { + return this.workshopOffline.saveEvaluateSubmission(workshopId, submissionId, courseId, feedbackText, published, + gradeOver, siteId).then(() => { + return false; + }); + }; + + // If we are editing an offline discussion, discard previous first. + return this.workshopOffline.deleteEvaluateSubmission(workshopId, submissionId, siteId).then(() => { + if (!this.appProvider.isOnline()) { + // App is offline, store the action. + return storeOffline(); + } + + return this.evaluateSubmissionOnline(submissionId, feedbackText, published, gradeOver, siteId).catch((error) => { + if (!this.utils.isWebServiceError(error)) { + // Couldn't connect to server, store in offline. + return storeOffline(); + } else { + // The WebService has thrown an error or offline not supported, reject. + return Promise.reject(error); + } + }); + }); + } + + /** + * Evaluates a submission (used by teachers for provide feedback or override the submission grade). + * It will fail if offline or cannot connect. + * + * @param {number} submissionId The submission id. + * @param {string} feedbackText The feedback for the author. + * @param {boolean} published Whether to publish the submission for other users. + * @param {any} gradeOver The new submission grade (empty for no overriding the grade). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the submission is evaluated. + */ + evaluateSubmissionOnline(submissionId: number, feedbackText: string, published: boolean, gradeOver: any, + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + submissionid: submissionId, + feedbacktext: feedbackText || '', + feedbackformat: 1, + published: published ? 1 : 0, + gradeover: gradeOver + }; + + return site.write('mod_workshop_evaluate_submission', params).then((response) => { + // Other errors ocurring. + if (!response || !response.status) { + return Promise.reject(this.utils.createFakeWSError('')); + } + + // Return if worked. + return Promise.resolve(true); + }); + }); + } + + /** + * Evaluates an assessment (used by teachers for provide feedback to the reviewer). + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId The assessment id. + * @param {number} courseId Course ID the workshop belongs to. + * @param {string} feedbackText The feedback for the reviewer. + * @param {boolean} weight The new weight for the assessment. + * @param {any} gradingGradeOver The new grading grade (empty for no overriding the grade). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when assessment is evaluated if sent online, + * resolved with false if stored offline. + */ + evaluateAssessment(workshopId: number, assessmentId: number, courseId: number, feedbackText: string, weight: number, + gradingGradeOver: any, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a message to be synchronized later. + const storeOffline = (): Promise => { + return this.workshopOffline.saveEvaluateAssessment(workshopId, assessmentId, courseId, feedbackText, weight, + gradingGradeOver, siteId).then(() => { + return false; + }); + }; + + // If we are editing an offline discussion, discard previous first. + return this.workshopOffline.deleteEvaluateAssessment(workshopId, assessmentId, siteId).then(() => { + if (!this.appProvider.isOnline()) { + // App is offline, store the action. + return storeOffline(); + } + + return this.evaluateAssessmentOnline(assessmentId, feedbackText, weight, gradingGradeOver, siteId).catch((error) => { + if (!this.utils.isWebServiceError(error)) { + // Couldn't connect to server, store in offline. + return storeOffline(); + } else { + // The WebService has thrown an error or offline not supported, reject. + return Promise.reject(error); + } + }); + }); + } + + /** + * Evaluates an assessment (used by teachers for provide feedback to the reviewer). It will fail if offline or cannot connect. + * + * @param {number} assessmentId The assessment id. + * @param {string} feedbackText The feedback for the reviewer. + * @param {number} weight The new weight for the assessment. + * @param {any} gradingGradeOver The new grading grade (empty for no overriding the grade). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the assessment is evaluated. + */ + evaluateAssessmentOnline(assessmentId: number, feedbackText: string, weight: number, gradingGradeOver: any, siteId?: string): + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + assessmentid: assessmentId, + feedbacktext: feedbackText || '', + feedbackformat: 1, + weight: weight, + gradinggradeover: gradingGradeOver + }; + + return site.write('mod_workshop_evaluate_assessment', params).then((response) => { + // Other errors ocurring. + if (!response || !response.status) { + return Promise.reject(this.utils.createFakeWSError('')); + } + + // Return if worked. + return Promise.resolve(true); + }); + }); + } + + /** + * Invalidate the prefetched content except files. + * To invalidate files, use $mmaModWorkshop#invalidateFiles. + * + * @param {number} moduleId The module ID. + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promised resolved when content is invalidated. + */ + invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.getWorkshop(courseId, moduleId, siteId, true).then((workshop) => { + return this.invalidateContentById(workshop.id, courseId, siteId); + }); + } + + /** + * Invalidate the prefetched content except files using the activityId. + * To invalidate files, use AdddonModWorkshop#invalidateFiles. + * + * @param {number} workshopId Workshop ID. + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when content is invalidated. + */ + invalidateContentById(workshopId: number, courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const promises = [ + // Do not invalidate workshop data before getting workshop info, we need it! + this.invalidateWorkshopData(courseId, siteId), + this.invalidateWorkshopWSData(workshopId, siteId), + ]; + + return Promise.all(promises); + } + + /** + * Invalidate the prefetched files. + * + * @param {number} moduleId The module ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the files are invalidated. + */ + invalidateFiles(moduleId: number, siteId?: string): Promise { + return this.filepoolProvider.invalidateFilesByComponent(siteId, AddonModWorkshopProvider.COMPONENT, moduleId); + } + + /** + * Report the workshop as being viewed. + * + * @param {string} id Workshop ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the WS call is successful. + */ + logView(id: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + workshopid: id + }; + + return site.write('mod_workshop_view_workshop', params); + }); + } + + /** + * Report the workshop submission as being viewed. + * + * @param {string} id Submission ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the WS call is successful. + */ + logViewSubmission(id: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + submissionid: id + }; + + return site.write('mod_workshop_view_submission', params); + }); + } +} From d000b3da36ca0bf99a6c9c5e1ddcbea035b0127a Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 30 May 2018 17:04:43 +0200 Subject: [PATCH 05/20] MOBILE-2354 workshop: Assessment strategy delegate and helper provider --- .../providers/assessment-strategy-delegate.ts | 135 +++++ src/addon/mod/workshop/providers/helper.ts | 534 ++++++++++++++++++ 2 files changed, 669 insertions(+) create mode 100644 src/addon/mod/workshop/providers/assessment-strategy-delegate.ts create mode 100644 src/addon/mod/workshop/providers/helper.ts diff --git a/src/addon/mod/workshop/providers/assessment-strategy-delegate.ts b/src/addon/mod/workshop/providers/assessment-strategy-delegate.ts new file mode 100644 index 000000000..92cd86faa --- /dev/null +++ b/src/addon/mod/workshop/providers/assessment-strategy-delegate.ts @@ -0,0 +1,135 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; + +/** + * Interface that all assessment strategy handlers must implement. + */ +export interface AddonWorkshopAssessmentStrategyHandler extends CoreDelegateHandler { + /** + * The name of the assessment strategy. E.g. 'accumulative'. + * @type {string} + */ + strategyName: string; + + /** + * Returns the component to render the plugin. + * + * @return {any} The component to use, undefined if not found. + */ + getComponent?(): any; + + /** + * Prepare original values to be shown and compared. + * + * @param {any} form Original data of the form. + * @param {number} workshopId WorkShop Id + * @return {Promise} Promise resolved with original values sorted. + */ + getOriginalValues?(form: any, workshopId: number): Promise; + + /** + * Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin. + * + * @param {any[]} originalValues Original values of the form. + * @param {any[]} currentValues Current values of the form. + * @return {boolean} True if data has changed, false otherwise. + */ + hasDataChanged?(originalValues: any[], currentValues: any[]): boolean; + + /** + * Prepare assessment data to be sent to the server depending on the strategy selected. + * + * @param {any{}} currentValues Current values of the form. + * @param {any} form Assessment form data. + * @return {Promise} Promise resolved with the data to be sent. Or rejected with the input errors object. + */ + prepareAssessmentData(currentValues: any[], form: any): Promise; +} + +/** + * Delegate to register workshop assessment strategy handlers. + * You can use this service to register your own assessment strategy handlers to be used in a workshop. + */ + @Injectable() + export class AddonWorkshopAssessmentStrategyDelegate extends CoreDelegate { + + protected handlerNameProperty = 'strategyName'; + + constructor(protected loggerProvider: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider, + protected eventsProvider: CoreEventsProvider) { + super('AddonWorkshopAssessmentStrategyDelegate', loggerProvider, sitesProvider, eventsProvider); + } + + /** + * Check if an assessment strategy plugin is supported. + * + * @param {string} workshopStrategy Assessment strategy name. + * @return {boolean} True if supported, false otherwise. + */ + isPluginSupported(workshopStrategy: string): boolean { + return this.hasHandler(workshopStrategy, true); + } + + /** + * Get the directive to use for a certain assessment strategy plugin. + * + * @param {string} workshopStrategy Assessment strategy name. + * @return {any} The component, undefined if not found. + */ + getComponentForPlugin(workshopStrategy: string): Promise { + return this.executeFunctionOnEnabled(workshopStrategy, 'getComponent', []); + } + + /** + * Prepare original values to be shown and compared depending on the strategy selected. + * + * @param {string} workshopStrategy Workshop strategy. + * @param {any} form Original data of the form. + * @param {number} workshopId Workshop ID. + * @return {Promise} Resolved with original values sorted. + */ + getOriginalValues(workshopStrategy: string, form: any, workshopId: number): Promise { + return Promise.resolve(this.executeFunctionOnEnabled(workshopStrategy, 'getOriginalValues', [form, workshopId]) || []); + } + + /** + * Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin. + * + * @param {any} workshop Workshop. + * @param {any[]} originalValues Original values of the form. + * @param {any[]} currentValues Current values of the form. + * @return {boolean} True if data has changed, false otherwise. + */ + hasDataChanged(workshop: any, originalValues: any[], currentValues: any[]): boolean { + return this.executeFunctionOnEnabled(workshop.strategy, 'hasDataChanged', [originalValues, currentValues]) || false; + } + + /** + * Prepare assessment data to be sent to the server depending on the strategy selected. + * + * @param {string} workshopStrategy Workshop strategy to follow. + * @param {any{}} currentValues Current values of the form. + * @param {any} form Assessment form data. + * @return {Promise} Promise resolved with the data to be sent. Or rejected with the input errors object. + */ + prepareAssessmentData(workshopStrategy: string, currentValues: any, form: any): Promise { + return Promise.resolve(this.executeFunctionOnEnabled(workshopStrategy, 'prepareAssessmentData', [currentValues, form])); + } +} diff --git a/src/addon/mod/workshop/providers/helper.ts b/src/addon/mod/workshop/providers/helper.ts new file mode 100644 index 000000000..48736cd8b --- /dev/null +++ b/src/addon/mod/workshop/providers/helper.ts @@ -0,0 +1,534 @@ +// (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 { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreFileProvider } from '@providers/file'; +import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { AddonModWorkshopProvider } from './workshop'; +import { AddonModWorkshopOfflineProvider } from './offline'; +import { AddonWorkshopAssessmentStrategyDelegate } from './assessment-strategy-delegate'; + +/** + * Helper to gather some common functions for workshop. + */ +@Injectable() +export class AddonModWorkshopHelperProvider { + + constructor( + private translate: TranslateService, + private fileProvider: CoreFileProvider, + private uploaderProvider: CoreFileUploaderProvider, + private sitesProvider: CoreSitesProvider, + private textUtils: CoreTextUtilsProvider, + private utils: CoreUtilsProvider, + private workshopProvider: AddonModWorkshopProvider, + private workshopOffline: AddonModWorkshopOfflineProvider, + private strategyDelegate: AddonWorkshopAssessmentStrategyDelegate) {} + + /** + * Get a task by code. + * + * @param {any[]} tasks Array of tasks. + * @param {string} taskCode Unique task code. + * @return {any} Task requested + */ + getTask(tasks: any[], taskCode: string): any { + for (const x in tasks) { + if (tasks[x].code == taskCode) { + return tasks[x]; + } + } + + return false; + } + + /** + * Check is task code is done. + * + * @param {any[]} tasks Array of tasks. + * @param {string} taskCode Unique task code. + * @return {boolean} True if task is completed. + */ + isTaskDone(tasks: any[], taskCode: string): boolean { + const task = this.getTask(tasks, taskCode); + + if (task) { + return task.completed; + } + + // Task not found, assume true. + return true; + } + + /** + * Return if a user can submit a workshop. + * + * @param {any} workshop Workshop info. + * @param {any} access Access information. + * @param {any[]} tasks Array of tasks. + * @return {boolean} True if the user can submit the workshop. + */ + canSubmit(workshop: any, access: any, tasks: any[]): boolean { + const examplesMust = workshop.useexamples && workshop.examplesmode == AddonModWorkshopProvider.EXAMPLES_BEFORE_SUBMISSION; + const examplesDone = access.canmanageexamples || workshop.examplesmode == AddonModWorkshopProvider.EXAMPLES_VOLUNTARY || + this.isTaskDone(tasks, 'examples'); + + return workshop.phase > AddonModWorkshopProvider.PHASE_SETUP && access.cansubmit && (!examplesMust || examplesDone); + } + + /** + * Return if a user can assess a workshop. + * + * @param {any} workshop Workshop info. + * @param {any} access Access information. + * @return {boolean} True if the user can assess the workshop. + */ + canAssess(workshop: any, access: any): boolean { + const examplesMust = workshop.useexamples && workshop.examplesmode == AddonModWorkshopProvider.EXAMPLES_BEFORE_ASSESSMENT; + const examplesDone = access.canmanageexamples; + + return !examplesMust || examplesDone; + } + + /** + * Return a particular user submission from the submission list. + * + * @param {number} workshopId Workshop ID. + * @param {number} [userId] User ID. If not defined current user Id. + * @return {Promise} Resolved with the submission, resolved with false if not found. + */ + getUserSubmission(workshopId: number, userId: number = 0): Promise { + return this.workshopProvider.getSubmissions(workshopId).then((submissions) => { + userId = userId || this.sitesProvider.getCurrentSiteUserId(); + + for (const x in submissions) { + if (submissions[x].authorid == userId) { + return submissions[x]; + } + } + + return false; + }); + } + + /** + * Return a particular submission. It will use prefetched data if fetch fails. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId Submission ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved with the submission, resolved with false if not found. + */ + getSubmissionById(workshopId: number, submissionId: number, siteId?: string): Promise { + return this.workshopProvider.getSubmission(workshopId, submissionId, siteId).catch(() => { + return this.workshopProvider.getSubmissions(workshopId, undefined, undefined, undefined, undefined, siteId) + .then((submissions) => { + for (const x in submissions) { + if (submissions[x].id == submissionId) { + return submissions[x]; + } + } + + return false; + }); + }); + } + + /** + * Return a particular assesment. It will use prefetched data if fetch fails. It will add assessment form data. + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId Assessment ID. + * @param {number} [userId] User ID. If not defined, current user. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved with the assessment. + */ + getReviewerAssessmentById(workshopId: number, assessmentId: number, userId: number = 0, siteId?: string): Promise { + return this.workshopProvider.getAssessment(workshopId, assessmentId, siteId).catch(() => { + return this.workshopProvider.getReviewerAssessments(workshopId, userId, undefined, undefined, siteId) + .then((assessments) => { + for (const x in assessments) { + if (assessments[x].id == assessmentId) { + return assessments[x]; + } + } + + return false; + }); + }).then((assessment) => { + if (!assessment) { + return false; + } + + return this.workshopProvider.getAssessmentForm(workshopId, assessmentId, undefined, undefined, undefined, siteId) + .then((assessmentForm) => { + assessment.form = assessmentForm; + + return assessment; + }); + }); + } + + /** + * Retrieves the assessment of the given user and all the related data. + * + * @param {number} workshopId Workshop ID. + * @param {number} [userId=0] User ID. If not defined, current user. + * @param {boolean} [offline=false] True if it should return cached data. Has priority over ignoreCache. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the workshop data is retrieved. + */ + getReviewerAssessments(workshopId: number, userId: number = 0, offline: boolean = false, ignoreCache: boolean = false, + siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.workshopProvider.getReviewerAssessments(workshopId, userId, offline, ignoreCache, siteId) + .then((assessments) => { + const promises = assessments.map((assessment) => { + return this.getSubmissionById(workshopId, assessment.submissionid, siteId).then((submission) => { + assessment.submission = submission; + }); + }); + + return Promise.all(promises).then(() => { + return assessments; + }); + }); + } + + /** + * Delete stored attachment files for a submission. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId If not editing, it will refer to timecreated. + * @param {boolean} editing If the submission is being edited or added otherwise. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when deleted. + */ + deleteSubmissionStoredFiles(workshopId: number, submissionId: number, editing: boolean, siteId?: string): Promise { + return this.workshopOffline.getSubmissionFolder(workshopId, submissionId, editing, siteId).then((folderPath) => { + return this.fileProvider.removeDir(folderPath).catch(() => { + // Ignore any errors, CoreFileProvider.removeDir fails if folder doesn't exists. + }); + }); + } + + /** + * Given a list of files (either online files or local files), store the local files in a local folder + * to be submitted later. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId If not editing, it will refer to timecreated. + * @param {boolean} editing If the submission is being edited or added otherwise. + * @param {any[]} files List of files. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if success, rejected otherwise. + */ + storeSubmissionFiles(workshopId: number, submissionId: number, editing: boolean, files: any[], siteId?: string): Promise { + // Get the folder where to store the files. + return this.workshopOffline.getSubmissionFolder(workshopId, submissionId, editing, siteId).then((folderPath) => { + return this.uploaderProvider.storeFilesToUpload(folderPath, files); + }); + } + + /** + * Upload or store some files for a submission, depending if the user is offline or not. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId If not editing, it will refer to timecreated. + * @param {any[]} files List of files. + * @param {boolean} editing If the submission is being edited or added otherwise. + * @param {boolean} offline True if files sould be stored for offline, false to upload them. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if success. + */ + uploadOrStoreSubmissionFiles(workshopId: number, submissionId: number, files: any[], editing: boolean, offline: boolean, + siteId?: string): Promise { + if (offline) { + return this.storeSubmissionFiles(workshopId, submissionId, editing, files, siteId); + } else { + return this.uploaderProvider.uploadOrReuploadFiles(files, AddonModWorkshopProvider.COMPONENT, workshopId, siteId); + } + } + + /** + * Get a list of stored attachment files for a submission. See $mmaModWorkshopHelper#storeFiles. + * + * @param {number} workshopId Workshop ID. + * @param {number} submissionId If not editing, it will refer to timecreated. + * @param {boolean} editing If the submission is being edited or added otherwise. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the files. + */ + getStoredSubmissionFiles(workshopId: number, submissionId: number, editing: boolean, siteId?: string): Promise { + return this.workshopOffline.getSubmissionFolder(workshopId, submissionId, editing, siteId).then((folderPath) => { + return this.uploaderProvider.getStoredFiles(folderPath).catch(() => { + // Ignore not found files. + return []; + }); + }); + } + + /** + * Get a list of stored attachment files for a submission and online files also. See $mmaModWorkshopHelper#storeFiles. + * + * @param {any} filesObject Files object combining offline and online information. + * @param {number} workshopId Workshop ID. + * @param {number} submissionId If not editing, it will refer to timecreated. + * @param {boolean} editing If the submission is being edited or added otherwise. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the files. + */ + getSubmissionFilesFromOfflineFilesObject(filesObject: any, workshopId: number, submissionId: number, editing: boolean, + siteId?: string): Promise { + return this.workshopOffline.getSubmissionFolder(workshopId, submissionId, editing, siteId).then((folderPath) => { + return this.uploaderProvider.getStoredFilesFromOfflineFilesObject(filesObject, folderPath); + }); + } + + /** + * Delete stored attachment files for an assessment. + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId Assessment ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when deleted. + */ + deleteAssessmentStoredFiles(workshopId: number, assessmentId: number, siteId?: string): Promise { + return this.workshopOffline.getAssessmentFolder(workshopId, assessmentId, siteId).then((folderPath) => { + return this.fileProvider.removeDir(folderPath).catch(() => { + // Ignore any errors, CoreFileProvider.removeDir fails if folder doesn't exists. + }); + }); + } + + /** + * Given a list of files (either online files or local files), store the local files in a local folder + * to be submitted later. + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId Assessment ID. + * @param {any[]} files List of files. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if success, rejected otherwise. + */ + storeAssessmentFiles(workshopId: number, assessmentId: number, files: any[], siteId?: string): Promise { + // Get the folder where to store the files. + return this.workshopOffline.getAssessmentFolder(workshopId, assessmentId, siteId).then((folderPath) => { + return this.uploaderProvider.storeFilesToUpload(folderPath, files); + }); + } + + /** + * Upload or store some files for an assessment, depending if the user is offline or not. + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId ID. + * @param {any[]} files List of files. + * @param {boolean} offline True if files sould be stored for offline, false to upload them. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if success. + */ + uploadOrStoreAssessmentFiles(workshopId: number, assessmentId: number, files: any[], offline: boolean, siteId?: string): + Promise { + if (offline) { + return this.storeAssessmentFiles(workshopId, assessmentId, files, siteId); + } else { + return this.uploaderProvider.uploadOrReuploadFiles(files, AddonModWorkshopProvider.COMPONENT, workshopId, siteId); + } + } + + /** + * Get a list of stored attachment files for an assessment. See $mmaModWorkshopHelper#storeFiles. + * + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId Assessment ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the files. + */ + getStoredAssessmentFiles(workshopId: number, assessmentId: number, siteId?: string): Promise { + return this.workshopOffline.getAssessmentFolder(workshopId, assessmentId, siteId).then((folderPath) => { + return this.uploaderProvider.getStoredFiles(folderPath).catch(() => { + // Ignore not found files. + return []; + }); + }); + } + + /** + * Get a list of stored attachment files for an assessment and online files also. See $mmaModWorkshopHelper#storeFiles. + * + * @param {object} filesObject Files object combining offline and online information. + * @param {number} workshopId Workshop ID. + * @param {number} assessmentId Assessment ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the files. + */ + getAssessmentFilesFromOfflineFilesObject(filesObject: any, workshopId: number, assessmentId: number, siteId?: string): + Promise { + return this.workshopOffline.getAssessmentFolder(workshopId, assessmentId, siteId).then((folderPath) => { + return this.uploaderProvider.getStoredFilesFromOfflineFilesObject(filesObject, folderPath); + }); + } + + /** + * Returns the action of a given submission. + * + * @param {any[]} actions Offline actions to be applied to the given submission. + * @param {number} submissionId ID of the submission to filter by or false. + * @return {any[]} Promise resolved with the files. + */ + filterSubmissionActions(actions: any[], submissionId: number): any[] { + return actions.filter((action) => { + if (submissionId) { + return action.submissionid == submissionId; + } else { + return action.submissionid < 0; + } + }); + } + + /** + * Applies offline data to submission. + * + * @param {any} submission Submission object to be modified. + * @param {any[]} actions Offline actions to be applied to the given submission. + * @return {Promise} Promise resolved with the files. + */ + applyOfflineData(submission: any, actions: any[]): Promise { + if (actions.length && !submission) { + submission = {}; + } + + let editing = true, + attachmentsid = false, + workshopId; + + actions.forEach((action) => { + switch (action.action) { + case 'add': + submission.id = action.submissionid; + editing = false; + case 'update': + submission.title = action.title; + submission.content = action.content; + submission.title = action.title; + submission.courseid = action.courseid; + submission.submissionmodified = parseInt(action.timemodified, 10) / 1000; + submission.offline = true; + attachmentsid = action.attachmentsid; + workshopId = action.workshopid; + break; + case 'delete': + submission.deleted = true; + submission.submissionmodified = parseInt(action.timemodified, 10) / 1000; + break; + default: + } + }); + + // Check offline files for latest attachmentsid. + if (actions.length) { + if (attachmentsid) { + return this.getSubmissionFilesFromOfflineFilesObject(attachmentsid, workshopId, submission.id, editing) + .then((files) => { + submission.attachmentfiles = files; + + return submission; + }); + } else { + submission.attachmentfiles = []; + } + } + + return Promise.resolve(submission); + } + + /** + * Prepare assessment data to be sent to the server. + * + * @param {any} workshop Workshop object. + * @param {any[]} selectedValues Assessment current values + * @param {string} feedbackText Feedback text. + * @param {any[]} feedbackFiles Feedback attachments. + * @param {any} form Assessment form original data. + * @param {number} attachmentsId The draft file area id for attachments. + * @return {Promise} Promise resolved with the data to be sent. Or rejected with the input errors object. + */ + prepareAssessmentData(workshop: any, selectedValues: any[], feedbackText: string, feedbackFiles: any[], form: any, + attachmentsId: number): Promise { + if (workshop.overallfeedbackmode == 2 && !feedbackText) { + return Promise.reject({feedbackauthor: this.translate.instant('core.err_required')}); + } + + return this.strategyDelegate.prepareAssessmentData(workshop.strategy, selectedValues, form).then((data) => { + data.feedbackauthor = feedbackText; + data.feedbackauthorattachmentsid = attachmentsId || 0; + data.nodims = form.dimenssionscount; + + return data; + }); + } + + /** + * Calculates the real value of a grade based on real_grade_value. + * + * @param {number} value Percentual value from 0 to 100. + * @param {number} max The maximal grade. + * @param {number} decimals Decimals to show in the formatted grade. + * @return {string} Real grade formatted. + */ + protected realGradeValueHelper(value: number, max: number, decimals: number): string { + if (value == null) { + return null; + } else if (max == 0) { + return '0'; + } else { + value = this.textUtils.roundToDecimals(max * value / 100, decimals); + + return this.utils.formatFloat(value); + } + } + + /** + * Calculates the real value of a grades of an assessment. + * + * @param {any} workshop Workshop object. + * @param {any} assessment Assessment data. + * @return {any} Assessment with real grades. + */ + realGradeValue(workshop: any, assessment: any): any { + assessment.grade = this.realGradeValueHelper(assessment.grade, workshop.grade, workshop.gradedecimals); + assessment.gradinggrade = this.realGradeValueHelper(assessment.gradinggrade, workshop.gradinggrade, workshop.gradedecimals); + assessment.gradinggradeover = this.realGradeValueHelper(assessment.gradinggradeover, workshop.gradinggrade, + workshop.gradedecimals); + + return assessment; + } + + /** + * Check grade should be shown + * + * @param {any} grade Grade to be shown + * @return {boolean} If grade should be shown or not. + */ + showGrade(grade: any): boolean { + return typeof grade !== 'undefined' && grade !== null; + } +} From 3652d0591dfa9fde71c1caf9409a394b05411aef Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 30 May 2018 17:05:15 +0200 Subject: [PATCH 06/20] MOBILE-2354 workshop: Sync provider --- src/addon/mod/workshop/providers/sync.ts | 565 +++++++++++++++++++++++ 1 file changed, 565 insertions(+) create mode 100644 src/addon/mod/workshop/providers/sync.ts diff --git a/src/addon/mod/workshop/providers/sync.ts b/src/addon/mod/workshop/providers/sync.ts new file mode 100644 index 000000000..8c1e6fb15 --- /dev/null +++ b/src/addon/mod/workshop/providers/sync.ts @@ -0,0 +1,565 @@ +// (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 { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreAppProvider } from '@providers/app'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { AddonModWorkshopProvider } from './workshop'; +import { AddonModWorkshopHelperProvider } from './helper'; +import { AddonModWorkshopOfflineProvider } from './offline'; + +/** + * Service to sync workshops. + */ +@Injectable() +export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { + + static AUTO_SYNCED = 'addon_mod_workshop_autom_synced'; + static MANUAL_SYNCED = 'addon_mod_workshop_manual_synced'; + static SYNC_TIME = 300000; + + protected componentTranslate: string; + + constructor(translate: TranslateService, + appProvider: CoreAppProvider, + courseProvider: CoreCourseProvider, + private eventsProvider: CoreEventsProvider, + loggerProvider: CoreLoggerProvider, + sitesProvider: CoreSitesProvider, + syncProvider: CoreSyncProvider, + textUtils: CoreTextUtilsProvider, + private utils: CoreUtilsProvider, + private workshopProvider: AddonModWorkshopProvider, + private workshopHelper: AddonModWorkshopHelperProvider, + private workshopOffline: AddonModWorkshopOfflineProvider) { + + super('AddonModWorkshopSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate); + + this.componentTranslate = courseProvider.translateModuleName('workshop'); + } + + /** + * Check if an workshop has data to synchronize. + * + * @param {number} workshopId Workshop ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if has data to sync, false otherwise. + */ + hasDataToSync(workshopId: number, siteId?: string): Promise { + return this.workshopOffline.hasWorkshopOfflineData(workshopId, siteId); + } + + /** + * Try to synchronize all workshops that need it and haven't been synchronized in a while. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @return {Promise} Promise resolved when the sync is done. + */ + syncAllWorkshops(siteId?: string): Promise { + return this.syncOnSites('all workshops', this.syncAllWorkshopsFunc.bind(this), [], siteId); + } + + /** + * Sync all workshops on a site. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + protected syncAllWorkshopsFunc(siteId?: string): Promise { + return this.workshopOffline.getAllWorkshops(siteId).then((workshopIds) => { + const promises = []; + + // Sync all workshops that haven't been synced for a while. + workshopIds.forEach((workshopId) => { + promises.push(this.syncWorkshopIfNeeded(workshopId, siteId).then((data) => { + if (data && data.updated) { + // Sync done. Send event. + this.eventsProvider.trigger(AddonModWorkshopSyncProvider.AUTO_SYNCED, { + workshopId: workshopId, + warnings: data.warnings + }, siteId); + } + })); + }); + + return Promise.all(promises); + }); + } + + /** + * Sync a workshop only if a certain time has passed since the last time. + * + * @param {number} workshopId Workshop ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the workshop is synced or if it doesn't need to be synced. + */ + syncWorkshopIfNeeded(workshopId: number, siteId?: string): Promise { + return this.isSyncNeeded(workshopId, siteId).then((needed) => { + if (needed) { + return this.syncWorkshop(workshopId, siteId); + } + }); + } + + /** + * Try to synchronize a workshop. + * + * @param {number} workshopId Workshop ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + syncWorkshop(workshopId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (this.isSyncing(workshopId, siteId)) { + // There's already a sync ongoing for this discussion, return the promise. + return this.getOngoingSync(workshopId, siteId); + } + + // Verify that workshop isn't blocked. + if (this.syncProvider.isBlocked(AddonModWorkshopProvider.COMPONENT, workshopId, siteId)) { + this.logger.debug('Cannot sync workshop ' + workshopId + ' because it is blocked.'); + + return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate})); + } + + this.logger.debug('Try to sync workshop ' + workshopId); + + const syncPromises = []; + + // Get offline submissions to be sent. + syncPromises.push(this.workshopOffline.getSubmissions(workshopId, siteId).catch(() => { + // No offline data found, return empty array. + return []; + })); + + // Get offline submission assessments to be sent. + syncPromises.push(this.workshopOffline.getAssessments(workshopId, siteId).catch(() => { + // No offline data found, return empty array. + return []; + })); + + // Get offline submission evaluations to be sent. + syncPromises.push(this.workshopOffline.getEvaluateSubmissions(workshopId, siteId).catch(() => { + // No offline data found, return empty array. + return []; + })); + + // Get offline assessment evaluations to be sent. + syncPromises.push(this.workshopOffline.getEvaluateAssessments(workshopId, siteId).catch(() => { + // No offline data found, return empty array. + return []; + })); + + const result = { + warnings: [], + updated: false + }; + + // Get offline submissions to be sent. + const syncPromise = Promise.all(syncPromises).then((syncs) => { + let courseId; + + // Get courseId from the first object + for (const x in syncs) { + if (syncs[x].length > 0 && syncs[x][0].courseid) { + courseId = syncs[x][0].courseid; + break; + } + } + + if (!courseId) { + // Nothing to sync. + return; + } else if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + return Promise.reject(null); + } + + return this.workshopProvider.getWorkshopById(courseId, workshopId, siteId).then((workshop) => { + const submissionsActions = syncs[0], + assessments = syncs[1], + submissionEvaluations = syncs[2], + assessmentEvaluations = syncs[3], + promises = [], + offlineSubmissions = {}; + + submissionsActions.forEach((action) => { + if (typeof offlineSubmissions[action.submissionid] == 'undefined') { + offlineSubmissions[action.submissionid] = []; + } + offlineSubmissions[action.submissionid].push(action); + }); + + Object.keys(offlineSubmissions).forEach((submissionId) => { + const submissionActions = offlineSubmissions[submissionId]; + promises.push(this.syncSubmission(workshop, submissionActions, result, siteId).then(() => { + result.updated = true; + })); + }); + + assessments.forEach((assessment) => { + promises.push(this.syncAssessment(workshop, assessment, result, siteId).then(() => { + result.updated = true; + })); + }); + + submissionEvaluations.forEach((evaluation) => { + promises.push(this.syncEvaluateSubmission(workshop, evaluation, result, siteId).then(() => { + result.updated = true; + })); + }); + + assessmentEvaluations.forEach((evaluation) => { + promises.push(this.syncEvaluateAssessment(workshop, evaluation, result, siteId).then(() => { + result.updated = true; + })); + }); + + return Promise.all(promises); + }).then(() => { + if (result.updated) { + // Data has been sent to server. Now invalidate the WS calls. + return this.workshopProvider.invalidateContentById(workshopId, courseId, siteId).catch(() => { + // Ignore errors. + }); + } + }); + }).then(() => { + // Sync finished, set sync time. + return this.setSyncTime(workshopId, siteId).catch(() => { + // Ignore errors. + }); + }).then(() => { + // All done, return the warnings. + return result; + }); + + return this.addOngoingSync(workshopId, syncPromise, siteId); + } + + /** + * Synchronize a submission. + * + * @param {any} workshop Workshop. + * @param {any[]} submissionActions Submission actions offline data. + * @param {any} result Object with the result of the sync. + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved if success, rejected otherwise. + */ + protected syncSubmission(workshop: any, submissionActions: any, result: any, siteId: string): Promise { + let discardError; + let editing = false; + + // Sort entries by timemodified. + submissionActions = submissionActions.sort((a, b) => { + return a.timemodified - b.timemodified; + }); + + let timePromise = null; + let submissionId = submissionActions[0].submissionid; + + if (submissionId > 0) { + editing = true; + timePromise = this.workshopProvider.getSubmission(workshop.id, submissionId, siteId).then((submission) => { + return submission.timemodified; + }).catch(() => { + return -1; + }); + } else { + timePromise = Promise.resolve(0); + } + + return timePromise.then((timemodified) => { + if (timemodified < 0 || timemodified >= submissionActions[0].timemodified) { + // The entry was not found in Moodle or the entry has been modified, discard the action. + result.updated = true; + discardError = this.translate.instant('addon.mod_workshop.warningsubmissionmodified'); + + return this.workshopOffline.deleteAllSubmissionActions(workshop.id, submissionId, siteId); + } + + let promise = Promise.resolve(); + + submissionActions.forEach((action) => { + promise = promise.then(() => { + submissionId = action.submissionid > 0 ? action.submissionid : submissionId; + + let fileProm; + // Upload attachments first if any. + if (action.attachmentsid) { + fileProm = this.workshopHelper.getSubmissionFilesFromOfflineFilesObject(action.attachmentsid, workshop.id, + submissionId, editing, siteId).then((files) => { + return this.workshopHelper.uploadOrStoreSubmissionFiles(workshop.id, submissionId, files, editing, + false, siteId); + }); + } else { + // Remove all files. + fileProm = this.workshopHelper.uploadOrStoreSubmissionFiles(workshop.id, submissionId, [], editing, false, + siteId); + } + + return fileProm.then((attachmentsId) => { + // Perform the action. + switch (action.action) { + case 'add': + return this.workshopProvider.addSubmissionOnline(workshop.id, action.title, action.content, + attachmentsId, siteId).then((newSubmissionId) => { + submissionId = newSubmissionId; + }); + case 'update': + return this.workshopProvider.updateSubmissionOnline(submissionId, action.title, action.content, + attachmentsId, siteId); + case 'delete': + return this.workshopProvider.deleteSubmissionOnline(submissionId, siteId); + default: + return Promise.resolve(); + } + }).catch((error) => { + if (error && this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means it cannot be performed. Discard. + discardError = error.message || error.error; + } else { + // Couldn't connect to server, reject. + return Promise.reject(error); + } + }).then(() => { + // Delete the offline data. + result.updated = true; + + return this.workshopOffline.deleteSubmissionAction(action.workshopid, action.submissionid, action.action, + siteId); + }); + }); + }); + + return promise.then(() => { + if (discardError) { + // Submission was discarded, add a warning. + const message = this.translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: workshop.name, + error: discardError + }); + + if (result.warnings.indexOf(message) == -1) { + result.warnings.push(message); + } + } + }); + }); + } + + /** + * Synchronize an assessment. + * + * @param {any} workshop Workshop. + * @param {any} assessment Assessment offline data. + * @param {any} result Object with the result of the sync. + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved if success, rejected otherwise. + */ + protected syncAssessment(workshop: any, assessmentData: any, result: any, siteId: string): Promise { + let discardError; + const assessmentId = assessmentData.assessmentid; + + const timePromise = this.workshopProvider.getAssessment(workshop.id, assessmentId, siteId).then((assessment) => { + return assessment.timemodified; + }).catch(() => { + return -1; + }); + + return timePromise.then((timemodified) => { + if (timemodified < 0 || timemodified >= assessmentData.timemodified) { + // The entry was not found in Moodle or the entry has been modified, discard the action. + result.updated = true; + discardError = this.translate.instant('addon.mod_workshop.warningassessmentmodified'); + + return this.workshopOffline.deleteAssessment(workshop.id, assessmentId, siteId); + } + + let fileProm; + const inputData = assessmentData.inputdata; + + // Upload attachments first if any. + if (inputData.feedbackauthorattachmentsid) { + fileProm = this.workshopHelper.getAssessmentFilesFromOfflineFilesObject(inputData.feedbackauthorattachmentsid, + workshop.id, assessmentId, siteId).then((files) => { + return this.workshopHelper.uploadOrStoreAssessmentFiles(workshop.id, assessmentId, files, false, siteId); + }); + } else { + // Remove all files. + fileProm = this.workshopHelper.uploadOrStoreAssessmentFiles(workshop.id, assessmentId, [], false, siteId); + } + + return fileProm.then((attachmentsId) => { + inputData.feedbackauthorattachmentsid = attachmentsId || 0; + + return this.workshopProvider.updateAssessmentOnline(assessmentId, inputData, siteId); + }).catch((error) => { + if (error && this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means it cannot be performed. Discard. + discardError = error.message || error.error; + } else { + // Couldn't connect to server, reject. + return Promise.reject(error); + } + }).then(() => { + // Delete the offline data. + result.updated = true; + + return this.workshopOffline.deleteAssessment(workshop.id, assessmentId, siteId); + }); + }).then(() => { + if (discardError) { + // Assessment was discarded, add a warning. + const message = this.translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: workshop.name, + error: discardError + }); + + if (result.warnings.indexOf(message) == -1) { + result.warnings.push(message); + } + } + }); + } + + /** + * Synchronize a submission evaluation. + * + * @param {any} workshop Workshop. + * @param {any} evaluate Submission evaluation offline data. + * @param {any} result Object with the result of the sync. + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved if success, rejected otherwise. + */ + protected syncEvaluateSubmission(workshop: any, evaluate: any, result: any, siteId: string): Promise { + let discardError; + const submissionId = evaluate.submissionid; + + const timePromise = this.workshopProvider.getSubmission(workshop.id, submissionId, siteId).then((submission) => { + return submission.timemodified; + }).catch(() => { + return -1; + }); + + return timePromise.then((timemodified) => { + if (timemodified < 0 || timemodified >= evaluate.timemodified) { + // The entry was not found in Moodle or the entry has been modified, discard the action. + result.updated = true; + discardError = this.translate.instant('addon.mod_workshop.warningsubmissionmodified'); + + return this.workshopOffline.deleteEvaluateSubmission(workshop.id, submissionId, siteId); + } + + return this.workshopProvider.evaluateSubmissionOnline(submissionId, evaluate.feedbacktext, evaluate.published, + evaluate.gradeover, siteId).catch((error) => { + if (error && this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means it cannot be performed. Discard. + discardError = error.message || error.error; + } else { + // Couldn't connect to server, reject. + return Promise.reject(error && error.error); + } + }).then(() => { + // Delete the offline data. + result.updated = true; + + return this.workshopOffline.deleteEvaluateSubmission(workshop.id, submissionId, siteId); + }); + }).then(() => { + if (discardError) { + // Assessment was discarded, add a warning. + const message = this.translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: workshop.name, + error: discardError + }); + + if (result.warnings.indexOf(message) == -1) { + result.warnings.push(message); + } + } + }); + } + + /** + * Synchronize a assessment evaluation. + * + * @param {any} workshop Workshop. + * @param {any} evaluate Assessment evaluation offline data. + * @param {any} result Object with the result of the sync. + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved if success, rejected otherwise. + */ + protected syncEvaluateAssessment(workshop: any, evaluate: any, result: any, siteId: string): Promise { + let discardError; + const assessmentId = evaluate.assessmentid; + + const timePromise = this.workshopProvider.getAssessment(workshop.id, assessmentId, siteId).then((assessment) => { + return assessment.timemodified; + }).catch(() => { + return -1; + }); + + return timePromise.then((timemodified) => { + if (timemodified < 0 || timemodified >= evaluate.timemodified) { + // The entry was not found in Moodle or the entry has been modified, discard the action. + result.updated = true; + discardError = this.translate.instant('addon.mod_workshop.warningassessmentmodified'); + + return this.workshopOffline.deleteEvaluateAssessment(workshop.id, assessmentId, siteId); + } + + return this.workshopProvider.evaluateAssessmentOnline(assessmentId, evaluate.feedbacktext, evaluate.weight, + evaluate.gradinggradeover, siteId).catch((error) => { + if (error && this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means it cannot be performed. Discard. + discardError = error.message || error.error; + } else { + // Couldn't connect to server, reject. + return Promise.reject(error && error.error); + } + }).then(() => { + // Delete the offline data. + result.updated = true; + + return this.workshopOffline.deleteEvaluateAssessment(workshop.id, assessmentId, siteId); + }); + }).then(() => { + if (discardError) { + // Assessment was discarded, add a warning. + const message = this.translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: workshop.name, + error: discardError + }); + + if (result.warnings.indexOf(message) == -1) { + result.warnings.push(message); + } + } + }); + } +} From 0591c35a0b97937a2fbb6e77a8f90d259f1ef088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 30 May 2018 16:03:36 +0200 Subject: [PATCH 07/20] MOBILE-2354 workshop: Index page --- .../workshop/components/components.module.ts | 45 ++ .../mod/workshop/components/index/index.html | 176 +++++++ .../mod/workshop/components/index/index.ts | 485 ++++++++++++++++++ src/addon/mod/workshop/lang/en.json | 60 +++ src/addon/mod/workshop/pages/index/index.html | 16 + .../mod/workshop/pages/index/index.module.ts | 33 ++ src/addon/mod/workshop/pages/index/index.ts | 50 ++ src/addon/mod/workshop/pages/phase/phase.html | 25 + .../mod/workshop/pages/phase/phase.module.ts | 33 ++ src/addon/mod/workshop/pages/phase/phase.ts | 56 ++ src/addon/mod/workshop/providers/helper.ts | 8 +- .../mod/workshop/providers/link-handler.ts | 30 ++ .../mod/workshop/providers/module-handler.ts | 72 +++ src/addon/mod/workshop/providers/offline.ts | 8 +- src/addon/mod/workshop/providers/workshop.ts | 14 +- src/addon/mod/workshop/workshop.module.ts | 49 ++ src/app/app.module.ts | 2 + 17 files changed, 1146 insertions(+), 16 deletions(-) create mode 100644 src/addon/mod/workshop/components/components.module.ts create mode 100644 src/addon/mod/workshop/components/index/index.html create mode 100644 src/addon/mod/workshop/components/index/index.ts create mode 100644 src/addon/mod/workshop/lang/en.json create mode 100644 src/addon/mod/workshop/pages/index/index.html create mode 100644 src/addon/mod/workshop/pages/index/index.module.ts create mode 100644 src/addon/mod/workshop/pages/index/index.ts create mode 100644 src/addon/mod/workshop/pages/phase/phase.html create mode 100644 src/addon/mod/workshop/pages/phase/phase.module.ts create mode 100644 src/addon/mod/workshop/pages/phase/phase.ts create mode 100644 src/addon/mod/workshop/providers/link-handler.ts create mode 100644 src/addon/mod/workshop/providers/module-handler.ts create mode 100644 src/addon/mod/workshop/workshop.module.ts diff --git a/src/addon/mod/workshop/components/components.module.ts b/src/addon/mod/workshop/components/components.module.ts new file mode 100644 index 000000000..6111f4bb4 --- /dev/null +++ b/src/addon/mod/workshop/components/components.module.ts @@ -0,0 +1,45 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreCourseComponentsModule } from '@core/course/components/components.module'; +import { AddonModWorkshopIndexComponent } from './index/index'; + +@NgModule({ + declarations: [ + AddonModWorkshopIndexComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CoreCourseComponentsModule + ], + providers: [ + ], + exports: [ + AddonModWorkshopIndexComponent + ], + entryComponents: [ + AddonModWorkshopIndexComponent + ] +}) +export class AddonModWorkshopComponentsModule {} diff --git a/src/addon/mod/workshop/components/index/index.html b/src/addon/mod/workshop/components/index/index.html new file mode 100644 index 000000000..15f9ed06c --- /dev/null +++ b/src/addon/mod/workshop/components/index/index.html @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + +

{{ phases[selectedPhase].title }}

+

{{ 'addon.mod_workshop.userplancurrentphase' | translate }}

+ +
+ + + {{ 'addon.mod_workshop.switchphase' + selectedPhase | translate }} + + +
+ + + + + + + + +

{{task.title}}

+

+ +
+
+ + +
+ + {{ 'core.hasdatatosync' | translate: {$a: moduleName} }} +
+ +
+ + + + +

{{ 'addon.mod_workshop.areainstructauthors' | translate }}

+ +
+
+ + + +

{{ 'addon.mod_workshop.yoursubmission' | translate }}

+

{{ 'addon.mod_workshop.noyoursubmission' | translate }}

+
+ + +
+ + + + + + + + +
+ + + + + +

{{ 'addon.mod_workshop.areainstructreviewers' | translate }}

+ +
+
+ + + +

{{ 'addon.mod_workshop.assignedassessments' | translate }}

+
+ +
+
+ + + +

{{ 'addon.mod_workshop.yoursubmission' | translate }}

+
+ +

{{ 'addon.mod_workshop.assignedassessments' | translate }}

+
+
+ + + + + +

{{ 'addon.mod_workshop.conclusion' | translate }}

+ +
+
+ + + +

{{ 'addon.mod_workshop.yourgrades' | translate }}

+
+ +

{{ 'addon.mod_workshop.submissiongrade' | translate }}

+ +
+ +

{{ 'addon.mod_workshop.gradinggrade' | translate }}

+ +
+
+ + + +

{{ 'addon.mod_workshop.publishedsubmissions' | translate }}

+
+ +
+
+ + + + +

{{ 'addon.mod_workshop.submissionsreport' | translate }}

+
+ +

{{ 'addon.mod_workshop.gradesreport' | translate }}

+
+ + {{ 'core.groupsseparate' | translate }} + {{ 'core.groupsvisible' | translate }} + + {{groupOpt.name}} + + + + + + + + + + + + + + + +
+
+
diff --git a/src/addon/mod/workshop/components/index/index.ts b/src/addon/mod/workshop/components/index/index.ts new file mode 100644 index 000000000..ecbb91bbf --- /dev/null +++ b/src/addon/mod/workshop/components/index/index.ts @@ -0,0 +1,485 @@ +// (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, Optional, Injector } from '@angular/core'; +import { Content, ModalController, NavController, Platform } from 'ionic-angular'; +import { CoreGroupInfo, CoreGroupsProvider } from '@providers/groups'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; +import { AddonModWorkshopProvider } from '../../providers/workshop'; +import { AddonModWorkshopHelperProvider } from '../../providers/helper'; +import { AddonModWorkshopSyncProvider } from '../../providers/sync'; +import { AddonModWorkshopOfflineProvider } from '../../providers/offline'; + +/** + * Component that displays a workshop index page. + */ +@Component({ + selector: 'addon-mod-workshop-index', + templateUrl: 'index.html', +}) +export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivityComponent { + @Input() group = 0; + + moduleName = 'workshop'; + workshop: any; + page = 0; + access: any; + phases: any; + grades: any; + assessments: any; + userGrades: any; + publishedSubmissions: any; + selectedPhase: number; + submission: any; + groupInfo: CoreGroupInfo = { + groups: [], + separateGroups: false, + visibleGroups: false + }; + canSubmit = false; + canAssess = false; + hasNextPage = false; + + workshopPhases = { + PHASE_SETUP: AddonModWorkshopProvider.PHASE_SETUP, + PHASE_SUBMISSION: AddonModWorkshopProvider.PHASE_SUBMISSION, + PHASE_ASSESSMENT: AddonModWorkshopProvider.PHASE_ASSESSMENT, + PHASE_EVALUATION: AddonModWorkshopProvider.PHASE_EVALUATION, + PHASE_CLOSED: AddonModWorkshopProvider.PHASE_CLOSED + }; + + protected offlineSubmissions = []; + protected supportedTasks = { // Add here native supported tasks. + submit: true + }; + protected obsSubmissionChanged: any; + protected obsAssessmentSaved: any; + protected appResumeSubscription: any; + + constructor(injector: Injector, private workshopProvider: AddonModWorkshopProvider, @Optional() content: Content, + private workshopOffline: AddonModWorkshopOfflineProvider, private groupsProvider: CoreGroupsProvider, + private navCtrl: NavController, private modalCtrl: ModalController, private utils: CoreUtilsProvider, + platform: Platform, private workshopHelper: AddonModWorkshopHelperProvider, + private workshopSync: AddonModWorkshopSyncProvider) { + super(injector, content); + + // Listen to submission and assessment changes. + this.obsSubmissionChanged = this.eventsProvider.on(AddonModWorkshopProvider.SUBMISSION_CHANGED, (data) => { + this.eventReceived(data); + }, this.siteId); + + // Listen to submission and assessment changes. + this.obsAssessmentSaved = this.eventsProvider.on(AddonModWorkshopProvider.ASSESSMENT_SAVED, (data) => { + this.eventReceived(data); + }, this.siteId); + + // Since most actions will take the user out of the app, we should refresh the view when the app is resumed. + this.appResumeSubscription = platform.resume.subscribe(() => { + this.content && this.content.scrollToTop(); + + this.loaded = false; + this.refreshContent(true, false); + }); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + super.ngOnInit(); + + this.loadContent(false, true).then(() => { + if (!this.workshop) { + return; + } + + this.workshopProvider.logView(this.workshop.id).then(() => { + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); + }); + }); + } + + /** + * Function called when we receive an event of submission changes. + * + * @param {any} data Data received by the event. + */ + protected eventReceived(data: any): void { + if ((this.workshop && this.workshop.id === data.workshopid) || data.cmid === module.id) { + this.content && this.content.scrollToTop(); + + this.loaded = false; + this.refreshContent(true, false); + // Check completion since it could be configured to complete once the user adds a new discussion or replies. + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); + } + } + + /** + * Perform the invalidate content function. + * + * @return {Promise} Resolved when done. + */ + protected invalidateContent(): Promise { + const promises = []; + + promises.push(this.workshopProvider.invalidateWorkshopData(this.courseId)); + if (this.workshop) { + promises.push(this.workshopProvider.invalidateWorkshopAccessInformationData(this.workshop.id)); + promises.push(this.workshopProvider.invalidateUserPlanPhasesData(this.workshop.id)); + if (this.canSubmit) { + promises.push(this.workshopProvider.invalidateSubmissionsData(this.workshop.id)); + } + if (this.access.canviewallsubmissions) { + promises.push(this.workshopProvider.invalidateGradeReportData(this.workshop.id)); + promises.push(this.groupsProvider.invalidateActivityAllowedGroups(this.workshop.coursemodule)); + promises.push(this.groupsProvider.invalidateActivityGroupMode(this.workshop.coursemodule)); + } + if (this.canAssess) { + promises.push(this.workshopProvider.invalidateReviewerAssesmentsData(this.workshop.id)); + } + promises.push(this.workshopProvider.invalidateGradesData(this.workshop.id)); + } + + return Promise.all(promises); + } + + /** + * Compares sync event data with current data to check if refresh content is needed. + * + * @param {any} syncEventData Data receiven on sync observer. + * @return {boolean} True if refresh is needed, false otherwise. + */ + protected isRefreshSyncNeeded(syncEventData: any): boolean { + if (this.workshop && syncEventData.workshopId == this.workshop.id) { + // Refresh the data. + this.content.scrollToTop(); + + return true; + } + + return false; + } + + /** + * Download feedback contents. + * + * @param {boolean} [refresh=false] If it's refreshing content. + * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [showErrors=false] If show errors to the user of hide them. + * @return {Promise} Promise resolved when done. + */ + protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + return this.workshopProvider.getWorkshop(this.courseId, this.module.id).then((workshop) => { + this.workshop = workshop; + + this.selectedPhase = workshop.phase; + + this.description = workshop.intro || workshop.description; + this.dataRetrieved.emit(workshop); + + if (sync) { + // Try to synchronize the feedback. + return this.syncActivity(showErrors); + } + }).then(() => { + // Check if there are answers stored in offline. + return this.workshopProvider.getWorkshopAccessInformation(this.workshop.id); + }).then((accessData) => { + this.access = accessData; + + if (accessData.canviewallsubmissions) { + return this.groupsProvider.getActivityGroupInfo(this.workshop.coursemodule, + accessData.canviewallsubmissions).then((groupInfo) => { + this.groupInfo = groupInfo; + + // Check selected group is accessible. + if (groupInfo && groupInfo.groups && groupInfo.groups.length > 0) { + const found = groupInfo.groups.some((group) => { + return group.id == this.group; + }); + if (!found) { + this.group = groupInfo.groups[0].id; + } + } + }); + } + + return Promise.resolve(); + }).then(() => { + return this.workshopProvider.getUserPlanPhases(this.workshop.id); + }).then((phases) => { + this.phases = phases; + + // Treat phases. + for (const x in phases) { + phases[x].tasks.forEach((task) => { + if (!task.link && (task.code == 'examples' || task.code == 'prepareexamples')) { + // Add links to manage examples. + task.link = this.externalUrl; + } else if (task.link && typeof this.supportedTasks[task.code] !== 'undefined') { + task.support = true; + } + }); + const action = phases[x].actions.find((action) => { + return action.url && action.type == 'switchphase'; + }); + phases[x].switchUrl = action ? action.url : ''; + } + + // Check if there are info stored in offline. + return this.workshopOffline.hasWorkshopOfflineData(this.workshop.id).then((hasOffline) => { + this.hasOffline = hasOffline; + if (hasOffline) { + return this.workshopOffline.getSubmissions(this.workshop.id).then((submissionsActions) => { + this.offlineSubmissions = submissionsActions; + }); + } else { + this.offlineSubmissions = []; + } + }); + }).then(() => { + return this.setPhaseInfo(); + }).then(() => { + // All data obtained, now fill the context menu. + this.fillContextMenu(refresh); + }); + } + + /** + * Retrieves and shows submissions grade page. + * + * @param {number} page Page number to be retrieved. + * @return {Promise} Resolved when done. + */ + gotoSubmissionsPage(page: number): Promise { + return this.workshopProvider.getGradesReport(this.workshop.id, this.group, page).then((report) => { + const numEntries = (report && report.grades && report.grades.length) || 0; + + this.page = page; + + this.hasNextPage = numEntries >= AddonModWorkshopProvider.PER_PAGE && ((this.page + 1) * + AddonModWorkshopProvider.PER_PAGE) < report.totalcount; + + this.grades = report.grades || []; + + this.grades.forEach((submission) => { + const actions = this.workshopHelper.filterSubmissionActions(this.offlineSubmissions, submission.submissionid + || false); + submission = this.workshopHelper.applyOfflineData(submission, actions); + + return this.workshopHelper.applyOfflineData(submission, actions).then((offlineSubmission) => { + submission = offlineSubmission; + }); + }); + }); + } + + /** + * Open task. + * + * @param {any} task Task to be done. + */ + runTask(task: any): void { + if (task.support) { + if (task.code == 'submit' && this.canSubmit && ((this.access.creatingsubmissionallowed && !this.submission) || + (this.access.modifyingsubmissionallowed && this.submission))) { + const params = { + module: module, + access: this.access, + courseId: this.courseId, + submission: this.submission + }; + + if (this.submission.id) { + params['submissionId'] = this.submission.id; + } + + this.navCtrl.push('AddonModWorkshopEditSubmissionPage', params); + } + } else if (task.link) { + this.utils.openInBrowser(task.link); + } + } + + /** + * Run task link on current phase. + * + * @param {string} taskCode Code related to the task to run. + */ + runTaskByCode(taskCode: string): void { + const task = this.workshopHelper.getTask(this.phases[this.workshop.phase].tasks, taskCode); + + return task ? this.runTask(task) : null; + } + + /** + * Select Phase to be shown. + */ + selectPhase(): void { + if (this.phases) { + const modal = this.modalCtrl.create('AddonModWorkshopPhaseSelectorPage', { + phases: this.utils.objectToArray(this.phases), + selected: this.selectedPhase, + workshopPhase: this.workshop.phase + }); + modal.onDidDismiss((phase) => { + // Add data to search object. + typeof phase != 'undefined' && this.switchPhase(phase); + }); + modal.present(); + } + } + + /** + * Set group to see the workshop. + * @param {number} groupId Group Id. + * @return {Promise} Promise resolved when done. + */ + setGroup(groupId: number): Promise { + this.group = groupId; + + return this.gotoSubmissionsPage(0); + } + + /** + * Convenience function to set current phase information. + * + * @return {Promise} Promise resolved when done. + */ + protected setPhaseInfo(): Promise { + this.submission = false; + this.canAssess = false; + this.assessments = false; + this.userGrades = false; + this.publishedSubmissions = false; + + this.canSubmit = this.workshopHelper.canSubmit(this.workshop, this.access, + this.phases[AddonModWorkshopProvider.PHASE_SUBMISSION].tasks); + + const promises = []; + + if (this.canSubmit) { + promises.push(this.workshopHelper.getUserSubmission(this.workshop.id).then((submission) => { + const actions = this.workshopHelper.filterSubmissionActions(this.offlineSubmissions, submission.id || false); + + return this.workshopHelper.applyOfflineData(submission, actions).then((submission) => { + this.submission = submission; + }); + })); + } + + if (this.access.canviewallsubmissions && this.workshop.phase >= AddonModWorkshopProvider.PHASE_SUBMISSION) { + promises.push(this.gotoSubmissionsPage(this.page)); + } + + let assessPromise = Promise.resolve(); + + if (this.workshop.phase >= AddonModWorkshopProvider.PHASE_ASSESSMENT) { + this.canAssess = this.workshopHelper.canAssess(this.workshop, this.access); + if (this.canAssess) { + assessPromise = this.workshopHelper.getReviewerAssessments(this.workshop.id).then((assessments) => { + const p2 = []; + + assessments.forEach((assessment) => { + assessment.strategy = this.workshop.strategy; + if (this.hasOffline) { + p2.push(this.workshopOffline.getAssessment(this.workshop.id, assessment.id) + .then((offlineAssessment) => { + assessment.offline = true; + assessment.timemodified = Math.floor(offlineAssessment.timemodified / 1000); + }).catch(() => { + // Ignore errors. + })); + } + }); + + return Promise.all(p2).then(() => { + this.assessments = assessments; + }); + }); + promises.push(assessPromise); + } + } + + if (this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) { + promises.push(this.workshopProvider.getGrades(this.workshop.id).then((grades) => { + this.userGrades = grades.submissionlongstrgrade || grades.assessmentlongstrgrade ? grades : false; + })); + + if (this.access.canviewpublishedsubmissions) { + promises.push(assessPromise.then(() => { + return this.workshopProvider.getSubmissions(this.workshop.id).then((submissions) => { + this.publishedSubmissions = submissions.filter((submission) => { + if (submission.published) { + this.assessments.forEach((assessment) => { + submission.reviewedby = []; + if (assessment.submissionid == submission.id) { + submission.reviewedby.push(this.workshopHelper.realGradeValue(this.workshop, assessment)); + } + }); + + return true; + } + + return false; + }); + }); + })); + } + } + + return Promise.all(promises); + } + + /** + * Switch shown phase. + * + * @param {number} phase Selected phase. + */ + switchPhase(phase: number): void { + this.selectedPhase = phase; + this.page = 0; + } + + /** + * Performs the sync of the activity. + * + * @return {Promise} Promise resolved when done. + */ + protected sync(): Promise { + return this.workshopSync.syncWorkshop(this.workshop.id); + } + + /** + * Checks if sync has succeed from result sync data. + * + * @param {any} result Data returned on the sync function. + * @return {boolean} If suceed or not. + */ + protected hasSyncSucceed(result: any): boolean { + return result.updated; + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + super.ngOnDestroy(); + this.obsSubmissionChanged && this.obsSubmissionChanged.off(); + this.obsAssessmentSaved && this.obsAssessmentSaved.off(); + this.appResumeSubscription && this.appResumeSubscription.unsubscribe(); + } +} diff --git a/src/addon/mod/workshop/lang/en.json b/src/addon/mod/workshop/lang/en.json new file mode 100644 index 000000000..57243db4a --- /dev/null +++ b/src/addon/mod/workshop/lang/en.json @@ -0,0 +1,60 @@ +{ + "alreadygraded": "Already graded", + "areainstructauthors": "Instructions for submission", + "areainstructreviewers": "Instructions for assessment", + "assess": "Assess", + "assessedsubmission": "Assessed submission", + "assessmentform": "Assessment form", + "assessmentsettings": "Assessment settings", + "assessmentstrategynotsupported": "Assessment strategy {{$a}} not supported", + "assessmentweight": "Assessment weight", + "assignedassessments": "Assigned submissions to assess", + "conclusion": "Conclusion", + "createsubmission": "Start preparing your submission", + "deletesubmission": "Delete submission", + "editsubmission": "Edit submission", + "feedbackauthor": "Feedback for the author", + "feedbackby": "Feedback by {{$a}}", + "feedbackreviewer": "Feedback for the reviewer", + "givengrades": "Grades given", + "gradecalculated": "Calculated grade for submission", + "gradeinfo": "Grade: {{$a.received}} of {{$a.max}}", + "gradeover": "Override grade for submission", + "gradesreport": "Workshop grades report", + "gradinggrade": "Grade for assessment", + "gradinggradecalculated": "Calculated grade for assessment", + "gradinggradeof": "Grade for assessment (of {{$a}})", + "gradinggradeover": "Override grade for assessment", + "nogradeyet": "No grade yet", + "notassessed": "Not assessed yet", + "notoverridden": "Not overridden", + "noyoursubmission": "You have not submitted your work yet", + "overallfeedback": "Overall feedback", + "publishedsubmissions": "Published submissions", + "publishsubmission": "Publish submission", + "publishsubmission_help": "Published submissions are available to the others when the workshop is closed.", + "reassess": "Re-assess", + "receivedgrades": "Grades received", + "selectphase": "Select phase", + "submissionattachment": "Attachment", + "submissioncontent": "Submission content", + "submissiondeleteconfirm": "Are you sure you want to delete the following submission?", + "submissiongrade": "Grade for submission", + "submissiongradeof": "Grade for submission (of {{$a}})", + "submissionrequiredcontent": "You need to enter some text or add a file.", + "submissionsreport": "Workshop submissions report", + "submissiontitle": "Title", + "switchphase10": "Switch to the setup phase", + "switchphase20": "Switch to the submission phase", + "switchphase30": "Switch to the assessment phase", + "switchphase40": "Switch to the evaluation phase", + "switchphase50": "Close workshop", + "userplancurrentphase": "Current phase", + "warningassessmentmodified": "The submission was modified on the site.", + "warningsubmissionmodified": "The assessment was modified on the site.", + "weightinfo": "Weight: {{$a}}", + "yourassessment": "Your assessment", + "yourassessmentfor": "Your assessment for {{$a}}", + "yourgrades": "Your grades", + "yoursubmission": "Your submission" +} diff --git a/src/addon/mod/workshop/pages/index/index.html b/src/addon/mod/workshop/pages/index/index.html new file mode 100644 index 000000000..6a977cfa3 --- /dev/null +++ b/src/addon/mod/workshop/pages/index/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/addon/mod/workshop/pages/index/index.module.ts b/src/addon/mod/workshop/pages/index/index.module.ts new file mode 100644 index 000000000..b18d9e2ef --- /dev/null +++ b/src/addon/mod/workshop/pages/index/index.module.ts @@ -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 { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonModWorkshopComponentsModule } from '../../components/components.module'; +import { AddonModWorkshopIndexPage } from './index'; + +@NgModule({ + declarations: [ + AddonModWorkshopIndexPage, + ], + imports: [ + CoreDirectivesModule, + AddonModWorkshopComponentsModule, + IonicPageModule.forChild(AddonModWorkshopIndexPage), + TranslateModule.forChild() + ], +}) +export class AddonModWorkshopIndexPageModule {} diff --git a/src/addon/mod/workshop/pages/index/index.ts b/src/addon/mod/workshop/pages/index/index.ts new file mode 100644 index 000000000..db244a7d1 --- /dev/null +++ b/src/addon/mod/workshop/pages/index/index.ts @@ -0,0 +1,50 @@ +// (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, ViewChild } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { AddonModWorkshopIndexComponent } from '../../components/index/index'; + +/** + * Page that displays a workshop. + */ +@IonicPage({ segment: 'addon-mod-workshop-index' }) +@Component({ + selector: 'page-addon-mod-workshop-index', + templateUrl: 'index.html', +}) +export class AddonModWorkshopIndexPage { + @ViewChild(AddonModWorkshopIndexComponent) workshopComponent: AddonModWorkshopIndexComponent; + + title: string; + module: any; + courseId: number; + selectedGroup: number; + + constructor(navParams: NavParams) { + this.module = navParams.get('module') || {}; + this.courseId = navParams.get('courseId'); + this.selectedGroup = navParams.get('group') || 0; + this.title = this.module.name; + } + + /** + * Update some data based on the workshop instance. + * + * @param {any} workshop Workshop instance. + */ + updateData(workshop: any): void { + this.title = workshop.name || this.title; + } +} diff --git a/src/addon/mod/workshop/pages/phase/phase.html b/src/addon/mod/workshop/pages/phase/phase.html new file mode 100644 index 000000000..a8ff1f37a --- /dev/null +++ b/src/addon/mod/workshop/pages/phase/phase.html @@ -0,0 +1,25 @@ + + + {{ 'addon.mod_workshop.selectphase' | translate }} + + + + + + + + + + {{ phase.title }} +

{{ 'addon.mod_workshop.userplancurrentphase' | translate }}

+
+ +
+ + {{ phase.title }} + +
+
+
diff --git a/src/addon/mod/workshop/pages/phase/phase.module.ts b/src/addon/mod/workshop/pages/phase/phase.module.ts new file mode 100644 index 000000000..2085e6ca9 --- /dev/null +++ b/src/addon/mod/workshop/pages/phase/phase.module.ts @@ -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 { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonModWorkshopPhaseSelectorPage } from './phase'; +import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module'; + +@NgModule({ + declarations: [ + AddonModWorkshopPhaseSelectorPage, + ], + imports: [ + CoreDirectivesModule, + CoreCompileHtmlComponentModule, + IonicPageModule.forChild(AddonModWorkshopPhaseSelectorPage), + TranslateModule.forChild() + ], +}) +export class AddonModWorkshopPhaseSelectorPageModule {} diff --git a/src/addon/mod/workshop/pages/phase/phase.ts b/src/addon/mod/workshop/pages/phase/phase.ts new file mode 100644 index 000000000..f1fe2fdcc --- /dev/null +++ b/src/addon/mod/workshop/pages/phase/phase.ts @@ -0,0 +1,56 @@ +// (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 } from '@angular/core'; +import { IonicPage, NavParams, ViewController } from 'ionic-angular'; + +/** + * Page that displays the phase selector modal. + */ +@IonicPage({ segment: 'addon-mod-workshop-phase-selector' }) +@Component({ + selector: 'page-addon-mod-workshop-phase-selector', + templateUrl: 'phase.html', +}) +export class AddonModWorkshopPhaseSelectorPage { + selected: number; + phases: any; + workshopPhase: number; + protected original: number; + + constructor(params: NavParams, private viewCtrl: ViewController) { + this.selected = params.get('selected'); + this.original = this.selected; + this.phases = params.get('phases'); + this.workshopPhase = params.get('workshopPhase'); + } + + /** + * Close modal. + */ + closeModal(): void { + this.viewCtrl.dismiss(); + } + + /** + * Select phase. + */ + switchPhase(): void { + // This is a quick hack to avoid the first switch phase call done just when opening the modal. + if (this.original != this.selected) { + this.viewCtrl.dismiss(this.selected); + } + this.original = null; + } +} diff --git a/src/addon/mod/workshop/providers/helper.ts b/src/addon/mod/workshop/providers/helper.ts index 48736cd8b..4007b223a 100644 --- a/src/addon/mod/workshop/providers/helper.ts +++ b/src/addon/mod/workshop/providers/helper.ts @@ -268,7 +268,7 @@ export class AddonModWorkshopHelperProvider { } /** - * Get a list of stored attachment files for a submission. See $mmaModWorkshopHelper#storeFiles. + * Get a list of stored attachment files for a submission. See AddonModWorkshopHelperProvider#storeFiles. * * @param {number} workshopId Workshop ID. * @param {number} submissionId If not editing, it will refer to timecreated. @@ -286,7 +286,7 @@ export class AddonModWorkshopHelperProvider { } /** - * Get a list of stored attachment files for a submission and online files also. See $mmaModWorkshopHelper#storeFiles. + * Get a list of stored attachment files for a submission and online files also. See AddonModWorkshopHelperProvider#storeFiles. * * @param {any} filesObject Files object combining offline and online information. * @param {number} workshopId Workshop ID. @@ -355,7 +355,7 @@ export class AddonModWorkshopHelperProvider { } /** - * Get a list of stored attachment files for an assessment. See $mmaModWorkshopHelper#storeFiles. + * Get a list of stored attachment files for an assessment. See AddonModWorkshopHelperProvider#storeFiles. * * @param {number} workshopId Workshop ID. * @param {number} assessmentId Assessment ID. @@ -372,7 +372,7 @@ export class AddonModWorkshopHelperProvider { } /** - * Get a list of stored attachment files for an assessment and online files also. See $mmaModWorkshopHelper#storeFiles. + * Get a list of stored attachment files for an assessment and online files also. See AddonModWorkshopHelperProvider#storeFiles. * * @param {object} filesObject Files object combining offline and online information. * @param {number} workshopId Workshop ID. diff --git a/src/addon/mod/workshop/providers/link-handler.ts b/src/addon/mod/workshop/providers/link-handler.ts new file mode 100644 index 000000000..6c06a0ca6 --- /dev/null +++ b/src/addon/mod/workshop/providers/link-handler.ts @@ -0,0 +1,30 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { AddonModWorkshopProvider } from './workshop'; + +/** + * Handler to treat links to workshop. + */ +@Injectable() +export class AddonModWorkshopLinkHandler extends CoreContentLinksModuleIndexHandler { + name = 'AddonModWorkshopLinkHandler'; + + constructor(courseHelper: CoreCourseHelperProvider) { + super(courseHelper, AddonModWorkshopProvider.COMPONENT, 'workshop'); + } +} diff --git a/src/addon/mod/workshop/providers/module-handler.ts b/src/addon/mod/workshop/providers/module-handler.ts new file mode 100644 index 000000000..bb275e5d9 --- /dev/null +++ b/src/addon/mod/workshop/providers/module-handler.ts @@ -0,0 +1,72 @@ +// (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 { Injectable } from '@angular/core'; +import { NavController, NavOptions } from 'ionic-angular'; +import { AddonModWorkshopIndexComponent } from '../components/index/index'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { AddonModWorkshopProvider } from './workshop'; + +/** + * Handler to support workshop modules. + */ +@Injectable() +export class AddonModWorkshopModuleHandler implements CoreCourseModuleHandler { + name = 'AddonModWorkshop'; + modName = 'workshop'; + + constructor(private courseProvider: CoreCourseProvider, private workshopProvider: AddonModWorkshopProvider) { } + + /** + * Check if the handler is enabled on a site level. + * + * @return {Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): Promise { + return this.workshopProvider.isPluginEnabled(); + } + + /** + * Get the data required to display the module in the course contents view. + * + * @param {any} module The module object. + * @param {number} courseId The course ID. + * @param {number} sectionId The section ID. + * @return {CoreCourseModuleHandlerData} Data to render the module. + */ + getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData { + return { + icon: this.courseProvider.getModuleIconSrc('workshop'), + title: module.name, + class: 'addon-mod_workshop-handler', + showDownloadButton: true, + action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { + navCtrl.push('AddonModWorkshopIndexPage', {module: module, courseId: courseId}, options); + } + }; + } + + /** + * Get the component to render the module. This is needed to support singleactivity course format. + * The component returned must implement CoreCourseModuleMainComponent. + * + * @param {any} course The course object. + * @param {any} module The module object. + * @return {any} The component to use, undefined if not found. + */ + getMainComponent(course: any, module: any): any { + return AddonModWorkshopIndexComponent; + } +} diff --git a/src/addon/mod/workshop/providers/offline.ts b/src/addon/mod/workshop/providers/offline.ts index 6ac09becb..c74a1b48e 100644 --- a/src/addon/mod/workshop/providers/offline.ts +++ b/src/addon/mod/workshop/providers/offline.ts @@ -418,7 +418,7 @@ export class AddonModWorkshopOfflineProvider { getAllAssessments(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.getDb().getRecords(this.ASSESSMENTS_TABLE).then((records) => { - records.forEach(this.parseAssessnentRecord.bind(this)); + records.forEach(this.parseAssessmentRecord.bind(this)); return records; }); @@ -439,7 +439,7 @@ export class AddonModWorkshopOfflineProvider { }; return site.getDb().getRecords(this.ASSESSMENTS_TABLE, conditions).then((records) => { - records.forEach(this.parseAssessnentRecord.bind(this)); + records.forEach(this.parseAssessmentRecord.bind(this)); return records; }); @@ -462,7 +462,7 @@ export class AddonModWorkshopOfflineProvider { }; return site.getDb().getRecord(this.ASSESSMENTS_TABLE, conditions).then((record) => { - this.parseAssessnentRecord(record); + this.parseAssessmentRecord(record); return record; }); @@ -498,7 +498,7 @@ export class AddonModWorkshopOfflineProvider { * * @param {any} record Assessnent record, modified in place. */ - protected parseAssessnentRecord(record: any): void { + protected parseAssessmentRecord(record: any): void { record.inputdata = this.textUtils.parseJSON(record.inputdata); } diff --git a/src/addon/mod/workshop/providers/workshop.ts b/src/addon/mod/workshop/providers/workshop.ts index 5ce7d3a79..9962b836d 100644 --- a/src/addon/mod/workshop/providers/workshop.ts +++ b/src/addon/mod/workshop/providers/workshop.ts @@ -24,7 +24,7 @@ import { AddonModWorkshopOfflineProvider } from './offline'; */ @Injectable() export class AddonModWorkshopProvider { - static COMPONENT = 'mmaWorkshopUrl'; + static COMPONENT = 'mmaModWorkshop'; static PER_PAGE = 10; static PHASE_SETUP = 10; static PHASE_SUBMISSION = 20; @@ -35,6 +35,9 @@ export class AddonModWorkshopProvider { static EXAMPLES_BEFORE_SUBMISSION: 1; static EXAMPLES_BEFORE_ASSESSMENT: 2; + static SUBMISSION_CHANGED = 'addon_mod_workshop_submission_changed'; + static ASSESSMENT_SAVED = 'addon_mod_workshop_assessment_saved'; + protected ROOT_CACHE_KEY = 'mmaModWorkshop:'; constructor( @@ -346,12 +349,7 @@ export class AddonModWorkshopProvider { return site.read('mod_workshop_get_user_plan', params, preSets).then((response) => { if (response && response.userplan && response.userplan.phases) { - const phases = {}; - response.userplan.phases.forEach((phase) => { - phases[phase.code] = phase; - }); - - return phases; + return this.utils.arrayToObject(response.userplan.phases, 'code'); } return Promise.reject(null); @@ -1295,7 +1293,7 @@ export class AddonModWorkshopProvider { /** * Invalidate the prefetched content except files. - * To invalidate files, use $mmaModWorkshop#invalidateFiles. + * To invalidate files, use AddonModWorkshopProvider#invalidateFiles. * * @param {number} moduleId The module ID. * @param {number} courseId Course ID. diff --git a/src/addon/mod/workshop/workshop.module.ts b/src/addon/mod/workshop/workshop.module.ts new file mode 100644 index 000000000..2653be857 --- /dev/null +++ b/src/addon/mod/workshop/workshop.module.ts @@ -0,0 +1,49 @@ +// (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 { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; +import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; +import { AddonModWorkshopComponentsModule } from './components/components.module'; +import { AddonModWorkshopModuleHandler } from './providers/module-handler'; +import { AddonModWorkshopProvider } from './providers/workshop'; +import { AddonModWorkshopLinkHandler } from './providers/link-handler'; +import { AddonModWorkshopOfflineProvider } from './providers/offline'; +import { AddonModWorkshopSyncProvider } from './providers/sync'; +import { AddonModWorkshopHelperProvider } from './providers/helper'; +import { AddonWorkshopAssessmentStrategyDelegate } from './providers/assessment-strategy-delegate'; + +@NgModule({ + declarations: [ + ], + imports: [ + AddonModWorkshopComponentsModule + ], + providers: [ + AddonModWorkshopProvider, + AddonModWorkshopModuleHandler, + AddonModWorkshopLinkHandler, + AddonModWorkshopOfflineProvider, + AddonModWorkshopSyncProvider, + AddonModWorkshopHelperProvider, + AddonWorkshopAssessmentStrategyDelegate + ] +}) +export class AddonModWorkshopModule { + constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModWorkshopModuleHandler, + contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModWorkshopLinkHandler) { + moduleDelegate.registerHandler(moduleHandler); + contentLinksDelegate.registerHandler(linkHandler); + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 8737ec72f..d11018cb9 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -96,6 +96,7 @@ import { AddonModQuizModule } from '@addon/mod/quiz/quiz.module'; import { AddonModScormModule } from '@addon/mod/scorm/scorm.module'; import { AddonModUrlModule } from '@addon/mod/url/url.module'; import { AddonModSurveyModule } from '@addon/mod/survey/survey.module'; +import { AddonModWorkshopModule } from '@addon/mod/workshop/workshop.module'; import { AddonModImscpModule } from '@addon/mod/imscp/imscp.module'; import { AddonModWikiModule } from '@addon/mod/wiki/wiki.module'; import { AddonMessageOutputModule } from '@addon/messageoutput/messageoutput.module'; @@ -202,6 +203,7 @@ export const CORE_PROVIDERS: any[] = [ AddonModScormModule, AddonModUrlModule, AddonModSurveyModule, + AddonModWorkshopModule, AddonModImscpModule, AddonModWikiModule, AddonMessageOutputModule, From afc2fc1f643a142417a4c4334ad632efe55537c3 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Thu, 31 May 2018 13:45:31 +0200 Subject: [PATCH 08/20] MOBILE-2354 workshop: Prefech and sync cron handlers --- .../workshop/providers/prefetch-handler.ts | 368 ++++++++++++++++++ .../workshop/providers/sync-cron-handler.ts | 47 +++ src/addon/mod/workshop/workshop.module.ts | 14 +- 3 files changed, 427 insertions(+), 2 deletions(-) create mode 100644 src/addon/mod/workshop/providers/prefetch-handler.ts create mode 100644 src/addon/mod/workshop/providers/sync-cron-handler.ts diff --git a/src/addon/mod/workshop/providers/prefetch-handler.ts b/src/addon/mod/workshop/providers/prefetch-handler.ts new file mode 100644 index 000000000..1c641fb57 --- /dev/null +++ b/src/addon/mod/workshop/providers/prefetch-handler.ts @@ -0,0 +1,368 @@ +// (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 { Injectable, Injector } from '@angular/core'; +import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler'; +import { CoreGroupsProvider } from '@providers/groups'; +import { CoreUserProvider } from '@core/user/providers/user'; +import { AddonModWorkshopProvider } from './workshop'; +import { AddonModWorkshopHelperProvider } from './helper'; + +/** + * Handler to prefetch workshops. + */ +@Injectable() +export class AddonModWorkshopPrefetchHandler extends CoreCourseModulePrefetchHandlerBase { + name = 'AddonModWorkshop'; + modName = 'workshop'; + component = AddonModWorkshopProvider.COMPONENT; + updatesNames = new RegExp('^configuration$|^.*files$|^completion|^gradeitems$|^outcomes$|^submissions$|^assessments$' + + '|^assessmentgrades$|^usersubmissions$|^userassessments$|^userassessmentgrades$|^userassessmentgrades$'); + + constructor(injector: Injector, + private groupsProvider: CoreGroupsProvider, + private userProvider: CoreUserProvider, + private workshopProvider: AddonModWorkshopProvider, + private workshopHelper: AddonModWorkshopHelperProvider) { + super(injector); + } + + /** + * Download the module. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch. + * @return {Promise} Promise resolved when all content is downloaded. + */ + download(module: any, courseId: number, dirPath?: string): Promise { + // Workshop cannot be downloaded right away, only prefetched. + return this.prefetch(module, courseId, false, dirPath); + } + + /** + * Get list of files. If not defined, we'll assume they're in module.contents. + * + * @param {any} module Module. + * @param {Number} courseId Course ID the module belongs to. + * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. + * @return {Promise} Promise resolved with the list of files. + */ + getFiles(module: any, courseId: number, single?: boolean): Promise { + return this.getWorkshopInfoHelper(module, courseId, true).then((info) => { + return info.files; + }); + } + + /** + * Helper function to get all workshop info just once. + * + * @param {any} module Module to get the files. + * @param {number} courseId Course ID the module belongs to. + * @param {boolean} [omitFail=false] True to always return even if fails. Default false. + * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the info fetched. + */ + protected getWorkshopInfoHelper(module: any, courseId: number, omitFail: boolean = false, forceCache: boolean = false, + ignoreCache: boolean = false, siteId?: string): Promise { + let workshop, + groups = [], + files = [], + access; + + return this.sitesProvider.getSite(siteId).then((site) => { + const userId = site.getUserId(); + + return this.workshopProvider.getWorkshop(courseId, module.id, siteId, forceCache).then((data) => { + files = this.getIntroFilesFromInstance(module, data); + files = files.concat(data.instructauthorsfiles).concat(data.instructreviewersfiles); + workshop = data; + + return this.workshopProvider.getWorkshopAccessInformation(workshop.id, false, true, siteId).then((accessData) => { + access = accessData; + if (access.canviewallsubmissions) { + return this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId).then((groupInfo) => { + if (!groupInfo.groups || groupInfo.groups.length == 0) { + groupInfo.groups = [{id: 0}]; + } + groups = groupInfo.groups; + }); + } + }); + }).then(() => { + return this.workshopProvider.getUserPlanPhases(workshop.id, false, true, siteId).then((phases) => { + // Get submission phase info. + const submissionPhase = phases[AddonModWorkshopProvider.PHASE_SUBMISSION], + canSubmit = this.workshopHelper.canSubmit(workshop, access, submissionPhase.tasks), + canAssess = this.workshopHelper.canAssess(workshop, access), + promises = []; + + if (canSubmit) { + promises.push(this.workshopHelper.getUserSubmission(workshop.id, userId).then((submission) => { + if (submission) { + files = files.concat(submission.contentfiles).concat(submission.attachmentfiles); + } + })); + } + + if (access.canviewallsubmissions && workshop.phase >= AddonModWorkshopProvider.PHASE_SUBMISSION) { + promises.push(this.workshopProvider.getSubmissions(workshop.id).then((submissions) => { + const promises2 = []; + submissions.forEach((submission) => { + files = files.concat(submission.contentfiles).concat(submission.attachmentfiles); + promises2.push(this.workshopProvider.getSubmissionAssessments(workshop.id, submission.id) + .then((assessments) => { + assessments.forEach((assessment) => { + files = files.concat(assessment.feedbackattachmentfiles) + .concat(assessment.feedbackcontentfiles); + }); + })); + }); + + return Promise.all(promises2); + })); + } + + // Get assessment files. + if (workshop.phase >= AddonModWorkshopProvider.PHASE_ASSESSMENT && canAssess) { + promises.push(this.workshopHelper.getReviewerAssessments(workshop.id).then((assessments) => { + assessments.forEach((assessment) => { + files = files.concat(assessment.feedbackattachmentfiles).concat(assessment.feedbackcontentfiles); + }); + })); + } + + return Promise.all(promises); + }); + }); + }).then(() => { + return { + workshop: workshop, + groups: groups, + files: files.filter((file) => typeof file !== 'undefined') + }; + }).catch((message) => { + if (omitFail) { + // Any error, return the info we have. + return Promise.resolve({ + workshop: workshop, + groups: groups, + files: files.filter((file) => typeof file !== 'undefined') + }); + } + + return Promise.reject(message); + }); + } + + /** + * Invalidate the prefetched content. + * + * @param {number} moduleId The module ID. + * @param {number} courseId The course ID the module belongs to. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number): Promise { + return this.workshopProvider.invalidateContent(moduleId, courseId); + } + + /** + * Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable. + * + * @param {any} module Module. + * @param {number} courseId Course ID the module belongs to. + * @return {boolean|Promise} Whether the module can be downloaded. The promise should never be rejected. + */ + isDownloadable(module: any, courseId: number): boolean | Promise { + return this.workshopProvider.getWorkshop(courseId, module.id, undefined, true).then((workshop) => { + return this.workshopProvider.getWorkshopAccessInformation(workshop.id).then((accessData) => { + // Check if workshop is setup by phase. + return accessData.canswitchphase || workshop.phase > AddonModWorkshopProvider.PHASE_SETUP; + }); + }); + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled. + */ + isEnabled(): boolean | Promise { + return this.workshopProvider.isPluginEnabled(); + } + + /** + * Prefetch a module. + * + * @param {any} module Module. + * @param {number} courseId Course ID the module belongs to. + * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. + * @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch. + * @return {Promise} Promise resolved when done. + */ + prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise { + return this.prefetchPackage(module, courseId, single, this.prefetchWorkshop.bind(this)); + } + + /** + * Retrieves all the grades reports for all the groups and then returns only unique grades. + * + * @param {number} workshopId Workshop ID. + * @param {any[]} groups Array of groups in the activity. + * @param {string} siteId Site ID. If not defined, current site. + * @return {Promise} All unique entries. + */ + protected getAllGradesReport(workshopId: number, groups: any[], siteId: string): Promise { + const promises = []; + + groups.forEach((group) => { + promises.push(this.workshopProvider.fetchAllGradeReports( + workshopId, group.id, undefined, false, false, siteId)); + }); + + return Promise.all(promises).then((grades) => { + const uniqueGrades = {}; + + grades.forEach((groupGrades) => { + groupGrades.forEach((grade) => { + if (grade.submissionid) { + uniqueGrades[grade.submissionid] = grade; + } + }); + }); + + return uniqueGrades; + }); + } + + /** + * Prefetch a workshop. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID the module belongs to. + * @param {boolean} single True if we're downloading a single module, false if we're downloading a whole section. + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved when done. + */ + protected prefetchWorkshop(module: any, courseId: number, single: boolean, siteId: string): Promise { + const userIds = []; + + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.sitesProvider.getSite(siteId).then((site) => { + const currentUserId = site.getUserId(); + + // Prefetch the workshop data. + return this.getWorkshopInfoHelper(module, courseId, false, false, true, siteId).then((info) => { + const workshop = info.workshop, + promises = [], + assessments = []; + + promises.push(this.filepoolProvider.addFilesToQueue(siteId, info.files, this.component, module.id)); + promises.push(this.workshopProvider.getWorkshopAccessInformation(workshop.id, false, true, siteId) + .then((access) => { + return this.workshopProvider.getUserPlanPhases(workshop.id, false, true, siteId).then((phases) => { + + // Get submission phase info. + const submissionPhase = phases[AddonModWorkshopProvider.PHASE_SUBMISSION], + canSubmit = this.workshopHelper.canSubmit(workshop, access, submissionPhase.tasks), + canAssess = this.workshopHelper.canAssess(workshop, access), + promises2 = []; + + if (canSubmit) { + promises2.push(this.workshopProvider.getSubmissions(workshop.id)); + // Add userId to the profiles to prefetch. + userIds.push(currentUserId); + } + + let reportPromise = Promise.resolve(); + if (access.canviewallsubmissions && workshop.phase >= AddonModWorkshopProvider.PHASE_SUBMISSION) { + reportPromise = this.getAllGradesReport(workshop.id, info.groups, siteId) + .then((grades) => { + grades.forEach((grade) => { + userIds.push(grade.userid); + userIds.push(grade.gradeoverby); + + grade.reviewedby.forEach((assessment) => { + userIds.push(assessment.userid); + userIds.push(assessment.gradinggradeoverby); + assessments[assessment.assessmentid] = assessment; + }); + + grade.reviewerof.forEach((assessment) => { + userIds.push(assessment.userid); + userIds.push(assessment.gradinggradeoverby); + assessments[assessment.assessmentid] = assessment; + }); + }); + }); + } + + if (workshop.phase >= AddonModWorkshopProvider.PHASE_ASSESSMENT && canAssess) { + // Wait the report promise to finish to override assessments array if needed. + reportPromise = reportPromise.finally(() => { + return this.workshopHelper.getReviewerAssessments(workshop.id, currentUserId, undefined, + undefined, siteId).then((revAssessments) => { + let p = Promise.resolve(); + revAssessments.forEach((assessment) => { + if (assessment.submission.authorid == currentUserId) { + p = this.workshopProvider.getAssessment(workshop.id, assessment.id); + } + userIds.push(assessment.reviewerid); + userIds.push(assessment.gradinggradeoverby); + assessments[assessment.id] = assessment; + }); + + return p; + }); + }); + } + + if (assessments.length > 0) { + reportPromise = reportPromise.finally(() => { + const promises3 = []; + assessments.forEach((assessment, id) => { + promises3.push(this.workshopProvider.getAssessmentForm(workshop.id, id, undefined, undefined, + undefined, siteId)); + }); + + return Promise.all(promises3); + }); + } + promises2.push(reportPromise); + + if (workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) { + promises2.push(this.workshopProvider.getGrades(workshop.id)); + if (access.canviewpublishedsubmissions) { + promises2.push(this.workshopProvider.getSubmissions(workshop.id)); + } + } + + return Promise.all(promises2); + }); + })); + // Add Basic Info to manage links. + promises.push(this.courseProvider.getModuleBasicInfoByInstance(workshop.id, 'workshop', siteId)); + promises.push(this.courseProvider.getModuleBasicGradeInfo(module.id, siteId)); + + return Promise.all(promises); + }); + }).then(() => { + // Prefetch user profiles. + return this.userProvider.prefetchProfiles(userIds, courseId, siteId); + }); + } +} diff --git a/src/addon/mod/workshop/providers/sync-cron-handler.ts b/src/addon/mod/workshop/providers/sync-cron-handler.ts new file mode 100644 index 000000000..86bf07017 --- /dev/null +++ b/src/addon/mod/workshop/providers/sync-cron-handler.ts @@ -0,0 +1,47 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreCronHandler } from '@providers/cron'; +import { AddonModWorkshopSyncProvider } from './sync'; + +/** + * Synchronization cron handler. + */ +@Injectable() +export class AddonModWorkshopSyncCronHandler implements CoreCronHandler { + name = 'AddonModWorkshopSyncCronHandler'; + + constructor(private workshopSync: AddonModWorkshopSyncProvider) {} + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param {string} [siteId] ID of the site affected, undefined for all sites. + * @return {Promise} Promise resolved when done, rejected if failure. + */ + execute(siteId?: string): Promise { + return this.workshopSync.syncAllWorkshops(siteId); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return AddonModWorkshopSyncProvider.SYNC_TIME; + } +} diff --git a/src/addon/mod/workshop/workshop.module.ts b/src/addon/mod/workshop/workshop.module.ts index 2653be857..2c1c864e9 100644 --- a/src/addon/mod/workshop/workshop.module.ts +++ b/src/addon/mod/workshop/workshop.module.ts @@ -13,8 +13,10 @@ // limitations under the License. import { NgModule } from '@angular/core'; +import { CoreCronDelegate } from '@providers/cron'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { AddonModWorkshopComponentsModule } from './components/components.module'; import { AddonModWorkshopModuleHandler } from './providers/module-handler'; import { AddonModWorkshopProvider } from './providers/workshop'; @@ -23,6 +25,8 @@ import { AddonModWorkshopOfflineProvider } from './providers/offline'; import { AddonModWorkshopSyncProvider } from './providers/sync'; import { AddonModWorkshopHelperProvider } from './providers/helper'; import { AddonWorkshopAssessmentStrategyDelegate } from './providers/assessment-strategy-delegate'; +import { AddonModWorkshopPrefetchHandler } from './providers/prefetch-handler'; +import { AddonModWorkshopSyncCronHandler } from './providers/sync-cron-handler'; @NgModule({ declarations: [ @@ -37,13 +41,19 @@ import { AddonWorkshopAssessmentStrategyDelegate } from './providers/assessment- AddonModWorkshopOfflineProvider, AddonModWorkshopSyncProvider, AddonModWorkshopHelperProvider, - AddonWorkshopAssessmentStrategyDelegate + AddonWorkshopAssessmentStrategyDelegate, + AddonModWorkshopPrefetchHandler, + AddonModWorkshopSyncCronHandler ] }) export class AddonModWorkshopModule { constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModWorkshopModuleHandler, - contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModWorkshopLinkHandler) { + contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModWorkshopLinkHandler, + prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModWorkshopPrefetchHandler, + cronDelegate: CoreCronDelegate, syncHandler: AddonModWorkshopSyncCronHandler) { moduleDelegate.registerHandler(moduleHandler); contentLinksDelegate.registerHandler(linkHandler); + prefetchDelegate.registerHandler(prefetchHandler); + cronDelegate.register(syncHandler); } } From b25e5b3ec1bf791478c5df9b7a4b2eb5cde465c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 31 May 2018 16:58:35 +0200 Subject: [PATCH 09/20] MOBILE-2354 workshop: Submission component --- .../addon-mod-assign-feedback-comments.html | 2 +- .../workshop/components/components.module.ts | 9 +- .../mod/workshop/components/index/index.html | 38 +++-- .../components/submission/submission.html | 82 +++++++++++ .../components/submission/submission.scss | 35 +++++ .../components/submission/submission.ts | 132 ++++++++++++++++++ .../components/list/addon-notes-list.html | 2 +- src/app/app.ios.scss | 8 ++ src/app/app.md.scss | 9 ++ src/app/app.wp.scss | 8 ++ 10 files changed, 306 insertions(+), 19 deletions(-) create mode 100644 src/addon/mod/workshop/components/submission/submission.html create mode 100644 src/addon/mod/workshop/components/submission/submission.scss create mode 100644 src/addon/mod/workshop/components/submission/submission.ts diff --git a/src/addon/mod/assign/feedback/comments/component/addon-mod-assign-feedback-comments.html b/src/addon/mod/assign/feedback/comments/component/addon-mod-assign-feedback-comments.html index 3732d55bb..62f3857f9 100644 --- a/src/addon/mod/assign/feedback/comments/component/addon-mod-assign-feedback-comments.html +++ b/src/addon/mod/assign/feedback/comments/component/addon-mod-assign-feedback-comments.html @@ -11,7 +11,7 @@ - + {{ 'core.notsent' | translate }} diff --git a/src/addon/mod/workshop/components/components.module.ts b/src/addon/mod/workshop/components/components.module.ts index 6111f4bb4..cdcf3f5e6 100644 --- a/src/addon/mod/workshop/components/components.module.ts +++ b/src/addon/mod/workshop/components/components.module.ts @@ -18,12 +18,15 @@ import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; import { CoreCourseComponentsModule } from '@core/course/components/components.module'; import { AddonModWorkshopIndexComponent } from './index/index'; +import { AddonModWorkshopSubmissionComponent } from './submission/submission'; @NgModule({ declarations: [ - AddonModWorkshopIndexComponent + AddonModWorkshopIndexComponent, + AddonModWorkshopSubmissionComponent ], imports: [ CommonModule, @@ -31,12 +34,14 @@ import { AddonModWorkshopIndexComponent } from './index/index'; TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, + CorePipesModule, CoreCourseComponentsModule ], providers: [ ], exports: [ - AddonModWorkshopIndexComponent + AddonModWorkshopIndexComponent, + AddonModWorkshopSubmissionComponent ], entryComponents: [ AddonModWorkshopIndexComponent diff --git a/src/addon/mod/workshop/components/index/index.html b/src/addon/mod/workshop/components/index/index.html index 15f9ed06c..c8a039a5b 100644 --- a/src/addon/mod/workshop/components/index/index.html +++ b/src/addon/mod/workshop/components/index/index.html @@ -14,7 +14,7 @@ - +

{{ phases[selectedPhase].title }}

{{ 'addon.mod_workshop.userplancurrentphase' | translate }}

@@ -27,8 +27,8 @@
- - + + @@ -56,13 +56,15 @@ - +

{{ 'addon.mod_workshop.yoursubmission' | translate }}

{{ 'addon.mod_workshop.noyoursubmission' | translate }}

- + + +
@@ -89,15 +91,17 @@
- +

{{ 'addon.mod_workshop.assignedassessments' | translate }}

- + + +
- +

{{ 'addon.mod_workshop.yoursubmission' | translate }}

@@ -115,7 +119,7 @@
- +

{{ 'addon.mod_workshop.yourgrades' | translate }}

@@ -129,16 +133,18 @@
- +

{{ 'addon.mod_workshop.publishedsubmissions' | translate }}

- + + +
- +

{{ 'addon.mod_workshop.submissionsreport' | translate }}

@@ -153,9 +159,11 @@ - + + + - + - diff --git a/src/addon/mod/workshop/components/submission/submission.html b/src/addon/mod/workshop/components/submission/submission.html new file mode 100644 index 000000000..b2cda93ed --- /dev/null +++ b/src/addon/mod/workshop/components/submission/submission.html @@ -0,0 +1,82 @@ + +
+ + + + +

{{submission.title}}

+

{{profile.fullname}}

+

+ {{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{submission.submissiongrade}} +

+

+ {{ 'addon.mod_workshop.gradeover' | translate }}: {{submission.submissiongradeover}} +

+

+ {{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{submission.gradinggrade}} +

+ + {{ 'core.notsent' | translate }} + + + {{submission.timemodified | coreDateDayOrTime}} + {{ 'core.notsent' | translate }} + {{ 'core.deletedoffline' | translate }} + +
+ + + + + + + + + + + + +

{{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateByProfile.fullname} }}

+ +
+ + + +
+ + + + + + +

{{submission.title}}

+

{{profile.fullname}}

+

+ {{ 'addon.mod_workshop.receivedgrades' | translate }}: {{submission.reviewedbycount}} / {{submission.reviewedby.length}} +

+

+ {{ 'addon.mod_workshop.givengrades' | translate }}: {{submission.reviewerofcount}} / {{submission.reviewerof.length}} +

+

+ {{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{submission.submissiongrade}} +

+

+ {{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{submission.submissiongradeover}} +

+

+ {{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{submission.gradinggrade}} +

+ + {{ 'addon.mod_workshop.assessedsubmission' | translate }} + {{ 'addon.mod_workshop.notassessed' | translate }} + + + {{submission.timemodified | coreDateDayOrTime}} +
{{ 'core.notsent' | translate }}
+
{{ 'core.deletedoffline' | translate }}
+
+
+
diff --git a/src/addon/mod/workshop/components/submission/submission.scss b/src/addon/mod/workshop/components/submission/submission.scss new file mode 100644 index 000000000..9c1f97329 --- /dev/null +++ b/src/addon/mod/workshop/components/submission/submission.scss @@ -0,0 +1,35 @@ +addon-mod-workshop-submission { + .item-md.item-block .item-inner { + border-bottom: 1px solid $list-md-border-color; + } + + .item-ios.item-block .item-inner { + border-bottom: $hairlines-width solid $list-ios-border-color; + } + + .item-wp.item-block .item-inner { + border-bottom: 1px solid $list-wp-border-color; + } + + &:last-child .item .item-inner { + border-bottom: 0; + } +} + +.card.with-borders addon-mod-workshop-submission { + .item-md.item-block .item-inner { + border-bottom: 1px solid $list-md-border-color; + } + + .item-ios.item-block .item-inner { + border-bottom: $hairlines-width solid $list-ios-border-color; + } + + .item-wp.item-block .item-inner { + border-bottom: 1px solid $list-wp-border-color; + } + + &:last-child .item .item-inner { + border-bottom: 0; + } +} \ No newline at end of file diff --git a/src/addon/mod/workshop/components/submission/submission.ts b/src/addon/mod/workshop/components/submission/submission.ts new file mode 100644 index 000000000..eb6220555 --- /dev/null +++ b/src/addon/mod/workshop/components/submission/submission.ts @@ -0,0 +1,132 @@ +// (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 { NavController } from 'ionic-angular'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUserProvider } from '@core/user/providers/user'; +import { AddonModWorkshopProvider } from '../../providers/workshop'; +import { AddonModWorkshopHelperProvider } from '../../providers/helper'; +import { AddonModWorkshopOfflineProvider } from '../../providers/offline'; + +/** + * Component that displays workshop submission. + */ +@Component({ + selector: 'addon-mod-workshop-submission', + templateUrl: 'submission.html', +}) +export class AddonModWorkshopSubmissionComponent implements OnInit { + @Input() submission: any; + @Input() module: any; + @Input() workshop: any; + @Input() access: any; + @Input() courseId: number; + @Input() assessment?: any; + @Input() summary?: boolean; + + component = AddonModWorkshopProvider.COMPONENT; + componentId: number; + userId: number; + loaded = false; + offline = false; + viewDetails = false; + profile: any; + showGrade: any; + evaluateByProfile: any; + + constructor(private workshopOffline: AddonModWorkshopOfflineProvider, private workshopHelper: AddonModWorkshopHelperProvider, + private navCtrl: NavController, private userProvider: CoreUserProvider, sitesProvider: CoreSitesProvider) { + this.userId = sitesProvider.getCurrentSiteUserId(); + this.showGrade = this.workshopHelper.showGrade; + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.componentId = this.module.instance; + this.userId = this.submission.authorid || this.submission.userid || this.userId; + this.submission.title = this.submission.title || this.submission.submissiontitle; + this.submission.timemodified = this.submission.timemodified || this.submission.submissionmodified; + this.submission.id = this.submission.id || this.submission.submissionid; + + if (this.workshop.phase == AddonModWorkshopProvider.PHASE_ASSESSMENT) { + if (this.submission.reviewedby && this.submission.reviewedby.length) { + this.submission.reviewedbycount = this.submission.reviewedby.reduce((a, b) => { + return a + (b.grade ? 1 : 0); + }, 0); + } + + if (this.submission.reviewerof && this.submission.reviewerof.length) { + this.submission.reviewerofcount = this.submission.reviewerof.reduce((a, b) => { + return a + (b.grade ? 1 : 0); + }, 0); + } + } + + const promises = []; + + this.offline = (this.submission && this.submission.offline) || (this.assessment && this.assessment.offline); + + if (this.submission.id) { + promises.push(this.workshopOffline.getEvaluateSubmission(this.workshop.id, this.submission.id) + .then((offlineSubmission) => { + this.submission.submissiongradeover = offlineSubmission.gradeover; + this.offline = true; + }).catch(() => { + // Ignore errors. + })); + } + + if (this.userId) { + promises.push(this.userProvider.getProfile(this.userId, this.courseId, true).then((profile) => { + this.profile = profile; + })); + } + + this.viewDetails = !this.summary && this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED && + this.navCtrl.getActive().name !== 'AddonModWorkshopSubmissionPage'; + + if (this.viewDetails && this.submission.gradeoverby) { + promises.push(this.userProvider.getProfile(this.submission.gradeoverby, this.courseId, true).then((profile) => { + this.evaluateByProfile = profile; + })); + } + + Promise.all(promises).finally(() => { + this.loaded = true; + }); + } + + /** + * Navigate to the submission. + */ + gotoSubmission(): void { + if (this.submission.timemodified) { + const params = { + module: this.module, + workshop: this.workshop, + access: this.access, + courseId: this.courseId, + profile: this.profile, + submission: this.submission, + assessment: this.assessment, + submissionId: this.submission.id + }; + + this.navCtrl.push('AddonModWorkshopSubmissionPage', params); + } + } +} diff --git a/src/addon/notes/components/list/addon-notes-list.html b/src/addon/notes/components/list/addon-notes-list.html index 5b2fd8ebc..21f24aba2 100644 --- a/src/addon/notes/components/list/addon-notes-list.html +++ b/src/addon/notes/components/list/addon-notes-list.html @@ -32,7 +32,7 @@

{{note.userfullname}}

{{note.lastmodified | coreDateDayOrTime}}

-

{{ 'core.notsent' | translate }}

+

{{ 'core.notsent' | translate }}

diff --git a/src/app/app.ios.scss b/src/app/app.ios.scss index a7410863d..58b921be0 100644 --- a/src/app/app.ios.scss +++ b/src/app/app.ios.scss @@ -111,3 +111,11 @@ padding-left: 15px * $i + $item-ios-padding-start; } } + +// Recover borders on items inside cards. +.card-ios.with-borders .item-ios.item-block .item-inner { + border-bottom: $hairlines-width solid $list-ios-border-color; +} +.card-ios.with-borders .item-ios:last-child .item-inner { + border-bottom: 0; +} \ No newline at end of file diff --git a/src/app/app.md.scss b/src/app/app.md.scss index 866b98788..e39f28792 100644 --- a/src/app/app.md.scss +++ b/src/app/app.md.scss @@ -112,3 +112,12 @@ padding-left: 15px * $i + $item-md-padding-start; } } + + +// Recover borders on items inside cards. +.card-md.with-borders .item-md.item-block .item-inner { + border-bottom: 1px solid $list-md-border-color; +} +.card-md.with-borders .item-md:last-child .item-inner { + border-bottom: 0; +} \ No newline at end of file diff --git a/src/app/app.wp.scss b/src/app/app.wp.scss index 0e7342032..eda1dc65c 100644 --- a/src/app/app.wp.scss +++ b/src/app/app.wp.scss @@ -47,3 +47,11 @@ padding-left: 15px * $i + $item-wp-padding-start; } } + +// Recover borders on items inside cards. +.card-wp.with-borders .item-wp.item-block .item-inner { + border-bottom: 1px solid $list-wp-border-color; +} +.card-wp.with-borders .item-wp:last-child .item-inner { + border-bottom: 0; +} \ No newline at end of file From a420d4eb8a5db266f2066185dc3277e68dfc2c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 1 Jun 2018 13:31:33 +0200 Subject: [PATCH 10/20] MOBILE-2354 workshop: Edit submission page --- src/addon/mod/wiki/pages/edit/edit.ts | 3 +- .../mod/workshop/components/index/index.ts | 4 +- .../edit-submission/edit-submission.html | 30 ++ .../edit-submission/edit-submission.module.ts | 33 ++ .../pages/edit-submission/edit-submission.ts | 395 ++++++++++++++++++ src/addon/mod/workshop/providers/workshop.ts | 7 +- 6 files changed, 464 insertions(+), 8 deletions(-) create mode 100644 src/addon/mod/workshop/pages/edit-submission/edit-submission.html create mode 100644 src/addon/mod/workshop/pages/edit-submission/edit-submission.module.ts create mode 100644 src/addon/mod/workshop/pages/edit-submission/edit-submission.ts diff --git a/src/addon/mod/wiki/pages/edit/edit.ts b/src/addon/mod/wiki/pages/edit/edit.ts index b64a0fbe5..0a7dfdc14 100644 --- a/src/addon/mod/wiki/pages/edit/edit.ts +++ b/src/addon/mod/wiki/pages/edit/edit.ts @@ -482,8 +482,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy { pageId: this.pageId, subwikiId: this.subwikiId, pageTitle: title, - siteId: this.sitesProvider.getCurrentSiteId() - }); + }, this.sitesProvider.getCurrentSiteId()); }); } else { // Page stored in offline. Go to see the offline page. diff --git a/src/addon/mod/workshop/components/index/index.ts b/src/addon/mod/workshop/components/index/index.ts index ecbb91bbf..dbe91b367 100644 --- a/src/addon/mod/workshop/components/index/index.ts +++ b/src/addon/mod/workshop/components/index/index.ts @@ -117,7 +117,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity * @param {any} data Data received by the event. */ protected eventReceived(data: any): void { - if ((this.workshop && this.workshop.id === data.workshopid) || data.cmid === module.id) { + if ((this.workshop && this.workshop.id === data.workshopId) || data.cmId === this.module.id) { this.content && this.content.scrollToTop(); this.loaded = false; @@ -297,7 +297,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity if (task.code == 'submit' && this.canSubmit && ((this.access.creatingsubmissionallowed && !this.submission) || (this.access.modifyingsubmissionallowed && this.submission))) { const params = { - module: module, + module: this.module, access: this.access, courseId: this.courseId, submission: this.submission diff --git a/src/addon/mod/workshop/pages/edit-submission/edit-submission.html b/src/addon/mod/workshop/pages/edit-submission/edit-submission.html new file mode 100644 index 000000000..e03d7087c --- /dev/null +++ b/src/addon/mod/workshop/pages/edit-submission/edit-submission.html @@ -0,0 +1,30 @@ + + + {{ 'addon.mod_workshop.editsubmission' | translate }} + + + + + + + + + + +
+ + {{ 'addon.mod_workshop.submissiontitle' | translate }} + + + + + {{ 'addon.mod_workshop.submissioncontent' | translate }} + + + + +
+
+
diff --git a/src/addon/mod/workshop/pages/edit-submission/edit-submission.module.ts b/src/addon/mod/workshop/pages/edit-submission/edit-submission.module.ts new file mode 100644 index 000000000..c49f5e606 --- /dev/null +++ b/src/addon/mod/workshop/pages/edit-submission/edit-submission.module.ts @@ -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 { AddonModWorkshopEditSubmissionPage } from './edit-submission'; + +@NgModule({ + declarations: [ + AddonModWorkshopEditSubmissionPage, + ], + imports: [ + CoreDirectivesModule, + CoreComponentsModule, + IonicPageModule.forChild(AddonModWorkshopEditSubmissionPage), + TranslateModule.forChild() + ], +}) +export class AddonModWorkshopEditSubmissionPageModule {} diff --git a/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts b/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts new file mode 100644 index 000000000..213aebe00 --- /dev/null +++ b/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts @@ -0,0 +1,395 @@ +// (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 } from '@angular/core'; +import { IonicPage, NavParams, NavController } from 'ionic-angular'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; +import { CoreFileSessionProvider } from '@providers/file-session'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; +import { AddonModWorkshopProvider } from '../../providers/workshop'; +import { AddonModWorkshopHelperProvider } from '../../providers/helper'; +import { AddonModWorkshopOfflineProvider } from '../../providers/offline'; + +/** + * Page that displays the workshop edit submission. + */ +@IonicPage({ segment: 'addon-mod-workshop-edit-submission' }) +@Component({ + selector: 'page-addon-mod-workshop-edit-submission', + templateUrl: 'edit-submission.html', +}) +export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { + + module: any; + courseId: number; + access: any; + submission: any; + + title: string; + loaded = false; + component = AddonModWorkshopProvider.COMPONENT; + componentId: number; + editForm: FormGroup; // The form group. + + protected workshopId: number; + protected userId: number; + protected originalData: any = {}; + protected hasOffline = false; + protected editing = false; + protected forceLeave = false; + protected siteId: string; + protected workshop: any; + + constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, protected fileUploaderProvider: CoreFileUploaderProvider, + protected workshopProvider: AddonModWorkshopProvider, protected workshopOffline: AddonModWorkshopOfflineProvider, + protected workshopHelper: AddonModWorkshopHelperProvider, protected navCtrl: NavController, + protected fileSessionprovider: CoreFileSessionProvider, protected syncProvider: CoreSyncProvider, + protected textUtils: CoreTextUtilsProvider, protected domUtils: CoreDomUtilsProvider, protected fb: FormBuilder, + protected translate: TranslateService, protected eventsProvider: CoreEventsProvider) { + this.module = navParams.get('module'); + this.courseId = navParams.get('courseId'); + this.access = navParams.get('access'); + this.submission = navParams.get('submission') || {}; + + this.title = this.module.name; + this.workshopId = this.module.instance; + this.componentId = this.module.id; + this.userId = sitesProvider.getCurrentSiteUserId(); + this.siteId = sitesProvider.getCurrentSiteId(); + + this.editForm = new FormGroup({}); + this.editForm.addControl('title', this.fb.control('', Validators.required)); + this.editForm.addControl('content', this.fb.control('')); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.fetchSubmissionData(); + } + + /** + * Check if we can leave the page or not. + * + * @return {boolean|Promise} Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): boolean | Promise { + if (this.forceLeave) { + return true; + } + + let promise; + + // Check if data has changed. + if (!this.hasDataChanged()) { + promise = Promise.resolve(); + } else { + // Show confirmation if some data has been modified. + promise = this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); + } + + return promise.then(() => { + if (this.submission.attachmentfiles) { + // Delete the local files from the tmp folder. + this.fileUploaderProvider.clearTmpFiles(this.submission.attachmentfiles); + } + }); + } + + /** + * Fetch the submission data. + * + * @return {Promise} Resolved when done. + */ + protected fetchSubmissionData(): Promise { + return this.workshopProvider.getWorkshop(this.courseId, this.module.id).then((workshopData) => { + this.workshop = workshopData; + + if (this.submission && this.submission.id > 0) { + this.editing = true; + + return this.workshopHelper.getSubmissionById(this.workshopId, this.submission.id).then((submissionData) => { + this.submission = submissionData; + this.submission.text = submissionData.content; + + const canEdit = (this.userId == submissionData.authorid && this.access.cansubmit && + this.access.modifyingsubmissionallowed); + if (!canEdit) { + // Should not happen, but go back if does. + this.forceLeavePage(); + + return; + } + }); + } else if (!this.access.cansubmit || !this.access.creatingsubmissionallowed) { + // Should not happen, but go back if does. + this.forceLeavePage(); + + return; + } + + }).then(() => { + return this.workshopOffline.getSubmissions(this.workshopId).then((submissionsActions) => { + if (submissionsActions && submissionsActions.length) { + this.hasOffline = true; + const actions = this.workshopHelper.filterSubmissionActions(submissionsActions, this.editing ? + this.submission.id : false); + + return this.workshopHelper.applyOfflineData(this.submission, actions).then((offlineSubmission) => { + this.submission.title = offlineSubmission.title; + this.submission.text = offlineSubmission.content; + this.submission.attachmentfiles = offlineSubmission.attachmentfiles; + }); + } else { + this.hasOffline = false; + } + }).finally(() => { + this.originalData.title = this.submission.title; + this.originalData.content = this.submission.text; + this.originalData.attachmentfiles = []; + + this.submission.attachmentfiles.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.attachmentfiles.push({ + filename : filename, + fileurl: file.fileurl + }); + }); + }); + }).then(() => { + // Create the form group and its controls. + this.editForm.controls['title'].setValue(this.submission.title); + this.editForm.controls['content'].setValue(this.submission.content); + + const submissionId = this.submission.id || 'newsub'; + this.fileSessionprovider.setFiles(this.component, + this.workshopId + '_' + submissionId, this.submission.attachmentfiles || []); + + this.loaded = true; + }).catch((message) => { + this.loaded = false; + + this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); + + this.forceLeavePage(); + }); + } + + /** + * Force leaving the page, without checking for changes. + */ + protected forceLeavePage(): void { + this.forceLeave = true; + this.navCtrl.pop(); + } + + /** + * Get the form input data. + * + * @return {any} Object with all the info. + */ + protected getInputData(): any { + const submissionId = this.submission.id || 'newsub'; + + const values = this.editForm.value; + values['attachmentfiles'] = this.fileSessionprovider.getFiles(this.component, this.workshopId + '_' + submissionId) || []; + + return values; + } + + /** + * Check if data has changed. + * + * @return {boolean} True if changed or false if not. + */ + protected hasDataChanged(): boolean { + if (!this.loaded) { + return false; + } + + const inputData = this.getInputData(); + if (!this.originalData || typeof this.originalData.title == 'undefined') { + // There is no original data, assume it hasn't changed. + return false; + } + + if (this.originalData.title != inputData.title || this.originalData.content != inputData.content) { + return true; + } + + return this.fileUploaderProvider.areFileListDifferent(inputData.attachmentfiles, this.originalData.attachmentfiles); + } + + /** + * Pull to refresh. + * + * @param {any} refresher Refresher. + */ + refreshSubmission(refresher: any): void { + if (this.loaded) { + const promises = []; + + promises.push(this.workshopProvider.invalidateSubmissionData(this.workshopId, this.submission.id)); + promises.push(this.workshopProvider.invalidateSubmissionsData(this.workshopId)); + + Promise.all(promises).finally(() => { + return this.fetchSubmissionData(); + }).finally(() => { + refresher.complete(); + }); + } + } + + /** + * Save the submission. + */ + save(): void { + // Check if data has changed. + if (this.hasDataChanged()) { + this.saveSubmission().then(() => { + // Go back to entry list. + this.forceLeavePage(); + }).catch(() => { + // Nothing to do. + }); + } else { + // Nothing to save, just go back. + this.forceLeavePage(); + } + } + + /** + * Send submission and save. + * + * @return {Promise} Resolved when done. + */ + protected saveSubmission(): Promise { + const inputData = this.getInputData(); + + if (!inputData.title) { + this.domUtils.showAlert('core.notice', 'core.requireduserdatamissing'); + + return Promise.reject(null); + } + if (!inputData.content) { + this.domUtils.showAlert('core.notice', 'addon.mod_workshop.submissionrequiredcontent'); + + return Promise.reject(null); + } + + let allowOffline = true, + saveOffline = false; + + const modal = this.domUtils.showModalLoading('core.sending', true), + submissionId = this.submission && (this.submission.id || this.submission.submissionid) || false; + + // Check if rich text editor is enabled or not. + return this.domUtils.isRichTextEditorEnabled().then((rteEnabled) => { + if (rteEnabled) { + inputData.content = this.textUtils.restorePluginfileUrls(inputData.content, this.submission.inlinefiles); + } else { + // Rich text editor not enabled, add some HTML to the message if needed. + inputData.content = this.textUtils.formatHtmlLines(inputData.content); + } + + // Upload attachments first if any. + allowOffline = !inputData.attachmentfiles.length; + + return this.workshopHelper.uploadOrStoreSubmissionFiles(this.workshopId, submissionId, inputData.attachmentfiles, + this.editing, saveOffline).catch(() => { + // Cannot upload them in online, save them in offline. + saveOffline = true; + allowOffline = true; + + return this.workshopHelper.uploadOrStoreSubmissionFiles(this.workshopId, submissionId, inputData.attachmentfiles, + this.editing, saveOffline); + }); + }).then((attachmentsId) => { + if (this.editing) { + if (saveOffline) { + // Save submission in offline. + return this.workshopOffline.saveSubmission(this.workshopId, this.courseId, inputData.title, + inputData.content, attachmentsId, submissionId, 'update').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.updateSubmission(this.workshopId, submissionId, this.courseId, inputData.title, + inputData.content, attachmentsId, undefined, allowOffline); + } + + if (saveOffline) { + // Save submission in offline. + return this.workshopOffline.saveSubmission(this.workshopId, this.courseId, inputData.title, inputData.content, + attachmentsId, submissionId, 'add').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.addSubmission(this.workshopId, this.courseId, inputData.title, inputData.content, + attachmentsId, undefined, submissionId, allowOffline); + }).then((newSubmissionId) => { + const data = { + workshopId: this.workshopId, + cmId: this.module.cmid + }; + + if (newSubmissionId && submissionId) { + // Data sent to server, delete stored files (if any). + this.workshopOffline.deleteSubmissionAction(this.workshopId, submissionId, this.editing ? 'update' : 'add'); + this.workshopHelper.deleteSubmissionStoredFiles(this.workshopId, submissionId, this.editing); + data['submissionId'] = newSubmissionId; + } + + const promise = newSubmissionId ? this.workshopProvider.invalidateSubmissionData(this.workshopId, newSubmissionId) : + Promise.resolve(); + + return promise.finally(() => { + this.eventsProvider.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId); + + // Delete the local files from the tmp folder. + this.fileUploaderProvider.clearTmpFiles(inputData.attachmentfiles); + }); + }).catch((message) => { + this.domUtils.showErrorModalDefault(message, 'Cannot save submission'); + }).finally(() => { + modal.dismiss(); + }); + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + this.syncProvider.unblockOperation(this.component, this.workshopId); + } +} diff --git a/src/addon/mod/workshop/providers/workshop.ts b/src/addon/mod/workshop/providers/workshop.ts index 9962b836d..27660ef6f 100644 --- a/src/addon/mod/workshop/providers/workshop.ts +++ b/src/addon/mod/workshop/providers/workshop.ts @@ -215,10 +215,9 @@ export class AddonModWorkshopProvider { return site.read('mod_workshop_get_workshops_by_courses', params, preSets).then((response) => { if (response && response.workshops) { - for (const x in response.workshops) { - if (response.workshops[x][key] == value) { - return response.workshops[x]; - } + const workshopFound = response.workshops.find((workshop) => workshop[key] == value); + if (workshopFound) { + return workshopFound; } } From af51fb8a7e873a832beb61a561268da23a96f703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 1 Jun 2018 15:54:06 +0200 Subject: [PATCH 11/20] MOBILE-2354 workshop: View submission page --- .../mod/workshop/components/index/index.ts | 4 - .../components/submission/submission.ts | 1 - .../pages/edit-submission/edit-submission.ts | 10 +- .../workshop/pages/submission/submission.html | 106 ++++ .../pages/submission/submission.module.ts | 35 ++ .../workshop/pages/submission/submission.ts | 514 ++++++++++++++++++ src/addon/mod/workshop/providers/workshop.ts | 2 + src/addon/mod/workshop/workshop.scss | 13 + 8 files changed, 677 insertions(+), 8 deletions(-) create mode 100644 src/addon/mod/workshop/pages/submission/submission.html create mode 100644 src/addon/mod/workshop/pages/submission/submission.module.ts create mode 100644 src/addon/mod/workshop/pages/submission/submission.ts create mode 100644 src/addon/mod/workshop/workshop.scss diff --git a/src/addon/mod/workshop/components/index/index.ts b/src/addon/mod/workshop/components/index/index.ts index dbe91b367..ed7555163 100644 --- a/src/addon/mod/workshop/components/index/index.ts +++ b/src/addon/mod/workshop/components/index/index.ts @@ -303,10 +303,6 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity submission: this.submission }; - if (this.submission.id) { - params['submissionId'] = this.submission.id; - } - this.navCtrl.push('AddonModWorkshopEditSubmissionPage', params); } } else if (task.link) { diff --git a/src/addon/mod/workshop/components/submission/submission.ts b/src/addon/mod/workshop/components/submission/submission.ts index eb6220555..b704faac7 100644 --- a/src/addon/mod/workshop/components/submission/submission.ts +++ b/src/addon/mod/workshop/components/submission/submission.ts @@ -123,7 +123,6 @@ export class AddonModWorkshopSubmissionComponent implements OnInit { profile: this.profile, submission: this.submission, assessment: this.assessment, - submissionId: this.submission.id }; this.navCtrl.push('AddonModWorkshopSubmissionPage', params); diff --git a/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts b/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts index 213aebe00..fd83fa272 100644 --- a/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts +++ b/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts @@ -42,7 +42,6 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { access: any; submission: any; - title: string; loaded = false; component = AddonModWorkshopProvider.COMPONENT; componentId: number; @@ -56,6 +55,7 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { protected forceLeave = false; protected siteId: string; protected workshop: any; + protected isDestroyed = false; constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, protected fileUploaderProvider: CoreFileUploaderProvider, protected workshopProvider: AddonModWorkshopProvider, protected workshopOffline: AddonModWorkshopOfflineProvider, @@ -68,7 +68,6 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { this.access = navParams.get('access'); this.submission = navParams.get('submission') || {}; - this.title = this.module.name; this.workshopId = this.module.instance; this.componentId = this.module.id; this.userId = sitesProvider.getCurrentSiteUserId(); @@ -83,6 +82,11 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { * Component being initialized. */ ngOnInit(): void { + if (!this.isDestroyed) { + // Block the workshop. + this.syncProvider.blockOperation(this.component, this.workshopId); + } + this.fetchSubmissionData(); } @@ -182,7 +186,6 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { }); }); }).then(() => { - // Create the form group and its controls. this.editForm.controls['title'].setValue(this.submission.title); this.editForm.controls['content'].setValue(this.submission.content); @@ -390,6 +393,7 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { * Component being destroyed. */ ngOnDestroy(): void { + this.isDestroyed = true; this.syncProvider.unblockOperation(this.component, this.workshopId); } } diff --git a/src/addon/mod/workshop/pages/submission/submission.html b/src/addon/mod/workshop/pages/submission/submission.html new file mode 100644 index 000000000..d77cd6d4a --- /dev/null +++ b/src/addon/mod/workshop/pages/submission/submission.html @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

{{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateByProfile.fullname} }}

+ +
+
+ + + +

{{ 'addon.mod_workshop.yourassessment' | translate }}

+
+ +
+ + + +

{{ 'addon.mod_workshop.receivedgrades' | translate }}

+
+ +
+ + + +

{{ 'addon.mod_workshop.givengrades' | translate }}

+
+ +
+ +
+ +

{{ 'addon.mod_workshop.feedbackauthor' | translate }}

+
+ + {{ 'addon.mod_workshop.publishsubmission' | translate }} + +

{{ 'addon.mod_workshop.publishsubmission_help' | translate }}

+
+ + +

{{ 'addon.mod_workshop.gradecalculated' | translate }}

+

{{ submission.submissiongrade }}

+
+ + {{ 'addon.mod_workshop.gradeover' | translate }} + + {{grade.label}} + + + + {{ 'addon.mod_workshop.feedbackauthor' | translate }} + + +
+ + + + + + + + +

{{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateGradingByProfile.fullname} }}

+ +
+
+
+
diff --git a/src/addon/mod/workshop/pages/submission/submission.module.ts b/src/addon/mod/workshop/pages/submission/submission.module.ts new file mode 100644 index 000000000..0c7c7fe8d --- /dev/null +++ b/src/addon/mod/workshop/pages/submission/submission.module.ts @@ -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 { AddonModWorkshopComponentsModule } from '../../components/components.module'; +import { AddonModWorkshopSubmissionPage } from './submission'; + +@NgModule({ + declarations: [ + AddonModWorkshopSubmissionPage, + ], + imports: [ + CoreDirectivesModule, + CoreComponentsModule, + AddonModWorkshopComponentsModule, + IonicPageModule.forChild(AddonModWorkshopSubmissionPage), + TranslateModule.forChild() + ], +}) +export class AddonModWorkshopSubmissionPageModule {} diff --git a/src/addon/mod/workshop/pages/submission/submission.ts b/src/addon/mod/workshop/pages/submission/submission.ts new file mode 100644 index 000000000..4a6ba7235 --- /dev/null +++ b/src/addon/mod/workshop/pages/submission/submission.ts @@ -0,0 +1,514 @@ +// (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, Optional } from '@angular/core'; +import { Content, IonicPage, NavParams, NavController } from 'ionic-angular'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; +import { CoreFileSessionProvider } from '@providers/file-session'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreUserProvider } from '@core/user/providers/user'; +import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; +import { AddonModWorkshopProvider } from '../../providers/workshop'; +import { AddonModWorkshopHelperProvider } from '../../providers/helper'; +import { AddonModWorkshopOfflineProvider } from '../../providers/offline'; +import { AddonModWorkshopSyncProvider } from '../../providers/sync'; + +/** + * Page that displays a workshop submission. + */ +@IonicPage({ segment: 'addon-mod-workshop-submission' }) +@Component({ + selector: 'page-addon-mod-workshop-submission', + templateUrl: 'submission.html', +}) +export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { + + module: any; + workshop: any; + access: any; + assessment: any; + submissionInfo: any; + submission: any; + + courseId: number; + profile: any; + + title: string; + loaded = false; + ownAssessment = false; + strategy: any; + assessmentId: number; + assessmentUserId: number; + evaluate: any; + canAddFeedback = false; + canEdit = false; + canDelete = false; + evaluationGrades: any; + evaluateGradingByProfile: any; + evaluateByProfile: any; + feedbackForm: FormGroup; // The form group. + + protected submissionId: number; + protected workshopId: number; + protected currentUserId: number; + protected userId: number; + protected siteId: string; + protected originalEvaluation: any = {}; + protected hasOffline = false; + protected component = AddonModWorkshopProvider.COMPONENT; + protected forceLeave = false; + protected obsAssessmentSaved: any; + protected syncObserver: any; + protected isDestroyed = false; + + constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, protected fileUploaderProvider: CoreFileUploaderProvider, + protected workshopProvider: AddonModWorkshopProvider, protected workshopOffline: AddonModWorkshopOfflineProvider, + protected workshopHelper: AddonModWorkshopHelperProvider, protected navCtrl: NavController, + protected fileSessionprovider: CoreFileSessionProvider, protected syncProvider: CoreSyncProvider, + protected textUtils: CoreTextUtilsProvider, protected domUtils: CoreDomUtilsProvider, protected fb: FormBuilder, + protected translate: TranslateService, protected eventsProvider: CoreEventsProvider, + protected courseProvider: CoreCourseProvider, @Optional() protected content: Content, + protected gradesHelper: CoreGradesHelperProvider, protected userProvider: CoreUserProvider) { + this.module = navParams.get('module'); + this.workshop = navParams.get('workshop'); + this.access = navParams.get('access'); + this.courseId = navParams.get('courseId'); + this.profile = navParams.get('profile'); + this.submissionInfo = navParams.get('submission'); + this.assessment = navParams.get('assessment') || null; + + this.title = this.module.name; + this.workshopId = this.module.instance; + this.currentUserId = sitesProvider.getCurrentSiteUserId(); + this.siteId = sitesProvider.getCurrentSiteId(); + this.submissionId = this.submissionInfo.submissionid || this.submissionInfo.id; + this.userId = this.submissionInfo.userid || null; + this.strategy = (this.assessment && this.assessment.strategy) || (this.workshop && this.workshop.strategy); + this.assessmentId = this.assessment && (this.assessment.assessmentid || this.assessment.id); + this.assessmentUserId = this.assessment && (this.assessment.reviewerid || this.assessment.userid); + + this.feedbackForm = new FormGroup({}); + this.feedbackForm.addControl('published', this.fb.control('')); + this.feedbackForm.addControl('grade', this.fb.control('')); + this.feedbackForm.addControl('text', this.fb.control('')); + + this.obsAssessmentSaved = this.eventsProvider.on(AddonModWorkshopProvider.ASSESSMENT_SAVED, (data) => { + this.eventReceived(data); + }, this.siteId); + + // Refresh workshop on sync. + this.syncObserver = this.eventsProvider.on(AddonModWorkshopSyncProvider.AUTO_SYNCED, (data) => { + // Update just when all database is synced. + this.eventReceived(data); + }, this.siteId); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.fetchSubmissionData().then(() => { + this.workshopProvider.logViewSubmission(this.submissionId).then(() => { + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); + }); + }); + } + + /** + * Check if we can leave the page or not. + * + * @return {boolean|Promise} Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): boolean | Promise { + if (this.forceLeave || !this.canAddFeedback) { + return true; + } + + if (!this.hasEvaluationChanged()) { + return Promise.resolve(); + } + + // Show confirmation if some data has been modified. + return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); + } + + /** + * Goto edit submission page. + */ + editSubmission(): void { + const params = { + module: module, + access: this.access, + courseid: this.courseId, + submission: this.submission + }; + + this.navCtrl.push('AddonModWorkshopEditSubmissionPage', params); + } + + /** + * Function called when we receive an event of submission changes. + * + * @param {any} data Event data received. + */ + protected eventReceived(data: any): void { + if (this.workshopId === data.workshopid) { + this.content && this.content.scrollToTop(); + + this.loaded = false; + this.refreshAllData(); + } + } + + /** + * Fetch the submission data. + * + * @return {Promise} Resolved when done. + */ + protected fetchSubmissionData(): Promise { + return this.workshopHelper.getSubmissionById(this.workshopId, this.submissionId).then((submissionData) => { + const promises = []; + + this.submission = submissionData; + this.submission.submissiongrade = this.submissionInfo && this.submissionInfo.submissiongrade; + this.submission.gradinggrade = this.submissionInfo && this.submissionInfo.gradinggrade; + this.submission.submissiongradeover = this.submissionInfo && this.submissionInfo.submissiongradeover; + this.userId = submissionData.authorid || this.userId; + this.canEdit = this.currentUserId == this.userId && this.access.cansubmit && this.access.modifyingsubmissionallowed; + this.canDelete = this.access.candeletesubmissions; + this.canAddFeedback = !this.assessmentId && this.workshop.phase > AddonModWorkshopProvider.PHASE_ASSESSMENT && + this.workshop.phase < AddonModWorkshopProvider.PHASE_CLOSED && this.access.canoverridegrades; + this.ownAssessment = false; + + if (this.access.canviewallassessments) { + // Get new data, different that came from stateParams. + promises.push(this.workshopProvider.getSubmissionAssessments(this.workshopId, this.submissionId) + .then((subAssessments) => { + // Only allow the student to delete their own submission if it's still editable and hasn't been assessed. + if (this.canDelete) { + this.canDelete = !subAssessments.length; + } + + this.submissionInfo.reviewedby = subAssessments; + + this.submissionInfo.reviewedby.forEach((assessment) => { + assessment.userid = assessment.reviewerid; + assessment = this.workshopHelper.realGradeValue(this.workshop, assessment); + + if (this.currentUserId == assessment.userid) { + this.ownAssessment = assessment; + assessment.ownAssessment = true; + } + }); + })); + } else if (this.currentUserId == this.userId && this.assessmentId) { + // Get new data, different that came from stateParams. + promises.push(this.workshopProvider.getAssessment(this.workshopId, this.assessmentId).then((assessment) => { + // Only allow the student to delete their own submission if it's still editable and hasn't been assessed. + if (this.canDelete) { + this.canDelete = !assessment; + } + + assessment.userid = assessment.reviewerid; + assessment = this.workshopHelper.realGradeValue(this.workshop, assessment); + + if (this.currentUserId == assessment.userid) { + this.ownAssessment = assessment; + assessment.ownAssessment = true; + } + + this.submissionInfo.reviewedby = [assessment]; + })); + } + + if (this.canAddFeedback || this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) { + this.evaluate = { + published: submissionData.published, + text: submissionData.feedbackauthor || '' + }; + } + + if (this.canAddFeedback) { + + if (!this.isDestroyed) { + // Block the workshop. + this.syncProvider.blockOperation(this.component, this.workshopId); + } + + const defaultGrade = this.translate.instant('addon.mod_workshop.notoverridden'); + + promises.push(this.gradesHelper.makeGradesMenu(this.workshop.grade, this.workshopId, defaultGrade, -1) + .then((grades) => { + this.evaluationGrades = grades; + + this.evaluate.grade = { + label: this.gradesHelper.getGradeLabelFromValue(grades, this.submissionInfo.submissiongradeover) || + defaultGrade, + value: this.submissionInfo.submissiongradeover || -1 + }; + + return this.workshopOffline.getEvaluateSubmission(this.workshopId, this.submissionId) + .then((offlineSubmission) => { + this.hasOffline = true; + this.evaluate.published = offlineSubmission.published; + this.evaluate.text = offlineSubmission.feedbacktext; + this.evaluate.grade = { + label: this.gradesHelper.getGradeLabelFromValue(grades, offlineSubmission.gradeover) || defaultGrade, + value: offlineSubmission.gradeover || -1 + }; + }).catch(() => { + this.hasOffline = false; + // Ignore errors. + }).finally(() => { + this.originalEvaluation.published = this.evaluate.published; + this.originalEvaluation.text = this.evaluate.text; + this.originalEvaluation.grade = this.evaluate.grade.value; + + this.feedbackForm.controls['published'].setValue(this.evaluate.published); + this.feedbackForm.controls['grade'].setValue(this.evaluate.grade.value); + this.feedbackForm.controls['text'].setValue(this.evaluate.text); + }); + })); + } else if (this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED && submissionData.gradeoverby) { + promises.push(this.userProvider.getProfile(submissionData.gradeoverby, this.courseId, true).then((profile) => { + this.evaluateByProfile = profile; + })); + } + + if (this.assessmentId && !this.access.assessingallowed && this.assessment.feedbackreviewer && + this.assessment.gradinggradeoverby) { + promises.push(this.userProvider.getProfile(this.assessment.gradinggradeoverby, this.courseId, true) + .then((profile) => { + this.evaluateGradingByProfile = profile; + })); + } + + return Promise.all(promises); + }).then(() => { + return this.workshopOffline.getSubmissions(this.workshopId).then((submissionsActions) => { + const actions = this.workshopHelper.filterSubmissionActions(submissionsActions, this.submissionId); + + return this.workshopHelper.applyOfflineData(this.submission, actions).then((submission) => { + this.submission = submission; + }); + }); + }).catch((message) => { + this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Force leaving the page, without checking for changes. + */ + protected forceLeavePage(): void { + this.forceLeave = true; + this.navCtrl.pop(); + } + + /** + * Check if data has changed. + * + * @return {boolean} True if changed, false otherwise. + */ + protected hasEvaluationChanged(): boolean { + if (!this.loaded || !this.access.canoverridegrades) { + return false; + } + + const inputData = this.feedbackForm.value; + + if (this.originalEvaluation.published != inputData.published) { + return true; + } + + if (this.originalEvaluation.text != inputData.text) { + return true; + } + + if (this.originalEvaluation.grade != inputData.grade) { + return true; + } + + return false; + } + + /** + * Convenience function to refresh all the data. + * + * @return {Promise} Resolved when done. + */ + protected refreshAllData(): Promise { + const promises = []; + + promises.push(this.workshopProvider.invalidateSubmissionData(this.workshopId, this.submissionId)); + promises.push(this.workshopProvider.invalidateSubmissionsData(this.workshopId)); + promises.push(this.workshopProvider.invalidateSubmissionAssesmentsData(this.workshopId, this.submissionId)); + + if (this.assessmentId) { + promises.push(this.workshopProvider.invalidateAssessmentFormData(this.workshopId, this.assessmentId)); + promises.push(this.workshopProvider.invalidateAssessmentData(this.workshopId, this.assessmentId)); + } + + return Promise.all(promises).finally(() => { + this.eventsProvider.trigger(AddonModWorkshopProvider.ASSESSMENT_INVALIDATED, this.siteId); + + return this.fetchSubmissionData(); + }); + } + + /** + * Pull to refresh. + * + * @param {any} refresher Refresher. + */ + refreshSubmission(refresher: any): void { + if (this.loaded) { + this.refreshAllData().finally(() => { + refresher.complete(); + }); + } + } + + /** + * Save the assessment. + */ + saveAssessment(): void { + // Call trigger to save. + this.eventsProvider.trigger(AddonModWorkshopProvider.ASSESSMENT_SAVE, undefined, this.siteId); + } + + /** + * Save the submission evaluation. + */ + saveEvaluation(): void { + // Check if data has changed. + if (this.hasEvaluationChanged()) { + this.sendEvaluation().then(() => { + this.forceLeavePage(); + }); + } else { + // Nothing to save, just go back. + this.forceLeavePage(); + } + } + + /** + * Sends the evaluation to be saved on the server. + * + * @return {Promise} Resolved when done. + */ + protected sendEvaluation(): Promise { + const modal = this.domUtils.showModalLoading('core.sending', true); + + // Check if rich text editor is enabled or not. + return this.domUtils.isRichTextEditorEnabled().then((rteEnabled) => { + const inputData = this.feedbackForm.value; + + inputData.grade = inputData.grade >= 0 ? inputData.grade : ''; + if (!rteEnabled) { + // Rich text editor not enabled, add some HTML to the message if needed. + inputData.text = this.textUtils.formatHtmlLines(inputData.text); + } + + // Try to send it to server. + return this.workshopProvider.evaluateSubmission(this.workshopId, this.submissionId, this.courseId, inputData.text, + inputData.published, inputData.grade); + }).then(() => { + const data = { + workshopId: this.workshopId, + cmId: this.module.cmid, + submissionId: this.submissionId + }; + + return this.workshopProvider.invalidateSubmissionData(this.workshopId, this.submissionId).finally(() => { + this.eventsProvider.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId); + }); + }).catch((message) => { + this.domUtils.showErrorModalDefault(message, 'Cannot save submission evaluation'); + }).finally(() => { + modal.dismiss(); + }); + } + + /** + * Perform the submission delete action. + */ + deleteSubmission(): void { + this.domUtils.showConfirm(this.translate.instant('addon.mod_workshop.submissiondeleteconfirm')).then(() => { + const modal = this.domUtils.showModalLoading('core.deleting', true); + let success = false; + this.workshopProvider.deleteSubmission(this.workshopId, this.submissionId, this.courseId).then(() => { + success = true; + + return this.workshopProvider.invalidateSubmissionData(this.workshopId, this.submissionId); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Cannot delete submission'); + }).finally(() => { + modal.dismiss(); + if (success) { + const data = { + workshopId: this.workshopId, + cmId: this.module.cmid, + submissionId: this.submissionId + }; + + this.eventsProvider.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId); + + this.forceLeavePage(); + } + }); + }); + } + + /** + * Undo the submission delete action. + * + * @return {Promise} Resolved when done. + */ + undoDeleteSubmission(): Promise { + return this.workshopOffline.deleteSubmissionAction(this.workshopId, this.submissionId, 'delete').finally(() => { + + const data = { + workshopId: this.workshopId, + cmId: this.module.cmid, + submissionId: this.submissionId + }; + + this.eventsProvider.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId); + + return this.refreshAllData(); + }); + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + this.isDestroyed = true; + + this.syncObserver && this.syncObserver.off(); + this.obsAssessmentSaved && this.obsAssessmentSaved.off(); + // Restore original back functions. + this.syncProvider.unblockOperation(this.component, this.workshopId); + } +} diff --git a/src/addon/mod/workshop/providers/workshop.ts b/src/addon/mod/workshop/providers/workshop.ts index 27660ef6f..ca1889686 100644 --- a/src/addon/mod/workshop/providers/workshop.ts +++ b/src/addon/mod/workshop/providers/workshop.ts @@ -36,7 +36,9 @@ export class AddonModWorkshopProvider { static EXAMPLES_BEFORE_ASSESSMENT: 2; static SUBMISSION_CHANGED = 'addon_mod_workshop_submission_changed'; + static ASSESSMENT_SAVE = 'addon_mod_workshop_assessment_save'; static ASSESSMENT_SAVED = 'addon_mod_workshop_assessment_saved'; + static ASSESSMENT_INVALIDATED = 'addon_mod_workshop_assessment_invalidated'; protected ROOT_CACHE_KEY = 'mmaModWorkshop:'; diff --git a/src/addon/mod/workshop/workshop.scss b/src/addon/mod/workshop/workshop.scss new file mode 100644 index 000000000..3f79267f3 --- /dev/null +++ b/src/addon/mod/workshop/workshop.scss @@ -0,0 +1,13 @@ +addon-mod-workshop-submission, +addon-mod-workshop-assessment, // TODO, change names +addon-mod-workshop-assessment { + + p.addon-overriden-grade { + color: color($colors, success); + } + + p.addon-has-overriden-grade { + color: color($colors, danger); + text-decoration: line-through; + } +} From aef3f535282bd330bd19faada50f2849c0c1ecbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 4 Jun 2018 12:14:46 +0200 Subject: [PATCH 12/20] MOBILE-2354 workshop: Assessment component --- .../components/assessment/assessment.html | 27 ++++ .../components/assessment/assessment.scss | 35 ++++ .../components/assessment/assessment.ts | 149 ++++++++++++++++++ .../workshop/components/components.module.ts | 7 +- .../workshop/pages/submission/submission.html | 8 +- .../workshop/pages/submission/submission.ts | 2 +- src/addon/mod/workshop/workshop.scss | 5 +- 7 files changed, 225 insertions(+), 8 deletions(-) create mode 100644 src/addon/mod/workshop/components/assessment/assessment.html create mode 100644 src/addon/mod/workshop/components/assessment/assessment.scss create mode 100644 src/addon/mod/workshop/components/assessment/assessment.ts diff --git a/src/addon/mod/workshop/components/assessment/assessment.html b/src/addon/mod/workshop/components/assessment/assessment.html new file mode 100644 index 000000000..d277f8eb7 --- /dev/null +++ b/src/addon/mod/workshop/components/assessment/assessment.html @@ -0,0 +1,27 @@ + + + + + + +

{{profile.fullname}}

+

+ {{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{assessment.grade}} +

+

+ {{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{assessment.gradinggrade}} +

+

+ {{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{assessment.gradinggradeover}} +

+

+ {{ 'addon.mod_workshop.weightinfo' | translate:{$a: assessment.weight } }} +

+ {{ 'addon.mod_workshop.notassessed' | translate }} + + + + {{ 'core.notsent' | translate }} + +
+
diff --git a/src/addon/mod/workshop/components/assessment/assessment.scss b/src/addon/mod/workshop/components/assessment/assessment.scss new file mode 100644 index 000000000..9ed4dbcc5 --- /dev/null +++ b/src/addon/mod/workshop/components/assessment/assessment.scss @@ -0,0 +1,35 @@ +addon-mod-workshop-assessment { + .item-md.item-block .item-inner { + border-bottom: 1px solid $list-md-border-color; + } + + .item-ios.item-block .item-inner { + border-bottom: $hairlines-width solid $list-ios-border-color; + } + + .item-wp.item-block .item-inner { + border-bottom: 1px solid $list-wp-border-color; + } + + &:last-child .item .item-inner { + border-bottom: 0; + } +} + +.card.with-borders addon-mod-workshop-assessment { + .item-md.item-block .item-inner { + border-bottom: 1px solid $list-md-border-color; + } + + .item-ios.item-block .item-inner { + border-bottom: $hairlines-width solid $list-ios-border-color; + } + + .item-wp.item-block .item-inner { + border-bottom: 1px solid $list-wp-border-color; + } + + &:last-child .item .item-inner { + border-bottom: 0; + } +} \ No newline at end of file diff --git a/src/addon/mod/workshop/components/assessment/assessment.ts b/src/addon/mod/workshop/components/assessment/assessment.ts new file mode 100644 index 000000000..7e16f1491 --- /dev/null +++ b/src/addon/mod/workshop/components/assessment/assessment.ts @@ -0,0 +1,149 @@ +// (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 { NavController } from 'ionic-angular'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUserProvider } from '@core/user/providers/user'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { AddonModWorkshopHelperProvider } from '../../providers/helper'; +import { AddonModWorkshopOfflineProvider } from '../../providers/offline'; + +/** + * Component that displays workshop assessment. + */ +@Component({ + selector: 'addon-mod-workshop-assessment', + templateUrl: 'assessment.html', +}) +export class AddonModWorkshopAssessmentComponent implements OnInit { + @Input() assessment: any; + @Input() summary?: boolean; + @Input() courseId: number; + @Input() submission: any; + @Input() module?: any; + @Input() workshop: any; + @Input() access: any; + + canViewAssessment = false; + canSelfAssess = false; + profile: any; + showGrade: any; + offline = false; + loaded = false; + + protected currentUserId: number; + protected assessmentId: number; + + constructor(private workshopOffline: AddonModWorkshopOfflineProvider, private workshopHelper: AddonModWorkshopHelperProvider, + private navCtrl: NavController, private userProvider: CoreUserProvider, private domUtils: CoreDomUtilsProvider, + sitesProvider: CoreSitesProvider) { + this.currentUserId = sitesProvider.getCurrentSiteUserId(); + this.showGrade = this.workshopHelper.showGrade; + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + const canAssess = this.access && this.access.assessingallowed, + userId = this.assessment.userid || this.assessment.reviewerid, + promises = []; + + this.assessmentId = this.assessment.assessmentid || this.assessment.id; + this.canViewAssessment = this.assessment.grade; + this.canSelfAssess = canAssess && userId == this.currentUserId; + + if (userId) { + promises.push(this.userProvider.getProfile(userId, this.courseId, true).then((profile) => { + this.profile = profile; + })); + } + + let assessOffline; + if (userId == this.currentUserId) { + assessOffline = this.workshopOffline.getAssessment(this.workshop.id, this.assessmentId) .then((offlineAssess) => { + this.offline = true; + this.assessment.weight = offlineAssess.inputdata.weight; + }); + } else { + assessOffline = this.workshopOffline.getEvaluateAssessment(this.workshop.id, this.assessmentId) + .then((offlineAssess) => { + this.offline = true; + this.assessment.gradinggradeover = offlineAssess.gradinggradeover; + this.assessment.weight = offlineAssess.weight; + }); + } + + promises.push(assessOffline.catch(() => { + this.offline = false; + // Ignore errors. + })); + + Promise.all(promises).finally(() => { + this.loaded = true; + }); + } + + /** + * Navigate to the assessment. + */ + gotoAssessment(): void { + if (!this.canSelfAssess && this.canViewAssessment) { + const params = { + assessment: this.assessment, + submission: this.submission, + profile: this.profile, + courseId: this.courseId, + assessmentId: this.assessmentId + }; + + if (!this.submission) { + const modal = this.domUtils.showModalLoading('core.sending', true); + + this.workshopHelper.getSubmissionById(this.workshop.id, this.assessment.submissionid) + .then((submissionData) => { + + params.submission = submissionData; + this.navCtrl.push('AddonModWorkshopAssessmentPage', params); + }).catch((message) => { + this.domUtils.showErrorModalDefault(message, 'Cannot load submission'); + }).finally(() => { + modal.dismiss(); + }); + } else { + this.navCtrl.push('AddonModWorkshopAssessmentPage', params); + } + } + } + + /** + * Navigate to my own assessment. + */ + gotoOwnAssessment(): void { + if (this.canSelfAssess) { + const params = { + module: this.module, + workshop: this.workshop, + access: this.access, + courseId: this.courseId, + profile: this.profile, + submission: this.submission, + assessment: this.assessment + }; + + this.navCtrl.push('AddonModWorkshopSubmissionPage', params); + } + } +} diff --git a/src/addon/mod/workshop/components/components.module.ts b/src/addon/mod/workshop/components/components.module.ts index cdcf3f5e6..0c1e07224 100644 --- a/src/addon/mod/workshop/components/components.module.ts +++ b/src/addon/mod/workshop/components/components.module.ts @@ -22,11 +22,13 @@ import { CorePipesModule } from '@pipes/pipes.module'; import { CoreCourseComponentsModule } from '@core/course/components/components.module'; import { AddonModWorkshopIndexComponent } from './index/index'; import { AddonModWorkshopSubmissionComponent } from './submission/submission'; +import { AddonModWorkshopAssessmentComponent } from './assessment/assessment'; @NgModule({ declarations: [ AddonModWorkshopIndexComponent, - AddonModWorkshopSubmissionComponent + AddonModWorkshopSubmissionComponent, + AddonModWorkshopAssessmentComponent ], imports: [ CommonModule, @@ -41,7 +43,8 @@ import { AddonModWorkshopSubmissionComponent } from './submission/submission'; ], exports: [ AddonModWorkshopIndexComponent, - AddonModWorkshopSubmissionComponent + AddonModWorkshopSubmissionComponent, + AddonModWorkshopAssessmentComponent ], entryComponents: [ AddonModWorkshopIndexComponent diff --git a/src/addon/mod/workshop/pages/submission/submission.html b/src/addon/mod/workshop/pages/submission/submission.html index d77cd6d4a..851a1e531 100644 --- a/src/addon/mod/workshop/pages/submission/submission.html +++ b/src/addon/mod/workshop/pages/submission/submission.html @@ -48,21 +48,23 @@

{{ 'addon.mod_workshop.yourassessment' | translate }}

- +

{{ 'addon.mod_workshop.receivedgrades' | translate }}

- + + +

{{ 'addon.mod_workshop.givengrades' | translate }}

- +
diff --git a/src/addon/mod/workshop/pages/submission/submission.ts b/src/addon/mod/workshop/pages/submission/submission.ts index 4a6ba7235..7d8e30cf6 100644 --- a/src/addon/mod/workshop/pages/submission/submission.ts +++ b/src/addon/mod/workshop/pages/submission/submission.ts @@ -36,7 +36,7 @@ import { AddonModWorkshopSyncProvider } from '../../providers/sync'; */ @IonicPage({ segment: 'addon-mod-workshop-submission' }) @Component({ - selector: 'page-addon-mod-workshop-submission', + selector: 'page-addon-mod-workshop-submission-page', templateUrl: 'submission.html', }) export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { diff --git a/src/addon/mod/workshop/workshop.scss b/src/addon/mod/workshop/workshop.scss index 3f79267f3..ab0146f9c 100644 --- a/src/addon/mod/workshop/workshop.scss +++ b/src/addon/mod/workshop/workshop.scss @@ -1,6 +1,7 @@ addon-mod-workshop-submission, -addon-mod-workshop-assessment, // TODO, change names -addon-mod-workshop-assessment { +addon-mod-workshop-submission-page, +addon-mod-workshop-assessment, +addon-mod-workshop-assessment-page { p.addon-overriden-grade { color: color($colors, success); From 7e80c5c77b7374cdbaff90f4ed45ca15bc2dfbfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 4 Jun 2018 14:40:02 +0200 Subject: [PATCH 13/20] MOBILE-2354 workshop: Assessment page --- .../workshop/pages/assessment/assessment.html | 78 ++++ .../pages/assessment/assessment.module.ts | 35 ++ .../workshop/pages/assessment/assessment.ts | 375 ++++++++++++++++++ .../workshop/pages/submission/submission.ts | 7 +- 4 files changed, 490 insertions(+), 5 deletions(-) create mode 100644 src/addon/mod/workshop/pages/assessment/assessment.html create mode 100644 src/addon/mod/workshop/pages/assessment/assessment.module.ts create mode 100644 src/addon/mod/workshop/pages/assessment/assessment.ts diff --git a/src/addon/mod/workshop/pages/assessment/assessment.html b/src/addon/mod/workshop/pages/assessment/assessment.html new file mode 100644 index 000000000..2a36c1cc9 --- /dev/null +++ b/src/addon/mod/workshop/pages/assessment/assessment.html @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + +

{{profile.fullname}}

+ + +

+ {{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{assessment.grade}} +

+

+ {{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{assessment.gradinggrade}} +

+

+ {{ 'addon.mod_workshop.gradinggradeover' | translate }}: {{assessment.gradinggradeover}} +

+

+ {{ 'addon.mod_workshop.weightinfo' | translate:{$a: assessment.weight } }} +

+ + {{ 'addon.mod_workshop.notassessed' | translate }} + +
+ + + + + +

{{ 'addon.mod_workshop.assessmentsettings' | translate }}

+
+ + {{ 'addon.mod_workshop.assessmentweight' | translate }} + + {{ w }} + + + +

{{ 'addon.mod_workshop.gradinggradecalculated' | translate }}

+

{{ assessment.gradinggrade }}

+
+ + {{ 'addon.mod_workshop.gradinggradeover' | translate }} + + {{grade.label}} + + + + {{ 'addon.mod_workshop.feedbackreviewer' | translate }} + + + + + + + + +

{{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateGradingByProfile.fullname} }}

+ +
+
+
+
diff --git a/src/addon/mod/workshop/pages/assessment/assessment.module.ts b/src/addon/mod/workshop/pages/assessment/assessment.module.ts new file mode 100644 index 000000000..99417f1f6 --- /dev/null +++ b/src/addon/mod/workshop/pages/assessment/assessment.module.ts @@ -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 { AddonModWorkshopComponentsModule } from '../../components/components.module'; +import { AddonModWorkshopAssessmentPage } from './assessment'; + +@NgModule({ + declarations: [ + AddonModWorkshopAssessmentPage, + ], + imports: [ + CoreDirectivesModule, + CoreComponentsModule, + AddonModWorkshopComponentsModule, + IonicPageModule.forChild(AddonModWorkshopAssessmentPage), + TranslateModule.forChild() + ], +}) +export class AddonModWorkshopAssessmentPageModule {} diff --git a/src/addon/mod/workshop/pages/assessment/assessment.ts b/src/addon/mod/workshop/pages/assessment/assessment.ts new file mode 100644 index 000000000..db38a5f5e --- /dev/null +++ b/src/addon/mod/workshop/pages/assessment/assessment.ts @@ -0,0 +1,375 @@ +// (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 } from '@angular/core'; +import { IonicPage, NavParams, NavController } from 'ionic-angular'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreUserProvider } from '@core/user/providers/user'; +import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; +import { AddonModWorkshopProvider } from '../../providers/workshop'; +import { AddonModWorkshopHelperProvider } from '../../providers/helper'; +import { AddonModWorkshopOfflineProvider } from '../../providers/offline'; +import { AddonModWorkshopSyncProvider } from '../../providers/sync'; + +/** + * Page that displays a workshop assessment. + */ +@IonicPage({ segment: 'addon-mod-workshop-assessment' }) +@Component({ + selector: 'page-addon-mod-workshop-assessment-page', + templateUrl: 'assessment.html', +}) +export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy { + + assessment: any; + submission: any; + profile: any; + courseId: number; + + access: any = {}; + assessmentId: number; + evaluating = false; + loaded = false; + showGrade: any; + evaluateForm: FormGroup; + maxGrade: number; + workshop: any = {}; + strategy: any; + title: string; + evaluate: any; + weights = []; + evaluateByProfile: any; + evaluationGrades: any; + + protected workshopId: number; + protected originalEvaluation: any = {}; + protected hasOffline = false; + protected syncObserver: any; + protected isDestroyed = false; + protected siteId: string; + protected currentUserId: number; + protected forceLeave = false; + + constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, protected courseProvider: CoreCourseProvider, + protected workshopProvider: AddonModWorkshopProvider, protected workshopOffline: AddonModWorkshopOfflineProvider, + protected workshopHelper: AddonModWorkshopHelperProvider, protected navCtrl: NavController, + protected syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider, protected fb: FormBuilder, + protected translate: TranslateService, protected eventsProvider: CoreEventsProvider, + protected domUtils: CoreDomUtilsProvider, protected gradesHelper: CoreGradesHelperProvider, + protected userProvider: CoreUserProvider) { + + this.assessment = navParams.get('assessment') || {}; + this.submission = navParams.get('submission') || {}; + this.profile = navParams.get('profile') || {}; + this.courseId = navParams.get('courseId') || null; + + this.assessmentId = this.assessment.assessmentid || this.assessment.id; + this.workshopId = this.submission.workshopid || null; + this.siteId = sitesProvider.getCurrentSiteId(); + this.currentUserId = sitesProvider.getCurrentSiteUserId(); + + this.showGrade = this.workshopHelper.showGrade; + + this.evaluateForm = new FormGroup({}); + this.evaluateForm.addControl('weight', this.fb.control('', Validators.required)); + this.evaluateForm.addControl('grade', this.fb.control('')); + this.evaluateForm.addControl('text', this.fb.control('')); + + // Refresh workshop on sync. + this.syncObserver = this.eventsProvider.on(AddonModWorkshopSyncProvider.AUTO_SYNCED, (data) => { + // Update just when all database is synced. + if (this.workshopId === data.workshopid) { + this.loaded = false; + this.refreshAllData(); + } + }, this.siteId); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.fetchAssessmentData(); + } + + /** + * Check if we can leave the page or not. + * + * @return {boolean|Promise} Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): boolean | Promise { + if (this.forceLeave || !this.evaluating) { + return true; + } + + if (!this.hasEvaluationChanged()) { + return Promise.resolve(); + } + + // Show confirmation if some data has been modified. + return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); + } + + /** + * Fetch the assessment data. + * + * @return {Promise} Resolved when done. + */ + protected fetchAssessmentData(): Promise { + return this.workshopProvider.getWorkshopById(this.courseId, this.workshopId).then((workshopData) => { + this.workshop = workshopData; + this.title = this.workshop.name; + this.strategy = this.workshop.strategy; + + return this.courseProvider.getModuleBasicGradeInfo(workshopData.coursemodule); + }).then((gradeInfo) => { + this.maxGrade = gradeInfo.grade; + + return this.workshopProvider.getWorkshopAccessInformation(this.workshopId); + }).then((accessData) => { + this.access = accessData; + + // Load Weights selector. + if (this.assessmentId && (accessData.canallocate || accessData.canoverridegrades)) { + if (!this.isDestroyed) { + // Block the workshop. + this.syncProvider.blockOperation(AddonModWorkshopProvider.COMPONENT, this.workshopId); + } + + this.evaluating = true; + } else { + this.evaluating = false; + } + + if (this.evaluating || this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) { + // Get all info of the assessment. + return this.workshopHelper.getReviewerAssessmentById(this.workshopId, this.assessmentId, this.profile.id) + .then((assessment) => { + let defaultGrade, promise; + + this.assessment = this.workshopHelper.realGradeValue(this.workshop, assessment); + this.evaluate = { + weight: this.assessment.weight, + text: this.assessment.feedbackreviewer + }; + + if (this.evaluating) { + if (accessData.canallocate) { + this.weights = []; + for (let i = 16; i >= 0; i--) { + this.weights[i] = i; + } + } + + if (accessData.canoverridegrades) { + defaultGrade = this.translate.instant('addon.mod_workshop.notoverridden'); + promise = this.gradesHelper.makeGradesMenu(this.workshop.gradinggrade, this.workshopId, defaultGrade, + -1).then((grades) => { + this.evaluationGrades = grades; + }); + } else { + promise = Promise.resolve(); + } + + return promise.then(() => { + return this.workshopOffline.getEvaluateAssessment(this.workshopId, this.assessmentId) + .then((offlineAssess) => { + this.hasOffline = true; + this.evaluate.weight = offlineAssess.weight; + if (accessData.canoverridegrades) { + this.evaluate.text = offlineAssess.feedbacktext; + this.evaluate.grade = offlineAssess.gradinggradeover || -1; + } + }).catch(() => { + this.hasOffline = false; + // No offline, load online. + if (accessData.canoverridegrades) { + this.evaluate.text = this.assessment.feedbackreviewer; + this.evaluate.grade = this.assessment.gradinggradeover || -1; + } + }); + }).finally(() => { + this.originalEvaluation.weight = this.evaluate.weight; + if (accessData.canoverridegrades) { + this.originalEvaluation.text = this.evaluate.text; + this.originalEvaluation.grade = this.evaluate.grade; + } + + this.evaluateForm.controls['weight'].setValue(this.evaluate.weight); + if (accessData.canoverridegrades) { + this.evaluateForm.controls['grade'].setValue(this.evaluate.grade); + this.evaluateForm.controls['text'].setValue(this.evaluate.text); + } + }); + + } else if (this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED && this.assessment.gradinggradeoverby) { + return this.userProvider.getProfile(this.assessment.gradinggradeoverby, this.courseId, true) + .then((profile) => { + this.evaluateByProfile = profile; + }); + } + }); + } + }).catch((message) => { + this.domUtils.showErrorModalDefault(message, 'mm.course.errorgetmodule', true); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Force leaving the page, without checking for changes. + */ + protected forceLeavePage(): void { + this.forceLeave = true; + this.navCtrl.pop(); + } + + /** + * Check if data has changed. + * + * @return {boolean} True if changed, false otherwise. + */ + protected hasEvaluationChanged(): boolean { + if (!this.loaded || !this.evaluating) { + return false; + } + + const inputData = this.evaluateForm.value; + + if (this.originalEvaluation.weight != inputData.weight) { + return true; + } + + if (this.access.canoverridegrades) { + if (this.originalEvaluation.text != inputData.text) { + return true; + } + + if (this.originalEvaluation.grade != inputData.grade) { + return true; + } + } + + return false; + } + + /** + * Convenience function to refresh all the data. + * + * @return {Promise} Resolved when done. + */ + protected refreshAllData(): Promise { + const promises = []; + + promises.push(this.workshopProvider.invalidateWorkshopData(this.courseId)); + promises.push(this.workshopProvider.invalidateWorkshopAccessInformationData(this.workshopId)); + promises.push(this.workshopProvider.invalidateReviewerAssesmentsData(this.workshopId)); + + if (this.assessmentId) { + promises.push(this.workshopProvider.invalidateAssessmentFormData(this.workshopId, this.assessmentId)); + promises.push(this.workshopProvider.invalidateAssessmentData(this.workshopId, this.assessmentId)); + } + + return Promise.all(promises).finally(() => { + this.eventsProvider.trigger(AddonModWorkshopProvider.ASSESSMENT_INVALIDATED, this.siteId); + + return this.fetchAssessmentData(); + }); + } + + /** + * Pull to refresh. + * + * @param {any} refresher Refresher. + */ + refreshAssessment(refresher: any): void { + if (this.loaded) { + this.refreshAllData().finally(() => { + refresher.complete(); + }); + } + } + + /** + * Save the assessment evaluation. + */ + saveEvaluation(): void { + // Check if data has changed. + if (this.hasEvaluationChanged()) { + this.sendEvaluation().then(() => { + this.forceLeavePage(); + }); + } else { + // Nothing to save, just go back. + this.forceLeavePage(); + } + } + + /** + * Sends the evaluation to be saved on the server. + * + * @return {Promise} Resolved when done. + */ + protected sendEvaluation(): Promise { + const modal = this.domUtils.showModalLoading('core.sending', true); + + // Check if rich text editor is enabled or not. + return this.domUtils.isRichTextEditorEnabled().then((rteEnabled) => { + const inputData = this.evaluateForm.value; + + inputData.grade = inputData.grade >= 0 ? inputData.grade : ''; + if (!rteEnabled) { + // Rich text editor not enabled, add some HTML to the message if needed. + inputData.text = this.textUtils.formatHtmlLines(inputData.text); + } + + // Try to send it to server. + return this.workshopProvider.evaluateAssessment(this.workshopId, this.assessmentId, this.courseId, inputData.text, + inputData.weight, inputData.grade); + }).then(() => { + const data = { + workshopId: this.workshopId, + assessmentId: this.assessmentId, + userId: this.currentUserId + }; + + return this.workshopProvider.invalidateAssessmentData(this.workshopId, this.assessmentId).finally(() => { + this.eventsProvider.trigger(AddonModWorkshopProvider.ASSESSMENT_SAVED, data, this.siteId); + }); + }).catch((message) => { + this.domUtils.showErrorModalDefault(message, 'Cannot save assessment evaluation'); + }).finally(() => { + modal.dismiss(); + }); + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + this.isDestroyed = true; + + this.syncObserver && this.syncObserver.off(); + // Restore original back functions. + this.syncProvider.unblockOperation(AddonModWorkshopProvider.COMPONENT, this.workshopId); + } +} diff --git a/src/addon/mod/workshop/pages/submission/submission.ts b/src/addon/mod/workshop/pages/submission/submission.ts index 7d8e30cf6..d203dcec4 100644 --- a/src/addon/mod/workshop/pages/submission/submission.ts +++ b/src/addon/mod/workshop/pages/submission/submission.ts @@ -19,10 +19,8 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; -import { CoreFileSessionProvider } from '@providers/file-session'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; -import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; @@ -79,10 +77,9 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { protected syncObserver: any; protected isDestroyed = false; - constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, protected fileUploaderProvider: CoreFileUploaderProvider, - protected workshopProvider: AddonModWorkshopProvider, protected workshopOffline: AddonModWorkshopOfflineProvider, + constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, protected workshopProvider: AddonModWorkshopProvider, + protected workshopOffline: AddonModWorkshopOfflineProvider, protected syncProvider: CoreSyncProvider, protected workshopHelper: AddonModWorkshopHelperProvider, protected navCtrl: NavController, - protected fileSessionprovider: CoreFileSessionProvider, protected syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider, protected domUtils: CoreDomUtilsProvider, protected fb: FormBuilder, protected translate: TranslateService, protected eventsProvider: CoreEventsProvider, protected courseProvider: CoreCourseProvider, @Optional() protected content: Content, From f0c80085d8c6f9256d29ef3d6f5d0f66527b76f8 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Tue, 12 Jun 2018 10:18:12 +0200 Subject: [PATCH 14/20] MOBILE-2354 workshop: Assessment strategy component --- .../assessment-strategy.html | 46 +++ .../assessment-strategy.ts | 361 ++++++++++++++++++ .../workshop/components/components.module.ts | 7 +- 3 files changed, 412 insertions(+), 2 deletions(-) create mode 100644 src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.html create mode 100644 src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts 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 }}

+ +
+ + + + + + +
+ {{ 'addon.mod_workshop.assessmentstrategynotsupported' | translate:{$a: strategy} }} +
+ + + +

{{ 'addon.mod_workshop.overallfeedback' | translate }}

+
+ + {{ 'addon.mod_workshop.feedbackauthor' | translate }} + + + + + + + {{ 'addon.mod_workshop.assessmentweight' | translate }} + + {{w}} + + + + + + + + + + + + + +
+
+
\ 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 From e7582678f9d11636fe247c9cea5cc77daabf04c9 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Tue, 12 Jun 2018 10:18:42 +0200 Subject: [PATCH 15/20] MOBILE-2354 workshop: Assessment strategy handlers --- .../accumulative/accumulative.module.ts | 51 +++++++ .../accumulative/component/accumulative.html | 27 ++++ .../accumulative/component/accumulative.ts | 30 ++++ .../assessment/accumulative/lang/en.json | 6 + .../accumulative/providers/handler.ts | 144 ++++++++++++++++++ .../workshop/assessment/assessment.module.ts | 29 ++++ .../assessment/comments/comments.module.ts | 51 +++++++ .../comments/component/comments.html | 15 ++ .../assessment/comments/component/comments.ts | 30 ++++ .../workshop/assessment/comments/lang/en.json | 4 + .../assessment/comments/providers/handler.ts | 120 +++++++++++++++ .../numerrors/component/numerrors.html | 28 ++++ .../numerrors/component/numerrors.ts | 30 ++++ .../assessment/numerrors/lang/en.json | 5 + .../assessment/numerrors/numerrors.module.ts | 51 +++++++ .../assessment/numerrors/providers/handler.ts | 130 ++++++++++++++++ .../assessment/rubric/component/rubric.html | 13 ++ .../assessment/rubric/component/rubric.ts | 30 ++++ .../workshop/assessment/rubric/lang/en.json | 4 + .../assessment/rubric/providers/handler.ts | 121 +++++++++++++++ .../assessment/rubric/rubric.module.ts | 51 +++++++ src/addon/mod/workshop/workshop.module.ts | 4 +- 22 files changed, 973 insertions(+), 1 deletion(-) create mode 100644 src/addon/mod/workshop/assessment/accumulative/accumulative.module.ts create mode 100644 src/addon/mod/workshop/assessment/accumulative/component/accumulative.html create mode 100644 src/addon/mod/workshop/assessment/accumulative/component/accumulative.ts create mode 100644 src/addon/mod/workshop/assessment/accumulative/lang/en.json create mode 100644 src/addon/mod/workshop/assessment/accumulative/providers/handler.ts create mode 100644 src/addon/mod/workshop/assessment/assessment.module.ts create mode 100644 src/addon/mod/workshop/assessment/comments/comments.module.ts create mode 100644 src/addon/mod/workshop/assessment/comments/component/comments.html create mode 100644 src/addon/mod/workshop/assessment/comments/component/comments.ts create mode 100644 src/addon/mod/workshop/assessment/comments/lang/en.json create mode 100644 src/addon/mod/workshop/assessment/comments/providers/handler.ts create mode 100644 src/addon/mod/workshop/assessment/numerrors/component/numerrors.html create mode 100644 src/addon/mod/workshop/assessment/numerrors/component/numerrors.ts create mode 100644 src/addon/mod/workshop/assessment/numerrors/lang/en.json create mode 100644 src/addon/mod/workshop/assessment/numerrors/numerrors.module.ts create mode 100644 src/addon/mod/workshop/assessment/numerrors/providers/handler.ts create mode 100644 src/addon/mod/workshop/assessment/rubric/component/rubric.html create mode 100644 src/addon/mod/workshop/assessment/rubric/component/rubric.ts create mode 100644 src/addon/mod/workshop/assessment/rubric/lang/en.json create mode 100644 src/addon/mod/workshop/assessment/rubric/providers/handler.ts create mode 100644 src/addon/mod/workshop/assessment/rubric/rubric.module.ts diff --git a/src/addon/mod/workshop/assessment/accumulative/accumulative.module.ts b/src/addon/mod/workshop/assessment/accumulative/accumulative.module.ts new file mode 100644 index 000000000..f20ce1fec --- /dev/null +++ b/src/addon/mod/workshop/assessment/accumulative/accumulative.module.ts @@ -0,0 +1,51 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonModWorkshopAssessmentStrategyAccumulativeComponent } from './component/accumulative'; +import { AddonModWorkshopAssessmentStrategyAccumulativeHandler } from './providers/handler'; +import { AddonWorkshopAssessmentStrategyDelegate } from '../../providers/assessment-strategy-delegate'; + +@NgModule({ + declarations: [ + AddonModWorkshopAssessmentStrategyAccumulativeComponent, + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + AddonModWorkshopAssessmentStrategyAccumulativeHandler + ], + exports: [ + AddonModWorkshopAssessmentStrategyAccumulativeComponent + ], + entryComponents: [ + AddonModWorkshopAssessmentStrategyAccumulativeComponent + ] +}) +export class AddonModWorkshopAssessmentStrategyAccumulativeModule { + constructor(strategyDelegate: AddonWorkshopAssessmentStrategyDelegate, + strategyHandler: AddonModWorkshopAssessmentStrategyAccumulativeHandler) { + strategyDelegate.registerHandler(strategyHandler); + } +} diff --git a/src/addon/mod/workshop/assessment/accumulative/component/accumulative.html b/src/addon/mod/workshop/assessment/accumulative/component/accumulative.html new file mode 100644 index 000000000..a65046ab6 --- /dev/null +++ b/src/addon/mod/workshop/assessment/accumulative/component/accumulative.html @@ -0,0 +1,27 @@ + + +

{{ field.dimtitle }}

+ +
+ + {{ 'addon.mod_workshop_assessment_accumulative.dimensiongradefor' | translate : {'$a': field.dimtitle } }} + + {{grade.label}} + + + + +

{{ 'addon.mod_workshop_assessment_accumulative.dimensiongradefor' | translate : {'$a': field.dimtitle } }}

+ +

{{grade.label}}

+
+
+ + {{ 'addon.mod_workshop_assessment_accumulative.dimensioncommentfor' | translate : {'$a': field.dimtitle } }} + + + +

{{ 'addon.mod_workshop_assessment_accumulative.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}

+

+
+
diff --git a/src/addon/mod/workshop/assessment/accumulative/component/accumulative.ts b/src/addon/mod/workshop/assessment/accumulative/component/accumulative.ts new file mode 100644 index 000000000..962c20068 --- /dev/null +++ b/src/addon/mod/workshop/assessment/accumulative/component/accumulative.ts @@ -0,0 +1,30 @@ +// (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 } from '@angular/core'; + +/** + * Component for accumulative assessment strategy. + */ +@Component({ + selector: 'addon-mod-workshop-assessment-strategy-accumulative', + templateUrl: 'accumulative.html', +}) +export class AddonModWorkshopAssessmentStrategyAccumulativeComponent { + @Input() workshopId: number; + @Input() assessment: any; + @Input() edit: boolean; + @Input() selectedValues: any[]; + @Input() fieldErrors: any; +} diff --git a/src/addon/mod/workshop/assessment/accumulative/lang/en.json b/src/addon/mod/workshop/assessment/accumulative/lang/en.json new file mode 100644 index 000000000..72c26cfa8 --- /dev/null +++ b/src/addon/mod/workshop/assessment/accumulative/lang/en.json @@ -0,0 +1,6 @@ +{ + "dimensioncommentfor": "Comment for {{$a}}", + "dimensiongradefor": "Grade for {{$a}}", + "dimensionnumber": "Aspect {{$a}}", + "mustchoosegrade": "You have to select a grade for this aspect" +} diff --git a/src/addon/mod/workshop/assessment/accumulative/providers/handler.ts b/src/addon/mod/workshop/assessment/accumulative/providers/handler.ts new file mode 100644 index 000000000..e0d4496f2 --- /dev/null +++ b/src/addon/mod/workshop/assessment/accumulative/providers/handler.ts @@ -0,0 +1,144 @@ +// (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 { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; +import { AddonWorkshopAssessmentStrategyHandler } from '../../../providers/assessment-strategy-delegate'; +import { AddonModWorkshopAssessmentStrategyAccumulativeComponent } from '../component/accumulative'; + +/** + * Handler for accumulative assessment strategy plugin. + */ +@Injectable() +export class AddonModWorkshopAssessmentStrategyAccumulativeHandler implements AddonWorkshopAssessmentStrategyHandler { + name = 'AddonModWorkshopAssessmentStrategyAccumulative'; + strategyName = 'accumulative'; + + constructor(private translate: TranslateService, private gradesHelper: CoreGradesHelperProvider) {} + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Returns the component to render the plugin. + * + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(): any { + return AddonModWorkshopAssessmentStrategyAccumulativeComponent; + } + + /** + * Prepare original values to be shown and compared. + * + * @param {any} form Original data of the form. + * @param {number} workshopId WorkShop Id + * @return {Promise} Promise resolved with original values sorted. + */ + getOriginalValues(form: any, workshopId: number): Promise { + const defaultGrade = this.translate.instant('core.choosedots'), + originalValues = [], + promises = []; + + form.fields.forEach((field, n) => { + field.dimtitle = this.translate.instant( + 'addon.mod_workshop_assessment_accumulative.dimensionnumber', {$a: field.number}); + + const scale = parseInt(field.grade, 10) < 0 ? form.dimensionsinfo[n].scale : null; + + if (!form.current[n]) { + form.current[n] = {}; + } + + originalValues[n] = { + peercomment: form.current[n].peercomment || '', + number: field.number + }; + + form.current[n].grade = form.current[n].grade ? parseInt(form.current[n].grade, 10) : -1; + + promises.push(this.gradesHelper.makeGradesMenu(field.grade, workshopId, defaultGrade, -1, scale).then((grades) => { + field.grades = grades; + originalValues[n].grade = form.current[n].grade; + })); + }); + + return Promise.all(promises).then(() => { + return originalValues; + }); + } + + /** + * Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin. + * + * @param {any[]} originalValues Original values of the form. + * @param {any[]} currentValues Current values of the form. + * @return {boolean} True if data has changed, false otherwise. + */ + hasDataChanged(originalValues: any[], currentValues: any[]): boolean { + for (const x in originalValues) { + if (originalValues[x].grade != currentValues[x].grade) { + return true; + } + if (originalValues[x].peercomment != currentValues[x].peercomment) { + return true; + } + } + + return false; + } + + /** + * Prepare assessment data to be sent to the server depending on the strategy selected. + * + * @param {any{}} currentValues Current values of the form. + * @param {any} form Assessment form data. + * @return {Promise} Promise resolved with the data to be sent. Or rejected with the input errors object. + */ + prepareAssessmentData(currentValues: any[], form: any): Promise { + const data = {}; + const errors = {}; + let hasErrors = false; + + form.fields.forEach((field, idx) => { + const grade = parseInt(currentValues[idx].grade, 10); + if (!isNaN(grade) && grade >= 0) { + data['grade__idx_' + idx] = grade; + } else { + errors['grade_' + idx] = this.translate.instant('addon.mod_workshop_assessment_accumulative.mustchoosegrade'); + hasErrors = true; + } + + if (currentValues[idx].peercomment) { + data['peercomment__idx_' + idx] = currentValues[idx].peercomment; + } + + data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0; + data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10); + data['weight__idx_' + idx] = parseInt(field.weight, 10) || 0; + }); + + if (hasErrors) { + return Promise.reject(errors); + } + + return Promise.resolve(data); + } +} diff --git a/src/addon/mod/workshop/assessment/assessment.module.ts b/src/addon/mod/workshop/assessment/assessment.module.ts new file mode 100644 index 000000000..3a77823ad --- /dev/null +++ b/src/addon/mod/workshop/assessment/assessment.module.ts @@ -0,0 +1,29 @@ +// (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 { AddonModWorkshopAssessmentStrategyAccumulativeModule } from './accumulative/accumulative.module'; +import { AddonModWorkshopAssessmentStrategyCommentsModule } from './comments/comments.module'; +import { AddonModWorkshopAssessmentStrategyNumErrorsModule } from './numerrors/numerrors.module'; +import { AddonModWorkshopAssessmentStrategyRubricModule } from './rubric/rubric.module'; + +@NgModule({ + imports: [ + AddonModWorkshopAssessmentStrategyAccumulativeModule, + AddonModWorkshopAssessmentStrategyCommentsModule, + AddonModWorkshopAssessmentStrategyNumErrorsModule, + AddonModWorkshopAssessmentStrategyRubricModule, + ] +}) +export class AddonModWorkshopAssessmentStrategyModule {} diff --git a/src/addon/mod/workshop/assessment/comments/comments.module.ts b/src/addon/mod/workshop/assessment/comments/comments.module.ts new file mode 100644 index 000000000..c8f91b558 --- /dev/null +++ b/src/addon/mod/workshop/assessment/comments/comments.module.ts @@ -0,0 +1,51 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonModWorkshopAssessmentStrategyCommentsComponent } from './component/comments'; +import { AddonModWorkshopAssessmentStrategyCommentsHandler } from './providers/handler'; +import { AddonWorkshopAssessmentStrategyDelegate } from '../../providers/assessment-strategy-delegate'; + +@NgModule({ + declarations: [ + AddonModWorkshopAssessmentStrategyCommentsComponent, + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + AddonModWorkshopAssessmentStrategyCommentsHandler + ], + exports: [ + AddonModWorkshopAssessmentStrategyCommentsComponent + ], + entryComponents: [ + AddonModWorkshopAssessmentStrategyCommentsComponent + ] +}) +export class AddonModWorkshopAssessmentStrategyCommentsModule { + constructor(strategyDelegate: AddonWorkshopAssessmentStrategyDelegate, + strategyHandler: AddonModWorkshopAssessmentStrategyCommentsHandler) { + strategyDelegate.registerHandler(strategyHandler); + } +} diff --git a/src/addon/mod/workshop/assessment/comments/component/comments.html b/src/addon/mod/workshop/assessment/comments/component/comments.html new file mode 100644 index 000000000..e161192d8 --- /dev/null +++ b/src/addon/mod/workshop/assessment/comments/component/comments.html @@ -0,0 +1,15 @@ + + +

{{ field.dimtitle }}

+ +
+ + {{ 'addon.mod_workshop_assessment_comments.dimensioncommentfor' | translate : {'$a': field.dimtitle } }} + + + + +

{{ 'addon.mod_workshop_assessment_comments.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}

+

+
+
diff --git a/src/addon/mod/workshop/assessment/comments/component/comments.ts b/src/addon/mod/workshop/assessment/comments/component/comments.ts new file mode 100644 index 000000000..ef464cd8e --- /dev/null +++ b/src/addon/mod/workshop/assessment/comments/component/comments.ts @@ -0,0 +1,30 @@ +// (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 } from '@angular/core'; + +/** + * Component for comments assessment strategy. + */ +@Component({ + selector: 'addon-mod-workshop-assessment-strategy-comments', + templateUrl: 'comments.html', +}) +export class AddonModWorkshopAssessmentStrategyCommentsComponent { + @Input() workshopId: number; + @Input() assessment: any; + @Input() edit: boolean; + @Input() selectedValues: any[]; + @Input() fieldErrors: any; +} diff --git a/src/addon/mod/workshop/assessment/comments/lang/en.json b/src/addon/mod/workshop/assessment/comments/lang/en.json new file mode 100644 index 000000000..6db857323 --- /dev/null +++ b/src/addon/mod/workshop/assessment/comments/lang/en.json @@ -0,0 +1,4 @@ +{ + "dimensioncommentfor": "Comment for {{$a}}", + "dimensionnumber": "Aspect {{$a}}" +} diff --git a/src/addon/mod/workshop/assessment/comments/providers/handler.ts b/src/addon/mod/workshop/assessment/comments/providers/handler.ts new file mode 100644 index 000000000..ad0c525d4 --- /dev/null +++ b/src/addon/mod/workshop/assessment/comments/providers/handler.ts @@ -0,0 +1,120 @@ +// (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 { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { AddonWorkshopAssessmentStrategyHandler } from '../../../providers/assessment-strategy-delegate'; +import { AddonModWorkshopAssessmentStrategyCommentsComponent } from '../component/comments'; + +/** + * Handler for comments assessment strategy plugin. + */ +@Injectable() +export class AddonModWorkshopAssessmentStrategyCommentsHandler implements AddonWorkshopAssessmentStrategyHandler { + name = 'AddonModWorkshopAssessmentStrategyComments'; + strategyName = 'comments'; + + constructor(private translate: TranslateService) {} + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Returns the component to render the plugin. + * + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(): any { + return AddonModWorkshopAssessmentStrategyCommentsComponent; + } + + /** + * Prepare original values to be shown and compared. + * + * @param {any} form Original data of the form. + * @param {number} workshopId Workshop Id + * @return {Promise} Promise resolved with original values sorted. + */ + getOriginalValues(form: any, workshopId: number): Promise { + const originalValues = []; + + form.fields.forEach((field, n) => { + field.dimtitle = this.translate.instant('addon.mod_workshop_assessment_comments.dimensionnumber', {$a: field.number}); + + if (!form.current[n]) { + form.current[n] = {}; + } + + originalValues[n] = { + peercomment: form.current[n].peercomment || '', + number: field.number + }; + }); + + return Promise.resolve(originalValues); + } + + /** + * Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin. + * + * @param {any[]} originalValues Original values of the form. + * @param {any[]} currentValues Current values of the form. + * @return {boolean} True if data has changed, false otherwise. + */ + hasDataChanged(originalValues: any[], currentValues: any[]): boolean { + for (const x in originalValues) { + if (originalValues[x].peercomment != currentValues[x].peercomment) { + return true; + } + } + + return false; + } + + /** + * Prepare assessment data to be sent to the server depending on the strategy selected. + * + * @param {any{}} currentValues Current values of the form. + * @param {any} form Assessment form data. + * @return {Promise} Promise resolved with the data to be sent. Or rejected with the input errors object. + */ + prepareAssessmentData(currentValues: any[], form: any): Promise { + const data = {}; + const errors = {}; + let hasErrors = false; + + form.fields.forEach((field, idx) => { + if (currentValues[idx].peercomment) { + data['peercomment__idx_' + idx] = currentValues[idx].peercomment; + } else { + errors['peercomment_' + idx] = this.translate.instant('core.err_required'); + hasErrors = true; + } + + data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0; + data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10); + }); + + if (hasErrors) { + return Promise.reject(errors); + } + + return Promise.resolve(data); + } +} diff --git a/src/addon/mod/workshop/assessment/numerrors/component/numerrors.html b/src/addon/mod/workshop/assessment/numerrors/component/numerrors.html new file mode 100644 index 000000000..f98777c83 --- /dev/null +++ b/src/addon/mod/workshop/assessment/numerrors/component/numerrors.html @@ -0,0 +1,28 @@ + + +

{{ field.dimtitle }}

+ +
+ + + {{ 'addon.mod_workshop.yourassessmentfor' | translate : {'$a': field.dimtitle } }} + + + + + + + + + + + + + {{ 'addon.mod_workshop_assessment_numerrors.dimensioncommentfor' | translate : {'$a': field.dimtitle } }} + + + +

{{ 'addon.mod_workshop_assessment_numerrors.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}

+

+
+
diff --git a/src/addon/mod/workshop/assessment/numerrors/component/numerrors.ts b/src/addon/mod/workshop/assessment/numerrors/component/numerrors.ts new file mode 100644 index 000000000..37cc36e72 --- /dev/null +++ b/src/addon/mod/workshop/assessment/numerrors/component/numerrors.ts @@ -0,0 +1,30 @@ +// (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 } from '@angular/core'; + +/** + * Component for numerrors assessment strategy. + */ +@Component({ + selector: 'addon-mod-workshop-assessment-strategy-numerrors', + templateUrl: 'numerrors.html', +}) +export class AddonModWorkshopAssessmentStrategyNumErrorsComponent { + @Input() workshopId: number; + @Input() assessment: any; + @Input() edit: boolean; + @Input() selectedValues: any[]; + @Input() fieldErrors: any; +} diff --git a/src/addon/mod/workshop/assessment/numerrors/lang/en.json b/src/addon/mod/workshop/assessment/numerrors/lang/en.json new file mode 100644 index 000000000..d12b03206 --- /dev/null +++ b/src/addon/mod/workshop/assessment/numerrors/lang/en.json @@ -0,0 +1,5 @@ +{ + "dimensioncommentfor": "Comment for {{$a}}", + "dimensiongradefor": "Grade for {{$a}}", + "dimensionnumber": "Assertion {{$a}}" +} diff --git a/src/addon/mod/workshop/assessment/numerrors/numerrors.module.ts b/src/addon/mod/workshop/assessment/numerrors/numerrors.module.ts new file mode 100644 index 000000000..d066236cb --- /dev/null +++ b/src/addon/mod/workshop/assessment/numerrors/numerrors.module.ts @@ -0,0 +1,51 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonModWorkshopAssessmentStrategyNumErrorsComponent } from './component/numerrors'; +import { AddonModWorkshopAssessmentStrategyNumErrorsHandler } from './providers/handler'; +import { AddonWorkshopAssessmentStrategyDelegate } from '../../providers/assessment-strategy-delegate'; + +@NgModule({ + declarations: [ + AddonModWorkshopAssessmentStrategyNumErrorsComponent, + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + AddonModWorkshopAssessmentStrategyNumErrorsHandler + ], + exports: [ + AddonModWorkshopAssessmentStrategyNumErrorsComponent + ], + entryComponents: [ + AddonModWorkshopAssessmentStrategyNumErrorsComponent + ] +}) +export class AddonModWorkshopAssessmentStrategyNumErrorsModule { + constructor(strategyDelegate: AddonWorkshopAssessmentStrategyDelegate, + strategyHandler: AddonModWorkshopAssessmentStrategyNumErrorsHandler) { + strategyDelegate.registerHandler(strategyHandler); + } +} diff --git a/src/addon/mod/workshop/assessment/numerrors/providers/handler.ts b/src/addon/mod/workshop/assessment/numerrors/providers/handler.ts new file mode 100644 index 000000000..3f3630c65 --- /dev/null +++ b/src/addon/mod/workshop/assessment/numerrors/providers/handler.ts @@ -0,0 +1,130 @@ +// (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 { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { AddonWorkshopAssessmentStrategyHandler } from '../../../providers/assessment-strategy-delegate'; +import { AddonModWorkshopAssessmentStrategyNumErrorsComponent } from '../component/numerrors'; + +/** + * Handler for numerrors assessment strategy plugin. + */ +@Injectable() +export class AddonModWorkshopAssessmentStrategyNumErrorsHandler implements AddonWorkshopAssessmentStrategyHandler { + name = 'AddonModWorkshopAssessmentStrategyNumErrors'; + strategyName = 'numerrors'; + + constructor(private translate: TranslateService) {} + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Returns the component to render the plugin. + * + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(): any { + return AddonModWorkshopAssessmentStrategyNumErrorsComponent; + } + + /** + * Prepare original values to be shown and compared. + * + * @param {any} form Original data of the form. + * @param {number} workshopId Workshop Id + * @return {Promise} Promise resolved with original values sorted. + */ + getOriginalValues(form: any, workshopId: number): Promise { + const originalValues = []; + + form.fields.forEach((field, n) => { + field.dimtitle = this.translate.instant('addon.mod_workshop_assessment_numerrors.dimensionnumber', {$a: field.number}); + + if (!form.current[n]) { + form.current[n] = {}; + } + + originalValues[n] = { + peercomment: form.current[n].peercomment || '', + number: field.number, + grade: form.current[n].grade || '' + }; + }); + + return Promise.resolve(originalValues); + } + + /** + * Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin. + * + * @param {any[]} originalValues Original values of the form. + * @param {any[]} currentValues Current values of the form. + * @return {boolean} True if data has changed, false otherwise. + */ + hasDataChanged(originalValues: any[], currentValues: any[]): boolean { + for (const x in originalValues) { + if (originalValues[x].grade != currentValues[x].grade) { + return true; + } + if (originalValues[x].peercomment != currentValues[x].peercomment) { + return true; + } + } + + return false; + } + + /** + * Prepare assessment data to be sent to the server depending on the strategy selected. + * + * @param {any{}} currentValues Current values of the form. + * @param {any} form Assessment form data. + * @return {Promise} Promise resolved with the data to be sent. Or rejected with the input errors object. + */ + prepareAssessmentData(currentValues: any[], form: any): Promise { + const data = {}; + const errors = {}; + let hasErrors = false; + + form.fields.forEach((field, idx) => { + const grade = parseInt(currentValues[idx].grade); + if (!isNaN(grade) && grade >= 0) { + data['grade__idx_' + idx] = grade; + } else { + errors['grade_' + idx] = this.translate.instant('core.required'); + hasErrors = true; + } + + if (currentValues[idx].peercomment) { + data['peercomment__idx_' + idx] = currentValues[idx].peercomment; + } + + data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0; + data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10); + data['weight__idx_' + idx] = parseInt(field.weight, 10) || 0; + }); + + if (hasErrors) { + return Promise.reject(errors); + } + + return Promise.resolve(data); + } +} diff --git a/src/addon/mod/workshop/assessment/rubric/component/rubric.html b/src/addon/mod/workshop/assessment/rubric/component/rubric.html new file mode 100644 index 000000000..ad4a76917 --- /dev/null +++ b/src/addon/mod/workshop/assessment/rubric/component/rubric.html @@ -0,0 +1,13 @@ + + +

{{ field.dimtitle }}

+ + +
+ + +

+ +
+
+
diff --git a/src/addon/mod/workshop/assessment/rubric/component/rubric.ts b/src/addon/mod/workshop/assessment/rubric/component/rubric.ts new file mode 100644 index 000000000..4c946500e --- /dev/null +++ b/src/addon/mod/workshop/assessment/rubric/component/rubric.ts @@ -0,0 +1,30 @@ +// (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 } from '@angular/core'; + +/** + * Component for rubric assessment strategy. + */ +@Component({ + selector: 'addon-mod-workshop-assessment-strategy-rubric', + templateUrl: 'rubric.html', +}) +export class AddonModWorkshopAssessmentStrategyRubricComponent { + @Input() workshopId: number; + @Input() assessment: any; + @Input() edit: boolean; + @Input() selectedValues: any[]; + @Input() fieldErrors: any; +} diff --git a/src/addon/mod/workshop/assessment/rubric/lang/en.json b/src/addon/mod/workshop/assessment/rubric/lang/en.json new file mode 100644 index 000000000..8b0b20c4a --- /dev/null +++ b/src/addon/mod/workshop/assessment/rubric/lang/en.json @@ -0,0 +1,4 @@ +{ + "dimensionnumber": "Criterion {{$a}}", + "mustchooseone": "You have to select one of these items" +} \ No newline at end of file diff --git a/src/addon/mod/workshop/assessment/rubric/providers/handler.ts b/src/addon/mod/workshop/assessment/rubric/providers/handler.ts new file mode 100644 index 000000000..a0cfc563a --- /dev/null +++ b/src/addon/mod/workshop/assessment/rubric/providers/handler.ts @@ -0,0 +1,121 @@ +// (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 { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { AddonWorkshopAssessmentStrategyHandler } from '../../../providers/assessment-strategy-delegate'; +import { AddonModWorkshopAssessmentStrategyRubricComponent } from '../component/rubric'; + +/** + * Handler for rubric assessment strategy plugin. + */ +@Injectable() +export class AddonModWorkshopAssessmentStrategyRubricHandler implements AddonWorkshopAssessmentStrategyHandler { + name = 'AddonModWorkshopAssessmentStrategyRubric'; + strategyName = 'rubric'; + + constructor(private translate: TranslateService) {} + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Returns the component to render the plugin. + * + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(): any { + return AddonModWorkshopAssessmentStrategyRubricComponent; + } + + /** + * Prepare original values to be shown and compared. + * + * @param {any} form Original data of the form. + * @param {number} workshopId Workshop Id + * @return {Promise} Promise resolved with original values sorted. + */ + getOriginalValues(form: any, workshopId: number): Promise { + const originalValues = []; + + form.fields.forEach((field, n) => { + field.dimtitle = this.translate.instant('addon.mod_workshop_assessment_rubric.dimensionnumber', {$a: field.number}); + + if (!form.current[n]) { + form.current[n] = {}; + } + + originalValues[n] = { + chosenlevelid: form.current[n].chosenlevelid || '', + number: field.number + }; + }); + + return Promise.resolve(originalValues); + } + + /** + * Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin. + * + * @param {any[]} originalValues Original values of the form. + * @param {any[]} currentValues Current values of the form. + * @return {boolean} True if data has changed, false otherwise. + */ + hasDataChanged(originalValues: any[], currentValues: any[]): boolean { + for (const x in originalValues) { + if (originalValues[x].chosenlevelid != (currentValues[x].chosenlevelid || '')) { + return true; + } + } + + return false; + } + + /** + * Prepare assessment data to be sent to the server depending on the strategy selected. + * + * @param {any{}} currentValues Current values of the form. + * @param {any} form Assessment form data. + * @return {Promise} Promise resolved with the data to be sent. Or rejected with the input errors object. + */ + prepareAssessmentData(currentValues: any[], form: any): Promise { + const data = {}; + const errors = {}; + let hasErrors = false; + + form.fields.forEach((field, idx) => { + const id = parseInt(currentValues[idx].chosenlevelid, 10); + if (!isNaN(id) && id >= 0) { + data['chosenlevelid__idx_' + idx] = id; + } else { + errors['chosenlevelid_' + idx] = this.translate.instant('addon.mod_workshop_assessment_rubric.mustchooseone'); + hasErrors = true; + } + + data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0; + data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10); + }); + + if (hasErrors) { + return Promise.reject(errors); + } + + return Promise.resolve(data); + } +} diff --git a/src/addon/mod/workshop/assessment/rubric/rubric.module.ts b/src/addon/mod/workshop/assessment/rubric/rubric.module.ts new file mode 100644 index 000000000..2b5392f31 --- /dev/null +++ b/src/addon/mod/workshop/assessment/rubric/rubric.module.ts @@ -0,0 +1,51 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonModWorkshopAssessmentStrategyRubricComponent } from './component/rubric'; +import { AddonModWorkshopAssessmentStrategyRubricHandler } from './providers/handler'; +import { AddonWorkshopAssessmentStrategyDelegate } from '../../providers/assessment-strategy-delegate'; + +@NgModule({ + declarations: [ + AddonModWorkshopAssessmentStrategyRubricComponent, + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + AddonModWorkshopAssessmentStrategyRubricHandler + ], + exports: [ + AddonModWorkshopAssessmentStrategyRubricComponent + ], + entryComponents: [ + AddonModWorkshopAssessmentStrategyRubricComponent + ] +}) +export class AddonModWorkshopAssessmentStrategyRubricModule { + constructor(strategyDelegate: AddonWorkshopAssessmentStrategyDelegate, + strategyHandler: AddonModWorkshopAssessmentStrategyRubricHandler) { + strategyDelegate.registerHandler(strategyHandler); + } +} diff --git a/src/addon/mod/workshop/workshop.module.ts b/src/addon/mod/workshop/workshop.module.ts index 2c1c864e9..b2ab8c4d8 100644 --- a/src/addon/mod/workshop/workshop.module.ts +++ b/src/addon/mod/workshop/workshop.module.ts @@ -17,6 +17,7 @@ import { CoreCronDelegate } from '@providers/cron'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { AddonModWorkshopAssessmentStrategyModule } from './assessment/assessment.module'; import { AddonModWorkshopComponentsModule } from './components/components.module'; import { AddonModWorkshopModuleHandler } from './providers/module-handler'; import { AddonModWorkshopProvider } from './providers/workshop'; @@ -32,7 +33,8 @@ import { AddonModWorkshopSyncCronHandler } from './providers/sync-cron-handler'; declarations: [ ], imports: [ - AddonModWorkshopComponentsModule + AddonModWorkshopComponentsModule, + AddonModWorkshopAssessmentStrategyModule ], providers: [ AddonModWorkshopProvider, From 71a5999c603b9df2b4d87593488c8c56123cb4ec Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 11 Jun 2018 16:18:36 +0200 Subject: [PATCH 16/20] MOBILE-2354 workshop: Integrate assessment strategy and some fixes --- .../mod/workshop/components/index/index.ts | 9 +++- src/addon/mod/workshop/lang/en.json | 1 + .../workshop/pages/assessment/assessment.html | 9 ++-- .../workshop/pages/assessment/assessment.ts | 32 +++++++-------- .../pages/edit-submission/edit-submission.ts | 41 +++++++++---------- .../workshop/pages/submission/submission.html | 2 +- .../workshop/pages/submission/submission.ts | 37 +++++++++++------ src/addon/mod/workshop/providers/workshop.ts | 1 - 8 files changed, 75 insertions(+), 57 deletions(-) diff --git a/src/addon/mod/workshop/components/index/index.ts b/src/addon/mod/workshop/components/index/index.ts index ed7555163..4c626f1bd 100644 --- a/src/addon/mod/workshop/components/index/index.ts +++ b/src/addon/mod/workshop/components/index/index.ts @@ -67,6 +67,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity protected obsSubmissionChanged: any; protected obsAssessmentSaved: any; protected appResumeSubscription: any; + protected syncObserver: any; constructor(injector: Injector, private workshopProvider: AddonModWorkshopProvider, @Optional() content: Content, private workshopOffline: AddonModWorkshopOfflineProvider, private groupsProvider: CoreGroupsProvider, @@ -92,6 +93,12 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity this.loaded = false; this.refreshContent(true, false); }); + + // Refresh workshop on sync. + this.syncObserver = this.eventsProvider.on(AddonModWorkshopSyncProvider.AUTO_SYNCED, (data) => { + // Update just when all database is synced. + this.eventReceived(data); + }, this.siteId); } /** @@ -300,7 +307,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity module: this.module, access: this.access, courseId: this.courseId, - submission: this.submission + submissionId: this.submission && this.submission.id }; this.navCtrl.push('AddonModWorkshopEditSubmissionPage', params); diff --git a/src/addon/mod/workshop/lang/en.json b/src/addon/mod/workshop/lang/en.json index 57243db4a..ff7cd8d55 100644 --- a/src/addon/mod/workshop/lang/en.json +++ b/src/addon/mod/workshop/lang/en.json @@ -42,6 +42,7 @@ "submissiongrade": "Grade for submission", "submissiongradeof": "Grade for submission (of {{$a}})", "submissionrequiredcontent": "You need to enter some text or add a file.", + "submissionrequiredtitle": "You need to enter a title.", "submissionsreport": "Workshop submissions report", "submissiontitle": "Title", "switchphase10": "Switch to the setup phase", diff --git a/src/addon/mod/workshop/pages/assessment/assessment.html b/src/addon/mod/workshop/pages/assessment/assessment.html index 2a36c1cc9..704c9671a 100644 --- a/src/addon/mod/workshop/pages/assessment/assessment.html +++ b/src/addon/mod/workshop/pages/assessment/assessment.html @@ -20,14 +20,13 @@

{{profile.fullname}}

- -

+

{{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{assessment.grade}}

-

+

{{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{assessment.gradinggrade}}

-

+

{{ 'addon.mod_workshop.gradinggradeover' | translate }}: {{assessment.gradinggradeover}}

@@ -38,7 +37,7 @@ - +

diff --git a/src/addon/mod/workshop/pages/assessment/assessment.ts b/src/addon/mod/workshop/pages/assessment/assessment.ts index db38a5f5e..208734b40 100644 --- a/src/addon/mod/workshop/pages/assessment/assessment.ts +++ b/src/addon/mod/workshop/pages/assessment/assessment.ts @@ -43,18 +43,21 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy { submission: any; profile: any; courseId: number; - - access: any = {}; + access: any; assessmentId: number; evaluating = false; loaded = false; showGrade: any; evaluateForm: FormGroup; maxGrade: number; - workshop: any = {}; + workshop: any; strategy: any; title: string; - evaluate: any; + evaluate = { + text: '', + grade: -1, + weight: 1 + }; weights = []; evaluateByProfile: any; evaluationGrades: any; @@ -76,10 +79,10 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy { protected domUtils: CoreDomUtilsProvider, protected gradesHelper: CoreGradesHelperProvider, protected userProvider: CoreUserProvider) { - this.assessment = navParams.get('assessment') || {}; + this.assessment = navParams.get('assessment'); this.submission = navParams.get('submission') || {}; - this.profile = navParams.get('profile') || {}; - this.courseId = navParams.get('courseId') || null; + this.profile = navParams.get('profile'); + this.courseId = navParams.get('courseId'); this.assessmentId = this.assessment.assessmentid || this.assessment.id; this.workshopId = this.submission.workshopid || null; @@ -96,7 +99,7 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy { // Refresh workshop on sync. this.syncObserver = this.eventsProvider.on(AddonModWorkshopSyncProvider.AUTO_SYNCED, (data) => { // Update just when all database is synced. - if (this.workshopId === data.workshopid) { + if (this.workshopId === data.workshopId) { this.loaded = false; this.refreshAllData(); } @@ -166,10 +169,8 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy { let defaultGrade, promise; this.assessment = this.workshopHelper.realGradeValue(this.workshop, assessment); - this.evaluate = { - weight: this.assessment.weight, - text: this.assessment.feedbackreviewer - }; + this.evaluate.text = this.assessment.feedbackreviewer || ''; + this.evaluate.weight = this.assessment.weight; if (this.evaluating) { if (accessData.canallocate) { @@ -195,14 +196,14 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy { this.hasOffline = true; this.evaluate.weight = offlineAssess.weight; if (accessData.canoverridegrades) { - this.evaluate.text = offlineAssess.feedbacktext; + this.evaluate.text = offlineAssess.feedbacktext || ''; this.evaluate.grade = offlineAssess.gradinggradeover || -1; } }).catch(() => { this.hasOffline = false; // No offline, load online. if (accessData.canoverridegrades) { - this.evaluate.text = this.assessment.feedbackreviewer; + this.evaluate.text = this.assessment.feedbackreviewer || ''; this.evaluate.grade = this.assessment.gradinggradeover || -1; } }); @@ -259,7 +260,7 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy { return true; } - if (this.access.canoverridegrades) { + if (this.access && this.access.canoverridegrades) { if (this.originalEvaluation.text != inputData.text) { return true; } @@ -335,7 +336,6 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy { // Check if rich text editor is enabled or not. return this.domUtils.isRichTextEditorEnabled().then((rteEnabled) => { const inputData = this.evaluateForm.value; - inputData.grade = inputData.grade >= 0 ? inputData.grade : ''; if (!rteEnabled) { // Rich text editor not enabled, add some HTML to the message if needed. diff --git a/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts b/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts index fd83fa272..66fc4d69a 100644 --- a/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts +++ b/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts @@ -40,7 +40,12 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { module: any; courseId: number; access: any; - submission: any; + submission = { + id: 0, + title: '', + content: '', + attachmentfiles: [], + }; loaded = false; component = AddonModWorkshopProvider.COMPONENT; @@ -48,6 +53,7 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { editForm: FormGroup; // The form group. protected workshopId: number; + protected submissionId: number; protected userId: number; protected originalData: any = {}; protected hasOffline = false; @@ -66,7 +72,7 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { this.module = navParams.get('module'); this.courseId = navParams.get('courseId'); this.access = navParams.get('access'); - this.submission = navParams.get('submission') || {}; + this.submissionId = navParams.get('submissionId'); this.workshopId = this.module.instance; this.componentId = this.module.id; @@ -127,12 +133,11 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { return this.workshopProvider.getWorkshop(this.courseId, this.module.id).then((workshopData) => { this.workshop = workshopData; - if (this.submission && this.submission.id > 0) { + if (this.submissionId > 0) { this.editing = true; - return this.workshopHelper.getSubmissionById(this.workshopId, this.submission.id).then((submissionData) => { + return this.workshopHelper.getSubmissionById(this.workshopId, this.submissionId).then((submissionData) => { this.submission = submissionData; - this.submission.text = submissionData.content; const canEdit = (this.userId == submissionData.authorid && this.access.cansubmit && this.access.modifyingsubmissionallowed); @@ -155,19 +160,15 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { if (submissionsActions && submissionsActions.length) { this.hasOffline = true; const actions = this.workshopHelper.filterSubmissionActions(submissionsActions, this.editing ? - this.submission.id : false); + this.submission.id : 0); - return this.workshopHelper.applyOfflineData(this.submission, actions).then((offlineSubmission) => { - this.submission.title = offlineSubmission.title; - this.submission.text = offlineSubmission.content; - this.submission.attachmentfiles = offlineSubmission.attachmentfiles; - }); + return this.workshopHelper.applyOfflineData(this.submission, actions); } else { this.hasOffline = false; } }).finally(() => { this.originalData.title = this.submission.title; - this.originalData.content = this.submission.text; + this.originalData.content = this.submission.content; this.originalData.attachmentfiles = []; this.submission.attachmentfiles.forEach((file) => { @@ -295,12 +296,12 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { const inputData = this.getInputData(); if (!inputData.title) { - this.domUtils.showAlert('core.notice', 'core.requireduserdatamissing'); + this.domUtils.showAlertTranslated('core.notice', 'addon.mod_workshop.submissionrequiredtitle'); return Promise.reject(null); } if (!inputData.content) { - this.domUtils.showAlert('core.notice', 'addon.mod_workshop.submissionrequiredcontent'); + this.domUtils.showAlertTranslated('core.notice', 'addon.mod_workshop.submissionrequiredcontent'); return Promise.reject(null); } @@ -309,13 +310,11 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { saveOffline = false; const modal = this.domUtils.showModalLoading('core.sending', true), - submissionId = this.submission && (this.submission.id || this.submission.submissionid) || false; + submissionId = this.submission.id; // Check if rich text editor is enabled or not. return this.domUtils.isRichTextEditorEnabled().then((rteEnabled) => { - if (rteEnabled) { - inputData.content = this.textUtils.restorePluginfileUrls(inputData.content, this.submission.inlinefiles); - } else { + if (!rteEnabled) { // Rich text editor not enabled, add some HTML to the message if needed. inputData.content = this.textUtils.formatHtmlLines(inputData.content); } @@ -323,14 +322,14 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { // Upload attachments first if any. allowOffline = !inputData.attachmentfiles.length; - return this.workshopHelper.uploadOrStoreSubmissionFiles(this.workshopId, submissionId, inputData.attachmentfiles, + return this.workshopHelper.uploadOrStoreSubmissionFiles(this.workshopId, this.submission.id, inputData.attachmentfiles, this.editing, saveOffline).catch(() => { // Cannot upload them in online, save them in offline. saveOffline = true; allowOffline = true; - return this.workshopHelper.uploadOrStoreSubmissionFiles(this.workshopId, submissionId, inputData.attachmentfiles, - this.editing, saveOffline); + return this.workshopHelper.uploadOrStoreSubmissionFiles(this.workshopId, this.submission.id, + inputData.attachmentfiles, this.editing, saveOffline); }); }).then((attachmentsId) => { if (this.editing) { diff --git a/src/addon/mod/workshop/pages/submission/submission.html b/src/addon/mod/workshop/pages/submission/submission.html index 851a1e531..2a48dbbc9 100644 --- a/src/addon/mod/workshop/pages/submission/submission.html +++ b/src/addon/mod/workshop/pages/submission/submission.html @@ -93,7 +93,7 @@
- + diff --git a/src/addon/mod/workshop/pages/submission/submission.ts b/src/addon/mod/workshop/pages/submission/submission.ts index d203dcec4..5e8b24bc4 100644 --- a/src/addon/mod/workshop/pages/submission/submission.ts +++ b/src/addon/mod/workshop/pages/submission/submission.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, OnDestroy, Optional } from '@angular/core'; +import { Component, OnInit, OnDestroy, Optional, ViewChild } from '@angular/core'; import { Content, IonicPage, NavParams, NavController } from 'ionic-angular'; import { FormGroup, FormBuilder } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; @@ -24,6 +24,7 @@ import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; +import { AddonModWorkshopAssessmentStrategyComponent } from '../../components/assessment-strategy/assessment-strategy'; import { AddonModWorkshopProvider } from '../../providers/workshop'; import { AddonModWorkshopHelperProvider } from '../../providers/helper'; import { AddonModWorkshopOfflineProvider } from '../../providers/offline'; @@ -39,6 +40,8 @@ import { AddonModWorkshopSyncProvider } from '../../providers/sync'; }) export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { + @ViewChild(AddonModWorkshopAssessmentStrategyComponent) assessmentStrategy: AddonModWorkshopAssessmentStrategyComponent; + module: any; workshop: any; access: any; @@ -69,7 +72,11 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { protected currentUserId: number; protected userId: number; protected siteId: string; - protected originalEvaluation: any = {}; + protected originalEvaluation = { + published: '', + text: '', + grade: '' + }; protected hasOffline = false; protected component = AddonModWorkshopProvider.COMPONENT; protected forceLeave = false; @@ -89,7 +96,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { this.access = navParams.get('access'); this.courseId = navParams.get('courseId'); this.profile = navParams.get('profile'); - this.submissionInfo = navParams.get('submission'); + this.submissionInfo = navParams.get('submission') || {}; this.assessment = navParams.get('assessment') || null; this.title = this.module.name; @@ -135,14 +142,11 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { * @return {boolean|Promise} Resolved if we can leave it, rejected if not. */ ionViewCanLeave(): boolean | Promise { - if (this.forceLeave || !this.canAddFeedback) { + const assessmentHasChanged = this.assessmentStrategy && this.assessmentStrategy.hasDataChanged(); + if (this.forceLeave || (!this.hasEvaluationChanged() && !assessmentHasChanged)) { return true; } - if (!this.hasEvaluationChanged()) { - return Promise.resolve(); - } - // Show confirmation if some data has been modified. return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); } @@ -155,7 +159,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { module: module, access: this.access, courseid: this.courseId, - submission: this.submission + submissionId: this.submission.id }; this.navCtrl.push('AddonModWorkshopEditSubmissionPage', params); @@ -167,7 +171,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { * @param {any} data Event data received. */ protected eventReceived(data: any): void { - if (this.workshopId === data.workshopid) { + if (this.workshopId === data.workshopId) { this.content && this.content.scrollToTop(); this.loaded = false; @@ -185,6 +189,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { const promises = []; this.submission = submissionData; + this.submission.attachmentfiles = submissionData.attachmentfiles || []; this.submission.submissiongrade = this.submissionInfo && this.submissionInfo.submissiongrade; this.submission.gradinggrade = this.submissionInfo && this.submissionInfo.gradinggrade; this.submission.submissiongradeover = this.submissionInfo && this.submissionInfo.submissiongradeover; @@ -390,8 +395,16 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { * Save the assessment. */ saveAssessment(): void { - // Call trigger to save. - this.eventsProvider.trigger(AddonModWorkshopProvider.ASSESSMENT_SAVE, undefined, this.siteId); + if (this.assessmentStrategy && this.assessmentStrategy.hasDataChanged()) { + this.assessmentStrategy.saveAssessment().then(() => { + this.forceLeavePage(); + }).catch(() => { + // Error, stay on the page. + }); + } else { + // Nothing to save, just go back. + this.forceLeavePage(); + } } /** diff --git a/src/addon/mod/workshop/providers/workshop.ts b/src/addon/mod/workshop/providers/workshop.ts index ca1889686..cb7757834 100644 --- a/src/addon/mod/workshop/providers/workshop.ts +++ b/src/addon/mod/workshop/providers/workshop.ts @@ -36,7 +36,6 @@ export class AddonModWorkshopProvider { static EXAMPLES_BEFORE_ASSESSMENT: 2; static SUBMISSION_CHANGED = 'addon_mod_workshop_submission_changed'; - static ASSESSMENT_SAVE = 'addon_mod_workshop_assessment_save'; static ASSESSMENT_SAVED = 'addon_mod_workshop_assessment_saved'; static ASSESSMENT_INVALIDATED = 'addon_mod_workshop_assessment_invalidated'; From 973f1cdc72871bd4027edbd8ad1ae5a54edaf0c8 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Thu, 14 Jun 2018 11:12:17 +0200 Subject: [PATCH 17/20] MOBILE-2354 workshop: PR fixes --- .../assessment-strategy/assessment-strategy.html | 1 - .../assessment-strategy/assessment-strategy.ts | 2 +- src/addon/mod/workshop/components/index/index.ts | 13 ++++--------- .../mod/workshop/providers/prefetch-handler.ts | 6 +++--- src/addon/mod/workshop/providers/workshop.ts | 2 +- 5 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.html b/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.html index 68bb847bb..cf21347bf 100644 --- a/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.html +++ b/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.html @@ -6,7 +6,6 @@ -
{{ 'addon.mod_workshop.assessmentstrategynotsupported' | translate:{$a: strategy} }}
diff --git a/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts b/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts index 6ce7c2a65..05431ba3d 100644 --- a/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts +++ b/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts @@ -263,7 +263,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit { /** * Save the assessment. * - * @return {Promise} Promise resolved when done. + * @return {Promise} Promise resolved when done, rejected if assessment could not be saved. */ saveAssessment(): Promise { const files = this.fileSessionProvider.getFiles(AddonModWorkshopProvider.COMPONENT, diff --git a/src/addon/mod/workshop/components/index/index.ts b/src/addon/mod/workshop/components/index/index.ts index 4c626f1bd..ffa4ee648 100644 --- a/src/addon/mod/workshop/components/index/index.ts +++ b/src/addon/mod/workshop/components/index/index.ts @@ -88,10 +88,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity // Since most actions will take the user out of the app, we should refresh the view when the app is resumed. this.appResumeSubscription = platform.resume.subscribe(() => { - this.content && this.content.scrollToTop(); - - this.loaded = false; - this.refreshContent(true, false); + this.showLoadingAndRefresh(true); }); // Refresh workshop on sync. @@ -114,6 +111,8 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity this.workshopProvider.logView(this.workshop.id).then(() => { this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); + }).catch((error) => { + // Ignore errors. }); }); } @@ -125,10 +124,8 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity */ protected eventReceived(data: any): void { if ((this.workshop && this.workshop.id === data.workshopId) || data.cmId === this.module.id) { - this.content && this.content.scrollToTop(); + this.showLoadingAndRefresh(true); - this.loaded = false; - this.refreshContent(true, false); // Check completion since it could be configured to complete once the user adds a new discussion or replies. this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); } @@ -223,8 +220,6 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity } }); } - - return Promise.resolve(); }).then(() => { return this.workshopProvider.getUserPlanPhases(this.workshop.id); }).then((phases) => { diff --git a/src/addon/mod/workshop/providers/prefetch-handler.ts b/src/addon/mod/workshop/providers/prefetch-handler.ts index 1c641fb57..0dc4f488e 100644 --- a/src/addon/mod/workshop/providers/prefetch-handler.ts +++ b/src/addon/mod/workshop/providers/prefetch-handler.ts @@ -154,14 +154,14 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseModulePrefetchHan groups: groups, files: files.filter((file) => typeof file !== 'undefined') }; - }).catch((message) => { + }).catch((message): any => { if (omitFail) { // Any error, return the info we have. - return Promise.resolve({ + return { workshop: workshop, groups: groups, files: files.filter((file) => typeof file !== 'undefined') - }); + }; } return Promise.reject(message); diff --git a/src/addon/mod/workshop/providers/workshop.ts b/src/addon/mod/workshop/providers/workshop.ts index cb7757834..5a1a1c5c3 100644 --- a/src/addon/mod/workshop/providers/workshop.ts +++ b/src/addon/mod/workshop/providers/workshop.ts @@ -1060,7 +1060,7 @@ export class AddonModWorkshopProvider { * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the data is invalidated. */ - invalidateAssessmentFormData(workshopId: number, assessmentId: number, mode: string = 'assesssment', siteId?: string): + invalidateAssessmentFormData(workshopId: number, assessmentId: number, mode: string = 'assessment', siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.invalidateWsCacheForKey(this.getAssessmentFormDataCacheKey(workshopId, assessmentId, mode)); From 7a86e1beebf6025777a9b5cdcae0cf07d90d7579 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 13 Jun 2018 12:26:37 +0200 Subject: [PATCH 18/20] MOBILE-2354 workshop: Migrate tables from the old app --- src/addon/mod/workshop/providers/offline.ts | 62 +++++++++++---------- src/addon/mod/workshop/workshop.module.ts | 53 +++++++++++++++++- 2 files changed, 84 insertions(+), 31 deletions(-) diff --git a/src/addon/mod/workshop/providers/offline.ts b/src/addon/mod/workshop/providers/offline.ts index c74a1b48e..63ed454a8 100644 --- a/src/addon/mod/workshop/providers/offline.ts +++ b/src/addon/mod/workshop/providers/offline.ts @@ -25,14 +25,14 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time'; export class AddonModWorkshopOfflineProvider { // Variables for database. - protected SUBMISSIONS_TABLE = 'addon_mod_workshop_submissions'; - protected ASSESSMENTS_TABLE = 'addon_mod_workshop_assessments'; - protected EVALUATE_SUBMISSIONS_TABLE = 'addon_mod_workshop_evaluate_submissions'; - protected EVALUATE_ASSESSMENTS_TABLE = 'addon_mod_workshop_evaluate_assessments'; + static SUBMISSIONS_TABLE = 'addon_mod_workshop_submissions'; + static ASSESSMENTS_TABLE = 'addon_mod_workshop_assessments'; + static EVALUATE_SUBMISSIONS_TABLE = 'addon_mod_workshop_evaluate_submissions'; + static EVALUATE_ASSESSMENTS_TABLE = 'addon_mod_workshop_evaluate_assessments'; protected tablesSchema = [ { - name: this.SUBMISSIONS_TABLE, + name: AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE, columns: [ { name: 'workshopid', @@ -70,7 +70,7 @@ export class AddonModWorkshopOfflineProvider { primaryKeys: ['workshopid', 'submissionid', 'action'] }, { - name: this.ASSESSMENTS_TABLE, + name: AddonModWorkshopOfflineProvider.ASSESSMENTS_TABLE, columns: [ { name: 'workshopid', @@ -96,7 +96,7 @@ export class AddonModWorkshopOfflineProvider { primaryKeys: ['workshopid', 'assessmentid'] }, { - name: this.EVALUATE_SUBMISSIONS_TABLE, + name: AddonModWorkshopOfflineProvider.EVALUATE_SUBMISSIONS_TABLE, columns: [ { name: 'workshopid', @@ -130,7 +130,7 @@ export class AddonModWorkshopOfflineProvider { primaryKeys: ['workshopid', 'submissionid'] }, { - name: this.EVALUATE_ASSESSMENTS_TABLE, + name: AddonModWorkshopOfflineProvider.EVALUATE_ASSESSMENTS_TABLE, columns: [ { name: 'workshopid', @@ -240,7 +240,7 @@ export class AddonModWorkshopOfflineProvider { action: action }; - return site.getDb().deleteRecords(this.SUBMISSIONS_TABLE, conditions); + return site.getDb().deleteRecords(AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE, conditions); }); } @@ -259,7 +259,7 @@ export class AddonModWorkshopOfflineProvider { submissionid: submissionId, }; - return site.getDb().deleteRecords(this.SUBMISSIONS_TABLE, conditions); + return site.getDb().deleteRecords(AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE, conditions); }); } @@ -271,7 +271,7 @@ export class AddonModWorkshopOfflineProvider { */ getAllSubmissions(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getRecords(this.SUBMISSIONS_TABLE).then((records) => { + return site.getDb().getRecords(AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE).then((records) => { records.forEach(this.parseSubmissionRecord.bind(this)); return records; @@ -292,7 +292,7 @@ export class AddonModWorkshopOfflineProvider { workshopid: workshopId }; - return site.getDb().getRecords(this.SUBMISSIONS_TABLE, conditions).then((records) => { + return site.getDb().getRecords(AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE, conditions).then((records) => { records.forEach(this.parseSubmissionRecord.bind(this)); return records; @@ -315,7 +315,7 @@ export class AddonModWorkshopOfflineProvider { submissionid: submissionId }; - return site.getDb().getRecords(this.SUBMISSIONS_TABLE, conditions).then((records) => { + return site.getDb().getRecords(AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE, conditions).then((records) => { records.forEach(this.parseSubmissionRecord.bind(this)); return records; @@ -340,7 +340,7 @@ export class AddonModWorkshopOfflineProvider { action: action }; - return site.getDb().getRecord(this.SUBMISSIONS_TABLE, conditions).then((record) => { + return site.getDb().getRecord(AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE, conditions).then((record) => { this.parseSubmissionRecord(record); return record; @@ -377,7 +377,7 @@ export class AddonModWorkshopOfflineProvider { timemodified: timemodified }; - return site.getDb().insertRecord(this.SUBMISSIONS_TABLE, assessment); + return site.getDb().insertRecord(AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE, assessment); }); } @@ -405,7 +405,7 @@ export class AddonModWorkshopOfflineProvider { assessmentid: assessmentId }; - return site.getDb().deleteRecords(this.ASSESSMENTS_TABLE, conditions); + return site.getDb().deleteRecords(AddonModWorkshopOfflineProvider.ASSESSMENTS_TABLE, conditions); }); } @@ -417,7 +417,7 @@ export class AddonModWorkshopOfflineProvider { */ getAllAssessments(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getRecords(this.ASSESSMENTS_TABLE).then((records) => { + return site.getDb().getRecords(AddonModWorkshopOfflineProvider.ASSESSMENTS_TABLE).then((records) => { records.forEach(this.parseAssessmentRecord.bind(this)); return records; @@ -438,7 +438,7 @@ export class AddonModWorkshopOfflineProvider { workshopid: workshopId }; - return site.getDb().getRecords(this.ASSESSMENTS_TABLE, conditions).then((records) => { + return site.getDb().getRecords(AddonModWorkshopOfflineProvider.ASSESSMENTS_TABLE, conditions).then((records) => { records.forEach(this.parseAssessmentRecord.bind(this)); return records; @@ -461,7 +461,7 @@ export class AddonModWorkshopOfflineProvider { assessmentid: assessmentId }; - return site.getDb().getRecord(this.ASSESSMENTS_TABLE, conditions).then((record) => { + return site.getDb().getRecord(AddonModWorkshopOfflineProvider.ASSESSMENTS_TABLE, conditions).then((record) => { this.parseAssessmentRecord(record); return record; @@ -489,7 +489,7 @@ export class AddonModWorkshopOfflineProvider { timemodified: this.timeUtils.timestamp() }; - return site.getDb().insertRecord(this.ASSESSMENTS_TABLE, assessment); + return site.getDb().insertRecord(AddonModWorkshopOfflineProvider.ASSESSMENTS_TABLE, assessment); }); } @@ -517,7 +517,7 @@ export class AddonModWorkshopOfflineProvider { }; return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().deleteRecords(this.EVALUATE_SUBMISSIONS_TABLE, conditions); + return site.getDb().deleteRecords(AddonModWorkshopOfflineProvider.EVALUATE_SUBMISSIONS_TABLE, conditions); }); } @@ -529,7 +529,7 @@ export class AddonModWorkshopOfflineProvider { */ getAllEvaluateSubmissions(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getRecords(this.EVALUATE_SUBMISSIONS_TABLE).then((records) => { + return site.getDb().getRecords(AddonModWorkshopOfflineProvider.EVALUATE_SUBMISSIONS_TABLE).then((records) => { records.forEach(this.parseEvaluateSubmissionRecord.bind(this)); return records; @@ -550,7 +550,8 @@ export class AddonModWorkshopOfflineProvider { workshopid: workshopId }; - return site.getDb().getRecords(this.EVALUATE_SUBMISSIONS_TABLE, conditions).then((records) => { + return site.getDb().getRecords(AddonModWorkshopOfflineProvider.EVALUATE_SUBMISSIONS_TABLE, conditions) + .then((records) => { records.forEach(this.parseEvaluateSubmissionRecord.bind(this)); return records; @@ -573,7 +574,7 @@ export class AddonModWorkshopOfflineProvider { submissionid: submissionId }; - return site.getDb().getRecord(this.EVALUATE_SUBMISSIONS_TABLE, conditions).then((record) => { + return site.getDb().getRecord(AddonModWorkshopOfflineProvider.EVALUATE_SUBMISSIONS_TABLE, conditions).then((record) => { this.parseEvaluateSubmissionRecord(record); return record; @@ -606,7 +607,7 @@ export class AddonModWorkshopOfflineProvider { gradeover: JSON.stringify(gradeOver) }; - return site.getDb().insertRecord(this.EVALUATE_SUBMISSIONS_TABLE, submission); + return site.getDb().insertRecord(AddonModWorkshopOfflineProvider.EVALUATE_SUBMISSIONS_TABLE, submission); }); } @@ -635,7 +636,7 @@ export class AddonModWorkshopOfflineProvider { assessmentid: assessmentId }; - return site.getDb().deleteRecords(this.EVALUATE_ASSESSMENTS_TABLE, conditions); + return site.getDb().deleteRecords(AddonModWorkshopOfflineProvider.EVALUATE_ASSESSMENTS_TABLE, conditions); }); } @@ -647,7 +648,7 @@ export class AddonModWorkshopOfflineProvider { */ getAllEvaluateAssessments(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getRecords(this.EVALUATE_ASSESSMENTS_TABLE).then((records) => { + return site.getDb().getRecords(AddonModWorkshopOfflineProvider.EVALUATE_ASSESSMENTS_TABLE).then((records) => { records.forEach(this.parseEvaluateAssessmentRecord.bind(this)); return records; @@ -668,7 +669,8 @@ export class AddonModWorkshopOfflineProvider { workshopid: workshopId }; - return site.getDb().getRecords(this.EVALUATE_ASSESSMENTS_TABLE, conditions).then((records) => { + return site.getDb().getRecords(AddonModWorkshopOfflineProvider.EVALUATE_ASSESSMENTS_TABLE, conditions) + .then((records) => { records.forEach(this.parseEvaluateAssessmentRecord.bind(this)); return records; @@ -691,7 +693,7 @@ export class AddonModWorkshopOfflineProvider { assessmentid: assessmentId }; - return site.getDb().getRecord(this.EVALUATE_ASSESSMENTS_TABLE, conditions).then((record) => { + return site.getDb().getRecord(AddonModWorkshopOfflineProvider.EVALUATE_ASSESSMENTS_TABLE, conditions).then((record) => { this.parseEvaluateAssessmentRecord(record); return record; @@ -724,7 +726,7 @@ export class AddonModWorkshopOfflineProvider { gradinggradeover: JSON.stringify(gradingGradeOver) }; - return site.getDb().insertRecord(this.EVALUATE_ASSESSMENTS_TABLE, assessment); + return site.getDb().insertRecord(AddonModWorkshopOfflineProvider.EVALUATE_ASSESSMENTS_TABLE, assessment); }); } diff --git a/src/addon/mod/workshop/workshop.module.ts b/src/addon/mod/workshop/workshop.module.ts index b2ab8c4d8..c8883148b 100644 --- a/src/addon/mod/workshop/workshop.module.ts +++ b/src/addon/mod/workshop/workshop.module.ts @@ -28,6 +28,7 @@ import { AddonModWorkshopHelperProvider } from './providers/helper'; import { AddonWorkshopAssessmentStrategyDelegate } from './providers/assessment-strategy-delegate'; import { AddonModWorkshopPrefetchHandler } from './providers/prefetch-handler'; import { AddonModWorkshopSyncCronHandler } from './providers/sync-cron-handler'; +import { CoreUpdateManagerProvider } from '@providers/update-manager'; @NgModule({ declarations: [ @@ -52,10 +53,60 @@ export class AddonModWorkshopModule { constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModWorkshopModuleHandler, contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModWorkshopLinkHandler, prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModWorkshopPrefetchHandler, - cronDelegate: CoreCronDelegate, syncHandler: AddonModWorkshopSyncCronHandler) { + cronDelegate: CoreCronDelegate, syncHandler: AddonModWorkshopSyncCronHandler, + updateManager: CoreUpdateManagerProvider) { + moduleDelegate.registerHandler(moduleHandler); contentLinksDelegate.registerHandler(linkHandler); prefetchDelegate.registerHandler(prefetchHandler); cronDelegate.register(syncHandler); + + // Allow migrating the tables from the old app to the new schema. + updateManager.registerSiteTablesMigration([ + { + name: 'mma_mod_workshop_offline_submissions', + newName: AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE, + fields: [ + { + name: 'attachmentsid', + type: 'object' + } + ] + }, + { + name: 'mma_mod_workshop_offline_assessments', + newName: AddonModWorkshopOfflineProvider.ASSESSMENTS_TABLE, + fields: [ + { + name: 'inputdata', + type: 'object' + } + ] + }, + { + name: 'mma_mod_workshop_offline_evaluate_submissions', + newName: AddonModWorkshopOfflineProvider.EVALUATE_SUBMISSIONS_TABLE, + fields: [ + { + name: 'gradeover', + type: 'object' + }, + { + name: 'published', + type: 'boolean' + } + ] + }, + { + name: 'mma_mod_workshop_offline_evaluate_assessments', + newName: AddonModWorkshopOfflineProvider.EVALUATE_ASSESSMENTS_TABLE, + fields: [ + { + name: 'gradinggradeover', + type: 'object' + } + ] + } + ]); } } From 27c57b01b0db420cdd0fd0f1d67fbe515cd8f7ef Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 13 Jun 2018 12:35:54 +0200 Subject: [PATCH 19/20] MOBILE-2354 workshop: Inject providers and components to compile --- gulpfile.js | 3 ++- ....html => addon-mod-workshop-assessment-strategy.html} | 0 .../assessment-strategy/assessment-strategy.ts | 2 +- ...ssessment.html => addon-mod-workshop-assessment.html} | 0 .../mod/workshop/components/assessment/assessment.ts | 2 +- .../index/{index.html => addon-mod-workshop-index.html} | 0 src/addon/mod/workshop/components/index/index.ts | 2 +- ...ubmission.html => addon-mod-workshop-submission.html} | 0 .../mod/workshop/components/submission/submission.ts | 2 +- src/addon/mod/workshop/workshop.module.ts | 9 +++++++++ src/core/compile/providers/compile.ts | 5 ++++- 11 files changed, 19 insertions(+), 6 deletions(-) rename src/addon/mod/workshop/components/assessment-strategy/{assessment-strategy.html => addon-mod-workshop-assessment-strategy.html} (100%) rename src/addon/mod/workshop/components/assessment/{assessment.html => addon-mod-workshop-assessment.html} (100%) rename src/addon/mod/workshop/components/index/{index.html => addon-mod-workshop-index.html} (100%) rename src/addon/mod/workshop/components/submission/{submission.html => addon-mod-workshop-submission.html} (100%) diff --git a/gulpfile.js b/gulpfile.js index 7994ecc06..1fc070779 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -293,7 +293,8 @@ var templatesSrc = [ './src/core/**/components/**/*.html', './src/core/**/component/**/*.html', // Only some addon components are injected to compile to decrease load time. Copy only the ones that are needed. - './src/addon/mod/assign/components/**/*.html' + './src/addon/mod/assign/components/**/*.html', + './src/addon/mod/workshop/components/**/*.html' ], templatesDest = './www/templates'; diff --git a/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.html b/src/addon/mod/workshop/components/assessment-strategy/addon-mod-workshop-assessment-strategy.html similarity index 100% rename from src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.html rename to src/addon/mod/workshop/components/assessment-strategy/addon-mod-workshop-assessment-strategy.html diff --git a/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts b/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts index 05431ba3d..b4c0fa51a 100644 --- a/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts +++ b/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts @@ -33,7 +33,7 @@ import { AddonWorkshopAssessmentStrategyDelegate } from '../../providers/assessm */ @Component({ selector: 'addon-mod-workshop-assessment-strategy', - templateUrl: 'assessment-strategy.html', + templateUrl: 'addon-mod-workshop-assessment-strategy.html', }) export class AddonModWorkshopAssessmentStrategyComponent implements OnInit { diff --git a/src/addon/mod/workshop/components/assessment/assessment.html b/src/addon/mod/workshop/components/assessment/addon-mod-workshop-assessment.html similarity index 100% rename from src/addon/mod/workshop/components/assessment/assessment.html rename to src/addon/mod/workshop/components/assessment/addon-mod-workshop-assessment.html diff --git a/src/addon/mod/workshop/components/assessment/assessment.ts b/src/addon/mod/workshop/components/assessment/assessment.ts index 7e16f1491..6df7556f9 100644 --- a/src/addon/mod/workshop/components/assessment/assessment.ts +++ b/src/addon/mod/workshop/components/assessment/assessment.ts @@ -25,7 +25,7 @@ import { AddonModWorkshopOfflineProvider } from '../../providers/offline'; */ @Component({ selector: 'addon-mod-workshop-assessment', - templateUrl: 'assessment.html', + templateUrl: 'addon-mod-workshop-assessment.html', }) export class AddonModWorkshopAssessmentComponent implements OnInit { @Input() assessment: any; diff --git a/src/addon/mod/workshop/components/index/index.html b/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html similarity index 100% rename from src/addon/mod/workshop/components/index/index.html rename to src/addon/mod/workshop/components/index/addon-mod-workshop-index.html diff --git a/src/addon/mod/workshop/components/index/index.ts b/src/addon/mod/workshop/components/index/index.ts index ffa4ee648..5a1004709 100644 --- a/src/addon/mod/workshop/components/index/index.ts +++ b/src/addon/mod/workshop/components/index/index.ts @@ -27,7 +27,7 @@ import { AddonModWorkshopOfflineProvider } from '../../providers/offline'; */ @Component({ selector: 'addon-mod-workshop-index', - templateUrl: 'index.html', + templateUrl: 'addon-mod-workshop-index.html', }) export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivityComponent { @Input() group = 0; diff --git a/src/addon/mod/workshop/components/submission/submission.html b/src/addon/mod/workshop/components/submission/addon-mod-workshop-submission.html similarity index 100% rename from src/addon/mod/workshop/components/submission/submission.html rename to src/addon/mod/workshop/components/submission/addon-mod-workshop-submission.html diff --git a/src/addon/mod/workshop/components/submission/submission.ts b/src/addon/mod/workshop/components/submission/submission.ts index b704faac7..235a8d179 100644 --- a/src/addon/mod/workshop/components/submission/submission.ts +++ b/src/addon/mod/workshop/components/submission/submission.ts @@ -25,7 +25,7 @@ import { AddonModWorkshopOfflineProvider } from '../../providers/offline'; */ @Component({ selector: 'addon-mod-workshop-submission', - templateUrl: 'submission.html', + templateUrl: 'addon-mod-workshop-submission.html', }) export class AddonModWorkshopSubmissionComponent implements OnInit { @Input() submission: any; diff --git a/src/addon/mod/workshop/workshop.module.ts b/src/addon/mod/workshop/workshop.module.ts index c8883148b..cff6c9151 100644 --- a/src/addon/mod/workshop/workshop.module.ts +++ b/src/addon/mod/workshop/workshop.module.ts @@ -30,6 +30,15 @@ import { AddonModWorkshopPrefetchHandler } from './providers/prefetch-handler'; import { AddonModWorkshopSyncCronHandler } from './providers/sync-cron-handler'; import { CoreUpdateManagerProvider } from '@providers/update-manager'; +// List of providers (without handlers). +export const ADDON_MOD_WORKSHOP_PROVIDERS: any[] = [ + AddonModWorkshopProvider, + AddonModWorkshopOfflineProvider, + AddonModWorkshopSyncProvider, + AddonModWorkshopHelperProvider, + AddonWorkshopAssessmentStrategyDelegate +]; + @NgModule({ declarations: [ ], diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts index ec515a010..0d5eab7b8 100644 --- a/src/core/compile/providers/compile.ts +++ b/src/core/compile/providers/compile.ts @@ -108,6 +108,7 @@ import { ADDON_MOD_SCORM_PROVIDERS } from '@addon/mod/scorm/scorm.module'; import { ADDON_MOD_SURVEY_PROVIDERS } from '@addon/mod/survey/survey.module'; import { ADDON_MOD_URL_PROVIDERS } from '@addon/mod/url/url.module'; import { ADDON_MOD_WIKI_PROVIDERS } from '@addon/mod/wiki/wiki.module'; +import { ADDON_MOD_WORKSHOP_PROVIDERS } from '@addon/mod/workshop/workshop.module'; import { ADDON_NOTES_PROVIDERS } from '@addon/notes/notes.module'; import { ADDON_NOTIFICATIONS_PROVIDERS } from '@addon/notifications/notifications.module'; import { ADDON_PUSHNOTIFICATIONS_PROVIDERS } from '@addon/pushnotifications/pushnotifications.module'; @@ -115,6 +116,7 @@ import { ADDON_REMOTETHEMES_PROVIDERS } from '@addon/remotethemes/remotethemes.m // Import some addon modules that define components, directives and pipes. Only import the important ones. import { AddonModAssignComponentsModule } from '@addon/mod/assign/components/components.module'; +import { AddonModWorkshopComponentsModule } from '@addon/mod/workshop/components/components.module'; /** * Service to provide functionalities regarding compiling dynamic HTML and Javascript. @@ -136,6 +138,7 @@ export class CoreCompileProvider { IonicModule, TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, CorePipesModule, CoreCourseComponentsModule, CoreCoursesComponentsModule, CoreSiteHomeComponentsModule, CoreUserComponentsModule, CoreCourseDirectivesModule, CoreSitePluginsDirectivesModule, CoreQuestionComponentsModule, AddonModAssignComponentsModule, + AddonModWorkshopComponentsModule ]; constructor(protected injector: Injector, logger: CoreLoggerProvider, compilerFactory: JitCompilerFactory) { @@ -222,7 +225,7 @@ export class CoreCompileProvider { .concat(ADDON_MOD_LESSON_PROVIDERS).concat(ADDON_MOD_LTI_PROVIDERS).concat(ADDON_MOD_PAGE_PROVIDERS) .concat(ADDON_MOD_QUIZ_PROVIDERS).concat(ADDON_MOD_RESOURCE_PROVIDERS).concat(ADDON_MOD_SCORM_PROVIDERS) .concat(ADDON_MOD_SURVEY_PROVIDERS).concat(ADDON_MOD_URL_PROVIDERS).concat(ADDON_MOD_WIKI_PROVIDERS) - .concat(ADDON_NOTES_PROVIDERS).concat(ADDON_NOTIFICATIONS_PROVIDERS) + .concat(ADDON_MOD_WORKSHOP_PROVIDERS).concat(ADDON_NOTES_PROVIDERS).concat(ADDON_NOTIFICATIONS_PROVIDERS) .concat(ADDON_PUSHNOTIFICATIONS_PROVIDERS).concat(ADDON_REMOTETHEMES_PROVIDERS); // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance. From 29df582cee9629c714c74598c44d2b7f31552569 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 13 Jun 2018 14:46:59 +0200 Subject: [PATCH 20/20] MOBILE-2354 workshop: Support assessment plugins in site plugins --- .../accumulative/component/accumulative.ts | 10 +-- .../accumulative/providers/handler.ts | 8 +- .../assessment/comments/component/comments.ts | 10 +-- .../assessment/comments/providers/handler.ts | 8 +- .../numerrors/component/numerrors.ts | 10 +-- .../assessment/numerrors/providers/handler.ts | 8 +- .../assessment/rubric/component/rubric.ts | 10 +-- .../assessment/rubric/providers/handler.ts | 8 +- .../classes/assessment-strategy-component.ts | 31 +++++++ ...ddon-mod-workshop-assessment-strategy.html | 4 +- .../assessment-strategy.ts | 7 +- .../providers/assessment-strategy-delegate.ts | 27 ++++--- .../workshop-assessment-strategy-handler.ts | 81 +++++++++++++++++++ ...eplugins-workshop-assessment-strategy.html | 1 + .../workshop-assessment-strategy.ts | 54 +++++++++++++ src/core/siteplugins/providers/helper.ts | 29 ++++++- 16 files changed, 248 insertions(+), 58 deletions(-) create mode 100644 src/addon/mod/workshop/classes/assessment-strategy-component.ts create mode 100644 src/core/siteplugins/classes/handlers/workshop-assessment-strategy-handler.ts create mode 100644 src/core/siteplugins/components/workshop-assessment-strategy/core-siteplugins-workshop-assessment-strategy.html create mode 100644 src/core/siteplugins/components/workshop-assessment-strategy/workshop-assessment-strategy.ts diff --git a/src/addon/mod/workshop/assessment/accumulative/component/accumulative.ts b/src/addon/mod/workshop/assessment/accumulative/component/accumulative.ts index 962c20068..98385f1f0 100644 --- a/src/addon/mod/workshop/assessment/accumulative/component/accumulative.ts +++ b/src/addon/mod/workshop/assessment/accumulative/component/accumulative.ts @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input } from '@angular/core'; +import { Component } from '@angular/core'; +import { AddonModWorkshopAssessmentStrategyComponentBase } from '../../../classes/assessment-strategy-component'; /** * Component for accumulative assessment strategy. @@ -21,10 +22,5 @@ import { Component, Input } from '@angular/core'; selector: 'addon-mod-workshop-assessment-strategy-accumulative', templateUrl: 'accumulative.html', }) -export class AddonModWorkshopAssessmentStrategyAccumulativeComponent { - @Input() workshopId: number; - @Input() assessment: any; - @Input() edit: boolean; - @Input() selectedValues: any[]; - @Input() fieldErrors: any; +export class AddonModWorkshopAssessmentStrategyAccumulativeComponent extends AddonModWorkshopAssessmentStrategyComponentBase { } diff --git a/src/addon/mod/workshop/assessment/accumulative/providers/handler.ts b/src/addon/mod/workshop/assessment/accumulative/providers/handler.ts index e0d4496f2..3a845746b 100644 --- a/src/addon/mod/workshop/assessment/accumulative/providers/handler.ts +++ b/src/addon/mod/workshop/assessment/accumulative/providers/handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; import { AddonWorkshopAssessmentStrategyHandler } from '../../../providers/assessment-strategy-delegate'; @@ -37,11 +37,13 @@ export class AddonModWorkshopAssessmentStrategyAccumulativeHandler implements Ad } /** - * Returns the component to render the plugin. + * Return the Component to render the plugin. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(): any { + getComponent(injector: Injector): any | Promise { return AddonModWorkshopAssessmentStrategyAccumulativeComponent; } diff --git a/src/addon/mod/workshop/assessment/comments/component/comments.ts b/src/addon/mod/workshop/assessment/comments/component/comments.ts index ef464cd8e..2f963dc84 100644 --- a/src/addon/mod/workshop/assessment/comments/component/comments.ts +++ b/src/addon/mod/workshop/assessment/comments/component/comments.ts @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input } from '@angular/core'; +import { Component } from '@angular/core'; +import { AddonModWorkshopAssessmentStrategyComponentBase } from '../../../classes/assessment-strategy-component'; /** * Component for comments assessment strategy. @@ -21,10 +22,5 @@ import { Component, Input } from '@angular/core'; selector: 'addon-mod-workshop-assessment-strategy-comments', templateUrl: 'comments.html', }) -export class AddonModWorkshopAssessmentStrategyCommentsComponent { - @Input() workshopId: number; - @Input() assessment: any; - @Input() edit: boolean; - @Input() selectedValues: any[]; - @Input() fieldErrors: any; +export class AddonModWorkshopAssessmentStrategyCommentsComponent extends AddonModWorkshopAssessmentStrategyComponentBase { } diff --git a/src/addon/mod/workshop/assessment/comments/providers/handler.ts b/src/addon/mod/workshop/assessment/comments/providers/handler.ts index ad0c525d4..339c16a88 100644 --- a/src/addon/mod/workshop/assessment/comments/providers/handler.ts +++ b/src/addon/mod/workshop/assessment/comments/providers/handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { AddonWorkshopAssessmentStrategyHandler } from '../../../providers/assessment-strategy-delegate'; import { AddonModWorkshopAssessmentStrategyCommentsComponent } from '../component/comments'; @@ -36,11 +36,13 @@ export class AddonModWorkshopAssessmentStrategyCommentsHandler implements AddonW } /** - * Returns the component to render the plugin. + * Return the Component to render the plugin. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(): any { + getComponent(injector: Injector): any | Promise { return AddonModWorkshopAssessmentStrategyCommentsComponent; } diff --git a/src/addon/mod/workshop/assessment/numerrors/component/numerrors.ts b/src/addon/mod/workshop/assessment/numerrors/component/numerrors.ts index 37cc36e72..ead73dcaf 100644 --- a/src/addon/mod/workshop/assessment/numerrors/component/numerrors.ts +++ b/src/addon/mod/workshop/assessment/numerrors/component/numerrors.ts @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input } from '@angular/core'; +import { Component } from '@angular/core'; +import { AddonModWorkshopAssessmentStrategyComponentBase } from '../../../classes/assessment-strategy-component'; /** * Component for numerrors assessment strategy. @@ -21,10 +22,5 @@ import { Component, Input } from '@angular/core'; selector: 'addon-mod-workshop-assessment-strategy-numerrors', templateUrl: 'numerrors.html', }) -export class AddonModWorkshopAssessmentStrategyNumErrorsComponent { - @Input() workshopId: number; - @Input() assessment: any; - @Input() edit: boolean; - @Input() selectedValues: any[]; - @Input() fieldErrors: any; +export class AddonModWorkshopAssessmentStrategyNumErrorsComponent extends AddonModWorkshopAssessmentStrategyComponentBase { } diff --git a/src/addon/mod/workshop/assessment/numerrors/providers/handler.ts b/src/addon/mod/workshop/assessment/numerrors/providers/handler.ts index 3f3630c65..4eaaf2012 100644 --- a/src/addon/mod/workshop/assessment/numerrors/providers/handler.ts +++ b/src/addon/mod/workshop/assessment/numerrors/providers/handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { AddonWorkshopAssessmentStrategyHandler } from '../../../providers/assessment-strategy-delegate'; import { AddonModWorkshopAssessmentStrategyNumErrorsComponent } from '../component/numerrors'; @@ -36,11 +36,13 @@ export class AddonModWorkshopAssessmentStrategyNumErrorsHandler implements Addon } /** - * Returns the component to render the plugin. + * Return the Component to render the plugin. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(): any { + getComponent(injector: Injector): any | Promise { return AddonModWorkshopAssessmentStrategyNumErrorsComponent; } diff --git a/src/addon/mod/workshop/assessment/rubric/component/rubric.ts b/src/addon/mod/workshop/assessment/rubric/component/rubric.ts index 4c946500e..d28b3f578 100644 --- a/src/addon/mod/workshop/assessment/rubric/component/rubric.ts +++ b/src/addon/mod/workshop/assessment/rubric/component/rubric.ts @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input } from '@angular/core'; +import { Component } from '@angular/core'; +import { AddonModWorkshopAssessmentStrategyComponentBase } from '../../../classes/assessment-strategy-component'; /** * Component for rubric assessment strategy. @@ -21,10 +22,5 @@ import { Component, Input } from '@angular/core'; selector: 'addon-mod-workshop-assessment-strategy-rubric', templateUrl: 'rubric.html', }) -export class AddonModWorkshopAssessmentStrategyRubricComponent { - @Input() workshopId: number; - @Input() assessment: any; - @Input() edit: boolean; - @Input() selectedValues: any[]; - @Input() fieldErrors: any; +export class AddonModWorkshopAssessmentStrategyRubricComponent extends AddonModWorkshopAssessmentStrategyComponentBase { } diff --git a/src/addon/mod/workshop/assessment/rubric/providers/handler.ts b/src/addon/mod/workshop/assessment/rubric/providers/handler.ts index a0cfc563a..bc5dbdb1f 100644 --- a/src/addon/mod/workshop/assessment/rubric/providers/handler.ts +++ b/src/addon/mod/workshop/assessment/rubric/providers/handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { AddonWorkshopAssessmentStrategyHandler } from '../../../providers/assessment-strategy-delegate'; import { AddonModWorkshopAssessmentStrategyRubricComponent } from '../component/rubric'; @@ -36,11 +36,13 @@ export class AddonModWorkshopAssessmentStrategyRubricHandler implements AddonWor } /** - * Returns the component to render the plugin. + * Return the Component to render the plugin. + * It's recommended to return the class of the component, but you can also return an instance of the component. * + * @param {Injector} injector Injector. * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ - getComponent(): any { + getComponent(injector: Injector): any | Promise { return AddonModWorkshopAssessmentStrategyRubricComponent; } diff --git a/src/addon/mod/workshop/classes/assessment-strategy-component.ts b/src/addon/mod/workshop/classes/assessment-strategy-component.ts new file mode 100644 index 000000000..6c49dbb99 --- /dev/null +++ b/src/addon/mod/workshop/classes/assessment-strategy-component.ts @@ -0,0 +1,31 @@ +// (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 { Input } from '@angular/core'; + +/** + * Base class for component to render an assessment strategy. + */ +export class AddonModWorkshopAssessmentStrategyComponentBase { + @Input() workshopId: number; + @Input() assessment: any; + @Input() edit: boolean; + @Input() selectedValues: any[]; + @Input() fieldErrors: any; + @Input() strategy: string; + + constructor() { + // Nothing to do. + } +} diff --git a/src/addon/mod/workshop/components/assessment-strategy/addon-mod-workshop-assessment-strategy.html b/src/addon/mod/workshop/components/assessment-strategy/addon-mod-workshop-assessment-strategy.html index cf21347bf..26a10d6c8 100644 --- a/src/addon/mod/workshop/components/assessment-strategy/addon-mod-workshop-assessment-strategy.html +++ b/src/addon/mod/workshop/components/assessment-strategy/addon-mod-workshop-assessment-strategy.html @@ -16,9 +16,7 @@
{{ 'addon.mod_workshop.feedbackauthor' | translate }} - - + } The component (or promise resolved with component) to use, undefined if not found. */ - getComponent?(): any; + getComponent?(injector: Injector): any | Promise; /** * Prepare original values to be shown and compared. @@ -87,14 +89,15 @@ export interface AddonWorkshopAssessmentStrategyHandler extends CoreDelegateHand return this.hasHandler(workshopStrategy, true); } - /** - * Get the directive to use for a certain assessment strategy plugin. - * - * @param {string} workshopStrategy Assessment strategy name. - * @return {any} The component, undefined if not found. - */ - getComponentForPlugin(workshopStrategy: string): Promise { - return this.executeFunctionOnEnabled(workshopStrategy, 'getComponent', []); + /** + * Get the directive to use for a certain assessment strategy plugin. + * + * @param {Injector} injector Injector. + * @param {string} workshopStrategy Assessment strategy name. + * @return {any} The component, undefined if not found. + */ + getComponentForPlugin(injector: Injector, workshopStrategy: string): Promise { + return this.executeFunctionOnEnabled(workshopStrategy, 'getComponent', [injector]); } /** diff --git a/src/core/siteplugins/classes/handlers/workshop-assessment-strategy-handler.ts b/src/core/siteplugins/classes/handlers/workshop-assessment-strategy-handler.ts new file mode 100644 index 000000000..c85da31db --- /dev/null +++ b/src/core/siteplugins/classes/handlers/workshop-assessment-strategy-handler.ts @@ -0,0 +1,81 @@ +// (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 { Injector } from '@angular/core'; +import { AddonWorkshopAssessmentStrategyHandler } from '@addon/mod/workshop/providers/assessment-strategy-delegate'; +import { + CoreSitePluginsWorkshopAssessmentStrategyComponent +} from '../../components/workshop-assessment-strategy/workshop-assessment-strategy'; + +/** + * Handler to display a workshop assessment strategy site plugin. + */ +export class CoreSitePluginsWorkshopAssessmentStrategyHandler implements AddonWorkshopAssessmentStrategyHandler { + + constructor(public name: string, public strategyName: string) { } + + /** + * Return the Component to use to display the plugin data, either in read or in edit mode. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} plugin The plugin object. + * @param {boolean} [edit] Whether the user is editing. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector): any | Promise { + return CoreSitePluginsWorkshopAssessmentStrategyComponent; + } + + /** + * Prepare original values to be shown and compared. + * + * @param {any} form Original data of the form. + * @param {number} workshopId WorkShop Id + * @return {Promise} Promise resolved with original values sorted. + */ + getOriginalValues(form: any, workshopId: number): Promise { + return Promise.resolve([]); + } + + /** + * Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin. + * + * @param {any[]} originalValues Original values of the form. + * @param {any[]} currentValues Current values of the form. + * @return {boolean} True if data has changed, false otherwise. + */ + hasDataChanged(originalValues: any[], currentValues: any[]): boolean { + return false; + } + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Prepare assessment data to be sent to the server depending on the strategy selected. + * + * @param {any{}} currentValues Current values of the form. + * @param {any} form Assessment form data. + * @return {Promise} Promise resolved with the data to be sent. Or rejected with the input errors object. + */ + prepareAssessmentData(currentValues: any[], form: any): Promise { + return Promise.resolve({}); + } +} diff --git a/src/core/siteplugins/components/workshop-assessment-strategy/core-siteplugins-workshop-assessment-strategy.html b/src/core/siteplugins/components/workshop-assessment-strategy/core-siteplugins-workshop-assessment-strategy.html new file mode 100644 index 000000000..f58bcd913 --- /dev/null +++ b/src/core/siteplugins/components/workshop-assessment-strategy/core-siteplugins-workshop-assessment-strategy.html @@ -0,0 +1 @@ + diff --git a/src/core/siteplugins/components/workshop-assessment-strategy/workshop-assessment-strategy.ts b/src/core/siteplugins/components/workshop-assessment-strategy/workshop-assessment-strategy.ts new file mode 100644 index 000000000..ef72738d1 --- /dev/null +++ b/src/core/siteplugins/components/workshop-assessment-strategy/workshop-assessment-strategy.ts @@ -0,0 +1,54 @@ +// (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, Input } from '@angular/core'; +import { CoreSitePluginsProvider } from '../../providers/siteplugins'; +import { CoreSitePluginsCompileInitComponent } from '../../classes/compile-init-component'; + +/** + * Component that displays a workshop assessment strategy plugin created using a site plugin. + */ +@Component({ + selector: 'core-siteplugins-workshop-assessment-strategy', + templateUrl: 'core-siteplugins-workshop-assessment-strategy.html', +}) +export class CoreSitePluginsWorkshopAssessmentStrategyComponent extends CoreSitePluginsCompileInitComponent implements OnInit { + @Input() workshopId: number; + @Input() assessment: any; + @Input() edit: boolean; + @Input() selectedValues: any[]; + @Input() fieldErrors: any; + @Input() strategy: string; + + constructor(sitePluginsProvider: CoreSitePluginsProvider) { + super(sitePluginsProvider); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Pass the input and output data to the component. + this.jsData = { + workshopId: this.workshopId, + assessment: this.assessment, + edit: this.edit, + selectedValues: this.selectedValues, + fieldErrors: this.fieldErrors, + strategy: this.strategy + }; + + this.getHandlerData('workshopform_' + this.strategy); + } +} diff --git a/src/core/siteplugins/providers/helper.ts b/src/core/siteplugins/providers/helper.ts index 1a64945e0..040d3379a 100644 --- a/src/core/siteplugins/providers/helper.ts +++ b/src/core/siteplugins/providers/helper.ts @@ -43,6 +43,7 @@ import { AddonMessageOutputDelegate } from '@addon/messageoutput/providers/deleg import { AddonModQuizAccessRuleDelegate } from '@addon/mod/quiz/providers/access-rules-delegate'; import { AddonModAssignFeedbackDelegate } from '@addon/mod/assign/providers/feedback-delegate'; import { AddonModAssignSubmissionDelegate } from '@addon/mod/assign/providers/submission-delegate'; +import { AddonWorkshopAssessmentStrategyDelegate } from '@addon/mod/workshop/providers/assessment-strategy-delegate'; // Handler classes. import { CoreSitePluginsCourseFormatHandler } from '../classes/handlers/course-format-handler'; @@ -59,6 +60,7 @@ import { CoreSitePluginsMessageOutputHandler } from '../classes/handlers/message import { CoreSitePluginsQuizAccessRuleHandler } from '../classes/handlers/quiz-access-rule-handler'; import { CoreSitePluginsAssignFeedbackHandler } from '../classes/handlers/assign-feedback-handler'; import { CoreSitePluginsAssignSubmissionHandler } from '../classes/handlers/assign-submission-handler'; +import { CoreSitePluginsWorkshopAssessmentStrategyHandler } from '../classes/handlers/workshop-assessment-strategy-handler'; /** * Helper service to provide functionalities regarding site plugins. It basically has the features to load and register site @@ -85,7 +87,8 @@ export class CoreSitePluginsHelperProvider { private questionBehaviourDelegate: CoreQuestionBehaviourDelegate, private questionProvider: CoreQuestionProvider, private messageOutputDelegate: AddonMessageOutputDelegate, private accessRulesDelegate: AddonModQuizAccessRuleDelegate, private assignSubmissionDelegate: AddonModAssignSubmissionDelegate, private translate: TranslateService, - private assignFeedbackDelegate: AddonModAssignFeedbackDelegate) { + private assignFeedbackDelegate: AddonModAssignFeedbackDelegate, + private workshopAssessmentStrategyDelegate: AddonWorkshopAssessmentStrategyDelegate) { this.logger = logger.getInstance('CoreSitePluginsHelperProvider'); @@ -483,6 +486,10 @@ export class CoreSitePluginsHelperProvider { promise = Promise.resolve(this.registerAssignSubmissionHandler(plugin, handlerName, handlerSchema)); break; + case 'AddonWorkshopAssessmentStrategyDelegate': + promise = Promise.resolve(this.registerWorkshopAssessmentStrategyHandler(plugin, handlerName, handlerSchema)); + break; + default: // Nothing to do. promise = Promise.resolve(); @@ -864,4 +871,24 @@ export class CoreSitePluginsHelperProvider { return new CoreSitePluginsUserProfileFieldHandler(uniqueName, fieldType); }); } + + /** + * Given a handler in a plugin, register it in the workshop assessment strategy delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @return {string|Promise} A string (or a promise resolved with a string) to identify the handler. + */ + protected registerWorkshopAssessmentStrategyHandler(plugin: any, handlerName: string, handlerSchema: any) + : string | Promise { + + return this.registerComponentInitHandler(plugin, handlerName, handlerSchema, this.workshopAssessmentStrategyDelegate, + (uniqueName: string, result: any) => { + + const strategyName = plugin.component.replace('workshopform_', ''); + + return new CoreSitePluginsWorkshopAssessmentStrategyHandler(uniqueName, strategyName); + }); + } }