diff --git a/scripts/langindex.json b/scripts/langindex.json index 5c2eb522717..620d2c29978 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -430,8 +430,12 @@ "addon.mod_assign.numwords": "moodle", "addon.mod_assign.outof": "assign", "addon.mod_assign.overdue": "assign", + "addon.mod_assign.removesubmission": "assign", + "addon.mod_assign.removesubmissionconfirm": "assign", + "addon.mod_assign.removesubmissionconfirmwithtimelimit": "assign", "addon.mod_assign.submission": "assign", "addon.mod_assign.submissioneditable": "assign", + "addon.mod_assign.submissionempty": "assign", "addon.mod_assign.submissionnoteditable": "assign", "addon.mod_assign.submissionnotsupported": "local_moodlemobileapp", "addon.mod_assign.submissionslocked": "assign", diff --git a/src/addons/mod/assign/components/index/index.ts b/src/addons/mod/assign/components/index/index.ts index e343bbaff73..eb31d7487f4 100644 --- a/src/addons/mod/assign/components/index/index.ts +++ b/src/addons/mod/assign/components/index/index.ts @@ -47,6 +47,7 @@ import { ADDON_MOD_ASSIGN_GRADED_EVENT, ADDON_MOD_ASSIGN_PAGE_NAME, ADDON_MOD_ASSIGN_STARTED_EVENT, + ADDON_MOD_ASSIGN_SUBMISSION_REMOVED_EVENT, ADDON_MOD_ASSIGN_SUBMISSION_SAVED_EVENT, ADDON_MOD_ASSIGN_SUBMITTED_FOR_GRADING_EVENT, ADDON_MOD_ASSIGN_WARN_GROUPS_OPTIONAL, @@ -126,6 +127,17 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo this.siteId, ); + this.savedObserver = CoreEvents.on( + ADDON_MOD_ASSIGN_SUBMISSION_REMOVED_EVENT, + (data) => { + if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) { + // Assignment submission removed, refresh data. + this.showLoadingAndRefresh(true, false); + } + }, + this.siteId, + ); + this.submittedObserver = CoreEvents.on( ADDON_MOD_ASSIGN_SUBMITTED_FOR_GRADING_EVENT, (data) => { diff --git a/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html b/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html index 0ee574a3fb2..ad7abb28772 100644 --- a/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html +++ b/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html @@ -167,13 +167,20 @@

{{ 'addon.mod_assign.userswhoneedtosubmit' | translate: {$a: ''} }}

- - - {{ 'addon.mod_assign.editsubmission' | translate }} - + +
+ + {{ 'addon.mod_assign.editsubmission' | translate }} + + + {{ 'addon.mod_assign.removesubmission' | translate }} + +
- + {{ 'addon.mod_assign.addsubmission' | translate }} @@ -182,7 +189,7 @@

{{ 'addon.mod_assign.userswhoneedtosubmit' | translate: {$a: ''} }}

- + {{ 'addon.mod_assign.addnewattemptfromprevious' | translate }} @@ -191,12 +198,18 @@

{{ 'addon.mod_assign.userswhoneedtosubmit' | translate: {$a: ''} }}

{{ 'addon.mod_assign.addnewattempt' | translate }}
- - - {{ 'addon.mod_assign.editsubmission' | translate }} - + +
+ + {{ 'addon.mod_assign.editsubmission' | translate }} + + + {{ 'addon.mod_assign.removesubmission' | translate }} + +
diff --git a/src/addons/mod/assign/components/submission/submission.ts b/src/addons/mod/assign/components/submission/submission.ts index f5fdd8df273..1341860e885 100644 --- a/src/addons/mod/assign/components/submission/submission.ts +++ b/src/addons/mod/assign/components/submission/submission.ts @@ -64,6 +64,7 @@ import { ADDON_MOD_ASSIGN_GRADED_EVENT, ADDON_MOD_ASSIGN_MANUAL_SYNCED, ADDON_MOD_ASSIGN_PAGE_NAME, + ADDON_MOD_ASSIGN_SUBMISSION_REMOVED_EVENT, ADDON_MOD_ASSIGN_SUBMITTED_FOR_GRADING_EVENT, ADDON_MOD_ASSIGN_UNLIMITED_ATTEMPTS, } from '../../constants'; @@ -96,8 +97,9 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can isSubmittedForGrading = false; // Whether the submission has been submitted for grading. acceptStatement = false; // Statement accepted (for grading). feedback?: AddonModAssignSubmissionFeedbackFormatted; // The feedback. - hasOffline = false; // Whether there is offline data. + editedOffline = false; // Whether the submission was added or edited in offline. submittedOffline = false; // Whether it was submitted in offline. + removedOffline = false; // Whether the submission was removed in offline. fromDate?: string; // Readable date when the assign started accepting submissions. currentAttempt = 0; // The current attempt number. maxAttemptsText: string; // The text for maximum attempts. @@ -108,6 +110,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can membersToSubmitBlind: number[] = []; // Team members that need to submit the assignment (blindmarking). canSubmit = false; // Whether the user can submit for grading. canEdit = false; // Whether the user can edit the submission. + isRemoveAvailable = false; // Whether WS to remove submission is available. submissionStatement?: string; // The submission statement. showErrorStatementEdit = false; // Whether to show an error in edit due to submission statement. showErrorStatementSubmit = false; // Whether to show an error in submit due to submission statement. @@ -406,6 +409,47 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can ); } + /** + * Remove submisson. + */ + async remove(): Promise { + if (!this.assign || !this.userSubmission) { + return; + } + const message = this.assign?.timelimit ? + 'addon.mod_assign.removesubmissionconfirmwithtimelimit' : + 'addon.mod_assign.removesubmissionconfirm'; + try { + await CoreDomUtils.showDeleteConfirm(message); + } catch { + return; + } + + const modal = await CoreLoadings.show('core.sending', true); + + try { + const sent = await AddonModAssign.removeSubmission(this.assign, this.userSubmission); + + if (sent) { + CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'assign' }); + } + + CoreEvents.trigger( + ADDON_MOD_ASSIGN_SUBMISSION_REMOVED_EVENT, + { + assignmentId: this.assign.id, + submissionId: this.userSubmission.id, + userId: this.currentUserId, + }, + CoreSites.getCurrentSiteId(), + ); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error removing submission.'); + } finally { + modal.dismiss(); + } + } + /** * Check if there's data to save (grade). * @@ -633,13 +677,14 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can try { const submission = await AddonModAssignOffline.getSubmission(this.assign.id, this.submitId); - this.hasOffline = submission && submission.plugindata && Object.keys(submission.plugindata).length > 0; - - this.submittedOffline = !!submission?.submitted; + this.removedOffline = submission && Object.keys(submission.plugindata).length == 0; + this.editedOffline = submission && !this.removedOffline; + this.submittedOffline = !!submission?.submitted && !this.removedOffline; } catch (error) { // No offline data found. - this.hasOffline = false; + this.editedOffline = false; this.submittedOffline = false; + this.removedOffline = false; } } @@ -821,14 +866,14 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can return; } - if (this.hasOffline || this.submittedOffline) { - // Offline data. + if (this.editedOffline || this.submittedOffline) { + // Added, edited or submitted offline. this.statusTranslated = Translate.instant('core.notsent'); this.statusColor = CoreIonicColorNames.WARNING; } else if (!this.assign.teamsubmission) { // Single submission. - if (this.userSubmission && this.userSubmission.status != this.statusNew) { + if (this.userSubmission && this.userSubmission.status != this.statusNew && !this.removedOffline) { this.statusTranslated = Translate.instant('addon.mod_assign.submissionstatus_' + this.userSubmission.status); this.statusColor = AddonModAssign.getSubmissionStatusColor(this.userSubmission.status); } else { @@ -844,10 +889,10 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can } else { // Team submission. - if (!status.lastattempt?.submissiongroup && this.assign.preventsubmissionnotingroup) { + if (!status.lastattempt?.submissiongroup && this.assign.preventsubmissionnotingroup && !this.removedOffline) { this.statusTranslated = Translate.instant('addon.mod_assign.nosubmission'); this.statusColor = AddonModAssign.getSubmissionStatusColor(AddonModAssignSubmissionStatusValues.NO_SUBMISSION); - } else if (this.userSubmission && this.userSubmission.status != this.statusNew) { + } else if (this.userSubmission && this.userSubmission.status != this.statusNew && !this.removedOffline) { this.statusTranslated = Translate.instant('addon.mod_assign.submissionstatus_' + this.userSubmission.status); this.statusColor = AddonModAssign.getSubmissionStatusColor(this.userSubmission.status); } else { @@ -907,7 +952,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can this.courseId, acceptStatement, this.userSubmission.timemodified, - this.hasOffline, + this.editedOffline, ); // Submitted, trigger event. @@ -1142,11 +1187,12 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can this.assign.requiresubmissionstatement = 0; } - this.canSubmit = !this.isSubmittedForGrading && !this.submittedOffline && (lastAttempt.cansubmit || - (this.hasOffline && AddonModAssign.canSubmitOffline(this.assign, submissionStatus))); + this.canSubmit = !this.isSubmittedForGrading && !this.submittedOffline && !this.removedOffline && + (lastAttempt.cansubmit || (this.editedOffline && AddonModAssign.canSubmitOffline(this.assign, submissionStatus))); this.canEdit = !this.isSubmittedForGrading && lastAttempt.canedit && (!this.submittedOffline || !this.assign.submissiondrafts); + this.isRemoveAvailable = AddonModAssign.isRemoveSubmissionAvailable(); // Get submission statement if needed. if (this.assign.requiresubmissionstatement && this.assign.submissiondrafts && this.submitId == this.currentUserId) { diff --git a/src/addons/mod/assign/constants.ts b/src/addons/mod/assign/constants.ts index 52797e8540c..914a1a99805 100644 --- a/src/addons/mod/assign/constants.ts +++ b/src/addons/mod/assign/constants.ts @@ -25,6 +25,7 @@ export const ADDON_MOD_ASSIGN_WARN_GROUPS_OPTIONAL = 'warnoptional'; // Events. export const ADDON_MOD_ASSIGN_SUBMISSION_SAVED_EVENT = 'addon_mod_assign_submission_saved'; +export const ADDON_MOD_ASSIGN_SUBMISSION_REMOVED_EVENT = 'addon_mod_assign_submission_removed'; export const ADDON_MOD_ASSIGN_SUBMITTED_FOR_GRADING_EVENT = 'addon_mod_assign_submitted_for_grading'; export const ADDON_MOD_ASSIGN_GRADED_EVENT = 'addon_mod_assign_graded'; export const ADDON_MOD_ASSIGN_STARTED_EVENT = 'addon_mod_assign_started'; diff --git a/src/addons/mod/assign/lang.json b/src/addons/mod/assign/lang.json index 6806d8ad031..e1e2cf0aa65 100644 --- a/src/addons/mod/assign/lang.json +++ b/src/addons/mod/assign/lang.json @@ -82,8 +82,12 @@ "numwords": "{{$a}} words", "outof": "{{$a.current}} out of {{$a.total}}", "overdue": "Assignment is overdue by: {{$a}}", + "removesubmission": "Remove submission", + "removesubmissionconfirm": "Are you sure you want to remove your submission?", + "removesubmissionconfirmwithtimelimit": "Are you sure you want to remove your submission? Please note that this will not reset your time limit.", "submission": "Submission", "submissioneditable": "Student can edit this submission", + "submissionempty": "Nothing was submitted", "submissionnoteditable": "Student cannot edit this submission", "submissionnotsupported": "This submission is not supported by the app and may not contain all the information.", "submissionslocked": "This assignment is not accepting submissions", diff --git a/src/addons/mod/assign/pages/edit/edit.ts b/src/addons/mod/assign/pages/edit/edit.ts index d11712cd6a9..6b6154c4378 100644 --- a/src/addons/mod/assign/pages/edit/edit.ts +++ b/src/addons/mod/assign/pages/edit/edit.ts @@ -231,15 +231,8 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave { this.timeLimitEndTime = 0; } - try { - // Check if there's any offline data for this submission. - const offlineData = await AddonModAssignOffline.getSubmission(this.assign.id, this.userId); - - this.hasOffline = offlineData?.plugindata && Object.keys(offlineData.plugindata).length > 0; - } catch { - // No offline data found. - this.hasOffline = false; - } + // Check if there's any offline data for this submission. + this.hasOffline = await CoreUtils.promiseWorks(AddonModAssignOffline.getSubmission(this.assign.id, this.userId)); CoreAnalytics.logEvent({ type: CoreAnalyticsEventType.VIEW_ITEM, @@ -398,6 +391,10 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave { throw Translate.instant('addon.mod_assign.acceptsubmissionstatement'); } + if (AddonModAssignHelper.isSubmissionEmptyForEdit(this.assign!, this.userSubmission!, inputData)) { + throw Translate.instant('addon.mod_assign.submissionempty'); + } + let modal = await CoreLoadings.show(); let size = -1; diff --git a/src/addons/mod/assign/services/assign-helper.ts b/src/addons/mod/assign/services/assign-helper.ts index c191c8013dd..a816ecab6de 100644 --- a/src/addons/mod/assign/services/assign-helper.ts +++ b/src/addons/mod/assign/services/assign-helper.ts @@ -81,6 +81,12 @@ export class AddonModAssignHelperProvider { return true; } + if (await CoreUtils.promiseWorks(AddonModAssignOffline.getSubmission(assign.id, submission.userid))) { + // Submission was saved or deleted offline, allow editing it or creating a new one. + return true; + } + + // Submission was created online, check if plugins allow editing it. let canEdit = true; const promises = submission.plugins @@ -237,6 +243,31 @@ export class AddonModAssignHelperProvider { return true; } + /** + * Check whether the edited submission has no content. + * + * @param assign Assignment object. + * @param submission Submission to inspect. + * @param inputData Data entered in the submission form. + * @returns Whether the submission is empty. + */ + isSubmissionEmptyForEdit( + assign: AddonModAssignAssign, + submission: AddonModAssignSubmission, + inputData: CoreFormFields, + ): boolean { + const anyNotEmpty = submission.plugins?.some((plugin) => + !AddonModAssignSubmissionDelegate.isPluginEmptyForEdit(assign, plugin, inputData)); + + // If any plugin is not empty, we consider that the submission is not empty either. + if (anyNotEmpty) { + return false; + } + + // If all the plugins were empty (or there were no plugins), we consider the submission to be empty. + return true; + } + /** * List the participants for a single assignment, with some summary info about their submissions. * @@ -467,7 +498,7 @@ export class AddonModAssignHelperProvider { submission.manyGroups = !!participant.groups && participant.groups.length > 1; submission.noGroups = !!participant.groups && participant.groups.length == 0; if (participant.groupname) { - submission.groupid = participant.groupid; + submission.groupid = participant.groupid ?? 0; submission.groupname = participant.groupname; } @@ -724,18 +755,15 @@ export const AddonModAssignHelper = makeSingleton(AddonModAssignHelperProvider); /** * Assign submission with some calculated data. */ -export type AddonModAssignSubmissionFormatted = - Omit & { - userid?: number; // Student id. - groupid?: number; // Group id. - blindid?: number; // Calculated in the app. Blindid of the user that did the submission. - submitid?: number; // Calculated in the app. Userid or blindid of the user that did the submission. - userfullname?: string; // Calculated in the app. Full name of the user that did the submission. - userprofileimageurl?: string; // Calculated in the app. Avatar of the user that did the submission. - manyGroups?: boolean; // Calculated in the app. Whether the user belongs to more than 1 group. - noGroups?: boolean; // Calculated in the app. Whether the user doesn't belong to any group. - groupname?: string; // Calculated in the app. Name of the group the submission belongs to. - }; +export interface AddonModAssignSubmissionFormatted extends AddonModAssignSubmission { + blindid?: number; // Calculated in the app. Blindid of the user that did the submission. + submitid?: number; // Calculated in the app. Userid or blindid of the user that did the submission. + userfullname?: string; // Calculated in the app. Full name of the user that did the submission. + userprofileimageurl?: string; // Calculated in the app. Avatar of the user that did the submission. + manyGroups?: boolean; // Calculated in the app. Whether the user belongs to more than 1 group. + noGroups?: boolean; // Calculated in the app. Whether the user doesn't belong to any group. + groupname?: string; // Calculated in the app. Name of the group the submission belongs to. +} /** * Assignment plugin config. diff --git a/src/addons/mod/assign/services/assign-sync.ts b/src/addons/mod/assign/services/assign-sync.ts index 19137968e20..063e3f1cdb3 100644 --- a/src/addons/mod/assign/services/assign-sync.ts +++ b/src/addons/mod/assign/services/assign-sync.ts @@ -331,27 +331,29 @@ export class AddonModAssignSyncProvider extends CoreCourseActivitySyncBaseProvid } try { - if (submission?.plugins) { - // Prepare plugins data. - await Promise.all(submission.plugins.map((plugin) => - AddonModAssignSubmissionDelegate.preparePluginSyncData( - assign, - submission, - plugin, - offlineData, - pluginData, - siteId, - ))); - } + if (Object.keys(offlineData.plugindata).length == 0) { + await AddonModAssign.removeSubmissionOnline(assign.id, offlineData.userid, siteId); + } else { + if (submission?.plugins) { + // Prepare plugins data. + await Promise.all(submission.plugins.map((plugin) => + AddonModAssignSubmissionDelegate.preparePluginSyncData( + assign, + submission, + plugin, + offlineData, + pluginData, + siteId, + ))); + } - // Now save the submission. - if (Object.keys(pluginData).length > 0) { + // Now save the submission. await AddonModAssign.saveSubmissionOnline(assign.id, pluginData, siteId); - } - if (assign.submissiondrafts && offlineData.submitted) { - // The user submitted the assign manually. Submit it for grading. - await AddonModAssign.submitForGradingOnline(assign.id, !!offlineData.submissionstatement, siteId); + if (assign.submissiondrafts && offlineData.submitted) { + // The user submitted the assign manually. Submit it for grading. + await AddonModAssign.submitForGradingOnline(assign.id, !!offlineData.submissionstatement, siteId); + } } // Submission data sent, update cached data. No need to block the user for this. diff --git a/src/addons/mod/assign/services/assign.ts b/src/addons/mod/assign/services/assign.ts index 976ca8abed3..a4d5216cd36 100644 --- a/src/addons/mod/assign/services/assign.ts +++ b/src/addons/mod/assign/services/assign.ts @@ -42,6 +42,7 @@ import { ADDON_MOD_ASSIGN_GRADED_EVENT, ADDON_MOD_ASSIGN_MANUAL_SYNCED, ADDON_MOD_ASSIGN_STARTED_EVENT, + ADDON_MOD_ASSIGN_SUBMISSION_REMOVED_EVENT, ADDON_MOD_ASSIGN_SUBMISSION_SAVED_EVENT, ADDON_MOD_ASSIGN_SUBMITTED_FOR_GRADING_EVENT, } from '../constants'; @@ -55,6 +56,7 @@ declare module '@singletons/events' { */ export interface CoreEventsData { [ADDON_MOD_ASSIGN_SUBMISSION_SAVED_EVENT]: AddonModAssignSubmissionSavedEventData; + [ADDON_MOD_ASSIGN_SUBMISSION_REMOVED_EVENT]: AddonModAssignSubmissionRemovedEventData; [ADDON_MOD_ASSIGN_SUBMITTED_FOR_GRADING_EVENT]: AddonModAssignSubmittedForGradingEventData; [ADDON_MOD_ASSIGN_GRADED_EVENT]: AddonModAssignGradedEventData; [ADDON_MOD_ASSIGN_STARTED_EVENT]: AddonModAssignStartedEventData; @@ -1314,6 +1316,125 @@ export class AddonModAssignProvider { } } + /** + * Remove the assignment submission of a user. + * + * @param assign Assign. + * @param submission Last online submission. + * @param siteId Site ID. If not defined, current site. + * @returns Promise resolved with true if sent to server, resolved with false if stored in offline. + * @since 4.5 + */ + async removeSubmission( + assign: AddonModAssignAssign, + submission: AddonModAssignSubmission, + siteId?: string, + ): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + // Function to store the submission to be synchronized later. + const storeOffline = async (): Promise => { + await AddonModAssignOffline.saveSubmission( + assign.id, + assign.course, + {}, + submission.timemodified, + !!assign.submissiondrafts, + submission.userid, + siteId, + ); + + return false; + }; + + if (submission.status === AddonModAssignSubmissionStatusValues.NEW || + submission.status == AddonModAssignSubmissionStatusValues.REOPENED) { + // The submission was created offline and not synced, just delete the offline submission. + await AddonModAssignOffline.deleteSubmission(assign.id, submission.userid, siteId); + + return false; + } + + if (!CoreNetwork.isOnline()) { + // App is offline, store the action. + return storeOffline(); + } + + try { + // If there's an offline submission, discard it first. + const offlineData = await AddonModAssignOffline.getSubmission(assign.id, submission.userid, siteId); + + if (offlineData) { + if (submission.plugins) { + // Delete all plugin data. + await Promise.all(submission.plugins.map((plugin) => + AddonModAssignSubmissionDelegate.deletePluginOfflineData( + assign, + submission, + plugin, + offlineData, + siteId, + ))); + } + + await AddonModAssignOffline.deleteSubmission(assign.id, submission.userid, siteId); + } + + await this.removeSubmissionOnline(assign.id, submission.userid, siteId); + + return true; + } catch (error) { + if (error && !CoreUtils.isWebServiceError(error)) { + // Couldn't connect to server, store in offline. + return storeOffline(); + } else { + // The WebService has thrown an error or offline not supported, reject. + throw error; + } + } + } + + /** + * Remove the assignment submission of a user. + * + * @param assignId Assign ID. + * @param userId User ID. If not defined, current user. + * @param siteId Site ID. If not defined, current site. + * @returns Promise resolved when submitted, rejected otherwise. + * @since 4.5 + */ + async removeSubmissionOnline(assignId: number, userId?: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + userId = userId || site.getUserId(); + + const params: AddonModAssignRemoveSubmissionWSParams = { + assignid: assignId, + userid: userId, + }; + const result = await site.write('mod_assign_remove_submission', params); + + if (!result.status) { + if (result.warnings?.length) { + throw new CoreWSError(result.warnings[0]); + } else { + throw new CoreError('Error removing assignment submission.'); + } + } + } + + /** + * Returns whether or not remove submission WS available or not. + * + * @param site Site. If not defined, current site. + * @returns If WS is available. + * @since 4.5 + */ + isRemoveSubmissionAvailable(site?: CoreSite): boolean { + site = site ?? CoreSites.getRequiredCurrentSite(); + + return site.wsAvailable('mod_assign_remove_submission'); + } + } export const AddonModAssign = makeSingleton(AddonModAssignProvider); @@ -1755,6 +1876,22 @@ type AddonModAssignStartSubmissionWSParams = { assignid: number; // Assignment instance id. }; +/** + * Params of mod_assign_remove_submission WS. + */ +type AddonModAssignRemoveSubmissionWSParams = { + userid: number; // User id. + assignid: number; // Assignment instance id. +}; + +/** + * Data returned by mod_assign_remove_submission WS. + */ +export type AddonModAssignRemoveSubmissionWSResponse = { + status: boolean; + warnings?: CoreWSExternalWarning[]; +}; + /** * Data returned by mod_assign_start_submission WS. * @@ -1784,6 +1921,11 @@ export type AddonModAssignSubmittedForGradingEventData = { */ export type AddonModAssignSubmissionSavedEventData = AddonModAssignSubmittedForGradingEventData; +/** + * Data sent by SUBMISSION_REMOVED_EVENT event. + */ +export type AddonModAssignSubmissionRemovedEventData = AddonModAssignSubmittedForGradingEventData; + /** * Data sent by GRADED_EVENT event. */ diff --git a/src/addons/mod/assign/services/handlers/default-submission.ts b/src/addons/mod/assign/services/handlers/default-submission.ts index e78516b5731..2365c122e54 100644 --- a/src/addons/mod/assign/services/handlers/default-submission.ts +++ b/src/addons/mod/assign/services/handlers/default-submission.ts @@ -50,6 +50,17 @@ export class AddonModAssignDefaultSubmissionHandler implements AddonModAssignSub return true; } + /** + * @inheritdoc + */ + isEmptyForEdit( + assign: AddonModAssignAssign, // eslint-disable-line @typescript-eslint/no-unused-vars + plugin: AddonModAssignPlugin, // eslint-disable-line @typescript-eslint/no-unused-vars + inputData: CoreFormFields, // eslint-disable-line @typescript-eslint/no-unused-vars + ): boolean { + return true; + } + /** * @inheritdoc */ diff --git a/src/addons/mod/assign/services/submission-delegate.ts b/src/addons/mod/assign/services/submission-delegate.ts index 872fe75f1ac..f50e718fc10 100644 --- a/src/addons/mod/assign/services/submission-delegate.ts +++ b/src/addons/mod/assign/services/submission-delegate.ts @@ -62,6 +62,20 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { plugin: AddonModAssignPlugin, ): boolean; + /** + * Check if a plugin has no data in the edit form. + * + * @param assign The assignment. + * @param plugin The plugin object. + * @param inputData Data entered by the user for the submission. + * @returns Whether the plugin is empty. + */ + isEmptyForEdit?( + assign: AddonModAssignAssign, + plugin: AddonModAssignPlugin, + inputData: CoreFormFields, + ): boolean; + /** * Should clear temporary data for a cancelled submission. * @@ -502,6 +516,22 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegatefilesData.plugindata.files_filemanager; - // It has offline data. - let offlineFiles: FileEntry[] = []; - if (offlineDataFiles.offline) { - offlineFiles = await CoreUtils.ignoreErrors( - AddonModAssignHelper.getStoredSubmissionFiles( - this.assign.id, - AddonModAssignSubmissionFileHandlerService.FOLDER_NAME, - ), - [], - ); - } + if (offlineData) { + // Offline submission, get files if submission is not removed. + if (offlineData.plugindata.files_filemanager) { + const offlineDataFiles = offlineData.plugindata.files_filemanager; + // It has offline data. + let offlineFiles: FileEntry[] = []; + if (offlineDataFiles.offline) { + offlineFiles = await CoreUtils.ignoreErrors( + AddonModAssignHelper.getStoredSubmissionFiles( + this.assign.id, + AddonModAssignSubmissionFileHandlerService.FOLDER_NAME, + ), + [], + ); + } - this.files = offlineDataFiles.online || []; - this.files = this.files.concat(offlineFiles); + this.files = offlineDataFiles.online || []; + this.files = this.files.concat(offlineFiles); + } } else { // No offline data, get the online files. this.files = AddonModAssign.getSubmissionPluginAttachments(this.plugin); diff --git a/src/addons/mod/assign/submission/file/services/handler.ts b/src/addons/mod/assign/submission/file/services/handler.ts index 595572d4d7e..838c26a1eeb 100644 --- a/src/addons/mod/assign/submission/file/services/handler.ts +++ b/src/addons/mod/assign/submission/file/services/handler.ts @@ -61,6 +61,15 @@ export class AddonModAssignSubmissionFileHandlerService implements AddonModAssig return files.length === 0; } + /** + * @inheritdoc + */ + isEmptyForEdit(assign: AddonModAssignAssign): boolean { + const currentFiles = CoreFileSession.getFiles(ADDON_MOD_ASSIGN_COMPONENT, assign.id); + + return currentFiles.length == 0; + } + /** * @inheritdoc */ diff --git a/src/addons/mod/assign/submission/onlinetext/component/onlinetext.ts b/src/addons/mod/assign/submission/onlinetext/component/onlinetext.ts index 47760672d7f..04447b4d330 100644 --- a/src/addons/mod/assign/submission/onlinetext/component/onlinetext.ts +++ b/src/addons/mod/assign/submission/onlinetext/component/onlinetext.ts @@ -69,8 +69,11 @@ export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignS this.wordLimit = parseInt(this.configs?.wordlimit || '0'); try { - if (offlineData && offlineData.plugindata && offlineData.plugindata.onlinetext_editor) { - this.text = (offlineData.plugindata).onlinetext_editor.text; + if (offlineData && offlineData.plugindata) { + // Offline submission, get text if submission is not removed. + if (offlineData.plugindata.onlinetext_editor) { + this.text = (offlineData.plugindata).onlinetext_editor.text; + } } else { // No offline data found, return online text. this.text = AddonModAssign.getSubmissionPluginText(this.plugin); diff --git a/src/addons/mod/assign/submission/onlinetext/services/handler.ts b/src/addons/mod/assign/submission/onlinetext/services/handler.ts index ac88ded41bb..73a5b318d94 100644 --- a/src/addons/mod/assign/submission/onlinetext/services/handler.ts +++ b/src/addons/mod/assign/submission/onlinetext/services/handler.ts @@ -58,6 +58,29 @@ export class AddonModAssignSubmissionOnlineTextHandlerService implements AddonMo return text.trim().length === 0; } + /** + * @inheritdoc + */ + isEmptyForEdit( + assign: AddonModAssignAssign, + plugin: AddonModAssignPlugin, + inputData: AddonModAssignSubmissionOnlineTextData, + ): boolean { + const text = this.getTextToSubmit(plugin, inputData); + + if (CoreText.countWords(text) > 0) { + return false; + } + + // Check if the online text submission contains video, audio or image elements + // that can be ignored and stripped by count_words(). + if (/<\s*((video|audio)[^>]*>(.*?)<\s*\/\s*(video|audio)>)|(img[^>]*>)/.test(text)) { + return false; + } + + return true; + } + /** * @inheritdoc */ diff --git a/src/addons/mod/assign/tests/behat/basic_usage.feature b/src/addons/mod/assign/tests/behat/basic_usage.feature index 94bce17794b..9cbf92c1702 100755 --- a/src/addons/mod/assign/tests/behat/basic_usage.feature +++ b/src/addons/mod/assign/tests/behat/basic_usage.feature @@ -120,6 +120,17 @@ Feature: Test basic usage of assignment activity in app Then I should find "Online text submissions" in the app And I should find "Submission test 2nd attempt" in the app + @lms_from4.5 + Scenario: Remove submission (online text) + Given I entered the assign activity "assignment1" on course "Course 1" as "student1" in the app + And I press "Add submission" in the app + And I set the field "Online text submissions" to "Submission test" in the app + And I press "Save" in the app + + When I press "Remove submission" in the app + And I press "DELETE" in the app + Then I should find "No attempt" in the app + Scenario: Add submission offline (online text) & Submit for grading offline & Sync submissions Given I entered the assign activity "assignment1" on course "Course 1" as "student1" in the app When I press "Add submission" in the app @@ -164,3 +175,73 @@ Feature: Test basic usage of assignment activity in app Then I should find "Submitted for grading" in the app And I should find "Submission test edited offline" in the app But I should not find "This Assignment has offline data to be synchronised." in the app + + @lms_from4.5 + Scenario: Remove submission offline and syncrhonize it + Given I entered the assign activity "assignment1" on course "Course 1" as "student1" in the app + And I press "Add submission" in the app + And I set the field "Online text submissions" to "Submission test" in the app + And I press "Save" in the app + Then I should find "Draft (not submitted)" in the app + + # Remove submission added online. + When I switch network connection to offline + And I press "Remove submission" in the app + And I press "DELETE" in the app + Then I should find "No attempt" in the app + And I should find "This Assignment has offline data to be synchronised." in the app + + # Synchronize submission removal. + When I switch network connection to wifi + And I press the back button in the app + And I press "assignment1" in the app + Then I should find "No attempt" in the app + But I should not find "This Assignment has offline data to be synchronised." in the app + + # Remove submission added offline (while offline) + Given I press "Add submission" in the app + And I set the field "Online text submissions" to "Submission test offline" in the app + And I switch network connection to offline + And I press "Save" in the app + + When I press "Remove submission" in the app + And I press "DELETE" in the app + Then I should find "No attempt" in the app + But I should not find "This Assignment has offline data to be synchronised." in the app + + # Remove submission added offline (while online before synchronising) + Given I press "Add submission" in the app + And I set the field "Online text submissions" to "Submission test offline" in the app + And I switch network connection to offline + And I press "Save" in the app + And I switch network connection to wifi + + When I press "Remove submission" in the app + And I press "DELETE" in the app + Then I should find "No attempt" in the app + But I should not find "This Assignment has offline data to be synchronised." in the app + + @lms_from4.5 + Scenario: Add submission offline after removing a submission offline + Given I entered the assign activity "assignment1" on course "Course 1" as "student1" in the app + When I press "Add submission" in the app + And I set the field "Online text submissions" to "Submission test online" in the app + And I press "Save" in the app + And I switch network connection to offline + And I press "Remove submission" in the app + And I press "DELETE" in the app + Then I should find "This Assignment has offline data to be synchronised." in the app + And I should find "No attempt" in the app + + When I press "Add submission" in the app + And I set the field "Online text submissions" to "Submission test offline" in the app + And I press "Save" in the app + And I press "OK" in the app + Then I should find "This Assignment has offline data to be synchronised." in the app + And I should find "Submission test offline" in the app + + When I switch network connection to wifi + And I go back in the app + And I press "assignment1" in the app + Then I should find "Submission test offline" in the app + But I should not find "This Assignment has offline data to be synchronised." in the app