MOBILE-2272 quiz: Support add new files in essay in online

main
Dani Palou 2020-09-29 10:46:17 +02:00
parent 3f40127661
commit 46efcc441a
29 changed files with 393 additions and 102 deletions

View File

@ -569,7 +569,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
// Prepare the answers to be sent for the attempt.
protected prepareAnswers(): Promise<any> {
return this.questionHelper.prepareAnswers(this.questions, this.getAnswers(), this.offline);
return this.questionHelper.prepareAnswers(this.questions, this.getAnswers(), this.offline, this.component,
this.quiz.coursemodule);
}
/**
@ -612,6 +613,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
if (this.formElement) {
this.domUtils.triggerFormSubmittedEvent(this.formElement, !this.offline, this.sitesProvider.getCurrentSiteId());
}
return this.questionHelper.clearTmpData(this.questions, this.component, this.quiz.coursemodule);
});
}

View File

@ -342,8 +342,8 @@ export class AddonModQuizOfflineProvider {
for (const slot in questionsWithAnswers) {
const question = questionsWithAnswers[slot];
promises.push(this.behaviourDelegate.determineNewState(
quiz.preferredbehaviour, AddonModQuizProvider.COMPONENT, attempt.id, question, siteId).then((state) => {
promises.push(this.behaviourDelegate.determineNewState(quiz.preferredbehaviour, AddonModQuizProvider.COMPONENT,
attempt.id, question, quiz.coursemodule, siteId).then((state) => {
// Check if state has changed.
if (state && state.name != question.state) {
newStates[question.slot] = state.name;

View File

@ -40,13 +40,14 @@ export class AddonQbehaviourDeferredCBMHandler implements CoreQuestionBehaviourH
* @param component Component the question belongs to.
* @param attemptId Attempt ID the question belongs to.
* @param question The question.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return New state (or promise resolved with state).
*/
determineNewState(component: string, attemptId: number, question: any, siteId?: string)
determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string)
: CoreQuestionState | Promise<CoreQuestionState> {
// Depends on deferredfeedback.
return this.deferredFeedbackHandler.determineNewStateDeferred(component, attemptId, question, siteId,
return this.deferredFeedbackHandler.determineNewStateDeferred(component, attemptId, question, componentId, siteId,
this.isCompleteResponse.bind(this), this.isSameResponse.bind(this));
}
@ -71,11 +72,13 @@ export class AddonQbehaviourDeferredCBMHandler implements CoreQuestionBehaviourH
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
protected isCompleteResponse(question: any, answers: any): number {
protected isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number {
// First check if the question answer is complete.
const complete = this.questionDelegate.isCompleteResponse(question, answers);
const complete = this.questionDelegate.isCompleteResponse(question, answers, component, componentId);
if (complete > 0) {
// Answer is complete, check the user answered CBM too.
return answers['-certainty'] ? 1 : 0;
@ -101,12 +104,14 @@ export class AddonQbehaviourDeferredCBMHandler implements CoreQuestionBehaviourH
* @param prevBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...).
* @param newAnswers Object with the new question answers.
* @param newBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
protected isSameResponse(question: any, prevAnswers: any, prevBasicAnswers: any, newAnswers: any, newBasicAnswers: any)
: boolean {
protected isSameResponse(question: any, prevAnswers: any, prevBasicAnswers: any, newAnswers: any, newBasicAnswers: any,
component: string, componentId: string | number): boolean {
// First check if the question answer is the same.
const same = this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers);
const same = this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers, component, componentId);
if (same) {
// Same response, check the CBM is the same too.
return prevAnswers['-certainty'] == newAnswers['-certainty'];

View File

@ -23,9 +23,11 @@ import { CoreQuestionProvider, CoreQuestionState } from '@core/question/provider
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
export type isCompleteResponseFunction = (question: any, answers: any) => number;
export type isCompleteResponseFunction = (question: any, answers: any, component: string, componentId: string | number) => number;
/**
* Check if two responses are the same.
@ -35,10 +37,12 @@ export type isCompleteResponseFunction = (question: any, answers: any) => number
* @param prevBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...).
* @param newAnswers Object with the new question answers.
* @param newBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
export type isSameResponseFunction = (question: any, prevAnswers: any, prevBasicAnswers: any, newAnswers: any,
newBasicAnswers: any) => boolean;
newBasicAnswers: any, component: string, componentId: string | number) => boolean;
/**
* Handler to support deferred feedback question behaviour.
@ -58,12 +62,13 @@ export class AddonQbehaviourDeferredFeedbackHandler implements CoreQuestionBehav
* @param component Component the question belongs to.
* @param attemptId Attempt ID the question belongs to.
* @param question The question.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return New state (or promise resolved with state).
*/
determineNewState(component: string, attemptId: number, question: any, siteId?: string)
determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string)
: CoreQuestionState | Promise<CoreQuestionState> {
return this.determineNewStateDeferred(component, attemptId, question, siteId);
return this.determineNewStateDeferred(component, attemptId, question, componentId, siteId);
}
/**
@ -72,12 +77,13 @@ export class AddonQbehaviourDeferredFeedbackHandler implements CoreQuestionBehav
* @param component Component the question belongs to.
* @param attemptId Attempt ID the question belongs to.
* @param question The question.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @param isCompleteFn Function to override the default isCompleteResponse check.
* @param isSameFn Function to override the default isSameResponse check.
* @return Promise resolved with state.
*/
determineNewStateDeferred(component: string, attemptId: number, question: any, siteId?: string,
determineNewStateDeferred(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string,
isCompleteFn?: isCompleteResponseFunction, isSameFn?: isSameResponseFunction): Promise<CoreQuestionState> {
// Check if we have local data for the question.
@ -103,11 +109,12 @@ export class AddonQbehaviourDeferredFeedbackHandler implements CoreQuestionBehav
// If answers haven't changed the state is the same.
if (isSameFn) {
if (isSameFn(question, prevAnswers, prevBasicAnswers, question.answers, newBasicAnswers)) {
if (isSameFn(question, prevAnswers, prevBasicAnswers, question.answers, newBasicAnswers,
component, componentId)) {
return state;
}
} else {
if (this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers)) {
if (this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers, component, componentId)) {
return state;
}
}
@ -117,10 +124,10 @@ export class AddonQbehaviourDeferredFeedbackHandler implements CoreQuestionBehav
newState: string;
if (isCompleteFn) {
// Pass all the answers since some behaviours might need the extra data.
complete = isCompleteFn(question, question.answers);
complete = isCompleteFn(question, question.answers, component, componentId);
} else {
// Only pass the basic answers since questions should be independent of extra data.
complete = this.questionDelegate.isCompleteResponse(question, newBasicAnswers);
complete = this.questionDelegate.isCompleteResponse(question, newBasicAnswers, component, componentId);
}
if (complete < 0) {

View File

@ -35,10 +35,11 @@ export class AddonQbehaviourInformationItemHandler implements CoreQuestionBehavi
* @param component Component the question belongs to.
* @param attemptId Attempt ID the question belongs to.
* @param question The question.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return New state (or promise resolved with state).
*/
determineNewState(component: string, attemptId: number, question: any, siteId?: string)
determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string)
: CoreQuestionState | Promise<CoreQuestionState> {
if (question.answers['-seen']) {
return this.questionProvider.getState('complete');

View File

@ -23,9 +23,11 @@ import { CoreQuestionProvider, CoreQuestionState } from '@core/question/provider
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
export type isCompleteResponseFunction = (question: any, answers: any) => number;
export type isCompleteResponseFunction = (question: any, answers: any, component: string, componentId: string | number) => number;
/**
* Check if two responses are the same.
@ -35,10 +37,12 @@ export type isCompleteResponseFunction = (question: any, answers: any) => number
* @param prevBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...).
* @param newAnswers Object with the new question answers.
* @param newBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
export type isSameResponseFunction = (question: any, prevAnswers: any, prevBasicAnswers: any, newAnswers: any,
newBasicAnswers: any) => boolean;
newBasicAnswers: any, component: string, componentId: string | number) => boolean;
/**
* Handler to support manual graded question behaviour.
@ -58,12 +62,13 @@ export class AddonQbehaviourManualGradedHandler implements CoreQuestionBehaviour
* @param component Component the question belongs to.
* @param attemptId Attempt ID the question belongs to.
* @param question The question.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return New state (or promise resolved with state).
*/
determineNewState(component: string, attemptId: number, question: any, siteId?: string)
determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string)
: CoreQuestionState | Promise<CoreQuestionState> {
return this.determineNewStateManualGraded(component, attemptId, question, siteId);
return this.determineNewStateManualGraded(component, attemptId, question, componentId, siteId);
}
/**
@ -72,13 +77,15 @@ export class AddonQbehaviourManualGradedHandler implements CoreQuestionBehaviour
* @param component Component the question belongs to.
* @param attemptId Attempt ID the question belongs to.
* @param question The question.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @param isCompleteFn Function to override the default isCompleteResponse check.
* @param isSameFn Function to override the default isSameResponse check.
* @return Promise resolved with state.
*/
determineNewStateManualGraded(component: string, attemptId: number, question: any, siteId?: string,
isCompleteFn?: isCompleteResponseFunction, isSameFn?: isSameResponseFunction): Promise<CoreQuestionState> {
determineNewStateManualGraded(component: string, attemptId: number, question: any, componentId: string | number,
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(() => {
@ -103,11 +110,12 @@ export class AddonQbehaviourManualGradedHandler implements CoreQuestionBehaviour
// If answers haven't changed the state is the same.
if (isSameFn) {
if (isSameFn(question, prevAnswers, prevBasicAnswers, question.answers, newBasicAnswers)) {
if (isSameFn(question, prevAnswers, prevBasicAnswers, question.answers, newBasicAnswers,
component, componentId)) {
return state;
}
} else {
if (this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers)) {
if (this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers, component, componentId)) {
return state;
}
}
@ -117,10 +125,10 @@ export class AddonQbehaviourManualGradedHandler implements CoreQuestionBehaviour
newState: string;
if (isCompleteFn) {
// Pass all the answers since some behaviours might need the extra data.
complete = isCompleteFn(question, question.answers);
complete = isCompleteFn(question, question.answers, component, componentId);
} else {
// Only pass the basic answers since questions should be independent of extra data.
complete = this.questionDelegate.isCompleteResponse(question, newBasicAnswers);
complete = this.questionDelegate.isCompleteResponse(question, newBasicAnswers, component, componentId);
}
if (complete < 0) {

View File

@ -46,9 +46,11 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
isCompleteResponse(question: any, answers: any): number {
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number {
if (this.isGradableResponse(question, answers) === 0 || !this.validateUnits(answers['answer'])) {
return 0;
}
@ -93,9 +95,11 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
* @param question Question.
* @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new question answers.
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean {
return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer') &&
this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'unit');
}

View File

@ -46,9 +46,11 @@ export class AddonQtypeCalculatedMultiHandler implements CoreQuestionHandler {
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
isCompleteResponse(question: any, answers: any): number {
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number {
// This question type depends on multichoice.
return this.multichoiceHandler.isCompleteResponseSingle(answers);
}
@ -81,9 +83,11 @@ export class AddonQtypeCalculatedMultiHandler implements CoreQuestionHandler {
* @param question Question.
* @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new question answers.
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean {
// This question type depends on multichoice.
return this.multichoiceHandler.isSameResponseSingle(prevAnswers, newAnswers);
}

View File

@ -46,11 +46,13 @@ export class AddonQtypeCalculatedSimpleHandler implements CoreQuestionHandler {
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
isCompleteResponse(question: any, answers: any): number {
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number {
// This question type depends on calculated.
return this.calculatedHandler.isCompleteResponse(question, answers);
return this.calculatedHandler.isCompleteResponse(question, answers, component, componentId);
}
/**
@ -81,10 +83,12 @@ export class AddonQtypeCalculatedSimpleHandler implements CoreQuestionHandler {
* @param question Question.
* @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new question answers.
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean {
// This question type depends on calculated.
return this.calculatedHandler.isSameResponse(question, prevAnswers, newAnswers);
return this.calculatedHandler.isSameResponse(question, prevAnswers, newAnswers, component, componentId);
}
}

View File

@ -61,9 +61,11 @@ export class AddonQtypeDdImageOrTextHandler implements CoreQuestionHandler {
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
isCompleteResponse(question: any, answers: any): number {
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number {
// An answer is complete if all drop zones have an answer.
// We should always receive all the drop zones with their value ('' if not answered).
for (const name in answers) {
@ -110,9 +112,11 @@ export class AddonQtypeDdImageOrTextHandler implements CoreQuestionHandler {
* @param question Question.
* @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new question answers.
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean {
return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers);
}
}

View File

@ -62,9 +62,11 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler {
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
isCompleteResponse(question: any, answers: any): number {
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number {
// If 1 dragitem is set we assume the answer is complete (like Moodle does).
for (const name in answers) {
if (answers[name]) {
@ -93,7 +95,7 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler {
* @return 1 if gradable, 0 if not gradable, -1 if cannot determine.
*/
isGradableResponse(question: any, answers: any): number {
return this.isCompleteResponse(question, answers);
return this.isCompleteResponse(question, answers, null, null);
}
/**
@ -102,9 +104,11 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler {
* @param question Question.
* @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new question answers.
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean {
return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers);
}

View File

@ -61,9 +61,11 @@ export class AddonQtypeDdwtosHandler implements CoreQuestionHandler {
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
isCompleteResponse(question: any, answers: any): number {
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number {
for (const name in answers) {
const value = answers[name];
if (!value || value === '0') {
@ -108,9 +110,11 @@ export class AddonQtypeDdwtosHandler implements CoreQuestionHandler {
* @param question Question.
* @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new question answers.
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean {
return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers);
}
}

View File

@ -27,7 +27,7 @@
<!-- Attachments. -->
<ng-container *ngIf="question.allowsAttachments">
<core-attachments *ngIf="uploadFilesSupported" [files]="attachments" [component]="component" [componentId]="componentId" [maxSize]="question.attachmentsMaxBytes" [maxSubmissions]="question.attachmentsMaxFiles"></core-attachments>
<core-attachments *ngIf="uploadFilesSupported" [files]="attachments" [component]="component" [componentId]="componentId" [maxSize]="question.attachmentsMaxBytes" [maxSubmissions]="question.attachmentsMaxFiles" [allowOffline]="offline"></core-attachments>
<input item-content *ngIf="uploadFilesSupported" type="hidden" [name]="question.attachmentsDraftIdInput.name" [value]="question.attachmentsDraftIdInput.value" >

View File

@ -14,9 +14,9 @@
import { Component, OnInit, Injector } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSites } from '@providers/sites';
import { CoreWSExternalFile } from '@providers/ws';
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
import { CoreQuestion } from '@core/question/providers/question';
import { FormControl, FormBuilder } from '@angular/forms';
import { CoreFileSession } from '@providers/file-session';
@ -42,14 +42,15 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
* Component being initialized.
*/
ngOnInit(): void {
this.uploadFilesSupported = CoreSites.instance.getCurrentSite().isVersionGreaterEqualThan('3.10');
this.uploadFilesSupported = typeof this.question.responsefileareas != 'undefined';
this.initEssayComponent();
this.formControl = this.fb.control(this.question.textarea && this.question.textarea.text);
if (this.question.allowsAttachments && this.uploadFilesSupported) {
this.attachments = Array.from(this.questionHelper.getResponseFileAreaFiles(this.question, 'attachments'));
CoreFileSession.instance.setFiles(this.component, this.componentId + '_' + this.question.id, this.attachments);
CoreFileSession.instance.setFiles(this.component,
CoreQuestion.instance.getQuestionComponentId(this.question, this.componentId), this.attachments);
}
}
}

View File

@ -14,12 +14,15 @@
// limitations under the License.
import { Injectable, Injector } from '@angular/core';
import { CoreFileSession } from '@providers/file-session';
import { CoreSites } from '@providers/sites';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreFileUploader } from '@core/fileuploader/providers/fileuploader';
import { CoreQuestionHandler } from '@core/question/providers/delegate';
import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
import { CoreQuestion } from '@core/question/providers/question';
import { AddonQtypeEssayComponent } from '../component/essay';
/**
@ -33,6 +36,24 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler {
constructor(private utils: CoreUtilsProvider, private questionHelper: CoreQuestionHelperProvider,
private textUtils: CoreTextUtilsProvider, private domUtils: CoreDomUtilsProvider) { }
/**
* Clear temporary data after the data has been saved.
*
* @param question Question.
* @param component The component the question is related to.
* @param componentId Component ID.
*/
clearTmpData(question: any, component: string, componentId: string | number): void {
const questionComponentId = CoreQuestion.instance.getQuestionComponentId(question, componentId);
const files = CoreFileSession.instance.getFiles(component, questionComponentId);
// Clear the files in session for this question.
CoreFileSession.instance.clearFiles(component, questionComponentId);
// Now delete the local files from the tmp folder.
CoreFileUploader.instance.clearTmpFiles(files);
}
/**
* 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.
@ -66,13 +87,14 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler {
*/
getPreventSubmitMessage(question: any): string {
const element = this.domUtils.convertToElement(question.html);
const uploadFilesSupported = typeof question.responsefileareas != 'undefined';
if (element.querySelector('div[id*=filemanager]')) {
if (!uploadFilesSupported && element.querySelector('div[id*=filemanager]')) {
// The question allows attachments. Since the app cannot attach files yet we will prevent submitting the question.
return 'core.question.errorattachmentsnotsupportedinsite';
}
if (this.questionHelper.hasDraftFileUrls(element.innerHTML)) {
if (!uploadFilesSupported && this.questionHelper.hasDraftFileUrls(element.innerHTML)) {
return 'core.question.errorinlinefilesnotsupportedinsite';
}
}
@ -82,20 +104,36 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler {
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
isCompleteResponse(question: any, answers: any): number {
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number {
const element = this.domUtils.convertToElement(question.html);
const hasInlineText = answers['answer'] && answers['answer'] !== '',
allowsAttachments = !!element.querySelector('div[id*=filemanager]');
const hasInlineText = answers['answer'] && answers['answer'] !== '';
const allowsInlineText = !!element.querySelector('textarea[name*=_answer]');
const allowsAttachments = !!element.querySelector('div[id*=filemanager]');
const uploadFilesSupported = typeof question.responsefileareas != 'undefined';
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;
if (!uploadFilesSupported) {
// We can't know if the attachments are required or if the user added any in web.
return -1;
}
const questionComponentId = CoreQuestion.instance.getQuestionComponentId(question, componentId);
const attachments = CoreFileSession.instance.getFiles(component, questionComponentId);
if (!allowsInlineText) {
return attachments && attachments.length > 0 ? 1 : 0;
}
// If any of the fields is missing return -1 because we can't know if they're required or not.
return hasInlineText && attachments && attachments.length > 0 ? 1 : -1;
}
/**
@ -125,10 +163,30 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler {
* @param question Question.
* @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new question answers.
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer');
isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean {
const element = this.domUtils.convertToElement(question.html);
const allowsInlineText = !!element.querySelector('textarea[name*=_answer]');
const allowsAttachments = !!element.querySelector('div[id*=filemanager]');
const uploadFilesSupported = typeof question.responsefileareas != 'undefined';
// First check the inline text.
const answerIsEqual = allowsInlineText ? this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer') : true;
if (!allowsAttachments || !uploadFilesSupported || !answerIsEqual) {
// No need to check attachments.
return answerIsEqual;
}
// Check attachments now.
const questionComponentId = CoreQuestion.instance.getQuestionComponentId(question, componentId);
const attachments = CoreFileSession.instance.getFiles(component, questionComponentId);
const originalAttachments = this.questionHelper.getResponseFileAreaFiles(question, 'attachments');
return CoreFileUploader.instance.areFileListDifferent(attachments, originalAttachments);
}
/**
@ -137,11 +195,16 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler {
* @param question Question.
* @param answers The answers retrieved from the form. Prepared answers must be stored in this object.
* @param offline Whether the data should be saved in offline.
* @param component The component the question is related to.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return Return a promise resolved when done if async, void if sync.
*/
async prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): Promise<void> {
async prepareAnswers(question: any, answers: any, offline: boolean, component: string, componentId: string | number,
siteId?: string): Promise<void> {
const element = this.domUtils.convertToElement(question.html);
const attachmentsInput = <HTMLInputElement> element.querySelector('.attachments input[name*=_attachments]');
// Search the textarea to get its name.
const textarea = <HTMLTextAreaElement> element.querySelector('textarea[name*=_answer]');
@ -158,5 +221,18 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler {
// Add some HTML to the text if needed.
answers[textarea.name] = this.textUtils.formatHtmlLines(answers[textarea.name]);
}
if (attachmentsInput) {
// Treat attachments if any.
const questionComponentId = CoreQuestion.instance.getQuestionComponentId(question, componentId);
const attachments = CoreFileSession.instance.getFiles(component, questionComponentId);
const draftId = Number(attachmentsInput.value);
if (offline) {
// @TODO Support offline.
} else {
await CoreFileUploader.instance.uploadFiles(draftId, attachments, siteId);
}
}
}
}

View File

@ -61,9 +61,11 @@ export class AddonQtypeGapSelectHandler implements CoreQuestionHandler {
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
isCompleteResponse(question: any, answers: any): number {
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): 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];
@ -110,9 +112,11 @@ export class AddonQtypeGapSelectHandler implements CoreQuestionHandler {
* @param question Question.
* @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new question answers.
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean {
return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers);
}
}

View File

@ -61,9 +61,11 @@ export class AddonQtypeMatchHandler implements CoreQuestionHandler {
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
isCompleteResponse(question: any, answers: any): number {
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): 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];
@ -110,9 +112,11 @@ export class AddonQtypeMatchHandler implements CoreQuestionHandler {
* @param question Question.
* @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new question answers.
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean {
return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers);
}
}

View File

@ -62,9 +62,11 @@ export class AddonQtypeMultiAnswerHandler implements CoreQuestionHandler {
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
isCompleteResponse(question: any, answers: any): number {
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): 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) {
@ -112,9 +114,11 @@ export class AddonQtypeMultiAnswerHandler implements CoreQuestionHandler {
* @param question Question.
* @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new question answers.
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean {
return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers);
}

View File

@ -45,9 +45,11 @@ export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler {
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
isCompleteResponse(question: any, answers: any): number {
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number {
let isSingle = true,
isMultiComplete = false;
@ -98,7 +100,7 @@ export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler {
* @return 1 if gradable, 0 if not gradable, -1 if cannot determine.
*/
isGradableResponse(question: any, answers: any): number {
return this.isCompleteResponse(question, answers);
return this.isCompleteResponse(question, answers, null, null);
}
/**
@ -118,9 +120,11 @@ export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler {
* @param question Question.
* @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new question answers.
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean {
let isSingle = true,
isMultiSame = true;
@ -158,10 +162,13 @@ export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler {
* @param question Question.
* @param answers The answers retrieved from the form. Prepared answers must be stored in this object.
* @param offline Whether the data should be saved in offline.
* @param component The component the question is related to.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return Return a promise resolved when done if async, void if sync.
*/
prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise<any> {
prepareAnswers(question: any, answers: any, offline: boolean, component: string, componentId: string | number, siteId?: string)
: void | Promise<any> {
if (question && !question.multi && typeof answers[question.optionsName] != 'undefined' && !answers[question.optionsName]) {
/* It's a single choice and the user hasn't answered. Delete the answer because
sending an empty string (default value) will mark the first option as selected. */

View File

@ -46,11 +46,13 @@ export class AddonQtypeRandomSaMatchHandler implements CoreQuestionHandler {
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
isCompleteResponse(question: any, answers: any): number {
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number {
// This question behaves like a match question.
return this.matchHandler.isCompleteResponse(question, answers);
return this.matchHandler.isCompleteResponse(question, answers, component, componentId);
}
/**
@ -81,10 +83,12 @@ export class AddonQtypeRandomSaMatchHandler implements CoreQuestionHandler {
* @param question Question.
* @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new question answers.
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean {
// This question behaves like a match question.
return this.matchHandler.isSameResponse(question, prevAnswers, newAnswers);
return this.matchHandler.isSameResponse(question, prevAnswers, newAnswers, component, componentId);
}
}

View File

@ -45,9 +45,11 @@ export class AddonQtypeShortAnswerHandler implements CoreQuestionHandler {
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
isCompleteResponse(question: any, answers: any): number {
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number {
return (answers['answer'] || answers['answer'] === 0) ? 1 : 0;
}
@ -69,7 +71,7 @@ export class AddonQtypeShortAnswerHandler implements CoreQuestionHandler {
* @return 1 if gradable, 0 if not gradable, -1 if cannot determine.
*/
isGradableResponse(question: any, answers: any): number {
return this.isCompleteResponse(question, answers);
return this.isCompleteResponse(question, answers, null, null);
}
/**
@ -78,9 +80,11 @@ export class AddonQtypeShortAnswerHandler implements CoreQuestionHandler {
* @param question Question.
* @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new question answers.
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean {
return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer');
}
}

View File

@ -46,9 +46,11 @@ export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler {
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
isCompleteResponse(question: any, answers: any): number {
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number {
return answers['answer'] ? 1 : 0;
}
@ -70,7 +72,7 @@ export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler {
* @return 1 if gradable, 0 if not gradable, -1 if cannot determine.
*/
isGradableResponse(question: any, answers: any): number {
return this.isCompleteResponse(question, answers);
return this.isCompleteResponse(question, answers, null, null);
}
/**
@ -79,9 +81,11 @@ export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler {
* @param question Question.
* @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new question answers.
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean {
return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer');
}
@ -91,10 +95,13 @@ export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler {
* @param question Question.
* @param answers The answers retrieved from the form. Prepared answers must be stored in this object.
* @param offline Whether the data should be saved in offline.
* @param component The component the question is related to.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return Return a promise resolved when done if async, void if sync.
*/
prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise<any> {
prepareAnswers(question: any, answers: any, offline: boolean, component: string, componentId: string | number, siteId?: string)
: void | Promise<any> {
if (question && typeof answers[question.optionsName] != 'undefined' && !answers[question.optionsName]) {
// The user hasn't answered. Delete the answer to prevent marking one of the answers automatically.
delete answers[question.optionsName];

View File

@ -15,6 +15,7 @@
import { Injectable } from '@angular/core';
import { ModalController } from 'ionic-angular';
import { Camera, CameraOptions } from '@ionic-native/camera';
import { FileEntry } from '@ionic-native/file';
import { MediaCapture, MediaFile, CaptureError, CaptureAudioOptions, CaptureVideoOptions } from '@ionic-native/media-capture';
import { TranslateService } from '@ngx-translate/core';
import { CoreFileProvider } from '@providers/file';
@ -25,9 +26,10 @@ import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreWSFileUploadOptions } from '@providers/ws';
import { CoreWSFileUploadOptions, CoreWSExternalFile } from '@providers/ws';
import { Subject } from 'rxjs';
import { CoreApp } from '@providers/app';
import { makeSingleton } from '@singletons/core.singletons';
/**
* File upload options.
@ -96,7 +98,7 @@ export class CoreFileUploaderProvider {
// Currently we are going to compare the order of the files as well.
// This function can be improved comparing more fields or not comparing the order.
for (let i = 0; i < a.length; i++) {
if ((a[i].name || a[i].filename) != (b[i].name || b[i].filename)) {
if (a[i].name != b[i].name || a[i].filename != b[i].filename) {
return true;
}
}
@ -534,6 +536,37 @@ export class CoreFileUploaderProvider {
});
}
/**
* Given a list of files (either online files or local files), upload the local files to the draft area.
* Local files are not deleted from the device after upload.
*
* @param itemId Draft ID.
* @param files List of files.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the itemId.
*/
async uploadFiles(itemId: number, files: (CoreWSExternalFile | FileEntry)[], siteId?: string): Promise<void> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
if (!files || !files.length) {
return;
}
await Promise.all(files.map(async (file) => {
if ((<CoreWSExternalFile> file).filename && !(<FileEntry> file).name) {
// File already uploaded, ignore it.
return;
}
file = <FileEntry> file;
// Now upload the file.
const options = this.getFileUploadOptions(file.toURL(), file.name, undefined, false, 'draft', itemId);
await this.uploadFile(file.toURL(), options, undefined, siteId);
}));
}
/**
* Upload a file to a draft area and return the draft ID.
*
@ -615,3 +648,5 @@ export class CoreFileUploaderProvider {
});
}
}
export class CoreFileUploader extends makeSingleton(CoreFileUploaderProvider) {}

View File

@ -34,10 +34,11 @@ export class CoreQuestionBehaviourBaseHandler implements CoreQuestionBehaviourHa
* @param component Component the question belongs to.
* @param attemptId Attempt ID the question belongs to.
* @param question The question.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return New state (or promise resolved with state).
*/
determineNewState(component: string, attemptId: number, question: any, siteId?: string)
determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string)
: CoreQuestionState | Promise<CoreQuestionState> {
// Return the current state.
return this.questionProvider.getState(question.state);

View File

@ -79,9 +79,11 @@ export class CoreQuestionBaseHandler implements CoreQuestionHandler {
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
isCompleteResponse(question: any, answers: any): number {
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number {
return -1;
}
@ -103,9 +105,11 @@ export class CoreQuestionBaseHandler implements CoreQuestionHandler {
* @param question Question.
* @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new question answers.
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Whether they're the same.
*/
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean {
return false;
}
@ -115,10 +119,13 @@ export class CoreQuestionBaseHandler implements CoreQuestionHandler {
* @param question Question.
* @param answers The answers retrieved from the form. Prepared answers must be stored in this object.
* @param offline Whether the data should be saved in offline.
* @param component The component the question is related to.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return Return a promise resolved when done if async, void if sync.
*/
prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise<any> {
prepareAnswers(question: any, answers: any, offline: boolean, component: string, componentId: string | number, siteId?: string)
: void | Promise<any> {
// Nothing to do.
}

View File

@ -36,10 +36,11 @@ export interface CoreQuestionBehaviourHandler extends CoreDelegateHandler {
* @param component Component the question belongs to.
* @param attemptId Attempt ID the question belongs to.
* @param question The question.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return State (or promise resolved with state).
*/
determineNewState?(component: string, attemptId: number, question: any, siteId?: string)
determineNewState?(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string)
: CoreQuestionState | Promise<CoreQuestionState>;
/**
@ -74,15 +75,16 @@ export class CoreQuestionBehaviourDelegate extends CoreDelegate {
* @param component Component the question belongs to.
* @param attemptId Attempt ID the question belongs to.
* @param question The question.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with state.
*/
determineNewState(behaviour: string, component: string, attemptId: number, question: any, siteId?: string)
: Promise<CoreQuestionState> {
determineNewState(behaviour: string, component: string, attemptId: number, question: any, componentId: string | number,
siteId?: string): Promise<CoreQuestionState> {
behaviour = this.questionDelegate.getBehaviourForQuestion(question, behaviour);
return Promise.resolve(this.executeFunctionOnEnabled(behaviour, 'determineNewState',
[component, attemptId, question, siteId]));
[component, attemptId, question, componentId, siteId]));
}
/**

View File

@ -62,9 +62,11 @@ export interface CoreQuestionHandler extends CoreDelegateHandler {
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
isCompleteResponse?(question: any, answers: any): number;
isCompleteResponse?(question: any, answers: any, component: string, componentId: string | number): number;
/**
* Check if a student has provided enough of an answer for the question to be graded automatically,
@ -84,7 +86,7 @@ export interface CoreQuestionHandler extends CoreDelegateHandler {
* @param newAnswers Object with the new question answers.
* @return Whether they're the same.
*/
isSameResponse?(question: any, prevAnswers: any, newAnswers: any): boolean;
isSameResponse?(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean;
/**
* Prepare and add to answers the data to send to server based in the input. Return promise if async.
@ -92,10 +94,13 @@ export interface CoreQuestionHandler extends CoreDelegateHandler {
* @param question Question.
* @param answers The answers retrieved from the form. Prepared answers must be stored in this object.
* @param offline Whether the data should be saved in offline.
* @param component The component the question is related to.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return Return a promise resolved when done if async, void if sync.
*/
prepareAnswers?(question: any, answers: any, offline: boolean, siteId?: string): void | Promise<any>;
prepareAnswers?(question: any, answers: any, offline: boolean, component: string, componentId: string | number,
siteId?: string): void | Promise<any>;
/**
* Validate if an offline sequencecheck is valid compared with the online one.
@ -115,6 +120,15 @@ export interface CoreQuestionHandler extends CoreDelegateHandler {
* @return List of URLs.
*/
getAdditionalDownloadableFiles?(question: any, usageId: number): string[];
/**
* Clear temporary data after the data has been saved.
*
* @param question Question.
* @param component The component the question is related to.
* @param componentId Component ID.
*/
clearTmpData?(question: any, component: string, componentId: string | number): void | Promise<void>;
}
/**
@ -196,12 +210,14 @@ export class CoreQuestionDelegate extends CoreDelegate {
*
* @param question The question.
* @param answers Object with the question answers (without prefix).
* @param component The component the question is related to.
* @param componentId Component ID.
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/
isCompleteResponse(question: any, answers: any): number {
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number {
const type = this.getTypeName(question);
return this.executeFunctionOnEnabled(type, 'isCompleteResponse', [question, answers]);
return this.executeFunctionOnEnabled(type, 'isCompleteResponse', [question, answers, component, componentId]);
}
/**
@ -226,10 +242,10 @@ export class CoreQuestionDelegate extends CoreDelegate {
* @param newAnswers Object with the new question answers.
* @return Whether they're the same.
*/
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean {
const type = this.getTypeName(question);
return this.executeFunctionOnEnabled(type, 'isSameResponse', [question, prevAnswers, newAnswers]);
return this.executeFunctionOnEnabled(type, 'isSameResponse', [question, prevAnswers, newAnswers, component, componentId]);
}
/**
@ -248,13 +264,17 @@ export class CoreQuestionDelegate extends CoreDelegate {
* @param question Question.
* @param answers The answers retrieved from the form. Prepared answers must be stored in this object.
* @param offline Whether the data should be saved in offline.
* @param component The component the question is related to.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when data has been prepared.
*/
prepareAnswersForQuestion(question: any, answers: any, offline: boolean, siteId?: string): Promise<any> {
prepareAnswersForQuestion(question: any, answers: any, offline: boolean, component: string, componentId: string | number,
siteId?: string): Promise<any> {
const type = this.getTypeName(question);
return Promise.resolve(this.executeFunctionOnEnabled(type, 'prepareAnswers', [question, answers, offline, siteId]));
return Promise.resolve(this.executeFunctionOnEnabled(type, 'prepareAnswers',
[question, answers, offline, component, componentId, siteId]));
}
/**
@ -282,4 +302,17 @@ export class CoreQuestionDelegate extends CoreDelegate {
return this.executeFunctionOnEnabled(type, 'getAdditionalDownloadableFiles', [question, usageId]) || [];
}
/**
* Clear temporary data after the data has been saved.
*
* @param question Question.
* @param component The component the question is related to.
* @param componentId Component ID.
*/
clearTmpData(question: any, component: string, componentId: string | number): void | Promise<void> {
const type = this.getTypeName(question);
return this.executeFunctionOnEnabled(type, 'clearTmpData', [question, component, componentId]);
}
}

View File

@ -60,6 +60,22 @@ export class CoreQuestionHelperProvider {
});
}
/**
* Clear questions temporary data after the data has been saved.
*
* @param questions The list of questions.
* @param component The component the question is related to.
* @param componentId Component ID.
* @return Promise resolved when done.
*/
async clearTmpData(questions: any[], component: string, componentId: string | number): Promise<void> {
questions = questions || [];
await Promise.all(questions.map(async (question) => {
await this.questionDelegate.clearTmpData(question, component, componentId);
}));
}
/**
* 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.
@ -541,7 +557,7 @@ export class CoreQuestionHelperProvider {
if (!component) {
component = CoreQuestionProvider.COMPONENT;
componentId = question.id;
componentId = question.number;
}
urls.push(...this.questionDelegate.getAdditionalDownloadableFiles(question, usageId));
@ -572,15 +588,19 @@ export class CoreQuestionHelperProvider {
* @param questions The list of questions.
* @param answers The input data.
* @param offline True if data should be saved in offline.
* @param component The component the question is related to.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with answers to send to server.
*/
prepareAnswers(questions: any[], answers: any, offline?: boolean, siteId?: string): Promise<any> {
prepareAnswers(questions: any[], answers: any, offline: boolean, component: string, componentId: string | number,
siteId?: string): Promise<any> {
const promises = [];
questions = questions || [];
questions.forEach((question) => {
promises.push(this.questionDelegate.prepareAnswersForQuestion(question, answers, offline, siteId));
promises.push(this.questionDelegate.prepareAnswersForQuestion(question, answers, offline, component, componentId,
siteId));
});
return this.utils.allPromises(promises).then(() => {

View File

@ -13,10 +13,13 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreFile } from '@providers/file';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
import { CoreTextUtils } from '@providers/utils/text';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { makeSingleton } from '@singletons/core.singletons';
/**
* An object to represent a question state.
@ -413,6 +416,35 @@ export class CoreQuestionProvider {
});
}
/**
* Given a question and a componentId, return a componentId that is unique for the question.
*
* @param question Question.
* @param componentId Component ID.
* @return Question component ID.
*/
getQuestionComponentId(question: any, componentId: string | number): string {
return componentId + '_' + question.number;
}
/**
* Get the path to the folder where to store files for an offline question.
*
* @param type Question type.
* @param component Component the question is related to.
* @param componentId Question component ID, returned by getQuestionComponentId.
* @param siteId Site ID. If not defined, current site.
* @return Folder path.
*/
getQuestionFolder(type: string, component: string, componentId: string, siteId?: string): string {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
const siteFolderPath = CoreFile.instance.getSiteFolder(siteId);
const questionFolderPath = 'offlinequestion/' + type + '/' + component + '/' + componentId;
return CoreTextUtils.instance.concatenatePaths(siteFolderPath, questionFolderPath);
}
/**
* Extract the question slot from a question name.
*
@ -612,3 +644,5 @@ export class CoreQuestionProvider {
});
}
}
export class CoreQuestion extends makeSingleton(CoreQuestionProvider) {}