Merge pull request #4191 from albertgasset/MOBILE-3893

MOBILE-3893 assign: Add button to remove submissions
main
Dani Palou 2024-10-04 15:32:55 +02:00 committed by GitHub
commit b5b44a8a1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 493 additions and 84 deletions

View File

@ -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",

View File

@ -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) => {

View File

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

View File

@ -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<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).
*
@ -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) {

View File

@ -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';

View File

@ -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",

View File

@ -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;

View File

@ -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<AddonModAssignSubmission, 'userid'|'groupid'> & {
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.

View File

@ -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.

View File

@ -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<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);
@ -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.
*/

View File

@ -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
*/

View File

@ -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 CoreDelegate<AddonM
return this.executeFunctionOnEnabled(plugin.type, 'isEmpty', [assign, plugin]);
}
/**
* Check if a plugin has no data in the edit form
*
* @param assign The assignment.
* @param plugin The plugin object.
* @param inputData Data entered in the submission form.
* @returns Whether the plugin is empty.
*/
isPluginEmptyForEdit(
assign: AddonModAssignAssign,
plugin: AddonModAssignPlugin,
inputData: CoreFormFields,
): boolean | undefined {
return this.executeFunctionOnEnabled(plugin.type, 'isEmptyForEdit', [assign, plugin, inputData]);
}
/**
* Prefetch any required data for a submission plugin.
*

View File

@ -54,28 +54,31 @@ export class AddonModAssignSubmissionFileComponent extends AddonModAssignSubmiss
: undefined;
// Get the offline data.
const filesData = await CoreUtils.ignoreErrors(
const offlineData = await CoreUtils.ignoreErrors(
AddonModAssignOffline.getSubmission(this.assign.id),
undefined,
);
try {
if (filesData && filesData.plugindata && filesData.plugindata.files_filemanager) {
const offlineDataFiles = <CoreFileUploaderStoreFilesResult>filesData.plugindata.files_filemanager;
// It has offline data.
let offlineFiles: FileEntry[] = [];
if (offlineDataFiles.offline) {
offlineFiles = <FileEntry[]>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 = <CoreFileUploaderStoreFilesResult>offlineData.plugindata.files_filemanager;
// It has offline data.
let offlineFiles: FileEntry[] = [];
if (offlineDataFiles.offline) {
offlineFiles = <FileEntry[]>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);

View File

@ -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
*/

View File

@ -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 = (<AddonModAssignSubmissionOnlineTextPluginData>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 = (<AddonModAssignSubmissionOnlineTextPluginData>offlineData.plugindata).onlinetext_editor.text;
}
} else {
// No offline data found, return online text.
this.text = AddonModAssign.getSubmissionPluginText(this.plugin);

View File

@ -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
*/

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
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