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. // Prepare the answers to be sent for the attempt.
protected prepareAnswers(): Promise<any> { 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) { if (this.formElement) {
this.domUtils.triggerFormSubmittedEvent(this.formElement, !this.offline, this.sitesProvider.getCurrentSiteId()); 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) { for (const slot in questionsWithAnswers) {
const question = questionsWithAnswers[slot]; const question = questionsWithAnswers[slot];
promises.push(this.behaviourDelegate.determineNewState( promises.push(this.behaviourDelegate.determineNewState(quiz.preferredbehaviour, AddonModQuizProvider.COMPONENT,
quiz.preferredbehaviour, AddonModQuizProvider.COMPONENT, attempt.id, question, siteId).then((state) => { attempt.id, question, quiz.coursemodule, siteId).then((state) => {
// Check if state has changed. // Check if state has changed.
if (state && state.name != question.state) { if (state && state.name != question.state) {
newStates[question.slot] = state.name; newStates[question.slot] = state.name;

View File

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

View File

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

View File

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

View File

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

View File

@ -46,9 +46,11 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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'])) { if (this.isGradableResponse(question, answers) === 0 || !this.validateUnits(answers['answer'])) {
return 0; return 0;
} }
@ -93,9 +95,11 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
* @param question Question. * @param question Question.
* @param prevAnswers Object with the previous question answers. * @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new 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. * @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') && return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer') &&
this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'unit'); this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'unit');
} }

View File

@ -46,9 +46,11 @@ export class AddonQtypeCalculatedMultiHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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. // This question type depends on multichoice.
return this.multichoiceHandler.isCompleteResponseSingle(answers); return this.multichoiceHandler.isCompleteResponseSingle(answers);
} }
@ -81,9 +83,11 @@ export class AddonQtypeCalculatedMultiHandler implements CoreQuestionHandler {
* @param question Question. * @param question Question.
* @param prevAnswers Object with the previous question answers. * @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new 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. * @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. // This question type depends on multichoice.
return this.multichoiceHandler.isSameResponseSingle(prevAnswers, newAnswers); return this.multichoiceHandler.isSameResponseSingle(prevAnswers, newAnswers);
} }

View File

@ -46,11 +46,13 @@ export class AddonQtypeCalculatedSimpleHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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. // 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 question Question.
* @param prevAnswers Object with the previous question answers. * @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new 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. * @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. // 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 question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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. // 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). // We should always receive all the drop zones with their value ('' if not answered).
for (const name in answers) { for (const name in answers) {
@ -110,9 +112,11 @@ export class AddonQtypeDdImageOrTextHandler implements CoreQuestionHandler {
* @param question Question. * @param question Question.
* @param prevAnswers Object with the previous question answers. * @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new 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. * @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); return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers);
} }
} }

View File

@ -62,9 +62,11 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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). // If 1 dragitem is set we assume the answer is complete (like Moodle does).
for (const name in answers) { for (const name in answers) {
if (answers[name]) { if (answers[name]) {
@ -93,7 +95,7 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler {
* @return 1 if gradable, 0 if not gradable, -1 if cannot determine. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine.
*/ */
isGradableResponse(question: any, answers: any): number { 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 question Question.
* @param prevAnswers Object with the previous question answers. * @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new 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. * @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); return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers);
} }

View File

@ -61,9 +61,11 @@ export class AddonQtypeDdwtosHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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) { for (const name in answers) {
const value = answers[name]; const value = answers[name];
if (!value || value === '0') { if (!value || value === '0') {
@ -108,9 +110,11 @@ export class AddonQtypeDdwtosHandler implements CoreQuestionHandler {
* @param question Question. * @param question Question.
* @param prevAnswers Object with the previous question answers. * @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new 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. * @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); return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers);
} }
} }

View File

