commit
						c3bbcd2702
					
				| @ -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", | ||||
|  | ||||
| @ -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); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -20,7 +20,7 @@ | ||||
|     </ion-item> | ||||
| 
 | ||||
|     <!-- Tabs: see the submission or grade it. --> | ||||
|     <core-tabs [selectedIndex]="selectedTab" [hideUntil]="loaded" parentScrollable="true"> | ||||
|     <core-tabs [selectedIndex]="selectedTab" [hideUntil]="loaded" parentScrollable="true" (ionChange)="tabSelected($event)"> | ||||
|         <!-- View the submission tab. --> | ||||
|         <core-tab [title]="'addon.mod_assign.submission' | translate"> | ||||
|             <ng-template> | ||||
|  | ||||
| @ -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,9 @@ import { | ||||
| } from '../../providers/assign'; | ||||
| import { AddonModAssignHelperProvider } from '../../providers/helper'; | ||||
| import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; | ||||
| 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'; | ||||
| import { AddonModAssignSubmissionPluginComponent } from '../submission-plugin/submission-plugin'; | ||||
| 
 | ||||
| @ -107,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, | ||||
| @ -129,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); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -241,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); | ||||
| @ -334,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<any> { | ||||
|     invalidateAndRefresh(sync?: boolean): Promise<any> { | ||||
|         this.loaded = false; | ||||
| 
 | ||||
|         const promises = []; | ||||
| @ -361,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 loadData(): Promise<any> { | ||||
|     protected async loadData(sync?: boolean): Promise<any> { | ||||
|         let isBlind = !!this.blindId; | ||||
| 
 | ||||
|         this.previousAttempt = undefined; | ||||
| @ -381,44 +408,53 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | ||||
|             isBlind = false; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             // Get the assignment.
 | ||||
|         return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assign) => { | ||||
|             const time = this.timeUtils.timestamp(), | ||||
|                 promises = []; | ||||
|             this.assign = await this.assignProvider.getAssignment(this.courseId, this.moduleId); | ||||
| 
 | ||||
|             this.assign = assign; | ||||
|             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 (assign.allowsubmissionsfromdate && assign.allowsubmissionsfromdate >= time) { | ||||
|                 this.fromDate = this.timeUtils.userDate(assign.allowsubmissionsfromdate * 1000); | ||||
|                     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 = []; | ||||
| 
 | ||||
|             if (this.assign.allowsubmissionsfromdate && this.assign.allowsubmissionsfromdate >= time) { | ||||
|                 this.fromDate = this.timeUtils.userDate(this.assign.allowsubmissionsfromdate * 1000); | ||||
|             } | ||||
| 
 | ||||
|             this.currentAttempt = 0; | ||||
|             this.maxAttemptsText = this.translate.instant('addon.mod_assign.unlimitedattempts'); | ||||
|             this.blindMarking = this.isSubmittedForGrading && assign.blindmarking && !assign.revealidentities; | ||||
|             this.blindMarking = this.isSubmittedForGrading && this.assign.blindmarking && !this.assign.revealidentities; | ||||
| 
 | ||||
|             if (!this.blindMarking && this.submitId != this.currentUserId) { | ||||
|                 promises.push(this.userProvider.getProfile(this.submitId, this.courseId).then((profile) => { | ||||
|                     this.user = profile; | ||||
|                 })); | ||||
|                 promises.push(this.loadSubmissionUserProfile()); | ||||
|             } | ||||
| 
 | ||||
|             // Check if there's any offline data for this submission.
 | ||||
|             promises.push(this.assignOfflineProvider.getSubmission(assign.id, this.submitId).then((data) => { | ||||
|                 this.hasOffline = data && data.plugindata && Object.keys(data.plugindata).length > 0; | ||||
|                 this.submittedOffline = data && data.submitted; | ||||
|             }).catch(() => { | ||||
|                 // No offline data found.
 | ||||
|                 this.hasOffline = false; | ||||
|                 this.submittedOffline = false; | ||||
|             })); | ||||
|             promises.push(this.loadSubmissionOfflineData()); | ||||
| 
 | ||||
|             await Promise.all(promises); | ||||
| 
 | ||||
|             return Promise.all(promises); | ||||
|         }).then(() => { | ||||
|             // Get submission status.
 | ||||
|             return this.assignProvider.getSubmissionStatusWithRetry(this.assign, this.submitId, undefined, isBlind); | ||||
|         }).then((response) => { | ||||
|             const response = await this.assignProvider.getSubmissionStatusWithRetry(this.assign, this.submitId, undefined, isBlind); | ||||
| 
 | ||||
|             const promises = []; | ||||
|             promises = []; | ||||
| 
 | ||||
|             this.submissionStatusAvailable = true; | ||||
|             this.lastAttempt = response.lastattempt; | ||||
| @ -450,16 +486,41 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | ||||
|             } | ||||
| 
 | ||||
|             // Get the submission plugins that don't support editing.
 | ||||
|             promises.push(this.assignProvider.getUnsupportedEditPlugins(this.userSubmission.plugins).then((list) => { | ||||
|                 this.unsupportedEditPlugins = list; | ||||
|             })); | ||||
|             promises.push(this.loadUnsupportedPlugins()); | ||||
| 
 | ||||
|             return Promise.all(promises); | ||||
|         }).catch((error) => { | ||||
|             await Promise.all(promises); | ||||
|         } catch (error) { | ||||
|             this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.'); | ||||
|         }).finally(() => { | ||||
|         } finally { | ||||
|             this.loaded = true; | ||||
|         }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load profile of submission's user. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async loadSubmissionUserProfile(): Promise<void> { | ||||
|         this.user = await this.userProvider.getProfile(this.submitId, this.courseId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load offline data for the submission (not the submission grade). | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async loadSubmissionOfflineData(): Promise<void> { | ||||
|         try { | ||||
|             const data = await this.assignOfflineProvider.getSubmission(this.assign.id, this.submitId); | ||||
| 
 | ||||
|             this.hasOffline = data && data.plugindata && Object.keys(data.plugindata).length > 0; | ||||
|             this.submittedOffline = data && data.submitted; | ||||
|         } catch (error) { | ||||
|             // No offline data found.
 | ||||
|             this.hasOffline = false; | ||||
|             this.submittedOffline = false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -537,11 +598,6 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | ||||
|             // Make sure outcomes is an array.
 | ||||
|             gradeInfo.outcomes = gradeInfo.outcomes || []; | ||||
| 
 | ||||
|             if (!this.isDestroyed) { | ||||
|                 // Block the assignment.
 | ||||
|                 this.syncProvider.blockOperation(AddonModAssignProvider.COMPONENT, this.assign.id); | ||||
|             } | ||||
| 
 | ||||
|             // Treat the grade info.
 | ||||
|             return this.treatGradeInfo(); | ||||
|         }).then(() => { | ||||
| @ -627,6 +683,15 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the submission plugins that don't support editing. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async loadUnsupportedPlugins(): Promise<void> { | ||||
|         this.unsupportedEditPlugins = await this.assignProvider.getUnsupportedEditPlugins(this.userSubmission.plugins); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the submission status name and class. | ||||
|      * | ||||
| @ -764,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, | ||||
| @ -952,15 +1017,42 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Block or unblock the automatic sync of the user grade. | ||||
|      * | ||||
|      * @param block Whether to block or unblock. | ||||
|      */ | ||||
|     protected setGradeSyncBlocked(block?: boolean): void { | ||||
|         if (this.isDestroyed || !this.assign || !this.isGrading) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const syncId = AddonModAssignSync.instance.getGradeSyncId(this.assign.id, this.submitId); | ||||
| 
 | ||||
|         if (block) { | ||||
|             this.syncProvider.blockOperation(AddonModAssignProvider.COMPONENT, syncId); | ||||
|         } else { | ||||
|             this.syncProvider.unblockOperation(AddonModAssignProvider.COMPONENT, syncId); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * A certain tab has been selected, either manually or automatically. | ||||
|      * | ||||
|      * @param tab The tab that was selected. | ||||
|      */ | ||||
|     tabSelected(tab: CoreTabComponent): void { | ||||
|         // Block sync when selecting grade tab, unblock when leaving it.
 | ||||
|         this.setGradeSyncBlocked(this.tabs.getIndex(tab) === 1); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.setGradeSyncBlocked(false); | ||||
|         this.isDestroyed = true; | ||||
| 
 | ||||
|         if (this.assign && this.isGrading) { | ||||
|             this.syncProvider.unblockOperation(AddonModAssignProvider.COMPONENT, this.assign.id); | ||||
|         } | ||||
|         this.syncObserver && this.syncObserver.off(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -89,6 +89,7 @@ | ||||
|     "submitassignment": "Submit assignment", | ||||
|     "submittedearly": "Assignment was submitted {{$a}} early", | ||||
|     "submittedlate": "Assignment was submitted {{$a}} late", | ||||
|     "syncblockedusercomponent": "user grade", | ||||
|     "timemodified": "Last modified", | ||||
|     "timeremaining": "Time remaining", | ||||
|     "ungroupedusers": "The setting 'Require group to make submission' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions.", | ||||
|  | ||||
| @ -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<any> { | ||||
| 
 | ||||
|     protected async fetchAssignment(sync?: boolean): Promise<void> { | ||||
|         try { | ||||
|             // Get assignment data.
 | ||||
|         return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assign) => { | ||||
|             this.title = assign.name || this.title; | ||||
|             this.assign = assign; | ||||
|             this.assign = await this.assignProvider.getAssignment(this.courseId, this.moduleId); | ||||
| 
 | ||||
|             // 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); | ||||
|             this.title = this.assign.name || this.title; | ||||
| 
 | ||||
|             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<any> { | ||||
|     protected refreshAllData(sync?: boolean): Promise<any> { | ||||
|         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(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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(); | ||||
|         }); | ||||
|  | ||||
| @ -25,12 +25,14 @@ 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 { AddonModAssignProvider, AddonModAssignAssign } from './assign'; | ||||
| import { CoreSyncBaseProvider, CoreSyncBlockedError } from '@classes/base-sync'; | ||||
| import { AddonModAssignProvider, AddonModAssignAssign, AddonModAssignSubmission } from './assign'; | ||||
| import { AddonModAssignOfflineProvider } from './assign-offline'; | ||||
| import { AddonModAssignSubmissionDelegate } from './submission-delegate'; | ||||
| import { AddonModAssignFeedbackDelegate } from './feedback-delegate'; | ||||
| 
 | ||||
| import { makeSingleton } from '@singletons/core.singletons'; | ||||
| 
 | ||||
| /** | ||||
|  * Data returned by an assign sync. | ||||
|  */ | ||||
| @ -44,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[]; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
| @ -53,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; | ||||
| 
 | ||||
| @ -79,6 +87,17 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||
|         this.componentTranslate = courseProvider.translateModuleName('assign'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the sync ID for a certain user grade. | ||||
|      * | ||||
|      * @param assignId Assign ID. | ||||
|      * @param userId User the grade belongs to. | ||||
|      * @return Sync ID. | ||||
|      */ | ||||
|     getGradeSyncId(assignId: number, userId: number): string { | ||||
|         return 'assignGrade#' + assignId + '#' + userId; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convenience function to get scale selected option. | ||||
|      * | ||||
| @ -121,7 +140,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||
|      * @param force Wether to force sync not depending on last execution. | ||||
|      * @return Promise resolved if sync is successful, rejected if sync fails. | ||||
|      */ | ||||
|     syncAllAssignments(siteId?: string, force?: boolean): Promise<any> { | ||||
|     syncAllAssignments(siteId?: string, force?: boolean): Promise<void> { | ||||
|         return this.syncOnSites('all assignments', this.syncAllAssignmentsFunc.bind(this), [force], siteId); | ||||
|     } | ||||
| 
 | ||||
| @ -132,26 +151,25 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||
|      * @param force Wether to force sync not depending on last execution. | ||||
|      * @param Promise resolved if sync is successful, rejected if sync fails. | ||||
|      */ | ||||
|     protected syncAllAssignmentsFunc(siteId?: string, force?: boolean): Promise<any> { | ||||
|     protected async syncAllAssignmentsFunc(siteId?: string, force?: boolean): Promise<void> { | ||||
|         // Get all assignments that have offline data.
 | ||||
|         return this.assignOfflineProvider.getAllAssigns(siteId).then((assignIds) => { | ||||
|             // Sync all assignments that haven't been synced for a while.
 | ||||
|             const promises = assignIds.map((assignId) => { | ||||
|                 const promise = force ? this.syncAssign(assignId, siteId) : this.syncAssignIfNeeded(assignId, siteId); | ||||
|         const assignIds = await this.assignOfflineProvider.getAllAssigns(siteId); | ||||
| 
 | ||||
|         // Try to sync all assignments.
 | ||||
|         await Promise.all(assignIds.map(async (assignId) => { | ||||
|             const data = force ? await this.syncAssign(assignId, siteId) : await this.syncAssignIfNeeded(assignId, siteId); | ||||
| 
 | ||||
|             if (!data || !data.updated) { | ||||
|                 // Not updated.
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|                 return promise.then((data) => { | ||||
|                     if (data && data.updated) { | ||||
|                         // Sync done. Send event.
 | ||||
|             this.eventsProvider.trigger(AddonModAssignSyncProvider.AUTO_SYNCED, { | ||||
|                 assignId: assignId, | ||||
|                             warnings: data.warnings | ||||
|                 warnings: data.warnings, | ||||
|                 gradesBlocked: data.gradesBlocked, | ||||
|             }, siteId); | ||||
|                     } | ||||
|                 }); | ||||
|             }); | ||||
| 
 | ||||
|             return Promise.all(promises); | ||||
|         }); | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -161,12 +179,12 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when the assign is synced or it doesn't need to be synced. | ||||
|      */ | ||||
|     syncAssignIfNeeded(assignId: number, siteId?: string): Promise<void | AddonModAssignSyncResult> { | ||||
|         return this.isSyncNeeded(assignId, siteId).then((needed) => { | ||||
|     async syncAssignIfNeeded(assignId: number, siteId?: string): Promise<void | AddonModAssignSyncResult> { | ||||
|         const needed = await this.isSyncNeeded(assignId, siteId); | ||||
| 
 | ||||
|         if (needed) { | ||||
|             return this.syncAssign(assignId, siteId); | ||||
|         } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -176,18 +194,9 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved in success. | ||||
|      */ | ||||
|     syncAssign(assignId: number, siteId?: string): Promise<AddonModAssignSyncResult> { | ||||
|     async syncAssign(assignId: number, siteId?: string): Promise<AddonModAssignSyncResult> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         const promises: Promise<any>[] = [], | ||||
|             result: AddonModAssignSyncResult = { | ||||
|                 warnings: [], | ||||
|                 updated: false | ||||
|             }; | ||||
|         let assign: AddonModAssignAssign, | ||||
|             courseId: number, | ||||
|             syncPromise: Promise<any>; | ||||
| 
 | ||||
|         if (this.isSyncing(assignId, siteId)) { | ||||
|             // There's already a sync ongoing for this assign, return the promise.
 | ||||
|             return this.getOngoingSync(assignId, siteId); | ||||
| @ -195,79 +204,126 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||
| 
 | ||||
|         // Verify that assign isn't blocked.
 | ||||
|         if (this.syncProvider.isBlocked(AddonModAssignProvider.COMPONENT, assignId, siteId)) { | ||||
|             this.logger.debug('Cannot sync assign ' + assignId + ' because it is blocked.'); | ||||
|             this.logger.error('Cannot sync assign ' + assignId + ' because it is blocked.'); | ||||
| 
 | ||||
|             return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate})); | ||||
|             throw new CoreSyncBlockedError(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate})); | ||||
|         } | ||||
| 
 | ||||
|         this.logger.debug('Try to sync assign ' + assignId + ' in site ' + siteId); | ||||
|         return this.addOngoingSync(assignId, this.performSyncAssign(assignId, siteId), siteId); | ||||
|     } | ||||
| 
 | ||||
|         // Get offline submissions to be sent.
 | ||||
|         promises.push(this.assignOfflineProvider.getAssignSubmissions(assignId, siteId).catch(() => { | ||||
|             // No offline data found, return empty array.
 | ||||
|             return []; | ||||
|         })); | ||||
|     /** | ||||
|      * Perform the assign submission. | ||||
|      * | ||||
|      * @param assignId Assign ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved in success. | ||||
|      */ | ||||
|     protected async performSyncAssign(assignId: number, siteId?: string): Promise<AddonModAssignSyncResult> { | ||||
| 
 | ||||
|         // Get offline submission grades to be sent.
 | ||||
|         promises.push(this.assignOfflineProvider.getAssignSubmissionsGrade(assignId, siteId).catch(() => { | ||||
|             // No offline data found, return empty array.
 | ||||
|             return []; | ||||
|         })); | ||||
|         this.logger.error('Try to sync assign ' + assignId + ' in site ' + siteId); | ||||
| 
 | ||||
|         // Sync offline logs.
 | ||||
|         promises.push(this.logHelper.syncIfNeeded(AddonModAssignProvider.COMPONENT, assignId, siteId)); | ||||
|         const result: AddonModAssignSyncResult = { | ||||
|             warnings: [], | ||||
|             updated: false, | ||||
|             gradesBlocked: [], | ||||
|         }; | ||||
| 
 | ||||
|         syncPromise = Promise.all(promises).then((results) => { | ||||
|             const submissions = results[0], | ||||
|                 grades = results[1]; | ||||
|         // Load offline data and sync offline logs.
 | ||||
|         const promisesResults = await Promise.all([ | ||||
|             this.getOfflineSubmissions(assignId, siteId), | ||||
|             this.getOfflineGrades(assignId, siteId), | ||||
|             this.logHelper.syncIfNeeded(AddonModAssignProvider.COMPONENT, assignId, siteId), | ||||
|         ]); | ||||
| 
 | ||||
|         const submissions = promisesResults[0]; | ||||
|         const grades = promisesResults[1]; | ||||
| 
 | ||||
|         if (!submissions.length && !grades.length) { | ||||
|             // Nothing to sync.
 | ||||
|                 return; | ||||
|             await this.utils.ignoreErrors(this.setSyncTime(assignId, siteId)); | ||||
| 
 | ||||
|             return result; | ||||
|         } else if (!this.appProvider.isOnline()) { | ||||
|             // Cannot sync in offline.
 | ||||
|                 return Promise.reject(null); | ||||
|             throw new Error(this.translate.instant('core.cannotconnect')); | ||||
|         } | ||||
| 
 | ||||
|             courseId = submissions.length > 0 ? submissions[0].courseid : grades[0].courseid; | ||||
|         const courseId = submissions.length > 0 ? submissions[0].courseid : grades[0].courseid; | ||||
| 
 | ||||
|             return this.assignProvider.getAssignmentById(courseId, assignId, false, siteId).then((assignData) => { | ||||
|                 assign = assignData; | ||||
|         const assign = await this.assignProvider.getAssignmentById(courseId, assignId, false, siteId); | ||||
| 
 | ||||
|                 const promises = []; | ||||
|         let promises = []; | ||||
| 
 | ||||
|         promises = promises.concat(submissions.map(async (submission) => { | ||||
|             await this.syncSubmission(assign, submission, result.warnings, siteId); | ||||
| 
 | ||||
|                 submissions.forEach((submission) => { | ||||
|                     promises.push(this.syncSubmission(assign, submission, result.warnings, siteId).then(() => { | ||||
|             result.updated = true; | ||||
|         })); | ||||
|                 }); | ||||
| 
 | ||||
|                 grades.forEach((grade) => { | ||||
|                     promises.push(this.syncSubmissionGrade(assign, grade, result.warnings, courseId, siteId).then(() => { | ||||
|         promises = promises.concat(grades.map(async (grade) => { | ||||
|             try { | ||||
|                 await this.syncSubmissionGrade(assign, grade, result.warnings, courseId, siteId); | ||||
| 
 | ||||
|                 result.updated = true; | ||||
|             } catch (error) { | ||||
|                 if (error instanceof CoreSyncBlockedError) { | ||||
|                     // Grade blocked, but allow finish the sync.
 | ||||
|                     result.gradesBlocked.push(grade.userid); | ||||
|                 } else { | ||||
|                     throw error; | ||||
|                 } | ||||
|             } | ||||
|         })); | ||||
|                 }); | ||||
| 
 | ||||
|                 return Promise.all(promises); | ||||
|             }).then(() => { | ||||
|         await Promise.all(promises); | ||||
| 
 | ||||
|         if (result.updated) { | ||||
|             // Data has been sent to server. Now invalidate the WS calls.
 | ||||
|                     return this.assignProvider.invalidateContent(assign.cmid, courseId, siteId).catch(() => { | ||||
|                         // Ignore errors.
 | ||||
|                     }); | ||||
|             await this.utils.ignoreErrors(this.assignProvider.invalidateContent(assign.cmid, courseId, siteId)); | ||||
|         } | ||||
|             }); | ||||
|         }).then(() => { | ||||
| 
 | ||||
|         // Sync finished, set sync time.
 | ||||
|             return this.setSyncTime(assignId, siteId).catch(() => { | ||||
|                 // Ignore errors.
 | ||||
|             }); | ||||
|         }).then(() => { | ||||
|         await this.utils.ignoreErrors(this.setSyncTime(assignId, siteId)); | ||||
| 
 | ||||
|         // All done, return the result.
 | ||||
|         return result; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|         return this.addOngoingSync(assignId, syncPromise, siteId); | ||||
|     /** | ||||
|      * Get offline grades to be sent. | ||||
|      * | ||||
|      * @param assignId Assign ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise with grades. | ||||
|      */ | ||||
|     protected async getOfflineGrades(assignId: number, siteId: string): Promise<any[]> { | ||||
|         try { | ||||
|             const submissions = await this.assignOfflineProvider.getAssignSubmissionsGrade(assignId, siteId); | ||||
| 
 | ||||
|             return submissions; | ||||
|         } catch (error) { | ||||
|             // No offline data found, return empty array.
 | ||||
|             return []; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get offline submissions to be sent. | ||||
|      * | ||||
|      * @param assignId Assign ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise with submissions. | ||||
|      */ | ||||
|     protected async getOfflineSubmissions(assignId: number, siteId: string): Promise<any[]> { | ||||
|         try { | ||||
|             const submissions = await this.assignOfflineProvider.getAssignSubmissions(assignId, siteId); | ||||
| 
 | ||||
|             return submissions; | ||||
|         } catch (error) { | ||||
|             // No offline data found, return empty array.
 | ||||
|             return []; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -279,83 +335,76 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if success, rejected otherwise. | ||||
|      */ | ||||
|     protected syncSubmission(assign: AddonModAssignAssign, offlineData: any, warnings: string[], siteId?: string): Promise<any> { | ||||
|         const userId = offlineData.userid, | ||||
|             pluginData = {}; | ||||
|         let discardError, | ||||
|             submission; | ||||
|     protected async syncSubmission(assign: AddonModAssignAssign, offlineData: any, warnings: string[], siteId?: string) | ||||
|             : Promise<void> { | ||||
| 
 | ||||
|         return this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId).then((status) => { | ||||
|             const promises = []; | ||||
|         const userId = offlineData.userid; | ||||
|         const pluginData = {}; | ||||
| 
 | ||||
|             submission = this.assignProvider.getSubmissionObjectFromAttempt(assign, status.lastattempt); | ||||
|         const status = await this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId); | ||||
| 
 | ||||
|         const submission = this.assignProvider.getSubmissionObjectFromAttempt(assign, status.lastattempt); | ||||
| 
 | ||||
|         if (submission.timemodified != offlineData.onlinetimemodified) { | ||||
|             // The submission was modified in Moodle, discard the submission.
 | ||||
|                 discardError = this.translate.instant('addon.mod_assign.warningsubmissionmodified'); | ||||
|             this.addOfflineDataDeletedWarning(warnings, this.componentTranslate, assign.name, | ||||
|                     this.translate.instant('addon.mod_assign.warningsubmissionmodified')); | ||||
| 
 | ||||
|                 return; | ||||
|             return this.deleteSubmissionData(assign, submission, offlineData, siteId); | ||||
|         } | ||||
| 
 | ||||
|             submission.plugins.forEach((plugin) => { | ||||
|                 promises.push(this.submissionDelegate.preparePluginSyncData(assign, submission, plugin, offlineData, pluginData, | ||||
|                         siteId)); | ||||
|             }); | ||||
|         try { | ||||
|             // Prepare plugins data.
 | ||||
|             await Promise.all(submission.plugins.map(async (plugin) => { | ||||
|                 await this.submissionDelegate.preparePluginSyncData(assign, submission, plugin, offlineData, pluginData, siteId); | ||||
|             })); | ||||
| 
 | ||||
|             return Promise.all(promises).then(() => { | ||||
|             // Now save the submission.
 | ||||
|                 let promise; | ||||
| 
 | ||||
|                 if (!Object.keys(pluginData).length) { | ||||
|                     // Nothing to save.
 | ||||
|                     promise = Promise.resolve(); | ||||
|                 } else { | ||||
|                     promise = this.assignProvider.saveSubmissionOnline(assign.id, pluginData, siteId); | ||||
|             if (Object.keys(pluginData).length > 0) { | ||||
|                 await this.assignProvider.saveSubmissionOnline(assign.id, pluginData, siteId); | ||||
|             } | ||||
| 
 | ||||
|                 return promise.then(() => { | ||||
|             if (assign.submissiondrafts && offlineData.submitted) { | ||||
|                 // The user submitted the assign manually. Submit it for grading.
 | ||||
|                         return this.assignProvider.submitForGradingOnline(assign.id, offlineData.submissionstatement, siteId); | ||||
|                 await this.assignProvider.submitForGradingOnline(assign.id, offlineData.submissionstatement, siteId); | ||||
|             } | ||||
|                 }).then(() => { | ||||
| 
 | ||||
|             // Submission data sent, update cached data. No need to block the user for this.
 | ||||
|             this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId); | ||||
|                 }); | ||||
|             }).catch((error) => { | ||||
|                 if (error && this.utils.isWebServiceError(error)) { | ||||
|         } catch (error) { | ||||
|             if (!error || !this.utils.isWebServiceError(error)) { | ||||
|                 // Local error, reject.
 | ||||
|                 throw error; | ||||
|             } | ||||
| 
 | ||||
|             // A WebService has thrown an error, this means it cannot be submitted. Discard the submission.
 | ||||
|                     discardError = this.textUtils.getErrorMessageFromError(error); | ||||
|                 } else { | ||||
|                     // Couldn't connect to server, reject.
 | ||||
|                     return Promise.reject(error); | ||||
|             this.addOfflineDataDeletedWarning(warnings, this.componentTranslate, assign.name, | ||||
|                 this.textUtils.getErrorMessageFromError(error)); | ||||
|         } | ||||
|             }); | ||||
|         }).then(() => { | ||||
| 
 | ||||
|         // Delete the offline data.
 | ||||
|             return this.assignOfflineProvider.deleteSubmission(assign.id, userId, siteId).then(() => { | ||||
|                 const promises = []; | ||||
| 
 | ||||
|                 submission.plugins.forEach((plugin) => { | ||||
|                     promises.push(this.submissionDelegate.deletePluginOfflineData(assign, submission, plugin, offlineData, siteId)); | ||||
|                 }); | ||||
| 
 | ||||
|                 return Promise.all(promises); | ||||
|             }); | ||||
|         }).then(() => { | ||||
|             if (discardError) { | ||||
|                 // Submission was discarded, add a warning.
 | ||||
|                 const message = this.translate.instant('core.warningofflinedatadeleted', { | ||||
|                     component: this.componentTranslate, | ||||
|                     name: assign.name, | ||||
|                     error: discardError | ||||
|                 }); | ||||
| 
 | ||||
|                 if (warnings.indexOf(message) == -1) { | ||||
|                     warnings.push(message); | ||||
|         await this.deleteSubmissionData(assign, submission, offlineData, siteId); | ||||
|     } | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|     /** | ||||
|      * Delete the submission offline data (not grades). | ||||
|      * | ||||
|      * @param assign Assign. | ||||
|      * @param submission Submission. | ||||
|      * @param offlineData Offline data. | ||||
|      * @param siteId Site ID. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async deleteSubmissionData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, offlineData: any, | ||||
|             siteId?: string): Promise<void> { | ||||
| 
 | ||||
|         // Delete the offline data.
 | ||||
|         await this.assignOfflineProvider.deleteSubmission(assign.id, offlineData.userid, siteId); | ||||
| 
 | ||||
|         // Delete plugins data.
 | ||||
|         await Promise.all(submission.plugins.map(async (plugin) => { | ||||
|             await this.submissionDelegate.deletePluginOfflineData(assign, submission, plugin, offlineData, siteId); | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -368,25 +417,36 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if success, rejected otherwise. | ||||
|      */ | ||||
|     protected syncSubmissionGrade(assign: AddonModAssignAssign, offlineData: any, warnings: string[], courseId: number, | ||||
|     protected async syncSubmissionGrade(assign: AddonModAssignAssign, offlineData: any, warnings: string[], courseId: number, | ||||
|             siteId?: string): Promise<any> { | ||||
| 
 | ||||
|         const userId = offlineData.userid; | ||||
|         let discardError; | ||||
|         const syncId = this.getGradeSyncId(assign.id, userId); | ||||
| 
 | ||||
|         // 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.!!!!`); | ||||
| 
 | ||||
|             throw new CoreSyncBlockedError(this.translate.instant('core.errorsyncblocked', | ||||
|                     {$a: this.translate.instant('addon.mod_assign.syncblockedusercomponent')})); | ||||
|         } | ||||
| 
 | ||||
|         const status = await this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId); | ||||
| 
 | ||||
|         return this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId).then((status) => { | ||||
|         const timemodified = status.feedback && (status.feedback.gradeddate || status.feedback.grade.timemodified); | ||||
| 
 | ||||
|         if (timemodified > offlineData.timemodified) { | ||||
|             // The submission grade was modified in Moodle, discard it.
 | ||||
|                 discardError = this.translate.instant('addon.mod_assign.warningsubmissiongrademodified'); | ||||
|             this.addOfflineDataDeletedWarning(warnings, this.componentTranslate, assign.name, | ||||
|                     this.translate.instant('addon.mod_assign.warningsubmissiongrademodified')); | ||||
| 
 | ||||
|                 return; | ||||
|             return this.assignOfflineProvider.deleteSubmissionGrade(assign.id, userId, siteId); | ||||
|         } | ||||
| 
 | ||||
|         // If grade has been modified from gradebook, do not use offline.
 | ||||
|             return this.gradesHelper.getGradeModuleItems(courseId, assign.cmid, userId, undefined, siteId, true).then((grades) => { | ||||
|                 return this.courseProvider.getModuleBasicGradeInfo(assign.cmid, siteId).then((gradeInfo) => { | ||||
|         const grades = await this.gradesHelper.getGradeModuleItems(courseId, assign.cmid, userId, undefined, siteId, true); | ||||
| 
 | ||||
|         const gradeInfo = await this.courseProvider.getModuleBasicGradeInfo(assign.cmid, siteId); | ||||
| 
 | ||||
|         // Override offline grade and outcomes based on the gradebook data.
 | ||||
|         grades.forEach((grade) => { | ||||
| @ -407,14 +467,14 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|                 }); | ||||
|             }).then(() => { | ||||
| 
 | ||||
|         try { | ||||
|              // Now submit the grade.
 | ||||
|                 return this.assignProvider.submitGradingFormOnline(assign.id, userId, offlineData.grade, offlineData.attemptnumber, | ||||
|             await this.assignProvider.submitGradingFormOnline(assign.id, userId, offlineData.grade, offlineData.attemptnumber, | ||||
|                     offlineData.addattempt, offlineData.workflowstate, offlineData.applytoall, offlineData.outcomes, | ||||
|                         offlineData.plugindata, siteId).then(() => { | ||||
|                     // Grades sent.
 | ||||
|                     // Discard grades drafts.
 | ||||
|                     offlineData.plugindata, siteId); | ||||
| 
 | ||||
|             // Grades sent. Discard grades drafts.
 | ||||
|             const promises = []; | ||||
|             if (status.feedback && status.feedback.plugins) { | ||||
|                 status.feedback.plugins.forEach((plugin) => { | ||||
| @ -425,33 +485,21 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { | ||||
|             // Update cached data.
 | ||||
|             promises.push(this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId)); | ||||
| 
 | ||||
|                     return Promise.all(promises); | ||||
|                 }).catch((error) => { | ||||
|                     if (error && this.utils.isWebServiceError(error)) { | ||||
|                         // The WebService has thrown an error, this means it cannot be submitted. Discard the offline data.
 | ||||
|                         discardError = this.textUtils.getErrorMessageFromError(error); | ||||
|                     } else { | ||||
|                         // Couldn't connect to server, reject.
 | ||||
|                     return Promise.reject(error); | ||||
|             await Promise.all(promises); | ||||
|         } catch (error) { | ||||
|             if (!error || !this.utils.isWebServiceError(error)) { | ||||
|                 // Local error, reject.
 | ||||
|                 throw error; | ||||
|             } | ||||
|                 }); | ||||
|             }); | ||||
|         }).then(() => { | ||||
|             // Delete the offline data.
 | ||||
|             return this.assignOfflineProvider.deleteSubmissionGrade(assign.id, userId, siteId); | ||||
|         }).then(() => { | ||||
|             if (discardError) { | ||||
|                 // Submission grade was discarded, add a warning.
 | ||||
|                 const message = this.translate.instant('core.warningofflinedatadeleted', { | ||||
|                     component: this.componentTranslate, | ||||
|                     name: assign.name, | ||||
|                     error: discardError | ||||
|                 }); | ||||
| 
 | ||||
|                 if (warnings.indexOf(message) == -1) { | ||||
|                     warnings.push(message); | ||||
|             // A WebService has thrown an error, this means it cannot be submitted. Discard the submission.
 | ||||
|             this.addOfflineDataDeletedWarning(warnings, this.componentTranslate, assign.name, | ||||
|                 this.textUtils.getErrorMessageFromError(error)); | ||||
|         } | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         // Delete the offline data.
 | ||||
|         await this.assignOfflineProvider.deleteSubmissionGrade(assign.id, userId, siteId); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class AddonModAssignSync extends makeSingleton(AddonModAssignSyncProvider) {} | ||||
|  | ||||
| @ -408,6 +408,7 @@ | ||||
|     "addon.mod_assign.submitassignment_help": "Once this assignment is submitted you will not be able to make any more changes.", | ||||
|     "addon.mod_assign.submittedearly": "Assignment was submitted {{$a}} early", | ||||
|     "addon.mod_assign.submittedlate": "Assignment was submitted {{$a}} late", | ||||
|     "addon.mod_assign.syncblockedusercomponent": "user grade", | ||||
|     "addon.mod_assign.timemodified": "Last modified", | ||||
|     "addon.mod_assign.timeremaining": "Time remaining", | ||||
|     "addon.mod_assign.ungroupedusers": "The setting 'Require group to make submission' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions.", | ||||
|  | ||||
| @ -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. | ||||
|  */ | ||||
| @ -52,6 +64,26 @@ export class CoreSyncBaseProvider { | ||||
|         this.component = component; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Add an offline data deleted warning to a list of warnings. | ||||
|      * | ||||
|      * @param warnings List of warnings. | ||||
|      * @param component Component. | ||||
|      * @param name Instance name. | ||||
|      * @param error Specific error message. | ||||
|      */ | ||||
|     protected addOfflineDataDeletedWarning(warnings: string[], component: string, name: string, error: string): void { | ||||
|         const warning = this.translate.instant('core.warningofflinedatadeleted', { | ||||
|             component: component, | ||||
|             name: name, | ||||
|             error: error, | ||||
|         }); | ||||
| 
 | ||||
|         if (warnings.indexOf(warning) == -1) { | ||||
|             warnings.push(warning); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Add an ongoing sync to the syncPromises list. On finish the promise will be removed. | ||||
|      * | ||||
| @ -60,7 +92,7 @@ export class CoreSyncBaseProvider { | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return The sync promise. | ||||
|      */ | ||||
|     addOngoingSync(id: string | number, promise: Promise<any>, siteId?: string): Promise<any> { | ||||
|     addOngoingSync<T>(id: string | number, promise: Promise<T>, siteId?: string): Promise<T> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         const uniqueId = this.getUniqueSyncId(id); | ||||
|  | ||||
| @ -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. | ||||
|      * | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user