diff --git a/src/addons/mod/lesson/components/components.module.ts b/src/addons/mod/lesson/components/components.module.ts
new file mode 100644
index 000000000..8999caf02
--- /dev/null
+++ b/src/addons/mod/lesson/components/components.module.ts
@@ -0,0 +1,39 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { IonicModule } from '@ionic/angular';
+import { TranslateModule } from '@ngx-translate/core';
+
+import { CoreSharedModule } from '@/core/shared.module';
+import { AddonModLessonPasswordModalComponent } from './password-modal/password-modal';
+
+@NgModule({
+ declarations: [
+ AddonModLessonPasswordModalComponent,
+ ],
+ imports: [
+ CommonModule,
+ IonicModule,
+ TranslateModule.forChild(),
+ CoreSharedModule,
+ ],
+ providers: [
+ ],
+ exports: [
+ AddonModLessonPasswordModalComponent,
+ ],
+})
+export class AddonModLessonComponentsModule {}
diff --git a/src/addons/mod/lesson/components/password-modal/password-modal.html b/src/addons/mod/lesson/components/password-modal/password-modal.html
new file mode 100644
index 000000000..ba0313e13
--- /dev/null
+++ b/src/addons/mod/lesson/components/password-modal/password-modal.html
@@ -0,0 +1,28 @@
+
+
+ {{ 'core.login.password' | translate }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/addons/mod/lesson/components/password-modal/password-modal.ts b/src/addons/mod/lesson/components/password-modal/password-modal.ts
new file mode 100644
index 000000000..746e525b2
--- /dev/null
+++ b/src/addons/mod/lesson/components/password-modal/password-modal.ts
@@ -0,0 +1,57 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, ViewChild, ElementRef } from '@angular/core';
+
+import { CoreSites } from '@services/sites';
+import { CoreDomUtils } from '@services/utils/dom';
+import { ModalController } from '@singletons';
+
+
+/**
+ * Modal that asks the password for a lesson.
+ */
+@Component({
+ selector: 'page-addon-mod-lesson-password-modal',
+ templateUrl: 'password-modal.html',
+})
+export class AddonModLessonPasswordModalComponent {
+
+ @ViewChild('passwordForm') formElement?: ElementRef;
+
+ /**
+ * Send the password back.
+ *
+ * @param e Event.
+ * @param password The input element.
+ */
+ submitPassword(e: Event, password: HTMLInputElement): void {
+ e.preventDefault();
+ e.stopPropagation();
+
+ CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, false, CoreSites.instance.getCurrentSiteId());
+
+ ModalController.instance.dismiss(password.value);
+ }
+
+ /**
+ * Close modal.
+ */
+ closeModal(): void {
+ CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, CoreSites.instance.getCurrentSiteId());
+
+ ModalController.instance.dismiss();
+ }
+
+}
diff --git a/src/addons/mod/lesson/lesson.module.ts b/src/addons/mod/lesson/lesson.module.ts
index 83df48fce..9b0785854 100644
--- a/src/addons/mod/lesson/lesson.module.ts
+++ b/src/addons/mod/lesson/lesson.module.ts
@@ -12,13 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { NgModule } from '@angular/core';
+import { APP_INITIALIZER, NgModule } from '@angular/core';
+import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
+import { CoreCronDelegate } from '@services/cron';
import { CORE_SITE_SCHEMAS } from '@services/sites';
+import { AddonModLessonComponentsModule } from './components/components.module';
import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/lesson';
+import { AddonModLessonPrefetchHandler } from './services/handlers/prefetch';
+import { AddonModLessonSyncCronHandler } from './services/handlers/sync-cron';
@NgModule({
imports: [
+ AddonModLessonComponentsModule,
],
providers: [
{
@@ -26,6 +32,15 @@ import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/lesson';
useValue: [SITE_SCHEMA, OFFLINE_SITE_SCHEMA],
multi: true,
},
+ {
+ provide: APP_INITIALIZER,
+ multi: true,
+ deps: [],
+ useFactory: () => () => {
+ CoreCourseModulePrefetchDelegate.instance.registerHandler(AddonModLessonPrefetchHandler.instance);
+ CoreCronDelegate.instance.register(AddonModLessonSyncCronHandler.instance);
+ },
+ },
],
})
export class AddonModLessonModule {}
diff --git a/src/addons/mod/lesson/services/database/lesson.ts b/src/addons/mod/lesson/services/database/lesson.ts
index ec4b6ec2b..843a690d6 100644
--- a/src/addons/mod/lesson/services/database/lesson.ts
+++ b/src/addons/mod/lesson/services/database/lesson.ts
@@ -15,7 +15,7 @@
import { CoreSiteSchema } from '@services/sites';
/**
- * Database variables for AddonModLessonOfflineProvider.
+ * Database variables for AddonModLessonProvider.
*/
export const PASSWORD_TABLE_NAME = 'addon_mod_lesson_password';
export const SITE_SCHEMA: CoreSiteSchema = {
@@ -145,6 +145,39 @@ export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
],
};
+/**
+ * Database variables for AddonModLessonSyncProvider.
+ */
+export const RETAKES_FINISHED_SYNC_TABLE_NAME = 'addon_mod_lesson_retakes_finished_sync';
+export const SYNC_SITE_SCHEMA: CoreSiteSchema = {
+ name: 'AddonModLessonSyncProvider',
+ version: 1,
+ tables: [
+ {
+ name: RETAKES_FINISHED_SYNC_TABLE_NAME,
+ columns: [
+ {
+ name: 'lessonid',
+ type: 'INTEGER',
+ primaryKey: true,
+ },
+ {
+ name: 'retake',
+ type: 'INTEGER',
+ },
+ {
+ name: 'pageid',
+ type: 'INTEGER',
+ },
+ {
+ name: 'timefinished',
+ type: 'INTEGER',
+ },
+ ],
+ },
+ ],
+};
+
/**
* Lesson retake data.
*/
@@ -183,3 +216,13 @@ export type AddonModLessonPageAttemptDBRecord = {
answerid: number | null;
useranswer: string | null;
};
+
+/**
+ * Data about a retake finished in sync.
+ */
+export type AddonModLessonRetakeFinishedInSyncDBRecord = {
+ lessonid: number;
+ retake: number;
+ pageid: number;
+ timefinished: number;
+};
diff --git a/src/addons/mod/lesson/services/handlers/prefetch.ts b/src/addons/mod/lesson/services/handlers/prefetch.ts
new file mode 100644
index 000000000..f81535a66
--- /dev/null
+++ b/src/addons/mod/lesson/services/handlers/prefetch.ts
@@ -0,0 +1,576 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreCanceledError } from '@classes/errors/cancelederror';
+import { CoreError } from '@classes/errors/error';
+
+import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
+import { CoreCourse, CoreCourseCommonModWSOptions, CoreCourseAnyModuleData } from '@features/course/services/course';
+import { CoreFilepool } from '@services/filepool';
+import { CoreGroups } from '@services/groups';
+import { CoreFileSizeSum, CorePluginFileDelegate } from '@services/plugin-file-delegate';
+import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
+import { CoreUtils } from '@services/utils/utils';
+import { CoreWSExternalFile } from '@services/ws';
+import { makeSingleton, ModalController, Translate } from '@singletons';
+import { AddonModLessonPasswordModalComponent } from '../../components/password-modal/password-modal';
+import {
+ AddonModLesson,
+ AddonModLessonGetAccessInformationWSResponse,
+ AddonModLessonLessonWSData,
+ AddonModLessonPasswordOptions,
+ AddonModLessonProvider,
+} from '../lesson';
+import { AddonModLessonSync, AddonModLessonSyncResult } from '../lesson-sync';
+
+/**
+ * Handler to prefetch lessons.
+ */
+@Injectable({ providedIn: 'root' })
+export class AddonModLessonPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase {
+
+ name = 'AddonModLesson';
+ modName = 'lesson';
+ component = AddonModLessonProvider.COMPONENT;
+ // Don't check timers to decrease positives. If a user performs some action it will be reflected in other items.
+ updatesNames = /^configuration$|^.*files$|^grades$|^gradeitems$|^pages$|^answers$|^questionattempts$|^pagesviewed$/;
+
+ /**
+ * Ask password.
+ *
+ * @return Promise resolved with the password.
+ */
+ protected async askUserPassword(): Promise {
+ // Create and show the modal.
+ const modal = await ModalController.instance.create({
+ component: AddonModLessonPasswordModalComponent,
+ });
+
+ await modal.present();
+
+ const password = await modal.onWillDismiss();
+
+ if (typeof password != 'string') {
+ throw new CoreCanceledError();
+ }
+
+ return password;
+ }
+
+ /**
+ * Get the download size of a module.
+ *
+ * @param module Module.
+ * @param courseId Course ID the module belongs to.
+ * @param single True if we're downloading a single module, false if we're downloading a whole section.
+ * @return Promise resolved with the size.
+ */
+ async getDownloadSize(module: CoreCourseAnyModuleData, courseId: number, single?: boolean): Promise {
+ const siteId = CoreSites.instance.getCurrentSiteId();
+
+ let lesson = await AddonModLesson.instance.getLesson(courseId, module.id, { siteId });
+
+ // Get the lesson password if it's needed.
+ const passwordData = await this.getLessonPassword(lesson.id, {
+ readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
+ askPassword: single,
+ siteId,
+ });
+
+ lesson = passwordData.lesson || lesson;
+
+ // Get intro files and media files.
+ let files = lesson.mediafiles || [];
+ files = files.concat(this.getIntroFilesFromInstance(module, lesson));
+
+ const result = await CorePluginFileDelegate.instance.getFilesDownloadSize(files);
+
+ // Get the pages to calculate the size.
+ const pages = await AddonModLesson.instance.getPages(lesson.id, {
+ cmId: module.id,
+ password: passwordData.password,
+ siteId,
+ });
+
+ pages.forEach((page) => {
+ result.size += page.filessizetotal;
+ });
+
+ return result;
+ }
+
+ /**
+ * Get the lesson password if needed. If not stored, it can ask the user to enter it.
+ *
+ * @param lessonId Lesson ID.
+ * @param options Other options.
+ * @return Promise resolved when done.
+ */
+ async getLessonPassword(
+ lessonId: number,
+ options: AddonModLessonGetPasswordOptions = {},
+ ): Promise {
+
+ options.siteId = options.siteId || CoreSites.instance.getCurrentSiteId();
+
+ // Get access information to check if password is needed.
+ const accessInfo = await AddonModLesson.instance.getAccessInformation(lessonId, options);
+
+ if (!accessInfo.preventaccessreasons.length) {
+ // Password not needed.
+ return { accessInfo };
+ }
+
+ const passwordNeeded = accessInfo.preventaccessreasons.length == 1 &&
+ AddonModLesson.instance.isPasswordProtected(accessInfo);
+
+ if (!passwordNeeded) {
+ // Lesson cannot be played, reject.
+ throw new CoreError(accessInfo.preventaccessreasons[0].message);
+ }
+
+ // The lesson requires a password. Check if there is one in DB.
+ let password = await CoreUtils.instance.ignoreErrors(AddonModLesson.instance.getStoredPassword(lessonId));
+
+ if (password) {
+ try {
+ return this.validatePassword(lessonId, accessInfo, password, options);
+ } catch {
+ // Error validating it.
+ }
+ }
+
+ // Ask for the password if allowed.
+ if (!options.askPassword) {
+ // Cannot ask for password, reject.
+ throw new CoreError(accessInfo.preventaccessreasons[0].message);
+ }
+
+ password = await this.askUserPassword();
+
+ return this.validatePassword(lessonId, accessInfo, password, options);
+ }
+
+ /**
+ * Invalidate the prefetched content.
+ *
+ * @param moduleId The module ID.
+ * @param courseId The course ID the module belongs to.
+ * @return Promise resolved when the data is invalidated.
+ */
+ async invalidateContent(moduleId: number, courseId: number): Promise {
+ // Only invalidate the data that doesn't ignore cache when prefetching.
+ await Promise.all([
+ AddonModLesson.instance.invalidateLessonData(courseId),
+ CoreCourse.instance.invalidateModule(moduleId),
+ CoreGroups.instance.invalidateActivityAllowedGroups(moduleId),
+ ]);
+ }
+
+ /**
+ * Invalidate WS calls needed to determine module status.
+ *
+ * @param module Module.
+ * @param courseId Course ID the module belongs to.
+ * @return Promise resolved when invalidated.
+ */
+ async invalidateModule(module: CoreCourseAnyModuleData, courseId: number): Promise {
+ // Invalidate data to determine if module is downloadable.
+ const siteId = CoreSites.instance.getCurrentSiteId();
+
+ const lesson = await AddonModLesson.instance.getLesson(courseId, module.id, {
+ readingStrategy: CoreSitesReadingStrategy.PreferCache,
+ siteId,
+ });
+
+ await Promise.all([
+ AddonModLesson.instance.invalidateLessonData(courseId, siteId),
+ AddonModLesson.instance.invalidateAccessInformation(lesson.id, siteId),
+ ]);
+ }
+
+ /**
+ * Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable.
+ *
+ * @param module Module.
+ * @param courseId Course ID the module belongs to.
+ * @return Whether the module can be downloaded. The promise should never be rejected.
+ */
+ async isDownloadable(module: CoreCourseAnyModuleData, courseId: number): Promise {
+ const siteId = CoreSites.instance.getCurrentSiteId();
+
+ const lesson = await AddonModLesson.instance.getLesson(courseId, module.id, { siteId });
+ const accessInfo = await AddonModLesson.instance.getAccessInformation(lesson.id, { cmId: module.id, siteId });
+
+ // If it's a student and lesson isn't offline, it isn't downloadable.
+ if (!accessInfo.canviewreports && !AddonModLesson.instance.isLessonOffline(lesson)) {
+ return false;
+ }
+
+ // It's downloadable if there are no prevent access reasons or there is just 1 and it's password.
+ return !accessInfo.preventaccessreasons.length ||
+ (accessInfo.preventaccessreasons.length == 1 && AddonModLesson.instance.isPasswordProtected(accessInfo));
+ }
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ *
+ * @return Promise resolved with a boolean indicating if the handler is enabled.
+ */
+ isEnabled(): Promise {
+ return AddonModLesson.instance.isPluginEnabled();
+ }
+
+ /**
+ * Prefetch a module.
+ *
+ * @param module Module.
+ * @param courseId Course ID the module belongs to.
+ * @param single True if we're downloading a single module, false if we're downloading a whole section.
+ * @param dirPath Path of the directory where to store all the content files.
+ * @return Promise resolved when done.
+ */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ prefetch(module: CoreCourseAnyModuleData, courseId?: number, single?: boolean, dirPath?: string): Promise {
+ return this.prefetchPackage(module, courseId, this.prefetchLesson.bind(this, module, courseId, single));
+ }
+
+ /**
+ * Prefetch a lesson.
+ *
+ * @param module Module.
+ * @param courseId Course ID the module belongs to.
+ * @param single True if we're downloading a single module, false if we're downloading a whole section.
+ * @return Promise resolved when done.
+ */
+ protected async prefetchLesson(module: CoreCourseAnyModuleData, courseId?: number, single?: boolean): Promise {
+ const siteId = CoreSites.instance.getCurrentSiteId();
+ courseId = courseId || module.course || 1;
+
+ const commonOptions = {
+ readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
+ siteId,
+ };
+ const modOptions = {
+ cmId: module.id,
+ ...commonOptions, // Include all common options.
+ };
+
+ let lesson = await AddonModLesson.instance.getLesson(courseId, module.id, commonOptions);
+
+ // Get the lesson password if it's needed.
+ const passwordData = await this.getLessonPassword(lesson.id, {
+ readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
+ askPassword: single,
+ siteId,
+ });
+
+ lesson = passwordData.lesson || lesson;
+ let accessInfo = passwordData.accessInfo;
+ const password = passwordData.password;
+
+ if (AddonModLesson.instance.isLessonOffline(lesson) && !AddonModLesson.instance.leftDuringTimed(accessInfo)) {
+ // The user didn't left during a timed session. Call launch retake to make sure there is a started retake.
+ accessInfo = await this.launchRetake(lesson.id, password, modOptions, siteId);
+ }
+
+ const promises: Promise[] = [];
+
+ // Download intro files and media files.
+ const files = (lesson.mediafiles || []).concat(this.getIntroFilesFromInstance(module, lesson));
+ promises.push(CoreFilepool.instance.addFilesToQueue(siteId, files, this.component, module.id));
+
+ if (AddonModLesson.instance.isLessonOffline(lesson)) {
+ promises.push(this.prefetchPlayData(lesson, password, accessInfo.attemptscount, modOptions));
+ }
+
+ if (accessInfo.canviewreports) {
+ promises.push(this.prefetchGroupInfo(module.id, lesson.id, modOptions));
+ promises.push(this.prefetchReportsData(module.id, lesson.id, modOptions));
+ }
+
+ await Promise.all(promises);
+ }
+
+ /**
+ * Launch a retake and return the updated access information.
+ *
+ * @param lessonId Lesson ID.
+ * @param password Password (if needed).
+ * @param modOptions Options.
+ * @param siteId Site ID.
+ */
+ protected async launchRetake(
+ lessonId: number,
+ password: string | undefined,
+ modOptions: CoreCourseCommonModWSOptions,
+ siteId: string,
+ ): Promise {
+ // The user didn't left during a timed session. Call launch retake to make sure there is a started retake.
+ await AddonModLesson.instance.launchRetake(lessonId, password, undefined, false, siteId);
+
+ const results = await Promise.all([
+ CoreUtils.instance.ignoreErrors(CoreFilepool.instance.updatePackageDownloadTime(siteId, this.component, module.id)),
+ AddonModLesson.instance.getAccessInformation(lessonId, modOptions),
+ ]);
+
+ return results[1];
+ }
+
+ /**
+ * Prefetch data to play the lesson in offline.
+ *
+ * @param lesson Lesson.
+ * @param password Password (if needed).
+ * @param retake Retake to prefetch.
+ * @param options Options.
+ * @return Promise resolved when done.
+ */
+ protected async prefetchPlayData(
+ lesson: AddonModLessonLessonWSData,
+ password: string | undefined,
+ retake: number,
+ modOptions: CoreCourseCommonModWSOptions,
+ ): Promise {
+ const passwordOptions = {
+ password,
+ ...modOptions, // Include all mod options.
+ };
+
+ await Promise.all([
+ this.prefetchPagesData(lesson, passwordOptions),
+ // Prefetch user timers to be able to calculate timemodified in offline.
+ CoreUtils.instance.ignoreErrors(AddonModLesson.instance.getTimers(lesson.id, modOptions)),
+ // Prefetch viewed pages in last retake to calculate progress.
+ AddonModLesson.instance.getContentPagesViewedOnline(lesson.id, retake, modOptions),
+ // Prefetch question attempts in last retake for offline calculations.
+ AddonModLesson.instance.getQuestionsAttemptsOnline(lesson.id, retake, modOptions),
+ ]);
+ }
+
+ /**
+ * Prefetch data related to pages.
+ *
+ * @param lesson Lesson.
+ * @param options Options.
+ * @return Promise resolved when done.
+ */
+ protected async prefetchPagesData(
+ lesson: AddonModLessonLessonWSData,
+ options: AddonModLessonPasswordOptions,
+ ): Promise {
+ const pages = await AddonModLesson.instance.getPages(lesson.id, options);
+
+ let hasRandomBranch = false;
+
+ // Get the data for each page.
+ const promises = pages.map(async (data) => {
+ // Check if any page has a RANDOMBRANCH jump.
+ if (!hasRandomBranch) {
+ for (let i = 0; i < data.jumps.length; i++) {
+ if (data.jumps[i] == AddonModLessonProvider.LESSON_RANDOMBRANCH) {
+ hasRandomBranch = true;
+ break;
+ }
+ }
+ }
+
+ // Get the page data. We don't pass accessInfo because we don't need to calculate the offline data.
+ const pageData = await AddonModLesson.instance.getPageData(lesson, data.page.id, {
+ includeContents: true,
+ includeOfflineData: false,
+ ...options, // Include all options.
+ });
+
+ // Download the page files.
+ let pageFiles = pageData.contentfiles || [];
+
+ pageData.answers.forEach((answer) => {
+ pageFiles = pageFiles.concat(answer.answerfiles);
+ pageFiles = pageFiles.concat(answer.responsefiles);
+ });
+
+ await CoreFilepool.instance.addFilesToQueue(options.siteId!, pageFiles, this.component, module.id);
+ });
+
+ // Prefetch the list of possible jumps for offline navigation. Do it here because we know hasRandomBranch.
+ promises.push(this.prefetchPossibleJumps(lesson.id, hasRandomBranch, options));
+
+ await Promise.all(promises);
+ }
+
+ /**
+ * Prefetch possible jumps.
+ *
+ * @param lessonId Lesson ID.
+ * @param hasRandomBranch Whether any page has a random branch jump.
+ * @param modOptions Options.
+ * @return Promise resolved when done.
+ */
+ protected async prefetchPossibleJumps(
+ lessonId: number,
+ hasRandomBranch: boolean,
+ modOptions: CoreCourseCommonModWSOptions,
+ ): Promise {
+ try {
+ await AddonModLesson.instance.getPagesPossibleJumps(lessonId, modOptions);
+ } catch (error) {
+ if (hasRandomBranch) {
+ // The WebSevice probably failed because RANDOMBRANCH aren't supported if the user hasn't seen any page.
+ throw new CoreError(Translate.instance.instant('addon.mod_lesson.errorprefetchrandombranch'));
+ }
+
+ throw error;
+ }
+ }
+
+ /**
+ * Prefetch group info.
+ *
+ * @param moduleId Module ID.
+ * @param lessonId Lesson ID.
+ * @param modOptions Options.
+ * @return Promise resolved when done.
+ */
+ protected async prefetchGroupInfo(
+ moduleId: number,
+ lessonId: number,
+ modOptions: CoreCourseCommonModWSOptions,
+ ): Promise {
+ const groupInfo = await CoreGroups.instance.getActivityGroupInfo(moduleId, false, undefined, modOptions.siteId, true);
+
+ await Promise.all(groupInfo.groups?.map(async (group) => {
+ await AddonModLesson.instance.getRetakesOverview(lessonId, {
+ groupId: group.id,
+ ...modOptions, // Include all options.
+ });
+ }) || []);
+ }
+
+ /**
+ * Prefetch reports data.
+ *
+ * @param moduleId Module ID.
+ * @param lessonId Lesson ID.
+ * @param modOptions Options.
+ * @return Promise resolved when done.
+ */
+ protected async prefetchReportsData(
+ moduleId: number,
+ lessonId: number,
+ modOptions: CoreCourseCommonModWSOptions,
+ ): Promise {
+ // Always get all participants, even if there are no groups.
+ const data = await AddonModLesson.instance.getRetakesOverview(lessonId, modOptions);
+ if (!data || !data.students) {
+ return;
+ }
+
+ // Prefetch the last retake for each user.
+ await Promise.all(data.students.map(async (student) => {
+ const lastRetake = student.attempts?.[student.attempts.length - 1];
+ if (!lastRetake) {
+ return;
+ }
+
+ const attempt = await AddonModLesson.instance.getUserRetake(lessonId, lastRetake.try, {
+ userId: student.id,
+ ...modOptions, // Include all options.
+ });
+
+ if (!attempt?.answerpages) {
+ return;
+ }
+
+ // Download embedded files in essays.
+ const files: CoreWSExternalFile[] = [];
+ attempt.answerpages.forEach((answerPage) => {
+ if (!answerPage.page || answerPage.page.qtype != AddonModLessonProvider.LESSON_PAGE_ESSAY) {
+ return;
+ }
+
+ answerPage.answerdata?.answers?.forEach((answer) => {
+ files.push(...CoreFilepool.instance.extractDownloadableFilesFromHtmlAsFakeFileObjects(answer[0]));
+ });
+ });
+
+ await CoreFilepool.instance.addFilesToQueue(modOptions.siteId!, files, this.component, moduleId);
+ }));
+ }
+
+ /**
+ * Validate the password.
+ *
+ * @param lessonId Lesson ID.
+ * @param info Lesson access info.
+ * @param pwd Password to check.
+ * @param options Other options.
+ * @return Promise resolved when done.
+ */
+ protected async validatePassword(
+ lessonId: number,
+ accessInfo: AddonModLessonGetAccessInformationWSResponse,
+ password: string,
+ options: CoreCourseCommonModWSOptions = {},
+ ): Promise {
+
+ options.siteId = options.siteId || CoreSites.instance.getCurrentSiteId();
+
+ const lesson = await AddonModLesson.instance.getLessonWithPassword(lessonId, {
+ password,
+ ...options, // Include all options.
+ });
+
+ // Password is ok, store it and return the data.
+ await AddonModLesson.instance.storePassword(lesson.id, password, options.siteId);
+
+ return {
+ password,
+ lesson,
+ accessInfo,
+ };
+ }
+
+ /**
+ * Sync a module.
+ *
+ * @param module Module.
+ * @param courseId Course ID the module belongs to
+ * @param siteId Site ID. If not defined, current site.
+ * @return Promise resolved when done.
+ */
+ sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise {
+ return AddonModLessonSync.instance.syncLesson(module.instance!, false, false, siteId);
+ }
+
+}
+
+export class AddonModLessonPrefetchHandler extends makeSingleton(AddonModLessonPrefetchHandlerService) {}
+
+/**
+ * Options to pass to get lesson password.
+ */
+export type AddonModLessonGetPasswordOptions = CoreCourseCommonModWSOptions & {
+ askPassword?: boolean; // True if we should ask for password if needed, false otherwise.
+};
+
+/**
+ * Result of getLessonPassword.
+ */
+export type AddonModLessonGetPasswordResult = {
+ password?: string;
+ lesson?: AddonModLessonLessonWSData;
+ accessInfo: AddonModLessonGetAccessInformationWSResponse;
+};
diff --git a/src/addons/mod/lesson/services/handlers/sync-cron.ts b/src/addons/mod/lesson/services/handlers/sync-cron.ts
new file mode 100644
index 000000000..c2dfce3ca
--- /dev/null
+++ b/src/addons/mod/lesson/services/handlers/sync-cron.ts
@@ -0,0 +1,52 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+
+import { CoreCronHandler } from '@services/cron';
+import { makeSingleton } from '@singletons';
+import { AddonModLessonSync } from '../lesson-sync';
+
+/**
+ * Synchronization cron handler.
+ */
+@Injectable({ providedIn: 'root' })
+export class AddonModLessonSyncCronHandlerService implements CoreCronHandler {
+
+ name = 'AddonModLessonSyncCronHandler';
+
+ /**
+ * Execute the process.
+ * Receives the ID of the site affected, undefined for all sites.
+ *
+ * @param siteId ID of the site affected, undefined for all sites.
+ * @param force Wether the execution is forced (manual sync).
+ * @return Promise resolved when done, rejected if failure.
+ */
+ execute(siteId?: string, force?: boolean): Promise {
+ return AddonModLessonSync.instance.syncAllLessons(siteId, force);
+ }
+
+ /**
+ * Get the time between consecutive executions.
+ *
+ * @return Time between consecutive executions (in ms).
+ */
+ getInterval(): number {
+ return AddonModLessonSync.instance.syncInterval;
+ }
+
+}
+
+export class AddonModLessonSyncCronHandler extends makeSingleton(AddonModLessonSyncCronHandlerService) {}
diff --git a/src/addons/mod/lesson/services/lesson-sync.ts b/src/addons/mod/lesson/services/lesson-sync.ts
new file mode 100644
index 000000000..48034d781
--- /dev/null
+++ b/src/addons/mod/lesson/services/lesson-sync.ts
@@ -0,0 +1,518 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+
+import { CoreError } from '@classes/errors/error';
+import { CoreNetworkError } from '@classes/errors/network-error';
+import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
+import { CoreCourse } from '@features/course/services/course';
+import { CoreCourseLogHelper } from '@features/course/services/log-helper';
+import { CoreApp } from '@services/app';
+import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
+import { CoreSync } from '@services/sync';
+import { CoreTextUtils } from '@services/utils/text';
+import { CoreTimeUtils } from '@services/utils/time';
+import { CoreUrlUtils } from '@services/utils/url';
+import { CoreUtils } from '@services/utils/utils';
+import { makeSingleton, Translate } from '@singletons';
+import { CoreEvents, CoreEventSiteData } from '@singletons/events';
+import { AddonModLessonRetakeFinishedInSyncDBRecord, RETAKES_FINISHED_SYNC_TABLE_NAME } from './database/lesson';
+import { AddonModLessonGetPasswordResult, AddonModLessonPrefetchHandler } from './handlers/prefetch';
+import { AddonModLesson, AddonModLessonLessonWSData, AddonModLessonProvider } from './lesson';
+import { AddonModLessonOffline, AddonModLessonPageAttemptRecord } from './lesson-offline';
+
+/**
+ * Service to sync lesson.
+ */
+@Injectable({ providedIn: 'root' })
+export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvider {
+
+ static readonly AUTO_SYNCED = 'addon_mod_lesson_autom_synced';
+
+ protected componentTranslate?: string;
+
+ constructor() {
+ super('AddonModLessonSyncProvider');
+ }
+
+ /**
+ * Unmark a retake as finished in a synchronization.
+ *
+ * @param lessonId Lesson ID.
+ * @param siteId Site ID. If not defined, current site.
+ * @return Promise resolved when done.
+ */
+ async deleteRetakeFinishedInSync(lessonId: number, siteId?: string): Promise {
+ const site = await CoreSites.instance.getSite(siteId);
+
+ // Ignore errors, maybe there is none.
+ await CoreUtils.instance.ignoreErrors(site.getDb().deleteRecords(RETAKES_FINISHED_SYNC_TABLE_NAME, { lessonid: lessonId }));
+ }
+
+ /**
+ * Get a retake finished in a synchronization for a certain lesson (if any).
+ *
+ * @param lessonId Lesson ID.
+ * @param siteId Site ID. If not defined, current site.
+ * @return Promise resolved with the retake entry (undefined if no retake).
+ */
+ async getRetakeFinishedInSync(
+ lessonId: number,
+ siteId?: string,
+ ): Promise {
+ const site = await CoreSites.instance.getSite(siteId);
+
+ return CoreUtils.instance.ignoreErrors(site.getDb().getRecord(RETAKES_FINISHED_SYNC_TABLE_NAME, { lessonid: lessonId }));
+ }
+
+ /**
+ * Check if a lesson has data to synchronize.
+ *
+ * @param lessonId Lesson ID.
+ * @param retake Retake number.
+ * @param siteId Site ID. If not defined, current site.
+ * @return Promise resolved with boolean: whether it has data to sync.
+ */
+ async hasDataToSync(lessonId: number, retake: number, siteId?: string): Promise {
+
+ const [hasAttempts, hasFinished] = await Promise.all([
+ CoreUtils.instance.ignoreErrors(AddonModLessonOffline.instance.hasRetakeAttempts(lessonId, retake, siteId)),
+ CoreUtils.instance.ignoreErrors(AddonModLessonOffline.instance.hasFinishedRetake(lessonId, siteId)),
+ ]);
+
+ return !!(hasAttempts || hasFinished);
+ }
+
+ /**
+ * Mark a retake as finished in a synchronization.
+ *
+ * @param lessonId Lesson ID.
+ * @param retake The retake number.
+ * @param pageId The page ID to start reviewing from.
+ * @param siteId Site ID. If not defined, current site.
+ * @return Promise resolved when done.
+ */
+ async setRetakeFinishedInSync(lessonId: number, retake: number, pageId: number, siteId?: string): Promise {
+ const site = await CoreSites.instance.getSite(siteId);
+
+ await site.getDb().insertRecord(RETAKES_FINISHED_SYNC_TABLE_NAME, {
+ lessonid: lessonId,
+ retake: Number(retake),
+ pageid: Number(pageId),
+ timefinished: CoreTimeUtils.instance.timestamp(),
+ });
+ }
+
+ /**
+ * Try to synchronize all the lessons in a certain site or in all sites.
+ *
+ * @param siteId Site ID to sync. If not defined, sync all sites.
+ * @param force Wether to force sync not depending on last execution.
+ * @return Promise resolved if sync is successful, rejected if sync fails.
+ */
+ syncAllLessons(siteId?: string, force?: boolean): Promise {
+ return this.syncOnSites('all lessons', this.syncAllLessonsFunc.bind(this, !!force), siteId);
+ }
+
+ /**
+ * Sync all lessons on a site.
+ *
+ * @param force Wether to force sync not depending on last execution.
+ * @param siteId Site ID to sync.
+ * @param Promise resolved if sync is successful, rejected if sync fails.
+ */
+ protected async syncAllLessonsFunc(force: boolean, siteId: string): Promise {
+ // Get all the lessons that have something to be synchronized.
+ const lessons = await AddonModLessonOffline.instance.getAllLessonsWithData(siteId);
+
+ // Sync all lessons that need it.
+ await Promise.all(lessons.map(async (lesson) => {
+ const result = force ?
+ await this.syncLesson(lesson.id, false, false, siteId) :
+ await this.syncLessonIfNeeded(lesson.id, false, siteId);
+
+ if (result?.updated) {
+ // Sync successful, send event.
+ CoreEvents.trigger(AddonModLessonSyncProvider.AUTO_SYNCED, {
+ lessonId: lesson.id,
+ warnings: result.warnings,
+ }, siteId);
+ }
+ }));
+ }
+
+ /**
+ * Sync a lesson only if a certain time has passed since the last time.
+ *
+ * @param lessonId Lesson ID.
+ * @param askPreflight Whether we should ask for password if needed.
+ * @param siteId Site ID. If not defined, current site.
+ * @return Promise resolved when the lesson is synced or if it doesn't need to be synced.
+ */
+ async syncLessonIfNeeded(
+ lessonId: number,
+ askPassword?: boolean,
+ siteId?: string,
+ ): Promise {
+ const needed = await this.isSyncNeeded(lessonId, siteId);
+
+ if (needed) {
+ return this.syncLesson(lessonId, askPassword, false, siteId);
+ }
+ }
+
+ /**
+ * Try to synchronize a lesson.
+ *
+ * @param lessonId Lesson ID.
+ * @param askPassword True if we should ask for password if needed, false otherwise.
+ * @param ignoreBlock True to ignore the sync block setting.
+ * @param siteId Site ID. If not defined, current site.
+ * @return Promise resolved in success.
+ */
+ async syncLesson(
+ lessonId: number,
+ askPassword?: boolean,
+ ignoreBlock?: boolean,
+ siteId?: string,
+ ): Promise {
+ siteId = siteId || CoreSites.instance.getCurrentSiteId();
+ this.componentTranslate = this.componentTranslate || CoreCourse.instance.translateModuleName('lesson');
+
+ let syncPromise = this.getOngoingSync(lessonId, siteId);
+ if (syncPromise) {
+ // There's already a sync ongoing for this lesson, return the promise.
+ return syncPromise;
+ }
+
+ // Verify that lesson isn't blocked.
+ if (!ignoreBlock && CoreSync.instance.isBlocked(AddonModLessonProvider.COMPONENT, lessonId, siteId)) {
+ this.logger.debug('Cannot sync lesson ' + lessonId + ' because it is blocked.');
+
+ throw new CoreError(Translate.instance.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
+ }
+
+ this.logger.debug('Try to sync lesson ' + lessonId + ' in site ' + siteId);
+
+ syncPromise = this.performSyncLesson(lessonId, askPassword, ignoreBlock, siteId);
+
+ return this.addOngoingSync(lessonId, syncPromise, siteId);
+ }
+
+ /**
+ * Try to synchronize a lesson.
+ *
+ * @param lessonId Lesson ID.
+ * @param askPassword True if we should ask for password if needed, false otherwise.
+ * @param ignoreBlock True to ignore the sync block setting.
+ * @param siteId Site ID. If not defined, current site.
+ * @return Promise resolved in success.
+ */
+ protected async performSyncLesson(
+ lessonId: number,
+ askPassword?: boolean,
+ ignoreBlock?: boolean,
+ siteId?: string,
+ ): Promise {
+ // Sync offline logs.
+ await CoreUtils.instance.ignoreErrors(
+ CoreCourseLogHelper.instance.syncActivity(AddonModLessonProvider.COMPONENT, lessonId, siteId),
+ );
+
+ const result: AddonModLessonSyncResult = {
+ warnings: [],
+ updated: false,
+ };
+
+ // Try to synchronize the page attempts first.
+ const passwordData = await this.syncAttempts(lessonId, result, askPassword, siteId);
+
+ // Now sync the retake.
+ await this.syncRetake(lessonId, result, passwordData, askPassword, ignoreBlock, siteId);
+
+ if (result.updated && result.courseId) {
+ try {
+ // Data has been sent to server, update data.
+ const module = await CoreCourse.instance.getModuleBasicInfoByInstance(lessonId, 'lesson', siteId);
+ await this.prefetchAfterUpdate(AddonModLessonPrefetchHandler.instance, module, result.courseId, undefined, siteId);
+ } catch {
+ // Ignore errors.
+ }
+ }
+
+ // Sync finished, set sync time.
+ await CoreUtils.instance.ignoreErrors(this.setSyncTime(String(lessonId), siteId));
+
+ // All done, return the result.
+ return result;
+ }
+
+ /**
+ * Sync all page attempts.
+ *
+ * @param lessonId Lesson ID.
+ * @param result Sync result where to store the result.
+ * @param askPassword True if we should ask for password if needed, false otherwise.
+ * @param siteId Site ID. If not defined, current site.
+ */
+ protected async syncAttempts(
+ lessonId: number,
+ result: AddonModLessonSyncResult,
+ askPassword?: boolean,
+ siteId?: string,
+ ): Promise {
+ let attempts = await AddonModLessonOffline.instance.getLessonAttempts(lessonId, siteId);
+
+ if (!attempts.length) {
+ return;
+ } else if (!CoreApp.instance.isOnline()) {
+ // Cannot sync in offline.
+ throw new CoreNetworkError();
+ }
+
+ result.courseId = attempts[0].courseid;
+ const attemptsLength = attempts.length;
+
+ // Get the info, access info and the lesson password if needed.
+ const lesson = await AddonModLesson.instance.getLessonById(result.courseId, lessonId, { siteId });
+
+ const passwordData = await AddonModLessonPrefetchHandler.instance.getLessonPassword(lessonId, {
+ readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
+ askPassword,
+ siteId,
+ });
+
+ const promises: Promise[] = [];
+ passwordData.lesson = passwordData.lesson || lesson;
+
+ // Filter the attempts, get only the ones that belong to the current retake.
+ attempts = attempts.filter((attempt) => {
+ if (attempt.retake == passwordData.accessInfo.attemptscount) {
+ return true;
+ }
+
+ // Attempt doesn't belong to current retake, delete.
+ promises.push(CoreUtils.instance.ignoreErrors(AddonModLessonOffline.instance.deleteAttempt(
+ lesson.id,
+ attempt.retake,
+ attempt.pageid,
+ attempt.timemodified,
+ siteId,
+ )));
+
+ return false;
+ });
+
+ if (attempts.length != attemptsLength) {
+ // Some attempts won't be sent, add a warning.
+ result.warnings.push(Translate.instance.instant('core.warningofflinedatadeleted', {
+ component: this.componentTranslate,
+ name: lesson.name,
+ error: Translate.instance.instant('addon.mod_lesson.warningretakefinished'),
+ }));
+ }
+
+ await Promise.all(promises);
+
+ if (!attempts.length) {
+ return passwordData;
+ }
+
+ // Send the attempts in the same order they were answered.
+ attempts.sort((a, b) => a.timemodified - b.timemodified);
+
+ const promisesData = attempts.map((attempt) => ({
+ function: this.sendAttempt.bind(this, lesson, passwordData.password, attempt, result, siteId),
+ blocking: true,
+ }));
+
+ await CoreUtils.instance.executeOrderedPromises(promisesData);
+
+ return passwordData;
+ }
+
+ /**
+ * Send an attempt to the site and delete it afterwards.
+ *
+ * @param lesson Lesson.
+ * @param password Password (if any).
+ * @param attempt Attempt to send.
+ * @param result Result where to store the data.
+ * @param siteId Site ID. If not defined, current site.
+ * @return Promise resolved when done.
+ */
+ protected async sendAttempt(
+ lesson: AddonModLessonLessonWSData,
+ password: string,
+ attempt: AddonModLessonPageAttemptRecord,
+ result: AddonModLessonSyncResult,
+ siteId?: string,
+ ): Promise {
+ const retake = attempt.retake;
+ const pageId = attempt.pageid;
+ const timemodified = attempt.timemodified;
+
+ try {
+ // Send the page data.
+ await AddonModLesson.instance.processPageOnline(lesson.id, attempt.pageid, attempt.data || {}, {
+ password,
+ siteId,
+ });
+
+ result.updated = true;
+
+ await AddonModLessonOffline.instance.deleteAttempt(lesson.id, retake, pageId, timemodified, siteId);
+ } catch (error) {
+ if (!error || !CoreUtils.instance.isWebServiceError(error)) {
+ // Couldn't connect to server.
+ throw error;
+ }
+
+ // The WebService has thrown an error, this means that the attempt cannot be submitted. Delete it.
+ result.updated = true;
+
+ await AddonModLessonOffline.instance.deleteAttempt(lesson.id, retake, pageId, timemodified, siteId);
+
+ // Attempt deleted, add a warning.
+ result.warnings.push(Translate.instance.instant('core.warningofflinedatadeleted', {
+ component: this.componentTranslate,
+ name: lesson.name,
+ error: CoreTextUtils.instance.getErrorMessageFromError(error),
+ }));
+ }
+ }
+
+ /**
+ * Sync retake.
+ *
+ * @param lessonId Lesson ID.
+ * @param result Sync result where to store the result.
+ * @param passwordData Password data. If not provided it will be calculated.
+ * @param askPassword True if we should ask for password if needed, false otherwise.
+ * @param ignoreBlock True to ignore the sync block setting.
+ * @param siteId Site ID. If not defined, current site.
+ */
+ protected async syncRetake(
+ lessonId: number,
+ result: AddonModLessonSyncResult,
+ passwordData?: AddonModLessonGetPasswordResult,
+ askPassword?: boolean,
+ ignoreBlock?: boolean,
+ siteId?: string,
+ ): Promise {
+ // Attempts sent or there was none. If there is a finished retake, send it.
+ const retake = await CoreUtils.instance.ignoreErrors(AddonModLessonOffline.instance.getRetake(lessonId, siteId));
+
+ if (!retake) {
+ // No retake to sync.
+ return;
+ }
+
+ if (!retake.finished) {
+ // The retake isn't marked as finished, nothing to send. Delete the retake.
+ await AddonModLessonOffline.instance.deleteRetake(lessonId, siteId);
+
+ return;
+ } else if (!CoreApp.instance.isOnline()) {
+ // Cannot sync in offline.
+ throw new CoreNetworkError();
+ }
+
+ result.courseId = retake.courseid || result.courseId;
+
+ if (!passwordData?.lesson) {
+ // Retrieve the needed data.
+ const lesson = await AddonModLesson.instance.getLessonById(result.courseId!, lessonId, { siteId });
+ passwordData = await AddonModLessonPrefetchHandler.instance.getLessonPassword(lessonId, {
+ readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
+ askPassword,
+ siteId,
+ });
+
+ passwordData.lesson = passwordData.lesson || lesson;
+ }
+
+ if (retake.retake != passwordData.accessInfo.attemptscount) {
+ // The retake changed, add a warning if it isn't there already.
+ if (!result.warnings.length) {
+ result.warnings.push(Translate.instance.instant('core.warningofflinedatadeleted', {
+ component: this.componentTranslate,
+ name: passwordData.lesson.name,
+ error: Translate.instance.instant('addon.mod_lesson.warningretakefinished'),
+ }));
+ }
+
+ await AddonModLessonOffline.instance.deleteRetake(lessonId, siteId);
+ }
+
+ try {
+ // All good, finish the retake.
+ const response = await AddonModLesson.instance.finishRetakeOnline(lessonId, {
+ password: passwordData.password,
+ siteId,
+ });
+
+ result.updated = true;
+
+ // Mark the retake as finished in a sync if it can be reviewed.
+ if (!ignoreBlock && response.data?.reviewlesson) {
+ const params = CoreUrlUtils.instance.extractUrlParams( response.data.reviewlesson.value);
+ if (params.pageid) {
+ // The retake can be reviewed, mark it as finished. Don't block the user for this.
+ this.setRetakeFinishedInSync(lessonId, retake.retake, Number(params.pageid), siteId);
+ }
+ }
+
+ await AddonModLessonOffline.instance.deleteRetake(lessonId, siteId);
+ } catch (error) {
+ if (!error || !CoreUtils.instance.isWebServiceError(error)) {
+ // Couldn't connect to server.
+ throw error;
+ }
+
+ // The WebService has thrown an error, this means that responses cannot be submitted. Delete them.
+ result.updated = true;
+
+ await AddonModLessonOffline.instance.deleteRetake(lessonId, siteId);
+
+ // Retake deleted, add a warning.
+ result.warnings.push(Translate.instance.instant('core.warningofflinedatadeleted', {
+ component: this.componentTranslate,
+ name: passwordData.lesson.name,
+ error: CoreTextUtils.instance.getErrorMessageFromError(error),
+ }));
+ }
+ }
+
+}
+
+export class AddonModLessonSync extends makeSingleton(AddonModLessonSyncProvider) {}
+
+/**
+ * Data returned by a lesson sync.
+ */
+export type AddonModLessonSyncResult = {
+ warnings: string[]; // List of warnings.
+ updated: boolean; // Whether some data was sent to the server or offline data was updated.
+ courseId?: number; // Course the lesson belongs to (if known).
+};
+
+/**
+ * Data passed to AUTO_SYNCED event.
+ */
+export type AddonModLessonAutoSyncData = CoreEventSiteData & {
+ lessonId: number;
+ warnings: string[];
+};