Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions scripts/langindex.json
Original file line number Diff line number Diff line change
Expand Up @@ -1861,6 +1861,7 @@
"core.mainmenu.help": "moodle",
"core.mainmenu.logout": "moodle",
"core.mainmenu.website": "local_moodlemobileapp",
"core.maxfilesize": "moodle",
"core.maxsizeandattachments": "moodle",
"core.min": "moodle",
"core.mins": "moodle",
Expand Down Expand Up @@ -1944,8 +1945,8 @@
"core.question.certainty": "qbehaviour_deferredcbm",
"core.question.complete": "question",
"core.question.correct": "question",
"core.question.errorattachmentsnotsupported": "local_moodlemobileapp",
"core.question.errorinlinefilesnotsupported": "local_moodlemobileapp",
"core.question.errorattachmentsnotsupportedinsite": "local_moodlemobileapp",
"core.question.errorembeddedfilesnotsupportedinsite": "local_moodlemobileapp",
"core.question.errorquestionnotsupported": "local_moodlemobileapp",
"core.question.feedback": "question",
"core.question.howtodraganddrop": "local_moodlemobileapp",
Expand Down
2 changes: 1 addition & 1 deletion src/addon/mod/lesson/providers/lesson-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid
const params = this.urlUtils.extractUrlParams(response.data.reviewlesson.value);
if (params && params.pageid) {
// The retake can be reviewed, mark it as finished. Don't block the user for this.
this.setRetakeFinishedInSync(lessonId, retake.retake, params.pageid, siteId);
this.setRetakeFinishedInSync(lessonId, retake.retake, Number(params.pageid), siteId);
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/addon/mod/quiz/pages/player/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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);
});
}

Expand Down
4 changes: 2 additions & 2 deletions src/addon/mod/quiz/providers/quiz-offline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
255 changes: 145 additions & 110 deletions src/addon/mod/quiz/providers/quiz-sync.ts

Large diffs are not rendered by default.

29 changes: 28 additions & 1 deletion src/addon/mod/quiz/providers/quiz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ export class AddonModQuizProvider {
};

return site.read('mod_quiz_get_attempt_data', params, preSets);
}).then((result) => {
return this.parseQuestions(result);
});
}

Expand Down Expand Up @@ -389,7 +391,9 @@ export class AddonModQuizProvider {
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
};

return site.read('mod_quiz_get_attempt_review', params, preSets);
return site.read('mod_quiz_get_attempt_review', params, preSets).then((result) => {
return this.parseQuestions(result);
});
});
}

Expand Down Expand Up @@ -427,6 +431,8 @@ export class AddonModQuizProvider {

return site.read('mod_quiz_get_attempt_summary', params, preSets).then((response) => {
if (response && response.questions) {
response = this.parseQuestions(response);

if (options.loadLocal) {
return this.quizOfflineProvider.loadQuestionsLocalStates(attemptId, response.questions, site.getId());
}
Expand Down Expand Up @@ -1560,6 +1566,27 @@ export class AddonModQuizProvider {
siteId);
}

/**
* Parse questions of a WS response.
*
* @param result Result to parse.
* @return Parsed result.
*/
parseQuestions(result: any): any {
for (let i = 0; i < result.questions.length; i++) {
const question = result.questions[i];

if (!question.settings) {
// Site doesn't return settings, stop.
break;
}

question.settings = this.textUtils.parseJSON(question.settings, null);
}

return result;
}

/**
* Process an attempt, saving its data.
*
Expand Down
19 changes: 12 additions & 7 deletions src/addon/qbehaviour/deferredcbm/providers/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand All @@ -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;
Expand All @@ -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'];
Expand Down
124 changes: 68 additions & 56 deletions src/addon/qbehaviour/deferredfeedback/providers/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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);
}

/**
Expand All @@ -72,75 +77,82 @@ 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,
isCompleteFn?: isCompleteResponseFunction, isSameFn?: isSameResponseFunction): Promise<CoreQuestionState> {
async 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.
return this.questionProvider.getQuestion(component, attemptId, question.slot, siteId).catch(() => {
let dbQuestion;
try {
dbQuestion = await this.questionProvider.getQuestion(component, attemptId, question.slot, siteId);
} catch (error) {
// No entry found, use the original data.
return question;
}).then((dbQuestion) => {
const state = this.questionProvider.getState(dbQuestion.state);
dbQuestion = question;
}

if (state.finished || !state.active) {
// Question is finished, it cannot change.
return state;
}
const state = this.questionProvider.getState(dbQuestion.state);

// We need to check if the answers have changed. Retrieve current stored answers.
return this.questionProvider.getQuestionAnswers(component, attemptId, question.slot, false, siteId)
.then((prevAnswers) => {
if (state.finished || !state.active) {
// Question is finished, it cannot change.
return state;
}

const newBasicAnswers = this.questionProvider.getBasicAnswers(question.answers);
const newBasicAnswers = this.questionProvider.getBasicAnswers(question.answers);

prevAnswers = this.questionProvider.convertAnswersArrayToObject(prevAnswers, true);
const prevBasicAnswers = this.questionProvider.getBasicAnswers(prevAnswers);
if (dbQuestion.state) {
// Question already has a state stored. Check if answer has changed.
let prevAnswers = await this.questionProvider.getQuestionAnswers(component, attemptId, question.slot, false, siteId);

// If answers haven't changed the state is the same.
if (isSameFn) {
if (isSameFn(question, prevAnswers, prevBasicAnswers, question.answers, newBasicAnswers)) {
return state;
}
} else {
if (this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers)) {
return state;
}
}
prevAnswers = this.questionProvider.convertAnswersArrayToObject(prevAnswers, true);
const prevBasicAnswers = this.questionProvider.getBasicAnswers(prevAnswers);

// Answers have changed. Now check if the response is complete and calculate the new state.
let complete: number,
newState: string;
if (isCompleteFn) {
// Pass all the answers since some behaviours might need the extra data.
complete = isCompleteFn(question, question.answers);
} else {
// Only pass the basic answers since questions should be independent of extra data.
complete = this.questionDelegate.isCompleteResponse(question, newBasicAnswers);
// If answers haven't changed the state is the same.
if (isSameFn) {
if (isSameFn(question, prevAnswers, prevBasicAnswers, question.answers, newBasicAnswers,
component, componentId)) {
return state;
}

if (complete < 0) {
newState = 'cannotdeterminestatus';
} else if (complete > 0) {
newState = 'complete';
} else {
const gradable = this.questionDelegate.isGradableResponse(question, newBasicAnswers);
if (gradable < 0) {
newState = 'cannotdeterminestatus';
} else if (gradable > 0) {
newState = 'invalid';
} else {
newState = 'todo';
}
} else {
if (this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers, component, componentId)) {
return state;
}
}
}

// Answers have changed. Now check if the response is complete and calculate the new state.
let complete: number;
let newState: string;

if (isCompleteFn) {
// Pass all the answers since some behaviours might need the extra data.
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, component, componentId);
}

if (complete < 0) {
newState = 'cannotdeterminestatus';
} else if (complete > 0) {
newState = 'complete';
} else {
const gradable = this.questionDelegate.isGradableResponse(question, newBasicAnswers, component, componentId);
if (gradable < 0) {
newState = 'cannotdeterminestatus';
} else if (gradable > 0) {
newState = 'invalid';
} else {
newState = 'todo';
}
}

return this.questionProvider.getState(newState);
});
});
return this.questionProvider.getState(newState);
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/addon/qbehaviour/informationitem/providers/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Loading