MOBILE-2354 workshop: Assessment strategy delegate and helper provider

main
Albert Gasset 2018-05-30 17:04:43 +02:00
parent f90e699ac3
commit d000b3da36
2 changed files with 669 additions and 0 deletions

View File

@ -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<any[]>} Promise resolved with original values sorted.
*/
getOriginalValues?(form: any, workshopId: number): Promise<any[]>;
/**
* 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<any>} Promise resolved with the data to be sent. Or rejected with the input errors object.
*/
prepareAssessmentData(currentValues: any[], form: any): Promise<any>;
}
/**
* 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<any> {
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<any[]>} Resolved with original values sorted.
*/
getOriginalValues(workshopStrategy: string, form: any, workshopId: number): Promise<any[]> {
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<any>} Promise resolved with the data to be sent. Or rejected with the input errors object.
*/
prepareAssessmentData(workshopStrategy: string, currentValues: any, form: any): Promise<any> {
return Promise.resolve(this.executeFunctionOnEnabled(workshopStrategy, 'prepareAssessmentData', [currentValues, form]));
}
}

View File

@ -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<any>} Resolved with the submission, resolved with false if not found.
*/
getUserSubmission(workshopId: number, userId: number = 0): Promise<any> {
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<any>} Resolved with the submission, resolved with false if not found.
*/
getSubmissionById(workshopId: number, submissionId: number, siteId?: string): Promise<any> {
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<any>} Resolved with the assessment.
*/
getReviewerAssessmentById(workshopId: number, assessmentId: number, userId: number = 0, siteId?: string): Promise<any> {
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<any[]>} Promise resolved when the workshop data is retrieved.
*/
getReviewerAssessments(workshopId: number, userId: number = 0, offline: boolean = false, ignoreCache: boolean = false,
siteId?: string): Promise<any[]> {
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<any>} Promise resolved when deleted.
*/
deleteSubmissionStoredFiles(workshopId: number, submissionId: number, editing: boolean, siteId?: string): Promise<any> {
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<any>} Promise resolved if success, rejected otherwise.
*/
storeSubmissionFiles(workshopId: number, submissionId: number, editing: boolean, files: any[], siteId?: string): Promise<any> {
// 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<any>} Promise resolved if success.
*/
uploadOrStoreSubmissionFiles(workshopId: number, submissionId: number, files: any[], editing: boolean, offline: boolean,
siteId?: string): Promise<any> {
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<any[]>} Promise resolved with the files.
*/
getStoredSubmissionFiles(workshopId: number, submissionId: number, editing: boolean, siteId?: string): Promise<any[]> {
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<any[]>} Promise resolved with the files.
*/
getSubmissionFilesFromOfflineFilesObject(filesObject: any, workshopId: number, submissionId: number, editing: boolean,
siteId?: string): Promise<any[]> {
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<any>} Promise resolved when deleted.
*/
deleteAssessmentStoredFiles(workshopId: number, assessmentId: number, siteId?: string): Promise<any> {
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<any>} Promise resolved if success, rejected otherwise.
*/
storeAssessmentFiles(workshopId: number, assessmentId: number, files: any[], siteId?: string): Promise<any> {
// 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<any>} Promise resolved if success.
*/
uploadOrStoreAssessmentFiles(workshopId: number, assessmentId: number, files: any[], offline: boolean, siteId?: string):
Promise<any> {
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<any>} Promise resolved with the files.
*/
getStoredAssessmentFiles(workshopId: number, assessmentId: number, siteId?: string): Promise<any> {
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<any>} Promise resolved with the files.
*/
getAssessmentFilesFromOfflineFilesObject(filesObject: any, workshopId: number, assessmentId: number, siteId?: string):
Promise<any> {
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<any>} Promise resolved with the files.
*/
applyOfflineData(submission: any, actions: any[]): Promise<any> {
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<any>} 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<any> {
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;
}
}