MOBILE-2061 course: Synchronize offline manual completion
This commit is contained in:
		
							parent
							
								
									be209e9cb5
								
							
						
					
					
						commit
						e4248a8a44
					
				| @ -1108,10 +1108,12 @@ | |||||||
|   "core.course.errorgetmodule": "local_moodlemobileapp", |   "core.course.errorgetmodule": "local_moodlemobileapp", | ||||||
|   "core.course.hiddenfromstudents": "moodle", |   "core.course.hiddenfromstudents": "moodle", | ||||||
|   "core.course.hiddenoncoursepage": "moodle", |   "core.course.hiddenoncoursepage": "moodle", | ||||||
|  |   "core.course.manualcompletionnotsynced": "local_moodlemobileapp", | ||||||
|   "core.course.nocontentavailable": "local_moodlemobileapp", |   "core.course.nocontentavailable": "local_moodlemobileapp", | ||||||
|   "core.course.overriddennotice": "grades", |   "core.course.overriddennotice": "grades", | ||||||
|   "core.course.sections": "moodle", |   "core.course.sections": "moodle", | ||||||
|   "core.course.useactivityonbrowser": "local_moodlemobileapp", |   "core.course.useactivityonbrowser": "local_moodlemobileapp", | ||||||
|  |   "core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp", | ||||||
|   "core.coursedetails": "moodle", |   "core.coursedetails": "moodle", | ||||||
|   "core.courses.allowguests": "enrol_guest", |   "core.courses.allowguests": "enrol_guest", | ||||||
|   "core.courses.availablecourses": "moodle", |   "core.courses.availablecourses": "moodle", | ||||||
|  | |||||||
| @ -72,7 +72,7 @@ export class CoreCourseModuleCompletionComponent implements OnChanges { | |||||||
|             const modal = this.domUtils.showModalLoading(); |             const modal = this.domUtils.showModalLoading(); | ||||||
| 
 | 
 | ||||||
|             this.courseProvider.markCompletedManually(this.completion.cmid, this.completion.state === 1 ? 0 : 1, |             this.courseProvider.markCompletedManually(this.completion.cmid, this.completion.state === 1 ? 0 : 1, | ||||||
|                     this.completion.courseId).then((response) => { |                     this.completion.courseId, this.completion.courseName).then((response) => { | ||||||
| 
 | 
 | ||||||
|                 if (!response.status) { |                 if (!response.status) { | ||||||
|                     return Promise.reject(null); |                     return Promise.reject(null); | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { NgModule } from '@angular/core'; | import { NgModule } from '@angular/core'; | ||||||
|  | import { CoreCronDelegate } from '@providers/cron'; | ||||||
| import { CoreCourseProvider } from './providers/course'; | import { CoreCourseProvider } from './providers/course'; | ||||||
| import { CoreCourseHelperProvider } from './providers/helper'; | import { CoreCourseHelperProvider } from './providers/helper'; | ||||||
| import { CoreCourseFormatDelegate } from './providers/format-delegate'; | import { CoreCourseFormatDelegate } from './providers/format-delegate'; | ||||||
| @ -26,6 +27,8 @@ import { CoreCourseFormatSingleActivityModule } from './formats/singleactivity/s | |||||||
| import { CoreCourseFormatSocialModule } from './formats/social/social.module'; | import { CoreCourseFormatSocialModule } from './formats/social/social.module'; | ||||||
| import { CoreCourseFormatTopicsModule } from './formats/topics/topics.module'; | import { CoreCourseFormatTopicsModule } from './formats/topics/topics.module'; | ||||||
| import { CoreCourseFormatWeeksModule } from './formats/weeks/weeks.module'; | import { CoreCourseFormatWeeksModule } from './formats/weeks/weeks.module'; | ||||||
|  | import { CoreCourseSyncProvider } from './providers/sync'; | ||||||
|  | import { CoreCourseSyncCronHandler } from './providers/sync-cron-handler'; | ||||||
| 
 | 
 | ||||||
| // List of providers (without handlers).
 | // List of providers (without handlers).
 | ||||||
| export const CORE_COURSE_PROVIDERS: any[] = [ | export const CORE_COURSE_PROVIDERS: any[] = [ | ||||||
| @ -35,7 +38,8 @@ export const CORE_COURSE_PROVIDERS: any[] = [ | |||||||
|     CoreCourseModuleDelegate, |     CoreCourseModuleDelegate, | ||||||
|     CoreCourseModulePrefetchDelegate, |     CoreCourseModulePrefetchDelegate, | ||||||
|     CoreCourseOptionsDelegate, |     CoreCourseOptionsDelegate, | ||||||
|     CoreCourseOfflineProvider |     CoreCourseOfflineProvider, | ||||||
|  |     CoreCourseSyncProvider | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
| @ -54,9 +58,15 @@ export const CORE_COURSE_PROVIDERS: any[] = [ | |||||||
|         CoreCourseModulePrefetchDelegate, |         CoreCourseModulePrefetchDelegate, | ||||||
|         CoreCourseOptionsDelegate, |         CoreCourseOptionsDelegate, | ||||||
|         CoreCourseOfflineProvider, |         CoreCourseOfflineProvider, | ||||||
|  |         CoreCourseSyncProvider, | ||||||
|         CoreCourseFormatDefaultHandler, |         CoreCourseFormatDefaultHandler, | ||||||
|         CoreCourseModuleDefaultHandler |         CoreCourseModuleDefaultHandler, | ||||||
|  |         CoreCourseSyncCronHandler | ||||||
|     ], |     ], | ||||||
|     exports: [] |     exports: [] | ||||||
| }) | }) | ||||||
| export class CoreCourseModule {} | export class CoreCourseModule { | ||||||
|  |     constructor(cronDelegate: CoreCronDelegate, syncHandler: CoreCourseSyncCronHandler) { | ||||||
|  |         cronDelegate.register(syncHandler); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
| @ -23,5 +23,6 @@ | |||||||
|     "overriddennotice": "Your final grade from this activity was manually adjusted.", |     "overriddennotice": "Your final grade from this activity was manually adjusted.", | ||||||
|     "refreshcourse": "Refresh course", |     "refreshcourse": "Refresh course", | ||||||
|     "sections": "Sections", |     "sections": "Sections", | ||||||
|     "useactivityonbrowser": "You can still use it using your device's web browser." |     "useactivityonbrowser": "You can still use it using your device's web browser.", | ||||||
|  |     "warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}" | ||||||
| } | } | ||||||
| @ -24,6 +24,7 @@ import { CoreCourseHelperProvider } from '../../providers/helper'; | |||||||
| import { CoreCourseFormatDelegate } from '../../providers/format-delegate'; | import { CoreCourseFormatDelegate } from '../../providers/format-delegate'; | ||||||
| import { CoreCourseModulePrefetchDelegate } from '../../providers/module-prefetch-delegate'; | import { CoreCourseModulePrefetchDelegate } from '../../providers/module-prefetch-delegate'; | ||||||
| import { CoreCourseOptionsDelegate, CoreCourseOptionsHandlerToDisplay } from '../../providers/options-delegate'; | import { CoreCourseOptionsDelegate, CoreCourseOptionsHandlerToDisplay } from '../../providers/options-delegate'; | ||||||
|  | import { CoreCourseSyncProvider } from '../../providers/sync'; | ||||||
| import { CoreCourseFormatComponent } from '../../components/format/format'; | import { CoreCourseFormatComponent } from '../../components/format/format'; | ||||||
| import { CoreCoursesProvider } from '@core/courses/providers/courses'; | import { CoreCoursesProvider } from '@core/courses/providers/courses'; | ||||||
| import { CoreTabsComponent } from '@components/tabs/tabs'; | import { CoreTabsComponent } from '@components/tabs/tabs'; | ||||||
| @ -62,6 +63,7 @@ export class CoreCourseSectionPage implements OnDestroy { | |||||||
|     protected module: any; |     protected module: any; | ||||||
|     protected completionObserver; |     protected completionObserver; | ||||||
|     protected courseStatusObserver; |     protected courseStatusObserver; | ||||||
|  |     protected syncObserver; | ||||||
|     protected firstTabName: string; |     protected firstTabName: string; | ||||||
|     protected isDestroyed = false; |     protected isDestroyed = false; | ||||||
| 
 | 
 | ||||||
| @ -70,7 +72,7 @@ export class CoreCourseSectionPage implements OnDestroy { | |||||||
|             private translate: TranslateService, private courseHelper: CoreCourseHelperProvider, eventsProvider: CoreEventsProvider, |             private translate: TranslateService, private courseHelper: CoreCourseHelperProvider, eventsProvider: CoreEventsProvider, | ||||||
|             private textUtils: CoreTextUtilsProvider, private coursesProvider: CoreCoursesProvider, |             private textUtils: CoreTextUtilsProvider, private coursesProvider: CoreCoursesProvider, | ||||||
|             sitesProvider: CoreSitesProvider, private navCtrl: NavController, private injector: Injector, |             sitesProvider: CoreSitesProvider, private navCtrl: NavController, private injector: Injector, | ||||||
|             private prefetchDelegate: CoreCourseModulePrefetchDelegate) { |             private prefetchDelegate: CoreCourseModulePrefetchDelegate, private syncProvider: CoreCourseSyncProvider) { | ||||||
|         this.course = navParams.get('course'); |         this.course = navParams.get('course'); | ||||||
|         this.sectionId = navParams.get('sectionId'); |         this.sectionId = navParams.get('sectionId'); | ||||||
|         this.sectionNumber = navParams.get('sectionNumber'); |         this.sectionNumber = navParams.get('sectionNumber'); | ||||||
| @ -87,7 +89,17 @@ export class CoreCourseSectionPage implements OnDestroy { | |||||||
|             if (shouldRefresh) { |             if (shouldRefresh) { | ||||||
|                 this.completionObserver = eventsProvider.on(CoreEventsProvider.COMPLETION_MODULE_VIEWED, (data) => { |                 this.completionObserver = eventsProvider.on(CoreEventsProvider.COMPLETION_MODULE_VIEWED, (data) => { | ||||||
|                     if (data && data.courseId == this.course.id) { |                     if (data && data.courseId == this.course.id) { | ||||||
|                         this.refreshAfterCompletionChange(); |                         this.refreshAfterCompletionChange(true); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |                 this.syncObserver = eventsProvider.on(CoreCourseSyncProvider.AUTO_SYNCED, (data) => { | ||||||
|  |                     if (data && data.courseId == this.course.id) { | ||||||
|  |                         this.refreshAfterCompletionChange(false); | ||||||
|  | 
 | ||||||
|  |                         if (data.warnings && data.warnings[0]) { | ||||||
|  |                             this.domUtils.showErrorModal(data.warnings[0]); | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
| @ -113,7 +125,7 @@ export class CoreCourseSectionPage implements OnDestroy { | |||||||
|             this.courseHelper.openModule(this.navCtrl, this.module, this.course.id, this.sectionId); |             this.courseHelper.openModule(this.navCtrl, this.module, this.course.id, this.sectionId); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.loadData().finally(() => { |         this.loadData(false, true).finally(() => { | ||||||
|             this.dataLoaded = true; |             this.dataLoaded = true; | ||||||
| 
 | 
 | ||||||
|             if (!this.downloadCourseEnabled) { |             if (!this.downloadCourseEnabled) { | ||||||
| @ -146,19 +158,34 @@ export class CoreCourseSectionPage implements OnDestroy { | |||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Fetch and load all the data required for the view. |      * Fetch and load all the data required for the view. | ||||||
|  |      * | ||||||
|  |      * @param {boolean} [refresh] If it's refreshing content. | ||||||
|  |      * @param {boolean} [sync] If the refresh is needs syncing. | ||||||
|  |      * @return {Promise<any>} Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected loadData(refresh?: boolean): Promise<any> { |     protected loadData(refresh?: boolean, sync?: boolean): Promise<any> { | ||||||
|         // First of all, get the course because the data might have changed.
 |         // First of all, get the course because the data might have changed.
 | ||||||
|         return this.coursesProvider.getUserCourse(this.course.id).catch(() => { |         return this.coursesProvider.getUserCourse(this.course.id).catch(() => { | ||||||
|             // Error getting the course, probably guest access.
 |             // Error getting the course, probably guest access.
 | ||||||
|         }).then((course) => { |         }).then((course) => { | ||||||
|             const promises = []; |  | ||||||
|             let promise; |  | ||||||
| 
 |  | ||||||
|             if (course) { |             if (course) { | ||||||
|                 this.course = course; |                 this.course = course; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |             if (sync) { | ||||||
|  |                 // Try to synchronize the course data.
 | ||||||
|  |                 return this.syncProvider.syncCourse(this.course.id).then((result) => { | ||||||
|  |                     if (result.warnings && result.warnings.length) { | ||||||
|  |                         this.domUtils.showErrorModal(result.warnings[0]); | ||||||
|  |                     } | ||||||
|  |                 }).catch(() => { | ||||||
|  |                     // For now we don't allow manual syncing, so ignore errors.
 | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         }).then(() => { | ||||||
|  |             const promises = []; | ||||||
|  |             let promise; | ||||||
|  | 
 | ||||||
|             // Get the completion status.
 |             // Get the completion status.
 | ||||||
|             if (this.course.enablecompletion === false) { |             if (this.course.enablecompletion === false) { | ||||||
|                 // Completion not enabled.
 |                 // Completion not enabled.
 | ||||||
| @ -185,7 +212,7 @@ export class CoreCourseSectionPage implements OnDestroy { | |||||||
|                     } |                     } | ||||||
|                 }).then((sections) => { |                 }).then((sections) => { | ||||||
| 
 | 
 | ||||||
|                     this.courseHelper.addHandlerDataForModules(sections, this.course.id, completionStatus); |                     this.courseHelper.addHandlerDataForModules(sections, this.course.id, completionStatus, this.course.fullname); | ||||||
| 
 | 
 | ||||||
|                     // Format the name of each section and check if it has content.
 |                     // Format the name of each section and check if it has content.
 | ||||||
|                     this.sections = sections.map((section) => { |                     this.sections = sections.map((section) => { | ||||||
| @ -268,7 +295,7 @@ export class CoreCourseSectionPage implements OnDestroy { | |||||||
|      */ |      */ | ||||||
|     doRefresh(refresher?: any): Promise<any> { |     doRefresh(refresher?: any): Promise<any> { | ||||||
|         return this.invalidateData().finally(() => { |         return this.invalidateData().finally(() => { | ||||||
|             return this.loadData(true).finally(() => { |             return this.loadData(true, true).finally(() => { | ||||||
|                 /* Do not call doRefresh on the format component if the refresher is defined in the format component |                 /* Do not call doRefresh on the format component if the refresher is defined in the format component | ||||||
|                    to prevent an inifinite loop. */ |                    to prevent an inifinite loop. */ | ||||||
|                  let promise; |                  let promise; | ||||||
| @ -290,7 +317,7 @@ export class CoreCourseSectionPage implements OnDestroy { | |||||||
|      */ |      */ | ||||||
|     onCompletionChange(): void { |     onCompletionChange(): void { | ||||||
|         this.invalidateData().finally(() => { |         this.invalidateData().finally(() => { | ||||||
|             this.refreshAfterCompletionChange(); |             this.refreshAfterCompletionChange(true); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -314,8 +341,10 @@ export class CoreCourseSectionPage implements OnDestroy { | |||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Refresh list after a completion change since there could be new activities. |      * Refresh list after a completion change since there could be new activities. | ||||||
|  |      * | ||||||
|  |      * @param {boolean} [sync] If the refresh is needs syncing. | ||||||
|      */ |      */ | ||||||
|     protected refreshAfterCompletionChange(): void { |     protected refreshAfterCompletionChange(sync?: boolean): void { | ||||||
|         // Save scroll position to restore it once done.
 |         // Save scroll position to restore it once done.
 | ||||||
|         const scrollElement = this.content.getScrollElement(), |         const scrollElement = this.content.getScrollElement(), | ||||||
|             scrollTop = scrollElement.scrollTop || 0, |             scrollTop = scrollElement.scrollTop || 0, | ||||||
| @ -324,7 +353,7 @@ export class CoreCourseSectionPage implements OnDestroy { | |||||||
|         this.dataLoaded = false; |         this.dataLoaded = false; | ||||||
|         this.domUtils.scrollToTop(this.content); // Scroll top so the spinner is seen.
 |         this.domUtils.scrollToTop(this.content); // Scroll top so the spinner is seen.
 | ||||||
| 
 | 
 | ||||||
|         this.loadData().then(() => { |         this.loadData(true, sync).then(() => { | ||||||
|             return this.formatComponent.doRefresh(undefined, undefined, true); |             return this.formatComponent.doRefresh(undefined, undefined, true); | ||||||
|         }).finally(() => { |         }).finally(() => { | ||||||
|             this.dataLoaded = true; |             this.dataLoaded = true; | ||||||
|  | |||||||
| @ -40,6 +40,10 @@ export class CoreCourseOfflineProvider { | |||||||
|                     name: 'courseid', |                     name: 'courseid', | ||||||
|                     type: 'INTEGER' |                     type: 'INTEGER' | ||||||
|                 }, |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: 'coursename', | ||||||
|  |                     type: 'TEXT' | ||||||
|  |                 }, | ||||||
|                 { |                 { | ||||||
|                     name: 'timecreated', |                     name: 'timecreated', | ||||||
|                     type: 'INTEGER' |                     type: 'INTEGER' | ||||||
| @ -66,6 +70,19 @@ export class CoreCourseOfflineProvider { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Get all offline manual completions for a certain course. | ||||||
|  |      * | ||||||
|  |      * @param {string} [siteId] Site ID. If not defined, current site. | ||||||
|  |      * @return {Promise<any[]>} Promise resolved with the list of completions. | ||||||
|  |      */ | ||||||
|  |     getAllManualCompletions(siteId?: string): Promise<any[]> { | ||||||
|  |         return this.sitesProvider.getSite(siteId).then((site) => { | ||||||
|  | 
 | ||||||
|  |             return site.getDb().getRecords(CoreCourseOfflineProvider.MANUAL_COMPLETION_TABLE); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Get all offline manual completions for a certain course. |      * Get all offline manual completions for a certain course. | ||||||
|      * |      * | ||||||
| @ -100,10 +117,11 @@ export class CoreCourseOfflineProvider { | |||||||
|      * @param {number} cmId The module ID to store the completion. |      * @param {number} cmId The module ID to store the completion. | ||||||
|      * @param {number} completed Whether the module is completed or not. |      * @param {number} completed Whether the module is completed or not. | ||||||
|      * @param {number} courseId Course ID the module belongs to. |      * @param {number} courseId Course ID the module belongs to. | ||||||
|  |      * @param {string} [courseName] Course name. Recommended, it is used to display a better warning message. | ||||||
|      * @param {string} [siteId] Site ID. If not defined, current site. |      * @param {string} [siteId] Site ID. If not defined, current site. | ||||||
|      * @return {Promise<{status: boolean, offline: boolean}>} Promise resolved when completion is successfully stored. |      * @return {Promise<{status: boolean, offline: boolean}>} Promise resolved when completion is successfully stored. | ||||||
|      */ |      */ | ||||||
|     markCompletedManually(cmId: number, completed: number, courseId: number, siteId?: string) |     markCompletedManually(cmId: number, completed: number, courseId: number, courseName?: string, siteId?: string) | ||||||
|             : Promise<{status: boolean, offline: boolean}> { |             : Promise<{status: boolean, offline: boolean}> { | ||||||
| 
 | 
 | ||||||
|         // Store the offline data.
 |         // Store the offline data.
 | ||||||
| @ -112,6 +130,7 @@ export class CoreCourseOfflineProvider { | |||||||
|                 cmid: cmId, |                 cmid: cmId, | ||||||
|                 completed: completed, |                 completed: completed, | ||||||
|                 courseid: courseId, |                 courseid: courseId, | ||||||
|  |                 coursename: courseName || '', | ||||||
|                 timecreated: Date.now() |                 timecreated: Date.now() | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -677,15 +677,18 @@ export class CoreCourseProvider { | |||||||
|      * @param {number} cmId The module ID. |      * @param {number} cmId The module ID. | ||||||
|      * @param {number} completed Whether the module is completed or not. |      * @param {number} completed Whether the module is completed or not. | ||||||
|      * @param {number} courseId Course ID the module belongs to. |      * @param {number} courseId Course ID the module belongs to. | ||||||
|  |      * @param {string} [courseName] Course name. Recommended, it is used to display a better warning message. | ||||||
|      * @param {string} [siteId] Site ID. If not defined, current site. |      * @param {string} [siteId] Site ID. If not defined, current site. | ||||||
|      * @return {Promise<any>} Promise resolved when completion is successfully sent or stored. |      * @return {Promise<any>} Promise resolved when completion is successfully sent or stored. | ||||||
|      */ |      */ | ||||||
|     markCompletedManually(cmId: number, completed: number, courseId: number, siteId?: string): Promise<any> { |     markCompletedManually(cmId: number, completed: number, courseId: number, courseName?: string, siteId?: string) | ||||||
|  |             : Promise<any> { | ||||||
|  | 
 | ||||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
|         // Convenience function to store a message to be synchronized later.
 |         // Convenience function to store a message to be synchronized later.
 | ||||||
|         const storeOffline = (): Promise<any> => { |         const storeOffline = (): Promise<any> => { | ||||||
|             return this.courseOffline.markCompletedManually(cmId, completed, courseId, siteId); |             return this.courseOffline.markCompletedManually(cmId, completed, courseId, courseName, siteId); | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         // Check if we already have a completion stored.
 |         // Check if we already have a completion stored.
 | ||||||
| @ -703,7 +706,7 @@ export class CoreCourseProvider { | |||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (!this.appProvider.isOnline()) { |             if (!this.appProvider.isOnline() && courseId) { | ||||||
|                 // App is offline, store the action.
 |                 // App is offline, store the action.
 | ||||||
|                 return storeOffline(); |                 return storeOffline(); | ||||||
|             } |             } | ||||||
| @ -720,7 +723,7 @@ export class CoreCourseProvider { | |||||||
| 
 | 
 | ||||||
|                 return result; |                 return result; | ||||||
|             }).catch((error) => { |             }).catch((error) => { | ||||||
|                 if (this.utils.isWebServiceError(error)) { |                 if (this.utils.isWebServiceError(error) || !courseId) { | ||||||
|                     // The WebService has thrown an error, this means that responses cannot be submitted.
 |                     // The WebService has thrown an error, this means that responses cannot be submitted.
 | ||||||
|                     return Promise.reject(error); |                     return Promise.reject(error); | ||||||
|                 } else { |                 } else { | ||||||
|  | |||||||
| @ -131,9 +131,10 @@ export class CoreCourseHelperProvider { | |||||||
|      * @param {any[]} sections List of sections to treat modules. |      * @param {any[]} sections List of sections to treat modules. | ||||||
|      * @param {number} courseId Course ID of the modules. |      * @param {number} courseId Course ID of the modules. | ||||||
|      * @param {any[]} [completionStatus] List of completion status. |      * @param {any[]} [completionStatus] List of completion status. | ||||||
|  |      * @param {string} [courseName] Course name. Recommended if completionStatus is supplied. | ||||||
|      * @return {boolean} Whether the sections have content. |      * @return {boolean} Whether the sections have content. | ||||||
|      */ |      */ | ||||||
|     addHandlerDataForModules(sections: any[], courseId: number, completionStatus?: any): boolean { |     addHandlerDataForModules(sections: any[], courseId: number, completionStatus?: any, courseName?: string): boolean { | ||||||
|         let hasContent = false; |         let hasContent = false; | ||||||
| 
 | 
 | ||||||
|         sections.forEach((section) => { |         sections.forEach((section) => { | ||||||
| @ -150,6 +151,7 @@ export class CoreCourseHelperProvider { | |||||||
|                     // Check if activity has completions and if it's marked.
 |                     // Check if activity has completions and if it's marked.
 | ||||||
|                     module.completionstatus = completionStatus[module.id]; |                     module.completionstatus = completionStatus[module.id]; | ||||||
|                     module.completionstatus.courseId = courseId; |                     module.completionstatus.courseId = courseId; | ||||||
|  |                     module.completionstatus.courseName = courseName; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 // Check if the module is stealth.
 |                 // Check if the module is stealth.
 | ||||||
|  | |||||||
							
								
								
									
										47
									
								
								src/core/course/providers/sync-cron-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/core/course/providers/sync-cron-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | |||||||
|  | // (C) Copyright 2015 Martin Dougiamas
 | ||||||
|  | //
 | ||||||
|  | // 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 { Injectable } from '@angular/core'; | ||||||
|  | import { CoreCronHandler } from '@providers/cron'; | ||||||
|  | import { CoreCourseSyncProvider } from './sync'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Synchronization cron handler. | ||||||
|  |  */ | ||||||
|  | @Injectable() | ||||||
|  | export class CoreCourseSyncCronHandler implements CoreCronHandler { | ||||||
|  |     name = 'CoreCourseSyncCronHandler'; | ||||||
|  | 
 | ||||||
|  |     constructor(private courseSync: CoreCourseSyncProvider) {} | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Execute the process. | ||||||
|  |      * Receives the ID of the site affected, undefined for all sites. | ||||||
|  |      * | ||||||
|  |      * @param {string} [siteId] ID of the site affected, undefined for all sites. | ||||||
|  |      * @return {Promise<any>} Promise resolved when done, rejected if failure. | ||||||
|  |      */ | ||||||
|  |     execute(siteId?: string): Promise<any> { | ||||||
|  |         return this.courseSync.syncAllCourses(siteId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the time between consecutive executions. | ||||||
|  |      * | ||||||
|  |      * @return {number} Time between consecutive executions (in ms). | ||||||
|  |      */ | ||||||
|  |     getInterval(): number { | ||||||
|  |         return this.courseSync.syncInterval; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										178
									
								
								src/core/course/providers/sync.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								src/core/course/providers/sync.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,178 @@ | |||||||
|  | // (C) Copyright 2015 Martin Dougiamas
 | ||||||
|  | //
 | ||||||
|  | // 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 { Injectable } from '@angular/core'; | ||||||
|  | import { CoreLoggerProvider } from '@providers/logger'; | ||||||
|  | import { CoreSyncBaseProvider } from '@classes/base-sync'; | ||||||
|  | import { CoreSitesProvider } from '@providers/sites'; | ||||||
|  | import { CoreAppProvider } from '@providers/app'; | ||||||
|  | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
|  | import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||||
|  | import { CoreCourseOfflineProvider } from './course-offline'; | ||||||
|  | import { CoreCourseProvider } from './course'; | ||||||
|  | import { CoreEventsProvider } from '@providers/events'; | ||||||
|  | import { TranslateService } from '@ngx-translate/core'; | ||||||
|  | import { CoreSyncProvider } from '@providers/sync'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Service to sync course offline data. This only syncs the offline data of the course itself, not the offline data of | ||||||
|  |  * the activities in the course. | ||||||
|  |  */ | ||||||
|  | @Injectable() | ||||||
|  | export class CoreCourseSyncProvider extends CoreSyncBaseProvider { | ||||||
|  | 
 | ||||||
|  |     static AUTO_SYNCED = 'core_course_autom_synced'; | ||||||
|  | 
 | ||||||
|  |     constructor(protected sitesProvider: CoreSitesProvider, loggerProvider: CoreLoggerProvider, | ||||||
|  |             protected appProvider: CoreAppProvider, private courseOffline: CoreCourseOfflineProvider, | ||||||
|  |             private eventsProvider: CoreEventsProvider,  private courseProvider: CoreCourseProvider, | ||||||
|  |             translate: TranslateService, private utils: CoreUtilsProvider, protected textUtils: CoreTextUtilsProvider, | ||||||
|  |             syncProvider: CoreSyncProvider) { | ||||||
|  | 
 | ||||||
|  |         super('CoreCourseSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Try to synchronize all the courses in a certain site or in all sites. | ||||||
|  |      * | ||||||
|  |      * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. | ||||||
|  |      * @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails. | ||||||
|  |      */ | ||||||
|  |     syncAllCourses(siteId?: string): Promise<any> { | ||||||
|  |         return this.syncOnSites('courses', this.syncAllCoursesFunc.bind(this), undefined, siteId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Sync all courses on a site. | ||||||
|  |      * | ||||||
|  |      * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. | ||||||
|  |      * @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails. | ||||||
|  |      */ | ||||||
|  |     protected syncAllCoursesFunc(siteId?: string): Promise<any> { | ||||||
|  |         return this.courseOffline.getAllManualCompletions(siteId).then((completions) => { | ||||||
|  |             const promises = []; | ||||||
|  | 
 | ||||||
|  |             // Sync all courses.
 | ||||||
|  |             completions.forEach((completion) => { | ||||||
|  |                 promises.push(this.syncCourseIfNeeded(completion.courseid, siteId).then((result) => { | ||||||
|  |                     if (result && result.updated) { | ||||||
|  |                         // Sync successful, send event.
 | ||||||
|  |                         this.eventsProvider.trigger(CoreCourseSyncProvider.AUTO_SYNCED, { | ||||||
|  |                             courseId: completion.courseid, | ||||||
|  |                             warnings: result.warnings | ||||||
|  |                         }, siteId); | ||||||
|  |                     } | ||||||
|  |                 })); | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Sync a course if it's needed. | ||||||
|  |      * | ||||||
|  |      * @param {number} courseId Course ID to be synced. | ||||||
|  |      * @param {string} [siteId] Site ID. If not defined, current site. | ||||||
|  |      * @return {Promise<any>} Promise resolved when the course is synced or it doesn't need to be synced. | ||||||
|  |      */ | ||||||
|  |     syncCourseIfNeeded(courseId: number, siteId?: string): Promise<any> { | ||||||
|  |         // Usually we call isSyncNeeded to check if a certain time has passed.
 | ||||||
|  |         // However, since we barely send data for now just sync the course.
 | ||||||
|  |         return this.syncCourse(courseId, siteId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Synchronize a course. | ||||||
|  |      * | ||||||
|  |      * @param {number} courseId Course ID to be synced. | ||||||
|  |      * @param {string} [siteId] Site ID. If not defined, current site. | ||||||
|  |      * @return {Promise<any>} Promise resolved if sync is successful, rejected otherwise. | ||||||
|  |      */ | ||||||
|  |     syncCourse(courseId: number, siteId?: string): Promise<any> { | ||||||
|  |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
|  | 
 | ||||||
|  |         if (this.isSyncing(courseId, siteId)) { | ||||||
|  |             // There's already a sync ongoing for this discussion, return the promise.
 | ||||||
|  |             return this.getOngoingSync(courseId, siteId); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.logger.debug(`Try to sync course '${courseId}'`); | ||||||
|  | 
 | ||||||
|  |         const result = { | ||||||
|  |             warnings: [], | ||||||
|  |             updated: false | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         // Get offline responses to be sent.
 | ||||||
|  |         const syncPromise = this.courseOffline.getCourseManualCompletions(courseId, siteId).catch(() => { | ||||||
|  |             // No offline data found, return empty list.
 | ||||||
|  |             return []; | ||||||
|  |         }).then((completions) => { | ||||||
|  |             if (!completions || !completions.length) { | ||||||
|  |                 // Nothing to sync.
 | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (!this.appProvider.isOnline()) { | ||||||
|  |                 // Cannot sync in offline.
 | ||||||
|  |                 return Promise.reject(null); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const promises = []; | ||||||
|  | 
 | ||||||
|  |             // Send all the completions.
 | ||||||
|  |             completions.forEach((entry) => { | ||||||
|  |                 promises.push(this.courseProvider.markCompletedManuallyOnline(entry.cmid, entry.completed, siteId).then(() => { | ||||||
|  |                     result.updated = true; | ||||||
|  | 
 | ||||||
|  |                     return this.courseOffline.deleteManualCompletion(entry.cmid, siteId); | ||||||
|  |                 }).catch((error) => { | ||||||
|  |                     if (this.utils.isWebServiceError(error)) { | ||||||
|  |                         // The WebService has thrown an error, this means that the completion cannot be submitted. Delete it.
 | ||||||
|  |                         result.updated = true; | ||||||
|  | 
 | ||||||
|  |                         return this.courseOffline.deleteManualCompletion(entry.cmid, siteId).then(() => { | ||||||
|  |                             // Responses deleted, add a warning.
 | ||||||
|  |                             result.warnings.push(this.translate.instant('core.course.warningofflinemanualcompletiondeleted', { | ||||||
|  |                                 name: entry.coursename || courseId, | ||||||
|  |                                 error: error.error | ||||||
|  |                             })); | ||||||
|  |                         }); | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     // Couldn't connect to server, reject.
 | ||||||
|  |                     return Promise.reject(error); | ||||||
|  |                 })); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             return Promise.all(promises); | ||||||
|  |         }).then(() => { | ||||||
|  |             if (result.updated) { | ||||||
|  |                 // Update data.
 | ||||||
|  |                 return this.courseProvider.invalidateSections(courseId, siteId).then(() => { | ||||||
|  |                     return this.courseProvider.getActivitiesCompletionStatus(courseId); | ||||||
|  |                 }).catch(() => { | ||||||
|  |                     // Ignore errors.
 | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         }).then(() => { | ||||||
|  |             // Sync finished, set sync time.
 | ||||||
|  |             return this.setSyncTime(courseId, siteId); | ||||||
|  |         }).then(() => { | ||||||
|  |             // All done, return the data.
 | ||||||
|  |             return result; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         return this.addOngoingSync(courseId, syncPromise, siteId); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user