forked from CIT/Vmeda.Online
		
	MOBILE-3651 quiz: Implement sync service and prefetch handler
This commit is contained in:
		
							parent
							
								
									596ef954ba
								
							
						
					
					
						commit
						7698fa673d
					
				@ -53,7 +53,7 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda
 | 
			
		||||
     * @return Promise resolved if sync is successful, rejected if sync fails.
 | 
			
		||||
     */
 | 
			
		||||
    async syncAllEvents(siteId?: string, force = false): Promise<void> {
 | 
			
		||||
        await this.syncOnSites('all calendar events', this.syncAllEventsFunc.bind(this, [force]), siteId);
 | 
			
		||||
        await this.syncOnSites('all calendar events', this.syncAllEventsFunc.bind(this, force), siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -74,17 +74,17 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider<AddonMessage
 | 
			
		||||
    syncAllDiscussions(siteId?: string, onlyDeviceOffline: boolean = false): Promise<void> {
 | 
			
		||||
        const syncFunctionLog = 'all discussions' + (onlyDeviceOffline ? ' (Only offline)' : '');
 | 
			
		||||
 | 
			
		||||
        return this.syncOnSites(syncFunctionLog, this.syncAllDiscussionsFunc.bind(this, [onlyDeviceOffline]), siteId);
 | 
			
		||||
        return this.syncOnSites(syncFunctionLog, this.syncAllDiscussionsFunc.bind(this, onlyDeviceOffline), siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all messages pending to be sent in the site.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID to sync. If not defined, sync all sites.
 | 
			
		||||
     * @param onlyDeviceOffline True to only sync discussions that failed because device was offline.
 | 
			
		||||
     * @param siteId Site ID to sync. If not defined, sync all sites.
 | 
			
		||||
     * @param Promise resolved if sync is successful, rejected if sync fails.
 | 
			
		||||
     */
 | 
			
		||||
    protected async syncAllDiscussionsFunc(siteId: string, onlyDeviceOffline = false): Promise<void> {
 | 
			
		||||
    protected async syncAllDiscussionsFunc(onlyDeviceOffline: boolean, siteId: string): Promise<void> {
 | 
			
		||||
        const userIds: number[] = [];
 | 
			
		||||
        const conversationIds: number[] = [];
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
@ -253,7 +253,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Sync finished, set sync time.
 | 
			
		||||
        await CoreUtils.instance.ignoreErrors(this.setSyncTime(String(lessonId), siteId));
 | 
			
		||||
        await CoreUtils.instance.ignoreErrors(this.setSyncTime(lessonId, siteId));
 | 
			
		||||
 | 
			
		||||
        // All done, return the result.
 | 
			
		||||
        return result;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										659
									
								
								src/addons/mod/quiz/services/handlers/prefetch.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										659
									
								
								src/addons/mod/quiz/services/handlers/prefetch.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,659 @@
 | 
			
		||||
// (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 { CoreConstants } from '@/core/constants';
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
 | 
			
		||||
import { CoreCourseAnyModuleData, CoreCourseCommonModWSOptions } from '@features/course/services/course';
 | 
			
		||||
import { CoreQuestionHelper } from '@features/question/services/question-helper';
 | 
			
		||||
import { CoreFilepool } from '@services/filepool';
 | 
			
		||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreWSExternalFile } from '@services/ws';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonModQuizAccessRuleDelegate } from '../access-rules-delegate';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModQuiz,
 | 
			
		||||
    AddonModQuizAttemptWSData,
 | 
			
		||||
    AddonModQuizGetQuizAccessInformationWSResponse,
 | 
			
		||||
    AddonModQuizProvider,
 | 
			
		||||
    AddonModQuizQuizWSData,
 | 
			
		||||
} from '../quiz';
 | 
			
		||||
import { AddonModQuizHelper } from '../quiz-helper';
 | 
			
		||||
import { AddonModQuizSync, AddonModQuizSyncResult } from '../quiz-sync';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler to prefetch quizzes.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModQuiz';
 | 
			
		||||
    modName = 'quiz';
 | 
			
		||||
    component = AddonModQuizProvider.COMPONENT;
 | 
			
		||||
    updatesNames = /^configuration$|^.*files$|^grades$|^gradeitems$|^questions$|^attempts$/;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Download the module.
 | 
			
		||||
     *
 | 
			
		||||
     * @param module The module object returned by WS.
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @param dirPath Path of the directory where to store all the content files.
 | 
			
		||||
     * @param single True if we're downloading a single module, false if we're downloading a whole section.
 | 
			
		||||
     * @param canStart If true, start a new attempt if needed.
 | 
			
		||||
     * @return Promise resolved when all content is downloaded.
 | 
			
		||||
     */
 | 
			
		||||
    download(
 | 
			
		||||
        module: CoreCourseAnyModuleData,
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        dirPath?: string,
 | 
			
		||||
        single?: boolean,
 | 
			
		||||
        canStart: boolean = true,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        // Same implementation for download and prefetch.
 | 
			
		||||
        return this.prefetch(module, courseId, single, dirPath, canStart);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get list of files. If not defined, we'll assume they're in module.contents.
 | 
			
		||||
     *
 | 
			
		||||
     * @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 list of files.
 | 
			
		||||
     */
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    async getFiles(module: CoreCourseAnyModuleData, courseId: number, single?: boolean): Promise<CoreWSExternalFile[]> {
 | 
			
		||||
        try {
 | 
			
		||||
            const quiz = await AddonModQuiz.instance.getQuiz(courseId, module.id);
 | 
			
		||||
 | 
			
		||||
            const files = this.getIntroFilesFromInstance(module, quiz);
 | 
			
		||||
 | 
			
		||||
            const attempts = await AddonModQuiz.instance.getUserAttempts(quiz.id, {
 | 
			
		||||
                cmId: module.id,
 | 
			
		||||
                readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const attemptFiles = await this.getAttemptsFeedbackFiles(quiz, attempts);
 | 
			
		||||
 | 
			
		||||
            return files.concat(attemptFiles);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Quiz not found, return empty list.
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the list of downloadable files on feedback attemptss.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param attempts Quiz user attempts.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return List of Files.
 | 
			
		||||
     */
 | 
			
		||||
    protected async getAttemptsFeedbackFiles(
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        attempts: AddonModQuizAttemptWSData[],
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<CoreWSExternalFile[]> {
 | 
			
		||||
        const getInlineFiles = CoreSites.instance.getCurrentSite()?.isVersionGreaterEqualThan('3.2');
 | 
			
		||||
        let files: CoreWSExternalFile[] = [];
 | 
			
		||||
 | 
			
		||||
        await Promise.all(attempts.map(async (attempt) => {
 | 
			
		||||
            if (!AddonModQuiz.instance.isAttemptFinished(attempt.state)) {
 | 
			
		||||
                // Attempt not finished, no feedback files.
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const attemptGrade = AddonModQuiz.instance.rescaleGrade(attempt.sumgrades, quiz, false);
 | 
			
		||||
            if (typeof attemptGrade == 'undefined') {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const feedback = await AddonModQuiz.instance.getFeedbackForGrade(quiz.id, Number(attemptGrade), {
 | 
			
		||||
                cmId: quiz.coursemodule,
 | 
			
		||||
                readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
                siteId,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (getInlineFiles && feedback.feedbackinlinefiles?.length) {
 | 
			
		||||
                files = files.concat(feedback.feedbackinlinefiles);
 | 
			
		||||
            } else if (feedback.feedbacktext && !getInlineFiles) {
 | 
			
		||||
                files = files.concat(
 | 
			
		||||
                    CoreFilepool.instance.extractDownloadableFilesFromHtmlAsFakeFileObjects(feedback.feedbacktext),
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        return files;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gather some preflight data for an attempt. This function will start a new attempt if needed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param accessInfo Quiz access info returned by AddonModQuizProvider.getQuizAccessInformation.
 | 
			
		||||
     * @param attempt Attempt to continue. Don't pass any value if the user needs to start a new attempt.
 | 
			
		||||
     * @param askPreflight Whether it should ask for preflight data if needed.
 | 
			
		||||
     * @param modalTitle Lang key of the title to set to preflight modal (e.g. 'addon.mod_quiz.startattempt').
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the preflight data.
 | 
			
		||||
     */
 | 
			
		||||
    async getPreflightData(
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
 | 
			
		||||
        attempt?: AddonModQuizAttemptWSData,
 | 
			
		||||
        askPreflight?: boolean,
 | 
			
		||||
        title?: string,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<Record<string, string>> {
 | 
			
		||||
        const preflightData: Record<string, string> = {};
 | 
			
		||||
 | 
			
		||||
        if (askPreflight) {
 | 
			
		||||
            // We can ask preflight, check if it's needed and get the data.
 | 
			
		||||
            await AddonModQuizHelper.instance.getAndCheckPreflightData(
 | 
			
		||||
                quiz,
 | 
			
		||||
                accessInfo,
 | 
			
		||||
                preflightData,
 | 
			
		||||
                attempt,
 | 
			
		||||
                false,
 | 
			
		||||
                true,
 | 
			
		||||
                title,
 | 
			
		||||
                siteId,
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            // Get some fixed preflight data from access rules (data that doesn't require user interaction).
 | 
			
		||||
            const rules = accessInfo?.activerulenames || [];
 | 
			
		||||
 | 
			
		||||
            await AddonModQuizAccessRuleDelegate.instance.getFixedPreflightData(rules, quiz, preflightData, attempt, true, siteId);
 | 
			
		||||
 | 
			
		||||
            if (!attempt) {
 | 
			
		||||
                // We need to create a new attempt.
 | 
			
		||||
                await AddonModQuiz.instance.startAttempt(quiz.id, preflightData, false, siteId);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return preflightData;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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.
 | 
			
		||||
     */
 | 
			
		||||
    invalidateContent(moduleId: number, courseId: number): Promise<void> {
 | 
			
		||||
        return AddonModQuiz.instance.invalidateContent(moduleId, courseId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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<void> {
 | 
			
		||||
        // Invalidate the calls required to check if a quiz is downloadable.
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            AddonModQuiz.instance.invalidateQuizData(courseId),
 | 
			
		||||
            AddonModQuiz.instance.invalidateUserAttemptsForUser(module.instance!),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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<boolean> {
 | 
			
		||||
        if (CoreSites.instance.getCurrentSite()?.isOfflineDisabled()) {
 | 
			
		||||
            // Don't allow downloading the quiz if offline is disabled to prevent wasting a lot of data when opening it.
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const siteId = CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        const quiz = await AddonModQuiz.instance.getQuiz(courseId, module.id, { siteId });
 | 
			
		||||
 | 
			
		||||
        if (quiz.allowofflineattempts !== 1 || quiz.hasquestions === 0) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Not downloadable if we reached max attempts or the quiz has an unfinished attempt.
 | 
			
		||||
        const attempts = await AddonModQuiz.instance.getUserAttempts(quiz.id, {
 | 
			
		||||
            cmId: module.id,
 | 
			
		||||
            siteId,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const isLastFinished = !attempts.length || AddonModQuiz.instance.isAttemptFinished(attempts[attempts.length - 1].state);
 | 
			
		||||
 | 
			
		||||
        return quiz.attempts === 0 || quiz.attempts! > attempts.length || !isLastFinished;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether or not the handler is enabled on a site level.
 | 
			
		||||
     *
 | 
			
		||||
     * @return A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
 | 
			
		||||
     */
 | 
			
		||||
    async isEnabled(): Promise<boolean> {
 | 
			
		||||
        return AddonModQuiz.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.
 | 
			
		||||
     * @param canStart If true, start a new attempt if needed.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async prefetch(
 | 
			
		||||
        module: SyncedModule,
 | 
			
		||||
        courseId?: number,
 | 
			
		||||
        single?: boolean,
 | 
			
		||||
        dirPath?: string,
 | 
			
		||||
        canStart: boolean = true,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        if (module.attemptFinished) {
 | 
			
		||||
            // Delete the value so it does not block anything if true.
 | 
			
		||||
            delete module.attemptFinished;
 | 
			
		||||
 | 
			
		||||
            // Quiz got synced recently and an attempt has finished. Do not prefetch.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const siteId = CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        return this.prefetchPackage(module, courseId, this.prefetchQuiz.bind(this, module, courseId, single, siteId, canStart));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prefetch a quiz.
 | 
			
		||||
     *
 | 
			
		||||
     * @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 siteId Site ID.
 | 
			
		||||
     * @param canStart If true, start a new attempt if needed.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async prefetchQuiz(
 | 
			
		||||
        module: CoreCourseAnyModuleData,
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        single: boolean,
 | 
			
		||||
        siteId: string,
 | 
			
		||||
        canStart: boolean,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        const commonOptions = {
 | 
			
		||||
            readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
            siteId,
 | 
			
		||||
        };
 | 
			
		||||
        const modOptions = {
 | 
			
		||||
            cmId: module.id,
 | 
			
		||||
            ...commonOptions, // Include all common options.
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Get quiz.
 | 
			
		||||
        const quiz = await AddonModQuiz.instance.getQuiz(courseId, module.id, commonOptions);
 | 
			
		||||
 | 
			
		||||
        const introFiles = this.getIntroFilesFromInstance(module, quiz);
 | 
			
		||||
 | 
			
		||||
        // Prefetch some quiz data.
 | 
			
		||||
        // eslint-disable-next-line prefer-const
 | 
			
		||||
        let [quizAccessInfo, attempts, attemptAccessInfo] = await Promise.all([
 | 
			
		||||
            AddonModQuiz.instance.getQuizAccessInformation(quiz.id, modOptions),
 | 
			
		||||
            AddonModQuiz.instance.getUserAttempts(quiz.id, modOptions),
 | 
			
		||||
            AddonModQuiz.instance.getAttemptAccessInformation(quiz.id, 0, modOptions),
 | 
			
		||||
            AddonModQuiz.instance.getQuizRequiredQtypes(quiz.id, modOptions),
 | 
			
		||||
            CoreFilepool.instance.addFilesToQueue(siteId, introFiles, AddonModQuizProvider.COMPONENT, module.id),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Check if we need to start a new attempt.
 | 
			
		||||
        let attempt: AddonModQuizAttemptWSData | undefined = attempts[attempts.length - 1];
 | 
			
		||||
        let preflightData: Record<string, string> = {};
 | 
			
		||||
        let startAttempt = false;
 | 
			
		||||
 | 
			
		||||
        if (canStart || attempt) {
 | 
			
		||||
            if (canStart && (!attempt || AddonModQuiz.instance.isAttemptFinished(attempt.state))) {
 | 
			
		||||
                // Check if the user can attempt the quiz.
 | 
			
		||||
                if (attemptAccessInfo.preventnewattemptreasons.length) {
 | 
			
		||||
                    throw new CoreError(CoreTextUtils.instance.buildMessage(attemptAccessInfo.preventnewattemptreasons));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                startAttempt = true;
 | 
			
		||||
                attempt = undefined;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Get the preflight data. This function will also start a new attempt if needed.
 | 
			
		||||
            preflightData = await this.getPreflightData(quiz, quizAccessInfo, attempt, single, 'core.download', siteId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const promises: Promise<unknown>[] = [];
 | 
			
		||||
 | 
			
		||||
        if (startAttempt) {
 | 
			
		||||
            // Re-fetch user attempts since we created a new one.
 | 
			
		||||
            promises.push(AddonModQuiz.instance.getUserAttempts(quiz.id, modOptions).then(async (atts) => {
 | 
			
		||||
                attempts = atts;
 | 
			
		||||
 | 
			
		||||
                const attemptFiles = await this.getAttemptsFeedbackFiles(quiz, attempts, siteId);
 | 
			
		||||
 | 
			
		||||
                return CoreFilepool.instance.addFilesToQueue(siteId, attemptFiles, AddonModQuizProvider.COMPONENT, module.id);
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            // Update the download time to prevent detecting the new attempt as an update.
 | 
			
		||||
            promises.push(CoreUtils.instance.ignoreErrors(
 | 
			
		||||
                CoreFilepool.instance.updatePackageDownloadTime(siteId, AddonModQuizProvider.COMPONENT, module.id),
 | 
			
		||||
            ));
 | 
			
		||||
        } else {
 | 
			
		||||
            // Use the already fetched attempts.
 | 
			
		||||
            promises.push(this.getAttemptsFeedbackFiles(quiz, attempts, siteId).then((attemptFiles) =>
 | 
			
		||||
                CoreFilepool.instance.addFilesToQueue(siteId, attemptFiles, AddonModQuizProvider.COMPONENT, module.id)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Fetch attempt related data.
 | 
			
		||||
        promises.push(AddonModQuiz.instance.getCombinedReviewOptions(quiz.id, modOptions));
 | 
			
		||||
        promises.push(AddonModQuiz.instance.getUserBestGrade(quiz.id, modOptions));
 | 
			
		||||
        promises.push(this.prefetchGradeAndFeedback(quiz, modOptions, siteId));
 | 
			
		||||
        promises.push(AddonModQuiz.instance.getAttemptAccessInformation(quiz.id, 0, modOptions)); // Last attempt.
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
        // We have quiz data, now we'll get specific data for each attempt.
 | 
			
		||||
        await Promise.all(attempts.map(async (attempt) => {
 | 
			
		||||
            await this.prefetchAttempt(quiz, attempt, preflightData, siteId);
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        if (!canStart) {
 | 
			
		||||
            // Nothing else to do.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If there's nothing to send, mark the quiz as synchronized.
 | 
			
		||||
        const hasData = await AddonModQuizSync.instance.hasDataToSync(quiz.id, siteId);
 | 
			
		||||
 | 
			
		||||
        if (!hasData) {
 | 
			
		||||
            AddonModQuizSync.instance.setSyncTime(quiz.id, siteId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prefetch all WS data for an attempt.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param attempt Attempt.
 | 
			
		||||
     * @param preflightData Preflight required data (like password).
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when the prefetch is finished. Data returned is not reliable.
 | 
			
		||||
     */
 | 
			
		||||
    async prefetchAttempt(
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        attempt: AddonModQuizAttemptWSData,
 | 
			
		||||
        preflightData: Record<string, string>,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        const pages = AddonModQuiz.instance.getPagesFromLayout(attempt.layout);
 | 
			
		||||
        const isSequential = AddonModQuiz.instance.isNavigationSequential(quiz);
 | 
			
		||||
        let promises: Promise<unknown>[] = [];
 | 
			
		||||
 | 
			
		||||
        const modOptions: CoreCourseCommonModWSOptions = {
 | 
			
		||||
            cmId: quiz.coursemodule,
 | 
			
		||||
            readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
            siteId,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (AddonModQuiz.instance.isAttemptFinished(attempt.state)) {
 | 
			
		||||
            // Attempt is finished, get feedback and review data.
 | 
			
		||||
            const attemptGrade = AddonModQuiz.instance.rescaleGrade(attempt.sumgrades, quiz, false);
 | 
			
		||||
            if (typeof attemptGrade != 'undefined') {
 | 
			
		||||
                promises.push(AddonModQuiz.instance.getFeedbackForGrade(quiz.id, Number(attemptGrade), modOptions));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Get the review for each page.
 | 
			
		||||
            pages.forEach((page) => {
 | 
			
		||||
                promises.push(CoreUtils.instance.ignoreErrors(AddonModQuiz.instance.getAttemptReview(attempt.id, {
 | 
			
		||||
                    page,
 | 
			
		||||
                    ...modOptions, // Include all options.
 | 
			
		||||
                })));
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Get the review for all questions in same page.
 | 
			
		||||
            promises.push(this.prefetchAttemptReviewFiles(quiz, attempt, modOptions, siteId));
 | 
			
		||||
        } else {
 | 
			
		||||
 | 
			
		||||
            // Attempt not finished, get data needed to continue the attempt.
 | 
			
		||||
            promises.push(AddonModQuiz.instance.getAttemptAccessInformation(quiz.id, attempt.id, modOptions));
 | 
			
		||||
            promises.push(AddonModQuiz.instance.getAttemptSummary(attempt.id, preflightData, modOptions));
 | 
			
		||||
 | 
			
		||||
            if (attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS) {
 | 
			
		||||
                // Get data for each page.
 | 
			
		||||
                promises = promises.concat(pages.map(async (page) => {
 | 
			
		||||
                    if (isSequential && page < attempt.currentpage!) {
 | 
			
		||||
                        // Sequential quiz, cannot get pages before the current one.
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    const data = await AddonModQuiz.instance.getAttemptData(attempt.id, page, preflightData, modOptions);
 | 
			
		||||
 | 
			
		||||
                    // Download the files inside the questions.
 | 
			
		||||
                    await Promise.all(data.questions.map(async (question) => {
 | 
			
		||||
                        await CoreQuestionHelper.instance.prefetchQuestionFiles(
 | 
			
		||||
                            question,
 | 
			
		||||
                            this.component,
 | 
			
		||||
                            quiz.coursemodule,
 | 
			
		||||
                            siteId,
 | 
			
		||||
                            attempt.uniqueid,
 | 
			
		||||
                        );
 | 
			
		||||
                    }));
 | 
			
		||||
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prefetch attempt review and its files.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param attempt Attempt.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @param siteId Site ID.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async prefetchAttemptReviewFiles(
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        attempt: AddonModQuizAttemptWSData,
 | 
			
		||||
        modOptions: CoreCourseCommonModWSOptions,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        // Get the review for all questions in same page.
 | 
			
		||||
        const data = await CoreUtils.instance.ignoreErrors(AddonModQuiz.instance.getAttemptReview(attempt.id, {
 | 
			
		||||
            page: -1,
 | 
			
		||||
            ...modOptions, // Include all options.
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        if (!data) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        // Download the files inside the questions.
 | 
			
		||||
        await Promise.all(data.questions.map((question) => {
 | 
			
		||||
            CoreQuestionHelper.instance.prefetchQuestionFiles(
 | 
			
		||||
                question,
 | 
			
		||||
                this.component,
 | 
			
		||||
                quiz.coursemodule,
 | 
			
		||||
                siteId,
 | 
			
		||||
                attempt.uniqueid,
 | 
			
		||||
            );
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prefetch quiz grade and its feedback.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param modOptions Other options.
 | 
			
		||||
     * @param siteId Site ID.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async prefetchGradeAndFeedback(
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        modOptions: CoreCourseCommonModWSOptions,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            const gradebookData = await AddonModQuiz.instance.getGradeFromGradebook(quiz.course, quiz.coursemodule, true, siteId);
 | 
			
		||||
 | 
			
		||||
            if (typeof gradebookData.graderaw != 'undefined') {
 | 
			
		||||
                await AddonModQuiz.instance.getFeedbackForGrade(quiz.id, gradebookData.graderaw, modOptions);
 | 
			
		||||
            }
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prefetches some data for a quiz and its last attempt.
 | 
			
		||||
     * This function will NOT start a new attempt, it only reads data for the quiz and the last attempt.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param askPreflight Whether it should ask for preflight data if needed.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async prefetchQuizAndLastAttempt(quiz: AddonModQuizQuizWSData, askPreflight?: boolean, siteId?: string): Promise<void> {
 | 
			
		||||
        siteId = siteId || CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        const modOptions = {
 | 
			
		||||
            cmId: quiz.coursemodule,
 | 
			
		||||
            readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
            siteId,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Get quiz data.
 | 
			
		||||
        const [quizAccessInfo, attempts] = await Promise.all([
 | 
			
		||||
            AddonModQuiz.instance.getQuizAccessInformation(quiz.id, modOptions),
 | 
			
		||||
            AddonModQuiz.instance.getUserAttempts(quiz.id, modOptions),
 | 
			
		||||
            AddonModQuiz.instance.getQuizRequiredQtypes(quiz.id, modOptions),
 | 
			
		||||
            AddonModQuiz.instance.getCombinedReviewOptions(quiz.id, modOptions),
 | 
			
		||||
            AddonModQuiz.instance.getUserBestGrade(quiz.id, modOptions),
 | 
			
		||||
            this.prefetchGradeAndFeedback(quiz, modOptions, siteId),
 | 
			
		||||
            AddonModQuiz.instance.getAttemptAccessInformation(quiz.id, 0, modOptions), // Last attempt.
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        const lastAttempt = attempts[attempts.length - 1];
 | 
			
		||||
        let preflightData: Record<string, string> = {};
 | 
			
		||||
        if (lastAttempt) {
 | 
			
		||||
            // Get the preflight data.
 | 
			
		||||
            preflightData = await this.getPreflightData(quiz, quizAccessInfo, lastAttempt, askPreflight, 'core.download', siteId);
 | 
			
		||||
 | 
			
		||||
            // Get data for last attempt.
 | 
			
		||||
            await this.prefetchAttempt(quiz, lastAttempt, preflightData, siteId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Prefetch finished, set the right status.
 | 
			
		||||
        await this.setStatusAfterPrefetch(quiz, {
 | 
			
		||||
            cmId: quiz.coursemodule,
 | 
			
		||||
            attempts,
 | 
			
		||||
            readingStrategy: CoreSitesReadingStrategy.PreferCache,
 | 
			
		||||
            siteId,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set the right status to a quiz after prefetching.
 | 
			
		||||
     * If the last attempt is finished or there isn't one, set it as not downloaded to show download icon.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async setStatusAfterPrefetch(
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        options: AddonModQuizSetStatusAfterPrefetchOptions = {},
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        options.siteId = options.siteId || CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        let attempts = options.attempts;
 | 
			
		||||
 | 
			
		||||
        if (!attempts) {
 | 
			
		||||
            // Get the attempts.
 | 
			
		||||
            attempts = await AddonModQuiz.instance.getUserAttempts(quiz.id, options);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check the current status of the quiz.
 | 
			
		||||
        const status = await CoreFilepool.instance.getPackageStatus(options.siteId, this.component, quiz.coursemodule);
 | 
			
		||||
 | 
			
		||||
        if (status === CoreConstants.NOT_DOWNLOADED) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Quiz was downloaded, set the new status.
 | 
			
		||||
        // If no attempts or last is finished we'll mark it as not downloaded to show download icon.
 | 
			
		||||
        const lastAttempt = attempts[attempts.length - 1];
 | 
			
		||||
        const isLastFinished = !lastAttempt || AddonModQuiz.instance.isAttemptFinished(lastAttempt.state);
 | 
			
		||||
        const newStatus = isLastFinished ? CoreConstants.NOT_DOWNLOADED : CoreConstants.DOWNLOADED;
 | 
			
		||||
 | 
			
		||||
        await CoreFilepool.instance.storePackageStatus(options.siteId, newStatus, this.component, quiz.coursemodule);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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.
 | 
			
		||||
     */
 | 
			
		||||
    async sync(module: SyncedModule, courseId: number, siteId?: string): Promise<AddonModQuizSyncResult | undefined> {
 | 
			
		||||
        const quiz = await AddonModQuiz.instance.getQuiz(courseId, module.id, { siteId });
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const result = await AddonModQuizSync.instance.syncQuiz(quiz, false, siteId);
 | 
			
		||||
 | 
			
		||||
            module.attemptFinished = result.attemptFinished || false;
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
            module.attemptFinished = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class AddonModQuizPrefetchHandler extends makeSingleton(AddonModQuizPrefetchHandlerService) {}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Options to pass to setStatusAfterPrefetch.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModQuizSetStatusAfterPrefetchOptions = CoreCourseCommonModWSOptions & {
 | 
			
		||||
    attempts?: AddonModQuizAttemptWSData[]; // List of attempts. If not provided, they will be calculated.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Module data with some calculated data.
 | 
			
		||||
 */
 | 
			
		||||
type SyncedModule = CoreCourseAnyModuleData & {
 | 
			
		||||
    attemptFinished?: boolean;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										513
									
								
								src/addons/mod/quiz/services/quiz-sync.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										513
									
								
								src/addons/mod/quiz/services/quiz-sync.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,513 @@
 | 
			
		||||
// (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 { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
 | 
			
		||||
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
 | 
			
		||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
 | 
			
		||||
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
 | 
			
		||||
import { CoreQuestion, CoreQuestionQuestionParsed } from '@features/question/services/question';
 | 
			
		||||
import { CoreQuestionDelegate } from '@features/question/services/question-delegate';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreEvents, CoreEventSiteData } from '@singletons/events';
 | 
			
		||||
import { AddonModQuizAttemptDBRecord } from './database/quiz';
 | 
			
		||||
import { AddonModQuizPrefetchHandler } from './handlers/prefetch';
 | 
			
		||||
import { AddonModQuiz, AddonModQuizAttemptWSData, AddonModQuizProvider, AddonModQuizQuizWSData } from './quiz';
 | 
			
		||||
import { AddonModQuizOffline, AddonModQuizQuestionsWithAnswers } from './quiz-offline';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service to sync quizzes.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider<AddonModQuizSyncResult> {
 | 
			
		||||
 | 
			
		||||
    static readonly AUTO_SYNCED = 'addon_mod_quiz_autom_synced';
 | 
			
		||||
 | 
			
		||||
    protected componentTranslate?: string;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super('AddonModQuizSyncProvider');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Finish a sync process: remove offline data if needed, prefetch quiz data, set sync time and return the result.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID.
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @param warnings List of warnings generated by the sync.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @return Promise resolved on success.
 | 
			
		||||
     */
 | 
			
		||||
    protected async finishSync(
 | 
			
		||||
        siteId: string,
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        warnings: string[],
 | 
			
		||||
        options?: FinishSyncOptions,
 | 
			
		||||
    ): Promise<AddonModQuizSyncResult> {
 | 
			
		||||
        options = options || {};
 | 
			
		||||
 | 
			
		||||
        // Invalidate the data for the quiz and attempt.
 | 
			
		||||
        await CoreUtils.instance.ignoreErrors(
 | 
			
		||||
            AddonModQuiz.instance.invalidateAllQuizData(quiz.id, courseId, options.attemptId, siteId),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (options.removeAttempt && options.attemptId) {
 | 
			
		||||
            const promises: Promise<unknown>[] = [];
 | 
			
		||||
 | 
			
		||||
            promises.push(AddonModQuizOffline.instance.removeAttemptAndAnswers(options.attemptId, siteId));
 | 
			
		||||
 | 
			
		||||
            if (options.onlineQuestions) {
 | 
			
		||||
                for (const slot in options.onlineQuestions) {
 | 
			
		||||
                    promises.push(CoreQuestionDelegate.instance.deleteOfflineData(
 | 
			
		||||
                        options.onlineQuestions[slot],
 | 
			
		||||
                        AddonModQuizProvider.COMPONENT,
 | 
			
		||||
                        quiz.coursemodule,
 | 
			
		||||
                        siteId,
 | 
			
		||||
                    ));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await Promise.all(promises);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (options.updated) {
 | 
			
		||||
            try {
 | 
			
		||||
                // Data has been sent. Update prefetched data.
 | 
			
		||||
                const module = await CoreCourse.instance.getModuleBasicInfoByInstance(quiz.id, 'quiz', siteId);
 | 
			
		||||
 | 
			
		||||
                await this.prefetchAfterUpdateQuiz(module, quiz, courseId, undefined, siteId);
 | 
			
		||||
            } catch {
 | 
			
		||||
                // Ignore errors.
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.instance.ignoreErrors(this.setSyncTime(quiz.id, siteId));
 | 
			
		||||
 | 
			
		||||
        // Check if online attempt was finished because of the sync.
 | 
			
		||||
        let attemptFinished = false;
 | 
			
		||||
        if (options.onlineAttempt && !AddonModQuiz.instance.isAttemptFinished(options.onlineAttempt.state)) {
 | 
			
		||||
            // Attempt wasn't finished at start. Check if it's finished now.
 | 
			
		||||
            const attempts = await AddonModQuiz.instance.getUserAttempts(quiz.id, { cmId: quiz.coursemodule, siteId });
 | 
			
		||||
 | 
			
		||||
            const attempt = attempts.find(attempt => attempt.id == options?.onlineAttempt?.id);
 | 
			
		||||
 | 
			
		||||
            attemptFinished = attempt ? AddonModQuiz.instance.isAttemptFinished(attempt.state) : false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return { warnings, attemptFinished, updated: !!options.updated || !!options.removeAttempt };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a quiz has data to synchronize.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quizId Quiz ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with boolean: whether it has data to sync.
 | 
			
		||||
     */
 | 
			
		||||
    async hasDataToSync(quizId: number, siteId?: string): Promise<boolean> {
 | 
			
		||||
        try {
 | 
			
		||||
            const attempts = await AddonModQuizOffline.instance.getQuizAttempts(quizId, siteId);
 | 
			
		||||
 | 
			
		||||
            return !!attempts.length;
 | 
			
		||||
        } catch {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Conveniece function to prefetch data after an update.
 | 
			
		||||
     *
 | 
			
		||||
     * @param module Module.
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @param regex If regex matches, don't download the data. Defaults to check files.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async prefetchAfterUpdateQuiz(
 | 
			
		||||
        module: CoreCourseAnyModuleData,
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        regex?: RegExp,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        regex = regex || /^.*files$/;
 | 
			
		||||
 | 
			
		||||
        let shouldDownload = false;
 | 
			
		||||
 | 
			
		||||
        // Get the module updates to check if the data was updated or not.
 | 
			
		||||
        const result = await CoreCourseModulePrefetchDelegate.instance.getModuleUpdates(module, courseId, true, siteId);
 | 
			
		||||
 | 
			
		||||
        if (result?.updates?.length) {
 | 
			
		||||
            // Only prefetch if files haven't changed.
 | 
			
		||||
            shouldDownload = !result.updates.find((entry) => entry.name.match(regex!));
 | 
			
		||||
 | 
			
		||||
            if (shouldDownload) {
 | 
			
		||||
                await AddonModQuizPrefetchHandler.instance.download(module, courseId, undefined, false, false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Prefetch finished or not needed, set the right status.
 | 
			
		||||
        await AddonModQuizPrefetchHandler.instance.setStatusAfterPrefetch(quiz, {
 | 
			
		||||
            cmId: module.id,
 | 
			
		||||
            readingStrategy: shouldDownload ? CoreSitesReadingStrategy.PreferCache : undefined,
 | 
			
		||||
            siteId,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Try to synchronize all the quizzes 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.
 | 
			
		||||
     */
 | 
			
		||||
    syncAllQuizzes(siteId?: string, force?: boolean): Promise<void> {
 | 
			
		||||
        return this.syncOnSites('all quizzes', this.syncAllQuizzesFunc.bind(this, !!force), siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sync all quizzes on a site.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID to sync.
 | 
			
		||||
     * @param force Wether to force sync not depending on last execution.
 | 
			
		||||
     * @param Promise resolved if sync is successful, rejected if sync fails.
 | 
			
		||||
     */
 | 
			
		||||
    protected async syncAllQuizzesFunc(siteId: string, force: boolean): Promise<void> {
 | 
			
		||||
        // Get all offline attempts.
 | 
			
		||||
        const attempts = await AddonModQuizOffline.instance.getAllAttempts(siteId);
 | 
			
		||||
 | 
			
		||||
        const quizIds: Record<number, boolean> = {}; // To prevent duplicates.
 | 
			
		||||
 | 
			
		||||
        // Sync all quizzes that haven't been synced for a while and that aren't attempted right now.
 | 
			
		||||
        await Promise.all(attempts.map(async (attempt) => {
 | 
			
		||||
            if (quizIds[attempt.quizid]) {
 | 
			
		||||
                // Quiz already treated.
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            quizIds[attempt.quizid] = true;
 | 
			
		||||
 | 
			
		||||
            if (CoreSync.instance.isBlocked(AddonModQuizProvider.COMPONENT, attempt.quizid, siteId)) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Quiz not blocked, try to synchronize it.
 | 
			
		||||
            const quiz = await AddonModQuiz.instance.getQuizById(attempt.courseid, attempt.quizid, { siteId });
 | 
			
		||||
 | 
			
		||||
            const data = await (force ? this.syncQuiz(quiz, false, siteId) : this.syncQuizIfNeeded(quiz, false, siteId));
 | 
			
		||||
 | 
			
		||||
            if (data?.warnings?.length) {
 | 
			
		||||
                // Store the warnings to show them when the user opens the quiz.
 | 
			
		||||
                await this.setSyncWarnings(quiz.id, data.warnings, siteId);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (data) {
 | 
			
		||||
                // Sync successful. Send event.
 | 
			
		||||
                CoreEvents.trigger<AddonModQuizAutoSyncData>(AddonModQuizSyncProvider.AUTO_SYNCED, {
 | 
			
		||||
                    quizId: quiz.id,
 | 
			
		||||
                    attemptFinished: data.attemptFinished,
 | 
			
		||||
                    warnings: data.warnings,
 | 
			
		||||
                }, siteId);
 | 
			
		||||
            }
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sync a quiz only if a certain time has passed since the last time.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param askPreflight Whether we should ask for preflight data if needed.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when the quiz is synced or if it doesn't need to be synced.
 | 
			
		||||
     */
 | 
			
		||||
    async syncQuizIfNeeded(
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        askPreflight?: boolean,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<AddonModQuizSyncResult | undefined> {
 | 
			
		||||
        const needed = await this.isSyncNeeded(quiz.id, siteId);
 | 
			
		||||
 | 
			
		||||
        if (needed) {
 | 
			
		||||
            return this.syncQuiz(quiz, askPreflight, siteId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Try to synchronize a quiz.
 | 
			
		||||
     * The promise returned will be resolved with an array with warnings if the synchronization is successful.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param askPreflight Whether we should ask for preflight data if needed.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved in success.
 | 
			
		||||
     */
 | 
			
		||||
    syncQuiz(quiz: AddonModQuizQuizWSData, askPreflight?: boolean, siteId?: string): Promise<AddonModQuizSyncResult> {
 | 
			
		||||
        siteId = siteId || CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        if (this.isSyncing(quiz.id, siteId)) {
 | 
			
		||||
            // There's already a sync ongoing for this quiz, return the promise.
 | 
			
		||||
            return this.getOngoingSync(quiz.id, siteId)!;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Verify that quiz isn't blocked.
 | 
			
		||||
        if (CoreSync.instance.isBlocked(AddonModQuizProvider.COMPONENT, quiz.id, siteId)) {
 | 
			
		||||
            this.logger.debug('Cannot sync quiz ' + quiz.id + ' because it is blocked.');
 | 
			
		||||
            this.componentTranslate = this.componentTranslate || CoreCourse.instance.translateModuleName('quiz');
 | 
			
		||||
 | 
			
		||||
            throw new CoreError(Translate.instance.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.addOngoingSync(quiz.id, this.performSyncQuiz(quiz, askPreflight, siteId), siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Perform the quiz sync.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param askPreflight Whether we should ask for preflight data if needed.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved in success.
 | 
			
		||||
     */
 | 
			
		||||
    async performSyncQuiz(quiz: AddonModQuizQuizWSData, askPreflight?: boolean, siteId?: string): Promise<AddonModQuizSyncResult> {
 | 
			
		||||
        siteId = siteId || CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        const warnings: string[] = [];
 | 
			
		||||
        const courseId = quiz.course;
 | 
			
		||||
        const modOptions = {
 | 
			
		||||
            cmId: quiz.coursemodule,
 | 
			
		||||
            readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
            siteId,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.logger.debug('Try to sync quiz ' + quiz.id + ' in site ' + siteId);
 | 
			
		||||
 | 
			
		||||
        // Sync offline logs.
 | 
			
		||||
        await CoreUtils.instance.ignoreErrors(
 | 
			
		||||
            CoreCourseLogHelper.instance.syncActivity(AddonModQuizProvider.COMPONENT, quiz.id, siteId),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Get all the offline attempts for the quiz. It should always be 0 or 1 attempt
 | 
			
		||||
        const offlineAttempts = await AddonModQuizOffline.instance.getQuizAttempts(quiz.id, siteId);
 | 
			
		||||
 | 
			
		||||
        if (!offlineAttempts.length) {
 | 
			
		||||
            // Nothing to sync, finish.
 | 
			
		||||
            return this.finishSync(siteId, quiz, courseId, warnings);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!CoreApp.instance.isOnline()) {
 | 
			
		||||
            // Cannot sync in offline.
 | 
			
		||||
            throw new CoreError(Translate.instance.instant('core.cannotconnect'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const offlineAttempt = offlineAttempts.pop()!;
 | 
			
		||||
 | 
			
		||||
        // Now get the list of online attempts to make sure this attempt exists and isn't finished.
 | 
			
		||||
        const onlineAttempts = await AddonModQuiz.instance.getUserAttempts(quiz.id, modOptions);
 | 
			
		||||
 | 
			
		||||
        const lastAttemptId = onlineAttempts.length ? onlineAttempts[onlineAttempts.length - 1].id : undefined;
 | 
			
		||||
        const onlineAttempt = onlineAttempts.find((attempt) => attempt.id == offlineAttempt.id);
 | 
			
		||||
 | 
			
		||||
        if (!onlineAttempt || AddonModQuiz.instance.isAttemptFinished(onlineAttempt.state)) {
 | 
			
		||||
            // Attempt not found or it's finished in online. Discard it.
 | 
			
		||||
            warnings.push(Translate.instance.instant('addon.mod_quiz.warningattemptfinished'));
 | 
			
		||||
 | 
			
		||||
            return this.finishSync(siteId, quiz, courseId, warnings, {
 | 
			
		||||
                attemptId: offlineAttempt.id,
 | 
			
		||||
                offlineAttempt,
 | 
			
		||||
                onlineAttempt,
 | 
			
		||||
                removeAttempt: true,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Get the data stored in offline.
 | 
			
		||||
        const answersList = await AddonModQuizOffline.instance.getAttemptAnswers(offlineAttempt.id, siteId);
 | 
			
		||||
 | 
			
		||||
        if (!answersList.length) {
 | 
			
		||||
            // No answers stored, finish.
 | 
			
		||||
            return this.finishSync(siteId, quiz, courseId, warnings, {
 | 
			
		||||
                attemptId: lastAttemptId,
 | 
			
		||||
                offlineAttempt,
 | 
			
		||||
                onlineAttempt,
 | 
			
		||||
                removeAttempt: true,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const offlineAnswers = CoreQuestion.instance.convertAnswersArrayToObject(answersList);
 | 
			
		||||
        const offlineQuestions = AddonModQuizOffline.instance.classifyAnswersInQuestions(offlineAnswers);
 | 
			
		||||
 | 
			
		||||
        // We're going to need preflightData, get it.
 | 
			
		||||
        const info = await AddonModQuiz.instance.getQuizAccessInformation(quiz.id, modOptions);
 | 
			
		||||
 | 
			
		||||
        const preflightData = await AddonModQuizPrefetchHandler.instance.getPreflightData(
 | 
			
		||||
            quiz,
 | 
			
		||||
            info,
 | 
			
		||||
            onlineAttempt,
 | 
			
		||||
            askPreflight,
 | 
			
		||||
            'core.settings.synchronization',
 | 
			
		||||
            siteId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Now get the online questions data.
 | 
			
		||||
        const onlineQuestions = await AddonModQuiz.instance.getAllQuestionsData(quiz, onlineAttempt, preflightData, {
 | 
			
		||||
            pages: AddonModQuiz.instance.getPagesFromLayoutAndQuestions(onlineAttempt.layout || '', offlineQuestions),
 | 
			
		||||
            readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
            siteId,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Validate questions, discarding the offline answers that can't be synchronized.
 | 
			
		||||
        const discardedData = await this.validateQuestions(onlineAttempt.id, onlineQuestions, offlineQuestions, siteId);
 | 
			
		||||
 | 
			
		||||
        // Let questions prepare the data to send.
 | 
			
		||||
        await Promise.all(Object.keys(offlineQuestions).map(async (slotString) => {
 | 
			
		||||
            const slot = Number(slotString);
 | 
			
		||||
            const onlineQuestion = onlineQuestions[slot];
 | 
			
		||||
 | 
			
		||||
            await CoreQuestionDelegate.instance.prepareSyncData(
 | 
			
		||||
                onlineQuestion,
 | 
			
		||||
                offlineQuestions[slot].answers,
 | 
			
		||||
                AddonModQuizProvider.COMPONENT,
 | 
			
		||||
                quiz.coursemodule,
 | 
			
		||||
                siteId,
 | 
			
		||||
            );
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        // Get the answers to send.
 | 
			
		||||
        const answers = AddonModQuizOffline.instance.extractAnswersFromQuestions(offlineQuestions);
 | 
			
		||||
        const finish = !!offlineAttempt.finished && !discardedData;
 | 
			
		||||
 | 
			
		||||
        if (discardedData) {
 | 
			
		||||
            if (offlineAttempt.finished) {
 | 
			
		||||
                warnings.push(Translate.instance.instant('addon.mod_quiz.warningdatadiscardedfromfinished'));
 | 
			
		||||
            } else {
 | 
			
		||||
                warnings.push(Translate.instance.instant('addon.mod_quiz.warningdatadiscarded'));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Send the answers.
 | 
			
		||||
        await AddonModQuiz.instance.processAttempt(quiz, onlineAttempt, answers, preflightData, finish, false, false, siteId);
 | 
			
		||||
 | 
			
		||||
        if (!finish) {
 | 
			
		||||
            // Answers sent, now set the current page.
 | 
			
		||||
            // Don't pass the quiz instance because we don't want to trigger a Firebase event in this case.
 | 
			
		||||
            await CoreUtils.instance.ignoreErrors(AddonModQuiz.instance.logViewAttempt(
 | 
			
		||||
                onlineAttempt.id,
 | 
			
		||||
                offlineAttempt.currentpage,
 | 
			
		||||
                preflightData,
 | 
			
		||||
                false,
 | 
			
		||||
                undefined,
 | 
			
		||||
                siteId,
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Data sent. Finish the sync.
 | 
			
		||||
        return this.finishSync(siteId, quiz, courseId, warnings, {
 | 
			
		||||
            attemptId: lastAttemptId,
 | 
			
		||||
            offlineAttempt,
 | 
			
		||||
            onlineAttempt,
 | 
			
		||||
            removeAttempt: true,
 | 
			
		||||
            updated: true,
 | 
			
		||||
            onlineQuestions,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Validate questions, discarding the offline answers that can't be synchronized.
 | 
			
		||||
     *
 | 
			
		||||
     * @param attemptId Attempt ID.
 | 
			
		||||
     * @param onlineQuestions Online questions
 | 
			
		||||
     * @param offlineQuestions Offline questions.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with boolean: true if some offline data was discarded, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    async validateQuestions(
 | 
			
		||||
        attemptId: number,
 | 
			
		||||
        onlineQuestions: Record<number, CoreQuestionQuestionParsed>,
 | 
			
		||||
        offlineQuestions: AddonModQuizQuestionsWithAnswers,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<boolean> {
 | 
			
		||||
        let discardedData = false;
 | 
			
		||||
 | 
			
		||||
        await Promise.all(Object.keys(offlineQuestions).map(async (slotString) => {
 | 
			
		||||
            const slot = Number(slotString);
 | 
			
		||||
            const offlineQuestion = offlineQuestions[slot];
 | 
			
		||||
            const onlineQuestion = onlineQuestions[slot];
 | 
			
		||||
            const offlineSequenceCheck = <string> offlineQuestion.answers[':sequencecheck'];
 | 
			
		||||
 | 
			
		||||
            if (onlineQuestion) {
 | 
			
		||||
                // We found the online data for the question, validate that the sequence check is ok.
 | 
			
		||||
                if (!CoreQuestionDelegate.instance.validateSequenceCheck(onlineQuestion, offlineSequenceCheck)) {
 | 
			
		||||
                    // Sequence check is not valid, remove the offline data.
 | 
			
		||||
                    await AddonModQuizOffline.instance.removeQuestionAndAnswers(attemptId, slot, siteId);
 | 
			
		||||
 | 
			
		||||
                    discardedData = true;
 | 
			
		||||
                    delete offlineQuestions[slot];
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Sequence check is valid. Use the online one to prevent synchronization errors.
 | 
			
		||||
                    offlineQuestion.answers[':sequencecheck'] = String(onlineQuestion.sequencecheck);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                // Online question not found, it can happen for 2 reasons:
 | 
			
		||||
                // 1- It's a sequential quiz and the question is in a page already passed.
 | 
			
		||||
                // 2- Quiz layout has changed (shouldn't happen since it's blocked if there are attempts).
 | 
			
		||||
                await AddonModQuizOffline.instance.removeQuestionAndAnswers(attemptId, slot, siteId);
 | 
			
		||||
 | 
			
		||||
                discardedData = true;
 | 
			
		||||
                delete offlineQuestions[slot];
 | 
			
		||||
            }
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        return discardedData;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class AddonModQuizSync extends makeSingleton(AddonModQuizSyncProvider) {}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by a quiz sync.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModQuizSyncResult = {
 | 
			
		||||
    warnings: string[]; // List of warnings.
 | 
			
		||||
    attemptFinished: boolean; // Whether an attempt was finished in the site due to the sync.
 | 
			
		||||
    updated: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Options to pass to finish sync.
 | 
			
		||||
 */
 | 
			
		||||
type FinishSyncOptions = {
 | 
			
		||||
    attemptId?: number; // Last attempt ID.
 | 
			
		||||
    offlineAttempt?: AddonModQuizAttemptDBRecord; // Offline attempt synchronized, if any.
 | 
			
		||||
    onlineAttempt?: AddonModQuizAttemptWSData; // Online data for the offline attempt.
 | 
			
		||||
    removeAttempt?: boolean; // Whether the offline data should be removed.
 | 
			
		||||
    updated?: boolean; // Whether the offline data should be removed.
 | 
			
		||||
    onlineQuestions?: Record<number, CoreQuestionQuestionParsed>; // Online questions indexed by slot.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data passed to AUTO_SYNCED event.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModQuizAutoSyncData = CoreEventSiteData & {
 | 
			
		||||
    quizId: number;
 | 
			
		||||
    attemptFinished: boolean;
 | 
			
		||||
    warnings: string[];
 | 
			
		||||
};
 | 
			
		||||
@ -50,17 +50,17 @@ export class CoreCourseSyncProvider extends CoreSyncBaseProvider<CoreCourseSyncR
 | 
			
		||||
     * @return Promise resolved if sync is successful, rejected if sync fails.
 | 
			
		||||
     */
 | 
			
		||||
    syncAllCourses(siteId?: string, force?: boolean): Promise<void> {
 | 
			
		||||
        return this.syncOnSites('courses', this.syncAllCoursesFunc.bind(this, siteId, force), siteId);
 | 
			
		||||
        return this.syncOnSites('courses', this.syncAllCoursesFunc.bind(this, !!force), siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sync all courses on a site.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID to sync.
 | 
			
		||||
     * @param force Wether the execution is forced (manual sync).
 | 
			
		||||
     * @param siteId Site ID to sync.
 | 
			
		||||
     * @return Promise resolved if sync is successful, rejected if sync fails.
 | 
			
		||||
     */
 | 
			
		||||
    protected async syncAllCoursesFunc(siteId: string, force: boolean): Promise<void> {
 | 
			
		||||
    protected async syncAllCoursesFunc(force: boolean, siteId: string): Promise<void> {
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            CoreCourseLogHelper.instance.syncSite(siteId),
 | 
			
		||||
            this.syncCoursesCompletion(siteId, force),
 | 
			
		||||
@ -149,7 +149,7 @@ export class CoreCourseSyncProvider extends CoreSyncBaseProvider<CoreCourseSyncR
 | 
			
		||||
 | 
			
		||||
        if (!completions || !completions.length) {
 | 
			
		||||
            // Nothing to sync, set sync time.
 | 
			
		||||
            await this.setSyncTime(String(courseId), siteId);
 | 
			
		||||
            await this.setSyncTime(courseId, siteId);
 | 
			
		||||
 | 
			
		||||
            // All done, return the data.
 | 
			
		||||
            return result;
 | 
			
		||||
@ -233,7 +233,7 @@ export class CoreCourseSyncProvider extends CoreSyncBaseProvider<CoreCourseSyncR
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Sync finished, set sync time.
 | 
			
		||||
        await this.setSyncTime(String(courseId), siteId);
 | 
			
		||||
        await this.setSyncTime(courseId, siteId);
 | 
			
		||||
 | 
			
		||||
        // All done, return the data.
 | 
			
		||||
        return result;
 | 
			
		||||
 | 
			
		||||
@ -443,7 +443,7 @@ export class CoreQuestionDelegateService extends CoreDelegate<CoreQuestionHandle
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return If async, promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async prepareSyncData?(
 | 
			
		||||
    async prepareSyncData(
 | 
			
		||||
        question: CoreQuestionQuestionParsed,
 | 
			
		||||
        answers: CoreQuestionsAnswers,
 | 
			
		||||
        component: string,
 | 
			
		||||
 | 
			
		||||
@ -100,7 +100,7 @@ export class CoreSyncProvider {
 | 
			
		||||
    async getSyncRecord(component: string, id: string | number, siteId?: string): Promise<CoreSyncRecord> {
 | 
			
		||||
        const db = await CoreSites.instance.getSiteDb(siteId);
 | 
			
		||||
 | 
			
		||||
        return await db.getRecord(SYNC_TABLE_NAME, { component: component, id: id });
 | 
			
		||||
        return await db.getRecord(SYNC_TABLE_NAME, { component: component, id: String(id) });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -121,7 +121,7 @@ export class CoreSyncProvider {
 | 
			
		||||
        const db = await CoreSites.instance.getSiteDb(siteId);
 | 
			
		||||
 | 
			
		||||
        data.component = component;
 | 
			
		||||
        data.id = id + '';
 | 
			
		||||
        data.id = String(id);
 | 
			
		||||
 | 
			
		||||
        await db.insertRecord(SYNC_TABLE_NAME, data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user