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; + } +}