commit
						be1045963d
					
				@ -67,6 +67,7 @@ import { CoreGradesModule } from '@core/grades/grades.module';
 | 
			
		||||
import { CoreSettingsModule } from '@core/settings/settings.module';
 | 
			
		||||
import { CoreSitePluginsModule } from '@core/siteplugins/siteplugins.module';
 | 
			
		||||
import { CoreCompileModule } from '@core/compile/compile.module';
 | 
			
		||||
import { CoreQuestionModule } from '@core/question/question.module';
 | 
			
		||||
 | 
			
		||||
// Addon modules.
 | 
			
		||||
import { AddonBadgesModule } from '@addon/badges/badges.module';
 | 
			
		||||
@ -155,6 +156,7 @@ export const CORE_PROVIDERS: any[] = [
 | 
			
		||||
        CoreSettingsModule,
 | 
			
		||||
        CoreSitePluginsModule,
 | 
			
		||||
        CoreCompileModule,
 | 
			
		||||
        CoreQuestionModule,
 | 
			
		||||
        AddonBadgesModule,
 | 
			
		||||
        AddonCalendarModule,
 | 
			
		||||
        AddonCompetencyModule,
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@ import { CORE_FILEUPLOADER_PROVIDERS } from '@core/fileuploader/fileuploader.mod
 | 
			
		||||
import { CORE_GRADES_PROVIDERS } from '@core/grades/grades.module';
 | 
			
		||||
import { CORE_LOGIN_PROVIDERS } from '@core/login/login.module';
 | 
			
		||||
import { CORE_MAINMENU_PROVIDERS } from '@core/mainmenu/mainmenu.module';
 | 
			
		||||
import { CORE_QUESTION_PROVIDERS } from '@core/question/question.module';
 | 
			
		||||
import { CORE_SHAREDFILES_PROVIDERS } from '@core/sharedfiles/sharedfiles.module';
 | 
			
		||||
import { CORE_SITEHOME_PROVIDERS } from '@core/sitehome/sitehome.module';
 | 
			
		||||
import { CORE_USER_PROVIDERS } from '@core/user/user.module';
 | 
			
		||||
@ -66,6 +67,7 @@ import { CoreCoursesComponentsModule } from '@core/courses/components/components
 | 
			
		||||
import { CoreSitePluginsDirectivesModule } from '@core/siteplugins/directives/directives.module';
 | 
			
		||||
import { CoreSiteHomeComponentsModule } from '@core/sitehome/components/components.module';
 | 
			
		||||
import { CoreUserComponentsModule } from '@core/user/components/components.module';
 | 
			
		||||
import { CoreQuestionComponentsModule } from '@core/question/components/components.module';
 | 
			
		||||
 | 
			
		||||
// Import some components listed in entryComponents so they can be injected dynamically.
 | 
			
		||||
import { CoreCourseUnsupportedModuleComponent } from '@core/course/components/unsupported-module/unsupported-module';
 | 
			
		||||
@ -92,7 +94,7 @@ export class CoreCompileProvider {
 | 
			
		||||
    protected IMPORTS = [
 | 
			
		||||
        IonicModule, TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, CorePipesModule,
 | 
			
		||||
        CoreCourseComponentsModule, CoreCoursesComponentsModule, CoreSiteHomeComponentsModule, CoreUserComponentsModule,
 | 
			
		||||
        CoreCourseDirectivesModule, CoreSitePluginsDirectivesModule
 | 
			
		||||
        CoreCourseDirectivesModule, CoreSitePluginsDirectivesModule, CoreQuestionComponentsModule
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    constructor(protected injector: Injector, logger: CoreLoggerProvider, protected compiler: Compiler) {
 | 
			
		||||
@ -164,7 +166,7 @@ export class CoreCompileProvider {
 | 
			
		||||
                .concat(CORE_COURSES_PROVIDERS).concat(CORE_FILEUPLOADER_PROVIDERS).concat(CORE_GRADES_PROVIDERS)
 | 
			
		||||
                .concat(CORE_LOGIN_PROVIDERS).concat(CORE_MAINMENU_PROVIDERS).concat(CORE_SHAREDFILES_PROVIDERS)
 | 
			
		||||
                .concat(CORE_SITEHOME_PROVIDERS).concat([CoreSitePluginsProvider]).concat(CORE_USER_PROVIDERS)
 | 
			
		||||
                .concat(IONIC_NATIVE_PROVIDERS).concat(this.OTHER_PROVIDERS);
 | 
			
		||||
                .concat(CORE_QUESTION_PROVIDERS).concat(IONIC_NATIVE_PROVIDERS).concat(this.OTHER_PROVIDERS);
 | 
			
		||||
 | 
			
		||||
        // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance.
 | 
			
		||||
        for (const i in providers) {
 | 
			
		||||
 | 
			
		||||
@ -43,7 +43,7 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
 | 
			
		||||
     * @param {any} course The course.
 | 
			
		||||
     * @return {string} Title.
 | 
			
		||||
     */
 | 
			
		||||
    getCourseTitle?(course: any): string {
 | 
			
		||||
    getCourseTitle(course: any): string {
 | 
			
		||||
        return course.fullname || '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										40
									
								
								src/core/question/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/core/question/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
			
		||||
// (C) Copyright 2015 Martin Dougiamas
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { CommonModule } from '@angular/common';
 | 
			
		||||
import { IonicModule } from 'ionic-angular';
 | 
			
		||||
import { TranslateModule } from '@ngx-translate/core';
 | 
			
		||||
import { CoreQuestionComponent } from './question/question';
 | 
			
		||||
import { CoreComponentsModule } from '@components/components.module';
 | 
			
		||||
import { CoreDirectivesModule } from '@directives/directives.module';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [
 | 
			
		||||
        CoreQuestionComponent
 | 
			
		||||
    ],
 | 
			
		||||
    imports: [
 | 
			
		||||
        CommonModule,
 | 
			
		||||
        IonicModule,
 | 
			
		||||
        TranslateModule.forChild(),
 | 
			
		||||
        CoreComponentsModule,
 | 
			
		||||
        CoreDirectivesModule
 | 
			
		||||
    ],
 | 
			
		||||
    providers: [
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [
 | 
			
		||||
        CoreQuestionComponent
 | 
			
		||||
    ]
 | 
			
		||||
})
 | 
			
		||||
export class CoreQuestionComponentsModule {}
 | 
			
		||||
							
								
								
									
										32
									
								
								src/core/question/components/question/question.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/core/question/components/question/question.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
 | 
			
		||||
<!-- Question contents. -->
 | 
			
		||||
<core-dynamic-component *ngIf="loaded" [component]="componentClass" [data]="data" class="core-question-{{question.slot}}">
 | 
			
		||||
    <!-- This content will only be shown if there's no component to render the question. -->
 | 
			
		||||
    <p padding>{{ 'core.question.errorquestionnotsupported' | translate:{$a: question.type} }}</p>
 | 
			
		||||
</core-dynamic-component>
 | 
			
		||||
 | 
			
		||||
<!-- Sequence check input. -->
 | 
			
		||||
<input *ngIf="seqCheck" type="hidden" name="{{seqCheck.name}}" value="{{seqCheck.value}}" >
 | 
			
		||||
 | 
			
		||||
<!-- Question behaviour components. -->
 | 
			
		||||
<core-dynamic-component *ngFor="let componentClass of behaviourComponents" [component]="componentClass" [data]="data"></core-dynamic-component>
 | 
			
		||||
 | 
			
		||||
<!-- Question validation error. -->
 | 
			
		||||
<ion-item text-wrap class="core-error-item" *ngIf="question.validationError">
 | 
			
		||||
    <p>{{ question.validationError }}</p>
 | 
			
		||||
</ion-item>
 | 
			
		||||
 | 
			
		||||
<!-- Question behaviour buttons. -->
 | 
			
		||||
<ion-item text-wrap *ngFor="let button of question.behaviourButtons">
 | 
			
		||||
    <a ion-button block (click)="action.emit(button)" [disabled]="button.disabled">{{ button.value }}</a>
 | 
			
		||||
</ion-item>
 | 
			
		||||
 | 
			
		||||
<!-- Question feedback. -->
 | 
			
		||||
<ion-item text-wrap class="core-question-feedback-container" *ngIf="question.feedbackHtml">
 | 
			
		||||
    <p><core-format-text [component]="component" [componentId]="componentId" [text]="question.feedbackHtml"></core-format-text></p>
 | 
			
		||||
</ion-item>
 | 
			
		||||
 | 
			
		||||
<!-- Question comment. -->
 | 
			
		||||
<ion-item text-wrap class="core-question-comment" *ngIf="question.commentHtml">
 | 
			
		||||
    <p><core-format-text [component]="component" [componentId]="componentId" [text]="question.commentHtml"></core-format-text></p>
 | 
			
		||||
</ion-item>
 | 
			
		||||
							
								
								
									
										149
									
								
								src/core/question/components/question/question.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								src/core/question/components/question/question.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,149 @@
 | 
			
		||||
// (C) Copyright 2015 Martin Dougiamas
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, Input, Output, OnInit, Injector, EventEmitter } from '@angular/core';
 | 
			
		||||
import { TranslateService } from '@ngx-translate/core';
 | 
			
		||||
import { CoreLoggerProvider } from '@providers/logger';
 | 
			
		||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
 | 
			
		||||
import { CoreUtilsProvider } from '@providers/utils/utils';
 | 
			
		||||
import { CoreQuestionProvider } from '../../providers/question';
 | 
			
		||||
import { CoreQuestionDelegate } from '../../providers/delegate';
 | 
			
		||||
import { CoreQuestionBehaviourDelegate } from '../../providers/behaviour-delegate';
 | 
			
		||||
import { CoreQuestionHelperProvider } from '../../providers/helper';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component to render a question.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'core-question',
 | 
			
		||||
    templateUrl: 'question.html'
 | 
			
		||||
})
 | 
			
		||||
export class CoreQuestionComponent implements OnInit {
 | 
			
		||||
    @Input() question: any; // The question to render.
 | 
			
		||||
    @Input() component: string; // The component the question belongs to.
 | 
			
		||||
    @Input() componentId: number; // ID of the component the question belongs to.
 | 
			
		||||
    @Input() attemptId: number; // Attempt ID.
 | 
			
		||||
    @Input() offlineEnabled?: boolean | string; // Whether the question can be answered in offline.
 | 
			
		||||
    @Output() buttonClicked: EventEmitter<any>; // Will emit an event when a behaviour button is clicked.
 | 
			
		||||
    @Output() onAbort: EventEmitter<void>; // Will emit an event if the question should be aborted.
 | 
			
		||||
 | 
			
		||||
    componentClass: any; // The class of the component to render.
 | 
			
		||||
    data: any = {}; // Data to pass to the component.
 | 
			
		||||
    seqCheck: {name: string, value: string}; // Sequenche check name and value (if any).
 | 
			
		||||
    behaviourComponents: any[] = []; // Components to render the question behaviour.
 | 
			
		||||
    loaded = false;
 | 
			
		||||
 | 
			
		||||
    protected logger;
 | 
			
		||||
 | 
			
		||||
    constructor(logger: CoreLoggerProvider, protected injector: Injector, protected questionDelegate: CoreQuestionDelegate,
 | 
			
		||||
            protected utils: CoreUtilsProvider, protected behaviourDelegate: CoreQuestionBehaviourDelegate,
 | 
			
		||||
            protected questionHelper: CoreQuestionHelperProvider, protected translate: TranslateService,
 | 
			
		||||
            protected questionProvider: CoreQuestionProvider, protected domUtils: CoreDomUtilsProvider) {
 | 
			
		||||
        logger = logger.getInstance('CoreQuestionComponent');
 | 
			
		||||
 | 
			
		||||
        this.buttonClicked = new EventEmitter();
 | 
			
		||||
        this.onAbort = new EventEmitter();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.offlineEnabled = this.utils.isTrueOrOne(this.offlineEnabled);
 | 
			
		||||
 | 
			
		||||
        if (!this.question) {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Get the component to render the question.
 | 
			
		||||
        this.questionDelegate.getComponentForQuestion(this.injector, this.question).then((componentClass) => {
 | 
			
		||||
            this.componentClass = componentClass;
 | 
			
		||||
 | 
			
		||||
            if (componentClass) {
 | 
			
		||||
                // Set up the data needed by the question and behaviour components.
 | 
			
		||||
                this.data = {
 | 
			
		||||
                    question: this.question,
 | 
			
		||||
                    component: this.component,
 | 
			
		||||
                    componentId: this.componentId,
 | 
			
		||||
                    attemptId: this.attemptId,
 | 
			
		||||
                    offlineEnabled: this.offlineEnabled,
 | 
			
		||||
                    buttonClicked: this.buttonClicked,
 | 
			
		||||
                    onAbort: this.onAbort
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                // Treat the question.
 | 
			
		||||
                this.questionHelper.extractQuestionScripts(this.question);
 | 
			
		||||
 | 
			
		||||
                // Handle question behaviour.
 | 
			
		||||
                const behaviour = this.questionDelegate.getBehaviourForQuestion(this.question, this.question.preferredBehaviour);
 | 
			
		||||
                if (!this.behaviourDelegate.isBehaviourSupported(behaviour)) {
 | 
			
		||||
                    // Behaviour not supported, abort.
 | 
			
		||||
                    this.logger.warn('Aborting question because the behaviour is not supported.', this.question.name);
 | 
			
		||||
                    this.questionHelper.showComponentError(this.onAbort,
 | 
			
		||||
                        this.translate.instant('addon.mod_quiz.errorbehaviournotsupported') + ' ' + behaviour);
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Get the sequence check (hidden input). This is required.
 | 
			
		||||
                this.seqCheck = this.questionHelper.getQuestionSequenceCheckFromHtml(this.question.html);
 | 
			
		||||
                if (!this.seqCheck) {
 | 
			
		||||
                    this.logger.warn('Aborting question because couldn\'t retrieve sequence check.', this.question.name);
 | 
			
		||||
                    this.questionHelper.showComponentError(this.onAbort);
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Load local answers if offline is enabled.
 | 
			
		||||
                let promise;
 | 
			
		||||
                if (this.offlineEnabled) {
 | 
			
		||||
                    promise = this.questionProvider.getQuestionAnswers(this.component, this.attemptId, this.question.slot)
 | 
			
		||||
                            .then((answers) => {
 | 
			
		||||
                        this.question.localAnswers = this.questionProvider.convertAnswersArrayToObject(answers, true);
 | 
			
		||||
                    }).catch(() => {
 | 
			
		||||
                        this.question.localAnswers = {};
 | 
			
		||||
                    });
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.question.localAnswers = {};
 | 
			
		||||
                    promise = Promise.resolve();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                promise.then(() => {
 | 
			
		||||
                    // Handle behaviour.
 | 
			
		||||
                    this.behaviourDelegate.handleQuestion(this.question, this.question.preferredBehaviour).then((comps) => {
 | 
			
		||||
                        this.behaviourComponents = comps;
 | 
			
		||||
                    });
 | 
			
		||||
                    this.questionHelper.extractQbehaviourRedoButton(this.question);
 | 
			
		||||
                    this.question.html = this.domUtils.removeElementFromHtml(this.question.html, '.im-controls');
 | 
			
		||||
 | 
			
		||||
                    // Extract the validation error of the question.
 | 
			
		||||
                    this.question.validationError = this.questionHelper.getValidationErrorFromHtml(this.question.html);
 | 
			
		||||
 | 
			
		||||
                    // Load the local answers in the HTML.
 | 
			
		||||
                    this.questionHelper.loadLocalAnswersInHtml(this.question);
 | 
			
		||||
 | 
			
		||||
                    // Try to extract the feedback and comment for the question.
 | 
			
		||||
                    this.questionHelper.extractQuestionFeedback(this.question);
 | 
			
		||||
                    this.questionHelper.extractQuestionComment(this.question);
 | 
			
		||||
 | 
			
		||||
                    this.loaded = true;
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }).catch(() => {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								src/core/question/lang/en.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/core/question/lang/en.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
{
 | 
			
		||||
    "answer": "Answer",
 | 
			
		||||
    "answersaved": "Answer saved",
 | 
			
		||||
    "certainty": "Certainty",
 | 
			
		||||
    "complete": "Complete",
 | 
			
		||||
    "correct": "Correct",
 | 
			
		||||
    "errorattachmentsnotsupported": "The application doesn't support attaching files to answers yet.",
 | 
			
		||||
    "errorinlinefilesnotsupported": "The application doesn't support editing inline files yet.",
 | 
			
		||||
    "errorquestionnotsupported": "This question type is not supported by the app: {{$a}}.",
 | 
			
		||||
    "feedback": "Feedback",
 | 
			
		||||
    "howtodraganddrop": "Tap to select then tap to drop.",
 | 
			
		||||
    "incorrect": "Incorrect",
 | 
			
		||||
    "information": "Information",
 | 
			
		||||
    "invalidanswer": "Incomplete answer",
 | 
			
		||||
    "notanswered": "Not answered",
 | 
			
		||||
    "notyetanswered": "Not yet answered",
 | 
			
		||||
    "partiallycorrect": "Partially correct",
 | 
			
		||||
    "questionmessage": "Question {{$a}}: {{$b}}",
 | 
			
		||||
    "questionno": "Question {{$a}}",
 | 
			
		||||
    "requiresgrading": "Requires grading",
 | 
			
		||||
    "unknown": "Cannot determine status"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										112
									
								
								src/core/question/providers/behaviour-delegate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/core/question/providers/behaviour-delegate.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,112 @@
 | 
			
		||||
// (C) Copyright 2015 Martin Dougiamas
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreLoggerProvider } from '@providers/logger';
 | 
			
		||||
import { CoreEventsProvider } from '@providers/events';
 | 
			
		||||
import { CoreSitesProvider } from '@providers/sites';
 | 
			
		||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
 | 
			
		||||
import { CoreQuestionState } from './question';
 | 
			
		||||
import { CoreQuestionDelegate } from './delegate';
 | 
			
		||||
import { CoreQuestionBehaviourDefaultHandler } from './default-behaviour-handler';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Interface that all question behaviour handlers must implement.
 | 
			
		||||
 */
 | 
			
		||||
export interface CoreQuestionBehaviourHandler extends CoreDelegateHandler {
 | 
			
		||||
    /**
 | 
			
		||||
     * Type of the behaviour the handler supports. E.g. 'adaptive'.
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     */
 | 
			
		||||
    type: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Determine a question new state based on its answer(s).
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} component Component the question belongs to.
 | 
			
		||||
     * @param {number} attemptId Attempt ID the question belongs to.
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @param {string} [siteId] Site ID. If not defined, current site.
 | 
			
		||||
     * @return {CoreQuestionState|Promise<CoreQuestionState>} State (or promise resolved with state).
 | 
			
		||||
     */
 | 
			
		||||
    determineNewState?(component: string, attemptId: number, question: any, siteId?: string)
 | 
			
		||||
        : CoreQuestionState | Promise<CoreQuestionState>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handle a question behaviour.
 | 
			
		||||
     * If the behaviour requires a submit button, it should add it to question.behaviourButtons.
 | 
			
		||||
     * If the behaviour requires to show some extra data, it should return the components to render it.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @return {any[]|Promise<any[]>} Components (or promise resolved with components) to render some extra data in the question
 | 
			
		||||
     *                                (e.g. certainty options). Don't return anything if no extra data is required.
 | 
			
		||||
     */
 | 
			
		||||
    handleQuestion?(question: any): any[] | Promise<any[]>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Delegate to register question behaviour handlers.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class CoreQuestionBehaviourDelegate extends CoreDelegate {
 | 
			
		||||
 | 
			
		||||
    protected handlerNameProperty = 'type';
 | 
			
		||||
 | 
			
		||||
    constructor(logger: CoreLoggerProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider,
 | 
			
		||||
            protected questionDelegate: CoreQuestionDelegate, protected defaultHandler: CoreQuestionBehaviourDefaultHandler) {
 | 
			
		||||
        super('CoreQuestionBehaviourDelegate', logger, sitesProvider, eventsProvider);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Determine a question new state based on its answer(s).
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} component Component the question belongs to.
 | 
			
		||||
     * @param {number} attemptId Attempt ID the question belongs to.
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @param {string} [siteId] Site ID. If not defined, current site.
 | 
			
		||||
     * @return {Promise<CoreQuestionState>} Promise resolved with state.
 | 
			
		||||
     */
 | 
			
		||||
    determineNewState(behaviour: string, component: string, attemptId: number, question: any, siteId?: string)
 | 
			
		||||
            : Promise<CoreQuestionState> {
 | 
			
		||||
        behaviour = this.questionDelegate.getBehaviourForQuestion(question, behaviour);
 | 
			
		||||
 | 
			
		||||
        return Promise.resolve(this.executeFunctionOnEnabled(behaviour, 'determineNewState',
 | 
			
		||||
                [component, attemptId, question, siteId]));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handle a question behaviour.
 | 
			
		||||
     * If the behaviour requires a submit button, it should add it to question.behaviourButtons.
 | 
			
		||||
     * If the behaviour requires to show some extra data, it should return a directive to render it.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} behaviour Default behaviour.
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @return {Promise<any[]>} Promise resolved with components to render some extra data in the question.
 | 
			
		||||
     */
 | 
			
		||||
    handleQuestion(behaviour: string, question: any): Promise<any[]> {
 | 
			
		||||
        behaviour = this.questionDelegate.getBehaviourForQuestion(question, behaviour);
 | 
			
		||||
 | 
			
		||||
        return Promise.resolve(this.executeFunctionOnEnabled(behaviour, 'handleQuestion', [question]));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a question behaviour is supported.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} behaviour Name of the behaviour.
 | 
			
		||||
     * @return {boolean} Whether it's supported.
 | 
			
		||||
     */
 | 
			
		||||
    isBehaviourSupported(behaviour: string): boolean {
 | 
			
		||||
        return this.hasHandler(behaviour, true);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								src/core/question/providers/default-behaviour-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/core/question/providers/default-behaviour-handler.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,66 @@
 | 
			
		||||
// (C) Copyright 2015 Martin Dougiamas
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreQuestionBehaviourHandler } from './behaviour-delegate';
 | 
			
		||||
import { CoreQuestionProvider, CoreQuestionState } from '@core/question/providers/question';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Default handler used when the question behaviour doesn't have a specific implementation.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class CoreQuestionBehaviourDefaultHandler implements CoreQuestionBehaviourHandler {
 | 
			
		||||
    name = 'CoreQuestionBehaviourDefault';
 | 
			
		||||
    type = 'default';
 | 
			
		||||
 | 
			
		||||
    constructor(private questionProvider: CoreQuestionProvider) { }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Determine a question new state based on its answer(s).
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} component Component the question belongs to.
 | 
			
		||||
     * @param {number} attemptId Attempt ID the question belongs to.
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @param {string} [siteId] Site ID. If not defined, current site.
 | 
			
		||||
     * @return {CoreQuestionState|Promise<CoreQuestionState>} New state (or promise resolved with state).
 | 
			
		||||
     */
 | 
			
		||||
    determineNewState(component: string, attemptId: number, question: any, siteId?: string)
 | 
			
		||||
            : CoreQuestionState | Promise<CoreQuestionState> {
 | 
			
		||||
        // Return the current state.
 | 
			
		||||
        return this.questionProvider.getState(question.state);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handle a question behaviour.
 | 
			
		||||
     * If the behaviour requires a submit button, it should add it to question.behaviourButtons.
 | 
			
		||||
     * If the behaviour requires to show some extra data, it should return the components to render it.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @return {any[]|Promise<any[]>} Components (or promise resolved with components) to render some extra data in the question
 | 
			
		||||
     *                                (e.g. certainty options). Don't return anything if no extra data is required.
 | 
			
		||||
     */
 | 
			
		||||
    handleQuestion(question: any): any[] | Promise<any[]> {
 | 
			
		||||
        // Nothing to do.
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether or not the handler is enabled on a site level.
 | 
			
		||||
     *
 | 
			
		||||
     * @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
 | 
			
		||||
     */
 | 
			
		||||
    isEnabled(): boolean | Promise<boolean> {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										134
									
								
								src/core/question/providers/default-question-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								src/core/question/providers/default-question-handler.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,134 @@
 | 
			
		||||
// (C) Copyright 2015 Martin Dougiamas
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injectable, Injector } from '@angular/core';
 | 
			
		||||
import { CoreQuestionHandler } from './delegate';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Default handler used when the question type doesn't have a specific implementation.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class CoreQuestionDefaultHandler implements CoreQuestionHandler {
 | 
			
		||||
    name = 'CoreQuestionDefault';
 | 
			
		||||
    type = 'default';
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        // Nothing to do.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether or not the handler is enabled on a site level.
 | 
			
		||||
     *
 | 
			
		||||
     * @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
 | 
			
		||||
     */
 | 
			
		||||
    isEnabled(): boolean | Promise<boolean> {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the Component to use to display the question.
 | 
			
		||||
     * It's recommended to return the class of the component, but you can also return an instance of the component.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {Injector} injector Injector.
 | 
			
		||||
     * @param {any} question The question to render.
 | 
			
		||||
     * @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
 | 
			
		||||
     */
 | 
			
		||||
    getComponent(injector: Injector, question: any): any | Promise<any> {
 | 
			
		||||
        // There is no default component for questions.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the name of the behaviour to use for the question.
 | 
			
		||||
     * If the question should use the default behaviour you shouldn't implement this function.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @param {string} behaviour The default behaviour.
 | 
			
		||||
     * @return {string} The behaviour to use.
 | 
			
		||||
     */
 | 
			
		||||
    getBehaviour(question: any, behaviour: string): string {
 | 
			
		||||
        return behaviour;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a question can be submitted.
 | 
			
		||||
     * If a question cannot be submitted it should return a message explaining why (translated or not).
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @return {string} Prevent submit message. Undefined or empty if can be submitted.
 | 
			
		||||
     */
 | 
			
		||||
    getPreventSubmitMessage(question: any): string {
 | 
			
		||||
        // Never prevent by default.
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a response is complete.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @param {any} answers Object with the question answers (without prefix).
 | 
			
		||||
     * @return {number} 1 if complete, 0 if not complete, -1 if cannot determine.
 | 
			
		||||
     */
 | 
			
		||||
    isCompleteResponse(question: any, answers: any): number {
 | 
			
		||||
        return -1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a student has provided enough of an answer for the question to be graded automatically,
 | 
			
		||||
     * or whether it must be considered aborted.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @param {any} answers Object with the question answers (without prefix).
 | 
			
		||||
     * @return {number} 1 if gradable, 0 if not gradable, -1 if cannot determine.
 | 
			
		||||
     */
 | 
			
		||||
    isGradableResponse(question: any, answers: any): number {
 | 
			
		||||
        return -1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if two responses are the same.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question Question.
 | 
			
		||||
     * @param {any} prevAnswers Object with the previous question answers.
 | 
			
		||||
     * @param {any} newAnswers Object with the new question answers.
 | 
			
		||||
     * @return {boolean} Whether they're the same.
 | 
			
		||||
     */
 | 
			
		||||
    isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prepare and add to answers the data to send to server based in the input. Return promise if async.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question Question.
 | 
			
		||||
     * @param {any} answers The answers retrieved from the form. Prepared answers must be stored in this object.
 | 
			
		||||
     * @param {boolean} [offline] Whether the data should be saved in offline.
 | 
			
		||||
     * @param {string} [siteId] Site ID. If not defined, current site.
 | 
			
		||||
     * @return {void|Promise<any>} Return a promise resolved when done if async, void if sync.
 | 
			
		||||
     */
 | 
			
		||||
    prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise<any> {
 | 
			
		||||
        // Nothing to do.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Validate if an offline sequencecheck is valid compared with the online one.
 | 
			
		||||
     * This function only needs to be implemented if a specific compare is required.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @param {string} offlineSequenceCheck Sequence check stored in offline.
 | 
			
		||||
     * @return {boolean} Whether sequencecheck is valid.
 | 
			
		||||
     */
 | 
			
		||||
    validateSequenceCheck(question: any, offlineSequenceCheck: string): boolean {
 | 
			
		||||
        return question.sequencecheck == offlineSequenceCheck;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										264
									
								
								src/core/question/providers/delegate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								src/core/question/providers/delegate.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,264 @@
 | 
			
		||||
// (C) Copyright 2015 Martin Dougiamas
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injectable, Injector } from '@angular/core';
 | 
			
		||||
import { CoreLoggerProvider } from '@providers/logger';
 | 
			
		||||
import { CoreEventsProvider } from '@providers/events';
 | 
			
		||||
import { CoreSitesProvider } from '@providers/sites';
 | 
			
		||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
 | 
			
		||||
import { CoreQuestionDefaultHandler } from './default-question-handler';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Interface that all question type handlers must implement.
 | 
			
		||||
 */
 | 
			
		||||
export interface CoreQuestionHandler extends CoreDelegateHandler {
 | 
			
		||||
    /**
 | 
			
		||||
     * Type of the question the handler supports. E.g. 'qtype_calculated'.
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     */
 | 
			
		||||
    type: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the Component to use to display the question.
 | 
			
		||||
     * It's recommended to return the class of the component, but you can also return an instance of the component.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {Injector} injector Injector.
 | 
			
		||||
     * @param {any} question The question to render.
 | 
			
		||||
     * @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
 | 
			
		||||
     */
 | 
			
		||||
    getComponent(injector: Injector, question: any): any | Promise<any>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the name of the behaviour to use for the question.
 | 
			
		||||
     * If the question should use the default behaviour you shouldn't implement this function.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @param {string} behaviour The default behaviour.
 | 
			
		||||
     * @return {string} The behaviour to use.
 | 
			
		||||
     */
 | 
			
		||||
    getBehaviour?(question: any, behaviour: string): string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a question can be submitted.
 | 
			
		||||
     * If a question cannot be submitted it should return a message explaining why (translated or not).
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @return {string} Prevent submit message. Undefined or empty if can be submitted.
 | 
			
		||||
     */
 | 
			
		||||
    getPreventSubmitMessage?(question: any): string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a response is complete.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @param {any} answers Object with the question answers (without prefix).
 | 
			
		||||
     * @return {number} 1 if complete, 0 if not complete, -1 if cannot determine.
 | 
			
		||||
     */
 | 
			
		||||
    isCompleteResponse?(question: any, answers: any): number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a student has provided enough of an answer for the question to be graded automatically,
 | 
			
		||||
     * or whether it must be considered aborted.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @param {any} answers Object with the question answers (without prefix).
 | 
			
		||||
     * @return {number} 1 if gradable, 0 if not gradable, -1 if cannot determine.
 | 
			
		||||
     */
 | 
			
		||||
    isGradableResponse?(question: any, answers: any): number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if two responses are the same.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question Question.
 | 
			
		||||
     * @param {any} prevAnswers Object with the previous question answers.
 | 
			
		||||
     * @param {any} newAnswers Object with the new question answers.
 | 
			
		||||
     * @return {boolean} Whether they're the same.
 | 
			
		||||
     */
 | 
			
		||||
    isSameResponse?(question: any, prevAnswers: any, newAnswers: any): boolean;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prepare and add to answers the data to send to server based in the input. Return promise if async.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question Question.
 | 
			
		||||
     * @param {any} answers The answers retrieved from the form. Prepared answers must be stored in this object.
 | 
			
		||||
     * @param {boolean} [offline] Whether the data should be saved in offline.
 | 
			
		||||
     * @param {string} [siteId] Site ID. If not defined, current site.
 | 
			
		||||
     * @return {void|Promise<any>} Return a promise resolved when done if async, void if sync.
 | 
			
		||||
     */
 | 
			
		||||
    prepareAnswers?(question: any, answers: any, offline: boolean, siteId?: string): void | Promise<any>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Validate if an offline sequencecheck is valid compared with the online one.
 | 
			
		||||
     * This function only needs to be implemented if a specific compare is required.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @param {string} offlineSequenceCheck Sequence check stored in offline.
 | 
			
		||||
     * @return {boolean} Whether sequencecheck is valid.
 | 
			
		||||
     */
 | 
			
		||||
    validateSequenceCheck?(question: any, offlineSequenceCheck: string): boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Delegate to register question handlers.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class CoreQuestionDelegate extends CoreDelegate {
 | 
			
		||||
 | 
			
		||||
    protected handlerNameProperty = 'type';
 | 
			
		||||
 | 
			
		||||
    constructor(logger: CoreLoggerProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider,
 | 
			
		||||
            protected defaultHandler: CoreQuestionDefaultHandler) {
 | 
			
		||||
        super('CoreQuestionDelegate', logger, sitesProvider, eventsProvider);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the behaviour to use for a certain question type.
 | 
			
		||||
     * E.g. 'qtype_essay' uses 'manualgraded'.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @param {string} behaviour The default behaviour.
 | 
			
		||||
     * @return {string} The behaviour to use.
 | 
			
		||||
     */
 | 
			
		||||
    getBehaviourForQuestion(question: any, behaviour: string): string {
 | 
			
		||||
        const type = this.getTypeName(question),
 | 
			
		||||
            questionBehaviour = this.executeFunctionOnEnabled(type, 'getBehaviour', [question, behaviour]);
 | 
			
		||||
 | 
			
		||||
        return questionBehaviour || behaviour;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the directive to use for a certain question type.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {Injector} injector Injector.
 | 
			
		||||
     * @param {any} question The question to render.
 | 
			
		||||
     * @return {Promise<any>} Promise resolved with component to use, undefined if not found.
 | 
			
		||||
     */
 | 
			
		||||
    getComponentForQuestion(injector: Injector, question: any): Promise<any> {
 | 
			
		||||
        const type = this.getTypeName(question);
 | 
			
		||||
 | 
			
		||||
        return Promise.resolve(this.executeFunctionOnEnabled(type, 'getComponent', [injector, question]));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a question can be submitted.
 | 
			
		||||
     * If a question cannot be submitted it should return a message explaining why (translated or not).
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question Question.
 | 
			
		||||
     * @return {string} Prevent submit message. Undefined or empty if can be submitted.
 | 
			
		||||
     */
 | 
			
		||||
    getPreventSubmitMessage(question: any): string {
 | 
			
		||||
        const type = this.getTypeName(question);
 | 
			
		||||
 | 
			
		||||
        return this.executeFunctionOnEnabled(type, 'getPreventSubmitMessage', [question]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a type name, return the full name of that type. E.g. 'calculated' -> 'qtype_calculated'.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} type Type to treat.
 | 
			
		||||
     * @return {string} Type full name.
 | 
			
		||||
     */
 | 
			
		||||
    protected getFullTypeName(type: string): string {
 | 
			
		||||
        return 'qtype_' + type;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a question, return the full name of its question type.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question Question.
 | 
			
		||||
     * @return {string} Type name.
 | 
			
		||||
     */
 | 
			
		||||
    protected getTypeName(question: any): string {
 | 
			
		||||
        return this.getFullTypeName(question.type);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a response is complete.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @param {any} answers Object with the question answers (without prefix).
 | 
			
		||||
     * @return {number} 1 if complete, 0 if not complete, -1 if cannot determine.
 | 
			
		||||
     */
 | 
			
		||||
    isCompleteResponse(question: any, answers: any): number {
 | 
			
		||||
        const type = this.getTypeName(question);
 | 
			
		||||
 | 
			
		||||
        return this.executeFunctionOnEnabled(type, 'isCompleteResponse', [question, answers]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a student has provided enough of an answer for the question to be graded automatically,
 | 
			
		||||
     * or whether it must be considered aborted.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @param {any} answers Object with the question answers (without prefix).
 | 
			
		||||
     * @return {number} 1 if gradable, 0 if not gradable, -1 if cannot determine.
 | 
			
		||||
     */
 | 
			
		||||
    isGradableResponse(question: any, answers: any): number {
 | 
			
		||||
        const type = this.getTypeName(question);
 | 
			
		||||
 | 
			
		||||
        return this.executeFunctionOnEnabled(type, 'isGradableResponse', [question, answers]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if two responses are the same.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question Question.
 | 
			
		||||
     * @param {any} prevAnswers Object with the previous question answers.
 | 
			
		||||
     * @param {any} newAnswers Object with the new question answers.
 | 
			
		||||
     * @return {boolean} Whether they're the same.
 | 
			
		||||
     */
 | 
			
		||||
    isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
 | 
			
		||||
        const type = this.getTypeName(question);
 | 
			
		||||
 | 
			
		||||
        return this.executeFunctionOnEnabled(type, 'isSameResponse', [question, prevAnswers, newAnswers]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a question type is supported.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} type Question type.
 | 
			
		||||
     * @return {boolean} Whether it's supported.
 | 
			
		||||
     */
 | 
			
		||||
    isQuestionSupported(type: string): boolean {
 | 
			
		||||
        return this.hasHandler(this.getFullTypeName(type), true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prepare the answers for a certain question.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question Question.
 | 
			
		||||
     * @param {any} answers The answers retrieved from the form. Prepared answers must be stored in this object.
 | 
			
		||||
     * @param {boolean} [offline] Whether the data should be saved in offline.
 | 
			
		||||
     * @param {string} [siteId] Site ID. If not defined, current site.
 | 
			
		||||
     * @return {Promise<any>} Promise resolved when data has been prepared.
 | 
			
		||||
     */
 | 
			
		||||
    prepareAnswersForQuestion(question: any, answers: any, offline: boolean, siteId?: string): Promise<any> {
 | 
			
		||||
        const type = this.getTypeName(question);
 | 
			
		||||
 | 
			
		||||
        return Promise.resolve(this.executeFunctionOnEnabled(type, 'prepareAnswers', [question, answers, offline, siteId]));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Validate if an offline sequencecheck is valid compared with the online one.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question The question.
 | 
			
		||||
     * @param {string} offlineSequenceCheck Sequence check stored in offline.
 | 
			
		||||
     * @return {boolean} Whether sequencecheck is valid.
 | 
			
		||||
     */
 | 
			
		||||
    validateSequenceCheck(question: any, offlineSequenceCheck: string): boolean {
 | 
			
		||||
        const type = this.getTypeName(question);
 | 
			
		||||
 | 
			
		||||
        return this.executeFunctionOnEnabled(type, 'validateSequenceCheck', [question, offlineSequenceCheck]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										306
									
								
								src/core/question/providers/helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										306
									
								
								src/core/question/providers/helper.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,306 @@
 | 
			
		||||
// (C) Copyright 2015 Martin Dougiamas
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injectable, EventEmitter } from '@angular/core';
 | 
			
		||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
 | 
			
		||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
 | 
			
		||||
import { CoreQuestionProvider } from './question';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service with some common functions to handle questions.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class CoreQuestionHelperProvider {
 | 
			
		||||
    protected lastErrorShown = 0;
 | 
			
		||||
    protected div = document.createElement('div'); // A div element to search in HTML code.
 | 
			
		||||
 | 
			
		||||
    constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider,
 | 
			
		||||
        private questionProvider: CoreQuestionProvider) { }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a behaviour button to the question's "behaviourButtons" property.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question Question.
 | 
			
		||||
     * @param {HTMLInputElement} button Behaviour button (DOM element).
 | 
			
		||||
     */
 | 
			
		||||
    protected addBehaviourButton(question: any, button: HTMLInputElement): void {
 | 
			
		||||
        if (!button || !question) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!question.behaviourButtons) {
 | 
			
		||||
            question.behaviourButtons = [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Extract the data we want.
 | 
			
		||||
        question.behaviourButtons.push({
 | 
			
		||||
            id: button.id,
 | 
			
		||||
            name: button.name,
 | 
			
		||||
            value: button.value,
 | 
			
		||||
            disabled: button.disabled
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the question has a redo button and, if so, add it to "behaviourButtons" property
 | 
			
		||||
     * and remove it from the HTML.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question Question to treat.
 | 
			
		||||
     */
 | 
			
		||||
    extractQbehaviourRedoButton(question: any): void {
 | 
			
		||||
        // Create a fake div element so we can search using querySelector.
 | 
			
		||||
        const redoSelector = 'input[type="submit"][name*=redoslot], input[type="submit"][name*=tryagain]';
 | 
			
		||||
 | 
			
		||||
        // Search redo button in feedback.
 | 
			
		||||
        if (!this.searchBehaviourButton(question, 'html', '.outcome ' + redoSelector)) {
 | 
			
		||||
            // Not found in question HTML.
 | 
			
		||||
            if (question.feedbackHtml) {
 | 
			
		||||
                // We extracted the feedback already, search it in there.
 | 
			
		||||
                if (this.searchBehaviourButton(question, 'feedbackHtml', redoSelector)) {
 | 
			
		||||
                    // Button found, stop.
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Button still not found. Now search in the info box if it exists.
 | 
			
		||||
            if (question.infoHtml) {
 | 
			
		||||
                this.searchBehaviourButton(question, 'infoHtml', redoSelector);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Removes the comment from the question HTML code and adds it in a new "commentHtml" property.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question Question.
 | 
			
		||||
     */
 | 
			
		||||
    extractQuestionComment(question: any): void {
 | 
			
		||||
        this.extractQuestionLastElementNotInContent(question, '.comment', 'commentHtml');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Removes the feedback from the question HTML code and adds it in a new "feedbackHtml" property.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question Question.
 | 
			
		||||
     */
 | 
			
		||||
    extractQuestionFeedback(question: any): void {
 | 
			
		||||
        this.extractQuestionLastElementNotInContent(question, '.outcome', 'feedbackHtml');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Extracts the info box from a question and add it to an "infoHtml" property.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question Question.
 | 
			
		||||
     * @param {string} selector Selector to search the element.
 | 
			
		||||
     */
 | 
			
		||||
    extractQuestionInfoBox(question: any, selector: string): void {
 | 
			
		||||
        this.extractQuestionLastElementNotInContent(question, selector, 'infoHtml');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Searches the last occurrence of a certain element and check it's not in the question contents.
 | 
			
		||||
     * If found, removes it from the question HTML and adds it to a new property inside question.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question Question.
 | 
			
		||||
     * @param {string} selector Selector to search the element.
 | 
			
		||||
     * @param {string} attrName Name of the attribute to store the HTML in.
 | 
			
		||||
     */
 | 
			
		||||
    protected extractQuestionLastElementNotInContent(question: any, selector: string, attrName: string): void {
 | 
			
		||||
        this.div.innerHTML = question.html;
 | 
			
		||||
 | 
			
		||||
        const matches = <HTMLElement[]> Array.from(this.div.querySelectorAll(selector));
 | 
			
		||||
 | 
			
		||||
        // Get the last element and check it's not in the question contents.
 | 
			
		||||
        let last = matches.pop();
 | 
			
		||||
        while (last) {
 | 
			
		||||
            if (!this.domUtils.closest(last, '.formulation')) {
 | 
			
		||||
                // Not in question contents. Add it to a separate attribute and remove it from the HTML.
 | 
			
		||||
                question[attrName] = last.innerHTML;
 | 
			
		||||
                last.parentElement.removeChild(last);
 | 
			
		||||
                question.html = this.div.innerHTML;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // It's inside the question content, treat next element.
 | 
			
		||||
            last = matches.pop();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Removes the scripts from a question's HTML and adds it in a new 'scriptsCode' property.
 | 
			
		||||
     * It will also search for init_question functions of the question type and add the object to an 'initObjects' property.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question Question.
 | 
			
		||||
     */
 | 
			
		||||
    extractQuestionScripts(question: any): void {
 | 
			
		||||
        question.scriptsCode = '';
 | 
			
		||||
        question.initObjects = [];
 | 
			
		||||
 | 
			
		||||
        if (question.html) {
 | 
			
		||||
            // Search the scripts.
 | 
			
		||||
            const matches = question.html.match(/<script[^>]*>[\s\S]*?<\/script>/mg);
 | 
			
		||||
            if (!matches) {
 | 
			
		||||
                // No scripts, stop.
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            matches.forEach((match: string) => {
 | 
			
		||||
                // Add the script to scriptsCode and remove it from html.
 | 
			
		||||
                question.scriptsCode += match;
 | 
			
		||||
                question.html = question.html.replace(match, '');
 | 
			
		||||
 | 
			
		||||
                // Search init_question functions for this type.
 | 
			
		||||
                const initMatches = match.match(new RegExp('M\.qtype_' + question.type + '\.init_question\\(.*?}\\);', 'mg'));
 | 
			
		||||
                if (initMatches) {
 | 
			
		||||
                    let initMatch = initMatches.pop();
 | 
			
		||||
 | 
			
		||||
                    // Remove start and end of the match, we only want the object.
 | 
			
		||||
                    initMatch = initMatch.replace('M.qtype_' + question.type + '.init_question(', '');
 | 
			
		||||
                    initMatch = initMatch.substr(0, initMatch.length - 2);
 | 
			
		||||
 | 
			
		||||
                    // Try to convert it to an object and add it to the question.
 | 
			
		||||
                    question.initObjects = this.textUtils.parseJSON(initMatch);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the sequence check from a question HTML.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} html Question's HTML.
 | 
			
		||||
     * @return {{name: string, value: string}} Object with the sequencecheck name and value.
 | 
			
		||||
     */
 | 
			
		||||
    getQuestionSequenceCheckFromHtml(html: string): {name: string, value: string} {
 | 
			
		||||
        if (html) {
 | 
			
		||||
            this.div.innerHTML = html;
 | 
			
		||||
 | 
			
		||||
            // Search the input holding the sequencecheck.
 | 
			
		||||
            const input = <HTMLInputElement> this.div.querySelector('input[name*=sequencecheck]');
 | 
			
		||||
            if (input && typeof input.name != 'undefined' && typeof input.value != 'undefined') {
 | 
			
		||||
                return {
 | 
			
		||||
                    name: input.name,
 | 
			
		||||
                    value: input.value
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the validation error message from a question HTML if it's there.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} html Question's HTML.
 | 
			
		||||
     * @return {string} Validation error message if present.
 | 
			
		||||
     */
 | 
			
		||||
    getValidationErrorFromHtml(html: string): string {
 | 
			
		||||
        this.div.innerHTML = html;
 | 
			
		||||
 | 
			
		||||
        return this.domUtils.getContentsOfElement(this.div, '.validationerror');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * For each input element found in the HTML, search if there's a local answer stored and
 | 
			
		||||
     * override the HTML's value with the local one.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question Question.
 | 
			
		||||
     */
 | 
			
		||||
    loadLocalAnswersInHtml(question: any): void {
 | 
			
		||||
        const form = document.createElement('form');
 | 
			
		||||
        form.innerHTML = question.html;
 | 
			
		||||
 | 
			
		||||
        // Search all input elements.
 | 
			
		||||
        Array.from(form.elements).forEach((element: HTMLInputElement | HTMLButtonElement) => {
 | 
			
		||||
            let name = element.name || '';
 | 
			
		||||
            // Ignore flag and submit inputs.
 | 
			
		||||
            if (!name || name.match(/_:flagged$/) || element.type == 'submit' || element.tagName == 'BUTTON') {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Search if there's a local answer.
 | 
			
		||||
            name = this.questionProvider.removeQuestionPrefix(name);
 | 
			
		||||
            if (question.localAnswers && typeof question.localAnswers[name] != 'undefined') {
 | 
			
		||||
 | 
			
		||||
                if (element.tagName == 'TEXTAREA') {
 | 
			
		||||
                    // Just put the answer inside the textarea.
 | 
			
		||||
                    element.innerHTML = question.localAnswers[name];
 | 
			
		||||
                } else if (element.tagName == 'SELECT') {
 | 
			
		||||
                    // Search the selected option and select it.
 | 
			
		||||
                    const selected = element.querySelector('option[value="' + question.localAnswers[name] + '"]');
 | 
			
		||||
                    if (selected) {
 | 
			
		||||
                        selected.setAttribute('selected', 'selected');
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (element.type == 'radio' || element.type == 'checkbox') {
 | 
			
		||||
                    // Check if this radio or checkbox is selected.
 | 
			
		||||
                    if (element.value == question.localAnswers[name]) {
 | 
			
		||||
                        element.setAttribute('checked', 'checked');
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Put the answer in the value.
 | 
			
		||||
                    element.setAttribute('value', question.localAnswers[name]);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Update the question HTML.
 | 
			
		||||
        question.html = form.innerHTML;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Search a behaviour button in a certain question property containing HTML.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} question Question.
 | 
			
		||||
     * @param {string} htmlProperty The name of the property containing the HTML to search.
 | 
			
		||||
     * @param {string} selector The selector to find the button.
 | 
			
		||||
     * @return {boolean} Whether the button is found.
 | 
			
		||||
     */
 | 
			
		||||
    protected searchBehaviourButton(question: any, htmlProperty: string, selector: string): boolean {
 | 
			
		||||
        this.div.innerHTML = question[htmlProperty];
 | 
			
		||||
 | 
			
		||||
        const button = <HTMLInputElement> this.div.querySelector(selector);
 | 
			
		||||
        if (button) {
 | 
			
		||||
            // Add a behaviour button to the question's "behaviourButtons" property.
 | 
			
		||||
            this.addBehaviourButton(question, button);
 | 
			
		||||
 | 
			
		||||
            // Remove the button from the HTML.
 | 
			
		||||
            button.parentElement.removeChild(button);
 | 
			
		||||
 | 
			
		||||
            // Update the question's html.
 | 
			
		||||
            question[htmlProperty] = this.div.innerHTML;
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convenience function to show a parsing error and abort.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {EventEmitter<void>} [onAbort] If supplied, will emit an event.
 | 
			
		||||
     * @param {string} [error] Error to show.
 | 
			
		||||
     */
 | 
			
		||||
    showComponentError(onAbort: EventEmitter<void>, error?: string): void {
 | 
			
		||||
        error = error || 'Error processing the question. This could be caused by custom modifications in your site.';
 | 
			
		||||
 | 
			
		||||
        // Prevent consecutive errors.
 | 
			
		||||
        const now = Date.now();
 | 
			
		||||
        if (now - this.lastErrorShown > 500) {
 | 
			
		||||
            this.lastErrorShown = now;
 | 
			
		||||
            this.domUtils.showErrorModal(error);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        onAbort && onAbort.emit();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										613
									
								
								src/core/question/providers/question.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										613
									
								
								src/core/question/providers/question.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,613 @@
 | 
			
		||||
// (C) Copyright 2015 Martin Dougiamas
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreLoggerProvider } from '@providers/logger';
 | 
			
		||||
import { CoreSitesProvider } from '@providers/sites';
 | 
			
		||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
 | 
			
		||||
import { CoreUtilsProvider } from '@providers/utils/utils';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An object to represent a question state.
 | 
			
		||||
 */
 | 
			
		||||
export interface CoreQuestionState {
 | 
			
		||||
    /**
 | 
			
		||||
     * Name of the state.
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     */
 | 
			
		||||
    name: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Class of the state.
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     */
 | 
			
		||||
    class: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The string key to translate the status.
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     */
 | 
			
		||||
    status: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether the question with this state is active.
 | 
			
		||||
     * @type {boolean}
 | 
			
		||||
     */
 | 
			
		||||
    active: boolean;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether the question with this state is finished.
 | 
			
		||||
     * @type {boolean}
 | 
			
		||||
     */
 | 
			
		||||
    finished: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service to handle questions.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class CoreQuestionProvider {
 | 
			
		||||
    // Variables for database.
 | 
			
		||||
    protected QUESTION_TABLE = 'questions';
 | 
			
		||||
    protected QUESTION_ANSWERS_TABLE = 'question_answers';
 | 
			
		||||
    protected tablesSchema = [
 | 
			
		||||
        {
 | 
			
		||||
            name: this.QUESTION_TABLE,
 | 
			
		||||
            columns: [
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'component',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                    notNull: true
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'attemptId',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                    notNull: true
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'slot',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                    notNull: true
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'componentId',
 | 
			
		||||
                    type: 'INTEGER'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'userId',
 | 
			
		||||
                    type: 'INTEGER'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'number',
 | 
			
		||||
                    type: 'INTEGER'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'state',
 | 
			
		||||
                    type: 'TEXT'
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            primaryKeys: ['component', 'attemptId', 'slot']
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: this.QUESTION_ANSWERS_TABLE,
 | 
			
		||||
            columns: [
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'component',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                    notNull: true
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'attemptId',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                    notNull: true
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'name',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                    notNull: true
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'componentId',
 | 
			
		||||
                    type: 'INTEGER'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'userId',
 | 
			
		||||
                    type: 'INTEGER'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'questionSlot',
 | 
			
		||||
                    type: 'INTEGER'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'state',
 | 
			
		||||
                    type: 'TEXT'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'value',
 | 
			
		||||
                    type: 'TEXT'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timemodified',
 | 
			
		||||
                    type: 'INTEGER'
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            primaryKeys: ['component', 'attemptId', 'name']
 | 
			
		||||
        }
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    protected QUESTION_PREFIX_REGEX = /q\d+:(\d+)_/;
 | 
			
		||||
    protected STATES: {[name: string]: CoreQuestionState} = {
 | 
			
		||||
        todo: {
 | 
			
		||||
            name: 'todo',
 | 
			
		||||
            class: 'core-question-notyetanswered',
 | 
			
		||||
            status: 'notyetanswered',
 | 
			
		||||
            active: true,
 | 
			
		||||
            finished: false
 | 
			
		||||
        },
 | 
			
		||||
        invalid: {
 | 
			
		||||
            name: 'invalid',
 | 
			
		||||
            class: 'core-question-invalidanswer',
 | 
			
		||||
            status: 'invalidanswer',
 | 
			
		||||
            active: true,
 | 
			
		||||
            finished: false
 | 
			
		||||
        },
 | 
			
		||||
        complete: {
 | 
			
		||||
            name: 'complete',
 | 
			
		||||
            class: 'core-question-answersaved',
 | 
			
		||||
            status: 'answersaved',
 | 
			
		||||
            active: true,
 | 
			
		||||
            finished: false
 | 
			
		||||
        },
 | 
			
		||||
        needsgrading: {
 | 
			
		||||
            name: 'needsgrading',
 | 
			
		||||
            class: 'core-question-requiresgrading',
 | 
			
		||||
            status: 'requiresgrading',
 | 
			
		||||
            active: false,
 | 
			
		||||
            finished: true
 | 
			
		||||
        },
 | 
			
		||||
        finished: {
 | 
			
		||||
            name: 'finished',
 | 
			
		||||
            class: 'core-question-complete',
 | 
			
		||||
            status: 'complete',
 | 
			
		||||
            active: false,
 | 
			
		||||
            finished: true
 | 
			
		||||
        },
 | 
			
		||||
        gaveup: {
 | 
			
		||||
            name: 'gaveup',
 | 
			
		||||
            class: 'core-question-notanswered',
 | 
			
		||||
            status: 'notanswered',
 | 
			
		||||
            active: false,
 | 
			
		||||
            finished: true
 | 
			
		||||
        },
 | 
			
		||||
        gradedwrong: {
 | 
			
		||||
            name: 'gradedwrong',
 | 
			
		||||
            class: 'core-question-incorrect',
 | 
			
		||||
            status: 'incorrect',
 | 
			
		||||
            active: false,
 | 
			
		||||
            finished: true
 | 
			
		||||
        },
 | 
			
		||||
        gradedpartial: {
 | 
			
		||||
            name: 'gradedpartial',
 | 
			
		||||
            class: 'core-question-partiallycorrect',
 | 
			
		||||
            status: 'partiallycorrect',
 | 
			
		||||
            active: false,
 | 
			
		||||
            finished: true
 | 
			
		||||
        },
 | 
			
		||||
        gradedright: {
 | 
			
		||||
            name: 'gradedright',
 | 
			
		||||
            class: 'core-question-correct',
 | 
			
		||||
            status: 'correct',
 | 
			
		||||
            active: false,
 | 
			
		||||
            finished: true
 | 
			
		||||
        },
 | 
			
		||||
        mangrwrong: {
 | 
			
		||||
            name: 'mangrwrong',
 | 
			
		||||
            class: 'core-question-incorrect',
 | 
			
		||||
            status: 'incorrect',
 | 
			
		||||
            active: false,
 | 
			
		||||
            finished: true
 | 
			
		||||
        },
 | 
			
		||||
        mangrpartial: {
 | 
			
		||||
            name: 'mangrpartial',
 | 
			
		||||
            class: 'core-question-partiallycorrect',
 | 
			
		||||
            status: 'partiallycorrect',
 | 
			
		||||
            active: false,
 | 
			
		||||
            finished: true
 | 
			
		||||
        },
 | 
			
		||||
        mangrright: {
 | 
			
		||||
            name: 'mangrright',
 | 
			
		||||
            class: 'core-question-correct',
 | 
			
		||||
            status: 'correct',
 | 
			
		||||
            active: false,
 | 
			
		||||
            finished: true
 | 
			
		||||
        },
 | 
			
		||||
        unknown: { // Special state for Mobile, sometimes we won't have enough data to detemrine the state.
 | 
			
		||||
            name: 'unknown',
 | 
			
		||||
            class: 'core-question-unknown',
 | 
			
		||||
            status: 'unknown',
 | 
			
		||||
            active: true,
 | 
			
		||||
            finished: false
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    protected logger;
 | 
			
		||||
 | 
			
		||||
    constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider,
 | 
			
		||||
            private utils: CoreUtilsProvider) {
 | 
			
		||||
        this.logger = logger.getInstance('CoreQuestionProvider');
 | 
			
		||||
        this.sitesProvider.createTablesFromSchema(this.tablesSchema);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Compare that all the answers in two objects are equal, except some extra data like sequencecheck or certainty.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} prevAnswers Object with previous answers.
 | 
			
		||||
     * @param {any} newAnswers Object with new answers.
 | 
			
		||||
     * @return {boolean} Whether all answers are equal.
 | 
			
		||||
     */
 | 
			
		||||
    compareAllAnswers(prevAnswers: any, newAnswers: any): boolean {
 | 
			
		||||
        // Get all the keys.
 | 
			
		||||
        const keys = this.utils.mergeArraysWithoutDuplicates(Object.keys(prevAnswers), Object.keys(newAnswers));
 | 
			
		||||
 | 
			
		||||
        // Check that all the keys have the same value on both objects.
 | 
			
		||||
        for (const i in keys) {
 | 
			
		||||
            const key = keys[i];
 | 
			
		||||
 | 
			
		||||
            // Ignore extra answers like sequencecheck or certainty.
 | 
			
		||||
            if (!this.isExtraAnswer(key[0])) {
 | 
			
		||||
                if (!this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, key)) {
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert a list of answers retrieved from local DB to an object with name - value.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any[]} answers List of answers.
 | 
			
		||||
     * @param {boolean} [removePrefix] Whether to remove the prefix in the answer's name.
 | 
			
		||||
     * @return {any} Object with name -> value.
 | 
			
		||||
     */
 | 
			
		||||
    convertAnswersArrayToObject(answers: any[], removePrefix?: boolean): any {
 | 
			
		||||
        const result = {};
 | 
			
		||||
 | 
			
		||||
        answers.forEach((answer) => {
 | 
			
		||||
            if (removePrefix) {
 | 
			
		||||
                const nameWithoutPrefix = this.removeQuestionPrefix(answer.name);
 | 
			
		||||
                result[nameWithoutPrefix] = answer.value;
 | 
			
		||||
            } else {
 | 
			
		||||
                result[answer.name] = answer.value;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve an answer from site DB.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} component Component the attempt belongs to.
 | 
			
		||||
     * @param {number} attemptId Attempt ID.
 | 
			
		||||
     * @param {string} name Answer's name.
 | 
			
		||||
     * @param {string} [siteId] Site ID. If not defined, current site.
 | 
			
		||||
     * @return {Promise<any>} Promise resolved with the answer.
 | 
			
		||||
     */
 | 
			
		||||
    getAnswer(component: string, attemptId: number, name: string, siteId?: string): Promise<any> {
 | 
			
		||||
        return this.sitesProvider.getSite(siteId).then((site) => {
 | 
			
		||||
            return site.getDb().getRecord(this.QUESTION_ANSWERS_TABLE, {component, attemptId, name});
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve an attempt answers from site DB.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} component Component the attempt belongs to.
 | 
			
		||||
     * @param {number} attemptId Attempt ID.
 | 
			
		||||
     * @param {string} [siteId] Site ID. If not defined, current site.
 | 
			
		||||
     * @return {Promise<any[]>} Promise resolved with the answers.
 | 
			
		||||
     */
 | 
			
		||||
    getAttemptAnswers(component: string, attemptId: number, siteId?: string): Promise<any[]> {
 | 
			
		||||
        return this.sitesProvider.getSite(siteId).then((site) => {
 | 
			
		||||
            return site.getDb().getRecords(this.QUESTION_ANSWERS_TABLE, {component, attemptId});
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve an attempt questions from site DB.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} component Component the attempt belongs to.
 | 
			
		||||
     * @param {number} attemptId Attempt ID.
 | 
			
		||||
     * @param {string} [siteId] Site ID. If not defined, current site.
 | 
			
		||||
     * @return {Promise<any[]>} Promise resolved with the questions.
 | 
			
		||||
     */
 | 
			
		||||
    getAttemptQuestions(component: string, attemptId: number, siteId?: string): Promise<any[]> {
 | 
			
		||||
        return this.sitesProvider.getSite(siteId).then((site) => {
 | 
			
		||||
            return site.getDb().getRecords(this.QUESTION_TABLE, {component, attemptId});
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all the answers that aren't "extra" (sequencecheck, certainty, ...).
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any} answers Object with all the answers.
 | 
			
		||||
     * @return {any} Object with the basic answers.
 | 
			
		||||
     */
 | 
			
		||||
    getBasicAnswers(answers: any): any {
 | 
			
		||||
        const result = {};
 | 
			
		||||
 | 
			
		||||
        for (const name in answers) {
 | 
			
		||||
            if (!this.isExtraAnswer(name)) {
 | 
			
		||||
                result[name] = answers[name];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all the answers that aren't "extra" (sequencecheck, certainty, ...).
 | 
			
		||||
     *
 | 
			
		||||
     * @param {any[]} answers List of answers.
 | 
			
		||||
     * @return {any[]} List with the basic answers.
 | 
			
		||||
     */
 | 
			
		||||
    getBasicAnswersFromArray(answers: any[]): any[] {
 | 
			
		||||
        const result = [];
 | 
			
		||||
 | 
			
		||||
        answers.forEach((answer) => {
 | 
			
		||||
            if (this.isExtraAnswer(answer.name)) {
 | 
			
		||||
                result.push(answer);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve a question from site DB.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} component Component the attempt belongs to.
 | 
			
		||||
     * @param {number} attemptId Attempt ID.
 | 
			
		||||
     * @param {string} slot Question slot.
 | 
			
		||||
     * @param {string} [siteId] Site ID. If not defined, current site.
 | 
			
		||||
     * @return {Promise<any>} Promise resolved with the question.
 | 
			
		||||
     */
 | 
			
		||||
    getQuestion(component: string, attemptId: number, slot: number, siteId?: string): Promise<any> {
 | 
			
		||||
        return this.sitesProvider.getSite(siteId).then((site) => {
 | 
			
		||||
            return site.getDb().getRecord(this.QUESTION_TABLE, {component, attemptId, slot});
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve a question answers from site DB.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} component Component the attempt belongs to.
 | 
			
		||||
     * @param {number} attemptId Attempt ID.
 | 
			
		||||
     * @param {string} slot Question slot.
 | 
			
		||||
     * @param {boolean} [filter] Whether it should ignore "extra" answers like sequencecheck or certainty.
 | 
			
		||||
     * @param {string} [siteId]  Site ID. If not defined, current site.
 | 
			
		||||
     * @return {Promise<any[]>} Promise resolved with the answers.
 | 
			
		||||
     */
 | 
			
		||||
    getQuestionAnswers(component: string, attemptId: number, slot: number, filter?: boolean, siteId?: string): Promise<any[]> {
 | 
			
		||||
        return this.sitesProvider.getSite(siteId).then((site) => {
 | 
			
		||||
            return site.getDb().getRecords(this.QUESTION_ANSWERS_TABLE, {component, attemptId, slot}).then((answers) => {
 | 
			
		||||
                if (filter) {
 | 
			
		||||
                    // Get only answers that isn't "extra" data like sequencecheck or certainty.
 | 
			
		||||
                    return this.getBasicAnswersFromArray(answers);
 | 
			
		||||
                } else {
 | 
			
		||||
                    return answers;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Extract the question slot from a question name.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} name Question name.
 | 
			
		||||
     * @return {number} Question slot.
 | 
			
		||||
     */
 | 
			
		||||
    getQuestionSlotFromName(name: string): number {
 | 
			
		||||
        if (name) {
 | 
			
		||||
            const match = name.match(this.QUESTION_PREFIX_REGEX);
 | 
			
		||||
            if (match && match[1]) {
 | 
			
		||||
                return parseInt(match[1], 10);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return -1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get question state based on state name.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} name State name.
 | 
			
		||||
     * @return {CoreQuestionState} State.
 | 
			
		||||
     */
 | 
			
		||||
    getState(name: string): CoreQuestionState {
 | 
			
		||||
        return this.STATES[name || 'unknown'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if an answer is extra data like sequencecheck or certainty.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} name Answer name.
 | 
			
		||||
     * @return {boolean} Whether it's extra data.
 | 
			
		||||
     */
 | 
			
		||||
    isExtraAnswer(name: string): boolean {
 | 
			
		||||
        // Maybe the name still has the prefix.
 | 
			
		||||
        name = this.removeQuestionPrefix(name);
 | 
			
		||||
 | 
			
		||||
        return name[0] == '-' || name[0] == ':';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove an attempt answers from site DB.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} component Component the attempt belongs to.
 | 
			
		||||
     * @param {number} attemptId Attempt ID.
 | 
			
		||||
     * @param {string} [siteId] Site ID. If not defined, current site.
 | 
			
		||||
     * @return {Promise<any>} Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    removeAttemptAnswers(component: string, attemptId: number, siteId?: string): Promise<any> {
 | 
			
		||||
        return this.sitesProvider.getSite(siteId).then((site) => {
 | 
			
		||||
            return site.getDb().deleteRecords(this.QUESTION_ANSWERS_TABLE, {component, attemptId});
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove an attempt questions from site DB.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} component Component the attempt belongs to.
 | 
			
		||||
     * @param {number} attemptId Attempt ID.
 | 
			
		||||
     * @param {string} [siteId] Site ID. If not defined, current site.
 | 
			
		||||
     * @return {Promise<any>} Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    removeAttemptQuestions(component: string, attemptId: number, siteId?: string): Promise<any> {
 | 
			
		||||
        return this.sitesProvider.getSite(siteId).then((site) => {
 | 
			
		||||
            return site.getDb().deleteRecords(this.QUESTION_TABLE, {component, attemptId});
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove an answer from site DB.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} component Component the attempt belongs to.
 | 
			
		||||
     * @param {number} attemptId Attempt ID.
 | 
			
		||||
     * @param {string} name Answer's name.
 | 
			
		||||
     * @param {string} [siteId] Site ID. If not defined, current site.
 | 
			
		||||
     * @return {Promise<any>} Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    removeAnswer(component: string, attemptId: number, name: string, siteId?: string): Promise<any> {
 | 
			
		||||
        return this.sitesProvider.getSite(siteId).then((site) => {
 | 
			
		||||
            return site.getDb().deleteRecords(this.QUESTION_ANSWERS_TABLE, {component, attemptId, name});
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove a question from site DB.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} component Component the attempt belongs to.
 | 
			
		||||
     * @param {number} attemptId Attempt ID.
 | 
			
		||||
     * @param {string} slot Question slot.
 | 
			
		||||
     * @param {string} [siteId] Site ID. If not defined, current site.
 | 
			
		||||
     * @return {Promise<any>} Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    removeQuestion(component: string, attemptId: number, slot: number, siteId?: string): Promise<any> {
 | 
			
		||||
        return this.sitesProvider.getSite(siteId).then((site) => {
 | 
			
		||||
            return site.getDb().deleteRecords(this.QUESTION_TABLE, {component, attemptId, slot});
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove a question answers from site DB.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} component Component the attempt belongs to.
 | 
			
		||||
     * @param {number} attemptId Attempt ID.
 | 
			
		||||
     * @param {string} slot Question slot.
 | 
			
		||||
     * @param {string} [siteId] Site ID. If not defined, current site.
 | 
			
		||||
     * @return {Promise<any>} Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    removeQuestionAnswers(component: string, attemptId: number, slot: number, siteId?: string): Promise<any> {
 | 
			
		||||
        return this.sitesProvider.getSite(siteId).then((site) => {
 | 
			
		||||
            return site.getDb().deleteRecords(this.QUESTION_ANSWERS_TABLE, {component, attemptId, slot});
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove the prefix from a question answer name.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} name Question name.
 | 
			
		||||
     * @return {string} Name without prefix.
 | 
			
		||||
     */
 | 
			
		||||
    removeQuestionPrefix(name: string): string {
 | 
			
		||||
        if (name) {
 | 
			
		||||
            return name.replace(this.QUESTION_PREFIX_REGEX, '');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save answers in local DB.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} component Component the answers belong to. E.g. 'mmaModQuiz'.
 | 
			
		||||
     * @param {number} componentId ID of the component the answers belong to.
 | 
			
		||||
     * @param {number} attemptId Attempt ID.
 | 
			
		||||
     * @param {number} userId User ID.
 | 
			
		||||
     * @param {any} answers Object with the answers to save.
 | 
			
		||||
     * @param {number} [timemodified] Time modified to set in the answers. If not defined, current time.
 | 
			
		||||
     * @param {string} [siteId] Site ID. If not defined, current site.
 | 
			
		||||
     * @return {Promise<any>} Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    saveAnswers(component: string, componentId: number, attemptId: number, userId: number, answers: any, timemodified?: number,
 | 
			
		||||
            siteId?: string): Promise<any> {
 | 
			
		||||
        timemodified = timemodified || this.timeUtils.timestamp();
 | 
			
		||||
 | 
			
		||||
        return this.sitesProvider.getSite(siteId).then((site) => {
 | 
			
		||||
            const db = site.getDb(),
 | 
			
		||||
                promises = [];
 | 
			
		||||
 | 
			
		||||
            for (const name in answers) {
 | 
			
		||||
                const value = answers[name],
 | 
			
		||||
                    entry = {
 | 
			
		||||
                        component: component,
 | 
			
		||||
                        componentId: componentId,
 | 
			
		||||
                        attemptId: attemptId,
 | 
			
		||||
                        userId: userId,
 | 
			
		||||
                        questionSlot: this.getQuestionSlotFromName(name),
 | 
			
		||||
                        name: name,
 | 
			
		||||
                        value: value,
 | 
			
		||||
                        timemodified: timemodified
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                promises.push(db.insertRecord(this.QUESTION_ANSWERS_TABLE, entry));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Promise.all(promises);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save a question in local DB.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} component Component the question belongs to. E.g. 'mmaModQuiz'.
 | 
			
		||||
     * @param {number} componentId ID of the component the question belongs to.
 | 
			
		||||
     * @param {number} attemptId Attempt ID.
 | 
			
		||||
     * @param {number} userId User ID.
 | 
			
		||||
     * @param {any} question The question to save.
 | 
			
		||||
     * @param {string} state Question's state.
 | 
			
		||||
     * @param {string} [siteId] Site ID. If not defined, current site.
 | 
			
		||||
     * @return {Promise<any>} Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    saveQuestion(component: string, componentId: number, attemptId: number, userId: number, question: any, state: string,
 | 
			
		||||
            siteId?: string): Promise<any> {
 | 
			
		||||
 | 
			
		||||
        return this.sitesProvider.getSite(siteId).then((site) => {
 | 
			
		||||
            const entry = {
 | 
			
		||||
                component: component,
 | 
			
		||||
                componentId: componentId,
 | 
			
		||||
                attemptid: attemptId,
 | 
			
		||||
                userid: userId,
 | 
			
		||||
                number: question.number,
 | 
			
		||||
                slot: question.slot,
 | 
			
		||||
                state: state
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            return site.getDb().insertRecord(this.QUESTION_TABLE, entry);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								src/core/question/question.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/core/question/question.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
			
		||||
// (C) Copyright 2015 Martin Dougiamas
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { CoreQuestionProvider } from './providers/question';
 | 
			
		||||
import { CoreQuestionDelegate } from './providers/delegate';
 | 
			
		||||
import { CoreQuestionBehaviourDelegate } from './providers/behaviour-delegate';
 | 
			
		||||
import { CoreQuestionDefaultHandler } from './providers/default-question-handler';
 | 
			
		||||
import { CoreQuestionBehaviourDefaultHandler } from './providers/default-behaviour-handler';
 | 
			
		||||
import { CoreQuestionHelperProvider } from './providers/helper';
 | 
			
		||||
 | 
			
		||||
// List of providers (without handlers).
 | 
			
		||||
export const CORE_QUESTION_PROVIDERS: any[] = [
 | 
			
		||||
    CoreQuestionProvider,
 | 
			
		||||
    CoreQuestionDelegate,
 | 
			
		||||
    CoreQuestionBehaviourDelegate,
 | 
			
		||||
    CoreQuestionHelperProvider
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [],
 | 
			
		||||
    imports: [
 | 
			
		||||
    ],
 | 
			
		||||
    providers: CORE_QUESTION_PROVIDERS.concat([
 | 
			
		||||
        CoreQuestionDefaultHandler,
 | 
			
		||||
        CoreQuestionBehaviourDefaultHandler
 | 
			
		||||
    ]),
 | 
			
		||||
    exports: []
 | 
			
		||||
})
 | 
			
		||||
export class CoreQuestionModule {}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user