diff --git a/kolibri/core/assets/src/exams/utils.js b/kolibri/core/assets/src/exams/utils.js index 3ce86f6da7f..c4a484ee5aa 100644 --- a/kolibri/core/assets/src/exams/utils.js +++ b/kolibri/core/assets/src/exams/utils.js @@ -233,8 +233,8 @@ export function getExamReport(examId, tryIndex = 0, questionNumber = 0, interact return exam; } - // TODO: Reports will eventually want to have the proper section-specific data to render - // the report page - but we are not updating the report UI yet. + // We need this array of questions to easily do questionNumber based indexing across + // all the sections. const questions = exam.question_sources.reduce((qs, sect) => { qs = [...qs, ...sect.questions]; return qs; @@ -254,3 +254,30 @@ export function getExamReport(examId, tryIndex = 0, questionNumber = 0, interact }); }); } + +export function annotateSections(sections, questions) { + // Adding the additional startQuestionNumber and endQuestionNumber fields to each section + // allows to more easily identify the overall place in the quiz that a question is. + // This is useful for deciding which section is currently active based on the global + // question number, and also for displaying the global question number in the UI. + if (!sections) { + return [ + { + title: '', + questions: questions, + startQuestionNumber: 0, + endQuestionNumber: questions.length - 1, + }, + ]; + } + let startQuestionNumber = 0; + return sections.map(section => { + const annotatedSection = { + ...section, + startQuestionNumber, + endQuestionNumber: startQuestionNumber + section.questions.length - 1, + }; + startQuestionNumber += section.questions.length; + return annotatedSection; + }); +} diff --git a/kolibri/core/assets/src/views/AttemptLogList.vue b/kolibri/core/assets/src/views/AttemptLogList.vue index dd8193ac771..ccb399c90b9 100644 --- a/kolibri/core/assets/src/views/AttemptLogList.vue +++ b/kolibri/core/assets/src/views/AttemptLogList.vue @@ -25,40 +25,38 @@ > {{ selectedSection.label }} - - - - - + + + + + - @@ -190,10 +154,10 @@ enhancedQuizManagementStrings, } from 'kolibri-common/strings/enhancedQuizManagementStrings'; import useAccordion from 'kolibri-common/components/useAccordion'; - import { coreStrings } from 'kolibri.coreVue.mixins.commonCoreStrings'; + import coreStrings from 'kolibri.utils.coreStrings'; import AccordionItem from 'kolibri-common/components/AccordionItem'; import AccordionContainer from 'kolibri-common/components/AccordionContainer'; - import { computed, watch } from 'kolibri.lib.vueCompositionApi'; + import { computed, onMounted, watch } from 'kolibri.lib.vueCompositionApi'; import { toRefs } from '@vueuse/core'; import AttemptLogItem from './AttemptLogItem'; @@ -207,31 +171,17 @@ setup(props, { emit }) { const { questionsLabel$, quizSectionsLabel$ } = enhancedQuizManagementStrings; const { questionNumberLabel$ } = coreStrings; - const { sections, selectedQuestionNumber } = toRefs(props); + const { currentSectionIndex, sections, selectedQuestionNumber } = toRefs(props); const { expand, isExpanded, toggle } = useAccordion(sections); /** Finds the section which the current attempt belongs to and expands it */ function expandCurrentSectionIfNeeded() { - if (!sections.value || !sections.value.length) { - return; - } - let qCount = 0; - for (let i = 0; i < sections?.value?.length; i++) { - qCount += sections?.value[i]?.questions?.length; - if (qCount >= selectedQuestionNumber.value) { - if (!isExpanded(i)) { - expand(i); - } - break; - } + if (!isExpanded(currentSectionIndex.value)) { + expand(currentSectionIndex.value); } } - const allQuestionsInOrder = computed(() => { - return sections.value.reduce((a, s) => [...a, ...s.questions], []); - }); - const sectionSelectOptions = computed(() => { return sections.value.map((section, index) => ({ value: index, @@ -239,33 +189,17 @@ })); }); - const currentSectionIndex = computed(() => { - let qCount = 0; - for (let i = 0; i < sections.value.length; i++) { - qCount += sections.value[i].questions.length; - if (qCount >= selectedQuestionNumber.value) { - return i; - } - } - return 0; - }); - const currentSection = computed(() => { return sections.value[currentSectionIndex.value]; }); const questionSelectOptions = computed(() => { return currentSection.value.questions.map((question, index) => ({ - value: question.item, + value: index, label: questionNumberLabel$({ questionNumber: index + 1 }), })); }); - // The question itself - const currentQuestion = computed(() => { - return allQuestionsInOrder.value[selectedQuestionNumber.value]; - }); - // The KSelect-shaped object for the current section const selectedSection = computed(() => { return sectionSelectOptions.value[currentSectionIndex.value]; @@ -273,33 +207,24 @@ // The KSelect-shaped object for the current question const selectedQuestion = computed(() => { - return questionSelectOptions.value.find(opt => opt.value === currentQuestion.value.item); + return questionSelectOptions.value[ + selectedQuestionNumber.value - currentSection.value.startQuestionNumber + ]; }); function handleQuestionChange(item) { - const questionIndex = allQuestionsInOrder.value.findIndex(q => q.item === item); - if (questionIndex !== -1) { - emit('select', questionIndex); - expandCurrentSectionIfNeeded(); - } + emit('select', item.value + currentSection.value.startQuestionNumber); + expandCurrentSectionIfNeeded(); } function handleSectionChange(index) { - const questionIndex = sections.value.slice(0, index).reduce((acc, s, i) => { - if (i !== index) { - acc += s.questions.length; - return acc; - } else { - // This will always be the last iteration thanks to slice - return acc + 1; - } - }, 0); + const questionIndex = sections.value[index].startQuestionNumber; emit('select', questionIndex); expandCurrentSectionIfNeeded(); } watch(selectedQuestionNumber, expandCurrentSectionIfNeeded); - expandCurrentSectionIfNeeded(); + onMounted(expandCurrentSectionIfNeeded); return { handleSectionChange, @@ -319,8 +244,11 @@ props: { sections: { type: Array, - required: false, - default: () => [], + required: true, + }, + currentSectionIndex: { + type: Number, + required: true, }, attemptLogs: { type: Array, diff --git a/kolibri/core/assets/src/views/ExamReport/index.vue b/kolibri/core/assets/src/views/ExamReport/index.vue index f6a3672c48e..0378ed2be1e 100644 --- a/kolibri/core/assets/src/views/ExamReport/index.vue +++ b/kolibri/core/assets/src/views/ExamReport/index.vue @@ -94,7 +94,8 @@ :attemptLogs="attemptLogs" :selectedQuestionNumber="questionNumber" :isSurvey="isSurvey" - :sections="sections" + :sections="annotatedSections" + :currentSectionIndex="currentSectionIndex" @select="navigateToQuestion" /> @@ -115,7 +116,8 @@ :attemptLogs="attemptLogs" :selectedQuestionNumber="questionNumber" :isSurvey="isSurvey" - :sections="sections" + :sections="annotatedSections" + :currentSectionIndex="currentSectionIndex" @select="navigateToQuestion" />
-

