Skip to content

Commit

Permalink
Merge pull request #12359 from rtibbles/quiz_reports
Browse files Browse the repository at this point in the history
Fix question listing and navigation in quiz reports
  • Loading branch information
rtibbles authored Jun 28, 2024
2 parents 4237257 + af98e38 commit 0a5ece1
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 272 deletions.
31 changes: 29 additions & 2 deletions kolibri/core/assets/src/exams/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
});
}
200 changes: 64 additions & 136 deletions kolibri/core/assets/src/views/AttemptLogList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,40 +25,38 @@
>
{{ selectedSection.label }}
</h2>
</div>

<KSelect
v-if="isMobile"
class="history-select"
:value="selectedQuestion"
:label="questionsLabel$()"
:options="questionSelectOptions"
:disabled="$attrs.disabled"
@change="handleQuestionChange($event.value)"
>
<template #display>
<AttemptLogItem
class="attempt-selected"
:isSurvey="isSurvey"
:attemptLog="attemptLogs[selectedQuestionNumber]"
displayTag="span"
/>
</template>
<template #option="{ index }">
<AttemptLogItem
class="attempt-option"
:isSurvey="isSurvey"
:attemptLog="attemptLogs[index]"
displayTag="span"
/>
</template>
</KSelect>
<KSelect
class="history-select"
:value="selectedQuestion"
:label="questionsLabel$()"
:options="questionSelectOptions"
:disabled="$attrs.disabled"
@change="handleQuestionChange($event.value)"
>
<template #display>
<AttemptLogItem
class="attempt-selected"
:isSurvey="isSurvey"
:attemptLog="attemptLogs[selectedQuestionNumber]"
displayTag="span"
/>
</template>
<template #option="{ index }">
<AttemptLogItem
class="attempt-option"
:isSurvey="isSurvey"
:attemptLog="attemptLogs[index]"
displayTag="span"
/>
</template>
</KSelect>
</div>

<AccordionContainer
v-else-if="sections && sections.length"
v-else
:hideTopActions="true"
:items="sections"
:style="{ backgroundColor: $themeTokens.surface }"
>
<AccordionItem
v-for="(section, index) in sections"
Expand All @@ -67,10 +65,16 @@
:title="displaySectionTitle(section, index)"
@focus="expand(index)"
>
<template #heading="{ title }">
<template
v-if="sections.length > 1"
#heading="{ title }"
>
<h3
v-if="title"
class="accordion-header"
:style="{
backgroundColor: index === currentSectionIndex ? $themePalette.grey.v_100 : '',
}"
>
<KButton
tabindex="0"
Expand All @@ -90,12 +94,7 @@
</h3>
</template>
<template #content>
<div
v-show="isExpanded(index)"
:style="{
backgroundColor: $themePalette.grey.v_100,
}"
>
<div v-show="sections.length === 1 || isExpanded(index)">
<ul
ref="attemptList"
class="history-list"
Expand All @@ -114,22 +113,26 @@
:key="`attempt-item-${qIndex}`"
class="attempt-item"
:style="{
backgroundColor: isSelected(qIndex) ? $themePalette.grey.v_100 : '',
backgroundColor: isSelected(section.startQuestionNumber + qIndex)
? $themePalette.grey.v_100
: '',
}"
>
<a
ref="attemptListOption"
role="option"
class="attempt-item-anchor"
:aria-selected="isSelected(qIndex).toString()"
:tabindex="isSelected(qIndex) ? 0 : -1"
@click.prevent="setSelectedAttemptLog(qIndex)"
@keydown.enter="setSelectedAttemptLog(qIndex)"
@keydown.space.prevent="setSelectedAttemptLog(qIndex)"
:aria-selected="isSelected(section.startQuestionNumber + qIndex).toString()"
:tabindex="isSelected(section.startQuestionNumber + qIndex) ? 0 : -1"
@click.prevent="setSelectedAttemptLog(section.startQuestionNumber + qIndex)"
@keydown.enter="setSelectedAttemptLog(section.startQuestionNumber + qIndex)"
@keydown.space.prevent="
setSelectedAttemptLog(section.startQuestionNumber + qIndex)
"
>
<AttemptLogItem
:isSurvey="isSurvey"
:attemptLog="attemptLogs[qIndex]"
:attemptLog="attemptLogs[section.startQuestionNumber + qIndex]"
displayTag="p"
/>
</a>
Expand All @@ -139,45 +142,6 @@
</template>
</AccordionItem>
</AccordionContainer>

<ul
v-else
ref="attemptList"
class="history-list"
role="listbox"
@keydown.home="setSelectedAttemptLog(0)"
@keydown.end="setSelectedAttemptLog(attemptLogs.length - 1)"
@keydown.up.prevent="setSelectedAttemptLog(previousQuestion(selectedQuestionNumber))"
@keydown.left.prevent="setSelectedAttemptLog(previousQuestion(selectedQuestionNumber))"
@keydown.down.prevent="setSelectedAttemptLog(nextQuestion(selectedQuestionNumber))"
@keydown.right.prevent="setSelectedAttemptLog(nextQuestion(selectedQuestionNumber))"
>
<li
v-for="(question, qIndex) in attemptLogs"
:key="`attempt-item-${qIndex}`"
class="attempt-item"
:style="{
backgroundColor: isSelected(qIndex) ? $themePalette.grey.v_100 : '',
}"
>
<a
ref="attemptListOption"
role="option"
class="attempt-item-anchor"
:aria-selected="isSelected(qIndex).toString()"
:tabindex="isSelected(qIndex) ? 0 : -1"
@click.prevent="setSelectedAttemptLog(qIndex)"
@keydown.enter="setSelectedAttemptLog(qIndex)"
@keydown.space.prevent="setSelectedAttemptLog(qIndex)"
>
<AttemptLogItem
:isSurvey="isSurvey"
:attemptLog="attemptLogs[qIndex]"
displayTag="p"
/>
</a>
</li>
</ul>
</div>

</template>
Expand All @@ -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';
Expand All @@ -207,99 +171,60 @@
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,
label: displaySectionTitle(section, index),
}));
});
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];
});
// 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,
Expand All @@ -319,8 +244,11 @@
props: {
sections: {
type: Array,
required: false,
default: () => [],
required: true,
},
currentSectionIndex: {
type: Number,
required: true,
},
attemptLogs: {
type: Array,
Expand Down
Loading

0 comments on commit 0a5ece1

Please sign in to comment.