MOBILE-3893 assign: Add button to remove submissions

main
Albert Gasset 2024-10-04 12:16:41 +02:00
parent a6076d3f42
commit 588df2dd9d
13 changed files with 389 additions and 84 deletions

View File

@ -430,6 +430,9 @@
"addon.mod_assign.numwords": "moodle", "addon.mod_assign.numwords": "moodle",
"addon.mod_assign.outof": "assign", "addon.mod_assign.outof": "assign",
"addon.mod_assign.overdue": "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.submission": "assign",
"addon.mod_assign.submissioneditable": "assign", "addon.mod_assign.submissioneditable": "assign",
"addon.mod_assign.submissionempty": "assign", "addon.mod_assign.submissionempty": "assign",

View File

@ -47,6 +47,7 @@ import {
ADDON_MOD_ASSIGN_GRADED_EVENT, ADDON_MOD_ASSIGN_GRADED_EVENT,
ADDON_MOD_ASSIGN_PAGE_NAME, ADDON_MOD_ASSIGN_PAGE_NAME,
ADDON_MOD_ASSIGN_STARTED_EVENT, ADDON_MOD_ASSIGN_STARTED_EVENT,
ADDON_MOD_ASSIGN_SUBMISSION_REMOVED_EVENT,
ADDON_MOD_ASSIGN_SUBMISSION_SAVED_EVENT, ADDON_MOD_ASSIGN_SUBMISSION_SAVED_EVENT,
ADDON_MOD_ASSIGN_SUBMITTED_FOR_GRADING_EVENT, ADDON_MOD_ASSIGN_SUBMITTED_FOR_GRADING_EVENT,
ADDON_MOD_ASSIGN_WARN_GROUPS_OPTIONAL, ADDON_MOD_ASSIGN_WARN_GROUPS_OPTIONAL,
@ -126,6 +127,17 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
this.siteId, 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( this.submittedObserver = CoreEvents.on(
ADDON_MOD_ASSIGN_SUBMITTED_FOR_GRADING_EVENT, ADDON_MOD_ASSIGN_SUBMITTED_FOR_GRADING_EVENT,
(data) => { (data) => {

View File

@ -167,13 +167,20 @@
<div class="list-item-limited-width" *ngIf="canEdit || canSubmit"> <div class="list-item-limited-width" *ngIf="canEdit || canSubmit">
<ng-container *ngIf="canEdit"> <ng-container *ngIf="canEdit">
<ng-container *ngIf=" !unsupportedEditPlugins.length && !showErrorStatementEdit"> <ng-container *ngIf=" !unsupportedEditPlugins.length && !showErrorStatementEdit">
<!-- If has offline data, show edit. --> <!-- If has offline data, show edit and remove. -->
<ion-button expand="block" class="ion-text-wrap" *ngIf="hasOffline" (click)="goToEdit()"> <div *ngIf="editedOffline" class="adaptable-buttons-row">
{{ 'addon.mod_assign.editsubmission' | translate }} <ion-button expand="block" class="ion-margin ion-text-wrap" (click)="goToEdit()">
</ion-button> {{ 'addon.mod_assign.editsubmission' | translate }}
</ion-button>
<ion-button *ngIf="isRemoveAvailable" expand="block" class="ion-margin ion-text-wrap"
(click)="remove()">
{{ 'addon.mod_assign.removesubmission' | translate }}
</ion-button>
</div>
<!-- If no submission or is new, show add submission. --> <!-- If no submission or is new, show add submission. -->
<ion-button expand="block" class="ion-text-wrap" (click)="goToEdit()" *ngIf="!hasOffline && <ion-button expand="block" class="ion-text-wrap" (click)="goToEdit()" *ngIf="!editedOffline &&
(!userSubmission || !userSubmission!.status || userSubmission!.status === statusNew)"> (removedOffline || !userSubmission || !userSubmission!.status ||
userSubmission!.status === statusNew)">
<ng-container *ngIf="!assign?.timelimit || userSubmission?.timestarted"> <ng-container *ngIf="!assign?.timelimit || userSubmission?.timestarted">
{{ 'addon.mod_assign.addsubmission' | translate }} {{ 'addon.mod_assign.addsubmission' | translate }}
</ng-container> </ng-container>
@ -182,7 +189,7 @@
</ng-container> </ng-container>
</ion-button> </ion-button>
<!-- If reopened, show addfromprevious and addnewattempt. --> <!-- If reopened, show addfromprevious and addnewattempt. -->
<ng-container *ngIf="!hasOffline && userSubmission?.status === statusReopened"> <ng-container *ngIf="!editedOffline && !removedOffline && userSubmission?.status === statusReopened">
<ion-button *ngIf="!isPreviousAttemptEmpty" expand="block" class="ion-text-wrap" <ion-button *ngIf="!isPreviousAttemptEmpty" expand="block" class="ion-text-wrap"
(click)="copyPrevious()"> (click)="copyPrevious()">
{{ 'addon.mod_assign.addnewattemptfromprevious' | translate }} {{ 'addon.mod_assign.addnewattemptfromprevious' | translate }}
@ -191,12 +198,18 @@
{{ 'addon.mod_assign.addnewattempt' | translate }} {{ 'addon.mod_assign.addnewattempt' | translate }}
</ion-button> </ion-button>
</ng-container> </ng-container>
<!-- Else show editsubmission. --> <!-- Else show editsubmission and removesubmission. -->
<ion-button expand="block" class="ion-text-wrap" *ngIf="!hasOffline && userSubmission && <div *ngIf="!editedOffline && !removedOffline && userSubmission && userSubmission!.status
userSubmission!.status && userSubmission!.status !== statusNew && && userSubmission!.status !== statusNew && userSubmission!.status !== statusReopened"
userSubmission!.status !== statusReopened" (click)="goToEdit()"> class="adaptable-buttons-row">
{{ 'addon.mod_assign.editsubmission' | translate }} <ion-button expand="block" class="ion-margin ion-text-wrap" (click)="goToEdit()">
</ion-button> {{ 'addon.mod_assign.editsubmission' | translate }}
</ion-button>
<ion-button *ngIf="isRemoveAvailable" expand="block" class="ion-margin ion-text-wrap"
(click)="remove()">
{{ 'addon.mod_assign.removesubmission' | translate }}
</ion-button>
</div>
</ng-container> </ng-container>
<ion-item class="core-danger-item ion-text-wrap" <ion-item class="core-danger-item ion-text-wrap"
*ngIf="(unsupportedEditPlugins.length && !showErrorStatementEdit)|| showErrorStatementEdit"> *ngIf="(unsupportedEditPlugins.length && !showErrorStatementEdit)|| showErrorStatementEdit">

View File

@ -64,6 +64,7 @@ import {
ADDON_MOD_ASSIGN_GRADED_EVENT, ADDON_MOD_ASSIGN_GRADED_EVENT,
ADDON_MOD_ASSIGN_MANUAL_SYNCED, ADDON_MOD_ASSIGN_MANUAL_SYNCED,
ADDON_MOD_ASSIGN_PAGE_NAME, ADDON_MOD_ASSIGN_PAGE_NAME,
ADDON_MOD_ASSIGN_SUBMISSION_REMOVED_EVENT,
ADDON_MOD_ASSIGN_SUBMITTED_FOR_GRADING_EVENT, ADDON_MOD_ASSIGN_SUBMITTED_FOR_GRADING_EVENT,
ADDON_MOD_ASSIGN_UNLIMITED_ATTEMPTS, ADDON_MOD_ASSIGN_UNLIMITED_ATTEMPTS,
} from '../../constants'; } from '../../constants';
@ -96,8 +97,9 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
isSubmittedForGrading = false; // Whether the submission has been submitted for grading. isSubmittedForGrading = false; // Whether the submission has been submitted for grading.
acceptStatement = false; // Statement accepted (for grading). acceptStatement = false; // Statement accepted (for grading).
feedback?: AddonModAssignSubmissionFeedbackFormatted; // The feedback. 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. 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. fromDate?: string; // Readable date when the assign started accepting submissions.
currentAttempt = 0; // The current attempt number. currentAttempt = 0; // The current attempt number.
maxAttemptsText: string; // The text for maximum attempts. 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). membersToSubmitBlind: number[] = []; // Team members that need to submit the assignment (blindmarking).
canSubmit = false; // Whether the user can submit for grading. canSubmit = false; // Whether the user can submit for grading.
canEdit = false; // Whether the user can edit the submission. canEdit = false; // Whether the user can edit the submission.
isRemoveAvailable = false; // Whether WS to remove submission is available.
submissionStatement?: string; // The submission statement. submissionStatement?: string; // The submission statement.
showErrorStatementEdit = false; // Whether to show an error in edit due to 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. 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<void> {
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). * Check if there's data to save (grade).
* *
@ -633,13 +677,14 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
try { try {
const submission = await AddonModAssignOffline.getSubmission(this.assign.id, this.submitId); const submission = await AddonModAssignOffline.getSubmission(this.assign.id, this.submitId);
this.hasOffline = submission && submission.plugindata && Object.keys(submission.plugindata).length > 0; this.removedOffline = submission && Object.keys(submission.plugindata).length == 0;
this.editedOffline = submission && !this.removedOffline;
this.submittedOffline = !!submission?.submitted; this.submittedOffline = !!submission?.submitted && !this.removedOffline;
} catch (error) { } catch (error) {
// No offline data found. // No offline data found.
this.hasOffline = false; this.editedOffline = false;
this.submittedOffline = false; this.submittedOffline = false;
this.removedOffline = false;
} }
} }
@ -821,14 +866,14 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
return; return;
} }
if (this.hasOffline || this.submittedOffline) { if (this.editedOffline || this.submittedOffline) {
// Offline data. // Added, edited or submitted offline.
this.statusTranslated = Translate.instant('core.notsent'); this.statusTranslated = Translate.instant('core.notsent');
this.statusColor = CoreIonicColorNames.WARNING; this.statusColor = CoreIonicColorNames.WARNING;
} else if (!this.assign.teamsubmission) { } else if (!this.assign.teamsubmission) {
// Single submission. // 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.statusTranslated = Translate.instant('addon.mod_assign.submissionstatus_' + this.userSubmission.status);
this.statusColor = AddonModAssign.getSubmissionStatusColor(this.userSubmission.status); this.statusColor = AddonModAssign.getSubmissionStatusColor(this.userSubmission.status);
} else { } else {
@ -844,10 +889,10 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
} else { } else {
// Team submission. // 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.statusTranslated = Translate.instant('addon.mod_assign.nosubmission');
this.statusColor = AddonModAssign.getSubmissionStatusColor(AddonModAssignSubmissionStatusValues.NO_SUBMISSION); 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.statusTranslated = Translate.instant('addon.mod_assign.submissionstatus_' + this.userSubmission.status);
this.statusColor = AddonModAssign.getSubmissionStatusColor(this.userSubmission.status); this.statusColor = AddonModAssign.getSubmissionStatusColor(this.userSubmission.status);
} else { } else {
@ -907,7 +952,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
this.courseId, this.courseId,
acceptStatement, acceptStatement,
this.userSubmission.timemodified, this.userSubmission.timemodified,
this.hasOffline, this.editedOffline,
); );
// Submitted, trigger event. // Submitted, trigger event.
@ -1142,11 +1187,12 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
this.assign.requiresubmissionstatement = 0; this.assign.requiresubmissionstatement = 0;
} }
this.canSubmit = !this.isSubmittedForGrading && !this.submittedOffline && (lastAttempt.cansubmit || this.canSubmit = !this.isSubmittedForGrading && !this.submittedOffline && !this.removedOffline &&
(this.hasOffline && AddonModAssign.canSubmitOffline(this.assign, submissionStatus))); (lastAttempt.cansubmit || (this.editedOffline && AddonModAssign.canSubmitOffline(this.assign, submissionStatus)));
this.canEdit = !this.isSubmittedForGrading && lastAttempt.canedit && this.canEdit = !this.isSubmittedForGrading && lastAttempt.canedit &&
(!this.submittedOffline || !this.assign.submissiondrafts); (!this.submittedOffline || !this.assign.submissiondrafts);
this.isRemoveAvailable = AddonModAssign.isRemoveSubmissionAvailable();
// Get submission statement if needed. // Get submission statement if needed.
if (this.assign.requiresubmissionstatement && this.assign.submissiondrafts && this.submitId == this.currentUserId) { if (this.assign.requiresubmissionstatement && this.assign.submissiondrafts && this.submitId == this.currentUserId) {

View File

@ -25,6 +25,7 @@ export const ADDON_MOD_ASSIGN_WARN_GROUPS_OPTIONAL = 'warnoptional';
// Events. // Events.
export const ADDON_MOD_ASSIGN_SUBMISSION_SAVED_EVENT = 'addon_mod_assign_submission_saved'; 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_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_GRADED_EVENT = 'addon_mod_assign_graded';
export const ADDON_MOD_ASSIGN_STARTED_EVENT = 'addon_mod_assign_started'; export const ADDON_MOD_ASSIGN_STARTED_EVENT = 'addon_mod_assign_started';

View File

@ -82,6 +82,9 @@
"numwords": "{{$a}} words", "numwords": "{{$a}} words",
"outof": "{{$a.current}} out of {{$a.total}}", "outof": "{{$a.current}} out of {{$a.total}}",
"overdue": "Assignment is overdue by: {{$a}}", "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", "submission": "Submission",
"submissioneditable": "Student can edit this submission", "submissioneditable": "Student can edit this submission",
"submissionempty": "Nothing was submitted", "submissionempty": "Nothing was submitted",

View File

@ -231,15 +231,8 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
this.timeLimitEndTime = 0; this.timeLimitEndTime = 0;
} }
try { // Check if there's any offline data for this submission.
// Check if there's any offline data for this submission. this.hasOffline = await CoreUtils.promiseWorks(AddonModAssignOffline.getSubmission(this.assign.id, this.userId));
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;
}
CoreAnalytics.logEvent({ CoreAnalytics.logEvent({
type: CoreAnalyticsEventType.VIEW_ITEM, type: CoreAnalyticsEventType.VIEW_ITEM,

View File

@ -81,6 +81,12 @@ export class AddonModAssignHelperProvider {
return true; 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; let canEdit = true;
const promises = submission.plugins const promises = submission.plugins
@ -492,7 +498,7 @@ export class AddonModAssignHelperProvider {
submission.manyGroups = !!participant.groups && participant.groups.length > 1; submission.manyGroups = !!participant.groups && participant.groups.length > 1;
submission.noGroups = !!participant.groups && participant.groups.length == 0; submission.noGroups = !!participant.groups && participant.groups.length == 0;
if (participant.groupname) { if (participant.groupname) {
submission.groupid = participant.groupid; submission.groupid = participant.groupid ?? 0;
submission.groupname = participant.groupname; submission.groupname = participant.groupname;
} }
@ -749,18 +755,15 @@ export const AddonModAssignHelper = makeSingleton(AddonModAssignHelperProvider);
/** /**
* Assign submission with some calculated data. * Assign submission with some calculated data.
*/ */
export type AddonModAssignSubmissionFormatted = export interface AddonModAssignSubmissionFormatted extends AddonModAssignSubmission {
Omit<AddonModAssignSubmission, 'userid'|'groupid'> & { blindid?: number; // Calculated in the app. Blindid of the user that did the submission.
userid?: number; // Student id. submitid?: number; // Calculated in the app. Userid or blindid of the user that did the submission.
groupid?: number; // Group id. userfullname?: string; // Calculated in the app. Full name of the user that did the submission.
blindid?: number; // Calculated in the app. Blindid of the user that did the submission. userprofileimageurl?: string; // Calculated in the app. Avatar of the user that did the submission.
submitid?: number; // Calculated in the app. Userid or blindid of the user that did the submission. manyGroups?: boolean; // Calculated in the app. Whether the user belongs to more than 1 group.
userfullname?: string; // Calculated in the app. Full name of the user that did the submission. noGroups?: boolean; // Calculated in the app. Whether the user doesn't belong to any group.
userprofileimageurl?: string; // Calculated in the app. Avatar of the user that did the submission. groupname?: string; // Calculated in the app. Name of the group the submission belongs to.
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. * Assignment plugin config.

View File

@ -331,27 +331,29 @@ export class AddonModAssignSyncProvider extends CoreCourseActivitySyncBaseProvid
} }
try { try {
if (submission?.plugins) { if (Object.keys(offlineData.plugindata).length == 0) {
// Prepare plugins data. await AddonModAssign.removeSubmissionOnline(assign.id, offlineData.userid, siteId);
await Promise.all(submission.plugins.map((plugin) => } else {
AddonModAssignSubmissionDelegate.preparePluginSyncData( if (submission?.plugins) {
assign, // Prepare plugins data.
submission, await Promise.all(submission.plugins.map((plugin) =>
plugin, AddonModAssignSubmissionDelegate.preparePluginSyncData(
offlineData, assign,
pluginData, submission,
siteId, plugin,
))); offlineData,
} pluginData,
siteId,
)));
}
// Now save the submission. // Now save the submission.
if (Object.keys(pluginData).length > 0) {
await AddonModAssign.saveSubmissionOnline(assign.id, pluginData, siteId); await AddonModAssign.saveSubmissionOnline(assign.id, pluginData, siteId);
}
if (assign.submissiondrafts && offlineData.submitted) { if (assign.submissiondrafts && offlineData.submitted) {
// The user submitted the assign manually. Submit it for grading. // The user submitted the assign manually. Submit it for grading.
await AddonModAssign.submitForGradingOnline(assign.id, !!offlineData.submissionstatement, siteId); await AddonModAssign.submitForGradingOnline(assign.id, !!offlineData.submissionstatement, siteId);
}
} }
// Submission data sent, update cached data. No need to block the user for this. // Submission data sent, update cached data. No need to block the user for this.

View File

@ -42,6 +42,7 @@ import {
ADDON_MOD_ASSIGN_GRADED_EVENT, ADDON_MOD_ASSIGN_GRADED_EVENT,
ADDON_MOD_ASSIGN_MANUAL_SYNCED, ADDON_MOD_ASSIGN_MANUAL_SYNCED,
ADDON_MOD_ASSIGN_STARTED_EVENT, ADDON_MOD_ASSIGN_STARTED_EVENT,
ADDON_MOD_ASSIGN_SUBMISSION_REMOVED_EVENT,
ADDON_MOD_ASSIGN_SUBMISSION_SAVED_EVENT, ADDON_MOD_ASSIGN_SUBMISSION_SAVED_EVENT,
ADDON_MOD_ASSIGN_SUBMITTED_FOR_GRADING_EVENT, ADDON_MOD_ASSIGN_SUBMITTED_FOR_GRADING_EVENT,
} from '../constants'; } from '../constants';
@ -55,6 +56,7 @@ declare module '@singletons/events' {
*/ */
export interface CoreEventsData { export interface CoreEventsData {
[ADDON_MOD_ASSIGN_SUBMISSION_SAVED_EVENT]: AddonModAssignSubmissionSavedEventData; [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_SUBMITTED_FOR_GRADING_EVENT]: AddonModAssignSubmittedForGradingEventData;
[ADDON_MOD_ASSIGN_GRADED_EVENT]: AddonModAssignGradedEventData; [ADDON_MOD_ASSIGN_GRADED_EVENT]: AddonModAssignGradedEventData;
[ADDON_MOD_ASSIGN_STARTED_EVENT]: AddonModAssignStartedEventData; [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<boolean> {
siteId = siteId || CoreSites.getCurrentSiteId();
// Function to store the submission to be synchronized later.
const storeOffline = async (): Promise<boolean> => {
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<void> {
const site = await CoreSites.getSite(siteId);
userId = userId || site.getUserId();
const params: AddonModAssignRemoveSubmissionWSParams = {
assignid: assignId,
userid: userId,
};
const result = await site.write<AddonModAssignRemoveSubmissionWSResponse>('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); export const AddonModAssign = makeSingleton(AddonModAssignProvider);
@ -1755,6 +1876,22 @@ type AddonModAssignStartSubmissionWSParams = {
assignid: number; // Assignment instance id. 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. * Data returned by mod_assign_start_submission WS.
* *
@ -1784,6 +1921,11 @@ export type AddonModAssignSubmittedForGradingEventData = {
*/ */
export type AddonModAssignSubmissionSavedEventData = AddonModAssignSubmittedForGradingEventData; export type AddonModAssignSubmissionSavedEventData = AddonModAssignSubmittedForGradingEventData;
/**
* Data sent by SUBMISSION_REMOVED_EVENT event.
*/
export type AddonModAssignSubmissionRemovedEventData = AddonModAssignSubmittedForGradingEventData;
/** /**
* Data sent by GRADED_EVENT event. * Data sent by GRADED_EVENT event.
*/ */

View File

@ -54,28 +54,31 @@ export class AddonModAssignSubmissionFileComponent extends AddonModAssignSubmiss
: undefined; : undefined;
// Get the offline data. // Get the offline data.
const filesData = await CoreUtils.ignoreErrors( const offlineData = await CoreUtils.ignoreErrors(
AddonModAssignOffline.getSubmission(this.assign.id), AddonModAssignOffline.getSubmission(this.assign.id),
undefined, undefined,
); );
try { try {
if (filesData && filesData.plugindata && filesData.plugindata.files_filemanager) { if (offlineData) {
const offlineDataFiles = <CoreFileUploaderStoreFilesResult>filesData.plugindata.files_filemanager; // Offline submission, get files if submission is not removed.
// It has offline data. if (offlineData.plugindata.files_filemanager) {
let offlineFiles: FileEntry[] = []; const offlineDataFiles = <CoreFileUploaderStoreFilesResult>offlineData.plugindata.files_filemanager;
if (offlineDataFiles.offline) { // It has offline data.
offlineFiles = <FileEntry[]>await CoreUtils.ignoreErrors( let offlineFiles: FileEntry[] = [];
AddonModAssignHelper.getStoredSubmissionFiles( if (offlineDataFiles.offline) {
this.assign.id, offlineFiles = <FileEntry[]>await CoreUtils.ignoreErrors(
AddonModAssignSubmissionFileHandlerService.FOLDER_NAME, AddonModAssignHelper.getStoredSubmissionFiles(
), this.assign.id,
[], AddonModAssignSubmissionFileHandlerService.FOLDER_NAME,
); ),
} [],
);
}
this.files = offlineDataFiles.online || []; this.files = offlineDataFiles.online || [];
this.files = this.files.concat(offlineFiles); this.files = this.files.concat(offlineFiles);
}
} else { } else {
// No offline data, get the online files. // No offline data, get the online files.
this.files = AddonModAssign.getSubmissionPluginAttachments(this.plugin); this.files = AddonModAssign.getSubmissionPluginAttachments(this.plugin);

View File

@ -69,8 +69,11 @@ export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignS
this.wordLimit = parseInt(this.configs?.wordlimit || '0'); this.wordLimit = parseInt(this.configs?.wordlimit || '0');
try { try {
if (offlineData && offlineData.plugindata && offlineData.plugindata.onlinetext_editor) { if (offlineData && offlineData.plugindata) {
this.text = (<AddonModAssignSubmissionOnlineTextPluginData>offlineData.plugindata).onlinetext_editor.text; // Offline submission, get text if submission is not removed.
if (offlineData.plugindata.onlinetext_editor) {
this.text = (<AddonModAssignSubmissionOnlineTextPluginData>offlineData.plugindata).onlinetext_editor.text;
}
} else { } else {
// No offline data found, return online text. // No offline data found, return online text.
this.text = AddonModAssign.getSubmissionPluginText(this.plugin); this.text = AddonModAssign.getSubmissionPluginText(this.plugin);

View File

@ -120,6 +120,17 @@ Feature: Test basic usage of assignment activity in app
Then I should find "Online text submissions" in the app Then I should find "Online text submissions" in the app
And I should find "Submission test 2nd attempt" 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 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 Given I entered the assign activity "assignment1" on course "Course 1" as "student1" in the app
When I press "Add submission" 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 Then I should find "Submitted for grading" in the app
And I should find "Submission test edited offline" 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 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