{{ questionNumberInSectionLabel }}

+

{{ questionNumberInSectionLabel }}

+ +

+ {{ currentSection.description }} +

[], + default: null, }, // An array of questions in the format: // { @@ -353,19 +361,28 @@ }; }, computed: { + annotatedSections() { + return annotateSections(this.sections, this.questions); + }, + currentSectionIndex() { + return this.annotatedSections.findIndex( + section => + this.questionNumber >= section.startQuestionNumber && + this.questionNumber <= section.endQuestionNumber, + ); + }, + currentSection() { + return this.annotatedSections[this.currentSectionIndex]; + }, questionNumberInSectionLabel() { - if (!this.sections) { - return ''; - } - for (let iSection = 0; iSection < this.sections.length; iSection++) { - const section = this.sections[iSection]; - for (let iQuestion = 0; iQuestion < section.questions.length; iQuestion++) { - if (section.questions[iQuestion].item === this.itemId) { - return this.coreString('questionNumberLabel', { questionNumber: iQuestion + 1 }); - } - } + const questionLabel = this.coreString('questionNumberLabel', { + questionNumber: this.questionNumber + 1, + }); + if (this.annotatedSections.length === 1) { + return questionLabel; } - return ''; + const sectionLabel = displaySectionTitle(this.currentSection, this.currentSectionIndex); + return `${sectionLabel} - ${questionLabel}`; }, attemptLogs() { if (this.isQuiz || this.isSurvey) { diff --git a/kolibri/plugins/learn/assets/src/modules/examViewer/index.js b/kolibri/plugins/learn/assets/src/modules/examViewer/index.js index 5a0c56df559..243c01409d0 100644 --- a/kolibri/plugins/learn/assets/src/modules/examViewer/index.js +++ b/kolibri/plugins/learn/assets/src/modules/examViewer/index.js @@ -20,36 +20,4 @@ export default { Object.assign(state, defaultState()); }, }, - getters: { - sections(state) { - if (!state.exam.question_sources) return []; - return state.exam.question_sources; - }, - currentQuestion(state) { - if (state.questions.length === 0) return null; - return state.questions[state.questionNumber]; - }, - currentSection(state, { sections, currentSectionIndex }) { - return sections[currentSectionIndex]; - }, - currentSectionIndex(state, { currentQuestion, sections }) { - return sections.findIndex(section => - section.questions.map(q => q.item).includes(currentQuestion.item), - ); - }, - sectionSelectOptions(state, { sections }) { - return ( - sections.map((section, i) => ({ - label: displaySectionTitle(section, i), - value: i, - })) || [] - ); - }, - currentSectionOption(state, { currentSection, sectionSelectOptions }) { - if (!currentSection) return {}; - return sectionSelectOptions.find( - (opt, i) => opt.label === displaySectionTitle(currentSection, i), - ); - }, - }, }; diff --git a/kolibri/plugins/learn/assets/src/views/ExamPage/AnswerHistory.vue b/kolibri/plugins/learn/assets/src/views/ExamPage/AnswerHistory.vue index e3f69a4bb51..09ec8d3f184 100644 --- a/kolibri/plugins/learn/assets/src/views/ExamPage/AnswerHistory.vue +++ b/kolibri/plugins/learn/assets/src/views/ExamPage/AnswerHistory.vue @@ -13,7 +13,14 @@ @focus="expand(index)" >