Skip to content

Commit

Permalink
Merge pull request #12182 from nucleogenesis/enhancement--quiz-sectio…
Browse files Browse the repository at this point in the history
…n-question-list

Updates to sections in ExamPage
  • Loading branch information
nucleogenesis committed Jun 11, 2024
2 parents cb1fcd7 + 766faf0 commit c25cdc8
Show file tree
Hide file tree
Showing 24 changed files with 1,502 additions and 624 deletions.
399 changes: 348 additions & 51 deletions kolibri/core/assets/src/views/AttemptLogList.vue

Large diffs are not rendered by default.

24 changes: 23 additions & 1 deletion kolibri/core/assets/src/views/ExamReport/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
:attemptLogs="attemptLogs"
:selectedQuestionNumber="questionNumber"
:isSurvey="isSurvey"
:sections="sections"
@select="navigateToQuestion"
/>
</template>
Expand All @@ -93,6 +94,7 @@
:attemptLogs="attemptLogs"
:selectedQuestionNumber="questionNumber"
:isSurvey="isSurvey"
:sections="sections"
@select="navigateToQuestion"
/>
<div
Expand All @@ -101,7 +103,7 @@
:class="windowIsSmall ? 'mobile-exercise-container' : ''"
:style="{ backgroundColor: $themeTokens.surface }"
>
<h3>{{ coreString('questionNumberLabel', { questionNumber: questionNumber + 1 }) }}</h3>
<h3>{{ questionNumberInSectionLabel }}</h3>

