From d2ae7505e18aed9e965d7512dd1ceb109b865756 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 30 Jul 2020 15:54:21 +0200 Subject: [PATCH] MOBILE-3449 assign: Add synchronization to more pages --- scripts/langindex.json | 1 + .../mod/assign/components/index/index.ts | 4 +- .../components/submission/submission.ts | 61 +++++++++++-- .../pages/submission-list/submission-list.ts | 88 +++++++++++++------ .../submission-review/submission-review.ts | 2 +- src/addon/mod/assign/providers/assign-sync.ts | 29 ++++-- src/classes/base-sync.ts | 12 +++ src/providers/events.ts | 27 ++++++ 8 files changed, 180 insertions(+), 44 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 8bd92d9cc..d8a010706 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -408,6 +408,7 @@ "addon.mod_assign.submitassignment_help": "assign", "addon.mod_assign.submittedearly": "assign", "addon.mod_assign.submittedlate": "assign", + "addon.mod_assign.syncblockedusercomponent": "local_moodlemobileapp", "addon.mod_assign.timemodified": "assign", "addon.mod_assign.timeremaining": "assign", "addon.mod_assign.ungroupedusers": "assign", diff --git a/src/addon/mod/assign/components/index/index.ts b/src/addon/mod/assign/components/index/index.ts index b47535c64..d7370fea3 100644 --- a/src/addon/mod/assign/components/index/index.ts +++ b/src/addon/mod/assign/components/index/index.ts @@ -299,7 +299,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo */ protected hasSyncSucceed(result: any): boolean { if (result.updated) { - this.submissionComponent && this.submissionComponent.invalidateAndRefresh(); + this.submissionComponent && this.submissionComponent.invalidateAndRefresh(false); } return result.updated; @@ -324,7 +324,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo } return Promise.all(promises).finally(() => { - this.submissionComponent && this.submissionComponent.invalidateAndRefresh(); + this.submissionComponent && this.submissionComponent.invalidateAndRefresh(true); }); } diff --git a/src/addon/mod/assign/components/submission/submission.ts b/src/addon/mod/assign/components/submission/submission.ts index 2031114ba..42ea027ba 100644 --- a/src/addon/mod/assign/components/submission/submission.ts +++ b/src/addon/mod/assign/components/submission/submission.ts @@ -16,7 +16,7 @@ import { Component, Input, OnInit, OnDestroy, ViewChild, Optional, ViewChildren, import { NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; -import { CoreEventsProvider } from '@providers/events'; +import { CoreEventsProvider, CoreEventObserver } from '@providers/events'; import { CoreGroupsProvider } from '@providers/groups'; import { CoreLangProvider } from '@providers/lang'; import { CoreSitesProvider } from '@providers/sites'; @@ -35,7 +35,7 @@ import { } from '../../providers/assign'; import { AddonModAssignHelperProvider } from '../../providers/helper'; import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; -import { AddonModAssignSync } from '../../providers/assign-sync'; +import { AddonModAssignSync, AddonModAssignSyncProvider } from '../../providers/assign-sync'; import { CoreTabsComponent } from '@components/tabs/tabs'; import { CoreTabComponent } from '@components/tabs/tab'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; @@ -109,6 +109,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { protected submissionStatusAvailable: boolean; // Whether we were able to retrieve the submission status. protected originalGrades: any = {}; // Object with the original grade data, to check for changes. protected isDestroyed: boolean; // Whether the component has been destroyed. + protected syncObserver: CoreEventObserver; constructor(protected navCtrl: NavController, protected appProvider: CoreAppProvider, protected domUtils: CoreDomUtilsProvider, sitesProvider: CoreSitesProvider, protected syncProvider: CoreSyncProvider, protected timeUtils: CoreTimeUtilsProvider, @@ -131,7 +132,29 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { this.selectedTab = this.showGrade && this.showGrade !== 'false' ? 1 : 0; this.isSubmittedForGrading = !!this.submitId; - this.loadData(); + this.loadData(true); + + // Refresh data if this assign is synchronized and it's grading. + const events = [AddonModAssignSyncProvider.AUTO_SYNCED, AddonModAssignSyncProvider.MANUAL_SYNCED]; + + this.syncObserver = this.eventsProvider.onMultiple(events, async (data) => { + // Check that user is grading and this grade wasn't blocked when sync was performed. + if (!this.loaded || !this.isGrading || data.gradesBlocked.indexOf(this.submitId) != -1) { + return; + } + + if (data.context == 'submission' && data.submitId == this.submitId) { + // Manual sync triggered by this same submission, ignore it. + return; + } + + // Don't refresh if the user has modified some data. + const hasDataToSave = await this.hasDataToSave(); + + if (!hasDataToSave) { + this.invalidateAndRefresh(false); + } + }, this.siteId); } /** @@ -243,7 +266,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { }, this.siteId); } else { // Invalidate and refresh data to update this view. - this.invalidateAndRefresh(); + this.invalidateAndRefresh(true); } }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'core.error', true); @@ -336,9 +359,10 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { /** * Invalidate and refresh data. * + * @param sync Whether to try to synchronize data. * @return Promise resolved when done. */ - invalidateAndRefresh(): Promise { + invalidateAndRefresh(sync?: boolean): Promise { this.loaded = false; const promises = []; @@ -363,16 +387,17 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { return Promise.all(promises).catch(() => { // Ignore errors. }).then(() => { - return this.loadData(); + return this.loadData(sync); }); } /** * Load the data to render the submission. * + * @param sync Whether to try to synchronize data. * @return Promise resolved when done. */ - protected async loadData(): Promise { + protected async loadData(sync?: boolean): Promise { let isBlind = !!this.blindId; this.previousAttempt = undefined; @@ -387,6 +412,25 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { // Get the assignment. this.assign = await this.assignProvider.getAssignment(this.courseId, this.moduleId); + if (this.submitId != this.currentUserId && sync) { + // Teacher viewing a student submission. Try to sync the assign, there could be offline grades stored. + try { + const result = await AddonModAssignSync.instance.syncAssign(this.assign.id); + + if (result && result.updated) { + this.eventsProvider.trigger(AddonModAssignSyncProvider.MANUAL_SYNCED, { + assignId: this.assign.id, + warnings: result.warnings, + gradesBlocked: result.gradesBlocked, + context: 'submission', + submitId: this.submitId, + }, this.siteId); + } + } catch (error) { + // Ignore errors, probably user is offline or sync is blocked. + } + } + const time = this.timeUtils.timestamp(); let promises = []; @@ -785,7 +829,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { return this.discardDrafts(); }).finally(() => { // Invalidate and refresh data. - this.invalidateAndRefresh(); + this.invalidateAndRefresh(true); this.eventsProvider.trigger(AddonModAssignProvider.GRADED_EVENT, { assignmentId: this.assign.id, @@ -1008,6 +1052,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.setGradeSyncBlocked(false); this.isDestroyed = true; + this.syncObserver && this.syncObserver.off(); } } diff --git a/src/addon/mod/assign/pages/submission-list/submission-list.ts b/src/addon/mod/assign/pages/submission-list/submission-list.ts index 1c1429011..3ca5c7674 100644 --- a/src/addon/mod/assign/pages/submission-list/submission-list.ts +++ b/src/addon/mod/assign/pages/submission-list/submission-list.ts @@ -15,7 +15,7 @@ import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; import { IonicPage, NavParams } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreEventsProvider } from '@providers/events'; +import { CoreEventsProvider, CoreEventObserver } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; @@ -23,6 +23,7 @@ import { AddonModAssignProvider, AddonModAssignAssign, AddonModAssignGrade, AddonModAssignSubmission } from '../../providers/assign'; import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; +import { AddonModAssignSyncProvider, AddonModAssignSync } from '../../providers/assign-sync'; import { AddonModAssignHelperProvider, AddonModAssignSubmissionFormatted } from '../../providers/helper'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; @@ -54,10 +55,11 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { protected moduleId: number; // Module ID the submission belongs to. protected courseId: number; // Course ID the assignment belongs to. protected selectedStatus: string; // The status to see. - protected gradedObserver; // Observer to refresh data when a grade changes. + protected gradedObserver: CoreEventObserver; // Observer to refresh data when a grade changes. + protected syncObserver: CoreEventObserver; // OObserver to refresh data when the async is synchronized. protected submissionsData: {canviewsubmissions: boolean, submissions?: AddonModAssignSubmission[]}; - constructor(navParams: NavParams, protected sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, + constructor(navParams: NavParams, protected sitesProvider: CoreSitesProvider, protected eventsProvider: CoreEventsProvider, protected domUtils: CoreDomUtilsProvider, protected translate: TranslateService, protected assignProvider: AddonModAssignProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider, protected assignHelper: AddonModAssignHelperProvider, protected groupsProvider: CoreGroupsProvider) { @@ -79,22 +81,37 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { // Update data if some grade changes. this.gradedObserver = eventsProvider.on(AddonModAssignProvider.GRADED_EVENT, (data) => { - if (this.assign && data.assignmentId == this.assign.id && data.userId == sitesProvider.getCurrentSiteUserId()) { + if (this.loaded && this.assign && data.assignmentId == this.assign.id && + data.userId == sitesProvider.getCurrentSiteUserId()) { // Grade changed, refresh the data. this.loaded = false; - this.refreshAllData().finally(() => { + this.refreshAllData(true).finally(() => { this.loaded = true; }); } }, sitesProvider.getCurrentSiteId()); + + // Refresh data if this assign is synchronized. + const events = [AddonModAssignSyncProvider.AUTO_SYNCED, AddonModAssignSyncProvider.MANUAL_SYNCED]; + this.syncObserver = eventsProvider.onMultiple(events, (data) => { + if (!this.loaded || data.context == 'submission-list') { + return; + } + + this.loaded = false; + + this.refreshAllData(false).finally(() => { + this.loaded = true; + }); + }, this.sitesProvider.getCurrentSiteId()); } /** * Component being initialized. */ ngOnInit(): void { - this.fetchAssignment().finally(() => { + this.fetchAssignment(true).finally(() => { if (!this.selectedSubmissionId && this.splitviewCtrl.isOn() && this.submissions.length > 0) { // Take first and load it. this.loadSubmission(this.submissions[0]); @@ -107,34 +124,49 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { /** * Fetch assignment data. * + * @param sync Whether to try to synchronize data. * @return Promise resolved when done. */ - protected fetchAssignment(): Promise { + protected async fetchAssignment(sync?: boolean): Promise { + try { + // Get assignment data. + this.assign = await this.assignProvider.getAssignment(this.courseId, this.moduleId); - // Get assignment data. - return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assign) => { - this.title = assign.name || this.title; - this.assign = assign; + this.title = this.assign.name || this.title; - // Get assignment submissions. - return this.assignProvider.getSubmissions(assign.id); - }).then((data) => { - if (!data.canviewsubmissions) { - // User shouldn't be able to reach here. - return Promise.reject(null); + if (sync) { + try { + // Try to synchronize data. + const result = await AddonModAssignSync.instance.syncAssign(this.assign.id); + + if (result && result.updated) { + this.eventsProvider.trigger(AddonModAssignSyncProvider.MANUAL_SYNCED, { + assignId: this.assign.id, + warnings: result.warnings, + gradesBlocked: result.gradesBlocked, + context: 'submission-list', + }, this.sitesProvider.getCurrentSiteId()); + } + } catch (error) { + // Ignore errors, probably user is offline or sync is blocked. + } } - this.submissionsData = data; + // Get assignment submissions. + this.submissionsData = await this.assignProvider.getSubmissions(this.assign.id); + + if (!this.submissionsData.canviewsubmissions) { + // User shouldn't be able to reach here. + throw new Error('Cannot view submissions.'); + } // Check if groupmode is enabled to avoid showing wrong numbers. - return this.groupsProvider.getActivityGroupInfo(this.assign.cmid, false).then((groupInfo) => { - this.groupInfo = groupInfo; + this.groupInfo = await this.groupsProvider.getActivityGroupInfo(this.assign.cmid, false); - return this.setGroup(this.groupsProvider.validateGroupId(this.groupId, groupInfo)); - }); - }).catch((error) => { + await this.setGroup(this.groupsProvider.validateGroupId(this.groupId, this.groupInfo)); + } catch (error) { this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.'); - }); + } } /** @@ -265,9 +297,10 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { /** * Refresh all the data. * + * @param sync Whether to try to synchronize data. * @return Promise resolved when done. */ - protected refreshAllData(): Promise { + protected refreshAllData(sync?: boolean): Promise { const promises = []; promises.push(this.assignProvider.invalidateAssignmentData(this.courseId)); @@ -279,7 +312,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { } return Promise.all(promises).finally(() => { - return this.fetchAssignment(); + return this.fetchAssignment(sync); }); } @@ -289,7 +322,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { * @param refresher Refresher. */ refreshList(refresher: any): void { - this.refreshAllData().finally(() => { + this.refreshAllData(true).finally(() => { refresher.complete(); }); } @@ -299,6 +332,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.gradedObserver && this.gradedObserver.off(); + this.syncObserver && this.syncObserver.off(); } } diff --git a/src/addon/mod/assign/pages/submission-review/submission-review.ts b/src/addon/mod/assign/pages/submission-review/submission-review.ts index 030b7c112..d3d2ed5ba 100644 --- a/src/addon/mod/assign/pages/submission-review/submission-review.ts +++ b/src/addon/mod/assign/pages/submission-review/submission-review.ts @@ -137,7 +137,7 @@ export class AddonModAssignSubmissionReviewPage implements OnInit { } return Promise.all(promises).finally(() => { - this.submissionComponent && this.submissionComponent.invalidateAndRefresh(); + this.submissionComponent && this.submissionComponent.invalidateAndRefresh(true); return this.fetchSubmission(); }); diff --git a/src/addon/mod/assign/providers/assign-sync.ts b/src/addon/mod/assign/providers/assign-sync.ts index 376f040b8..f0e14fe95 100644 --- a/src/addon/mod/assign/providers/assign-sync.ts +++ b/src/addon/mod/assign/providers/assign-sync.ts @@ -25,7 +25,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; -import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreSyncBaseProvider, CoreSyncBlockedError } from '@classes/base-sync'; import { AddonModAssignProvider, AddonModAssignAssign, AddonModAssignSubmission } from './assign'; import { AddonModAssignOfflineProvider } from './assign-offline'; import { AddonModAssignSubmissionDelegate } from './submission-delegate'; @@ -46,6 +46,11 @@ export interface AddonModAssignSyncResult { * Whether data was updated in the site. */ updated: boolean; + + /** + * Whether some grade couldn't be synced because it was blocked. + */ + gradesBlocked: number[]; } /** @@ -55,6 +60,7 @@ export interface AddonModAssignSyncResult { export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { static AUTO_SYNCED = 'addon_mod_assign_autom_synced'; + static MANUAL_SYNCED = 'addon_mod_assign_manual_synced'; protected componentTranslate: string; @@ -161,6 +167,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { this.eventsProvider.trigger(AddonModAssignSyncProvider.AUTO_SYNCED, { assignId: assignId, warnings: data.warnings, + gradesBlocked: data.gradesBlocked, }, siteId); })); } @@ -199,7 +206,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { if (this.syncProvider.isBlocked(AddonModAssignProvider.COMPONENT, assignId, siteId)) { this.logger.error('Cannot sync assign ' + assignId + ' because it is blocked.'); - throw new Error(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate})); + throw new CoreSyncBlockedError(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate})); } return this.addOngoingSync(assignId, this.performSyncAssign(assignId, siteId), siteId); @@ -219,6 +226,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { const result: AddonModAssignSyncResult = { warnings: [], updated: false, + gradesBlocked: [], }; // Load offline data and sync offline logs. @@ -254,9 +262,18 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { })); promises = promises.concat(grades.map(async (grade) => { - await this.syncSubmissionGrade(assign, grade, result.warnings, courseId, siteId); + try { + await this.syncSubmissionGrade(assign, grade, result.warnings, courseId, siteId); - result.updated = true; + result.updated = true; + } catch (error) { + if (error instanceof CoreSyncBlockedError) { + // Grade blocked, but allow finish the sync. + result.gradesBlocked.push(grade.userid); + } else { + throw error; + } + } })); await Promise.all(promises); @@ -408,9 +425,9 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { // Check if this grade sync is blocked. if (this.syncProvider.isBlocked(AddonModAssignProvider.COMPONENT, syncId, siteId)) { - this.logger.error(`Cannot sync grade for assign ${assign.id} and user ${userId} because it is blocked.`); + this.logger.error(`Cannot sync grade for assign ${assign.id} and user ${userId} because it is blocked.!!!!`); - throw new Error(this.translate.instant('core.errorsyncblocked', + throw new CoreSyncBlockedError(this.translate.instant('core.errorsyncblocked', {$a: this.translate.instant('addon.mod_assign.syncblockedusercomponent')})); } diff --git a/src/classes/base-sync.ts b/src/classes/base-sync.ts index 82537d37d..81d7f8ed8 100644 --- a/src/classes/base-sync.ts +++ b/src/classes/base-sync.ts @@ -20,6 +20,18 @@ import { CoreAppProvider } from '@providers/app'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; +/** + * Blocked sync error. + */ +export class CoreSyncBlockedError extends Error { + constructor(message: string) { + super(message); + + // Set the prototype explicitly, otherwise instanceof won't work as expected. + Object.setPrototypeOf(this, CoreSyncBlockedError.prototype); + } +} + /** * Base class to create sync providers. It provides some common functions. */ diff --git a/src/providers/events.ts b/src/providers/events.ts index 81493e9f6..3974e7281 100644 --- a/src/providers/events.ts +++ b/src/providers/events.ts @@ -124,6 +124,33 @@ export class CoreEventsProvider { }; } + /** + * Listen for several events. To stop listening to the events: + * let observer = eventsProvider.onMultiple(['something', 'another'], myCallBack); + * ... + * observer.off(); + * + * @param eventNames Names of the events to listen to. + * @param callBack Function to call when any of the events is triggered. + * @param siteId Site where to trigger the event. Undefined won't check the site. + * @return Observer to stop listening. + */ + onMultiple(eventNames: string[], callBack: (value: any) => void, siteId?: string): CoreEventObserver { + + const observers = eventNames.map((name) => { + return this.on(name, callBack, siteId); + }); + + // Create and return a CoreEventObserver. + return { + off: (): void => { + observers.forEach((observer) => { + observer.off(); + }); + } + }; + } + /** * Triggers an event, notifying all the observers. *