From 5350f715a35ba614f379c272804ae68ebf2b8427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 19 Feb 2021 12:41:30 +0100 Subject: [PATCH] MOBILE-3636 assign: Submission review and edit pages --- src/addons/mod/assign/assign-lazy.module.ts | 45 +- .../index/addon-mod-assign-index.html | 8 +- .../mod/assign/components/index/index.ts | 23 +- .../addon-mod-assign-submission.html | 148 ++++--- .../components/submission/submission.ts | 22 +- .../addon-mod-assign-feedback-comments.html | 13 +- src/addons/mod/assign/pages/edit/edit.html | 39 ++ src/addons/mod/assign/pages/edit/edit.ts | 408 ++++++++++++++++++ .../submission-list/submission-list.html | 16 +- .../submission-list/submission-list.page.ts | 26 +- .../submission-review/submission-review.html | 29 ++ .../submission-review/submission-review.ts | 184 ++++++++ .../mod/assign/services/assign-helper.ts | 85 ++-- src/addons/mod/assign/services/assign.ts | 14 +- .../assign/submission/file/component/file.ts | 2 +- ...ddon-mod-assign-submission-onlinetext.html | 13 +- .../onlinetext/component/onlinetext.ts | 2 +- src/core/services/cron.ts | 2 +- src/core/singletons/events.ts | 7 + 19 files changed, 912 insertions(+), 174 deletions(-) create mode 100644 src/addons/mod/assign/pages/edit/edit.html create mode 100644 src/addons/mod/assign/pages/edit/edit.ts create mode 100644 src/addons/mod/assign/pages/submission-review/submission-review.html create mode 100644 src/addons/mod/assign/pages/submission-review/submission-review.ts diff --git a/src/addons/mod/assign/assign-lazy.module.ts b/src/addons/mod/assign/assign-lazy.module.ts index 00fc82ac5..90caa2792 100644 --- a/src/addons/mod/assign/assign-lazy.module.ts +++ b/src/addons/mod/assign/assign-lazy.module.ts @@ -12,22 +12,61 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { conditionalRoutes } from '@/app/app-routing.module'; import { CoreSharedModule } from '@/core/shared.module'; import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { CanLeaveGuard } from '@guards/can-leave'; +import { CoreScreen } from '@services/screen'; import { AddonModAssignComponentsModule } from './components/components.module'; +import { AddonModAssignEditPage } from './pages/edit/edit'; import { AddonModAssignIndexPage } from './pages/index/index.page'; import { AddonModAssignSubmissionListPage } from './pages/submission-list/submission-list.page'; +import { AddonModAssignSubmissionReviewPage } from './pages/submission-review/submission-review'; -const routes: Routes = [ +const commonRoutes: Routes = [ { path: ':courseId/:cmId', component: AddonModAssignIndexPage, }, { - path: ':courseId/:cmId/submission-list', + path: ':courseId/:cmId/edit', + component: AddonModAssignEditPage, + canDeactivate: [CanLeaveGuard], + }, +]; + +const mobileRoutes: Routes = [ + ...commonRoutes, + { + path: ':courseId/:cmId/submission', component: AddonModAssignSubmissionListPage, }, + { + path: ':courseId/:cmId/submission/:submitId', + component: AddonModAssignSubmissionReviewPage, + canDeactivate: [CanLeaveGuard], + }, +]; + +const tabletRoutes: Routes = [ + ...commonRoutes, + { + path: ':courseId/:cmId/submission', + component: AddonModAssignSubmissionListPage, + children: [ + { + path: ':submitId', + component: AddonModAssignSubmissionReviewPage, + canDeactivate: [CanLeaveGuard], + }, + ], + }, +]; + +const routes: Routes = [ + ...conditionalRoutes(mobileRoutes, () => CoreScreen.instance.isMobile), + ...conditionalRoutes(tabletRoutes, () => CoreScreen.instance.isTablet), ]; @NgModule({ @@ -39,6 +78,8 @@ const routes: Routes = [ declarations: [ AddonModAssignIndexPage, AddonModAssignSubmissionListPage, + AddonModAssignSubmissionReviewPage, + AddonModAssignEditPage, ], }) export class AddonModAssignLazyModule {} diff --git a/src/addons/mod/assign/components/index/addon-mod-assign-index.html b/src/addons/mod/assign/components/index/addon-mod-assign-index.html index c71bacbc9..12d416cdc 100644 --- a/src/addons/mod/assign/components/index/addon-mod-assign-index.html +++ b/src/addons/mod/assign/components/index/addon-mod-assign-index.html @@ -35,7 +35,7 @@ + contextLevel="module" [contextInstanceId]="module!.id" [courseId]="courseId" (click)="expandDescription($event)"> @@ -97,7 +97,7 @@ + (click)="goToSubmissionList(submissionStatusDraft, !!summary.submissiondraftscount)">

{{ 'addon.mod_assign.numberofdraftsubmissions' | translate }}

{{ summary.submissiondraftscount }} @@ -107,7 +107,7 @@ + (click)="goToSubmissionList(submissionStatusSubmitted, !!summary.submissionssubmittedcount)">

{{ 'addon.mod_assign.numberofsubmittedassignments' | translate }}

{{ summary.submissionssubmittedcount }} @@ -136,7 +136,7 @@ + [moduleId]="module!.id"> diff --git a/src/addons/mod/assign/components/index/index.ts b/src/addons/mod/assign/components/index/index.ts index 1a03fe7eb..bf23f6e15 100644 --- a/src/addons/mod/assign/components/index/index.ts +++ b/src/addons/mod/assign/components/index/index.ts @@ -34,6 +34,7 @@ import { AddonModAssignGradedEventData, AddonModAssignProvider, AddonModAssignSubmissionGradingSummary, + AddonModAssignSubmissionSavedEventData, AddonModAssignSubmittedForGradingEventData, } from '../../services/assign'; import { AddonModAssignOffline } from '../../services/assign-offline'; @@ -106,12 +107,16 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo this.currentSite = CoreSites.instance.getCurrentSite(); // Listen to events. - this.savedObserver = CoreEvents.on(AddonModAssignProvider.SUBMISSION_SAVED_EVENT, (data) => { - if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) { + this.savedObserver = CoreEvents.on( + AddonModAssignProvider.SUBMISSION_SAVED_EVENT, + (data) => { + if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) { // Assignment submission saved, refresh data. - this.showLoadingAndRefresh(true, false); - } - }, this.siteId); + this.showLoadingAndRefresh(true, false); + } + }, + this.siteId, + ); this.submittedObserver = CoreEvents.on( AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, @@ -262,7 +267,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo * @param groupId Group ID. * @return Resolved when done. */ - async setGroup(groupId: number): Promise { + async setGroup(groupId = 0): Promise { this.group = groupId; const submissionStatus = await AddonModAssign.instance.getSubmissionStatus(this.assign!.id, { @@ -303,10 +308,10 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo * Go to view a list of submissions. * * @param status Status to see. - * @param count Number of submissions with the status. + * @param hasSubmissions If the status has any submission. */ - goToSubmissionList(status: string, count: number): void { - if (typeof status != 'undefined' && !count && this.showNumbers) { + goToSubmissionList(status?: string, hasSubmissions = false): void { + if (typeof status != 'undefined' && !hasSubmissions && this.showNumbers) { return; } diff --git a/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html b/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html index 5c45c5af4..2185a1124 100644 --- a/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html +++ b/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html @@ -1,10 +1,10 @@ - + -

{{ user.fullname }}

+

{{ user!.fullname }}

@@ -39,10 +39,10 @@ + *ngIf="userSubmission && userSubmission!.status != statusNew && userSubmission!.timemodified">

{{ 'addon.mod_assign.timemodified' | translate }}

-

{{ userSubmission.timemodified * 1000 | coreFormatDate }}

+

{{ userSubmission!.timemodified * 1000 | coreFormatDate }}

@@ -55,49 +55,49 @@ -

-

- +

{{ 'addon.mod_assign.duedate' | translate }}

-

{{ assign.duedate * 1000 | coreFormatDate }}

-

{{ 'addon.mod_assign.duedateno' | translate }}

+

{{ assign!.duedate * 1000 | coreFormatDate }}

+

{{ 'addon.mod_assign.duedateno' | translate }}

- +

{{ 'addon.mod_assign.cutoffdate' | translate }}

-

{{ assign.cutoffdate * 1000 | coreFormatDate }}

+

{{ assign!.cutoffdate * 1000 | coreFormatDate }}

+ *ngIf="assign!.duedate && lastAttempt?.extensionduedate && !isSubmittedForGrading">

{{ 'addon.mod_assign.extensionduedate' | translate }}

-

{{ lastAttempt.extensionduedate * 1000 | coreFormatDate }}

+

{{ lastAttempt!.extensionduedate * 1000 | coreFormatDate }}

{{ 'addon.mod_assign.attemptnumber' | translate }}

-

+

{{ 'addon.mod_assign.outof' | translate : {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }}

-

+

{{ 'addon.mod_assign.outof' | translate : - {'$a': {'current': currentAttempt, 'total': assign.maxattempts} } }} + {'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }}

@@ -114,12 +114,12 @@ {{ 'addon.mod_assign.addsubmission' | translate }} - + {{ 'addon.mod_assign.addnewattemptfromprevious' | translate }} @@ -130,9 +130,9 @@ + *ngIf="!hasOffline && userSubmission && userSubmission!.status && + userSubmission!.status != statusNew && + userSubmission!.status != statusReopened" (click)="goToEdit()"> {{ 'addon.mod_assign.editsubmission' | translate }} @@ -176,25 +176,25 @@ - +

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

- - - - -

{{ user.fullname }}

-
-
+
+ + + + +

{{ user.fullname }}

+
- - - - {{ 'addon.mod_assign.hiddenuser' | translate }} {{ blindId }} - - + + + + + {{ 'addon.mod_assign.hiddenuser' | translate }} {{ blindId }} + -
+ @@ -219,12 +219,12 @@ + *ngIf="feedback?.gradefordisplay && (!isGrading || grade.method != 'simple')">

{{ 'addon.mod_assign.currentgrade' | translate }}

-

+

- +
@@ -236,7 +236,7 @@

{{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo!.grade} }}

-

{{ 'addon.mod_assign.gradelocked' | translate }}

@@ -256,7 +256,7 @@

{{ outcome.name }}

+ interface="action-sheet" [disabled]="gradeInfo!.disabled"> {{grade.label}} @@ -268,18 +268,18 @@

{{ 'addon.mod_assign.currentgrade' | translate }}

-

+

{{ grade.gradebookGrade }}

-

+

{{ grade.scale[grade.gradebookGrade].label }}

-

-

+

-

- @@ -292,28 +292,30 @@
- -

{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}

- {{ 'addon.mod_assign.applytoteam' | translate }} + + +

{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}

+

{{ 'addon.mod_assign.applytoteam' | translate }}

+
- +

{{ 'addon.mod_assign.attemptsettings' | translate }}

-

+

{{ 'addon.mod_assign.outof' | translate : {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }}

-

+

{{ 'addon.mod_assign.outof' | translate : - {'$a': {'current': currentAttempt, 'total': assign.maxattempts} } }} + {'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }}

{{ 'addon.mod_assign.attemptreopenmethod' | translate }}: - {{ 'addon.mod_assign.attemptreopenmethod_' + assign.attemptreopenmethod | translate }} + {{ 'addon.mod_assign.attemptreopenmethod_' + assign!.attemptreopenmethod | translate }}

@@ -324,33 +326,37 @@
- +

{{ 'addon.mod_assign.gradedby' | translate }}

-

{{ grader.fullname }}

-

{{ feedback.gradeddate * 1000 | coreFormatDate }}

+

{{ grader!.fullname }}

+

{{ feedback!.gradeddate * 1000 | coreFormatDate }}

- +

{{ 'addon.mod_assign.gradedon' | translate }}

-

{{ feedback.gradeddate * 1000 | coreFormatDate }}

+

{{ feedback!.gradeddate * 1000 | coreFormatDate }}

-
- -

{{ 'addon.mod_assign.cannotgradefromapp' | translate }}

- - {{ 'core.openinbrowser' | translate }} - - -
+ + + + +

{{ 'addon.mod_assign.cannotgradefromapp' | translate }}

+ + {{ 'core.openinbrowser' | translate }} + + +
+
+
@@ -360,20 +366,20 @@

{{lastAttempt!.submissiongroupname}}

-

{{ 'addon.mod_assign.noteam' | translate }}

{{ 'addon.mod_assign.noteam_desc' | translate }}

- 1">

{{ 'addon.mod_assign.multipleteams' | translate }}

{{ 'addon.mod_assign.multipleteams_desc' | translate }}

-

+

{{ 'addon.mod_assign.defaultteam' | translate }}

diff --git a/src/addons/mod/assign/components/submission/submission.ts b/src/addons/mod/assign/components/submission/submission.ts index b01cc551d..962bb5e05 100644 --- a/src/addons/mod/assign/components/submission/submission.ts +++ b/src/addons/mod/assign/components/submission/submission.ts @@ -55,6 +55,7 @@ import { CoreError } from '@classes/errors/error'; import { CoreGroups } from '@services/groups'; import { CoreSync } from '@services/sync'; import { AddonModAssignSubmissionPluginComponent } from '../submission-plugin/submission-plugin'; +import { AddonModAssignModuleHandlerService } from '../../services/handlers/module'; /** * Component that displays an assignment submission. @@ -73,7 +74,6 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { @Input() moduleId!: number; // Module ID the submission belongs to. @Input() submitId!: number; // User that did the submission. @Input() blindId?: number; // Blinded user ID (if it's blinded). - @Input() showGrade = false; // Whether to display the grade tab at start. loaded = false; // Whether data has been loaded. selectedTab = 'submission'; // Tab selected on start. @@ -121,6 +121,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { canSaveGrades = false; // Whether the user can save the grades. allowAddAttempt = false; // Allow adding a new attempt when grading. gradeUrl?: string; // URL to grade in browser. + isPreviousAttemptEmpty = true; // Whether the previous attempt contains an empty submission. // Some constants. statusNew = AddonModAssignProvider.SUBMISSION_STATUS_NEW; @@ -131,7 +132,6 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { protected siteId: string; // Current site ID. protected currentUserId: number; // Current user ID. protected previousAttempt?: AddonModAssignSubmissionPreviousAttempt; // The previous attempt. - protected isPreviousAttemptEmpty = true; // Whether the previous attempt contains an empty submission. protected submissionStatusAvailable = false; // Whether we were able to retrieve the submission status. protected originalGrades: AddonModAssignSubmissionOriginalGrades = { addAttempt: false, @@ -180,7 +180,6 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { * Component being initialized. */ ngOnInit(): void { - this.selectedTab = this.showGrade ? 'grade' : 'submission'; this.isSubmittedForGrading = !!this.submitId; this.loadData(true); @@ -343,13 +342,14 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { * Go to the page to add or edit submission. */ goToEdit(): void { - CoreNavigator.instance.navigate('AddonModAssignEditPage', { - params: { - moduleId: this.moduleId, - courseId: this.courseId, - userId: this.submitId, - blindId: this.blindId, - } }); + CoreNavigator.instance.navigateToSitePath( + AddonModAssignModuleHandlerService.PAGE_NAME + '/' + this.courseId + '/' + this.moduleId + '/edit', + { + params: { + blindId: this.blindId, + }, + }, + ); } /** @@ -393,7 +393,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { try { return AddonModAssignHelper.instance.hasFeedbackDataChanged( this.assign!, - this.userSubmission!, // @todo + this.userSubmission, this.feedback, this.submitId, ); diff --git a/src/addons/mod/assign/feedback/comments/component/addon-mod-assign-feedback-comments.html b/src/addons/mod/assign/feedback/comments/component/addon-mod-assign-feedback-comments.html index 5b01232c3..e3d1fd1ab 100644 --- a/src/addons/mod/assign/feedback/comments/component/addon-mod-assign-feedback-comments.html +++ b/src/addons/mod/assign/feedback/comments/component/addon-mod-assign-feedback-comments.html @@ -24,11 +24,10 @@ - - - - + + + diff --git a/src/addons/mod/assign/pages/edit/edit.html b/src/addons/mod/assign/pages/edit/edit.html new file mode 100644 index 000000000..7acd635e3 --- /dev/null +++ b/src/addons/mod/assign/pages/edit/edit.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + {{ 'core.save' | translate }} + + + + + + + + +
+ + + + + + + + + + + + + +
+
+
+
diff --git a/src/addons/mod/assign/pages/edit/edit.ts b/src/addons/mod/assign/pages/edit/edit.ts new file mode 100644 index 000000000..49198b4c4 --- /dev/null +++ b/src/addons/mod/assign/pages/edit/edit.ts @@ -0,0 +1,408 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { CoreError } from '@classes/errors/error'; +import { CoreIonLoadingElement } from '@classes/ion-loading'; +import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuploader-helper'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreSync } from '@services/sync'; +import { CoreDomUtils } from '@services/utils/dom'; +import { Translate } from '@singletons'; +import { CoreEventActivityDataSentData, CoreEvents } from '@singletons/events'; +import { + AddonModAssignAssign, + AddonModAssignSubmission, + AddonModAssignProvider, + AddonModAssign, + AddonModAssignSubmissionStatusOptions, + AddonModAssignGetSubmissionStatusWSResponse, + AddonModAssignSavePluginData, + AddonModAssignSubmissionSavedEventData, + AddonModAssignSubmittedForGradingEventData, +} from '../../services/assign'; +import { AddonModAssignHelper } from '../../services/assign-helper'; +import { AddonModAssignOffline } from '../../services/assign-offline'; +import { AddonModAssignSync } from '../../services/assign-sync'; + +/** + * Page that allows adding or editing an assigment submission. + */ +@Component({ + selector: 'page-addon-mod-assign-edit', + templateUrl: 'edit.html', +}) +export class AddonModAssignEditPage implements OnInit, OnDestroy { + + @ViewChild('editSubmissionForm') formElement?: ElementRef; + + title: string; // Title to display. + assign?: AddonModAssignAssign; // Assignment. + courseId!: number; // Course ID the assignment belongs to. + moduleId!: number; // Module ID the submission belongs to. + userSubmission?: AddonModAssignSubmission; // The user submission. + allowOffline = false; // Whether offline is allowed. + submissionStatement?: string; // The submission statement. + submissionStatementAccepted = false; // Whether submission statement is accepted. + loaded = false; // Whether data has been loaded. + + protected userId: number; // User doing the submission. + protected isBlind = false; // Whether blind is used. + protected editText: string; // "Edit submission" translated text. + protected saveOffline = false; // Whether to save data in offline. + protected hasOffline = false; // Whether the assignment has offline data. + protected isDestroyed = false; // Whether the component has been destroyed. + protected forceLeave = false; // To allow leaving the page without checking for changes. + + constructor( + protected route: ActivatedRoute, + ) { + this.userId = CoreSites.instance.getCurrentSiteUserId(); // Right now we can only edit current user's submissions. + this.editText = Translate.instance.instant('addon.mod_assign.editsubmission'); + this.title = this.editText; + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.moduleId = CoreNavigator.instance.getRouteNumberParam('cmId')!; + this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!; + this.isBlind = !!CoreNavigator.instance.getRouteNumberParam('blindId'); + + this.fetchAssignment().finally(() => { + this.loaded = true; + }); + } + + /** + * Check if we can leave the page or not. + * + * @return Resolved if we can leave it, rejected if not. + */ + async ionViewCanLeave(): Promise { + if (this.forceLeave) { + return; + } + + // Check if data has changed. + const changed = await this.hasDataChanged(); + if (changed) { + await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmcanceledit')); + } + + // Nothing has changed or user confirmed to leave. Clear temporary data from plugins. + AddonModAssignHelper.instance.clearSubmissionPluginTmpData(this.assign!, this.userSubmission, this.getInputData()); + + CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, CoreSites.instance.getCurrentSiteId()); + } + + /** + * Fetch assignment data. + * + * @return Promise resolved when done. + */ + protected async fetchAssignment(): Promise { + const currentUserId = CoreSites.instance.getCurrentSiteUserId(); + + try { + // Get assignment data. + this.assign = await AddonModAssign.instance.getAssignment(this.courseId, this.moduleId); + this.title = this.assign.name || this.title; + + if (!this.isDestroyed) { + // Block the assignment. + CoreSync.instance.blockOperation(AddonModAssignProvider.COMPONENT, this.assign.id); + } + + // Wait for sync to be over (if any). + await AddonModAssignSync.instance.waitForSync(this.assign.id); + + // Get submission status. Ignore cache to get the latest data. + const options: AddonModAssignSubmissionStatusOptions = { + userId: this.userId, + isBlind: this.isBlind, + cmId: this.assign.cmid, + filter: false, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + }; + + let submissionStatus: AddonModAssignGetSubmissionStatusWSResponse; + try { + submissionStatus = await AddonModAssign.instance.getSubmissionStatus(this.assign.id, options); + this.userSubmission = + AddonModAssign.instance.getSubmissionObjectFromAttempt(this.assign, submissionStatus.lastattempt); + } catch (error) { + // Cannot connect. Get cached data. + options.filter = true; + options.readingStrategy = CoreSitesReadingStrategy.PreferCache; + + submissionStatus = await AddonModAssign.instance.getSubmissionStatus(this.assign.id, options); + this.userSubmission = + AddonModAssign.instance.getSubmissionObjectFromAttempt(this.assign, submissionStatus.lastattempt); + + // Check if the user can edit it in offline. + const canEditOffline = + await AddonModAssignHelper.instance.canEditSubmissionOffline(this.assign, this.userSubmission); + if (!canEditOffline) { + // Submission cannot be edited in offline, reject. + this.allowOffline = false; + throw error; + } + } + + if (!submissionStatus.lastattempt?.canedit) { + // Can't edit. Reject. + throw new CoreError(Translate.instance.instant('core.nopermissions', { $a: this.editText })); + } + + this.allowOffline = true; // If offline isn't allowed we shouldn't have reached this point. + // Only show submission statement if we are editing our own submission. + if (this.assign.requiresubmissionstatement && !this.assign.submissiondrafts && this.userId == currentUserId) { + this.submissionStatement = this.assign.submissionstatement; + } else { + this.submissionStatement = undefined; + } + + try { + // Check if there's any offline data for this submission. + const offlineData = await AddonModAssignOffline.instance.getSubmission(this.assign.id, this.userId); + + this.hasOffline = offlineData?.plugindata && Object.keys(offlineData.plugindata).length > 0; + } catch { + // No offline data found. + this.hasOffline = false; + } + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'Error getting assigment data.'); + + // Leave the player. + this.leaveWithoutCheck(); + } + } + + /** + * Get the input data. + * + * @return Input data. + */ + protected getInputData(): Record { + return CoreDomUtils.instance.getDataFromForm(document.forms['addon-mod_assign-edit-form']); + } + + /** + * Check if data has changed. + * + * @return Promise resolved with boolean: whether data has changed. + */ + protected hasDataChanged(): Promise { + // Usually the hasSubmissionDataChanged call will be resolved inmediately, causing the modal to be shown just an instant. + // We'll wait a bit before showing it to prevent this "blink". + let modal: CoreIonLoadingElement; + let showModal = true; + + setTimeout(async () => { + if (showModal) { + modal = await CoreDomUtils.instance.showModalLoading(); + } + }, 100); + + const data = this.getInputData(); + + return AddonModAssignHelper.instance.hasSubmissionDataChanged(this.assign!, this.userSubmission, data).finally(() => { + if (modal) { + modal.dismiss(); + } else { + showModal = false; + } + }); + } + + /** + * Leave the view without checking for changes. + */ + protected leaveWithoutCheck(): void { + this.forceLeave = true; + CoreNavigator.instance.back(); + } + + /** + * Get data to submit based on the input data. + * + * @param inputData The input data. + * @return Promise resolved with the data to submit. + */ + protected prepareSubmissionData(inputData: Record): Promise { + // If there's offline data, always save it in offline. + this.saveOffline = this.hasOffline; + + try { + return AddonModAssignHelper.instance.prepareSubmissionPluginData( + this.assign!, + this.userSubmission, + inputData, + this.hasOffline, + ); + } catch (error) { + if (this.allowOffline && !this.saveOffline) { + // Cannot submit in online, prepare for offline usage. + this.saveOffline = true; + + return AddonModAssignHelper.instance.prepareSubmissionPluginData( + this.assign!, + this.userSubmission, + inputData, + true, + ); + } + + throw error; + } + } + + /** + * Save the submission. + */ + async save(): Promise { + // Check if data has changed. + const changed = await this.hasDataChanged(); + if (!changed) { + // Nothing to save, just go back. + this.leaveWithoutCheck(); + + return; + } + try { + await this.saveSubmission(); + this.leaveWithoutCheck(); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'Error saving submission.'); + } + } + + /** + * Save the submission. + * + * @return Promise resolved when done. + */ + protected async saveSubmission(): Promise { + const inputData = this.getInputData(); + + if (this.submissionStatement && (!inputData.submissionstatement || inputData.submissionstatement === 'false')) { + throw Translate.instance.instant('addon.mod_assign.acceptsubmissionstatement'); + } + + let modal = await CoreDomUtils.instance.showModalLoading(); + let size = -1; + + // Get size to ask for confirmation. + try { + size = await AddonModAssignHelper.instance.getSubmissionSizeForEdit(this.assign!, this.userSubmission!, inputData); + } catch (error) { + // Error calculating size, return -1. + size = -1; + } + + modal.dismiss(); + + try { + // Confirm action. + await CoreFileUploaderHelper.instance.confirmUploadFile(size, true, this.allowOffline); + + modal = await CoreDomUtils.instance.showModalLoading('core.sending', true); + + const pluginData = await this.prepareSubmissionData(inputData); + if (!Object.keys(pluginData).length) { + // Nothing to save. + return; + } + + let sent: boolean; + + if (this.saveOffline) { + // Save submission in offline. + sent = false; + await AddonModAssignOffline.instance.saveSubmission( + this.assign!.id, + this.courseId, + pluginData, + this.userSubmission!.timemodified, + !this.assign!.submissiondrafts, + this.userId, + ); + } else { + // Try to send it to server. + sent = await AddonModAssign.instance.saveSubmission( + this.assign!.id, + this.courseId, + pluginData, + this.allowOffline, + this.userSubmission!.timemodified, + !!this.assign!.submissiondrafts, + this.userId, + ); + } + + // Clear temporary data from plugins. + AddonModAssignHelper.instance.clearSubmissionPluginTmpData(this.assign!, this.userSubmission, inputData); + + if (sent) { + CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'assign' }); + } + + // Submission saved, trigger events. + CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.instance.getCurrentSiteId()); + + CoreEvents.trigger( + AddonModAssignProvider.SUBMISSION_SAVED_EVENT, + { + assignmentId: this.assign!.id, + submissionId: this.userSubmission!.id, + userId: this.userId, + }, + CoreSites.instance.getCurrentSiteId(), + ); + + if (!this.assign!.submissiondrafts) { + // No drafts allowed, so it was submitted. Trigger event. + CoreEvents.trigger( + AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, + { + assignmentId: this.assign!.id, + submissionId: this.userSubmission!.id, + userId: this.userId, + }, + CoreSites.instance.getCurrentSiteId(), + ); + } + } finally { + modal.dismiss(); + } + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + this.isDestroyed = true; + + // Unblock the assignment. + if (this.assign) { + CoreSync.instance.unblockOperation(AddonModAssignProvider.COMPONENT, this.assign.id); + } + } + +} diff --git a/src/addons/mod/assign/pages/submission-list/submission-list.html b/src/addons/mod/assign/pages/submission-list/submission-list.html index bcd21d9da..4968d47b5 100644 --- a/src/addons/mod/assign/pages/submission-list/submission-list.html +++ b/src/addons/mod/assign/pages/submission-list/submission-list.html @@ -61,15 +61,15 @@ {{ 'addon.mod_assign.defaultteam' | translate }}

+ + {{ submission.statusTranslated }} + + + {{ submission.gradingStatusTranslationId | translate }} + - - {{ submission.statusTranslated }} - - - {{ submission.gradingStatusTranslationId | translate }} -
diff --git a/src/addons/mod/assign/pages/submission-list/submission-list.page.ts b/src/addons/mod/assign/pages/submission-list/submission-list.page.ts index a73e3adf5..84ab9e0ac 100644 --- a/src/addons/mod/assign/pages/submission-list/submission-list.page.ts +++ b/src/addons/mod/assign/pages/submission-list/submission-list.page.ts @@ -17,6 +17,7 @@ import { ActivatedRoute } from '@angular/router'; import { IonRefresher } from '@ionic/angular'; import { CoreGroupInfo, CoreGroups } from '@services/groups'; import { CoreNavigator } from '@services/navigator'; +import { CoreScreen } from '@services/screen'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; @@ -37,6 +38,7 @@ import { AddonModAssignManualSyncData, AddonModAssignAutoSyncData, } from '../../services/assign-sync'; +import { AddonModAssignModuleHandlerService } from '../../services/handlers/module'; /** * Page that displays a list of submissions of an assignment. @@ -137,10 +139,10 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { this.title = Translate.instance.instant('addon.mod_assign.numberofparticipants'); } this.fetchAssignment(true).finally(() => { - /* if (!this.selectedSubmissionId && this.splitviewCtrl.isOn() && this.submissions.length > 0) { + if (!this.selectedSubmissionId && CoreScreen.instance.isTablet && this.submissions.length > 0) { // Take first and load it. this.loadSubmission(this.submissions[0]); - }*/ + } this.loaded = true; }); @@ -153,7 +155,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { * @param sync Whether to try to synchronize data. * @return Promise resolved when done. */ - protected async fetchAssignment(sync?: boolean): Promise { + protected async fetchAssignment(sync = false): Promise { try { // Get assignment data. this.assign = await AddonModAssign.instance.getAssignment(this.courseId, this.moduleId); @@ -310,19 +312,21 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { * @param submission The submission to load. */ loadSubmission(submission: AddonModAssignSubmissionForList): void { - /* if (this.selectedSubmissionId === submission.submitid && this.splitviewCtrl.isOn()) { + if (this.selectedSubmissionId === submission.submitid) { // Already selected. return; - }*/ + } this.selectedSubmissionId = submission.submitid; - /* this.splitviewCtrl.push('AddonModAssignSubmissionReviewPage', { - courseId: this.courseId, - moduleId: this.moduleId, - submitId: submission.submitid, - blindId: submission.blindid, - });*/ + CoreNavigator.instance.navigateToSitePath( + AddonModAssignModuleHandlerService.PAGE_NAME+'/'+this.courseId+'/'+this.moduleId+'/submission/'+submission.submitid, + { + params: { + blindId: submission.blindid, + }, + }, + ); } /** diff --git a/src/addons/mod/assign/pages/submission-review/submission-review.html b/src/addons/mod/assign/pages/submission-review/submission-review.html new file mode 100644 index 000000000..6b010c098 --- /dev/null +++ b/src/addons/mod/assign/pages/submission-review/submission-review.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + {{ 'core.done' | translate }} + + + + + + + + + + + + + diff --git a/src/addons/mod/assign/pages/submission-review/submission-review.ts b/src/addons/mod/assign/pages/submission-review/submission-review.ts new file mode 100644 index 000000000..bca89ffa0 --- /dev/null +++ b/src/addons/mod/assign/pages/submission-review/submission-review.ts @@ -0,0 +1,184 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, ViewChild } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { CoreCourse } from '@features/course/services/course'; +import { IonRefresher } from '@ionic/angular'; +import { CoreNavigator } from '@services/navigator'; +import { CoreScreen } from '@services/screen'; +import { CoreDomUtils } from '@services/utils/dom'; +import { AddonModAssignSubmissionComponent } from '../../components/submission/submission'; +import { AddonModAssign, AddonModAssignAssign } from '../../services/assign'; + +/** + * Page that displays a submission. + */ +@Component({ + selector: 'page-addon-mod-assign-submission-review', + templateUrl: 'submission-review.html', +}) +export class AddonModAssignSubmissionReviewPage implements OnInit { + + @ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent; + + title = ''; // Title to display. + moduleId!: number; // Module ID the submission belongs to. + courseId!: number; // Course ID the assignment belongs to. + submitId!: number; // User that did the submission. + blindId?: number; // Blinded user ID (if it's blinded). + loaded = false; // Whether data has been loaded. + canSaveGrades = false; // Whether the user can save grades. + + protected assign?: AddonModAssignAssign; // The assignment the submission belongs to. + protected blindMarking = false; // Whether it uses blind marking. + protected forceLeave = false; // To allow leaving the page without checking for changes. + + + constructor( + protected route: ActivatedRoute, + ) { } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.route.queryParams.subscribe((params) => { + this.moduleId = CoreNavigator.instance.getRouteNumberParam('cmId')!; + this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!; + this.submitId = CoreNavigator.instance.getRouteNumberParam('submitId') || 0; + this.blindId = CoreNavigator.instance.getRouteNumberParam('blindId', params); + + this.fetchSubmission().finally(() => { + this.loaded = true; + }); + }); + } + + /** + * Check if we can leave the page or not. + * + * @return Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): boolean | Promise { + if (!this.submissionComponent || this.forceLeave) { + return true; + } + + // Check if data has changed. + return this.submissionComponent.canLeave(); + } + + /** + * User entered the page. + */ + ionViewDidEnter(): void { + this.submissionComponent?.ionViewDidEnter(); + } + + /** + * User left the page. + */ + ionViewDidLeave(): void { + this.submissionComponent?.ionViewDidLeave(); + } + + /** + * Get the submission. + * + * @return Promise resolved when done. + */ + protected async fetchSubmission(): Promise { + this.assign = await AddonModAssign.instance.getAssignment(this.courseId, this.moduleId); + this.title = this.assign.name; + + this.blindMarking = !!this.assign.blindmarking && !this.assign.revealidentities; + + const gradeInfo = await CoreCourse.instance.getModuleBasicGradeInfo(this.moduleId); + if (!gradeInfo) { + return; + } + + // Grades can be saved if simple grading. + if (gradeInfo.advancedgrading && gradeInfo.advancedgrading[0] && + typeof gradeInfo.advancedgrading[0].method != 'undefined') { + + const method = gradeInfo.advancedgrading[0].method || 'simple'; + this.canSaveGrades = method == 'simple'; + } else { + this.canSaveGrades = true; + } + } + + /** + * Refresh all the data. + * + * @return Promise resolved when done. + */ + protected async refreshAllData(): Promise { + const promises: Promise[] = []; + + promises.push(AddonModAssign.instance.invalidateAssignmentData(this.courseId)); + if (this.assign) { + promises.push(AddonModAssign.instance.invalidateSubmissionData(this.assign.id)); + promises.push(AddonModAssign.instance.invalidateAssignmentUserMappingsData(this.assign.id)); + promises.push(AddonModAssign.instance.invalidateSubmissionStatusData( + this.assign.id, + this.submitId, + undefined, + this.blindMarking, + )); + } + + try { + await Promise.all(promises); + } finally { + this.submissionComponent && this.submissionComponent.invalidateAndRefresh(true); + + await this.fetchSubmission(); + } + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + */ + refreshSubmission(refresher?: CustomEvent): void { + this.refreshAllData().finally(() => { + refresher?.detail.complete(); + }); + } + + /** + * Submit a grade and feedback. + */ + async submitGrade(): Promise { + if (!this.submissionComponent) { + return; + } + + try { + await this.submissionComponent.submitGrade(); + // Grade submitted, leave the view if not in tablet. + if (!CoreScreen.instance.isTablet) { + this.forceLeave = true; + CoreNavigator.instance.back(); + } + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.error', true); + } + } + +} diff --git a/src/addons/mod/assign/services/assign-helper.ts b/src/addons/mod/assign/services/assign-helper.ts index 6db331096..0e797419d 100644 --- a/src/addons/mod/assign/services/assign-helper.ts +++ b/src/addons/mod/assign/services/assign-helper.ts @@ -49,7 +49,7 @@ export class AddonModAssignHelperProvider { * @param submission Submission. * @return Whether it can be edited offline. */ - async canEditSubmissionOffline(assign: AddonModAssignAssign, submission: AddonModAssignSubmission): Promise { + async canEditSubmissionOffline(assign: AddonModAssignAssign, submission?: AddonModAssignSubmission): Promise { if (!submission) { return false; } @@ -85,7 +85,15 @@ export class AddonModAssignHelperProvider { * @param submission Submission to clear the data for. * @param inputData Data entered in the submission form. */ - clearSubmissionPluginTmpData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, inputData: any): void { + clearSubmissionPluginTmpData( + assign: AddonModAssignAssign, + submission: AddonModAssignSubmission | undefined, + inputData: Record, + ): void { + if (!submission) { + return; + } + submission.plugins?.forEach((plugin) => { AddonModAssignSubmissionDelegate.instance.clearTmpData(assign, submission, plugin, inputData); }); @@ -356,7 +364,7 @@ export class AddonModAssignHelperProvider { async getSubmissionSizeForEdit( assign: AddonModAssignAssign, submission: AddonModAssignSubmission, - inputData: any, + inputData: Record, ): Promise { let totalSize = 0; @@ -492,27 +500,28 @@ export class AddonModAssignHelperProvider { */ async hasFeedbackDataChanged( assign: AddonModAssignAssign, - submission: AddonModAssignSubmission | AddonModAssignSubmissionFormatted, + submission: AddonModAssignSubmission | AddonModAssignSubmissionFormatted | undefined, feedback: AddonModAssignSubmissionFeedback, userId: number, ): Promise { + if (!submission || !feedback.plugins) { + return false; + } let hasChanged = false; - const promises = feedback.plugins - ? feedback.plugins.map((plugin) => - this.prepareFeedbackPluginData(assign.id, userId, feedback).then(async (inputData) => { - const changed = await CoreUtils.instance.ignoreErrors( - AddonModAssignFeedbackDelegate.instance.hasPluginDataChanged(assign, submission, plugin, inputData, userId), - false, - ); - if (changed) { - hasChanged = true; - } + const promises = feedback.plugins.map((plugin) => + this.prepareFeedbackPluginData(assign.id, userId, feedback).then(async (inputData) => { + const changed = await CoreUtils.instance.ignoreErrors( + AddonModAssignFeedbackDelegate.instance.hasPluginDataChanged(assign, submission, plugin, inputData, userId), + false, + ); + if (changed) { + hasChanged = true; + } - return; - })) - : []; + return; + })); await CoreUtils.instance.allPromises(promises); @@ -529,9 +538,13 @@ export class AddonModAssignHelperProvider { */ async hasSubmissionDataChanged( assign: AddonModAssignAssign, - submission: AddonModAssignSubmission, - inputData: any, + submission: AddonModAssignSubmission | undefined, + inputData: Record, ): Promise { + if (!submission) { + return false; + } + let hasChanged = false; const promises = submission.plugins @@ -544,7 +557,7 @@ export class AddonModAssignHelperProvider { return; }).catch(() => { - // Ignore errors. + // Ignore errors. })) : []; @@ -591,23 +604,25 @@ export class AddonModAssignHelperProvider { */ async prepareSubmissionPluginData( assign: AddonModAssignAssign, - submission: AddonModAssignSubmission, - inputData: any, + submission: AddonModAssignSubmission | undefined, + inputData: Record, offline = false, - ): Promise { + ): Promise { - const pluginData = {}; - const promises = submission.plugins - ? submission.plugins.map((plugin) => - AddonModAssignSubmissionDelegate.instance.preparePluginSubmissionData( - assign, - submission, - plugin, - inputData, - pluginData, - offline, - )) - : []; + if (!submission || !submission.plugins) { + return {}; + } + + const pluginData: AddonModAssignSavePluginData = {}; + const promises = submission.plugins.map((plugin) => + AddonModAssignSubmissionDelegate.instance.preparePluginSubmissionData( + assign, + submission, + plugin, + inputData, + pluginData, + offline, + )); await Promise.all(promises); diff --git a/src/addons/mod/assign/services/assign.ts b/src/addons/mod/assign/services/assign.ts index d89407a5a..b1d63013b 100644 --- a/src/addons/mod/assign/services/assign.ts +++ b/src/addons/mod/assign/services/assign.ts @@ -30,6 +30,7 @@ import { CoreUtils } from '@services/utils/utils'; import { AddonModAssignOffline } from './assign-offline'; import { AddonModAssignSubmissionDelegate } from './submission-delegate'; import { CoreComments } from '@features/comments/services/comments'; +import { AddonModAssignSubmissionFormatted } from './assign-helper'; const ROOT_CACHE_KEY = 'mmaModAssign:'; @@ -1011,7 +1012,7 @@ export class AddonModAssignProvider { * @param assignId Assignment ID. * @return Promise resolved with boolean: whether it needs to be graded or not. */ - async needsSubmissionToBeGraded(submission: any, assignId: number): Promise { + async needsSubmissionToBeGraded(submission: AddonModAssignSubmissionFormatted, assignId: number): Promise { if (!submission.gradingstatus) { // This should not happen, but it's better to show rather than not showing any of the submissions. return true; @@ -1864,11 +1865,12 @@ export type AddonModAssignSubmittedForGradingEventData = { userId: number; }; +/** + * Data sent by SUBMISSION_SAVED_EVENT event. + */ +export type AddonModAssignSubmissionSavedEventData = AddonModAssignSubmittedForGradingEventData; + /** * Data sent by GRADED_EVENT event. */ -export type AddonModAssignGradedEventData = { - assignmentId: number; - submissionId: number; - userId: number; -}; +export type AddonModAssignGradedEventData = AddonModAssignSubmittedForGradingEventData; diff --git a/src/addons/mod/assign/submission/file/component/file.ts b/src/addons/mod/assign/submission/file/component/file.ts index 2dff6e98a..c6e545e11 100644 --- a/src/addons/mod/assign/submission/file/component/file.ts +++ b/src/addons/mod/assign/submission/file/component/file.ts @@ -41,7 +41,7 @@ export class AddonModAssignSubmissionFileComponent extends AddonModAssignSubmiss /** * Component being initialized. */ - async nOnInit(): Promise { + async ngOnInit(): Promise { // Get the offline data. const filesData = await CoreUtils.instance.ignoreErrors( AddonModAssignOffline.instance.getSubmission(this.assign.id), diff --git a/src/addons/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html b/src/addons/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html index cc3c5c014..4883118eb 100644 --- a/src/addons/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html +++ b/src/addons/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html @@ -24,12 +24,11 @@
- - - - + + + diff --git a/src/addons/mod/assign/submission/onlinetext/component/onlinetext.ts b/src/addons/mod/assign/submission/onlinetext/component/onlinetext.ts index b8a5b38ac..0a152e314 100644 --- a/src/addons/mod/assign/submission/onlinetext/component/onlinetext.ts +++ b/src/addons/mod/assign/submission/onlinetext/component/onlinetext.ts @@ -56,7 +56,7 @@ export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignS /** * Component being initialized. */ - async nOnInit(): Promise { + async ngOnInit(): Promise { // Get the text. Check if we have anything offline. const offlineData = await CoreUtils.instance.ignoreErrors( AddonModAssignOffline.instance.getSubmission(this.assign.id), diff --git a/src/core/services/cron.ts b/src/core/services/cron.ts index 2ccc6e868..b8eea95cb 100644 --- a/src/core/services/cron.ts +++ b/src/core/services/cron.ts @@ -267,7 +267,7 @@ export class CoreCronDelegateService { * @return True if handler uses network or not defined, false otherwise. */ protected handlerUsesNetwork(name: string): boolean { - if (!this.handlers[name] || this.handlers[name].usesNetwork) { + if (!this.handlers[name] || !this.handlers[name].usesNetwork) { // Invalid, return default. return true; } diff --git a/src/core/singletons/events.ts b/src/core/singletons/events.ts index 91d5a072d..030b21ce7 100644 --- a/src/core/singletons/events.ts +++ b/src/core/singletons/events.ts @@ -298,3 +298,10 @@ export type CoreEventSectionStatusChangedData = CoreEventSiteData & { courseId: number; sectionId?: number; }; + +/** + * Data passed to ACTIVITY_DATA_SENT event. + */ +export type CoreEventActivityDataSentData = CoreEventSiteData & { + module: string; +};