+
+
+
+
+
+
diff --git a/src/addon/qtype/essay/component/essay.ts b/src/addon/qtype/essay/component/essay.ts
new file mode 100644
index 000000000..c421fa629
--- /dev/null
+++ b/src/addon/qtype/essay/component/essay.ts
@@ -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();
+ }
+}
diff --git a/src/addon/qtype/essay/essay.module.ts b/src/addon/qtype/essay/essay.module.ts
new file mode 100644
index 000000000..9c3d58ba6
--- /dev/null
+++ b/src/addon/qtype/essay/essay.module.ts
@@ -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);
+ }
+}
diff --git a/src/addon/qtype/essay/providers/handler.ts b/src/addon/qtype/essay/providers/handler.ts
new file mode 100644
index 000000000..ead384f00
--- /dev/null
+++ b/src/addon/qtype/essay/providers/handler.ts
@@ -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} The component (or promise resolved with component) to use, undefined if not found.
+ */
+ getComponent(injector: Injector, question: any): any | Promise {
+ 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} True or promise resolved with true if enabled.
+ */
+ isEnabled(): boolean | Promise {
+ 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} Return a promise resolved when done if async, void if sync.
+ */
+ prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise {
+ this.div.innerHTML = question.html;
+
+ // Search the textarea to get its name.
+ const textarea = 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]);
+ }
+ });
+ }
+ }
+}
diff --git a/src/addon/qtype/match/component/match.ts b/src/addon/qtype/match/component/match.ts
index 117e59af5..9a2eb4ff6 100644
--- a/src/addon/qtype/match/component/match.ts
+++ b/src/addon/qtype/match/component/match.ts
@@ -12,10 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, Injector } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger';
-import { CoreDomUtilsProvider } from '@providers/utils/dom';
-import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
/**
@@ -27,8 +25,8 @@ import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-
})
export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent implements OnInit {
- constructor(logger: CoreLoggerProvider, questionHelper: CoreQuestionHelperProvider, domUtils: CoreDomUtilsProvider) {
- super(logger, 'AddonQtypeMatchComponent', questionHelper, domUtils);
+ constructor(logger: CoreLoggerProvider, injector: Injector) {
+ super(logger, 'AddonQtypeMatchComponent', injector);
}
/**
diff --git a/src/addon/qtype/multichoice/component/multichoice.ts b/src/addon/qtype/multichoice/component/multichoice.ts
index add6a7af7..395c4eabd 100644
--- a/src/addon/qtype/multichoice/component/multichoice.ts
+++ b/src/addon/qtype/multichoice/component/multichoice.ts
@@ -12,10 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, Injector } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger';
-import { CoreDomUtilsProvider } from '@providers/utils/dom';
-import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
/**
@@ -27,8 +25,8 @@ import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-
})
export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent implements OnInit {
- constructor(logger: CoreLoggerProvider, questionHelper: CoreQuestionHelperProvider, domUtils: CoreDomUtilsProvider) {
- super(logger, 'AddonQtypeMultichoiceComponent', questionHelper, domUtils);
+ constructor(logger: CoreLoggerProvider, injector: Injector) {
+ super(logger, 'AddonQtypeMultichoiceComponent', injector);
}
/**
diff --git a/src/addon/qtype/qtype.module.ts b/src/addon/qtype/qtype.module.ts
index a3a7fd3a8..96df1cebb 100644
--- a/src/addon/qtype/qtype.module.ts
+++ b/src/addon/qtype/qtype.module.ts
@@ -17,6 +17,7 @@ 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 { AddonQtypeMatchModule } from './match/match.module';
import { AddonQtypeMultichoiceModule } from './multichoice/multichoice.module';
import { AddonQtypeNumericalModule } from './numerical/numerical.module';
@@ -31,6 +32,7 @@ import { AddonQtypeTrueFalseModule } from './truefalse/truefalse.module';
AddonQtypeCalculatedMultiModule,
AddonQtypeCalculatedSimpleModule,
AddonQtypeDescriptionModule,
+ AddonQtypeEssayModule,
AddonQtypeMatchModule,
AddonQtypeMultichoiceModule,
AddonQtypeNumericalModule,
diff --git a/src/addon/qtype/shortanswer/component/shortanswer.ts b/src/addon/qtype/shortanswer/component/shortanswer.ts
index 77f2af456..152710d68 100644
--- a/src/addon/qtype/shortanswer/component/shortanswer.ts
+++ b/src/addon/qtype/shortanswer/component/shortanswer.ts
@@ -12,10 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, Injector } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger';
-import { CoreDomUtilsProvider } from '@providers/utils/dom';
-import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
/**
@@ -27,8 +25,8 @@ import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-
})
export class AddonQtypeShortAnswerComponent extends CoreQuestionBaseComponent implements OnInit {
- constructor(logger: CoreLoggerProvider, questionHelper: CoreQuestionHelperProvider, domUtils: CoreDomUtilsProvider) {
- super(logger, 'AddonQtypeShortAnswerComponent', questionHelper, domUtils);
+ constructor(logger: CoreLoggerProvider, injector: Injector) {
+ super(logger, 'AddonQtypeShortAnswerComponent', injector);
}
/**
diff --git a/src/core/question/classes/base-question-component.ts b/src/core/question/classes/base-question-component.ts
index cce52c112..6daac231d 100644
--- a/src/core/question/classes/base-question-component.ts
+++ b/src/core/question/classes/base-question-component.ts
@@ -12,9 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Input, EventEmitter } from '@angular/core';
+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';
/**
@@ -30,10 +31,17 @@ export class CoreQuestionBaseComponent {
@Input() onAbort: EventEmitter; // 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 questionHelper: CoreQuestionHelperProvider,
- protected domUtils: CoreDomUtilsProvider) {
+ 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);
}
/**
@@ -180,6 +188,48 @@ export class CoreQuestionBaseComponent {
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 = 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 = 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 has an input of type "text".
*
diff --git a/src/core/question/providers/helper.ts b/src/core/question/providers/helper.ts
index 5ffaf0c0e..61b67c5a8 100644
--- a/src/core/question/providers/helper.ts
+++ b/src/core/question/providers/helper.ts
@@ -13,6 +13,7 @@
// limitations under the License.
import { Injectable, EventEmitter } from '@angular/core';
+import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreQuestionProvider } from './question';
@@ -26,7 +27,7 @@ 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) { }
/**
* Add a behaviour button to the question's "behaviourButtons" property.
@@ -266,6 +267,40 @@ export class CoreQuestionHelperProvider {
}
}
+ /**
+ * 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 +334,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.