// (C) Copyright 2015 Moodle Pty Ltd. // // 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 { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCommentsProvider } from '@core/comments/providers/comments'; import { CoreGradesProvider } from '@core/grades/providers/grades'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModAssignSubmissionDelegate } from './submission-delegate'; import { AddonModAssignOfflineProvider } from './assign-offline'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreInterceptor } from '@classes/interceptor'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; /** * Service that provides some functions for assign. */ @Injectable() export class AddonModAssignProvider { static COMPONENT = 'mmaModAssign'; static SUBMISSION_COMPONENT = 'mmaModAssignSubmission'; static UNLIMITED_ATTEMPTS = -1; // Submission status. static SUBMISSION_STATUS_NEW = 'new'; static SUBMISSION_STATUS_REOPENED = 'reopened'; static SUBMISSION_STATUS_DRAFT = 'draft'; static SUBMISSION_STATUS_SUBMITTED = 'submitted'; // "Re-open" methods (to retry the assign). static ATTEMPT_REOPEN_METHOD_NONE = 'none'; static ATTEMPT_REOPEN_METHOD_MANUAL = 'manual'; // Grading status. static GRADING_STATUS_GRADED = 'graded'; static GRADING_STATUS_NOT_GRADED = 'notgraded'; static MARKING_WORKFLOW_STATE_RELEASED = 'released'; static NEED_GRADING = 'needgrading'; static GRADED_FOLLOWUP_SUBMIT = 'gradedfollowupsubmit'; // Group submissions warnings. static WARN_GROUPS_REQUIRED = 'warnrequired'; static WARN_GROUPS_OPTIONAL = 'warnoptional'; // Events. static SUBMISSION_SAVED_EVENT = 'addon_mod_assign_submission_saved'; static SUBMITTED_FOR_GRADING_EVENT = 'addon_mod_assign_submitted_for_grading'; static GRADED_EVENT = 'addon_mod_assign_graded'; protected ROOT_CACHE_KEY = 'mmaModAssign:'; protected logger; protected gradingOfflineEnabled: {[siteId: string]: boolean} = {}; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, private submissionDelegate: AddonModAssignSubmissionDelegate, private gradesProvider: CoreGradesProvider, private filepoolProvider: CoreFilepoolProvider, private assignOffline: AddonModAssignOfflineProvider, private commentsProvider: CoreCommentsProvider, private logHelper: CoreCourseLogHelperProvider) { this.logger = logger.getInstance('AddonModAssignProvider'); } /** * Check if the user can submit in offline. This should only be used if submissionStatus.lastattempt.cansubmit cannot * be used (offline usage). * This function doesn't check if the submission is empty, it should be checked before calling this function. * * @param assign Assignment instance. * @param submissionStatus Submission status returned by getSubmissionStatus. * @return Whether it can submit. */ canSubmitOffline(assign: any, submissionStatus: any): boolean { if (!this.isSubmissionOpen(assign, submissionStatus)) { return false; } const userSubmission = submissionStatus.lastattempt.submission, teamSubmission = submissionStatus.lastattempt.teamsubmission; if (teamSubmission) { if (teamSubmission.status === AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED) { // The assignment submission has been completed. return false; } else if (userSubmission && userSubmission.status === AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED) { // The user has already clicked the submit button on the team submission. return false; } else if (assign.preventsubmissionnotingroup && !submissionStatus.lastattempt.submissiongroup) { return false; } } else if (userSubmission) { if (userSubmission.status === AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED) { // The assignment submission has been completed. return false; } } else { // No valid submission or team submission. return false; } // Last check is that this instance allows drafts. return assign.submissiondrafts; } /** * Fix some submission status params. * * @param site Site to use. * @param userId User Id (empty for current user). * @param groupId Group Id (empty for all participants). * @param isBlind If blind marking is enabled or not. * @return Object with fixed params. */ protected fixSubmissionStatusParams(site: CoreSite, userId?: number, groupId?: number, isBlind?: boolean) : {userId: number, groupId: number, isBlind: boolean} { return { isBlind: !userId ? false : !!isBlind, groupId: site.isVersionGreaterEqualThan('3.5') ? groupId || 0 : 0, userId: userId || site.getUserId(), }; } /** * Get an assignment by course module ID. * * @param courseId Course ID the assignment belongs to. * @param cmId Assignment module ID. * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the assignment. */ getAssignment(courseId: number, cmId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.getAssignmentByField(courseId, 'cmid', cmId, ignoreCache, siteId); } /** * Get an assigment with key=value. If more than one is found, only the first will be returned. * * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the assignment is retrieved. */ protected getAssignmentByField(courseId: number, key: string, value: any, ignoreCache?: boolean, siteId?: string) : Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId], includenotenrolledcourses: 1 }, preSets: CoreSiteWSPreSets = { cacheKey: this.getAssignmentCacheKey(courseId), updateFrequency: CoreSite.FREQUENCY_RARELY }; if (ignoreCache) { preSets.getFromCache = false; preSets.emergencyCache = false; } return site.read('mod_assign_get_assignments', params, preSets).catch(() => { // In 3.6 we added a new parameter includenotenrolledcourses that could cause offline data not to be found. // Retry again without the param to check if the request is already cached. delete params.includenotenrolledcourses; return site.read('mod_assign_get_assignments', params, preSets); }).then((response: AddonModAssignGetAssignmentsResult): any => { // Search the assignment to return. if (response.courses && response.courses.length) { const assignments = response.courses[0].assignments; for (let i = 0; i < assignments.length; i++) { if (assignments[i][key] == value) { return assignments[i]; } } } return Promise.reject(null); }); }); } /** * Get an assignment by instance ID. * * @param courseId Course ID the assignment belongs to. * @param cmId Assignment instance ID. * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the assignment. */ getAssignmentById(courseId: number, id: number, ignoreCache?: boolean, siteId?: string): Promise { return this.getAssignmentByField(courseId, 'id', id, ignoreCache, siteId); } /** * Get cache key for assignment data WS calls. * * @param courseId Course ID. * @return Cache key. */ protected getAssignmentCacheKey(courseId: number): string { return this.ROOT_CACHE_KEY + 'assignment:' + courseId; } /** * Get an assignment user mapping for blind marking. * * @param assignId Assignment Id. * @param userId User Id to be blinded. * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the user blind id. */ getAssignmentUserMappings(assignId: number, userId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { assignmentids: [assignId] }, preSets: CoreSiteWSPreSets = { cacheKey: this.getAssignmentUserMappingsCacheKey(assignId), updateFrequency: CoreSite.FREQUENCY_OFTEN }; if (ignoreCache) { preSets.getFromCache = false; preSets.emergencyCache = false; } return site.read('mod_assign_get_user_mappings', params, preSets) .then((response: AddonModAssignGetUserMappingsResult): any => { // Search the user. if (response.assignments && response.assignments.length) { if (!userId || userId < 0) { // User not valid, stop. return -1; } const assignment = response.assignments[0]; if (assignment.assignmentid == assignId) { const mappings = assignment.mappings; for (let i = 0; i < mappings.length; i++) { if (mappings[i].userid == userId) { return mappings[i].id; } } } } else if (response.warnings && response.warnings.length) { return Promise.reject(response.warnings[0]); } return Promise.reject(null); }); }); } /** * Get cache key for assignment user mappings data WS calls. * * @param assignId Assignment ID. * @return Cache key. */ protected getAssignmentUserMappingsCacheKey(assignId: number): string { return this.ROOT_CACHE_KEY + 'usermappings:' + assignId; } /** * Returns grade information from assign_grades for the requested assignment id * * @param assignId Assignment Id. * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @param siteId Site ID. If not defined, current site. * @return Resolved with requested info when done. */ getAssignmentGrades(assignId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { assignmentids: [assignId] }, preSets: CoreSiteWSPreSets = { cacheKey: this.getAssignmentGradesCacheKey(assignId) }; if (ignoreCache) { preSets.getFromCache = false; preSets.emergencyCache = false; } return site.read('mod_assign_get_grades', params, preSets).then((response: AddonModAssignGetGradesResult): any => { // Search the assignment. if (response.assignments && response.assignments.length) { const assignment = response.assignments[0]; if (assignment.assignmentid == assignId) { return assignment.grades; } } else if (response.warnings && response.warnings.length) { if (response.warnings[0].warningcode == '3') { // No grades found. return []; } return Promise.reject(response.warnings[0]); } return Promise.reject(null); }); }); } /** * Get cache key for assignment grades data WS calls. * * @param assignId Assignment ID. * @return Cache key. */ protected getAssignmentGradesCacheKey(assignId: number): string { return this.ROOT_CACHE_KEY + 'assigngrades:' + assignId; } /** * Returns the color name for a given grading status name. * * @param status Grading status name * @return The color name. */ getSubmissionGradingStatusColor(status: string): string { if (!status) { return ''; } if (status == AddonModAssignProvider.GRADING_STATUS_GRADED || status == AddonModAssignProvider.MARKING_WORKFLOW_STATE_RELEASED) { return 'success'; } return 'danger'; } /** * Returns the translation id for a given grading status name. * * @param status Grading Status name * @return The status translation identifier. */ getSubmissionGradingStatusTranslationId(status: string): string { if (!status) { return; } if (status == AddonModAssignProvider.GRADING_STATUS_GRADED || status == AddonModAssignProvider.GRADING_STATUS_NOT_GRADED || status == AddonModAssignProvider.GRADED_FOLLOWUP_SUBMIT) { return 'addon.mod_assign.' + status; } return 'addon.mod_assign.markingworkflowstate' + status; } /** * Get the submission object from an attempt. * * @param assign Assign. * @param attempt Attempt. * @return Submission object or null. */ getSubmissionObjectFromAttempt(assign: AddonModAssignAssign, attempt: AddonModAssignSubmissionAttempt) : AddonModAssignSubmission { if (!attempt) { return null; } return assign.teamsubmission ? attempt.teamsubmission : attempt.submission; } /** * Get attachments of a submission plugin. * * @param submissionPlugin Submission plugin. * @return Submission plugin attachments. */ getSubmissionPluginAttachments(submissionPlugin: any): any[] { const files = []; if (submissionPlugin.fileareas) { submissionPlugin.fileareas.forEach((filearea) => { if (!filearea || !filearea.files) { // No files to get. return; } filearea.files.forEach((file) => { if (!file.filename) { // We don't have filename, extract it from the path. file.filename = file.filepath[0] == '/' ? file.filepath.substr(1) : file.filepath; } files.push(file); }); }); } return files; } /** * Get text of a submission plugin. * * @param submissionPlugin Submission plugin. * @param keepUrls True if it should keep original URLs, false if they should be replaced. * @return Submission text. */ getSubmissionPluginText(submissionPlugin: any, keepUrls?: boolean): string { let text = ''; if (submissionPlugin.editorfields) { submissionPlugin.editorfields.forEach((field) => { text += field.text; }); if (!keepUrls && submissionPlugin.fileareas && submissionPlugin.fileareas[0]) { text = this.textUtils.replacePluginfileUrls(text, submissionPlugin.fileareas[0].files); } } return text; } /** * Get an assignment submissions. * * @param assignId Assignment id. * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ getSubmissions(assignId: number, ignoreCache?: boolean, siteId?: string) : Promise<{canviewsubmissions: boolean, submissions?: AddonModAssignSubmission[]}> { return this.sitesProvider.getSite(siteId).then((site) => { const params = { assignmentids: [assignId] }, preSets: CoreSiteWSPreSets = { cacheKey: this.getSubmissionsCacheKey(assignId), updateFrequency: CoreSite.FREQUENCY_OFTEN }; if (ignoreCache) { preSets.getFromCache = false; preSets.emergencyCache = false; } return site.read('mod_assign_get_submissions', params, preSets) .then((response: AddonModAssignGetSubmissionsResult): any => { // Check if we can view submissions, with enough permissions. if (response.warnings.length > 0 && response.warnings[0].warningcode == '1') { return {canviewsubmissions: false}; } if (response.assignments && response.assignments.length) { return { canviewsubmissions: true, submissions: response.assignments[0].submissions }; } return Promise.reject(null); }); }); } /** * Get cache key for assignment submissions data WS calls. * * @param assignId Assignment id. * @return Cache key. */ protected getSubmissionsCacheKey(assignId: number): string { return this.ROOT_CACHE_KEY + 'submissions:' + assignId; } /** * Get information about an assignment submission status for a given user. * * @param assignId Assignment instance id. * @param userId User Id (empty for current user). * @param groupId Group Id (empty for all participants). * @param isBlind If blind marking is enabled or not. * @param filter True to filter WS response and rewrite URLs, false otherwise. * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @param siteId Site id (empty for current site). * @return Promise always resolved with the user submission status. */ getSubmissionStatus(assignId: number, userId?: number, groupId?: number, isBlind?: boolean, filter: boolean = true, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const fixedParams = this.fixSubmissionStatusParams(site, userId, groupId, isBlind); const params = { assignid: assignId, userid: fixedParams.userId }, preSets: CoreSiteWSPreSets = { cacheKey: this.getSubmissionStatusCacheKey(assignId, fixedParams.userId, fixedParams.groupId, fixedParams.isBlind), getCacheUsingCacheKey: true, // We use the cache key to take isBlind into account. filter: filter, rewriteurls: filter }; if (fixedParams.groupId) { params['groupid'] = fixedParams.groupId; } if (ignoreCache) { preSets.getFromCache = false; preSets.emergencyCache = false; } if (!filter) { // Don't cache when getting text without filters. // @todo Change this to support offline editing. preSets.saveToCache = false; } return site.read('mod_assign_get_submission_status', params, preSets); }); } /** * Get information about an assignment submission status for a given user. * If the data doesn't include the user submission, retry ignoring cache. * * @param assign Assignment. * @param userId User id (empty for current user). * @param groupId Group Id (empty for all participants). * @param isBlind If blind marking is enabled or not. * @param filter True to filter WS response and rewrite URLs, false otherwise. * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @param siteId Site id (empty for current site). * @return Promise always resolved with the user submission status. */ getSubmissionStatusWithRetry(assign: any, userId?: number, groupId?: number, isBlind?: boolean, filter: boolean = true, ignoreCache?: boolean, siteId?: string): Promise { return this.getSubmissionStatus(assign.id, userId, groupId, isBlind, filter, ignoreCache, siteId).then((response) => { const userSubmission = this.getSubmissionObjectFromAttempt(assign, response.lastattempt); if (!userSubmission) { // Try again, ignoring cache. return this.getSubmissionStatus(assign.id, userId, groupId, isBlind, filter, true, siteId).catch(() => { // Error, return the first result even if it doesn't have the user submission. return response; }); } return response; }); } /** * Get cache key for get submission status data WS calls. * * @param assignId Assignment instance id. * @param userId User id (empty for current user). * @param groupId Group Id (empty for all participants). * @param isBlind If blind marking is enabled or not. * @return Cache key. */ protected getSubmissionStatusCacheKey(assignId: number, userId: number, groupId?: number, isBlind?: boolean): string { return this.getSubmissionsCacheKey(assignId) + ':' + userId + ':' + (isBlind ? 1 : 0) + ':' + groupId; } /** * Returns the color name for a given status name. * * @param status Status name * @return The color name. */ getSubmissionStatusColor(status: string): string { switch (status) { case 'submitted': return 'success'; case 'draft': return 'info'; case 'new': case 'noattempt': case 'noonlinesubmissions': case 'nosubmission': case 'gradedfollowupsubmit': return 'danger'; default: return 'light'; } } /** * Given a list of plugins, returns the plugin names that aren't supported for editing. * * @param plugins Plugins to check. * @return Promise resolved with unsupported plugin names. */ getUnsupportedEditPlugins(plugins: any[]): Promise { const notSupported = [], promises = []; plugins.forEach((plugin) => { promises.push(this.submissionDelegate.isPluginSupportedForEdit(plugin.type).then((enabled) => { if (!enabled) { notSupported.push(plugin.name); } })); }); return Promise.all(promises).then(() => { return notSupported; }); } /** * List the participants for a single assignment, with some summary info about their submissions. * * @param assignId Assignment id. * @param groupId Group id. If not defined, 0. * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the list of participants and summary of submissions. */ listParticipants(assignId: number, groupId?: number, ignoreCache?: boolean, siteId?: string) : Promise { groupId = groupId || 0; return this.sitesProvider.getSite(siteId).then((site) => { if (!site.wsAvailable('mod_assign_list_participants')) { // Silently fail if is not available. (needs Moodle version >= 3.2) return Promise.reject(null); } const params = { assignid: assignId, groupid: groupId, filter: '' }, preSets: CoreSiteWSPreSets = { cacheKey: this.listParticipantsCacheKey(assignId, groupId), updateFrequency: CoreSite.FREQUENCY_OFTEN }; if (ignoreCache) { preSets.getFromCache = false; preSets.emergencyCache = false; } return site.read('mod_assign_list_participants', params, preSets); }); } /** * Get cache key for assignment list participants data WS calls. * * @param assignId Assignment id. * @param groupId Group id. * @return Cache key. */ protected listParticipantsCacheKey(assignId: number, groupId: number): string { return this.listParticipantsPrefixCacheKey(assignId) + ':' + groupId; } /** * Get prefix cache key for assignment list participants data WS calls. * * @param assignId Assignment id. * @return Cache key. */ protected listParticipantsPrefixCacheKey(assignId: number): string { return this.ROOT_CACHE_KEY + 'participants:' + assignId; } /** * Invalidates all submission status data. * * @param assignId Assignment instance id. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ invalidateAllSubmissionData(assignId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.invalidateWsCacheForKeyStartingWith(this.getSubmissionsCacheKey(assignId)); }); } /** * Invalidates assignment data WS calls. * * @param courseId Course ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ invalidateAssignmentData(courseId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.invalidateWsCacheForKey(this.getAssignmentCacheKey(courseId)); }); } /** * Invalidates assignment user mappings data WS calls. * * @param assignId Assignment ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ invalidateAssignmentUserMappingsData(assignId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.invalidateWsCacheForKey(this.getAssignmentUserMappingsCacheKey(assignId)); }); } /** * Invalidates assignment grades data WS calls. * * @param assignId Assignment ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ invalidateAssignmentGradesData(assignId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.invalidateWsCacheForKey(this.getAssignmentGradesCacheKey(assignId)); }); } /** * Invalidate the prefetched content except files. * To invalidate files, use AddonModAssignProvider.invalidateFiles. * * @param moduleId The module ID. * @param courseId Course ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); return this.getAssignment(courseId, moduleId, false, siteId).then((assign) => { const promises = []; // Do not invalidate assignment data before getting assignment info, we need it! promises.push(this.invalidateAllSubmissionData(assign.id, siteId)); promises.push(this.invalidateAssignmentUserMappingsData(assign.id, siteId)); promises.push(this.invalidateAssignmentGradesData(assign.id, siteId)); promises.push(this.invalidateListParticipantsData(assign.id, siteId)); promises.push(this.commentsProvider.invalidateCommentsByInstance('module', assign.id, siteId)); promises.push(this.invalidateAssignmentData(courseId, siteId)); promises.push(this.gradesProvider.invalidateAllCourseGradesData(courseId)); return Promise.all(promises); }); } /** * Invalidate the prefetched files. * * @param moduleId The module ID. * @return Promise resolved when the files are invalidated. */ invalidateFiles(moduleId: number): Promise { return this.filepoolProvider.invalidateFilesByComponent(this.sitesProvider.getCurrentSiteId(), AddonModAssignProvider.COMPONENT, moduleId); } /** * Invalidates assignment submissions data WS calls. * * @param assignId Assignment instance id. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ invalidateSubmissionData(assignId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.invalidateWsCacheForKey(this.getSubmissionsCacheKey(assignId)); }); } /** * Invalidates submission status data. * * @param assignId Assignment instance id. * @param userId User id (empty for current user). * @param groupId Group Id (empty for all participants). * @param isBlind Whether blind marking is enabled or not. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ invalidateSubmissionStatusData(assignId: number, userId?: number, groupId?: number, isBlind?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const fixedParams = this.fixSubmissionStatusParams(site, userId, groupId, isBlind); return site.invalidateWsCacheForKey(this.getSubmissionStatusCacheKey(assignId, fixedParams.userId, fixedParams.groupId, fixedParams.isBlind)); }); } /** * Invalidates assignment participants data. * * @param assignId Assignment instance id. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ invalidateListParticipantsData(assignId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.invalidateWsCacheForKeyStartingWith(this.listParticipantsPrefixCacheKey(assignId)); }); } /** * Convenience function to check if grading offline is enabled. * * @param siteId Site ID. If not defined, current site. * @return Promise resolved with boolean: whether grading offline is enabled. */ protected isGradingOfflineEnabled(siteId?: string): Promise { if (typeof this.gradingOfflineEnabled[siteId] != 'undefined') { return Promise.resolve(this.gradingOfflineEnabled[siteId]); } return this.gradesProvider.isGradeItemsAvalaible(siteId).then((enabled) => { this.gradingOfflineEnabled[siteId] = enabled; return enabled; }); } /** * Outcomes only can be edited if mod_assign_submit_grading_form is avalaible. * * @param siteId Site ID. If not defined, current site. * @return Promise resolved with true if outcomes edit is enabled, rejected or resolved with false otherwise. * @since 3.2 */ isOutcomesEditEnabled(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.wsAvailable('mod_assign_submit_grading_form'); }); } /** * Check if assignments plugin is enabled in a certain site. * * @param siteId Site ID. If not defined, current site. * @return Whether the plugin is enabled. */ isPluginEnabled(siteId?: string): boolean { return true; } /** * Check if a submission is open. This function is based on Moodle's submissions_open. * * @param assign Assignment instance. * @param submissionStatus Submission status returned by getSubmissionStatus. * @return Whether submission is open. */ isSubmissionOpen(assign: any, submissionStatus: any): boolean { if (!assign || !submissionStatus) { return false; } const time = this.timeUtils.timestamp(), lastAttempt = submissionStatus.lastattempt, submission = this.getSubmissionObjectFromAttempt(assign, lastAttempt); let dateOpen = true, finalDate; if (assign.cutoffdate) { finalDate = assign.cutoffdate; } if (lastAttempt && lastAttempt.locked) { return false; } // User extensions. if (finalDate) { if (lastAttempt && lastAttempt.extensionduedate) { // Extension can be before cut off date. if (lastAttempt.extensionduedate > finalDate) { finalDate = lastAttempt.extensionduedate; } } } if (finalDate) { dateOpen = assign.allowsubmissionsfromdate <= time && time <= finalDate; } else { dateOpen = assign.allowsubmissionsfromdate <= time; } if (!dateOpen) { return false; } if (submission) { if (assign.submissiondrafts && submission.status == AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED) { // Drafts are tracked and the student has submitted the assignment. return false; } } return true; } /** * Report an assignment submission as being viewed. * * @param assignId Assignment ID. * @param name Name of the assign. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the WS call is successful. */ logSubmissionView(assignId: number, name?: string, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { assignid: assignId }; return this.logHelper.logSingle('mod_assign_view_submission_status', params, AddonModAssignProvider.COMPONENT, assignId, name, 'assign', {}, siteId); }); } /** * Report an assignment grading table is being viewed. * * @param assignId Assignment ID. * @param name Name of the assign. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the WS call is successful. */ logGradingView(assignId: number, name?: string, siteId?: string): Promise { const params = { assignid: assignId }; return this.logHelper.logSingle('mod_assign_view_grading_table', params, AddonModAssignProvider.COMPONENT, assignId, name, 'assign', {}, siteId); } /** * Report an assign as being viewed. * * @param assignId Assignment ID. * @param name Name of the assign. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the WS call is successful. */ logView(assignId: number, name?: string, siteId?: string): Promise { const params = { assignid: assignId }; return this.logHelper.logSingle('mod_assign_view_assign', params, AddonModAssignProvider.COMPONENT, assignId, name, 'assign', {}, siteId); } /** * Returns if a submissions needs to be graded. * * @param submission Submission. * @param assignId Assignment ID. * @return Promise resolved with boolean: whether it needs to be graded or not. */ needsSubmissionToBeGraded(submission: any, assignId: number): Promise { if (!submission.gradingstatus) { // This should not happen, but it's better to show rather than not showing any of the submissions. return Promise.resolve(true); } if (submission.gradingstatus != AddonModAssignProvider.GRADING_STATUS_GRADED && submission.gradingstatus != AddonModAssignProvider.MARKING_WORKFLOW_STATE_RELEASED) { // Not graded. return Promise.resolve(true); } // We need more data to decide that. return this.getSubmissionStatus(assignId, submission.submitid, undefined, submission.blindid).then((response) => { if (!response.feedback || !response.feedback.gradeddate) { // Not graded. return true; } // Submitted after grading? return response.feedback.gradeddate < submission.timemodified; }); } /** * Save current user submission for a certain assignment. * * @param assignId Assign ID. * @param courseId Course ID the assign belongs to. * @param pluginData Data to save. * @param allowOffline Whether to allow offline usage. * @param timemodified The time the submission was last modified in online. * @param allowsDrafts Whether the assignment allows submission drafts. * @param userId User ID. If not defined, site's current user. * @param siteId Site ID. If not defined, current site. * @return Promise resolved with true if sent to server, resolved with false if stored in offline. */ saveSubmission(assignId: number, courseId: number, pluginData: any, allowOffline: boolean, timemodified: number, allowsDrafts?: boolean, userId?: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Function to store the submission to be synchronized later. const storeOffline = (): Promise => { return this.assignOffline.saveSubmission(assignId, courseId, pluginData, timemodified, !allowsDrafts, userId, siteId) .then(() => { return false; }); }; if (allowOffline && !this.appProvider.isOnline()) { // App is offline, store the action. return storeOffline(); } // If there's already a submission to be sent to the server, discard it first. return this.assignOffline.deleteSubmission(assignId, userId, siteId).then(() => { return this.saveSubmissionOnline(assignId, pluginData, siteId).then(() => { return true; }).catch((error) => { if (allowOffline && error && !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); } }); }); } /** * Save current user submission for a certain assignment. It will fail if offline or cannot connect. * * @param assignId Assign ID. * @param pluginData Data to save. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when saved, rejected otherwise. */ saveSubmissionOnline(assignId: number, pluginData: any, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { assignmentid: assignId, plugindata: pluginData }; return site.write('mod_assign_save_submission', params).then((warnings: CoreWSExternalWarning[]) => { if (warnings && warnings.length) { // The WebService returned warnings, reject. return Promise.reject(warnings[0]); } }); }); } /** * Submit the current user assignment for grading. * * @param assignId Assign ID. * @param courseId Course ID the assign belongs to. * @param acceptStatement True if submission statement is accepted, false otherwise. * @param timemodified The time the submission was last modified in online. * @param forceOffline True to always mark it in offline. * @param siteId Site ID. If not defined, current site. * @return Promise resolved with true if sent to server, resolved with false if stored in offline. */ submitForGrading(assignId: number, courseId: number, acceptStatement: boolean, timemodified: number, forceOffline?: boolean, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Function to store the submission to be synchronized later. const storeOffline = (): Promise => { return this.assignOffline.markSubmitted(assignId, courseId, true, acceptStatement, timemodified, undefined, siteId) .then(() => { return false; }); }; if (forceOffline || !this.appProvider.isOnline()) { // App is offline, store the action. return storeOffline(); } // If there's already a submission to be sent to the server, discard it first. return this.assignOffline.deleteSubmission(assignId, undefined, siteId).then(() => { return this.submitForGradingOnline(assignId, acceptStatement, siteId).then(() => { return true; }).catch((error) => { if (error && !this.utils.isWebServiceError(error)) { // Couldn't connect to server, store in offline. return storeOffline(); } else { // The WebService has thrown an error, reject. return Promise.reject(error); } }); }); } /** * Submit the current user assignment for grading. It will fail if offline or cannot connect. * * @param assignId Assign ID. * @param acceptStatement True if submission statement is accepted, false otherwise. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when submitted, rejected otherwise. */ submitForGradingOnline(assignId: number, acceptStatement: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { assignmentid: assignId, acceptsubmissionstatement: acceptStatement ? 1 : 0 }; return site.write('mod_assign_submit_for_grading', params).then((warnings: CoreWSExternalWarning[]) => { if (warnings && warnings.length) { // The WebService returned warnings, reject. return Promise.reject(warnings[0]); } }); }); } /** * Submit the grading for the current user and assignment. It will use old or new WS depending on availability. * * @param assignId Assign ID. * @param userId User ID. * @param courseId Course ID the assign belongs to. * @param grade Grade to submit. * @param attemptNumber Number of the attempt being graded. * @param addAttempt Admit the user to attempt again. * @param workflowState Next workflow State. * @param applyToAll If it's a team submission, whether the grade applies to all group members. * @param outcomes Object including all outcomes values. If empty, any of them will be sent. * @param pluginData Feedback plugin data to save. * @param siteId Site ID. If not defined, current site. * @return Promise resolved with true if sent to server, resolved with false if stored offline. */ submitGradingForm(assignId: number, userId: number, courseId: number, grade: number, attemptNumber: number, addAttempt: boolean, workflowState: string, applyToAll: boolean, outcomes: any, pluginData: any, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Function to store the grading to be synchronized later. const storeOffline = (): Promise => { return this.assignOffline.submitGradingForm(assignId, userId, courseId, grade, attemptNumber, addAttempt, workflowState, applyToAll, outcomes, pluginData, siteId).then(() => { return false; }); }; // Grading offline is only allowed if WS of grade items is enabled to avoid inconsistency. return this.isGradingOfflineEnabled(siteId).then((enabled) => { if (!enabled) { return this.submitGradingFormOnline(assignId, userId, grade, attemptNumber, addAttempt, workflowState, applyToAll, outcomes, pluginData, siteId).then(() => { return true; }); } if (!this.appProvider.isOnline()) { // App is offline, store the action. return storeOffline(); } // If there's already a grade to be sent to the server, discard it first. return this.assignOffline.deleteSubmissionGrade(assignId, userId, siteId).then(() => { return this.submitGradingFormOnline(assignId, userId, grade, attemptNumber, addAttempt, workflowState, applyToAll, outcomes, pluginData, siteId).then(() => { return true; }).catch((error) => { if (error && !this.utils.isWebServiceError(error)) { // Couldn't connect to server, store in offline. return storeOffline(); } else { // The WebService has thrown an error, reject. return Promise.reject(error); } }); }); }); } /** * Submit the grading for the current user and assignment. It will use old or new WS depending on availability. * It will fail if offline or cannot connect. * * @param assignId Assign ID. * @param userId User ID. * @param grade Grade to submit. * @param attemptNumber Number of the attempt being graded. * @param addAttempt Allow the user to attempt again. * @param workflowState Next workflow State. * @param applyToAll If it's a team submission, if the grade applies to all group members. * @param outcomes Object including all outcomes values. If empty, any of them will be sent. * @param pluginData Feedback plugin data to save. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when submitted, rejected otherwise. */ submitGradingFormOnline(assignId: number, userId: number, grade: number, attemptNumber: number, addAttempt: boolean, workflowState: string, applyToAll: boolean, outcomes: any, pluginData: any, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); if (site.wsAvailable('mod_assign_submit_grading_form')) { // WS available @since 3.2. const jsonData = { grade: grade, attemptnumber: attemptNumber, addattempt: addAttempt ? 1 : 0, workflowstate: workflowState, applytoall: applyToAll ? 1 : 0 }; for (const index in outcomes) { jsonData['outcome_' + index + '[' + userId + ']'] = outcomes[index]; } for (const index in pluginData) { jsonData[index] = pluginData[index]; } const serialized = CoreInterceptor.serialize(jsonData, true), params = { assignmentid: assignId, userid: userId, jsonformdata: JSON.stringify(serialized) }; return site.write('mod_assign_submit_grading_form', params).then((warnings: CoreWSExternalWarning[]) => { if (warnings && warnings.length) { // The WebService returned warnings, reject. return Promise.reject(warnings[0]); } }); } else { // WS not available, fallback to save_grade. const params = { assignmentid: assignId, userid: userId, grade: grade, attemptnumber: attemptNumber, addattempt: addAttempt ? 1 : 0, workflowstate: workflowState, applytoall: applyToAll ? 1 : 0, plugindata: pluginData }, preSets = { responseExpected: false }; return site.write('mod_assign_save_grade', params, preSets); } }); } } /** * Assign data returned by mod_assign_get_assignments. */ export type AddonModAssignAssign = { id: number; // Assignment id. cmid: number; // Course module id. course: number; // Course id. name: string; // Assignment name. nosubmissions: number; // No submissions. submissiondrafts: number; // Submissions drafts. sendnotifications: number; // Send notifications. sendlatenotifications: number; // Send notifications. sendstudentnotifications: number; // Send student notifications (default). duedate: number; // Assignment due date. allowsubmissionsfromdate: number; // Allow submissions from date. grade: number; // Grade type. timemodified: number; // Last time assignment was modified. completionsubmit: number; // If enabled, set activity as complete following submission. cutoffdate: number; // Date after which submission is not accepted without an extension. gradingduedate?: number; // @since 3.3. The expected date for marking the submissions. teamsubmission: number; // If enabled, students submit as a team. requireallteammemberssubmit: number; // If enabled, all team members must submit. teamsubmissiongroupingid: number; // The grouping id for the team submission groups. blindmarking: number; // If enabled, hide identities until reveal identities actioned. hidegrader?: number; // @since 3.7. If enabled, hide grader to student. revealidentities: number; // Show identities for a blind marking assignment. attemptreopenmethod: string; // Method used to control opening new attempts. maxattempts: number; // Maximum number of attempts allowed. markingworkflow: number; // Enable marking workflow. markingallocation: number; // Enable marking allocation. requiresubmissionstatement: number; // Student must accept submission statement. preventsubmissionnotingroup?: number; // @since 3.2. Prevent submission not in group. submissionstatement?: string; // @since 3.2. Submission statement formatted. submissionstatementformat?: number; // @since 3.2. Submissionstatement format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). configs: AddonModAssignConfig[]; // Configuration settings. intro?: string; // Assignment intro, not allways returned because it deppends on the activity configuration. introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). introfiles?: CoreWSExternalFile[]; // @since 3.2. introattachments?: CoreWSExternalFile[]; }; /** * Config setting in an assign. */ export type AddonModAssignConfig = { id?: number; // Assign_plugin_config id. assignment?: number; // Assignment id. plugin: string; // Plugin. subtype: string; // Subtype. name: string; // Name. value: string; // Value. }; /** * Grade of an assign, returned by mod_assign_get_grades. */ export type AddonModAssignGrade = { id: number; // Grade id. assignment?: number; // Assignment id. userid: number; // Student id. attemptnumber: number; // Attempt number. timecreated: number; // Grade creation time. timemodified: number; // Grade last modified time. grader: number; // Grader, -1 if grader is hidden. grade: string; // Grade. gradefordisplay?: string; // Grade rendered into a format suitable for display. }; /** * Assign submission returned by mod_assign_get_submissions. */ export type AddonModAssignSubmission = { id: number; // Submission id. userid: number; // Student id. attemptnumber: number; // Attempt number. timecreated: number; // Submission creation time. timemodified: number; // Submission last modified time. status: string; // Submission status. groupid: number; // Group id. assignment?: number; // Assignment id. latest?: number; // Latest attempt. plugins?: AddonModAssignPlugin[]; // Plugins. gradingstatus?: string; // @since 3.2. Grading status. }; /** * Assign plugin. */ export type AddonModAssignPlugin = { type: string; // Submission plugin type. name: string; // Submission plugin name. fileareas?: { // Fileareas. area: string; // File area. files?: CoreWSExternalFile[]; }[]; editorfields?: { // Editorfields. name: string; // Field name. description: string; // Field description. text: string; // Field value. format: number; // Text format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). }[]; }; /** * Grading summary of an assign submission. */ export type AddonModAssignSubmissionGradingSummary = { participantcount: number; // Number of users who can submit. submissiondraftscount: number; // Number of submissions in draft status. submissionsenabled: boolean; // Whether submissions are enabled or not. submissionssubmittedcount: number; // Number of submissions in submitted status. submissionsneedgradingcount: number; // Number of submissions that need grading. warnofungroupedusers: string | boolean; // Whether we need to warn people about groups. }; /** * Attempt of an assign submission. */ export type AddonModAssignSubmissionAttempt = { submission?: AddonModAssignSubmission; // Submission info. teamsubmission?: AddonModAssignSubmission; // Submission info. submissiongroup?: number; // The submission group id (for group submissions only). submissiongroupmemberswhoneedtosubmit?: number[]; // List of users who still need to submit (for group submissions only). submissionsenabled: boolean; // Whether submissions are enabled or not. locked: boolean; // Whether new submissions are locked. graded: boolean; // Whether the submission is graded. canedit: boolean; // Whether the user can edit the current submission. caneditowner?: boolean; // @since 3.2. Whether the owner of the submission can edit it. cansubmit: boolean; // Whether the user can submit. extensionduedate: number; // Extension due date. blindmarking: boolean; // Whether blind marking is enabled. gradingstatus: string; // Grading status. usergroups: number[]; // User groups in the course. }; /** * Previous attempt of an assign submission. */ export type AddonModAssignSubmissionPreviousAttempt = { attemptnumber: number; // Attempt number. submission?: AddonModAssignSubmission; // Submission info. grade?: AddonModAssignGrade; // Grade information. feedbackplugins?: AddonModAssignPlugin[]; // Feedback info. }; /** * Feedback of an assign submission. */ export type AddonModAssignSubmissionFeedback = { grade: AddonModAssignGrade; // Grade information. gradefordisplay: string; // Grade rendered into a format suitable for display. gradeddate: number; // The date the user was graded. plugins?: AddonModAssignPlugin[]; // Plugins info. }; /** * Participant returned by mod_assign_list_participants. */ export type AddonModAssignParticipant = { id: number; // ID of the user. username?: string; // The username. firstname?: string; // The first name(s) of the user. lastname?: string; // The family name of the user. fullname: string; // The fullname of the user. email?: string; // Email address. address?: string; // Postal address. phone1?: string; // Phone 1. phone2?: string; // Phone 2. icq?: string; // Icq number. skype?: string; // Skype id. yahoo?: string; // Yahoo id. aim?: string; // Aim id. msn?: string; // Msn number. department?: string; // Department. institution?: string; // Institution. idnumber?: string; // The idnumber of the user. interests?: string; // User interests (separated by commas). firstaccess?: number; // First access to the site (0 if never). lastaccess?: number; // Last access to the site (0 if never). suspended?: boolean; // @since 3.2. Suspend user account, either false to enable user login or true to disable it. description?: string; // User profile description. descriptionformat?: number; // Int format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). city?: string; // Home city of the user. url?: string; // URL of the user. country?: string; // Home country code of the user, such as AU or CZ. profileimageurlsmall?: string; // User image profile URL - small version. profileimageurl?: string; // User image profile URL - big version. customfields?: { // User custom fields (also known as user profile fields). type: string; // The type of the custom field - text field, checkbox... value: string; // The value of the custom field. name: string; // The name of the custom field. shortname: string; // The shortname of the custom field - to be able to build the field class in the code. }[]; preferences?: { // Users preferences. name: string; // The name of the preferences. value: string; // The value of the preference. }[]; recordid?: number; // @since 3.7. Record id. groups?: { // User groups. id: number; // Group id. name: string; // Group name. description: string; // Group description. }[]; roles?: { // User roles. roleid: number; // Role id. name: string; // Role name. shortname: string; // Role shortname. sortorder: number; // Role sortorder. }[]; enrolledcourses?: { // Courses where the user is enrolled - limited by which courses the user is able to see. id: number; // Id of the course. fullname: string; // Fullname of the course. shortname: string; // Shortname of the course. }[]; submitted: boolean; // Have they submitted their assignment. requiregrading: boolean; // Is their submission waiting for grading. grantedextension?: boolean; // @since 3.3. Have they been granted an extension. groupid?: number; // For group assignments this is the group id. groupname?: string; // For group assignments this is the group name. }; /** * Result of WS mod_assign_get_assignments. */ export type AddonModAssignGetAssignmentsResult = { courses: { // List of courses. id: number; // Course id. fullname: string; // Course full name. shortname: string; // Course short name. timemodified: number; // Last time modified. assignments: AddonModAssignAssign[]; // Assignment info. }[]; warnings?: CoreWSExternalWarning[]; }; /** * Result of WS mod_assign_get_user_mappings. */ export type AddonModAssignGetUserMappingsResult = { assignments: { // List of assign user mapping data. assignmentid: number; // Assignment id. mappings: { id: number; // User mapping id. userid: number; // Student id. }[]; }[]; warnings?: CoreWSExternalWarning[]; }; /** * Result of WS mod_assign_get_grades. */ export type AddonModAssignGetGradesResult = { assignments: { // List of assignment grade information. assignmentid: number; // Assignment id. grades: AddonModAssignGrade[]; }[]; warnings?: CoreWSExternalWarning[]; }; /** * Result of WS mod_assign_get_submissions. */ export type AddonModAssignGetSubmissionsResult = { assignments: { // Assignment submissions. assignmentid: number; // Assignment id. submissions: AddonModAssignSubmission[]; }[]; warnings?: CoreWSExternalWarning[]; }; /** * Result of WS mod_assign_get_submission_status. */ export type AddonModAssignGetSubmissionStatusResult = { gradingsummary?: AddonModAssignSubmissionGradingSummary; // Grading information. lastattempt?: AddonModAssignSubmissionAttempt; // Last attempt information. feedback?: AddonModAssignSubmissionFeedback; // Feedback for the last attempt. previousattempts?: AddonModAssignSubmissionPreviousAttempt[]; // List all the previous attempts did by the user. warnings?: CoreWSExternalWarning[]; };