@ -27,7 +27,7 @@
<!-- Attachments. --> <!-- Attachments. -->
<ng-container *ngIf="question.allowsAttachments"> <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" > <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 { Component, OnInit, Injector } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreSites } from '@providers/sites';
import { CoreWSExternalFile } from '@providers/ws'; import { CoreWSExternalFile } from '@providers/ws';
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
import { CoreQuestion } from '@core/question/providers/question';
import { FormControl, FormBuilder } from '@angular/forms'; import { FormControl, FormBuilder } from '@angular/forms';
import { CoreFileSession } from '@providers/file-session'; import { CoreFileSession } from '@providers/file-session';
@ -42,14 +42,15 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
* Component being initialized. * Component being initialized.
*/ */
ngOnInit(): void { ngOnInit(): void {
this.uploadFilesSupported = CoreSites.instance.getCurrentSite().isVersionGreaterEqualThan('3.10'); this.uploadFilesSupported = typeof this.question.responsefileareas != 'undefined';
this.initEssayComponent(); this.initEssayComponent();
this.formControl = this.fb.control(this.question.textarea && this.question.textarea.text); this.formControl = this.fb.control(this.question.textarea && this.question.textarea.text);
if (this.question.allowsAttachments && this.uploadFilesSupported) { if (this.question.allowsAttachments && this.uploadFilesSupported) {
this.attachments = Array.from(this.questionHelper.getResponseFileAreaFiles(this.question, 'attachments')); 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. // limitations under the License.
import { Injectable, Injector } from '@angular/core'; import { Injectable, Injector } from '@angular/core';
import { CoreFileSession } from '@providers/file-session';
import { CoreSites } from '@providers/sites'; import { CoreSites } from '@providers/sites';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreFileUploader } from '@core/fileuploader/providers/fileuploader';
import { CoreQuestionHandler } from '@core/question/providers/delegate'; import { CoreQuestionHandler } from '@core/question/providers/delegate';
import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
import { CoreQuestion } from '@core/question/providers/question';
import { AddonQtypeEssayComponent } from '../component/essay'; import { AddonQtypeEssayComponent } from '../component/essay';
/** /**
@ -33,6 +36,24 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler {
constructor(private utils: CoreUtilsProvider, private questionHelper: CoreQuestionHelperProvider, constructor(private utils: CoreUtilsProvider, private questionHelper: CoreQuestionHelperProvider,
private textUtils: CoreTextUtilsProvider, private domUtils: CoreDomUtilsProvider) { } 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. * 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. * 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 { getPreventSubmitMessage(question: any): string {
const element = this.domUtils.convertToElement(question.html); 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. // The question allows attachments. Since the app cannot attach files yet we will prevent submitting the question.
return 'core.question.errorattachmentsnotsupportedinsite'; return 'core.question.errorattachmentsnotsupportedinsite';
} }
if (this.questionHelper.hasDraftFileUrls(element.innerHTML)) { if (!uploadFilesSupported && this.questionHelper.hasDraftFileUrls(element.innerHTML)) {
return 'core.question.errorinlinefilesnotsupportedinsite'; return 'core.question.errorinlinefilesnotsupportedinsite';
} }
} }
@ -82,20 +104,36 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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 element = this.domUtils.convertToElement(question.html);
const hasInlineText = answers['answer'] && answers['answer'] !== '', const hasInlineText = answers['answer'] && answers['answer'] !== '';
allowsAttachments = !!element.querySelector('div[id*=filemanager]'); const allowsInlineText = !!element.querySelector('textarea[name*=_answer]');
const allowsAttachments = !!element.querySelector('div[id*=filemanager]');
const uploadFilesSupported = typeof question.responsefileareas != 'undefined';
if (!allowsAttachments) { if (!allowsAttachments) {
return hasInlineText ? 1 : 0; return hasInlineText ? 1 : 0;
} }
// We can't know if the attachments are required or if the user added any in web. if (!uploadFilesSupported) {
return -1; // 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 question Question.
* @param prevAnswers Object with the previous question answers. * @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new 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. * @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'); 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 question Question.
* @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @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 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. * @param siteId Site ID. If not defined, current site.
* @return Return a promise resolved when done if async, void if sync. * @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 element = this.domUtils.convertToElement(question.html);
const attachmentsInput = <HTMLInputElement> element.querySelector('.attachments input[name*=_attachments]');
// Search the textarea to get its name. // Search the textarea to get its name.
const textarea = <HTMLTextAreaElement> element.querySelector('textarea[name*=_answer]'); 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. // Add some HTML to the text if needed.
answers[textarea.name] = this.textUtils.formatHtmlLines(answers[textarea.name]); 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 question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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. // We should always get a value for each select so we can assume we receive all the possible answers.
for (const name in answers) { for (const name in answers) {
const value = answers[name]; const value = answers[name];
@ -110,9 +112,11 @@ export class AddonQtypeGapSelectHandler implements CoreQuestionHandler {
* @param question Question. * @param question Question.
* @param prevAnswers Object with the previous question answers. * @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new 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. * @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); return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers);
} }
} }

View File

@ -61,9 +61,11 @@ export class AddonQtypeMatchHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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. // We should always get a value for each select so we can assume we receive all the possible answers.
for (const name in answers) { for (const name in answers) {
const value = answers[name]; const value = answers[name];
@ -110,9 +112,11 @@ export class AddonQtypeMatchHandler implements CoreQuestionHandler {
* @param question Question. * @param question Question.
* @param prevAnswers Object with the previous question answers. * @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new 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. * @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); return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers);
} }
} }

View File

@ -62,9 +62,11 @@ export class AddonQtypeMultiAnswerHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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. // 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)); const names = this.questionProvider.getBasicAnswers(this.questionHelper.getAllInputNamesFromHtml(question.html));
for (const name in names) { for (const name in names) {
@ -112,9 +114,11 @@ export class AddonQtypeMultiAnswerHandler implements CoreQuestionHandler {
* @param question Question. * @param question Question.
* @param prevAnswers Object with the previous question answers. * @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new 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. * @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); return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers);
} }

View File

@ -45,9 +45,11 @@ export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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, let isSingle = true,
isMultiComplete = false; isMultiComplete = false;
@ -98,7 +100,7 @@ export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler {
* @return 1 if gradable, 0 if not gradable, -1 if cannot determine. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine.
*/ */
isGradableResponse(question: any, answers: any): number { 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 question Question.
* @param prevAnswers Object with the previous question answers. * @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new 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. * @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, let isSingle = true,
isMultiSame = true; isMultiSame = true;
@ -158,10 +162,13 @@ export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler {
* @param question Question. * @param question Question.
* @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @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 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. * @param siteId Site ID. If not defined, current site.
* @return Return a promise resolved when done if async, void if sync. * @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]) { 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 /* 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. */ 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 question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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. // 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 question Question.
* @param prevAnswers Object with the previous question answers. * @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new 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. * @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. // 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 question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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; 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. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine.
*/ */
isGradableResponse(question: any, answers: any): number { 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 question Question.
* @param prevAnswers Object with the previous question answers. * @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new 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. * @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'); return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer');
} }
} }

