forked from CIT/Vmeda.Online
		
	MOBILE-3651 quiz: Implement base services and access rule delegate
This commit is contained in:
		
							parent
							
								
									dd060d8168
								
							
						
					
					
						commit
						596ef954ba
					
				@ -60,13 +60,13 @@ export class AddonModLessonPrefetchHandlerService extends CoreCourseActivityPref
 | 
			
		||||
 | 
			
		||||
        await modal.present();
 | 
			
		||||
 | 
			
		||||
        const password = <string | undefined> await modal.onWillDismiss();
 | 
			
		||||
        const result = await modal.onWillDismiss();
 | 
			
		||||
 | 
			
		||||
        if (typeof password != 'string') {
 | 
			
		||||
        if (typeof result.data != 'string') {
 | 
			
		||||
            throw new CoreCanceledError();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return password;
 | 
			
		||||
        return result.data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										83
									
								
								src/addons/mod/quiz/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/addons/mod/quiz/lang.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,83 @@
 | 
			
		||||
{
 | 
			
		||||
    "answercolon": "Answer:",
 | 
			
		||||
    "attemptfirst": "First attempt",
 | 
			
		||||
    "attemptlast": "Last attempt",
 | 
			
		||||
    "attemptnumber": "Attempt",
 | 
			
		||||
    "attemptquiznow": "Attempt quiz now",
 | 
			
		||||
    "attemptstate": "State",
 | 
			
		||||
    "canattemptbutnotsubmit": "You can attempt this quiz in the app, but you will need to submit the attempt in browser for the following reasons:",
 | 
			
		||||
    "cannotsubmitquizdueto": "This quiz attempt cannot be submitted for the following reasons:",
 | 
			
		||||
    "clearchoice": "Clear my choice",
 | 
			
		||||
    "comment": "Comment",
 | 
			
		||||
    "completedon": "Completed on",
 | 
			
		||||
    "confirmclose": "Once you submit, you will no longer be able to change your answers for this attempt.",
 | 
			
		||||
    "confirmcontinueoffline": "This attempt has not been synchronised since {{$a}}. If you have continued this attempt in another device since then, you may lose data.",
 | 
			
		||||
    "confirmleavequizonerror": "An error occurred while saving the answers. Are you sure you want to leave the quiz?",
 | 
			
		||||
    "confirmstart": "Your attempt will have a time limit of {{$a}}. When you start, the timer will begin to count down and cannot be paused. You must finish your attempt before it expires. Are you sure you wish to start now?",
 | 
			
		||||
    "confirmstartheader": "Time limit",
 | 
			
		||||
    "connectionerror": "Network connection lost. (Autosave failed).\n\nMake a note of any responses entered on this page in the last few minutes, then try to re-connect.\n\nOnce connection has been re-established, your responses should be saved and this message will disappear.",
 | 
			
		||||
    "continueattemptquiz": "Continue the last attempt",
 | 
			
		||||
    "continuepreview": "Continue the last preview",
 | 
			
		||||
    "errorbehaviournotsupported": "This quiz can't be attempted in the app because the question behaviour is not supported by the app:",
 | 
			
		||||
    "errordownloading": "Error downloading required data.",
 | 
			
		||||
    "errorgetattempt": "Error getting attempt data.",
 | 
			
		||||
    "errorgetquestions": "Error getting questions.",
 | 
			
		||||
    "errorgetquiz": "Error getting quiz data.",
 | 
			
		||||
    "errorparsequestions": "An error occurred while reading the questions. Please attempt this quiz in a web browser.",
 | 
			
		||||
    "errorquestionsnotsupported": "This quiz can't be attempted in the app because it only contains questions not supported by the app:",
 | 
			
		||||
    "errorrulesnotsupported": "This quiz can't be attempted in the app because it has access rules not supported by the app:",
 | 
			
		||||
    "errorsaveattempt": "An error occurred while saving the attempt data.",
 | 
			
		||||
    "feedback": "Feedback",
 | 
			
		||||
    "finishattemptdots": "Finish attempt...",
 | 
			
		||||
    "finishnotsynced": "Finished but not synchronised",
 | 
			
		||||
    "grade": "Grade",
 | 
			
		||||
    "gradeaverage": "Average grade",
 | 
			
		||||
    "gradehighest": "Highest grade",
 | 
			
		||||
    "grademethod": "Grading method",
 | 
			
		||||
    "gradesofar": "{{$a.method}}: {{$a.mygrade}} / {{$a.quizgrade}}.",
 | 
			
		||||
    "marks": "Marks",
 | 
			
		||||
    "modulenameplural": "Quizzes",
 | 
			
		||||
    "mustbesubmittedby": "This attempt must be submitted by {{$a}}.",
 | 
			
		||||
    "noquestions": "No questions have been added yet",
 | 
			
		||||
    "noreviewattempt": "You are not allowed to review this attempt.",
 | 
			
		||||
    "notyetgraded": "Not yet graded",
 | 
			
		||||
    "opentoc": "Open navigation popover",
 | 
			
		||||
    "outof": "{{$a.grade}} out of {{$a.maxgrade}}",
 | 
			
		||||
    "outofpercent": "{{$a.grade}} out of {{$a.maxgrade}} ({{$a.percent}}%)",
 | 
			
		||||
    "outofshort": "{{$a.grade}}/{{$a.maxgrade}}",
 | 
			
		||||
    "overallfeedback": "Overall feedback",
 | 
			
		||||
    "overdue": "Overdue",
 | 
			
		||||
    "overduemustbesubmittedby": "This attempt is now overdue. It should already have been submitted. If you would like this quiz to be graded, you must submit it by {{$a}}. If you do not submit it by then, no marks from this attempt will be counted.",
 | 
			
		||||
    "preview": "Preview",
 | 
			
		||||
    "previewquiznow": "Preview quiz now",
 | 
			
		||||
    "question": "Question",
 | 
			
		||||
    "quiznavigation": "Quiz navigation",
 | 
			
		||||
    "quizpassword": "Quiz password",
 | 
			
		||||
    "reattemptquiz": "Re-attempt quiz",
 | 
			
		||||
    "requirepasswordmessage": "To attempt this quiz you need to know the quiz password",
 | 
			
		||||
    "returnattempt": "Return to attempt",
 | 
			
		||||
    "review": "Review",
 | 
			
		||||
    "reviewofattempt": "Review of attempt {{$a}}",
 | 
			
		||||
    "reviewofpreview": "Review of preview",
 | 
			
		||||
    "showall": "Show all questions on one page",
 | 
			
		||||
    "showeachpage": "Show one page at a time",
 | 
			
		||||
    "startattempt": "Start attempt",
 | 
			
		||||
    "startedon": "Started on",
 | 
			
		||||
    "stateabandoned": "Never submitted",
 | 
			
		||||
    "statefinished": "Finished",
 | 
			
		||||
    "statefinisheddetails": "Submitted {{$a}}",
 | 
			
		||||
    "stateinprogress": "In progress",
 | 
			
		||||
    "stateoverdue": "Overdue",
 | 
			
		||||
    "stateoverduedetails": "Must be submitted by {{$a}}",
 | 
			
		||||
    "status": "Status",
 | 
			
		||||
    "submitallandfinish": "Submit all and finish",
 | 
			
		||||
    "summaryofattempt": "Summary of attempt",
 | 
			
		||||
    "summaryofattempts": "Summary of your previous attempts",
 | 
			
		||||
    "timeleft": "Time left",
 | 
			
		||||
    "timetaken": "Time taken",
 | 
			
		||||
    "warningattemptfinished": "Offline attempt discarded as it was finished on the site or not found.",
 | 
			
		||||
    "warningdatadiscarded": "Some offline answers were discarded because the questions were modified online.",
 | 
			
		||||
    "warningdatadiscardedfromfinished": "Attempt unfinished because some offline answers were discarded. Please review your answers then resubmit the attempt.",
 | 
			
		||||
    "warningquestionsnotsupported": "This quiz contains questions not supported by the app:",
 | 
			
		||||
    "yourfinalgradeis": "Your final grade for this quiz is {{$a}}."
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										326
									
								
								src/addons/mod/quiz/services/access-rules-delegate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										326
									
								
								src/addons/mod/quiz/services/access-rules-delegate.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,326 @@
 | 
			
		||||
// (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, Type } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonModQuizAttemptWSData, AddonModQuizQuizWSData } from './quiz';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Interface that all access rules handlers must implement.
 | 
			
		||||
 */
 | 
			
		||||
export interface AddonModQuizAccessRuleHandler extends CoreDelegateHandler {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Name of the rule the handler supports. E.g. 'password'.
 | 
			
		||||
     */
 | 
			
		||||
    ruleName: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether the rule requires a preflight check when prefetch/start/continue an attempt.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz The quiz the rule belongs to.
 | 
			
		||||
     * @param attempt The attempt started/continued. If not supplied, user is starting a new attempt.
 | 
			
		||||
     * @param prefetch Whether the user is prefetching the quiz.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Whether the rule requires a preflight check.
 | 
			
		||||
     */
 | 
			
		||||
    isPreflightCheckRequired(
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        attempt?: AddonModQuizAttemptWSData,
 | 
			
		||||
        prefetch?: boolean,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): boolean | Promise<boolean>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add preflight data that doesn't require user interaction. The data should be added to the preflightData param.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz The quiz the rule belongs to.
 | 
			
		||||
     * @param preflightData Object where to add the preflight data.
 | 
			
		||||
     * @param attempt The attempt started/continued. If not supplied, user is starting a new attempt.
 | 
			
		||||
     * @param prefetch Whether the user is prefetching the quiz.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done if async, void if it's synchronous.
 | 
			
		||||
     */
 | 
			
		||||
    getFixedPreflightData?(
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        preflightData: Record<string, string>,
 | 
			
		||||
        attempt?: AddonModQuizAttemptWSData,
 | 
			
		||||
        prefetch?: boolean,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): void | Promise<void>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the Component to use to display the access rule preflight.
 | 
			
		||||
     * Implement this if your access rule requires a preflight check with user interaction.
 | 
			
		||||
     * It's recommended to return the class of the component, but you can also return an instance of the component.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The component (or promise resolved with component) to use, undefined if not found.
 | 
			
		||||
     */
 | 
			
		||||
    getPreflightComponent?(): Type<unknown> | Promise<Type<unknown>>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Function called when the preflight check has passed. This is a chance to record that fact in some way.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz The quiz the rule belongs to.
 | 
			
		||||
     * @param attempt The attempt started/continued.
 | 
			
		||||
     * @param preflightData Preflight data gathered.
 | 
			
		||||
     * @param prefetch Whether the user is prefetching the quiz.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done if async, void if it's synchronous.
 | 
			
		||||
     */
 | 
			
		||||
    notifyPreflightCheckPassed?(
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        attempt: AddonModQuizAttemptWSData | undefined,
 | 
			
		||||
        preflightData: Record<string, string>,
 | 
			
		||||
        prefetch?: boolean,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): void | Promise<void>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Function called when the preflight check fails. This is a chance to record that fact in some way.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz The quiz the rule belongs to.
 | 
			
		||||
     * @param attempt The attempt started/continued.
 | 
			
		||||
     * @param preflightData Preflight data gathered.
 | 
			
		||||
     * @param prefetch Whether the user is prefetching the quiz.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done if async, void if it's synchronous.
 | 
			
		||||
     */
 | 
			
		||||
    notifyPreflightCheckFailed?(
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        attempt: AddonModQuizAttemptWSData | undefined,
 | 
			
		||||
        preflightData: Record<string, string>,
 | 
			
		||||
        prefetch?: boolean,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): void | Promise<void>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether or not the time left of an attempt should be displayed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param attempt The attempt.
 | 
			
		||||
     * @param endTime The attempt end time (in seconds).
 | 
			
		||||
     * @param timeNow The current time in seconds.
 | 
			
		||||
     * @return Whether it should be displayed.
 | 
			
		||||
     */
 | 
			
		||||
    shouldShowTimeLeft?(attempt: AddonModQuizAttemptWSData, endTime: number, timeNow: number): boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Delegate to register access rules for quiz module.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModQuizAccessRuleDelegateService extends CoreDelegate<AddonModQuizAccessRuleHandler> {
 | 
			
		||||
 | 
			
		||||
    protected handlerNameProperty = 'ruleName';
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super('AddonModQuizAccessRulesDelegate', true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the handler for a certain rule.
 | 
			
		||||
     *
 | 
			
		||||
     * @param ruleName Name of the access rule.
 | 
			
		||||
     * @return Handler. Undefined if no handler found for the rule.
 | 
			
		||||
     */
 | 
			
		||||
    getAccessRuleHandler(ruleName: string): AddonModQuizAccessRuleHandler {
 | 
			
		||||
        return this.getHandler(ruleName, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a list of rules, get some fixed preflight data (data that doesn't require user interaction).
 | 
			
		||||
     *
 | 
			
		||||
     * @param rules List of active rules names.
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param preflightData Object where to store the preflight data.
 | 
			
		||||
     * @param attempt The attempt started/continued. If not supplied, user is starting a new attempt.
 | 
			
		||||
     * @param prefetch Whether the user is prefetching the quiz.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when all the data has been gathered.
 | 
			
		||||
     */
 | 
			
		||||
    async getFixedPreflightData(
 | 
			
		||||
        rules: string[],
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        preflightData: Record<string, string>,
 | 
			
		||||
        attempt?: AddonModQuizAttemptWSData,
 | 
			
		||||
        prefetch?: boolean,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        rules = rules || [];
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.instance.ignoreErrors(CoreUtils.instance.allPromises(rules.map(async (rule) => {
 | 
			
		||||
            await this.executeFunctionOnEnabled(rule, 'getFixedPreflightData', [quiz, preflightData, attempt, prefetch, siteId]);
 | 
			
		||||
        })));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the Component to use to display the access rule preflight.
 | 
			
		||||
     *
 | 
			
		||||
     * @param rule Rule.
 | 
			
		||||
     * @return Promise resolved with the component to use, undefined if not found.
 | 
			
		||||
     */
 | 
			
		||||
    getPreflightComponent(rule: string): Promise<Type<unknown> | undefined> {
 | 
			
		||||
        return Promise.resolve(this.executeFunctionOnEnabled(rule, 'getPreflightComponent', []));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if an access rule is supported.
 | 
			
		||||
     *
 | 
			
		||||
     * @param ruleName Name of the rule.
 | 
			
		||||
     * @return Whether it's supported.
 | 
			
		||||
     */
 | 
			
		||||
    isAccessRuleSupported(ruleName: string): boolean {
 | 
			
		||||
        return this.hasHandler(ruleName, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a list of rules, check if preflight check is required.
 | 
			
		||||
     *
 | 
			
		||||
     * @param rules List of active rules names.
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param attempt The attempt started/continued. If not supplied, user is starting a new attempt.
 | 
			
		||||
     * @param prefetch Whether the user is prefetching the quiz.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with boolean: whether it's required.
 | 
			
		||||
     */
 | 
			
		||||
    async isPreflightCheckRequired(
 | 
			
		||||
        rules: string[],
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        attempt?: AddonModQuizAttemptWSData,
 | 
			
		||||
        prefetch?: boolean,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<boolean> {
 | 
			
		||||
        rules = rules || [];
 | 
			
		||||
        let isRequired = false;
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.instance.ignoreErrors(CoreUtils.instance.allPromises(rules.map(async (rule) => {
 | 
			
		||||
            const ruleRequired = await this.isPreflightCheckRequiredForRule(rule, quiz, attempt, prefetch, siteId);
 | 
			
		||||
 | 
			
		||||
            isRequired = isRequired || ruleRequired;
 | 
			
		||||
        })));
 | 
			
		||||
 | 
			
		||||
        return isRequired;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if preflight check is required for a certain rule.
 | 
			
		||||
     *
 | 
			
		||||
     * @param rule Rule name.
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param attempt The attempt started/continued. If not supplied, user is starting a new attempt.
 | 
			
		||||
     * @param prefetch Whether the user is prefetching the quiz.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with boolean: whether it's required.
 | 
			
		||||
     */
 | 
			
		||||
    async isPreflightCheckRequiredForRule(
 | 
			
		||||
        rule: string,
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        attempt?: AddonModQuizAttemptWSData,
 | 
			
		||||
        prefetch?: boolean,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<boolean> {
 | 
			
		||||
        const isRequired = await this.executeFunctionOnEnabled(rule, 'isPreflightCheckRequired', [quiz, attempt, prefetch, siteId]);
 | 
			
		||||
 | 
			
		||||
        return !!isRequired;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Notify all rules that the preflight check has passed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param rules List of active rules names.
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param attempt Attempt.
 | 
			
		||||
     * @param preflightData Preflight data gathered.
 | 
			
		||||
     * @param prefetch Whether the user is prefetching the quiz.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async notifyPreflightCheckPassed(
 | 
			
		||||
        rules: string[],
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        attempt: AddonModQuizAttemptWSData | undefined,
 | 
			
		||||
        preflightData: Record<string, string>,
 | 
			
		||||
        prefetch?: boolean,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        rules = rules || [];
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.instance.ignoreErrors(CoreUtils.instance.allPromises(rules.map(async (rule) => {
 | 
			
		||||
            await this.executeFunctionOnEnabled(
 | 
			
		||||
                rule,
 | 
			
		||||
                'notifyPreflightCheckPassed',
 | 
			
		||||
                [quiz, attempt, preflightData, prefetch, siteId],
 | 
			
		||||
            );
 | 
			
		||||
        })));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Notify all rules that the preflight check has failed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param rules List of active rules names.
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param attempt Attempt.
 | 
			
		||||
     * @param preflightData Preflight data gathered.
 | 
			
		||||
     * @param prefetch Whether the user is prefetching the quiz.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async notifyPreflightCheckFailed(
 | 
			
		||||
        rules: string[],
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        attempt: AddonModQuizAttemptWSData | undefined,
 | 
			
		||||
        preflightData: Record<string, string>,
 | 
			
		||||
        prefetch?: boolean,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        rules = rules || [];
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.instance.ignoreErrors(CoreUtils.instance.allPromises(rules.map(async (rule) => {
 | 
			
		||||
            await this.executeFunctionOnEnabled(
 | 
			
		||||
                rule,
 | 
			
		||||
                'notifyPreflightCheckFailed',
 | 
			
		||||
                [quiz, attempt, preflightData, prefetch, siteId],
 | 
			
		||||
            );
 | 
			
		||||
        })));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether or not the time left of an attempt should be displayed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param rules List of active rules names.
 | 
			
		||||
     * @param attempt The attempt.
 | 
			
		||||
     * @param endTime The attempt end time (in seconds).
 | 
			
		||||
     * @param timeNow The current time in seconds.
 | 
			
		||||
     * @return Whether it should be displayed.
 | 
			
		||||
     */
 | 
			
		||||
    shouldShowTimeLeft(rules: string[], attempt: AddonModQuizAttemptWSData, endTime: number, timeNow: number): boolean {
 | 
			
		||||
        rules = rules || [];
 | 
			
		||||
 | 
			
		||||
        for (const i in rules) {
 | 
			
		||||
            const rule = rules[i];
 | 
			
		||||
 | 
			
		||||
            if (this.executeFunctionOnEnabled(rule, 'shouldShowTimeLeft', [attempt, endTime, timeNow])) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class AddonModQuizAccessRuleDelegate extends makeSingleton(AddonModQuizAccessRuleDelegateService) {}
 | 
			
		||||
							
								
								
									
										83
									
								
								src/addons/mod/quiz/services/database/quiz.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/addons/mod/quiz/services/database/quiz.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,83 @@
 | 
			
		||||
// (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 { CoreSiteSchema } from '@services/sites';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Database variables for AddonModQuizOfflineProvider.
 | 
			
		||||
 */
 | 
			
		||||
export const ATTEMPTS_TABLE_NAME = 'addon_mod_quiz_attempts';
 | 
			
		||||
export const SITE_SCHEMA: CoreSiteSchema = {
 | 
			
		||||
    name: 'AddonModQuizOfflineProvider',
 | 
			
		||||
    version: 1,
 | 
			
		||||
    tables: [
 | 
			
		||||
        {
 | 
			
		||||
            name: ATTEMPTS_TABLE_NAME,
 | 
			
		||||
            columns: [
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'id', // Attempt ID.
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                    primaryKey: true,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'attempt', // Attempt number.
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'courseid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'userid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'quizid',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'currentpage',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timecreated',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timemodified',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'finished',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Quiz attempt.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModQuizAttemptDBRecord = {
 | 
			
		||||
    id: number;
 | 
			
		||||
    attempt: number;
 | 
			
		||||
    courseid: number;
 | 
			
		||||
    userid: number;
 | 
			
		||||
    quizid: number;
 | 
			
		||||
    currentpage?: number;
 | 
			
		||||
    timecreated: number;
 | 
			
		||||
    timemodified: number;
 | 
			
		||||
    finished: number;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										436
									
								
								src/addons/mod/quiz/services/quiz-helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										436
									
								
								src/addons/mod/quiz/services/quiz-helper.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,436 @@
 | 
			
		||||
// (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 { CoreCourseHelper } from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { makeSingleton, ModalController, Translate } from '@singletons';
 | 
			
		||||
import { AddonModQuizPreflightModalComponent } from '../components/preflight-modal/preflight-modal';
 | 
			
		||||
import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModQuiz,
 | 
			
		||||
    AddonModQuizAttemptWSData,
 | 
			
		||||
    AddonModQuizCombinedReviewOptions,
 | 
			
		||||
    AddonModQuizGetQuizAccessInformationWSResponse,
 | 
			
		||||
    AddonModQuizProvider,
 | 
			
		||||
    AddonModQuizQuizWSData,
 | 
			
		||||
} from './quiz';
 | 
			
		||||
import { AddonModQuizOffline } from './quiz-offline';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper service that provides some features for quiz.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModQuizHelperProvider {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Validate a preflight data or show a modal to input the preflight data if required.
 | 
			
		||||
     * It calls AddonModQuizProvider.startAttempt if a new attempt is needed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param accessInfo Quiz access info.
 | 
			
		||||
     * @param preflightData Object where to store the preflight data.
 | 
			
		||||
     * @param attempt Attempt to continue. Don't pass any value if the user needs to start a new attempt.
 | 
			
		||||
     * @param offline Whether the attempt is offline.
 | 
			
		||||
     * @param prefetch Whether user is prefetching.
 | 
			
		||||
     * @param title The title to display in the modal and in the submit button.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @param retrying Whether we're retrying after a failure.
 | 
			
		||||
     * @return Promise resolved when the preflight data is validated. The resolve param is the attempt.
 | 
			
		||||
     */
 | 
			
		||||
    async getAndCheckPreflightData(
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
 | 
			
		||||
        preflightData: Record<string, string>,
 | 
			
		||||
        attempt?: AddonModQuizAttemptWSData,
 | 
			
		||||
        offline?: boolean,
 | 
			
		||||
        prefetch?: boolean,
 | 
			
		||||
        title?: string,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
        retrying?: boolean,
 | 
			
		||||
    ): Promise<AddonModQuizAttemptWSData> {
 | 
			
		||||
 | 
			
		||||
        const rules = accessInfo?.activerulenames;
 | 
			
		||||
 | 
			
		||||
        // Check if the user needs to input preflight data.
 | 
			
		||||
        const preflightCheckRequired = await AddonModQuizAccessRuleDelegate.instance.isPreflightCheckRequired(
 | 
			
		||||
            rules,
 | 
			
		||||
            quiz,
 | 
			
		||||
            attempt,
 | 
			
		||||
            prefetch,
 | 
			
		||||
            siteId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (preflightCheckRequired) {
 | 
			
		||||
            // Preflight check is required. Show a modal with the preflight form.
 | 
			
		||||
            const data = await this.getPreflightData(quiz, accessInfo, attempt, prefetch, title, siteId);
 | 
			
		||||
 | 
			
		||||
            // Data entered by the user, add it to preflight data and check it again.
 | 
			
		||||
            Object.assign(preflightData, data);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Get some fixed preflight data from access rules (data that doesn't require user interaction).
 | 
			
		||||
        await AddonModQuizAccessRuleDelegate.instance.getFixedPreflightData(rules, quiz, preflightData, attempt, prefetch, siteId);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // All the preflight data is gathered, now validate it.
 | 
			
		||||
            return await this.validatePreflightData(quiz, accessInfo, preflightData, attempt, offline, prefetch, siteId);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
 | 
			
		||||
            if (prefetch) {
 | 
			
		||||
                throw error;
 | 
			
		||||
            } else if (retrying && !preflightCheckRequired) {
 | 
			
		||||
                // We're retrying after a failure, but the preflight check wasn't required.
 | 
			
		||||
                // This means there's something wrong with some access rule or user is offline and data isn't cached.
 | 
			
		||||
                // Don't retry again because it would lead to an infinite loop.
 | 
			
		||||
                throw error;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Show error and ask for the preflight again.
 | 
			
		||||
            // Wait to show the error because we want it to be shown over the preflight modal.
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                CoreDomUtils.instance.showErrorModalDefault(error, 'core.error', true);
 | 
			
		||||
            }, 100);
 | 
			
		||||
 | 
			
		||||
            return this.getAndCheckPreflightData(
 | 
			
		||||
                quiz,
 | 
			
		||||
                accessInfo,
 | 
			
		||||
                preflightData,
 | 
			
		||||
                attempt,
 | 
			
		||||
                offline,
 | 
			
		||||
                prefetch,
 | 
			
		||||
                title,
 | 
			
		||||
                siteId,
 | 
			
		||||
                true,
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the preflight data from the user using a modal.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param accessInfo Quiz access info.
 | 
			
		||||
     * @param attempt The attempt started/continued. If not supplied, user is starting a new attempt.
 | 
			
		||||
     * @param prefetch Whether the user is prefetching the quiz.
 | 
			
		||||
     * @param title The title to display in the modal and in the submit button.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the preflight data. Rejected if user cancels.
 | 
			
		||||
     */
 | 
			
		||||
    async getPreflightData(
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
 | 
			
		||||
        attempt?: AddonModQuizAttemptWSData,
 | 
			
		||||
        prefetch?: boolean,
 | 
			
		||||
        title?: string,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<Record<string, string>> {
 | 
			
		||||
        const notSupported: string[] = [];
 | 
			
		||||
        const rules = accessInfo?.activerulenames;
 | 
			
		||||
 | 
			
		||||
        // Check if there is any unsupported rule.
 | 
			
		||||
        rules.forEach((rule) => {
 | 
			
		||||
            if (!AddonModQuizAccessRuleDelegate.instance.isAccessRuleSupported(rule)) {
 | 
			
		||||
                notSupported.push(rule);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (notSupported.length) {
 | 
			
		||||
            throw new CoreError(
 | 
			
		||||
                Translate.instance.instant('addon.mod_quiz.errorrulesnotsupported') + ' ' + JSON.stringify(notSupported),
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create and show the modal.
 | 
			
		||||
        const modal = await ModalController.instance.create({
 | 
			
		||||
            component: AddonModQuizPreflightModalComponent,
 | 
			
		||||
            componentProps: {
 | 
			
		||||
                title: title,
 | 
			
		||||
                quiz,
 | 
			
		||||
                attempt,
 | 
			
		||||
                prefetch: !!prefetch,
 | 
			
		||||
                siteId: siteId,
 | 
			
		||||
                rules: rules,
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await modal.present();
 | 
			
		||||
 | 
			
		||||
        const result = await modal.onWillDismiss();
 | 
			
		||||
 | 
			
		||||
        if (!result.data) {
 | 
			
		||||
            throw new CoreCanceledError();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return <Record<string, string>> result.data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the mark string from a question HTML.
 | 
			
		||||
     * Example result: "Marked out of 1.00".
 | 
			
		||||
     *
 | 
			
		||||
     * @param html Question's HTML.
 | 
			
		||||
     * @return Question's mark.
 | 
			
		||||
     */
 | 
			
		||||
    getQuestionMarkFromHtml(html: string): string | undefined {
 | 
			
		||||
        const element = CoreDomUtils.instance.convertToElement(html);
 | 
			
		||||
 | 
			
		||||
        return CoreDomUtils.instance.getContentsOfElement(element, '.grade');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a quiz ID by attempt ID.
 | 
			
		||||
     *
 | 
			
		||||
     * @param attemptId Attempt ID.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @return Promise resolved with the quiz ID.
 | 
			
		||||
     */
 | 
			
		||||
    async getQuizIdByAttemptId(attemptId: number, options: { cmId?: number; siteId?: string } = {}): Promise<number> {
 | 
			
		||||
        // Use getAttemptReview to retrieve the quiz ID.
 | 
			
		||||
        const reviewData = await AddonModQuiz.instance.getAttemptReview(attemptId, options);
 | 
			
		||||
 | 
			
		||||
        if (reviewData.attempt.quiz) {
 | 
			
		||||
            return reviewData.attempt.quiz;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new CoreError('Cannot get quiz ID.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handle a review link.
 | 
			
		||||
     *
 | 
			
		||||
     * @param attemptId Attempt ID.
 | 
			
		||||
     * @param page Page to load, -1 to all questions in same page.
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @param quizId Quiz ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async handleReviewLink(attemptId: number, page?: number, courseId?: number, quizId?: number, siteId?: string): Promise<void> {
 | 
			
		||||
        siteId = siteId || CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        const modal = await CoreDomUtils.instance.showModalLoading();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            if (!quizId) {
 | 
			
		||||
                quizId = await this.getQuizIdByAttemptId(attemptId, { siteId });
 | 
			
		||||
            }
 | 
			
		||||
            if (!courseId) {
 | 
			
		||||
                courseId = await CoreCourseHelper.instance.getModuleCourseIdByInstance(quizId, 'quiz', siteId);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Go to the review page.
 | 
			
		||||
            const pageParams = {
 | 
			
		||||
                quizId,
 | 
			
		||||
                attemptId,
 | 
			
		||||
                courseId,
 | 
			
		||||
                page: page == undefined || isNaN(page) ? -1 : page,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            await CoreNavigator.instance.navigateToSitePath('@todo AddonModQuizReviewPage', { params: pageParams, siteId });
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'An error occurred while loading the required data.');
 | 
			
		||||
        } finally {
 | 
			
		||||
            modal.dismiss();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add some calculated data to the attempt.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param attempt Attempt.
 | 
			
		||||
     * @param highlight Whether we should check if attempt should be highlighted.
 | 
			
		||||
     * @param bestGrade Quiz's best grade (formatted). Required if highlight=true.
 | 
			
		||||
     * @param isLastAttempt Whether the attempt is the last one.
 | 
			
		||||
     * @param siteId Site ID.
 | 
			
		||||
     */
 | 
			
		||||
    async setAttemptCalculatedData(
 | 
			
		||||
        quiz: AddonModQuizQuizData,
 | 
			
		||||
        attempt: AddonModQuizAttemptWSData,
 | 
			
		||||
        highlight?: boolean,
 | 
			
		||||
        bestGrade?: string,
 | 
			
		||||
        isLastAttempt?: boolean,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<AddonModQuizAttempt> {
 | 
			
		||||
        const formattedAttempt = <AddonModQuizAttempt> attempt;
 | 
			
		||||
 | 
			
		||||
        formattedAttempt.rescaledGrade = AddonModQuiz.instance.rescaleGrade(attempt.sumgrades, quiz, false);
 | 
			
		||||
        formattedAttempt.finished = AddonModQuiz.instance.isAttemptFinished(attempt.state);
 | 
			
		||||
        formattedAttempt.readableState = AddonModQuiz.instance.getAttemptReadableState(quiz, attempt);
 | 
			
		||||
 | 
			
		||||
        if (quiz.showMarkColumn && formattedAttempt.finished) {
 | 
			
		||||
            formattedAttempt.readableMark = AddonModQuiz.instance.formatGrade(attempt.sumgrades, quiz.decimalpoints);
 | 
			
		||||
        } else {
 | 
			
		||||
            formattedAttempt.readableMark = '';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (quiz.showGradeColumn && formattedAttempt.finished) {
 | 
			
		||||
            formattedAttempt.readableGrade = AddonModQuiz.instance.formatGrade(
 | 
			
		||||
                Number(formattedAttempt.rescaledGrade),
 | 
			
		||||
                quiz.decimalpoints,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Highlight the highest grade if appropriate.
 | 
			
		||||
            formattedAttempt.highlightGrade = !!(highlight && !attempt.preview &&
 | 
			
		||||
                attempt.state == AddonModQuizProvider.ATTEMPT_FINISHED && formattedAttempt.readableGrade == bestGrade);
 | 
			
		||||
        } else {
 | 
			
		||||
            formattedAttempt.readableGrade = '';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isLastAttempt || isLastAttempt === undefined) {
 | 
			
		||||
            formattedAttempt.finishedOffline = await AddonModQuiz.instance.isAttemptFinishedOffline(attempt.id, siteId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return formattedAttempt;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add some calculated data to the quiz.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param options Review options.
 | 
			
		||||
     */
 | 
			
		||||
    setQuizCalculatedData(quiz: AddonModQuizQuizWSData, options: AddonModQuizCombinedReviewOptions): AddonModQuizQuizData {
 | 
			
		||||
        const formattedQuiz = <AddonModQuizQuizData> quiz;
 | 
			
		||||
 | 
			
		||||
        formattedQuiz.sumGradesFormatted = AddonModQuiz.instance.formatGrade(quiz.sumgrades, quiz.decimalpoints);
 | 
			
		||||
        formattedQuiz.gradeFormatted = AddonModQuiz.instance.formatGrade(quiz.grade, quiz.decimalpoints);
 | 
			
		||||
 | 
			
		||||
        formattedQuiz.showAttemptColumn = quiz.attempts != 1;
 | 
			
		||||
        formattedQuiz.showGradeColumn = options.someoptions.marks >= AddonModQuizProvider.QUESTION_OPTIONS_MARK_AND_MAX &&
 | 
			
		||||
            AddonModQuiz.instance.quizHasGrades(quiz);
 | 
			
		||||
        formattedQuiz.showMarkColumn = formattedQuiz.showGradeColumn && quiz.grade != quiz.sumgrades;
 | 
			
		||||
        formattedQuiz.showFeedbackColumn = !!quiz.hasfeedback && !!options.alloptions.overallfeedback;
 | 
			
		||||
 | 
			
		||||
        return formattedQuiz;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Validate the preflight data. It calls AddonModQuizProvider.startAttempt if a new attempt is needed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param accessInfo Quiz access info.
 | 
			
		||||
     * @param preflightData Object where to store the preflight data.
 | 
			
		||||
     * @param attempt Attempt to continue. Don't pass any value if the user needs to start a new attempt.
 | 
			
		||||
     * @param offline Whether the attempt is offline.
 | 
			
		||||
     * @param sent Whether preflight data has been entered by the user.
 | 
			
		||||
     * @param prefetch Whether user is prefetching.
 | 
			
		||||
     * @param title The title to display in the modal and in the submit button.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when the preflight data is validated.
 | 
			
		||||
     */
 | 
			
		||||
    async validatePreflightData(
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
 | 
			
		||||
        preflightData: Record<string, string>,
 | 
			
		||||
        attempt?: AddonModQuizAttempt,
 | 
			
		||||
        offline?: boolean,
 | 
			
		||||
        prefetch?: boolean,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<AddonModQuizAttempt> {
 | 
			
		||||
 | 
			
		||||
        const rules = accessInfo.activerulenames;
 | 
			
		||||
        const modOptions = {
 | 
			
		||||
            cmId: quiz.coursemodule,
 | 
			
		||||
            readingStrategy: offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
            siteId,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            if (attempt) {
 | 
			
		||||
                if (attempt.state != AddonModQuizProvider.ATTEMPT_OVERDUE && !attempt.finishedOffline) {
 | 
			
		||||
                    // We're continuing an attempt. Call getAttemptData to validate the preflight data.
 | 
			
		||||
                    await AddonModQuiz.instance.getAttemptData(attempt.id, attempt.currentpage!, preflightData, modOptions);
 | 
			
		||||
 | 
			
		||||
                    if (offline) {
 | 
			
		||||
                        // Get current page stored in local.
 | 
			
		||||
                        const storedAttempt = await CoreUtils.instance.ignoreErrors(
 | 
			
		||||
                            AddonModQuizOffline.instance.getAttemptById(attempt.id),
 | 
			
		||||
                        );
 | 
			
		||||
 | 
			
		||||
                        attempt.currentpage = storedAttempt?.currentpage ?? attempt.currentpage;
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Attempt is overdue or finished in offline, we can only see the summary.
 | 
			
		||||
                    // Call getAttemptSummary to validate the preflight data.
 | 
			
		||||
                    await AddonModQuiz.instance.getAttemptSummary(attempt.id, preflightData, modOptions);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                // We're starting a new attempt, call startAttempt.
 | 
			
		||||
                attempt = await AddonModQuiz.instance.startAttempt(quiz.id, preflightData, false, siteId);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Preflight data validated.
 | 
			
		||||
            AddonModQuizAccessRuleDelegate.instance.notifyPreflightCheckPassed(
 | 
			
		||||
                rules,
 | 
			
		||||
                quiz,
 | 
			
		||||
                attempt,
 | 
			
		||||
                preflightData,
 | 
			
		||||
                prefetch,
 | 
			
		||||
                siteId,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            return attempt;
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (CoreUtils.instance.isWebServiceError(error)) {
 | 
			
		||||
                // The WebService returned an error, assume the preflight failed.
 | 
			
		||||
                AddonModQuizAccessRuleDelegate.instance.notifyPreflightCheckFailed(
 | 
			
		||||
                    rules,
 | 
			
		||||
                    quiz,
 | 
			
		||||
                    attempt,
 | 
			
		||||
                    preflightData,
 | 
			
		||||
                    prefetch,
 | 
			
		||||
                    siteId,
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            throw error;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class AddonModQuizHelper extends makeSingleton(AddonModQuizHelperProvider) {}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Quiz data with calculated data.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModQuizQuizData = AddonModQuizQuizWSData & {
 | 
			
		||||
    sumGradesFormatted?: string;
 | 
			
		||||
    gradeFormatted?: string;
 | 
			
		||||
    showAttemptColumn?: boolean;
 | 
			
		||||
    showGradeColumn?: boolean;
 | 
			
		||||
    showMarkColumn?: boolean;
 | 
			
		||||
    showFeedbackColumn?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Attempt data with calculated data.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModQuizAttempt = AddonModQuizAttemptWSData & {
 | 
			
		||||
    finishedOffline?: boolean;
 | 
			
		||||
    rescaledGrade?: string;
 | 
			
		||||
    finished?: boolean;
 | 
			
		||||
    readableState?: string[];
 | 
			
		||||
    readableMark?: string;
 | 
			
		||||
    readableGrade?: string;
 | 
			
		||||
    highlightGrade?: boolean;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										372
									
								
								src/addons/mod/quiz/services/quiz-offline.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										372
									
								
								src/addons/mod/quiz/services/quiz-offline.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,372 @@
 | 
			
		||||
// (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 { CoreQuestionBehaviourDelegate, CoreQuestionQuestionWithAnswers } from '@features/question/services/behaviour-delegate';
 | 
			
		||||
import { CoreQuestionAnswerDBRecord } from '@features/question/services/database/question';
 | 
			
		||||
import { CoreQuestion, CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/question/services/question';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreLogger } from '@singletons/logger';
 | 
			
		||||
import { AddonModQuizAttemptDBRecord, ATTEMPTS_TABLE_NAME } from './database/quiz';
 | 
			
		||||
import { AddonModQuizAttemptWSData, AddonModQuizProvider, AddonModQuizQuizWSData } from './quiz';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service to handle offline quiz.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModQuizOfflineProvider {
 | 
			
		||||
 | 
			
		||||
    protected logger: CoreLogger;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.logger = CoreLogger.getInstance('AddonModQuizOfflineProvider');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Classify the answers in questions.
 | 
			
		||||
     *
 | 
			
		||||
     * @param answers List of answers.
 | 
			
		||||
     * @return Object with the questions, the keys are the slot. Each question contains its answers.
 | 
			
		||||
     */
 | 
			
		||||
    classifyAnswersInQuestions(answers: CoreQuestionsAnswers): AddonModQuizQuestionsWithAnswers {
 | 
			
		||||
        const questionsWithAnswers: AddonModQuizQuestionsWithAnswers = {};
 | 
			
		||||
 | 
			
		||||
        // Classify the answers in each question.
 | 
			
		||||
        for (const name in answers) {
 | 
			
		||||
            const slot = CoreQuestion.instance.getQuestionSlotFromName(name);
 | 
			
		||||
            const nameWithoutPrefix = CoreQuestion.instance.removeQuestionPrefix(name);
 | 
			
		||||
 | 
			
		||||
            if (!questionsWithAnswers[slot]) {
 | 
			
		||||
                questionsWithAnswers[slot] = {
 | 
			
		||||
                    answers: {},
 | 
			
		||||
                    prefix: name.substr(0, name.indexOf(nameWithoutPrefix)),
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
            questionsWithAnswers[slot].answers[nameWithoutPrefix] = answers[name];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return questionsWithAnswers;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a list of questions with answers classified in it, returns a list of answers (including prefix in the name).
 | 
			
		||||
     *
 | 
			
		||||
     * @param questions Questions.
 | 
			
		||||
     * @return Answers.
 | 
			
		||||
     */
 | 
			
		||||
    extractAnswersFromQuestions(questions: AddonModQuizQuestionsWithAnswers): CoreQuestionsAnswers {
 | 
			
		||||
        const answers: CoreQuestionsAnswers = {};
 | 
			
		||||
 | 
			
		||||
        for (const slot in questions) {
 | 
			
		||||
            const question = questions[slot];
 | 
			
		||||
 | 
			
		||||
            for (const name in question.answers) {
 | 
			
		||||
                answers[question.prefix + name] = question.answers[name];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return answers;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all the offline attempts in a certain site.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the offline attempts.
 | 
			
		||||
     */
 | 
			
		||||
    async getAllAttempts(siteId?: string): Promise<AddonModQuizAttemptDBRecord[]> {
 | 
			
		||||
        const db = await CoreSites.instance.getSiteDb(siteId);
 | 
			
		||||
 | 
			
		||||
        return db.getAllRecords(ATTEMPTS_TABLE_NAME);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve an attempt answers from site DB.
 | 
			
		||||
     *
 | 
			
		||||
     * @param attemptId Attempt ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the answers.
 | 
			
		||||
     */
 | 
			
		||||
    getAttemptAnswers(attemptId: number, siteId?: string): Promise<CoreQuestionAnswerDBRecord[]> {
 | 
			
		||||
        return CoreQuestion.instance.getAttemptAnswers(AddonModQuizProvider.COMPONENT, attemptId, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve an attempt from site DB.
 | 
			
		||||
     *
 | 
			
		||||
     * @param attemptId Attempt ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the attempt.
 | 
			
		||||
     */
 | 
			
		||||
    async getAttemptById(attemptId: number, siteId?: string): Promise<AddonModQuizAttemptDBRecord> {
 | 
			
		||||
        const db = await CoreSites.instance.getSiteDb(siteId);
 | 
			
		||||
 | 
			
		||||
        return db.getRecord(ATTEMPTS_TABLE_NAME, { id: attemptId });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve an attempt from site DB.
 | 
			
		||||
     *
 | 
			
		||||
     * @param attemptId Attempt ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @param userId User ID. If not defined, user current site's user.
 | 
			
		||||
     * @return Promise resolved with the attempts.
 | 
			
		||||
     */
 | 
			
		||||
    async getQuizAttempts(quizId: number, siteId?: string, userId?: number): Promise<AddonModQuizAttemptDBRecord[]> {
 | 
			
		||||
        const site = await CoreSites.instance.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        return site.getDb().getRecords(ATTEMPTS_TABLE_NAME, { quizid: quizId, userid: userId || site.getUserId() });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load local state in the questions.
 | 
			
		||||
     *
 | 
			
		||||
     * @param attemptId Attempt ID.
 | 
			
		||||
     * @param questions List of questions.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async loadQuestionsLocalStates(
 | 
			
		||||
        attemptId: number,
 | 
			
		||||
        questions: CoreQuestionQuestionParsed[],
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<CoreQuestionQuestionParsed[]> {
 | 
			
		||||
 | 
			
		||||
        await Promise.all(questions.map(async (question) => {
 | 
			
		||||
            const dbQuestion = await CoreUtils.instance.ignoreErrors(
 | 
			
		||||
                CoreQuestion.instance.getQuestion(AddonModQuizProvider.COMPONENT, attemptId, question.slot, siteId),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (!dbQuestion) {
 | 
			
		||||
                // Question not found.
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const state = CoreQuestion.instance.getState(dbQuestion.state);
 | 
			
		||||
            question.state = dbQuestion.state;
 | 
			
		||||
            question.status = Translate.instance.instant('core.question.' + state.status);
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        return questions;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Process an attempt, saving its data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param attempt Attempt.
 | 
			
		||||
     * @param questions Object with the questions of the quiz. The keys should be the question slot.
 | 
			
		||||
     * @param data Data to save.
 | 
			
		||||
     * @param finish Whether to finish the quiz.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved in success, rejected otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    async processAttempt(
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        attempt: AddonModQuizAttemptWSData,
 | 
			
		||||
        questions: Record<number, CoreQuestionQuestionParsed>,
 | 
			
		||||
        data: CoreQuestionsAnswers,
 | 
			
		||||
        finish?: boolean,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        siteId = siteId || CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
        const now = CoreTimeUtils.instance.timestamp();
 | 
			
		||||
 | 
			
		||||
        const db = await CoreSites.instance.getSiteDb(siteId);
 | 
			
		||||
 | 
			
		||||
        // Check if an attempt already exists. Return a new one if it doesn't.
 | 
			
		||||
        let entry = await CoreUtils.instance.ignoreErrors(this.getAttemptById(attempt.id, siteId));
 | 
			
		||||
 | 
			
		||||
        if (entry) {
 | 
			
		||||
            entry.timemodified = now;
 | 
			
		||||
            entry.finished = finish ? 1 : 0;
 | 
			
		||||
        } else {
 | 
			
		||||
            entry = {
 | 
			
		||||
                quizid: quiz.id,
 | 
			
		||||
                userid: attempt.userid!,
 | 
			
		||||
                id: attempt.id,
 | 
			
		||||
                courseid: quiz.course,
 | 
			
		||||
                timecreated: now,
 | 
			
		||||
                attempt: attempt.attempt!,
 | 
			
		||||
                currentpage: attempt.currentpage,
 | 
			
		||||
                timemodified: now,
 | 
			
		||||
                finished: finish ? 1 : 0,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Save attempt in DB.
 | 
			
		||||
        await db.insertRecord(ATTEMPTS_TABLE_NAME, entry);
 | 
			
		||||
 | 
			
		||||
        // Attempt has been saved, now we need to save the answers.
 | 
			
		||||
        await this.saveAnswers(quiz, attempt, questions, data, now, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove an attempt and its answers from local DB.
 | 
			
		||||
     *
 | 
			
		||||
     * @param attemptId Attempt ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async removeAttemptAndAnswers(attemptId: number, siteId?: string): Promise<void> {
 | 
			
		||||
        siteId = siteId || CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        const db = await CoreSites.instance.getSiteDb(siteId);
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            CoreQuestion.instance.removeAttemptAnswers(AddonModQuizProvider.COMPONENT, attemptId, siteId),
 | 
			
		||||
            CoreQuestion.instance.removeAttemptQuestions(AddonModQuizProvider.COMPONENT, attemptId, siteId),
 | 
			
		||||
            db.deleteRecords(ATTEMPTS_TABLE_NAME, { id: attemptId }),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove a question and its answers from local DB.
 | 
			
		||||
     *
 | 
			
		||||
     * @param attemptId Attempt ID.
 | 
			
		||||
     * @param slot Question slot.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when finished.
 | 
			
		||||
     */
 | 
			
		||||
    async removeQuestionAndAnswers(attemptId: number, slot: number, siteId?: string): Promise<void> {
 | 
			
		||||
        siteId = siteId || CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            CoreQuestion.instance.removeQuestion(AddonModQuizProvider.COMPONENT, attemptId, slot, siteId),
 | 
			
		||||
            CoreQuestion.instance.removeQuestionAnswers(AddonModQuizProvider.COMPONENT, attemptId, slot, siteId),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save an attempt's answers and calculate state for questions modified.
 | 
			
		||||
     *
 | 
			
		||||
     * @param quiz Quiz.
 | 
			
		||||
     * @param attempt Attempt.
 | 
			
		||||
     * @param questions Object with the questions of the quiz. The keys should be the question slot.
 | 
			
		||||
     * @param answers Answers to save.
 | 
			
		||||
     * @param timeMod Time modified to set in the answers. If not defined, current time.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async saveAnswers(
 | 
			
		||||
        quiz: AddonModQuizQuizWSData,
 | 
			
		||||
        attempt: AddonModQuizAttemptWSData,
 | 
			
		||||
        questions: Record<number, CoreQuestionQuestionParsed>,
 | 
			
		||||
        answers: CoreQuestionsAnswers,
 | 
			
		||||
        timeMod?: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        siteId = siteId || CoreSites.instance.getCurrentSiteId();
 | 
			
		||||
        timeMod = timeMod || CoreTimeUtils.instance.timestamp();
 | 
			
		||||
 | 
			
		||||
        const questionsWithAnswers: Record<number, CoreQuestionQuestionWithAnswers> = {};
 | 
			
		||||
        const newStates: Record<number, string> = {};
 | 
			
		||||
 | 
			
		||||
        // Classify the answers in each question.
 | 
			
		||||
        for (const name in answers) {
 | 
			
		||||
            const slot = CoreQuestion.instance.getQuestionSlotFromName(name);
 | 
			
		||||
            const nameWithoutPrefix = CoreQuestion.instance.removeQuestionPrefix(name);
 | 
			
		||||
 | 
			
		||||
            if (questions[slot]) {
 | 
			
		||||
                if (!questionsWithAnswers[slot]) {
 | 
			
		||||
                    questionsWithAnswers[slot] = questions[slot];
 | 
			
		||||
                    questionsWithAnswers[slot].answers = {};
 | 
			
		||||
                }
 | 
			
		||||
                questionsWithAnswers[slot].answers![nameWithoutPrefix] = answers[name];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // First determine the new state of each question. We won't save the new state yet.
 | 
			
		||||
        await Promise.all(Object.values(questionsWithAnswers).map(async (question) => {
 | 
			
		||||
 | 
			
		||||
            const state = await CoreQuestionBehaviourDelegate.instance.determineNewState(
 | 
			
		||||
                quiz.preferredbehaviour!,
 | 
			
		||||
                AddonModQuizProvider.COMPONENT,
 | 
			
		||||
                attempt.id,
 | 
			
		||||
                question,
 | 
			
		||||
                quiz.coursemodule,
 | 
			
		||||
                siteId,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Check if state has changed.
 | 
			
		||||
            if (state && state.name != question.state) {
 | 
			
		||||
                newStates[question.slot] = state.name;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Delete previously stored answers for this question.
 | 
			
		||||
            await CoreQuestion.instance.removeQuestionAnswers(AddonModQuizProvider.COMPONENT, attempt.id, question.slot, siteId);
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        // Now save the answers.
 | 
			
		||||
        await CoreQuestion.instance.saveAnswers(
 | 
			
		||||
            AddonModQuizProvider.COMPONENT,
 | 
			
		||||
            quiz.id,
 | 
			
		||||
            attempt.id,
 | 
			
		||||
            attempt.userid!,
 | 
			
		||||
            answers,
 | 
			
		||||
            timeMod,
 | 
			
		||||
            siteId,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // Answers have been saved, now we can save the questions with the states.
 | 
			
		||||
            await CoreUtils.instance.allPromises(Object.keys(newStates).map(async (slot) => {
 | 
			
		||||
                const question = questionsWithAnswers[Number(slot)];
 | 
			
		||||
 | 
			
		||||
                await CoreQuestion.instance.saveQuestion(
 | 
			
		||||
                    AddonModQuizProvider.COMPONENT,
 | 
			
		||||
                    quiz.id,
 | 
			
		||||
                    attempt.id,
 | 
			
		||||
                    attempt.userid!,
 | 
			
		||||
                    question,
 | 
			
		||||
                    newStates[slot],
 | 
			
		||||
                    siteId,
 | 
			
		||||
                );
 | 
			
		||||
            }));
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            // Ignore errors when saving question state.
 | 
			
		||||
            this.logger.error('Error saving question state', error);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set attempt's current page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param attemptId Attempt ID.
 | 
			
		||||
     * @param page Page to set.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved in success, rejected otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    async setAttemptCurrentPage(attemptId: number, page: number, siteId?: string): Promise<void> {
 | 
			
		||||
        const db = await CoreSites.instance.getSiteDb(siteId);
 | 
			
		||||
 | 
			
		||||
        await db.updateRecords(ATTEMPTS_TABLE_NAME, { currentpage: page }, { id: attemptId });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class AddonModQuizOffline extends makeSingleton(AddonModQuizOfflineProvider) {}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Answers classified by question slot.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModQuizQuestionsWithAnswers = Record<number, {
 | 
			
		||||
    prefix: string;
 | 
			
		||||
    answers: CoreQuestionsAnswers;
 | 
			
		||||
}>;
 | 
			
		||||
							
								
								
									
										2402
									
								
								src/addons/mod/quiz/services/quiz.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2402
									
								
								src/addons/mod/quiz/services/quiz.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1072,13 +1072,13 @@ export class CoreUtilsProvider {
 | 
			
		||||
     * @param sortByValue True to sort values alphabetically, false otherwise.
 | 
			
		||||
     * @return Array of objects with the name & value of each property.
 | 
			
		||||
     */
 | 
			
		||||
    objectToArrayOfObjects(
 | 
			
		||||
    objectToArrayOfObjects<T = Record<string, unknown>>(
 | 
			
		||||
        obj: Record<string, unknown>,
 | 
			
		||||
        keyName: string,
 | 
			
		||||
        valueName: string,
 | 
			
		||||
        sortByKey?: boolean,
 | 
			
		||||
        sortByValue?: boolean,
 | 
			
		||||
    ): Record<string, unknown>[] {
 | 
			
		||||
    ): T[] {
 | 
			
		||||
        // Get the entries from an object or primitive value.
 | 
			
		||||
        const getEntries = (elKey: string, value: unknown): Record<string, unknown>[] | unknown => {
 | 
			
		||||
            if (typeof value == 'undefined' || value == null) {
 | 
			
		||||
@ -1114,7 +1114,7 @@ export class CoreUtilsProvider {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // "obj" will always be an object, so "entries" will always be an array.
 | 
			
		||||
        const entries = getEntries('', obj) as Record<string, unknown>[];
 | 
			
		||||
        const entries = getEntries('', obj) as T[];
 | 
			
		||||
        if (sortByKey || sortByValue) {
 | 
			
		||||
            return entries.sort((a, b) => {
 | 
			
		||||
                if (sortByKey) {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user