commit
9b6fae7456
|
@ -0,0 +1,46 @@
|
|||
// (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 { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreQuestionDelegate } from '@core/question/providers/delegate';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonQtypeCalculatedHandler } from './providers/handler';
|
||||
import { AddonQtypeCalculatedComponent } from './component/calculated';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonQtypeCalculatedComponent
|
||||
],
|
||||
imports: [
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreDirectivesModule
|
||||
],
|
||||
providers: [
|
||||
AddonQtypeCalculatedHandler
|
||||
],
|
||||
exports: [
|
||||
AddonQtypeCalculatedComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonQtypeCalculatedComponent
|
||||
]
|
||||
})
|
||||
export class AddonQtypeCalculatedModule {
|
||||
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeCalculatedHandler) {
|
||||
questionDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<section ion-list class="addon-qtype-calculated-container" *ngIf="question.text || question.text === ''">
|
||||
<ion-item text-wrap>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
|
||||
</ion-item>
|
||||
|
||||
<!-- Display unit options before the answer input. -->
|
||||
<ng-container *ngIf="question.options && question.options.length && question.optionsFirst">
|
||||
<ng-container *ngTemplateOutlet="radioUnits"></ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ion-item text-wrap>
|
||||
<ion-row>
|
||||
<!-- Display unit select before the answer input. -->
|
||||
<ng-container *ngIf="question.select && question.selectFirst">
|
||||
<ng-container *ngTemplateOutlet="selectUnits"></ng-container>
|
||||
</ng-container>
|
||||
|
||||
<!-- Input to enter the answer. -->
|
||||
<ion-col>
|
||||
<ion-input type="text" placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.input.name" [value]="question.input.value" [disabled]="question.input.readOnly" [ngClass]='{"core-question-answer-correct": question.input.isCorrect === 1, "core-question-answer-incorrect": question.input.isCorrect === 0}' autocorrect="off">
|
||||
</ion-input>
|
||||
</ion-col>
|
||||
|
||||
<!-- Display unit select after the answer input. -->
|
||||
<ng-container *ngIf="question.select && !question.selectFirst">
|
||||
<ng-container *ngTemplateOutlet="selectUnits"></ng-container>
|
||||
</ng-container>
|
||||
</ion-row>
|
||||
</ion-item>
|
||||
|
||||
<!-- Display unit options after the answer input. -->
|
||||
<ng-container *ngIf="question.options && question.options.length && !question.optionsFirst">
|
||||
<ng-container *ngTemplateOutlet="radioUnits"></ng-container>
|
||||
</ng-container>
|
||||
</section>
|
||||
|
||||
<!-- Template for units entered using a select. -->
|
||||
<ng-template #selectUnits>
|
||||
<ion-col>
|
||||
<label *ngIf="question.select.accessibilityLabel" class="accesshide" for="{{question.select.id}}">{{ question.select.accessibilityLabel }}</label>
|
||||
<ion-select id="{{question.select.id}}" [name]="question.select.name" [ngModel]="question.select.selected">
|
||||
<ion-option *ngFor="let option of question.select.options" [value]="option.value">{{option.label}}</ion-option>
|
||||
</ion-select>
|
||||
<!-- @todo: select fix? -->
|
||||
</ion-col>
|
||||
</ng-template>
|
||||
|
||||
<!-- Template for units entered using radio buttons. -->
|
||||
<ng-template #radioUnits>
|
||||
<div radio-group [ngModel]="question.unit" [name]="question.optionsName">
|
||||
<ion-radio *ngFor="let option of question.options" [value]="option.value" [disabled]="option.disabled">
|
||||
<p>{{option.text}}</p>
|
||||
</ion-radio>
|
||||
</div>
|
||||
</ng-template>
|
|
@ -0,0 +1,38 @@
|
|||
// (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, OnInit, Injector } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
|
||||
|
||||
/**
|
||||
* Component to render a calculated question.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-qtype-calculated',
|
||||
templateUrl: 'calculated.html'
|
||||
})
|
||||
export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector) {
|
||||
super(logger, 'AddonQtypeCalculatedComponent', injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.initCalculatedComponent();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
|
||||
// (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 { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreQuestionHandler } from '@core/question/providers/delegate';
|
||||
import { AddonQtypeNumericalHandler } from '@addon/qtype/numerical/providers/handler';
|
||||
import { AddonQtypeCalculatedComponent } from '../component/calculated';
|
||||
|
||||
/**
|
||||
* Handler to support calculated question type.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
|
||||
name = 'AddonQtypeCalculated';
|
||||
type = 'qtype_calculated';
|
||||
|
||||
constructor(private utils: CoreUtilsProvider, private numericalHandler: AddonQtypeNumericalHandler) { }
|
||||
|
||||
/**
|
||||
* 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 AddonQtypeCalculatedComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// This question type depends on numerical.
|
||||
if (this.isGradableResponse(question, answers) === 0 || !this.numericalHandler.validateUnits(answers['answer'])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (this.requiresUnits(question)) {
|
||||
return this.isValidValue(answers['unit']) ? 1 : 0;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// This question type depends on numerical.
|
||||
let isGradable = this.isValidValue(answers['answer']);
|
||||
if (isGradable && this.requiresUnits(question)) {
|
||||
// The question requires a unit.
|
||||
isGradable = this.isValidValue(answers['unit']);
|
||||
}
|
||||
|
||||
return isGradable ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// This question type depends on numerical.
|
||||
return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer') &&
|
||||
this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'unit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is valid (not empty).
|
||||
*
|
||||
* @param {string|number} value Value to check.
|
||||
* @return {boolean} Whether the value is valid.
|
||||
*/
|
||||
isValidValue(value: string | number): boolean {
|
||||
return !!value || value === '0' || value === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a question requires units in a separate input.
|
||||
*
|
||||
* @param {any} question The question.
|
||||
* @return {boolean} Whether the question requires units.
|
||||
*/
|
||||
requiresUnits(question: any): boolean {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = question.html;
|
||||
|
||||
return !!(div.querySelector('select[name*=unit]') || div.querySelector('input[type="radio"]'));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// (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 { CoreQuestionDelegate } from '@core/question/providers/delegate';
|
||||
import { AddonQtypeCalculatedMultiHandler } from './providers/handler';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
providers: [
|
||||
AddonQtypeCalculatedMultiHandler
|
||||
]
|
||||
})
|
||||
export class AddonQtypeCalculatedMultiModule {
|
||||
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeCalculatedMultiHandler) {
|
||||
questionDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
|
||||
// (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 '@core/question/providers/delegate';
|
||||
import { AddonQtypeMultichoiceHandler } from '@addon/qtype/multichoice/providers/handler';
|
||||
import { AddonQtypeMultichoiceComponent } from '@addon/qtype/multichoice/component/multichoice';
|
||||
|
||||
/**
|
||||
* Handler to support calculated multi question type.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonQtypeCalculatedMultiHandler implements CoreQuestionHandler {
|
||||
name = 'AddonQtypeCalculatedMulti';
|
||||
type = 'qtype_calculatedmulti';
|
||||
|
||||
constructor(private multichoiceHandler: AddonQtypeMultichoiceHandler) { }
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
// Calculated multi behaves like a multichoice, use the same component.
|
||||
return AddonQtypeMultichoiceComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// This question type depends on multichoice.
|
||||
return this.multichoiceHandler.isCompleteResponseSingle(answers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// This question type depends on multichoice.
|
||||
return this.multichoiceHandler.isGradableResponseSingle(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 {
|
||||
// This question type depends on multichoice.
|
||||
return this.multichoiceHandler.isSameResponseSingle(prevAnswers, newAnswers);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// (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 { CoreQuestionDelegate } from '@core/question/providers/delegate';
|
||||
import { AddonQtypeCalculatedSimpleHandler } from './providers/handler';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
providers: [
|
||||
AddonQtypeCalculatedSimpleHandler
|
||||
],
|
||||
})
|
||||
export class AddonQtypeCalculatedSimpleModule {
|
||||
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeCalculatedSimpleHandler) {
|
||||
questionDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
|
||||
// (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 '@core/question/providers/delegate';
|
||||
import { AddonQtypeCalculatedHandler } from '@addon/qtype/calculated/providers/handler';
|
||||
import { AddonQtypeCalculatedComponent } from '@addon/qtype/calculated/component/calculated';
|
||||
|
||||
/**
|
||||
* Handler to support calculated simple question type.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonQtypeCalculatedSimpleHandler implements CoreQuestionHandler {
|
||||
name = 'AddonQtypeCalculatedSimple';
|
||||
type = 'qtype_calculatedsimple';
|
||||
|
||||
constructor(private calculatedHandler: AddonQtypeCalculatedHandler) { }
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
// Calculated simple behaves like a calculated, use the same component.
|
||||
return AddonQtypeCalculatedComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// This question type depends on calculated.
|
||||
return this.calculatedHandler.isCompleteResponse(question, answers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// This question type depends on calculated.
|
||||
return this.calculatedHandler.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 {
|
||||
// This question type depends on calculated.
|
||||
return this.calculatedHandler.isSameResponse(question, prevAnswers, newAnswers);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<section ion-list *ngIf="question.text || question.text === ''">
|
||||
<!-- "Seen" hidden input -->
|
||||
<input *ngIf="question.seenInput" type="hidden" [name]="question.seenInput.name" [value]="question.seenInput.value" >
|
||||
<ion-item text-wrap class="item item-text-wrap">
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
|
||||
</ion-item>
|
||||
</section>
|
|
@ -0,0 +1,48 @@
|
|||
// (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, OnInit, Injector } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
|
||||
|
||||
/**
|
||||
* Component to render a description question.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-qtype-description',
|
||||
templateUrl: 'description.html'
|
||||
})
|
||||
export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector) {
|
||||
super(logger, 'AddonQtypeDescriptionComponent', injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
const questionDiv = this.initComponent();
|
||||
if (questionDiv) {
|
||||
// Get the "seen" hidden input.
|
||||
const input = <HTMLInputElement> questionDiv.querySelector('input[type="hidden"][name*=seen]');
|
||||
if (input) {
|
||||
this.question.seenInput = {
|
||||
name: input.name,
|
||||
value: input.value
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// (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 { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreQuestionDelegate } from '@core/question/providers/delegate';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonQtypeDescriptionHandler } from './providers/handler';
|
||||
import { AddonQtypeDescriptionComponent } from './component/description';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonQtypeDescriptionComponent
|
||||
],
|
||||
imports: [
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreDirectivesModule
|
||||
],
|
||||
providers: [
|
||||
AddonQtypeDescriptionHandler
|
||||
],
|
||||
exports: [
|
||||
AddonQtypeDescriptionComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonQtypeDescriptionComponent
|
||||
]
|
||||
})
|
||||
export class AddonQtypeDescriptionModule {
|
||||
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeDescriptionHandler) {
|
||||
questionDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
|
||||
// (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 '@core/question/providers/delegate';
|
||||
import { AddonQtypeDescriptionComponent } from '../component/description';
|
||||
|
||||
/**
|
||||
* Handler to support description question type.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonQtypeDescriptionHandler implements CoreQuestionHandler {
|
||||
name = 'AddonQtypeDescription';
|
||||
type = 'qtype_description';
|
||||
|
||||
constructor() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 'informationitem';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 AddonQtypeDescriptionComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// Descriptions don't have any answer so we'll always treat them as valid.
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<section ion-list *ngIf="question.text || question.text === ''">
|
||||
<!-- Question text. -->
|
||||
<ion-item text-wrap>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
|
||||
</ion-item>
|
||||
|
||||
<!-- Textarea. -->
|
||||
<ion-item *ngIf="question.textarea && !question.hasDraftFiles">
|
||||
<!-- "Format" hidden input -->
|
||||
<input *ngIf="question.formatInput" type="hidden" [name]="question.formatInput.name" [value]="question.formatInput.value" >
|
||||
<!-- Plain text textarea. -->
|
||||
<ion-textarea *ngIf="question.isPlainText" class="core-question-textarea" [ngClass]='{"core-monospaced": question.isMonospaced}' placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.textarea.name" aria-multiline="true">{{question.textarea.text}}</ion-textarea>
|
||||
<!-- Rich text editor. -->
|
||||
<core-rich-text-editor *ngIf="!question.isPlainText" placeholder="{{ 'core.question.answer' | translate }}"></core-rich-text-editor>
|
||||
<!-- @todo: Attributes that were passed to RTE in Ionic 1 but now they aren't supported yet:
|
||||
model="textarea" [name]="textarea.name" [component]="component" [componentId]="componentId" -->
|
||||
</ion-item>
|
||||
|
||||
<!-- Draft files not supported. -->
|
||||
<ng-container *ngIf="question.textarea && question.hasDraftFiles">
|
||||
<ion-item text-wrap class="core-error-item">
|
||||
<p class="core-question-warning">{{ 'core.question.errorinlinefilesnotsupported' | translate }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.textarea.text"></core-format-text></p>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<!-- Attachments not supported in the app yet. -->
|
||||
<ion-item text-wrap *ngIf="question.allowsAttachments" class="core-error-item">
|
||||
<p class="core-question-warning">{{ 'core.question.errorattachmentsnotsupported' | translate }}</p>
|
||||
</ion-item>
|
||||
|
||||
<!-- Answer to the question and attachments (reviewing). -->
|
||||
<ion-item text-wrap *ngIf="!question.textarea && (question.answer || (!question.attachments.length && !question.allowsAttachments))">
|
||||
<p><core-format-text [ngClass]='{"core-monospaced": question.isMonospaced}' [component]="component" [componentId]="componentId" [text]="question.answer"></core-format-text></p>
|
||||
</ion-item>
|
||||
|
||||
<ion-item *ngIf="!question.textarea && question.attachments && question.attachments.length">
|
||||
<core-file *ngFor="let attachment of question.attachments" [file]="attachment" [component]="component" [componentId]="componentId"></core-file>
|
||||
</ion-item>
|
||||
</section>
|
|
@ -0,0 +1,38 @@
|
|||
// (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, OnInit, Injector } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
|
||||
|
||||
/**
|
||||
* Component to render an essay question.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-qtype-essay',
|
||||
templateUrl: 'essay.html'
|
||||
})
|
||||
export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector) {
|
||||
super(logger, 'AddonQtypeEssayComponent', injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.initEssayComponent();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
// (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 { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreQuestionDelegate } from '@core/question/providers/delegate';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonQtypeEssayHandler } from './providers/handler';
|
||||
import { AddonQtypeEssayComponent } from './component/essay';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonQtypeEssayComponent
|
||||
],
|
||||
imports: [
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule
|
||||
],
|
||||
providers: [
|
||||
AddonQtypeEssayHandler
|
||||
],
|
||||
exports: [
|
||||
AddonQtypeEssayComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonQtypeEssayComponent
|
||||
]
|
||||
})
|
||||
export class AddonQtypeEssayModule {
|
||||
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeEssayHandler) {
|
||||
questionDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
|
||||
// (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 { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreQuestionHandler } from '@core/question/providers/delegate';
|
||||
import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
|
||||
import { AddonQtypeEssayComponent } from '../component/essay';
|
||||
|
||||
/**
|
||||
* Handler to support essay question type.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonQtypeEssayHandler implements CoreQuestionHandler {
|
||||
name = 'AddonQtypeEssay';
|
||||
type = 'qtype_essay';
|
||||
|
||||
protected div = document.createElement('div'); // A div element to search in HTML code.
|
||||
|
||||
constructor(private utils: CoreUtilsProvider, private questionHelper: CoreQuestionHelperProvider,
|
||||
private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider) { }
|
||||
|
||||
/**
|
||||
* 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 'manualgraded';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 AddonQtypeEssayComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
this.div.innerHTML = question.html;
|
||||
|
||||
if (this.div.querySelector('div[id*=filemanager]')) {
|
||||
// The question allows attachments. Since the app cannot attach files yet we will prevent submitting the question.
|
||||
return 'core.question.errorattachmentsnotsupported';
|
||||
}
|
||||
|
||||
if (this.questionHelper.hasDraftFileUrls(this.div.innerHTML)) {
|
||||
return 'core.question.errorinlinefilesnotsupported';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
this.div.innerHTML = question.html;
|
||||
|
||||
const hasInlineText = answers['answer'] && answers['answer'] !== '',
|
||||
allowsAttachments = !!this.div.querySelector('div[id*=filemanager]');
|
||||
|
||||
if (!allowsAttachments) {
|
||||
return hasInlineText ? 1 : 0;
|
||||
}
|
||||
|
||||
// We can't know if the attachments are required or if the user added any in web.
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
this.div.innerHTML = question.html;
|
||||
|
||||
// Search the textarea to get its name.
|
||||
const textarea = <HTMLTextAreaElement> this.div.querySelector('textarea[name*=_answer]');
|
||||
|
||||
if (textarea && typeof answers[textarea.name] != 'undefined') {
|
||||
return this.domUtils.isRichTextEditorEnabled().then((enabled) => {
|
||||
if (!enabled) {
|
||||
// Rich text editor not enabled, add some HTML to the text if needed.
|
||||
answers[textarea.name] = this.textUtils.formatHtmlLines(answers[textarea.name]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<section ion-list class="addon-qtype-gapselect-container" *ngIf="question.text || question.text === ''">
|
||||
<ion-item text-wrap>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
|
||||
</ion-item>
|
||||
</section>
|
|
@ -0,0 +1,38 @@
|
|||
// (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, OnInit, Injector } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
|
||||
|
||||
/**
|
||||
* Component to render a gap select question.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-qtype-gapselect',
|
||||
templateUrl: 'gapselect.html'
|
||||
})
|
||||
export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector) {
|
||||
super(logger, 'AddonQtypeGapSelectComponent', injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.initOriginalTextComponent('.qtext');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// (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 { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreQuestionDelegate } from '@core/question/providers/delegate';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonQtypeGapSelectHandler } from './providers/handler';
|
||||
import { AddonQtypeGapSelectComponent } from './component/gapselect';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonQtypeGapSelectComponent
|
||||
],
|
||||
imports: [
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreDirectivesModule
|
||||
],
|
||||
providers: [
|
||||
AddonQtypeGapSelectHandler
|
||||
],
|
||||
exports: [
|
||||
AddonQtypeGapSelectComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonQtypeGapSelectComponent
|
||||
]
|
||||
})
|
||||
export class AddonQtypeGapSelectModule {
|
||||
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeGapSelectHandler) {
|
||||
questionDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
|
||||
// (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 { CoreQuestionProvider } from '@core/question/providers/question';
|
||||
import { CoreQuestionHandler } from '@core/question/providers/delegate';
|
||||
import { AddonQtypeGapSelectComponent } from '../component/gapselect';
|
||||
|
||||
/**
|
||||
* Handler to support gapselect question type.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonQtypeGapSelectHandler implements CoreQuestionHandler {
|
||||
name = 'AddonQtypeGapSelect';
|
||||
type = 'qtype_gapselect';
|
||||
|
||||
constructor(private questionProvider: CoreQuestionProvider) { }
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (behaviour === 'interactive') {
|
||||
return 'interactivecountback';
|
||||
}
|
||||
|
||||
return behaviour;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 AddonQtypeGapSelectComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// We should always get a value for each select so we can assume we receive all the possible answers.
|
||||
for (const name in answers) {
|
||||
const value = answers[name];
|
||||
if (!value || value === '0') {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// We should always get a value for each select so we can assume we receive all the possible answers.
|
||||
for (const name in answers) {
|
||||
const value = answers[name];
|
||||
if (value) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 this.questionProvider.compareAllAnswers(prevAnswers, newAnswers);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<section ion-list class="addon-qtype-match-container" *ngIf="question.loaded">
|
||||
<ion-item text-wrap>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngFor="let row of question.rows">
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<p><core-format-text id="addon-qtype-match-question-{{row.id}}" [component]="component" [componentId]="componentId" [text]="row.text"></core-format-text></p>
|
||||
</ion-col>
|
||||
<ion-col [ngClass]='{"core-question-answer-correct": row.isCorrect === 1, "core-question-answer-incorrect": row.isCorrect === 0}'>
|
||||
<label class="accesshide" for="{{row.id}}" *ngIf="row.accessibilityLabel">{{ row.accessibilityLabel }}</label>
|
||||
<ion-select id="{{row.id}}" [name]="row.name" [attr.aria-labelledby]="'addon-qtype-match-question-' + row.id" [ngModel]="row.selected">
|
||||
<ion-option *ngFor="let option of row.options" [value]="option.value">{{option.label}}</ion-option>
|
||||
</ion-select>
|
||||
<!-- @todo: select fix? -->
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-item>
|
||||
</section>
|
|
@ -0,0 +1,38 @@
|
|||
// (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, OnInit, Injector } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
|
||||
|
||||
/**
|
||||
* Component to render a match question.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-qtype-match',
|
||||
templateUrl: 'match.html'
|
||||
})
|
||||
export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector) {
|
||||
super(logger, 'AddonQtypeMatchComponent', injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.initMatchComponent();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// (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 { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreQuestionDelegate } from '@core/question/providers/delegate';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonQtypeMatchHandler } from './providers/handler';
|
||||
import { AddonQtypeMatchComponent } from './component/match';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonQtypeMatchComponent
|
||||
],
|
||||
imports: [
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreDirectivesModule
|
||||
],
|
||||
providers: [
|
||||
AddonQtypeMatchHandler
|
||||
],
|
||||
exports: [
|
||||
AddonQtypeMatchComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonQtypeMatchComponent
|
||||
]
|
||||
})
|
||||
export class AddonQtypeMatchModule {
|
||||
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeMatchHandler) {
|
||||
questionDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
|
||||
// (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 { CoreQuestionProvider } from '@core/question/providers/question';
|
||||
import { CoreQuestionHandler } from '@core/question/providers/delegate';
|
||||
import { AddonQtypeMatchComponent } from '../component/match';
|
||||
|
||||
/**
|
||||
* Handler to support match question type.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonQtypeMatchHandler implements CoreQuestionHandler {
|
||||
name = 'AddonQtypeMatch';
|
||||
type = 'qtype_match';
|
||||
|
||||
constructor(private questionProvider: CoreQuestionProvider) { }
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (behaviour === 'interactive') {
|
||||
return 'interactivecountback';
|
||||
}
|
||||
|
||||
return behaviour;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 AddonQtypeMatchComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// We should always get a value for each select so we can assume we receive all the possible answers.
|
||||
for (const name in answers) {
|
||||
const value = answers[name];
|
||||
if (!value || value === '0') {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// We should always get a value for each select so we can assume we receive all the possible answers.
|
||||
for (const name in answers) {
|
||||
const value = answers[name];
|
||||
if (value && value !== '0') {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 this.questionProvider.compareAllAnswers(prevAnswers, newAnswers);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<section ion-list class="addon-qtype-multianswer-container" *ngIf="question.text || question.text === ''">
|
||||
<ion-item text-wrap>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
|
||||
</ion-item>
|
||||
</section>
|
|
@ -0,0 +1,38 @@
|
|||
// (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, OnInit, Injector } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
|
||||
|
||||
/**
|
||||
* Component to render a multianswer question.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-qtype-multianswer',
|
||||
templateUrl: 'multianswer.html'
|
||||
})
|
||||
export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector) {
|
||||
super(logger, 'AddonQtypeMultiAnswerComponent', injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.initOriginalTextComponent('.formulation');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// (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 { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreQuestionDelegate } from '@core/question/providers/delegate';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonQtypeMultiAnswerHandler } from './providers/handler';
|
||||
import { AddonQtypeMultiAnswerComponent } from './component/multianswer';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonQtypeMultiAnswerComponent
|
||||
],
|
||||
imports: [
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreDirectivesModule
|
||||
],
|
||||
providers: [
|
||||
AddonQtypeMultiAnswerHandler
|
||||
],
|
||||
exports: [
|
||||
AddonQtypeMultiAnswerComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonQtypeMultiAnswerComponent
|
||||
]
|
||||
})
|
||||
export class AddonQtypeMultiAnswerModule {
|
||||
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeMultiAnswerHandler) {
|
||||
questionDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
|
||||
// (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 { CoreQuestionProvider } from '@core/question/providers/question';
|
||||
import { CoreQuestionHandler } from '@core/question/providers/delegate';
|
||||
import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
|
||||
import { AddonQtypeMultiAnswerComponent } from '../component/multianswer';
|
||||
|
||||
/**
|
||||
* Handler to support multianswer question type.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonQtypeMultiAnswerHandler implements CoreQuestionHandler {
|
||||
name = 'AddonQtypeMultiAnswer';
|
||||
type = 'qtype_multianswer';
|
||||
|
||||
constructor(private questionProvider: CoreQuestionProvider, private questionHelper: CoreQuestionHelperProvider) { }
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (behaviour === 'interactive') {
|
||||
return 'interactivecountback';
|
||||
}
|
||||
|
||||
return behaviour;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 AddonQtypeMultiAnswerComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// Get all the inputs in the question to check if they've all been answered.
|
||||
const names = this.questionProvider.getBasicAnswers(this.questionHelper.getAllInputNamesFromHtml(question.html));
|
||||
for (const name in names) {
|
||||
const value = answers[name];
|
||||
if (!value && value !== false && value !== 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// We should always get a value for each select so we can assume we receive all the possible answers.
|
||||
for (const name in answers) {
|
||||
const value = answers[name];
|
||||
if (value || value === false) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 this.questionProvider.compareAllAnswers(prevAnswers, newAnswers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (question.sequencecheck == offlineSequenceCheck) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For some reason, viewing a multianswer for the first time without answering it creates a new step "todo".
|
||||
// We'll treat this case as valid.
|
||||
if (question.sequencecheck == 2 && question.state == 'todo' && offlineSequenceCheck == '1') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<section ion-list *ngIf="question.text || question.text === ''">
|
||||
<!-- Question text first. -->
|
||||
<ion-item text-wrap>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
|
||||
<p *ngIf="question.prompt"><core-format-text [component]="component" [componentId]="componentId" [text]="question.prompt"></core-format-text></p>
|
||||
</ion-item>
|
||||
|
||||
<!-- Checkbox for multiple choice. -->
|
||||
<ng-container *ngIf="question.multi">
|
||||
<ion-checkbox text-wrap [name]="option.name" [ngModel]="option.checked" [disabled]="option.disabled" *ngFor="let option of question.options" [ngClass]="{'core-question-answer-correct': option.isCorrect === 1, 'core-question-answer-incorrect': option.isCorrect === 0}">
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="option.text"></core-format-text></p>
|
||||
<p *ngIf="option.feedback" class="core-question-feedback-container"><core-format-text [component]="component" [componentId]="componentId" [text]="option.feedback"></core-format-text></p>
|
||||
</ion-checkbox>
|
||||
</ng-container>
|
||||
|
||||
<!-- Radio buttons for single choice. -->
|
||||
<div *ngIf="!question.multi" radio-group [ngModel]="question.singleChoiceModel" [name]="question.optionsName">
|
||||
<ion-item text-wrap>
|
||||
<ion-label><core-format-text [component]="component" [componentId]="componentId" [text]="option.text"></core-format-text></ion-label>
|
||||
<ion-radio *ngFor="let option of question.options" [value]="option.value" [disabled]="option.disabled" [ngClass]='{"core-question-answer-correct": option.isCorrect === 1, "core-question-answer-incorrect": option.isCorrect === 0}'>
|
||||
<p *ngIf="option.feedback" class="core-question-feedback-container"><core-format-text [component]="component" [componentId]="componentId" [text]="option.feedback"></core-format-text></p>
|
||||
</ion-radio>
|
||||
</ion-item>
|
||||
</div>
|
||||
</section>
|
|
@ -0,0 +1,38 @@
|
|||
// (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, OnInit, Injector } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
|
||||
|
||||
/**
|
||||
* Component to render a multichoice question.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-qtype-multichoice',
|
||||
templateUrl: 'multichoice.html'
|
||||
})
|
||||
export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector) {
|
||||
super(logger, 'AddonQtypeMultichoiceComponent', injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.initMultichoiceComponent();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// (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 { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreQuestionDelegate } from '@core/question/providers/delegate';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonQtypeMultichoiceHandler } from './providers/handler';
|
||||
import { AddonQtypeMultichoiceComponent } from './component/multichoice';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonQtypeMultichoiceComponent
|
||||
],
|
||||
imports: [
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreDirectivesModule
|
||||
],
|
||||
providers: [
|
||||
AddonQtypeMultichoiceHandler
|
||||
],
|
||||
exports: [
|
||||
AddonQtypeMultichoiceComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonQtypeMultichoiceComponent
|
||||
]
|
||||
})
|
||||
export class AddonQtypeMultichoiceModule {
|
||||
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeMultichoiceHandler) {
|
||||
questionDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
|
||||
// (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 { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreQuestionHandler } from '@core/question/providers/delegate';
|
||||
import { AddonQtypeMultichoiceComponent } from '../component/multichoice';
|
||||
|
||||
/**
|
||||
* Handler to support multichoice question type.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler {
|
||||
name = 'AddonQtypeMultichoice';
|
||||
type = 'qtype_multichoice';
|
||||
|
||||
constructor(private utils: CoreUtilsProvider) { }
|
||||
|
||||
/**
|
||||
* 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 AddonQtypeMultichoiceComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
let isSingle = true,
|
||||
isMultiComplete = false;
|
||||
|
||||
// To know if it's single or multi answer we need to search for answers with "choice" in the name.
|
||||
for (const name in answers) {
|
||||
if (name.indexOf('choice') != -1) {
|
||||
isSingle = false;
|
||||
if (answers[name]) {
|
||||
isMultiComplete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isSingle) {
|
||||
// Single.
|
||||
return this.isCompleteResponseSingle(answers);
|
||||
} else {
|
||||
// Multi.
|
||||
return isMultiComplete ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a response is complete. Only for single answer.
|
||||
*
|
||||
* @param {any} question The question.uestion answers (without prefix).
|
||||
* @return {number} 1 if complete, 0 if not complete, -1 if cannot determine.
|
||||
*/
|
||||
isCompleteResponseSingle(answers: any): number {
|
||||
return (answers['answer'] && answers['answer'] !== '') ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 this.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. Only for single answer.
|
||||
*
|
||||
* @param {any} answers Object with the question answers (without prefix).
|
||||
* @return {number} 1 if gradable, 0 if not gradable, -1 if cannot determine.
|
||||
*/
|
||||
isGradableResponseSingle(answers: any): number {
|
||||
return this.isCompleteResponseSingle(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 {
|
||||
let isSingle = true,
|
||||
isMultiSame = true;
|
||||
|
||||
// To know if it's single or multi answer we need to search for answers with "choice" in the name.
|
||||
for (const name in newAnswers) {
|
||||
if (name.indexOf('choice') != -1) {
|
||||
isSingle = false;
|
||||
if (!this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, name)) {
|
||||
isMultiSame = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isSingle) {
|
||||
return this.isSameResponseSingle(prevAnswers, newAnswers);
|
||||
} else {
|
||||
return isMultiSame ;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two responses are the same. Only for single answer.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
isSameResponseSingle(prevAnswers: any, newAnswers: any): boolean {
|
||||
return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// (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 { CoreQuestionDelegate } from '@core/question/providers/delegate';
|
||||
import { AddonQtypeNumericalHandler } from './providers/handler';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
providers: [
|
||||
AddonQtypeNumericalHandler
|
||||
]
|
||||
})
|
||||
export class AddonQtypeNumericalModule {
|
||||
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeNumericalHandler) {
|
||||
questionDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
|
||||
// (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 { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreQuestionHandler } from '@core/question/providers/delegate';
|
||||
import { AddonQtypeShortAnswerComponent } from '@addon/qtype/shortanswer/component/shortanswer';
|
||||
|
||||
/**
|
||||
* Handler to support numerical question type.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonQtypeNumericalHandler implements CoreQuestionHandler {
|
||||
name = 'AddonQtypeNumerical';
|
||||
type = 'qtype_numerical';
|
||||
|
||||
constructor(private utils: CoreUtilsProvider) { }
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
// Numerical behaves like a short answer, use the same component.
|
||||
return AddonQtypeShortAnswerComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (this.isGradableResponse(question, answers) === 0 || !this.validateUnits(answers['answer'])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (answers['answer'] || answers['answer'] === '0' || answers['answer'] === 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a number with units. We don't have the list of valid units and conversions, so we can't perform
|
||||
* a full validation. If this function returns true it means we can't be sure it's valid.
|
||||
*
|
||||
* @param {string} answer Answer.
|
||||
* @return {boolean} False if answer isn't valid, true if we aren't sure if it's valid.
|
||||
*/
|
||||
validateUnits(answer: string): boolean {
|
||||
if (!answer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const regexString = '[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[-+]?\\d+)?';
|
||||
|
||||
// Strip spaces (which may be thousands separators) and change other forms of writing e to e.
|
||||
answer = answer.replace(' ', '');
|
||||
answer = answer.replace(/(?:e|E|(?:x|\*|×)10(?:\^|\*\*))([+-]?\d+)/, 'e$1');
|
||||
|
||||
// If a '.' is present or there are multiple ',' (i.e. 2,456,789) assume ',' is a thousands separator and stip it.
|
||||
// Else assume it is a decimal separator, and change it to '.'.
|
||||
if (answer.indexOf('.') != -1 || answer.split(',').length - 1 > 1) {
|
||||
answer = answer.replace(',', '');
|
||||
} else {
|
||||
answer = answer.replace(',', '.');
|
||||
}
|
||||
|
||||
// We don't know if units should be before or after so we check both.
|
||||
if (answer.match(new RegExp('^' + regexString)) === null || answer.match(new RegExp(regexString + '$')) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
// (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 { AddonQtypeCalculatedModule } from './calculated/calculated.module';
|
||||
import { AddonQtypeCalculatedMultiModule } from './calculatedmulti/calculatedmulti.module';
|
||||
import { AddonQtypeCalculatedSimpleModule } from './calculatedsimple/calculatedsimple.module';
|
||||
import { AddonQtypeDescriptionModule } from './description/description.module';
|
||||
import { AddonQtypeEssayModule } from './essay/essay.module';
|
||||
import { AddonQtypeGapSelectModule } from './gapselect/gapselect.module';
|
||||
import { AddonQtypeMatchModule } from './match/match.module';
|
||||
import { AddonQtypeMultiAnswerModule } from './multianswer/multianswer.module';
|
||||
import { AddonQtypeMultichoiceModule } from './multichoice/multichoice.module';
|
||||
import { AddonQtypeNumericalModule } from './numerical/numerical.module';
|
||||
import { AddonQtypeRandomSaMatchModule } from './randomsamatch/randomsamatch.module';
|
||||
import { AddonQtypeShortAnswerModule } from './shortanswer/shortanswer.module';
|
||||
import { AddonQtypeTrueFalseModule } from './truefalse/truefalse.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
AddonQtypeCalculatedModule,
|
||||
AddonQtypeCalculatedMultiModule,
|
||||
AddonQtypeCalculatedSimpleModule,
|
||||
AddonQtypeDescriptionModule,
|
||||
AddonQtypeEssayModule,
|
||||
AddonQtypeGapSelectModule,
|
||||
AddonQtypeMatchModule,
|
||||
AddonQtypeMultiAnswerModule,
|
||||
AddonQtypeMultichoiceModule,
|
||||
AddonQtypeNumericalModule,
|
||||
AddonQtypeRandomSaMatchModule,
|
||||
AddonQtypeShortAnswerModule,
|
||||
AddonQtypeTrueFalseModule
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: []
|
||||
})
|
||||
export class AddonQtypeModule { }
|
|
@ -0,0 +1,90 @@
|
|||
|
||||
// (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 '@core/question/providers/delegate';
|
||||
import { AddonQtypeMatchHandler } from '@addon/qtype/match/providers/handler';
|
||||
import { AddonQtypeMatchComponent } from '@addon/qtype/match/component/match';
|
||||
|
||||
/**
|
||||
* Handler to support random short-answer matching question type.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonQtypeRandomSaMatchHandler implements CoreQuestionHandler {
|
||||
name = 'AddonQtypeRandomSaMatch';
|
||||
type = 'qtype_randomsamatch';
|
||||
|
||||
constructor(private matchHandler: AddonQtypeMatchHandler) { }
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
// Random behaves like a match question, use the same component.
|
||||
return AddonQtypeMatchComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// This question behaves like a match question.
|
||||
return this.matchHandler.isCompleteResponse(question, answers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// This question behaves like a match question.
|
||||
return this.matchHandler.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 {
|
||||
// This question behaves like a match question.
|
||||
return this.matchHandler.isSameResponse(question, prevAnswers, newAnswers);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// (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 { CoreQuestionDelegate } from '@core/question/providers/delegate';
|
||||
import { AddonQtypeRandomSaMatchHandler } from './providers/handler';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
providers: [
|
||||
AddonQtypeRandomSaMatchHandler
|
||||
]
|
||||
})
|
||||
export class AddonQtypeRandomSaMatchModule {
|
||||
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeRandomSaMatchHandler) {
|
||||
questionDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<section ion-list *ngIf="question.text || question.text === ''">
|
||||
<ion-item text-wrap>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
|
||||
</ion-item>
|
||||
<ion-input type="text" placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.input.name" [value]="question.input.value" autocorrect="off" [disabled]="question.input.readOnly" [ngClass]='{"core-question-answer-correct": question.input.isCorrect === 1, "core-question-answer-incorrect": question.input.isCorrect === 0}'>
|
||||
</ion-input>
|
||||
</section>
|
|
@ -0,0 +1,38 @@
|
|||
// (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, OnInit, Injector } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
|
||||
|
||||
/**
|
||||
* Component to render a short answer question.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-qtype-shortanswer',
|
||||
templateUrl: 'shortanswer.html'
|
||||
})
|
||||
export class AddonQtypeShortAnswerComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector) {
|
||||
super(logger, 'AddonQtypeShortAnswerComponent', injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.initInputTextComponent();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
|
||||
// (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 { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreQuestionHandler } from '@core/question/providers/delegate';
|
||||
import { AddonQtypeShortAnswerComponent } from '../component/shortanswer';
|
||||
|
||||
/**
|
||||
* Handler to support short answer question type.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonQtypeShortAnswerHandler implements CoreQuestionHandler {
|
||||
name = 'AddonQtypeShortAnswer';
|
||||
type = 'qtype_shortanswer';
|
||||
|
||||
constructor(private utils: CoreUtilsProvider) { }
|
||||
|
||||
/**
|
||||
* 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 AddonQtypeShortAnswerComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (answers['answer'] || answers['answer'] === 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 this.isCompleteResponse(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 {
|
||||
return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// (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 { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreQuestionDelegate } from '@core/question/providers/delegate';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonQtypeShortAnswerHandler } from './providers/handler';
|
||||
import { AddonQtypeShortAnswerComponent } from './component/shortanswer';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonQtypeShortAnswerComponent
|
||||
],
|
||||
imports: [
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreDirectivesModule
|
||||
],
|
||||
providers: [
|
||||
AddonQtypeShortAnswerHandler
|
||||
],
|
||||
exports: [
|
||||
AddonQtypeShortAnswerComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonQtypeShortAnswerComponent
|
||||
]
|
||||
})
|
||||
export class AddonQtypeShortAnswerModule {
|
||||
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeShortAnswerHandler) {
|
||||
questionDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
|
||||
// (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 { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreQuestionHandler } from '@core/question/providers/delegate';
|
||||
import { AddonQtypeMultichoiceComponent } from '@addon/qtype/multichoice/component/multichoice';
|
||||
|
||||
/**
|
||||
* Handler to support true/false question type.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler {
|
||||
name = 'AddonQtypeTrueFalse';
|
||||
type = 'qtype_truefalse';
|
||||
|
||||
constructor(private utils: CoreUtilsProvider) { }
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
// True/false behaves like a multichoice, use the same component.
|
||||
return AddonQtypeMultichoiceComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 answers['answer'] ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 this.isCompleteResponse(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 {
|
||||
return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// (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 { CoreQuestionDelegate } from '@core/question/providers/delegate';
|
||||
import { AddonQtypeTrueFalseHandler } from './providers/handler';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
providers: [
|
||||
AddonQtypeTrueFalseHandler
|
||||
]
|
||||
})
|
||||
export class AddonQtypeTrueFalseModule {
|
||||
constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeTrueFalseHandler) {
|
||||
questionDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -87,6 +87,7 @@ import { AddonNotesModule } from '../addon/notes/notes.module';
|
|||
import { AddonPushNotificationsModule } from '@addon/pushnotifications/pushnotifications.module';
|
||||
import { AddonRemoteThemesModule } from '@addon/remotethemes/remotethemes.module';
|
||||
import { AddonQbehaviourModule } from '@addon/qbehaviour/qbehaviour.module';
|
||||
import { AddonQtypeModule } from '@addon/qtype/qtype.module';
|
||||
|
||||
// For translate loader. AoT requires an exported function for factories.
|
||||
export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
||||
|
@ -174,7 +175,8 @@ export const CORE_PROVIDERS: any[] = [
|
|||
AddonNotesModule,
|
||||
AddonPushNotificationsModule,
|
||||
AddonRemoteThemesModule,
|
||||
AddonQbehaviourModule
|
||||
AddonQbehaviourModule,
|
||||
AddonQtypeModule
|
||||
],
|
||||
bootstrap: [IonicApp],
|
||||
entryComponents: [
|
||||
|
|
|
@ -0,0 +1,488 @@
|
|||
// (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 { Input, EventEmitter, Injector } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
|
||||
|
||||
/**
|
||||
* Base class for components to render a question.
|
||||
*/
|
||||
export class CoreQuestionBaseComponent {
|
||||
@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.
|
||||
@Input() buttonClicked: EventEmitter<any>; // Should emit an event when a behaviour button is clicked.
|
||||
@Input() onAbort: EventEmitter<void>; // Should emit an event if the question should be aborted.
|
||||
|
||||
protected logger;
|
||||
protected questionHelper: CoreQuestionHelperProvider;
|
||||
protected domUtils: CoreDomUtilsProvider;
|
||||
protected textUtils: CoreTextUtilsProvider;
|
||||
|
||||
constructor(logger: CoreLoggerProvider, logName: string, protected injector: Injector) {
|
||||
this.logger = logger.getInstance(logName);
|
||||
|
||||
// Use an injector to get the providers to prevent having to modify all subclasses if a new provider is needed.
|
||||
this.questionHelper = injector.get(CoreQuestionHelperProvider);
|
||||
this.domUtils = injector.get(CoreDomUtilsProvider);
|
||||
this.textUtils = injector.get(CoreTextUtilsProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a question component of type calculated or calculated simple.
|
||||
*
|
||||
* @return {void|HTMLElement} Element containing the question HTML, void if the data is not valid.
|
||||
*/
|
||||
initCalculatedComponent(): void | HTMLElement {
|
||||
// Treat the input text first.
|
||||
const questionDiv = this.initInputTextComponent();
|
||||
if (questionDiv) {
|
||||
|
||||
// Check if the question has a select for units.
|
||||
const selectModel: any = {},
|
||||
select = <HTMLSelectElement> questionDiv.querySelector('select[name*=unit]'),
|
||||
options = select && Array.from(select.querySelectorAll('option'));
|
||||
|
||||
if (select && options && options.length) {
|
||||
|
||||
selectModel.id = select.id;
|
||||
selectModel.name = select.name;
|
||||
selectModel.disabled = select.disabled;
|
||||
selectModel.options = [];
|
||||
|
||||
// Treat each option.
|
||||
for (const i in options) {
|
||||
const optionEl = options[i];
|
||||
|
||||
if (typeof optionEl.value == 'undefined') {
|
||||
this.logger.warn('Aborting because couldn\'t find input.', this.question.name);
|
||||
|
||||
return this.questionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
|
||||
const option = {
|
||||
value: optionEl.value,
|
||||
label: optionEl.innerHTML
|
||||
};
|
||||
|
||||
if (optionEl.selected) {
|
||||
selectModel.selected = option.value;
|
||||
selectModel.selectedLabel = option.label;
|
||||
}
|
||||
|
||||
selectModel.options.push(option);
|
||||
}
|
||||
|
||||
if (!selectModel.selected) {
|
||||
// No selected option, select the first one.
|
||||
selectModel.selected = selectModel.options[0].value;
|
||||
selectModel.selectedLabel = selectModel.options[0].label;
|
||||
}
|
||||
|
||||
// Get the accessibility label.
|
||||
const accessibilityLabel = questionDiv.querySelector('label[for="' + select.id + '"]');
|
||||
selectModel.accessibilityLabel = accessibilityLabel && accessibilityLabel.innerHTML;
|
||||
|
||||
this.question.select = selectModel;
|
||||
|
||||
// Check which one should be displayed first: the select or the input.
|
||||
const input = questionDiv.querySelector('input[type="text"][name*=answer]');
|
||||
this.question.selectFirst =
|
||||
questionDiv.innerHTML.indexOf(input.outerHTML) > questionDiv.innerHTML.indexOf(select.outerHTML);
|
||||
|
||||
return questionDiv;
|
||||
}
|
||||
|
||||
// Check if the question has radio buttons for units.
|
||||
const radios = <HTMLInputElement[]> Array.from(questionDiv.querySelectorAll('input[type="radio"]'));
|
||||
if (!radios.length) {
|
||||
// No select and no radio buttons. The units need to be entered in the input text.
|
||||
return questionDiv;
|
||||
}
|
||||
|
||||
this.question.options = [];
|
||||
|
||||
for (const i in radios) {
|
||||
const radioEl = radios[i],
|
||||
option: any = {
|
||||
id: radioEl.id,
|
||||
name: radioEl.name,
|
||||
value: radioEl.value,
|
||||
checked: radioEl.checked,
|
||||
disabled: radioEl.disabled
|
||||
},
|
||||
// Get the label with the question text.
|
||||
label = <HTMLElement> questionDiv.querySelector('label[for="' + option.id + '"]');
|
||||
|
||||
this.question.optionsName = option.name;
|
||||
|
||||
if (label) {
|
||||
option.text = label.innerText;
|
||||
|
||||
// Check that we were able to successfully extract options required data.
|
||||
if (typeof option.name != 'undefined' && typeof option.value != 'undefined' &&
|
||||
typeof option.text != 'undefined') {
|
||||
|
||||
if (radioEl.checked) {
|
||||
// If the option is checked we use the model to select the one.
|
||||
this.question.unit = option.value;
|
||||
}
|
||||
|
||||
this.question.options.push(option);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Something went wrong when extracting the questions data. Abort.
|
||||
this.logger.warn('Aborting because of an error parsing options.', this.question.name, option.name);
|
||||
|
||||
return this.questionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
|
||||
// Check which one should be displayed first: the options or the input.
|
||||
const input = questionDiv.querySelector('input[type="text"][name*=answer]');
|
||||
this.question.optionsFirst =
|
||||
questionDiv.innerHTML.indexOf(input.outerHTML) > questionDiv.innerHTML.indexOf(options[0].outerHTML);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the component and the question text.
|
||||
*
|
||||
* @return {void|HTMLElement} Element containing the question HTML, void if the data is not valid.
|
||||
*/
|
||||
initComponent(): void | HTMLElement {
|
||||
if (!this.question) {
|
||||
this.logger.warn('Aborting because of no question received.');
|
||||
|
||||
return this.questionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = this.question.html;
|
||||
|
||||
// Extract question text.
|
||||
this.question.text = this.domUtils.getContentsOfElement(div, '.qtext');
|
||||
if (typeof this.question.text == 'undefined') {
|
||||
this.logger.warn('Aborting because of an error parsing question.', this.question.name);
|
||||
|
||||
return this.questionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a question component of type essay.
|
||||
*
|
||||
* @return {void|HTMLElement} Element containing the question HTML, void if the data is not valid.
|
||||
*/
|
||||
initEssayComponent(): void | HTMLElement {
|
||||
const questionDiv = this.initComponent();
|
||||
|
||||
if (questionDiv) {
|
||||
// First search the textarea.
|
||||
const textarea = <HTMLTextAreaElement> questionDiv.querySelector('textarea[name*=_answer]');
|
||||
this.question.allowsAttachments = !!questionDiv.querySelector('div[id*=filemanager]');
|
||||
this.question.isMonospaced = !!questionDiv.querySelector('.qtype_essay_monospaced');
|
||||
this.question.isPlainText = this.question.isMonospaced || !!questionDiv.querySelector('.qtype_essay_plain');
|
||||
this.question.hasDraftFiles = this.questionHelper.hasDraftFileUrls(questionDiv.innerHTML);
|
||||
|
||||
if (!textarea) {
|
||||
// Textarea not found, we might be in review. Search the answer and the attachments.
|
||||
this.question.answer = this.domUtils.getContentsOfElement(questionDiv, '.qtype_essay_response');
|
||||
this.question.attachments = this.questionHelper.getQuestionAttachmentsFromHtml(
|
||||
this.domUtils.getContentsOfElement(questionDiv, '.attachments'));
|
||||
} else {
|
||||
// Textarea found.
|
||||
const input = <HTMLInputElement> questionDiv.querySelector('input[type="hidden"][name*=answerformat]'),
|
||||
content = textarea.innerHTML;
|
||||
|
||||
this.question.textarea = {
|
||||
id: textarea.id,
|
||||
name: textarea.name,
|
||||
text: content ? this.textUtils.decodeHTML(content) : ''
|
||||
};
|
||||
|
||||
if (input) {
|
||||
this.question.formatInput = {
|
||||
name: input.name,
|
||||
value: input.value
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a question component that uses the original question text with some basic treatment.
|
||||
*
|
||||
* @param {string} contentSelector The selector to find the question content (text).
|
||||
* @return {void|HTMLElement} Element containing the question HTML, void if the data is not valid.
|
||||
*/
|
||||
initOriginalTextComponent(contentSelector: string): void | HTMLElement {
|
||||
if (!this.question) {
|
||||
this.logger.warn('Aborting because of no question received.');
|
||||
|
||||
return this.questionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = this.question.html;
|
||||
|
||||
// Get question content.
|
||||
const content = <HTMLElement> div.querySelector(contentSelector);
|
||||
if (!content) {
|
||||
this.logger.warn('Aborting because of an error parsing question.', this.question.name);
|
||||
|
||||
return this.questionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
|
||||
// Remove sequencecheck and validation error.
|
||||
this.domUtils.removeElement(content, 'input[name*=sequencecheck]');
|
||||
this.domUtils.removeElement(content, '.validationerror');
|
||||
|
||||
// Replace Moodle's correct/incorrect and feedback classes with our own.
|
||||
this.questionHelper.replaceCorrectnessClasses(div);
|
||||
this.questionHelper.replaceFeedbackClasses(div);
|
||||
|
||||
// Treat the correct/incorrect icons.
|
||||
this.questionHelper.treatCorrectnessIcons(div, this.component, this.componentId);
|
||||
|
||||
// Set the question text.
|
||||
this.question.text = content.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a question component that has an input of type "text".
|
||||
*
|
||||
* @return {void|HTMLElement} Element containing the question HTML, void if the data is not valid.
|
||||
*/
|
||||
initInputTextComponent(): void | HTMLElement {
|
||||
const questionDiv = this.initComponent();
|
||||
if (questionDiv) {
|
||||
// Get the input element.
|
||||
const input = <HTMLInputElement> questionDiv.querySelector('input[type="text"][name*=answer]');
|
||||
if (!input) {
|
||||
this.logger.warn('Aborting because couldn\'t find input.', this.question.name);
|
||||
|
||||
return this.questionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
|
||||
this.question.input = {
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
value: input.value,
|
||||
readOnly: input.readOnly
|
||||
};
|
||||
|
||||
// Check if question is marked as correct.
|
||||
if (input.className.indexOf('incorrect') >= 0) {
|
||||
this.question.input.isCorrect = 0;
|
||||
} else if (input.className.indexOf('correct') >= 0) {
|
||||
this.question.input.isCorrect = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return questionDiv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a question component with a "match" behaviour.
|
||||
*
|
||||
* @return {void|HTMLElement} Element containing the question HTML, void if the data is not valid.
|
||||
*/
|
||||
initMatchComponent(): void | HTMLElement {
|
||||
const questionDiv = this.initComponent();
|
||||
|
||||
if (questionDiv) {
|
||||
// Find rows.
|
||||
const rows = Array.from(questionDiv.querySelectorAll('tr'));
|
||||
if (!rows || !rows.length) {
|
||||
this.logger.warn('Aborting because couldn\'t find any row.', this.question.name);
|
||||
|
||||
return this.questionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
|
||||
this.question.rows = [];
|
||||
|
||||
for (const i in rows) {
|
||||
const row = rows[i],
|
||||
rowModel: any = {},
|
||||
columns = Array.from(row.querySelectorAll('td'));
|
||||
|
||||
if (!columns || columns.length < 2) {
|
||||
this.logger.warn('Aborting because couldn\'t the right columns.', this.question.name);
|
||||
|
||||
return this.questionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
|
||||
// Get the row's text. It should be in the first column.
|
||||
rowModel.text = columns[0].innerHTML;
|
||||
|
||||
// Get the select and the options.
|
||||
const select = columns[1].querySelector('select'),
|
||||
options = Array.from(columns[1].querySelectorAll('option'));
|
||||
|
||||
if (!select || !options || !options.length) {
|
||||
this.logger.warn('Aborting because couldn\'t find select or options.', this.question.name);
|
||||
|
||||
return this.questionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
|
||||
rowModel.id = select.id;
|
||||
rowModel.name = select.name;
|
||||
rowModel.disabled = select.disabled;
|
||||
rowModel.selected = false;
|
||||
rowModel.options = [];
|
||||
|
||||
// Check if answer is correct.
|
||||
if (columns[1].className.indexOf('incorrect') >= 0) {
|
||||
rowModel.isCorrect = 0;
|
||||
} else if (columns[1].className.indexOf('correct') >= 0) {
|
||||
rowModel.isCorrect = 1;
|
||||
}
|
||||
|
||||
// Treat each option.
|
||||
for (const j in options) {
|
||||
const optionEl = options[j];
|
||||
|
||||
if (typeof optionEl.value == 'undefined') {
|
||||
this.logger.warn('Aborting because couldn\'t find the value of an option.', this.question.name);
|
||||
|
||||
return this.questionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
|
||||
const option = {
|
||||
value: optionEl.value,
|
||||
label: optionEl.innerHTML,
|
||||
selected: optionEl.selected
|
||||
};
|
||||
|
||||
if (option.selected) {
|
||||
rowModel.selected = option;
|
||||
}
|
||||
|
||||
rowModel.options.push(option);
|
||||
}
|
||||
|
||||
// Get the accessibility label.
|
||||
const accessibilityLabel = columns[1].querySelector('label.accesshide');
|
||||
rowModel.accessibilityLabel = accessibilityLabel && accessibilityLabel.innerHTML;
|
||||
|
||||
this.question.rows.push(rowModel);
|
||||
}
|
||||
|
||||
this.question.loaded = true;
|
||||
}
|
||||
|
||||
return questionDiv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a question component with a multiple choice (checkbox) or single choice (radio).
|
||||
*
|
||||
* @return {void|HTMLElement} Element containing the question HTML, void if the data is not valid.
|
||||
*/
|
||||
initMultichoiceComponent(): void | HTMLElement {
|
||||
const questionDiv = this.initComponent();
|
||||
|
||||
if (questionDiv) {
|
||||
// Create the model for radio buttons.
|
||||
this.question.singleChoiceModel = {};
|
||||
|
||||
// Get the prompt.
|
||||
this.question.prompt = this.domUtils.getContentsOfElement(questionDiv, '.prompt');
|
||||
|
||||
// Search radio buttons first (single choice).
|
||||
let options = <HTMLInputElement[]> Array.from(questionDiv.querySelectorAll('input[type="radio"]'));
|
||||
if (!options || !options.length) {
|
||||
// Radio buttons not found, it should be a multi answer. Search for checkbox.
|
||||
this.question.multi = true;
|
||||
options = <HTMLInputElement[]> Array.from(questionDiv.querySelectorAll('input[type="checkbox"]'));
|
||||
|
||||
if (!options || !options.length) {
|
||||
// No checkbox found either. Abort.
|
||||
this.logger.warn('Aborting because of no radio and checkbox found.', this.question.name);
|
||||
|
||||
return this.questionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
}
|
||||
|
||||
this.question.options = [];
|
||||
|
||||
for (const i in options) {
|
||||
const element = options[i],
|
||||
option: any = {
|
||||
id: element.id,
|
||||
name: element.name,
|
||||
value: element.value,
|
||||
checked: element.checked,
|
||||
disabled: element.disabled
|
||||
},
|
||||
parent = element.parentElement;
|
||||
|
||||
this.question.optionsName = option.name;
|
||||
|
||||
// Get the label with the question text.
|
||||
const label = questionDiv.querySelector('label[for="' + option.id + '"]');
|
||||
if (label) {
|
||||
option.text = label.innerHTML;
|
||||
|
||||
// Check that we were able to successfully extract options required data.
|
||||
if (typeof option.name != 'undefined' && typeof option.value != 'undefined' &&
|
||||
typeof option.text != 'undefined') {
|
||||
|
||||
if (element.checked) {
|
||||
// If the option is checked and it's a single choice we use the model to select the one.
|
||||
if (!this.question.multi) {
|
||||
this.question.singleChoiceModel = option.value;
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
// Check if answer is correct.
|
||||
if (parent && parent.className.indexOf('incorrect') >= 0) {
|
||||
option.isCorrect = 0;
|
||||
} else if (parent && parent.className.indexOf('correct') >= 0) {
|
||||
option.isCorrect = 1;
|
||||
}
|
||||
|
||||
// Search the feedback.
|
||||
const feedback = parent.querySelector('.specificfeedback');
|
||||
if (feedback) {
|
||||
option.feedback = feedback.innerHTML;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.question.options.push(option);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Something went wrong when extracting the questions data. Abort.
|
||||
this.logger.warn('Aborting because of an error parsing options.', this.question.name, option.name);
|
||||
|
||||
return this.questionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
}
|
||||
|
||||
return questionDiv;
|
||||
}
|
||||
}
|
|
@ -13,6 +13,8 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Injectable, EventEmitter } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreQuestionProvider } from './question';
|
||||
|
@ -26,7 +28,8 @@ export class CoreQuestionHelperProvider {
|
|||
protected div = document.createElement('div'); // A div element to search in HTML code.
|
||||
|
||||
constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider,
|
||||
private questionProvider: CoreQuestionProvider) { }
|
||||
private questionProvider: CoreQuestionProvider, private sitesProvider: CoreSitesProvider,
|
||||
private translate: TranslateService) { }
|
||||
|
||||
/**
|
||||
* Add a behaviour button to the question's "behaviourButtons" property.
|
||||
|
@ -266,6 +269,69 @@ export class CoreQuestionHelperProvider {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the names of all the inputs inside an HTML code.
|
||||
* This function will return an object where the keys are the input names. The values will always be true.
|
||||
* This is in order to make this function compatible with other functions like CoreQuestionProvider.getBasicAnswers.
|
||||
*
|
||||
* @param {string} html HTML code.
|
||||
* @return {any} Object where the keys are the names.
|
||||
*/
|
||||
getAllInputNamesFromHtml(html: string): any {
|
||||
const form = document.createElement('form'),
|
||||
answers = {};
|
||||
|
||||
form.innerHTML = html;
|
||||
|
||||
// Search all input elements.
|
||||
Array.from(form.elements).forEach((element: HTMLInputElement) => {
|
||||
const name = element.name || '';
|
||||
|
||||
// Ignore flag and submit inputs.
|
||||
if (!name || name.match(/_:flagged$/) || element.type == 'submit' || element.tagName == 'BUTTON') {
|
||||
return;
|
||||
}
|
||||
|
||||
answers[this.questionProvider.removeQuestionPrefix(name)] = true;
|
||||
});
|
||||
|
||||
return answers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an HTML code with list of attachments, returns the list of attached files (filename and fileurl).
|
||||
* Please take into account that this function will treat all the anchors in the HTML, you should provide
|
||||
* an HTML containing only the attachments anchors.
|
||||
*
|
||||
* @param {String} html HTML code to search in.
|
||||
* @return {Object[]} Attachments.
|
||||
*/
|
||||
getQuestionAttachmentsFromHtml(html: string): any[] {
|
||||
this.div.innerHTML = html;
|
||||
|
||||
// Remove the filemanager (area to attach files to a question).
|
||||
this.domUtils.removeElement(this.div, 'div[id*=filemanager]');
|
||||
|
||||
// Search the anchors.
|
||||
const anchors = Array.from(this.div.querySelectorAll('a')),
|
||||
attachments = [];
|
||||
|
||||
anchors.forEach((anchor) => {
|
||||
let content = anchor.innerHTML;
|
||||
|
||||
// Check anchor is valid.
|
||||
if (anchor.href && content) {
|
||||
content = this.textUtils.cleanTags(content, true).trim();
|
||||
attachments.push({
|
||||
filename: content,
|
||||
fileurl: anchor.href
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sequence check from a question HTML.
|
||||
*
|
||||
|
@ -299,6 +365,22 @@ export class CoreQuestionHelperProvider {
|
|||
return this.domUtils.getContentsOfElement(this.div, '.validationerror');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if some HTML contains draft file URLs for the current site.
|
||||
*
|
||||
* @param {string} html Question's HTML.
|
||||
* @return {boolean} Whether it contains draft files URLs.
|
||||
*/
|
||||
hasDraftFileUrls(html: string): boolean {
|
||||
let url = this.sitesProvider.getCurrentSite().getURL();
|
||||
if (url.slice(-1) != '/') {
|
||||
url = url += '/';
|
||||
}
|
||||
url += 'draftfile.php';
|
||||
|
||||
return html.indexOf(url) != -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -346,6 +428,30 @@ export class CoreQuestionHelperProvider {
|
|||
question.html = form.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace Moodle's correct/incorrect classes with the Mobile ones.
|
||||
*
|
||||
* @param {HTMLElement} element DOM element.
|
||||
*/
|
||||
replaceCorrectnessClasses(element: HTMLElement): void {
|
||||
this.domUtils.replaceClassesInElement(element, {
|
||||
correct: 'core-question-answer-correct',
|
||||
incorrect: 'core-question-answer-incorrect'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace Moodle's feedback classes with the Mobile ones.
|
||||
*
|
||||
* @param {HTMLElement} element DOM element.
|
||||
*/
|
||||
replaceFeedbackClasses(element: HTMLElement): void {
|
||||
this.domUtils.replaceClassesInElement(element, {
|
||||
outcome: 'core-question-feedback-container core-question-feedback-padding',
|
||||
specificfeedback: 'core-question-feedback-container core-question-feedback-inline'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search a behaviour button in a certain question property containing HTML.
|
||||
*
|
||||
|
@ -392,4 +498,55 @@ export class CoreQuestionHelperProvider {
|
|||
|
||||
onAbort && onAbort.emit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Treat correctness icons, replacing them with local icons and setting click events to show the feedback if needed.
|
||||
*
|
||||
* @param {HTMLElement} element DOM element.
|
||||
*/
|
||||
treatCorrectnessIcons(element: HTMLElement, component?: string, componentId?: number): void {
|
||||
|
||||
const icons = <HTMLImageElement[]> Array.from(element.querySelectorAll('img.icon, img.questioncorrectnessicon'));
|
||||
icons.forEach((icon) => {
|
||||
// Replace the icon with the font version.
|
||||
if (icon.src) {
|
||||
const newIcon: any = document.createElement('i');
|
||||
|
||||
if (icon.src.indexOf('incorrect') > -1) {
|
||||
newIcon.className = 'icon fa fa-remove text-danger fa-fw questioncorrectnessicon';
|
||||
} else if (icon.src.indexOf('correct') > -1) {
|
||||
newIcon.className = 'icon fa fa-check text-success fa-fw questioncorrectnessicon';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
newIcon.title = icon.title;
|
||||
newIcon.ariaLabel = icon.title;
|
||||
icon.parentNode.replaceChild(newIcon, icon);
|
||||
}
|
||||
});
|
||||
|
||||
const spans = Array.from(element.querySelectorAll('.feedbackspan.accesshide'));
|
||||
spans.forEach((span) => {
|
||||
// Search if there's a hidden feedback for this element.
|
||||
const icon = <HTMLElement> span.previousSibling;
|
||||
if (!icon) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!icon.classList.contains('icon') && !icon.classList.contains('questioncorrectnessicon')) {
|
||||
return;
|
||||
}
|
||||
|
||||
icon.classList.add('questioncorrectnessicon');
|
||||
|
||||
if (span.innerHTML) {
|
||||
// There's a hidden feedback, show it when the icon is clicked.
|
||||
icon.addEventListener('click', (event) => {
|
||||
const title = this.translate.instant('core.question.feedback');
|
||||
this.textUtils.expandText(title, span.innerHTML, component, componentId);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue