Merge pull request #4191 from albertgasset/MOBILE-3893
MOBILE-3893 assign: Add button to remove submissionsmain
commit
b5b44a8a1d
|
@ -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",
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue