From 7792efff5544da8e91523e7064886737a804181c Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Fri, 16 Mar 2018 10:40:20 +0100
Subject: [PATCH] MOBILE-2388 qbehaviour: Implement all behaviour addons

---
 .../qbehaviour/adaptive/adaptive.module.ts    |  30 ++++
 .../qbehaviour/adaptive/providers/handler.ts  |  56 +++++++
 .../adaptivenopenalty.module.ts               |  30 ++++
 .../adaptivenopenalty/providers/handler.ts    |  56 +++++++
 .../deferredcbm/component/deferredcbm.html    |   6 +
 .../deferredcbm/component/deferredcbm.ts      |  36 ++++
 .../deferredcbm/deferredcbm.module.ts         |  46 ++++++
 .../deferredcbm/providers/handler.ts          | 116 +++++++++++++
 .../deferredfeedback.module.ts                |  30 ++++
 .../deferredfeedback/providers/handler.ts     | 154 ++++++++++++++++++
 .../immediatecbm/immediatecbm.module.ts       |  30 ++++
 .../immediatecbm/providers/handler.ts         |  59 +++++++
 .../immediatefeedback.module.ts               |  30 ++++
 .../immediatefeedback/providers/handler.ts    |  56 +++++++
 .../component/informationitem.html            |   1 +
 .../component/informationitem.ts              |  36 ++++
 .../informationitem/informationitem.module.ts |  42 +++++
 .../informationitem/providers/handler.ts      |  73 +++++++++
 .../interactive/interactive.module.ts         |  30 ++++
 .../interactive/providers/handler.ts          |  56 +++++++
 .../interactivecountback.module.ts            |  30 ++++
 .../interactivecountback/providers/handler.ts |  56 +++++++
 .../manualgraded/manualgraded.module.ts       |  30 ++++
 .../manualgraded/providers/handler.ts         | 147 +++++++++++++++++
 src/addon/qbehaviour/qbehaviour.module.ts     |  44 +++++
 src/app/app.module.ts                         |   4 +-
 src/core/question/providers/helper.ts         |  89 ++++++++++
 27 files changed, 1372 insertions(+), 1 deletion(-)
 create mode 100644 src/addon/qbehaviour/adaptive/adaptive.module.ts
 create mode 100644 src/addon/qbehaviour/adaptive/providers/handler.ts
 create mode 100644 src/addon/qbehaviour/adaptivenopenalty/adaptivenopenalty.module.ts
 create mode 100644 src/addon/qbehaviour/adaptivenopenalty/providers/handler.ts
 create mode 100644 src/addon/qbehaviour/deferredcbm/component/deferredcbm.html
 create mode 100644 src/addon/qbehaviour/deferredcbm/component/deferredcbm.ts
 create mode 100644 src/addon/qbehaviour/deferredcbm/deferredcbm.module.ts
 create mode 100644 src/addon/qbehaviour/deferredcbm/providers/handler.ts
 create mode 100644 src/addon/qbehaviour/deferredfeedback/deferredfeedback.module.ts
 create mode 100644 src/addon/qbehaviour/deferredfeedback/providers/handler.ts
 create mode 100644 src/addon/qbehaviour/immediatecbm/immediatecbm.module.ts
 create mode 100644 src/addon/qbehaviour/immediatecbm/providers/handler.ts
 create mode 100644 src/addon/qbehaviour/immediatefeedback/immediatefeedback.module.ts
 create mode 100644 src/addon/qbehaviour/immediatefeedback/providers/handler.ts
 create mode 100644 src/addon/qbehaviour/informationitem/component/informationitem.html
 create mode 100644 src/addon/qbehaviour/informationitem/component/informationitem.ts
 create mode 100644 src/addon/qbehaviour/informationitem/informationitem.module.ts
 create mode 100644 src/addon/qbehaviour/informationitem/providers/handler.ts
 create mode 100644 src/addon/qbehaviour/interactive/interactive.module.ts
 create mode 100644 src/addon/qbehaviour/interactive/providers/handler.ts
 create mode 100644 src/addon/qbehaviour/interactivecountback/interactivecountback.module.ts
 create mode 100644 src/addon/qbehaviour/interactivecountback/providers/handler.ts
 create mode 100644 src/addon/qbehaviour/manualgraded/manualgraded.module.ts
 create mode 100644 src/addon/qbehaviour/manualgraded/providers/handler.ts
 create mode 100644 src/addon/qbehaviour/qbehaviour.module.ts

diff --git a/src/addon/qbehaviour/adaptive/adaptive.module.ts b/src/addon/qbehaviour/adaptive/adaptive.module.ts
new file mode 100644
index 000000000..adbae973f
--- /dev/null
+++ b/src/addon/qbehaviour/adaptive/adaptive.module.ts
@@ -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 { AddonQbehaviourAdaptiveHandler } from './providers/handler';
+import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate';
+
+@NgModule({
+    declarations: [
+    ],
+    providers: [
+        AddonQbehaviourAdaptiveHandler
+    ]
+})
+export class AddonQbehaviourAdaptiveModule {
+    constructor(behaviourDelegate: CoreQuestionBehaviourDelegate, handler: AddonQbehaviourAdaptiveHandler) {
+        behaviourDelegate.registerHandler(handler);
+    }
+}
diff --git a/src/addon/qbehaviour/adaptive/providers/handler.ts b/src/addon/qbehaviour/adaptive/providers/handler.ts
new file mode 100644
index 000000000..37a5e908a
--- /dev/null
+++ b/src/addon/qbehaviour/adaptive/providers/handler.ts
@@ -0,0 +1,56 @@
+
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate';
+import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
+
+/**
+ * Handler to support adaptive question behaviour.
+ */
+@Injectable()
+export class AddonQbehaviourAdaptiveHandler implements CoreQuestionBehaviourHandler {
+    name = 'AddonQbehaviourAdaptive';
+    type = 'adaptive';
+
+    constructor(private questionHelper: CoreQuestionHelperProvider) {
+        // Nothing to do.
+    }
+
+    /**
+     * Handle a question behaviour.
+     * If the behaviour requires a submit button, it should add it to question.behaviourButtons.
+     * If the behaviour requires to show some extra data, it should return the components to render it.
+     *
+     * @param {any} question The question.
+     * @return {any[]|Promise<any[]>} Components (or promise resolved with components) to render some extra data in the question
+     *                                (e.g. certainty options). Don't return anything if no extra data is required.
+     */
+    handleQuestion(question: any): any[] | Promise<any[]> {
+        // Just extract the button, it doesn't need any specific component.
+        this.questionHelper.extractQbehaviourButtons(question);
+
+        return;
+    }
+
+    /**
+     * Whether or not the handler is enabled on a site level.
+     *
+     * @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
+     */
+    isEnabled(): boolean | Promise<boolean> {
+        return true;
+    }
+}
diff --git a/src/addon/qbehaviour/adaptivenopenalty/adaptivenopenalty.module.ts b/src/addon/qbehaviour/adaptivenopenalty/adaptivenopenalty.module.ts
new file mode 100644
index 000000000..6fa4c5683
--- /dev/null
+++ b/src/addon/qbehaviour/adaptivenopenalty/adaptivenopenalty.module.ts
@@ -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 { AddonQbehaviourAdaptiveNoPenaltyHandler } from './providers/handler';
+import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate';
+
+@NgModule({
+    declarations: [
+    ],
+    providers: [
+        AddonQbehaviourAdaptiveNoPenaltyHandler
+    ]
+})
+export class AddonQbehaviourAdaptiveNoPenaltyModule {
+    constructor(behaviourDelegate: CoreQuestionBehaviourDelegate, handler: AddonQbehaviourAdaptiveNoPenaltyHandler) {
+        behaviourDelegate.registerHandler(handler);
+    }
+}
diff --git a/src/addon/qbehaviour/adaptivenopenalty/providers/handler.ts b/src/addon/qbehaviour/adaptivenopenalty/providers/handler.ts
new file mode 100644
index 000000000..3f788eea5
--- /dev/null
+++ b/src/addon/qbehaviour/adaptivenopenalty/providers/handler.ts
@@ -0,0 +1,56 @@
+
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate';
+import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
+
+/**
+ * Handler to support adaptive no penalty question behaviour.
+ */
+@Injectable()
+export class AddonQbehaviourAdaptiveNoPenaltyHandler implements CoreQuestionBehaviourHandler {
+    name = 'AddonQbehaviourAdaptiveNoPenalty';
+    type = 'adaptivenopenalty';
+
+    constructor(private questionHelper: CoreQuestionHelperProvider) {
+        // Nothing to do.
+    }
+
+    /**
+     * Handle a question behaviour.
+     * If the behaviour requires a submit button, it should add it to question.behaviourButtons.
+     * If the behaviour requires to show some extra data, it should return the components to render it.
+     *
+     * @param {any} question The question.
+     * @return {any[]|Promise<any[]>} Components (or promise resolved with components) to render some extra data in the question
+     *                                (e.g. certainty options). Don't return anything if no extra data is required.
+     */
+    handleQuestion(question: any): any[] | Promise<any[]> {
+        // Just extract the button, it doesn't need any specific component.
+        this.questionHelper.extractQbehaviourButtons(question);
+
+        return;
+    }
+
+    /**
+     * Whether or not the handler is enabled on a site level.
+     *
+     * @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
+     */
+    isEnabled(): boolean | Promise<boolean> {
+        return true;
+    }
+}
diff --git a/src/addon/qbehaviour/deferredcbm/component/deferredcbm.html b/src/addon/qbehaviour/deferredcbm/component/deferredcbm.html
new file mode 100644
index 000000000..07c1ffc7d
--- /dev/null
+++ b/src/addon/qbehaviour/deferredcbm/component/deferredcbm.html
@@ -0,0 +1,6 @@
+<ion-item text-wrap class="addon-qbehaviour-deferredcbm-certainty-title" *ngIf="question.behaviourCertaintyOptions && question.behaviourCertaintyOptions.length">
+    <p>{{ 'core.question.certainty' | translate }}</p>
+</ion-item>
+<ion-radio *ngFor="let option of question.behaviourCertaintyOptions" id="{{option.id}}" name="{{option.name}}" [ngModel]="question.behaviourCertaintySelected" [value]="option.value" [disabled]="option.disabled">
+    <p><core-format-text [component]="component" [componentId]="componentId" [text]="option.text"></core-format-text></p>
+</ion-radio>
diff --git a/src/addon/qbehaviour/deferredcbm/component/deferredcbm.ts b/src/addon/qbehaviour/deferredcbm/component/deferredcbm.ts
new file mode 100644
index 000000000..a6c822770
--- /dev/null
+++ b/src/addon/qbehaviour/deferredcbm/component/deferredcbm.ts
@@ -0,0 +1,36 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Input, EventEmitter } from '@angular/core';
+
+/**
+ * Component to render the deferred CBM in a question.
+ */
+@Component({
+    selector: 'addon-qbehaviour-deferredcbm',
+    templateUrl: 'deferredcbm.html'
+})
+export class AddonQbehaviourDeferredCBMComponent {
+    @Input() question: any; // The question.
+    @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.
+
+    constructor() {
+        // Nothing to do.
+    }
+}
diff --git a/src/addon/qbehaviour/deferredcbm/deferredcbm.module.ts b/src/addon/qbehaviour/deferredcbm/deferredcbm.module.ts
new file mode 100644
index 000000000..3948f8abe
--- /dev/null
+++ b/src/addon/qbehaviour/deferredcbm/deferredcbm.module.ts
@@ -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 { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate';
+import { CoreDirectivesModule } from '@directives/directives.module';
+import { AddonQbehaviourDeferredCBMHandler } from './providers/handler';
+import { AddonQbehaviourDeferredCBMComponent } from './component/deferredcbm';
+
+@NgModule({
+    declarations: [
+        AddonQbehaviourDeferredCBMComponent
+    ],
+    imports: [
+        IonicModule,
+        TranslateModule.forChild(),
+        CoreDirectivesModule
+    ],
+    providers: [
+        AddonQbehaviourDeferredCBMHandler
+    ],
+    exports: [
+        AddonQbehaviourDeferredCBMComponent
+    ],
+    entryComponents: [
+        AddonQbehaviourDeferredCBMComponent
+    ]
+})
+export class AddonQbehaviourDeferredCBMModule {
+    constructor(behaviourDelegate: CoreQuestionBehaviourDelegate, handler: AddonQbehaviourDeferredCBMHandler) {
+        behaviourDelegate.registerHandler(handler);
+    }
+}
diff --git a/src/addon/qbehaviour/deferredcbm/providers/handler.ts b/src/addon/qbehaviour/deferredcbm/providers/handler.ts
new file mode 100644
index 000000000..e15d47c4d
--- /dev/null
+++ b/src/addon/qbehaviour/deferredcbm/providers/handler.ts
@@ -0,0 +1,116 @@
+
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate';
+import { CoreQuestionDelegate } from '@core/question/providers/delegate';
+import { CoreQuestionState } from '@core/question/providers/question';
+import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
+import { AddonQbehaviourDeferredFeedbackHandler } from '@addon/qbehaviour/deferredfeedback/providers/handler';
+import { AddonQbehaviourDeferredCBMComponent } from '../component/deferredcbm';
+
+/**
+ * Handler to support deferred CBM question behaviour.
+ */
+@Injectable()
+export class AddonQbehaviourDeferredCBMHandler implements CoreQuestionBehaviourHandler {
+    name = 'AddonQbehaviourDeferredCBM';
+    type = 'deferredcbm';
+
+    constructor(private questionDelegate: CoreQuestionDelegate, private questionHelper: CoreQuestionHelperProvider,
+            private deferredFeedbackHandler: AddonQbehaviourDeferredFeedbackHandler) {
+        // Nothing to do.
+    }
+
+    /**
+     * Determine a question new state based on its answer(s).
+     *
+     * @param {string} component Component the question belongs to.
+     * @param {number} attemptId Attempt ID the question belongs to.
+     * @param {any} question The question.
+     * @param {string} [siteId] Site ID. If not defined, current site.
+     * @return {CoreQuestionState|Promise<CoreQuestionState>} New state (or promise resolved with state).
+     */
+    determineNewState(component: string, attemptId: number, question: any, siteId?: string)
+            : CoreQuestionState | Promise<CoreQuestionState> {
+        // Depends on deferredfeedback.
+        return this.deferredFeedbackHandler.determineNewStateDeferred(component, attemptId, question, siteId,
+            this.isCompleteResponse.bind(this), this.isSameResponse.bind(this));
+    }
+
+    /**
+     * Handle a question behaviour.
+     * If the behaviour requires a submit button, it should add it to question.behaviourButtons.
+     * If the behaviour requires to show some extra data, it should return the components to render it.
+     *
+     * @param {any} question The question.
+     * @return {any[]|Promise<any[]>} Components (or promise resolved with components) to render some extra data in the question
+     *                                (e.g. certainty options). Don't return anything if no extra data is required.
+     */
+    handleQuestion(question: any): any[] | Promise<any[]> {
+        if (this.questionHelper.extractQbehaviourCBM(question)) {
+            return [AddonQbehaviourDeferredCBMComponent];
+        }
+    }
+
+    /**
+     * 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.
+     */
+    protected isCompleteResponse(question: any, answers: any): number {
+        // First check if the question answer is complete.
+        const complete = this.questionDelegate.isCompleteResponse(question, answers);
+        if (complete > 0) {
+            // Answer is complete, check the user answered CBM too.
+            return answers['-certainty'] ? 1 : 0;
+        }
+
+        return complete;
+    }
+
+    /**
+     * 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 two responses are the same.
+     *
+     * @param {any} question Question.
+     * @param {any} prevAnswers Object with the previous question answers.
+     * @param {any} prevBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...).
+     * @param {any} newAnswers Object with the new question answers.
+     * @param {any} newBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...).
+     * @return {boolean} Whether they're the same.
+     */
+    protected isSameResponse(question: any, prevAnswers: any, prevBasicAnswers: any, newAnswers: any, newBasicAnswers: any)
+            : boolean {
+        // First check if the question answer is the same.
+        const same = this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers);
+        if (same) {
+            // Same response, check the CBM is the same too.
+            return prevAnswers['-certainty'] == newAnswers['-certainty'];
+        }
+
+        return same;
+    }
+}
diff --git a/src/addon/qbehaviour/deferredfeedback/deferredfeedback.module.ts b/src/addon/qbehaviour/deferredfeedback/deferredfeedback.module.ts
new file mode 100644
index 000000000..99f39f0c5
--- /dev/null
+++ b/src/addon/qbehaviour/deferredfeedback/deferredfeedback.module.ts
@@ -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 { AddonQbehaviourDeferredFeedbackHandler } from './providers/handler';
+import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate';
+
+@NgModule({
+    declarations: [
+    ],
+    providers: [
+        AddonQbehaviourDeferredFeedbackHandler
+    ]
+})
+export class AddonQbehaviourDeferredFeedbackModule {
+    constructor(behaviourDelegate: CoreQuestionBehaviourDelegate, handler: AddonQbehaviourDeferredFeedbackHandler) {
+        behaviourDelegate.registerHandler(handler);
+    }
+}
diff --git a/src/addon/qbehaviour/deferredfeedback/providers/handler.ts b/src/addon/qbehaviour/deferredfeedback/providers/handler.ts
new file mode 100644
index 000000000..8a3d82ef5
--- /dev/null
+++ b/src/addon/qbehaviour/deferredfeedback/providers/handler.ts
@@ -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 } from '@angular/core';
+import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate';
+import { CoreQuestionDelegate } from '@core/question/providers/delegate';
+import { CoreQuestionProvider, CoreQuestionState } from '@core/question/providers/question';
+
+/**
+ * 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.
+ */
+export type isCompleteResponseFunction = (question: any, answers: any) => number;
+
+/**
+ * Check if two responses are the same.
+ *
+ * @param {any} question Question.
+ * @param {any} prevAnswers Object with the previous question answers.
+ * @param {any} prevBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...).
+ * @param {any} newAnswers Object with the new question answers.
+ * @param {any} newBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...).
+ * @return {boolean} Whether they're the same.
+ */
+export type isSameResponseFunction = (question: any, prevAnswers: any, prevBasicAnswers: any, newAnswers: any,
+        newBasicAnswers: any) => boolean;
+
+/**
+ * Handler to support deferred feedback question behaviour.
+ */
+@Injectable()
+export class AddonQbehaviourDeferredFeedbackHandler implements CoreQuestionBehaviourHandler {
+    name = 'AddonQbehaviourDeferredFeedback';
+    type = 'deferredfeedback';
+
+    constructor(private questionDelegate: CoreQuestionDelegate, private questionProvider: CoreQuestionProvider) {
+        // Nothing to do.
+    }
+
+    /**
+     * Determine a question new state based on its answer(s).
+     *
+     * @param {string} component Component the question belongs to.
+     * @param {number} attemptId Attempt ID the question belongs to.
+     * @param {any} question The question.
+     * @param {string} [siteId] Site ID. If not defined, current site.
+     * @return {CoreQuestionState|Promise<CoreQuestionState>} New state (or promise resolved with state).
+     */
+    determineNewState(component: string, attemptId: number, question: any, siteId?: string)
+            : CoreQuestionState | Promise<CoreQuestionState> {
+        return this.determineNewStateDeferred(component, attemptId, question, siteId);
+    }
+
+    /**
+     * Determine a question new state based on its answer(s) for deferred question behaviour.
+     *
+     * @param {string} component Component the question belongs to.
+     * @param {number} attemptId Attempt ID the question belongs to.
+     * @param {any} question The question.
+     * @param {string} [siteId] Site ID. If not defined, current site.
+     * @param {isCompleteResponseFunction} [isCompleteFn] Function to override the default isCompleteResponse check.
+     * @param {isSameResponseFunction} [isSameFn] Function to override the default isSameResponse check.
+     * @return {Promise<CoreQuestionState>} Promise resolved with state.
+     */
+    determineNewStateDeferred(component: string, attemptId: number, question: any, siteId?: string,
+            isCompleteFn?: isCompleteResponseFunction, isSameFn?: isSameResponseFunction): Promise<CoreQuestionState> {
+
+        // Check if we have local data for the question.
+        return this.questionProvider.getQuestion(component, attemptId, question.slot, siteId).catch(() => {
+            // No entry found, use the original data.
+            return question;
+        }).then((dbQuestion) => {
+            const state = this.questionProvider.getState(dbQuestion.state);
+
+            if (state.finished || !state.active) {
+                // Question is finished, it cannot change.
+                return state;
+            }
+
+            // We need to check if the answers have changed. Retrieve current stored answers.
+            return this.questionProvider.getQuestionAnswers(component, attemptId, question.slot, false, siteId)
+                    .then((prevAnswers) => {
+
+                const newBasicAnswers = this.questionProvider.getBasicAnswers(question.answers);
+
+                prevAnswers = this.questionProvider.convertAnswersArrayToObject(prevAnswers, true);
+                const prevBasicAnswers = this.questionProvider.getBasicAnswers(prevAnswers);
+
+                // If answers haven't changed the state is the same.
+                if (isSameFn) {
+                    if (isSameFn(question, prevAnswers, prevBasicAnswers, question.answers, newBasicAnswers)) {
+                        return state;
+                    }
+                } else {
+                    if (this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers)) {
+                        return state;
+                    }
+                }
+
+                // Answers have changed. Now check if the response is complete and calculate the new state.
+                let complete: number,
+                    newState: string;
+                if (isCompleteFn) {
+                    // Pass all the answers since some behaviours might need the extra data.
+                    complete = isCompleteFn(question, question.answers);
+                } else {
+                    // Only pass the basic answers since questions should be independent of extra data.
+                    complete = this.questionDelegate.isCompleteResponse(question, newBasicAnswers);
+                }
+
+                if (complete < 0) {
+                    newState = 'unknown';
+                } else if (complete > 0) {
+                    newState = 'complete';
+                } else {
+                    const gradable = this.questionDelegate.isGradableResponse(question, newBasicAnswers);
+                    if (gradable < 0) {
+                        newState = 'unknown';
+                    } else if (gradable > 0) {
+                        newState = 'invalid';
+                    } else {
+                        newState = 'todo';
+                    }
+                }
+
+                return this.questionProvider.getState(newState);
+            });
+        });
+    }
+
+    /**
+     * 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;
+    }
+}
diff --git a/src/addon/qbehaviour/immediatecbm/immediatecbm.module.ts b/src/addon/qbehaviour/immediatecbm/immediatecbm.module.ts
new file mode 100644
index 000000000..a13e3e130
--- /dev/null
+++ b/src/addon/qbehaviour/immediatecbm/immediatecbm.module.ts
@@ -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 { AddonQbehaviourImmediateCBMHandler } from './providers/handler';
+import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate';
+
+@NgModule({
+    declarations: [
+    ],
+    providers: [
+        AddonQbehaviourImmediateCBMHandler
+    ]
+})
+export class AddonQbehaviourImmediateCBMModule {
+    constructor(behaviourDelegate: CoreQuestionBehaviourDelegate, handler: AddonQbehaviourImmediateCBMHandler) {
+        behaviourDelegate.registerHandler(handler);
+    }
+}
diff --git a/src/addon/qbehaviour/immediatecbm/providers/handler.ts b/src/addon/qbehaviour/immediatecbm/providers/handler.ts
new file mode 100644
index 000000000..0f1493809
--- /dev/null
+++ b/src/addon/qbehaviour/immediatecbm/providers/handler.ts
@@ -0,0 +1,59 @@
+
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate';
+import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
+import { AddonQbehaviourDeferredCBMComponent } from '@addon/qbehaviour/deferredcbm/component/deferredcbm';
+
+/**
+ * Handler to support immediate CBM question behaviour.
+ */
+@Injectable()
+export class AddonQbehaviourImmediateCBMHandler implements CoreQuestionBehaviourHandler {
+    name = 'AddonQbehaviourImmediateCBM';
+    type = 'immediatecbm';
+
+    constructor(private questionHelper: CoreQuestionHelperProvider) {
+        // Nothing to do.
+    }
+
+    /**
+     * Handle a question behaviour.
+     * If the behaviour requires a submit button, it should add it to question.behaviourButtons.
+     * If the behaviour requires to show some extra data, it should return the components to render it.
+     *
+     * @param {any} question The question.
+     * @return {any[]|Promise<any[]>} Components (or promise resolved with components) to render some extra data in the question
+     *                                (e.g. certainty options). Don't return anything if no extra data is required.
+     */
+    handleQuestion(question: any): any[] | Promise<any[]> {
+        // Just extract the button, it doesn't need any specific component.
+        this.questionHelper.extractQbehaviourButtons(question);
+        if (this.questionHelper.extractQbehaviourCBM(question)) {
+            // Depends on deferredcbm.
+            return [AddonQbehaviourDeferredCBMComponent];
+        }
+    }
+
+    /**
+     * 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;
+    }
+}
diff --git a/src/addon/qbehaviour/immediatefeedback/immediatefeedback.module.ts b/src/addon/qbehaviour/immediatefeedback/immediatefeedback.module.ts
new file mode 100644
index 000000000..3d5e5ed66
--- /dev/null
+++ b/src/addon/qbehaviour/immediatefeedback/immediatefeedback.module.ts
@@ -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 { AddonQbehaviourImmediateFeedbackHandler } from './providers/handler';
+import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate';
+
+@NgModule({
+    declarations: [
+    ],
+    providers: [
+        AddonQbehaviourImmediateFeedbackHandler
+    ]
+})
+export class AddonQbehaviourImmediateFeedbackModule {
+    constructor(behaviourDelegate: CoreQuestionBehaviourDelegate, handler: AddonQbehaviourImmediateFeedbackHandler) {
+        behaviourDelegate.registerHandler(handler);
+    }
+}
diff --git a/src/addon/qbehaviour/immediatefeedback/providers/handler.ts b/src/addon/qbehaviour/immediatefeedback/providers/handler.ts
new file mode 100644
index 000000000..304e71bdc
--- /dev/null
+++ b/src/addon/qbehaviour/immediatefeedback/providers/handler.ts
@@ -0,0 +1,56 @@
+
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate';
+import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
+
+/**
+ * Handler to support immediate feedback question behaviour.
+ */
+@Injectable()
+export class AddonQbehaviourImmediateFeedbackHandler implements CoreQuestionBehaviourHandler {
+    name = 'AddonQbehaviourImmediateFeedback';
+    type = 'immediatefeedback';
+
+    constructor(private questionHelper: CoreQuestionHelperProvider) {
+        // Nothing to do.
+    }
+
+    /**
+     * Handle a question behaviour.
+     * If the behaviour requires a submit button, it should add it to question.behaviourButtons.
+     * If the behaviour requires to show some extra data, it should return the components to render it.
+     *
+     * @param {any} question The question.
+     * @return {any[]|Promise<any[]>} Components (or promise resolved with components) to render some extra data in the question
+     *                                (e.g. certainty options). Don't return anything if no extra data is required.
+     */
+    handleQuestion(question: any): any[] | Promise<any[]> {
+        // Just extract the button, it doesn't need any specific component.
+        this.questionHelper.extractQbehaviourButtons(question);
+
+        return;
+    }
+
+    /**
+     * Whether or not the handler is enabled on a site level.
+     *
+     * @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
+     */
+    isEnabled(): boolean | Promise<boolean> {
+        return true;
+    }
+}
diff --git a/src/addon/qbehaviour/informationitem/component/informationitem.html b/src/addon/qbehaviour/informationitem/component/informationitem.html
new file mode 100644
index 000000000..e7a0724c1
--- /dev/null
+++ b/src/addon/qbehaviour/informationitem/component/informationitem.html
@@ -0,0 +1 @@
+<input *ngIf="question.behaviourSeenInput" type="hidden" name="{{question.behaviourSeenInput.name}}" value="{{question.behaviourSeenInput.value}}" >
diff --git a/src/addon/qbehaviour/informationitem/component/informationitem.ts b/src/addon/qbehaviour/informationitem/component/informationitem.ts
new file mode 100644
index 000000000..35e37f3f3
--- /dev/null
+++ b/src/addon/qbehaviour/informationitem/component/informationitem.ts
@@ -0,0 +1,36 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Input, EventEmitter } from '@angular/core';
+
+/**
+ * Component to render a "seen" hidden input for informationitem question behaviour.
+ */
+@Component({
+    selector: 'addon-qbehaviour-informationitem',
+    templateUrl: 'informationitem.html'
+})
+export class AddonQbehaviourInformationItemComponent {
+    @Input() question: any; // The question.
+    @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.
+
+    constructor() {
+        // Nothing to do.
+    }
+}
diff --git a/src/addon/qbehaviour/informationitem/informationitem.module.ts b/src/addon/qbehaviour/informationitem/informationitem.module.ts
new file mode 100644
index 000000000..0f45881a0
--- /dev/null
+++ b/src/addon/qbehaviour/informationitem/informationitem.module.ts
@@ -0,0 +1,42 @@
+// (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 { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate';
+import { AddonQbehaviourInformationItemHandler } from './providers/handler';
+import { AddonQbehaviourInformationItemComponent } from './component/informationitem';
+
+@NgModule({
+    declarations: [
+        AddonQbehaviourInformationItemComponent
+    ],
+    imports: [
+        IonicModule
+    ],
+    providers: [
+        AddonQbehaviourInformationItemHandler
+    ],
+    exports: [
+        AddonQbehaviourInformationItemComponent
+    ],
+    entryComponents: [
+        AddonQbehaviourInformationItemComponent
+    ]
+})
+export class AddonQbehaviourInformationItemModule {
+    constructor(behaviourDelegate: CoreQuestionBehaviourDelegate, handler: AddonQbehaviourInformationItemHandler) {
+        behaviourDelegate.registerHandler(handler);
+    }
+}
diff --git a/src/addon/qbehaviour/informationitem/providers/handler.ts b/src/addon/qbehaviour/informationitem/providers/handler.ts
new file mode 100644
index 000000000..ac2df43f2
--- /dev/null
+++ b/src/addon/qbehaviour/informationitem/providers/handler.ts
@@ -0,0 +1,73 @@
+
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate';
+import { CoreQuestionProvider, CoreQuestionState } from '@core/question/providers/question';
+import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
+import { AddonQbehaviourInformationItemComponent } from '../component/informationitem';
+
+/**
+ * Handler to support information item question behaviour.
+ */
+@Injectable()
+export class AddonQbehaviourInformationItemHandler implements CoreQuestionBehaviourHandler {
+    name = 'AddonQbehaviourInformationItem';
+    type = 'informationitem';
+
+    constructor(private questionHelper: CoreQuestionHelperProvider, private questionProvider: CoreQuestionProvider) { }
+
+    /**
+     * Determine a question new state based on its answer(s).
+     *
+     * @param {string} component Component the question belongs to.
+     * @param {number} attemptId Attempt ID the question belongs to.
+     * @param {any} question The question.
+     * @param {string} [siteId] Site ID. If not defined, current site.
+     * @return {CoreQuestionState|Promise<CoreQuestionState>} New state (or promise resolved with state).
+     */
+    determineNewState(component: string, attemptId: number, question: any, siteId?: string)
+            : CoreQuestionState | Promise<CoreQuestionState> {
+        if (question.answers['-seen']) {
+            return this.questionProvider.getState('complete');
+        }
+
+        return this.questionProvider.getState(question.state || 'todo');
+    }
+
+    /**
+     * Handle a question behaviour.
+     * If the behaviour requires a submit button, it should add it to question.behaviourButtons.
+     * If the behaviour requires to show some extra data, it should return the components to render it.
+     *
+     * @param {any} question The question.
+     * @return {any[]|Promise<any[]>} Components (or promise resolved with components) to render some extra data in the question
+     *                                (e.g. certainty options). Don't return anything if no extra data is required.
+     */
+    handleQuestion(question: any): any[] | Promise<any[]> {
+        if (this.questionHelper.extractQbehaviourSeenInput(question)) {
+            return [AddonQbehaviourInformationItemComponent];
+        }
+    }
+
+    /**
+     * 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;
+    }
+}
diff --git a/src/addon/qbehaviour/interactive/interactive.module.ts b/src/addon/qbehaviour/interactive/interactive.module.ts
new file mode 100644
index 000000000..ab30d3e0a
--- /dev/null
+++ b/src/addon/qbehaviour/interactive/interactive.module.ts
@@ -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 { AddonQbehaviourInteractiveHandler } from './providers/handler';
+import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate';
+
+@NgModule({
+    declarations: [
+    ],
+    providers: [
+        AddonQbehaviourInteractiveHandler
+    ]
+})
+export class AddonQbehaviourInteractiveModule {
+    constructor(behaviourDelegate: CoreQuestionBehaviourDelegate, handler: AddonQbehaviourInteractiveHandler) {
+        behaviourDelegate.registerHandler(handler);
+    }
+}
diff --git a/src/addon/qbehaviour/interactive/providers/handler.ts b/src/addon/qbehaviour/interactive/providers/handler.ts
new file mode 100644
index 000000000..af6d6024c
--- /dev/null
+++ b/src/addon/qbehaviour/interactive/providers/handler.ts
@@ -0,0 +1,56 @@
+
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate';
+import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
+
+/**
+ * Handler to support interactive question behaviour.
+ */
+@Injectable()
+export class AddonQbehaviourInteractiveHandler implements CoreQuestionBehaviourHandler {
+    name = 'AddonQbehaviourInteractive';
+    type = 'interactive';
+
+    constructor(private questionHelper: CoreQuestionHelperProvider) {
+        // Nothing to do.
+    }
+
+    /**
+     * Handle a question behaviour.
+     * If the behaviour requires a submit button, it should add it to question.behaviourButtons.
+     * If the behaviour requires to show some extra data, it should return the components to render it.
+     *
+     * @param {any} question The question.
+     * @return {any[]|Promise<any[]>} Components (or promise resolved with components) to render some extra data in the question
+     *                                (e.g. certainty options). Don't return anything if no extra data is required.
+     */
+    handleQuestion(question: any): any[] | Promise<any[]> {
+        // Just extract the button, it doesn't need any specific component.
+        this.questionHelper.extractQbehaviourButtons(question);
+
+        return;
+    }
+
+    /**
+     * Whether or not the handler is enabled on a site level.
+     *
+     * @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
+     */
+    isEnabled(): boolean | Promise<boolean> {
+        return true;
+    }
+}
diff --git a/src/addon/qbehaviour/interactivecountback/interactivecountback.module.ts b/src/addon/qbehaviour/interactivecountback/interactivecountback.module.ts
new file mode 100644
index 000000000..bbe2dab24
--- /dev/null
+++ b/src/addon/qbehaviour/interactivecountback/interactivecountback.module.ts
@@ -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 { AddonQbehaviourInteractiveCountbackHandler } from './providers/handler';
+import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate';
+
+@NgModule({
+    declarations: [
+    ],
+    providers: [
+        AddonQbehaviourInteractiveCountbackHandler
+    ]
+})
+export class AddonQbehaviourInteractiveCountbackModule {
+    constructor(behaviourDelegate: CoreQuestionBehaviourDelegate, handler: AddonQbehaviourInteractiveCountbackHandler) {
+        behaviourDelegate.registerHandler(handler);
+    }
+}
diff --git a/src/addon/qbehaviour/interactivecountback/providers/handler.ts b/src/addon/qbehaviour/interactivecountback/providers/handler.ts
new file mode 100644
index 000000000..612abb104
--- /dev/null
+++ b/src/addon/qbehaviour/interactivecountback/providers/handler.ts
@@ -0,0 +1,56 @@
+
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate';
+import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
+
+/**
+ * Handler to support interactive countback question behaviour.
+ */
+@Injectable()
+export class AddonQbehaviourInteractiveCountbackHandler implements CoreQuestionBehaviourHandler {
+    name = 'AddonQbehaviourInteractiveCountback';
+    type = 'interactivecountback';
+
+    constructor(private questionHelper: CoreQuestionHelperProvider) {
+        // Nothing to do.
+    }
+
+    /**
+     * Handle a question behaviour.
+     * If the behaviour requires a submit button, it should add it to question.behaviourButtons.
+     * If the behaviour requires to show some extra data, it should return the components to render it.
+     *
+     * @param {any} question The question.
+     * @return {any[]|Promise<any[]>} Components (or promise resolved with components) to render some extra data in the question
+     *                                (e.g. certainty options). Don't return anything if no extra data is required.
+     */
+    handleQuestion(question: any): any[] | Promise<any[]> {
+        // Just extract the button, it doesn't need any specific component.
+        this.questionHelper.extractQbehaviourButtons(question);
+
+        return;
+    }
+
+    /**
+     * Whether or not the handler is enabled on a site level.
+     *
+     * @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
+     */
+    isEnabled(): boolean | Promise<boolean> {
+        return true;
+    }
+}
diff --git a/src/addon/qbehaviour/manualgraded/manualgraded.module.ts b/src/addon/qbehaviour/manualgraded/manualgraded.module.ts
new file mode 100644
index 000000000..dbeaf1d34
--- /dev/null
+++ b/src/addon/qbehaviour/manualgraded/manualgraded.module.ts
@@ -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 { AddonQbehaviourManualGradedHandler } from './providers/handler';
+import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate';
+
+@NgModule({
+    declarations: [
+    ],
+    providers: [
+        AddonQbehaviourManualGradedHandler
+    ]
+})
+export class AddonQbehaviourManualGradedModule {
+    constructor(behaviourDelegate: CoreQuestionBehaviourDelegate, handler: AddonQbehaviourManualGradedHandler) {
+        behaviourDelegate.registerHandler(handler);
+    }
+}
diff --git a/src/addon/qbehaviour/manualgraded/providers/handler.ts b/src/addon/qbehaviour/manualgraded/providers/handler.ts
new file mode 100644
index 000000000..58755ac3c
--- /dev/null
+++ b/src/addon/qbehaviour/manualgraded/providers/handler.ts
@@ -0,0 +1,147 @@
+
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate';
+import { CoreQuestionDelegate } from '@core/question/providers/delegate';
+import { CoreQuestionProvider, CoreQuestionState } from '@core/question/providers/question';
+
+/**
+ * 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.
+ */
+export type isCompleteResponseFunction = (question: any, answers: any) => number;
+
+/**
+ * Check if two responses are the same.
+ *
+ * @param {any} question Question.
+ * @param {any} prevAnswers Object with the previous question answers.
+ * @param {any} prevBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...).
+ * @param {any} newAnswers Object with the new question answers.
+ * @param {any} newBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...).
+ * @return {boolean} Whether they're the same.
+ */
+export type isSameResponseFunction = (question: any, prevAnswers: any, prevBasicAnswers: any, newAnswers: any,
+        newBasicAnswers: any) => boolean;
+
+/**
+ * Handler to support manual graded question behaviour.
+ */
+@Injectable()
+export class AddonQbehaviourManualGradedHandler implements CoreQuestionBehaviourHandler {
+    name = 'AddonQbehaviourManualGraded';
+    type = 'manualgraded';
+
+    constructor(private questionDelegate: CoreQuestionDelegate, private questionProvider: CoreQuestionProvider) {
+        // Nothing to do.
+    }
+
+    /**
+     * Determine a question new state based on its answer(s).
+     *
+     * @param {string} component Component the question belongs to.
+     * @param {number} attemptId Attempt ID the question belongs to.
+     * @param {any} question The question.
+     * @param {string} [siteId] Site ID. If not defined, current site.
+     * @return {CoreQuestionState|Promise<CoreQuestionState>} New state (or promise resolved with state).
+     */
+    determineNewState(component: string, attemptId: number, question: any, siteId?: string)
+            : CoreQuestionState | Promise<CoreQuestionState> {
+        return this.determineNewStateManualGraded(component, attemptId, question, siteId);
+    }
+
+    /**
+     * Determine a question new state based on its answer(s) for manual graded question behaviour.
+     *
+     * @param {string} component Component the question belongs to.
+     * @param {number} attemptId Attempt ID the question belongs to.
+     * @param {any} question The question.
+     * @param {string} [siteId] Site ID. If not defined, current site.
+     * @param {isCompleteResponseFunction} [isCompleteFn] Function to override the default isCompleteResponse check.
+     * @param {isSameResponseFunction} [isSameFn] Function to override the default isSameResponse check.
+     * @return {Promise<CoreQuestionState>} Promise resolved with state.
+     */
+    determineNewStateManualGraded(component: string, attemptId: number, question: any, siteId?: string,
+            isCompleteFn?: isCompleteResponseFunction, isSameFn?: isSameResponseFunction): Promise<CoreQuestionState> {
+
+        // Check if we have local data for the question.
+        return this.questionProvider.getQuestion(component, attemptId, question.slot, siteId).catch(() => {
+            // No entry found, use the original data.
+            return question;
+        }).then((dbQuestion) => {
+            const state = this.questionProvider.getState(dbQuestion.state);
+
+            if (state.finished || !state.active) {
+                // Question is finished, it cannot change.
+                return state;
+            }
+
+            // We need to check if the answers have changed. Retrieve current stored answers.
+            return this.questionProvider.getQuestionAnswers(component, attemptId, question.slot, false, siteId)
+                    .then((prevAnswers) => {
+
+                const newBasicAnswers = this.questionProvider.getBasicAnswers(question.answers);
+
+                prevAnswers = this.questionProvider.convertAnswersArrayToObject(prevAnswers, true);
+                const prevBasicAnswers = this.questionProvider.getBasicAnswers(prevAnswers);
+
+                // If answers haven't changed the state is the same.
+                if (isSameFn) {
+                    if (isSameFn(question, prevAnswers, prevBasicAnswers, question.answers, newBasicAnswers)) {
+                        return state;
+                    }
+                } else {
+                    if (this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers)) {
+                        return state;
+                    }
+                }
+
+                // Answers have changed. Now check if the response is complete and calculate the new state.
+                let complete: number,
+                    newState: string;
+                if (isCompleteFn) {
+                    // Pass all the answers since some behaviours might need the extra data.
+                    complete = isCompleteFn(question, question.answers);
+                } else {
+                    // Only pass the basic answers since questions should be independent of extra data.
+                    complete = this.questionDelegate.isCompleteResponse(question, newBasicAnswers);
+                }
+
+                if (complete < 0) {
+                    newState = 'unknown';
+                } else if (complete > 0) {
+                    newState = 'complete';
+                } else {
+                    newState = 'todo';
+                }
+
+                return this.questionProvider.getState(newState);
+            });
+        });
+    }
+
+    /**
+     * 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;
+    }
+}
diff --git a/src/addon/qbehaviour/qbehaviour.module.ts b/src/addon/qbehaviour/qbehaviour.module.ts
new file mode 100644
index 000000000..dbe1bf47c
--- /dev/null
+++ b/src/addon/qbehaviour/qbehaviour.module.ts
@@ -0,0 +1,44 @@
+// (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 { AddonQbehaviourAdaptiveModule } from './adaptive/adaptive.module';
+import { AddonQbehaviourAdaptiveNoPenaltyModule } from './adaptivenopenalty/adaptivenopenalty.module';
+import { AddonQbehaviourDeferredCBMModule } from './deferredcbm/deferredcbm.module';
+import { AddonQbehaviourDeferredFeedbackModule } from './deferredfeedback/deferredfeedback.module';
+import { AddonQbehaviourImmediateCBMModule } from './immediatecbm/immediatecbm.module';
+import { AddonQbehaviourImmediateFeedbackModule } from './immediatefeedback/immediatefeedback.module';
+import { AddonQbehaviourInformationItemModule } from './informationitem/informationitem.module';
+import { AddonQbehaviourInteractiveModule } from './interactive/interactive.module';
+import { AddonQbehaviourInteractiveCountbackModule } from './interactivecountback/interactivecountback.module';
+import { AddonQbehaviourManualGradedModule } from './manualgraded/manualgraded.module';
+
+@NgModule({
+    declarations: [],
+    imports: [
+        AddonQbehaviourAdaptiveModule,
+        AddonQbehaviourAdaptiveNoPenaltyModule,
+        AddonQbehaviourDeferredCBMModule,
+        AddonQbehaviourDeferredFeedbackModule,
+        AddonQbehaviourImmediateCBMModule,
+        AddonQbehaviourImmediateFeedbackModule,
+        AddonQbehaviourInformationItemModule,
+        AddonQbehaviourInteractiveModule,
+        AddonQbehaviourInteractiveCountbackModule,
+        AddonQbehaviourManualGradedModule
+    ],
+    providers: [
+    ],
+    exports: []
+})
+export class AddonQbehaviourModule { }
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index d54c324a9..fbcf159f6 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -86,6 +86,7 @@ import { AddonMessagesModule } from '@addon/messages/messages.module';
 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';
 
 // For translate loader. AoT requires an exported function for factories.
 export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
@@ -172,7 +173,8 @@ export const CORE_PROVIDERS: any[] = [
         AddonMessagesModule,
         AddonNotesModule,
         AddonPushNotificationsModule,
-        AddonRemoteThemesModule
+        AddonRemoteThemesModule,
+        AddonQbehaviourModule
     ],
     bootstrap: [IonicApp],
     entryComponents: [
diff --git a/src/core/question/providers/helper.ts b/src/core/question/providers/helper.ts
index 5216fb5fd..5ffaf0c0e 100644
--- a/src/core/question/providers/helper.ts
+++ b/src/core/question/providers/helper.ts
@@ -52,6 +52,68 @@ export class CoreQuestionHelperProvider {
         });
     }
 
+    /**
+     * Extract question behaviour submit buttons from the question's HTML and add them to "behaviourButtons" property.
+     * The buttons aren't deleted from the content because all the im-controls block will be removed afterwards.
+     *
+     * @param {any} question Question to treat.
+     * @param {string} [selector] Selector to search the buttons. By default, '.im-controls input[type="submit"]'.
+     */
+    extractQbehaviourButtons(question: any, selector?: string): void {
+        selector = selector || '.im-controls input[type="submit"]';
+
+        this.div.innerHTML = question.html;
+
+        // Search the buttons.
+        const buttons = <HTMLInputElement[]> Array.from(this.div.querySelectorAll(selector));
+        buttons.forEach((button) => {
+            this.addBehaviourButton(question, button);
+        });
+
+        question.html = this.div.innerHTML;
+    }
+
+    /**
+     * Check if the question has CBM and, if so, extract the certainty options and add them to a new
+     * "behaviourCertaintyOptions" property.
+     * The value of the selected option is stored in question.behaviourCertaintySelected.
+     * We don't remove them from HTML because the whole im-controls block will be removed afterwards.
+     *
+     * @param {any} question Question to treat.
+     * @return {boolean} Wether the certainty is found.
+     */
+    extractQbehaviourCBM(question: any): boolean {
+        this.div.innerHTML = question.html;
+
+        const labels = Array.from(this.div.querySelectorAll('.im-controls .certaintychoices label[for*="certainty"]'));
+        question.behaviourCertaintyOptions = [];
+
+        labels.forEach((label) => {
+            // Search the radio button inside this certainty and add its data to the options array.
+            const input = <HTMLInputElement> label.querySelector('input[type="radio"]');
+            if (input) {
+                question.behaviourCertaintyOptions.push({
+                    id: input.id,
+                    name: input.name,
+                    value: input.value,
+                    text: this.textUtils.cleanTags(label.innerHTML),
+                    disabled: input.disabled
+                });
+
+                if (input.checked) {
+                    question.behaviourCertaintySelected = input.value;
+                }
+            }
+        });
+
+        // If we have a certainty value stored in local we'll use that one.
+        if (question.localAnswers && typeof question.localAnswers['-certainty'] != 'undefined') {
+            question.behaviourCertaintySelected = question.localAnswers['-certainty'];
+        }
+
+        return labels.length > 0;
+    }
+
     /**
      * Check if the question has a redo button and, if so, add it to "behaviourButtons" property
      * and remove it from the HTML.
@@ -80,6 +142,33 @@ export class CoreQuestionHelperProvider {
         }
     }
 
+    /**
+     * Check if the question contains a "seen" input.
+     * If so, add the name and value to a "behaviourSeenInput" property and remove the input.
+     *
+     * @param {any} question Question to treat.
+     * @return {boolean} Whether the seen input is found.
+     */
+    extractQbehaviourSeenInput(question: any): boolean {
+        this.div.innerHTML = question.html;
+
+        // Search the "seen" input.
+        const seenInput = <HTMLInputElement> this.div.querySelector('input[type="hidden"][name*=seen]');
+        if (seenInput) {
+            // Get the data and remove the input.
+            question.behaviourSeenInput = {
+                name: seenInput.name,
+                value: seenInput.value
+            };
+            seenInput.parentElement.removeChild(seenInput);
+            question.html = this.div.innerHTML;
+
+            return true;
+        }
+
+        return false;
+    }
+
     /**
      * Removes the comment from the question HTML code and adds it in a new "commentHtml" property.
      *