Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement new questions replacement #13180

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open
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
1 change: 0 additions & 1 deletion kolibri/plugins/coach/assets/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ class CoachToolsModule extends KolibriApp {
const skipLoading = [
PageNames.EXAM_CREATION_ROOT,
PageNames.QUIZ_SECTION_EDITOR,
PageNames.QUIZ_REPLACE_QUESTIONS,
PageNames.QUIZ_SELECT_PRACTICE_QUIZ,
PageNames.QUIZ_SELECT_RESOURCES,
PageNames.QUIZ_SELECT_RESOURCES_INDEX,
Expand Down
101 changes: 53 additions & 48 deletions kolibri/plugins/coach/assets/src/composables/useQuizCreation.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { get, set } from '@vueuse/core';
import { computed, ref, provide, inject, getCurrentInstance, watch } from 'vue';
import { fetchExamWithContent } from 'kolibri-common/quizzes/utils';
// TODO: Probably move this to this file's local dir
import selectQuestions, { getExerciseQuestionsMap } from '../utils/selectQuestions.js';
import selectQuestions from '../utils/selectQuestions.js';
import { Quiz, QuizSection, QuizQuestion } from './quizCreationSpecs.js';

/** Validators **/
Expand Down Expand Up @@ -60,6 +60,17 @@ export default function useQuizCreation() {
// and have them available for quick access in active section resource pools and the like.
const _exerciseMap = {};

/**
* Question item to be replaced in the next save operation
*/
const _questionItemsToReplace = ref(null);

function setQuestionItemsToReplace(item) {
set(_questionItemsToReplace, item);
}

const questionItemsToReplace = computed(() => get(_questionItemsToReplace));

// ------------------
// Section Management
// ------------------
Expand Down Expand Up @@ -143,14 +154,37 @@ export default function useQuizCreation() {
updateSection({ sectionIndex, questions, resourcePool });
}

/**
* Replace `questionItemsToReplace` questions in the `baseQuestions` array with the
* `replacements` questions
* @param {Array<Question>} baseQuestions base questions array
* @param {Array<string>} questionItemsToReplace question items to replace
* @param {Array<Question>} replacements array of questions to replace the question items
* @returns
*/
function _replaceQuestions(baseQuestions, questionItemsToReplace, replacements) {
if (questionItemsToReplace.length !== replacements.length) {
throw new TypeError(
'The number of question items to replace must match the number of replacements',
);
}
const newQuestions = baseQuestions.map(question => {
if (questionItemsToReplace.includes(question.item)) {
return replacements.shift();
}
return question;
});
return newQuestions;
}

/**
* Add an array of questions to a section
* @param {Object} options
* @param {number} options.sectionIndex - The index of the section to add the questions to
* @param {QuizQuestion[]} options.questions - The questions array to add
* @param {QuizExercise[]} options.resources - The resources to add to the exercise map
*/
function addQuestionsToSection({ sectionIndex, questions, resources }) {
function addQuestionsToSection({ sectionIndex, questions, resources, questionItemsToReplace }) {
const targetSection = get(allSections)[sectionIndex];
if (!targetSection) {
throw new TypeError(`Section with id ${sectionIndex} not found; cannot be updated.`);
Expand All @@ -164,36 +198,20 @@ export default function useQuizCreation() {
q => !targetSection.questions.map(q => q.item).includes(q.item),
);

const questionsToAdd = [...targetSection.questions, ...newQuestions];
let questionsToAdd;
if (questionItemsToReplace?.length) {
questionsToAdd = _replaceQuestions(
targetSection.questions,
questionItemsToReplace,
newQuestions,
);
} else {
questionsToAdd = [...targetSection.questions, ...newQuestions];
}

updateSection({ sectionIndex, questions: questionsToAdd, resourcePool: resources });
}

function handleReplacement(replacements) {
const questions = activeQuestions.value.map(question => {
if (selectedActiveQuestions.value.includes(question.item)) {
return replacements.shift();
}
return question;
});
updateSection({
sectionIndex: get(activeSectionIndex),
questions,
});
}

/**
* @param {QuizQuestion[]} newQuestions
* @affects _quiz - Updates the active section's `questions` property
* @affects _selectedQuestionIds - Clears this back to an empty array
* @throws {TypeError} if newQuestions is not a valid array of QuizQuestions
* Updates the active section's `questions` property with the given newQuestions, and clears
* _selectedQuestionIds from it. Then it resets _selectedQuestionIds to an empty array */
// TODO WRITE THIS FUNCTION
function replaceSelectedQuestions(newQuestions) {
return newQuestions;
}

/** @returns {QuizSection}
* Adds a section to the quiz and returns it */
function addSection() {
Expand Down Expand Up @@ -377,13 +395,6 @@ export default function useQuizCreation() {
/** @type {ComputedRef<String[]>}
* All QuizQuestion.items the user selected for the active section */
const selectedActiveQuestions = computed(() => get(_selectedQuestionIds));
/** @type {ComputedRef<QuizQuestion[]>} Questions in the active section's exercises that
* are not in `questions` */
const replacementQuestionPool = computed(() => {
const excludedQuestions = get(allQuestionsInQuiz).map(q => q.item);
const questionsMap = getExerciseQuestionsMap(get(activeResourcePool), excludedQuestions);
return Object.values(questionsMap).reduce((acc, questions) => [...acc, ...questions], []);
});

/** @type {ComputedRef<Array<QuizQuestion>>} A list of all questions in the quiz */
const allQuestionsInQuiz = computed(() => {
Expand Down Expand Up @@ -429,8 +440,6 @@ export default function useQuizCreation() {
provide('updateSection', updateSection);
provide('addQuestionsToSection', addQuestionsToSection);
provide('addQuestionsToSectionFromResources', addQuestionsToSectionFromResources);
provide('handleReplacement', handleReplacement);
provide('replaceSelectedQuestions', replaceSelectedQuestions);
provide('addSection', addSection);
provide('removeSection', removeSection);
provide('updateQuiz', updateQuiz);
Expand All @@ -447,23 +456,22 @@ export default function useQuizCreation() {
provide('allResourceMap', allResourceMap);
provide('activeQuestions', activeQuestions);
provide('selectedActiveQuestions', selectedActiveQuestions);
provide('replacementQuestionPool', replacementQuestionPool);
provide('deleteActiveSelectedQuestions', deleteActiveSelectedQuestions);
provide('questionItemsToReplace', questionItemsToReplace);
provide('setQuestionItemsToReplace', setQuestionItemsToReplace);

return {
// Methods
saveQuiz,
updateSection,
addQuestionsToSectionFromResources,
handleReplacement,
replaceSelectedQuestions,
addSection,
removeSection,
initializeQuiz,
updateQuiz,
clearSelectedQuestions,
addQuestionsToSelection,
removeQuestionsFromSelection,
setQuestionItemsToReplace,

// Computed
quizHasChanged,
Expand All @@ -476,7 +484,6 @@ export default function useQuizCreation() {
activeResourceMap,
activeQuestions,
selectedActiveQuestions,
replacementQuestionPool,
selectAllLabel,
allSectionsEmpty,
noQuestionsSelected,
Expand All @@ -489,8 +496,6 @@ export function injectQuizCreation() {
const updateSection = inject('updateSection');
const addQuestionsToSection = inject('addQuestionsToSection');
const addQuestionsToSectionFromResources = inject('addQuestionsToSectionFromResources');
const handleReplacement = inject('handleReplacement');
const replaceSelectedQuestions = inject('replaceSelectedQuestions');
const addSection = inject('addSection');
const removeSection = inject('removeSection');
const updateQuiz = inject('updateQuiz');
Expand All @@ -506,23 +511,23 @@ export function injectQuizCreation() {
const allResourceMap = inject('allResourceMap');
const activeQuestions = inject('activeQuestions');
const selectedActiveQuestions = inject('selectedActiveQuestions');
const replacementQuestionPool = inject('replacementQuestionPool');
const deleteActiveSelectedQuestions = inject('deleteActiveSelectedQuestions');
const questionItemsToReplace = inject('questionItemsToReplace');
const setQuestionItemsToReplace = inject('setQuestionItemsToReplace');

return {
// Methods
deleteActiveSelectedQuestions,
updateSection,
addQuestionsToSection,
addQuestionsToSectionFromResources,
handleReplacement,
replaceSelectedQuestions,
addSection,
removeSection,
updateQuiz,
clearSelectedQuestions,
addQuestionsToSelection,
removeQuestionsFromSelection,
setQuestionItemsToReplace,

// Computed
allQuestionsInQuiz,
Expand All @@ -535,6 +540,6 @@ export function injectQuizCreation() {
allResourceMap,
activeQuestions,
selectedActiveQuestions,
replacementQuestionPool,
questionItemsToReplace,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,13 @@ export default function useResourceSelection({
topic.value = newTopic;
}
if (topicTree?.annotator) {
const annotatedResults = await topicTree.annotator(newTopic.children.results);
const annotatedResults = await topicTree.annotator(newTopic.children?.results || []);
return {
...newTopic.children,
results: annotatedResults,
};
}
return newTopic.children;
return newTopic.children || { results: [] };
};

const treeFetch = useFetch({
Expand Down
1 change: 0 additions & 1 deletion kolibri/plugins/coach/assets/src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export const PageNames = {
QUIZ_LEARNER_PAGE_ROOT: 'QUIZ_LEARNER_PAGE_ROOT',
QUIZ_LEARNER_REPORT: 'QUIZ_LEARNER_REPORT',
QUIZ_SECTION_EDITOR: 'QUIZ_SECTION_EDITOR',
QUIZ_REPLACE_QUESTIONS: 'QUIZ_REPLACE_QUESTIONS',
QUIZ_SELECT_RESOURCES_OLD: 'QUIZ_SELECT_RESOURCES_OLD',
QUIZ_SELECT_PRACTICE_QUIZ: 'QUIZ_SELECT_PRACTICE_QUIZ',
QUIZ_SECTION_ORDER: 'QUIZ_SECTION_ORDER',
Expand Down
6 changes: 0 additions & 6 deletions kolibri/plugins/coach/assets/src/routes/examRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import store from 'kolibri/store';
import { PageNames } from '../constants';
import CreateExamPage from '../views/quizzes/CreateExamPage';
import SectionEditor from '../views/quizzes/CreateExamPage/sidePanels/SectionSidePanel/SectionEditor.vue';
import ReplaceQuestions from '../views/quizzes/CreateExamPage/sidePanels/SectionSidePanel/ReplaceQuestions.vue';
import ExamsRootPage from '../views/quizzes/ExamsRootPage';
import QuizSummaryPage from '../views/quizzes/QuizSummaryPage';
import SectionOrder from '../views/quizzes/CreateExamPage/sidePanels/SectionSidePanel/SectionOrder.vue';
Expand Down Expand Up @@ -71,11 +70,6 @@ export default [
path: 'edit',
component: SectionEditor,
},
{
name: PageNames.QUIZ_REPLACE_QUESTIONS,
path: 'replace-questions',
component: ReplaceQuestions,
},
{
name: PageNames.QUIZ_SECTION_ORDER,
path: 'section-order',
Expand Down
2 changes: 1 addition & 1 deletion kolibri/plugins/coach/assets/src/utils/selectQuestions.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function exerciseToQuestionArray(exercise) {
});
}

export function getExerciseQuestionsMap(exercises, excludedQuestionIds = []) {
function getExerciseQuestionsMap(exercises, excludedQuestionIds = []) {
const excludedQuestionIdMap = {};
for (const uId of excludedQuestionIds) {
excludedQuestionIdMap[uId] = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
>
<AccordionItem
:title="displayQuestionTitle(question, getQuestionContent(question).title)"
:disabledTitle="questionItemsToReplace?.includes(question.item)"
:aria-selected="questionIsChecked(question)"
:headerAppearanceOverrides="{
userSelect: dragActive ? 'none !important' : 'text',
Expand Down Expand Up @@ -86,6 +87,15 @@
"
/>
</template>
<template #trailing-actions>
<span v-if="questionItemsToReplace?.includes(question.item)">
{{ replacingThisQuestionLabel$() }}
</span>
<slot
name="question-trailing-actions"
:question="question"
></slot>
</template>
<template #content>
<div
:id="`question-panel-${question.item}`"
Expand Down Expand Up @@ -152,12 +162,17 @@
const dragActive = ref(false);

const { upLabel$, downLabel$ } = searchAndFilterStrings;
const { selectAllLabel$, expandAll$, collapseAll$ } = enhancedQuizManagementStrings;
const { selectAllLabel$, expandAll$, collapseAll$, replacingThisQuestionLabel$ } =
enhancedQuizManagementStrings;

const { moveUpOne, moveDownOne } = useDrag();

function questionCheckboxDisabled(question) {
if (props.disabled || props.unselectableQuestionItems?.includes(question.item)) {
if (
props.disabled ||
props.unselectableQuestionItems?.includes(question.item) ||
props.questionItemsToReplace?.includes(question.item)
) {
return true;
}
if (
Expand All @@ -170,6 +185,9 @@
}

function questionIsChecked(question) {
if (props.questionItemsToReplace?.includes(question.item)) {
return false;
}
if (props.unselectableQuestionItems?.includes(question.item)) {
return true;
}
Expand Down Expand Up @@ -238,6 +256,7 @@
selectAllLabel$,
expandAll$,
collapseAll$,
replacingThisQuestionLabel$,
};
},
props: {
Expand Down Expand Up @@ -283,6 +302,15 @@
required: false,
default: null,
},
/**
* If provided, the question with this item will appear as disabled
* and with a `Replacing this question` message.
*/
questionItemsToReplace: {
type: Array,
required: false,
default: null,
},
},
computed: {
isSortable() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<div class="quiz-header-actions">
<KButton
appearance="flat-button"
:disabled="settings.isInReplaceMode"
:text="settingsLabel$()"
@click="onSettingsClick"
>
Expand Down Expand Up @@ -58,8 +59,12 @@
},
computed: {
quizTitle() {
const { selectUpToNResources$, selectUpToNQuestions$ } = enhancedQuizManagementStrings;
const { selectNQuestions$, selectUpToNResources$, selectUpToNQuestions$ } =
enhancedQuizManagementStrings;

if (this.settings.isInReplaceMode) {
return selectNQuestions$({ count: this.settings.questionCount });
}
if (this.settings.isChoosingManually) {
return selectUpToNQuestions$({ count: this.settings.questionCount });
}
Expand Down
Loading
Loading