<div v-if="!isSurvey" data-test="diff-business">
<KCheckbox
Expand Down Expand Up @@ -253,6 +255,12 @@
type: Function,
required: true,
},
// The exam.question_sources value
sections: {
type: Array,
required: false,
default: () => [],
},
// An array of questions in the format:
// {
// exercise_id: <exercise_id>,
Expand Down Expand Up @@ -315,6 +323,20 @@
};
},
computed: {
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 });
}
}
}
return '';
},
attemptLogs() {
if (this.isQuiz || this.isSurvey) {
return this.quizAttempts();
Expand Down
97 changes: 48 additions & 49 deletions kolibri/plugins/coach/assets/src/composables/useQuizCreation.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,6 @@ function uuidv4() {
return v4().replace(/-/g, '');
}

const { sectionLabel$ } = enhancedQuizManagementStrings;

function displaySectionTitle(section, index) {
return section.section_title === ''
? sectionLabel$({ sectionNumber: index + 1 })
: section.section_title;
}

/** Validators **/
/* objectSpecs expects every property to be available -- but we don't want to have to make an
* object with every property just to validate it. So we use these functions to validate subsets
Expand Down Expand Up @@ -89,6 +81,7 @@ export default function useQuizCreation() {
// The user has removed all resources from the section, so we can clear all questions too
updates.questions = [];
}

if (resource_pool?.length > 0) {
// The resource_pool is being updated
if (originalResourcePool.length === 0) {
Expand All @@ -103,10 +96,11 @@ export default function useQuizCreation() {
// if there weren't resources in the originalResourcePool before.
// ***
updates.questions = selectRandomQuestionsFromResources(
question_count || originalQuestionCount,
question_count || originalQuestionCount || 0,
resource_pool
);
} else {
// We're updating the resource_pool of a section that already had resources
if (question_count === 0) {
updates.questions = [];
} else {
Expand All @@ -125,7 +119,7 @@ export default function useQuizCreation() {
);
if (removedResourceQuestionIds.length !== 0) {
const questionsToKeep = originalQuestions.filter(
q => !removedResourceQuestionIds.includes(q.id)
q => !removedResourceQuestionIds.includes(q.item)
);
const numReplacementsNeeded =
(question_count || originalQuestionCount) - questionsToKeep.length;
Expand All @@ -136,30 +130,18 @@ export default function useQuizCreation() {
}
}
}
} else if (question_count !== originalQuestionCount) {
/**
* Handle edge cases re: questions and question_count changing. When the question_count
* changes, we remove/add questions to match the new count. If questions are deleted, then
* we will update question_count accordingly.
**/

// If the question count changed AND questions have changed, be sure they're the same length
// or we can add questions to match the new question_count
if (question_count < originalQuestionCount) {
// If the question_count is being reduced, we need to remove any questions that are now
// outside the bounds of the new question_count
updates.questions = originalQuestions.slice(0, question_count);
} else if (question_count > originalQuestionCount) {
// If the question_count is being increased, we need to add new questions to the end of the
// questions array
const numQuestionsToAdd = question_count - originalQuestionCount;
const newQuestions = selectRandomQuestionsFromResources(
numQuestionsToAdd,
originalResourcePool,
originalQuestions.map(q => q.id) // Exclude questions we already have to avoid duplicates
);
updates.questions = [...targetSection.questions, ...newQuestions];
}
}
// The resource pool isn't being updated but the question_count is so we need to update them
if (question_count > originalQuestionCount) {
updates.questions = [
...originalQuestions,
...selectRandomQuestionsFromResources(
question_count - originalQuestionCount,
originalResourcePool
),
];
} else if (question_count < originalQuestionCount) {
updates.questions = originalQuestions.slice(0, question_count);
}

set(_quiz, {
Expand Down Expand Up @@ -208,7 +190,11 @@ export default function useQuizCreation() {
exerciseTitles,
questionIdArrays,
Math.floor(Math.random() * 1000),
excludedIds
[
...excludedIds,
// Always exclude the questions that are already in the entire quiz
...get(allQuestionsInQuiz).map(q => q.item),
]
);
}

Expand Down Expand Up @@ -257,6 +243,7 @@ export default function useQuizCreation() {
* Sets the given section_id as the active section ID, however, if the ID is not found or is null
* it will set the activeId to the first section in _quiz.question_sources */
function setActiveSection(section_id = null) {
set(_selectedQuestionIds, []); // Clear the selected questions when changing sections
set(_activeSectionId, section_id);
}

Expand Down Expand Up @@ -306,11 +293,10 @@ export default function useQuizCreation() {

/**
* @returns {Promise<Quiz>}
* @throws {Error} if quiz is not valid
*/
function saveQuiz() {
if (!validateQuiz(get(_quiz))) {
throw new Error(`Quiz is not valid: ${JSON.stringify(get(_quiz))}`);
return Promise.reject(`Quiz is not valid: ${JSON.stringify(get(_quiz))}`);
}

const id = get(_quiz).id;
Expand Down Expand Up @@ -391,7 +377,7 @@ export default function useQuizCreation() {
} else {
set(
_selectedQuestionIds,
get(activeQuestions).map(q => q.id)
get(activeQuestions).map(q => q.item)
);
}
}
Expand Down Expand Up @@ -428,6 +414,9 @@ export default function useQuizCreation() {
const activeSection = computed(() =>
get(allSections).find(s => s.section_id === get(_activeSectionId))
);
const activeSectionIndex = computed(() =>
get(allSections).findIndex(s => isEqual(s.section_title === get(activeSection).section_title))
);
/** @type {ComputedRef<QuizSection[]>} The inactive sections */
const inactiveSections = computed(() =>
get(allSections).filter(s => s.section_id !== get(_activeSectionId))
Expand All @@ -445,15 +434,11 @@ export default function useQuizCreation() {
* exercises */
const activeQuestionsPool = computed(() => {
const pool = get(activeResourcePool);
const numQuestions = pool.reduce(
(count, r) => count + r.assessmentmetadata.assessment_item_ids.length,
0
);
const exerciseIds = pool.map(r => r.exercise_id);
const exerciseTitles = pool.map(r => r.title);
const questionIdArrays = pool.map(r => r.unique_question_ids);
return selectQuestions(
numQuestions,
pool.reduce((acc, r) => acc + r.assessmentmetadata.assessment_item_ids.length, 0),
exerciseIds,
exerciseTitles,
questionIdArrays,
Expand All @@ -468,12 +453,20 @@ export default function useQuizCreation() {
/** @type {ComputedRef<QuizQuestion[]>} Questions in the active section's `resource_pool` that
* are not in `questions` */
const replacementQuestionPool = computed(() => {
const activeQuestionIds = get(activeQuestions).map(q => q.id);
return get(activeQuestionsPool).filter(q => !activeQuestionIds.includes(q.id));
const excludedQuestions = get(allQuestionsInQuiz).map(q => q.item);
return get(activeQuestionsPool).filter(q => !excludedQuestions.includes(q.item));
});
/** @type {ComputedRef<Array>} A list of all channels available which have exercises */
const channels = computed(() => get(_channels));

/** @type {ComputedRef<Array<QuizQuestion>>} A list of all questions in the quiz */
const allQuestionsInQuiz = computed(() => {
return get(allSections).reduce((acc, section) => {
acc = [...acc, ...section.questions];
return acc;
}, []);
});

/** Handling the Select All Checkbox
* See: remove/toggleQuestionFromSelection() & selectAllQuestions() for more */

Expand All @@ -484,7 +477,7 @@ export default function useQuizCreation() {
isEqual(
get(selectedActiveQuestions).sort(),
get(activeQuestions)
.map(q => q.id)
.map(q => q.item)
.sort()
)
);
Expand All @@ -500,7 +493,7 @@ export default function useQuizCreation() {
function deleteActiveSelectedQuestions() {
const { section_id, questions: section_questions } = get(activeSection);
const selectedIds = get(selectedActiveQuestions);
const questions = section_questions.filter(q => !selectedIds.includes(q.id));
const questions = section_questions.filter(q => !selectedIds.includes(q.item));
const question_count = questions.length;
updateSection({
section_id,
Expand Down Expand Up @@ -528,6 +521,7 @@ export default function useQuizCreation() {
return !get(allQuestionsSelected) && !get(noQuestionsSelected);
});

provide('allQuestionsInQuiz', allQuestionsInQuiz);
provide('updateSection', updateSection);
provide('handleReplacement', handleReplacement);
provide('replaceSelectedQuestions', replaceSelectedQuestions);
Expand All @@ -542,6 +536,7 @@ export default function useQuizCreation() {
provide('replacements', replacements);
provide('allSections', allSections);
provide('activeSection', activeSection);
provide('activeSectionIndex', activeSectionIndex);
provide('inactiveSections', inactiveSections);
provide('activeResourcePool', activeResourcePool);
provide('activeResourceMap', activeResourceMap);
Expand Down Expand Up @@ -569,13 +564,13 @@ export default function useQuizCreation() {
clearSelectedQuestions,
addQuestionToSelection,
removeQuestionFromSelection,
displaySectionTitle,

// Computed
channels,
replacements,
quiz,
allSections,
activeSectionIndex,
activeSection,
inactiveSections,
activeResourcePool,
Expand All @@ -589,10 +584,12 @@ export default function useQuizCreation() {
allSectionsEmpty,
allQuestionsSelected,
noQuestionsSelected,
allQuestionsInQuiz,
};
}

export function injectQuizCreation() {
const allQuestionsInQuiz = inject('allQuestionsInQuiz');
const updateSection = inject('updateSection');
const handleReplacement = inject('handleReplacement');
const replaceSelectedQuestions = inject('replaceSelectedQuestions');
Expand All @@ -607,6 +604,7 @@ export function injectQuizCreation() {
const replacements = inject('replacements');
const allSections = inject('allSections');
const activeSection = inject('activeSection');
const activeSectionIndex = inject('activeSectionIndex');
const inactiveSections = inject('inactiveSections');
const activeResourcePool = inject('activeResourcePool');
const activeResourceMap = inject('activeResourceMap');
Expand Down Expand Up @@ -635,15 +633,16 @@ export function injectQuizCreation() {
addQuestionToSelection,
removeQuestionFromSelection,
toggleQuestionInSelection,
displaySectionTitle,

// Computed
allQuestionsSelected,
allQuestionsInQuiz,
selectAllIsIndeterminate,
channels,
replacements,
allSections,
activeSection,
activeSectionIndex,
inactiveSections,
activeResourcePool,
activeResourceMap,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,9 @@ export default {
return function finder({ title, excludeId }) {
return find(getters.exams, exam => {
// Coerce ids to same data type before comparing
String(exam.id) !== String(excludeId) && normalize(exam.title) === normalize(title);
return (
String(exam.id) !== String(excludeId) && normalize(exam.title) === normalize(title)
);
});
};
},
Expand Down
1 change: 1 addition & 0 deletions kolibri/plugins/coach/assets/src/utils/selectQuestions.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export default function selectQuestions(
question_id: uId.split(':')[1],
// TODO See #12127 re: replacing all `id` with `item`
id: uId,
item: uId,
title: exerciseTitles[ri],
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
:exerciseContentNodes="exerciseContentNodes"
:navigateTo="navigateTo"
:questions="questions"
:sections="exam.question_sources"
/>
</KPageContainer>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,11 @@ const coachStrings = createTranslator('CommonCoachStrings', {
context:
"Text shown on a modal pop-up window when the user clicks the 'Start Quiz' button. This explains what will happen when the user confirms the action of starting the quiz.",
},
canNoLongerEditQuizNotice: {
message: 'You will no longer be able to edit the questions and sections of the quiz.',
context:
'In the modal pop-up window when the user clicks the "Start Quiz" button, explains that they will not be able to edit the quiz after starting it.',
},
openQuizModalEmptySections: {
message: 'Any sections without questions will be removed from the quiz.',
context:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@
@submit="handleOpenQuiz(activeQuiz.id)"
>
<p>{{ openQuizModalDetail$() }}</p>
<p v-if="activeQuiz.draft">
{{ canNoLongerEditQuizNotice$() }}
</p>
<p
v-if="
activeQuiz.data_model_version === 3 &&
Expand Down Expand Up @@ -240,6 +243,7 @@
newQuizAction$,
filterQuizStatus$,
quizClosedLabel$,
canNoLongerEditQuizNotice$,
} = coachStrings;
const statusSelected = ref({
Expand Down Expand Up @@ -275,6 +279,7 @@
titleLabel$,
recipientsLabel$,
sizeLabel$,
canNoLongerEditQuizNotice$,
statusLabel$,
newQuizAction$,
filterQuizStatus$,
Expand Down
Loading

0 comments on commit c25cdc8

Please sign in to comment.