From f90e699ac31b213bc716a84fc6a5b912415ab88e Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 30 May 2018 11:33:29 +0200 Subject: [PATCH] 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); + }); + } +}