View File

@ -46,9 +46,11 @@ export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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; 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. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine.
*/ */
isGradableResponse(question: any, answers: any): number { 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 question Question.
* @param prevAnswers Object with the previous question answers. * @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new 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. * @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'); return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer');
} }
@ -91,10 +95,13 @@ export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler {
* @param question Question. * @param question Question.
* @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @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 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. * @param siteId Site ID. If not defined, current site.
* @return Return a promise resolved when done if async, void if sync. * @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]) { 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. // The user hasn't answered. Delete the answer to prevent marking one of the answers automatically.
delete answers[question.optionsName]; delete answers[question.optionsName];

View File

@ -15,6 +15,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ModalController } from 'ionic-angular'; import { ModalController } from 'ionic-angular';
import { Camera, CameraOptions } from '@ionic-native/camera'; 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 { MediaCapture, MediaFile, CaptureError, CaptureAudioOptions, CaptureVideoOptions } from '@ionic-native/media-capture';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreFileProvider } from '@providers/file'; import { CoreFileProvider } from '@providers/file';
@ -25,9 +26,10 @@ import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreWSFileUploadOptions } from '@providers/ws'; import { CoreWSFileUploadOptions, CoreWSExternalFile } from '@providers/ws';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { CoreApp } from '@providers/app'; import { CoreApp } from '@providers/app';
import { makeSingleton } from '@singletons/core.singletons';
/** /**
* File upload options. * File upload options.
@ -96,7 +98,7 @@ export class CoreFileUploaderProvider {
// Currently we are going to compare the order of the files as well. // 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. // This function can be improved comparing more fields or not comparing the order.
for (let i = 0; i < a.length; i++) { 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; 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. * 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 component Component the question belongs to.
* @param attemptId Attempt ID the question belongs to. * @param attemptId Attempt ID the question belongs to.
* @param question The question. * @param question The question.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
* @return New state (or promise resolved with state). * @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> { : CoreQuestionState | Promise<CoreQuestionState> {
// Return the current state. // Return the current state.
return this.questionProvider.getState(question.state); return this.questionProvider.getState(question.state);

View File

@ -79,9 +79,11 @@ export class CoreQuestionBaseHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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; return -1;
} }
@ -103,9 +105,11 @@ export class CoreQuestionBaseHandler implements CoreQuestionHandler {
* @param question Question. * @param question Question.
* @param prevAnswers Object with the previous question answers. * @param prevAnswers Object with the previous question answers.
* @param newAnswers Object with the new 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. * @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; return false;
} }
@ -115,10 +119,13 @@ export class CoreQuestionBaseHandler implements CoreQuestionHandler {
* @param question Question. * @param question Question.
* @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @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 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. * @param siteId Site ID. If not defined, current site.
* @return Return a promise resolved when done if async, void if sync. * @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. // Nothing to do.
} }

View File

@ -36,10 +36,11 @@ export interface CoreQuestionBehaviourHandler extends CoreDelegateHandler {
* @param component Component the question belongs to. * @param component Component the question belongs to.
* @param attemptId Attempt ID the question belongs to. * @param attemptId Attempt ID the question belongs to.
* @param question The question. * @param question The question.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
* @return State (or promise resolved with state). * @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>; : CoreQuestionState | Promise<CoreQuestionState>;
/** /**
@ -74,15 +75,16 @@ export class CoreQuestionBehaviourDelegate extends CoreDelegate {
* @param component Component the question belongs to. * @param component Component the question belongs to.
* @param attemptId Attempt ID the question belongs to. * @param attemptId Attempt ID the question belongs to.
* @param question The question. * @param question The question.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
* @return Promise resolved with state. * @return Promise resolved with state.
*/ */
determineNewState(behaviour: string, component: string, attemptId: number, question: any, siteId?: string) determineNewState(behaviour: string, component: string, attemptId: number, question: any, componentId: string | number,
: Promise<CoreQuestionState> { siteId?: string): Promise<CoreQuestionState> {
behaviour = this.questionDelegate.getBehaviourForQuestion(question, behaviour); behaviour = this.questionDelegate.getBehaviourForQuestion(question, behaviour);
return Promise.resolve(this.executeFunctionOnEnabled(behaviour, 'determineNewState', 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 question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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, * 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. * @param newAnswers Object with the new question answers.
* @return Whether they're the same. * @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. * 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 question Question.
* @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @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 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. * @param siteId Site ID. If not defined, current site.
* @return Return a promise resolved when done if async, void if sync. * @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. * 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. * @return List of URLs.
*/ */
getAdditionalDownloadableFiles?(question: any, usageId: number): string[]; 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 question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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); 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. * @param newAnswers Object with the new question answers.
* @return Whether they're the same. * @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); 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 question Question.
* @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @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 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. * @param siteId Site ID. If not defined, current site.
* @return Promise resolved when data has been prepared. * @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); 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]) || []; 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. * 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. * 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) { if (!component) {
component = CoreQuestionProvider.COMPONENT; component = CoreQuestionProvider.COMPONENT;
componentId = question.id; componentId = question.number;
} }
urls.push(...this.questionDelegate.getAdditionalDownloadableFiles(question, usageId)); urls.push(...this.questionDelegate.getAdditionalDownloadableFiles(question, usageId));
@ -572,15 +588,19 @@ export class CoreQuestionHelperProvider {
* @param questions The list of questions. * @param questions The list of questions.
* @param answers The input data. * @param answers The input data.
* @param offline True if data should be saved in offline. * @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. * @param siteId Site ID. If not defined, current site.
* @return Promise resolved with answers to send to server. * @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 = []; const promises = [];
questions = questions || []; questions = questions || [];
questions.forEach((question) => { 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(() => { return this.utils.allPromises(promises).then(() => {

View File

@ -13,10 +13,13 @@
// limitations under the License. // limitations under the License.
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreFile } from '@providers/file';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
import { CoreTextUtils } from '@providers/utils/text';
import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { makeSingleton } from '@singletons/core.singletons';
/** /**
* An object to represent a question state. * 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. * Extract the question slot from a question name.
* *
@ -612,3 +644,5 @@ export class CoreQuestionProvider {
}); });
} }
} }
export class CoreQuestion extends makeSingleton(CoreQuestionProvider) {}