Skip to content

Commit

Permalink
Merge pull request #13091 from AlexVelezLl/manual-selection-workflow
Browse files Browse the repository at this point in the history
Implement manual questions selection workflow
  • Loading branch information
AlexVelezLl authored Feb 24, 2025
2 parents 64a2ab4 + 67c471f commit f6fceb2
Show file tree
Hide file tree
Showing 21 changed files with 719 additions and 384 deletions.
2 changes: 1 addition & 1 deletion kolibri/plugins/coach/assets/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class CoachToolsModule extends KolibriApp {
PageNames.QUIZ_SELECT_RESOURCES_LANDING_SETTINGS,
PageNames.QUIZ_SECTION_ORDER,
PageNames.QUIZ_BOOK_MARKED_RESOURCES,
PageNames.QUIZ_PREVIEW_SELECTED_QUESTIONS,
PageNames.QUIZ_LEARNER_REPORT,
PageNames.LESSON_SUMMARY,
PageNames.LESSON_SUMMARY_BETTER,
Expand All @@ -86,7 +87,6 @@ class CoachToolsModule extends KolibriApp {
PageNames.LESSON_SELECT_RESOURCES_SEARCH_RESULTS,
PageNames.LESSON_SELECT_RESOURCES_BOOKMARKS,
PageNames.LESSON_SELECT_RESOURCES_TOPIC_TREE,
PageNames.LESSON_PREVIEW_SELECTED_QUESTIONS,
];
// If we're navigating to the same page for a quiz summary page, don't set loading
if (
Expand Down
112 changes: 43 additions & 69 deletions kolibri/plugins/coach/assets/src/composables/useQuizCreation.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import isEqual from 'lodash/isEqual';
import { enhancedQuizManagementStrings } from 'kolibri-common/strings/enhancedQuizManagementStrings';
import uniq from 'lodash/uniq';
import { MAX_QUESTIONS_PER_QUIZ_SECTION } from 'kolibri/constants';
Expand Down Expand Up @@ -143,6 +142,32 @@ export default function useQuizCreation() {
updateSection({ sectionIndex, questions, resourcePool });
}

/**
* 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 }) {
const targetSection = get(allSections)[sectionIndex];
if (!targetSection) {
throw new TypeError(`Section with id ${sectionIndex} not found; cannot be updated.`);
}

if (!questions || questions.length === 0) {
throw new TypeError('Questions must be a non-empty array of questions');
}

const newQuestions = questions.filter(
q => !targetSection.questions.map(q => q.item).includes(q.item),
);

const 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)) {
Expand Down Expand Up @@ -286,46 +311,27 @@ export default function useQuizCreation() {
// Questions / Exercises management
// --------------------------------

/** @param {QuizQuestion} question
/** @param {QuizQuestion[]} questions
* @affects _selectedQuestionIds - Adds question to _selectedQuestionIds if it isn't
* there already */
function addQuestionToSelection(id) {
set(_selectedQuestionIds, uniq([...get(_selectedQuestionIds), id]));
function addQuestionsToSelection(ids) {
set(_selectedQuestionIds, uniq([...get(_selectedQuestionIds), ...ids]));
}

/**
* @param {QuizQuestion} question
* @param {QuizQuestion[]} questions
* @affects _selectedQuestionIds - Removes question from _selectedQuestionIds if it is there */
function removeQuestionFromSelection(id) {
function removeQuestionsFromSelection(ids) {
set(
_selectedQuestionIds,
get(_selectedQuestionIds).filter(_id => id !== _id),
get(_selectedQuestionIds).filter(_id => !ids.includes(_id)),
);
}

function toggleQuestionInSelection(id) {
if (get(_selectedQuestionIds).includes(id)) {
removeQuestionFromSelection(id);
} else {
addQuestionToSelection(id);
}
}

function clearSelectedQuestions() {
set(_selectedQuestionIds, []);
}

function selectAllQuestions() {
if (get(allQuestionsSelected)) {
clearSelectedQuestions();
} else {
set(
_selectedQuestionIds,
get(activeQuestions).map(q => q.item),
);
}
}

// Utilities

// Computed properties
Expand Down Expand Up @@ -386,22 +392,6 @@ export default function useQuizCreation() {
}, []);
});

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

/** @type {ComputedRef<Boolean>} Whether all active questions are selected */
const allQuestionsSelected = computed(() => {
return Boolean(
get(selectedActiveQuestions).length &&
isEqual(
get(selectedActiveQuestions).sort(),
get(activeQuestions)
.map(q => q.item)
.sort(),
),
);
});

const allSectionsEmpty = computed(() => {
return get(allSections).every(section => section.questions.length === 0);
});
Expand Down Expand Up @@ -434,21 +424,17 @@ export default function useQuizCreation() {
}
});

/** @type {ComputedRef<Boolean>} Whether the select all checkbox should be indeterminate */
const selectAllIsIndeterminate = computed(() => {
return !get(allQuestionsSelected) && !get(noQuestionsSelected);
});

provide('allQuestionsInQuiz', allQuestionsInQuiz);
provide('updateSection', updateSection);
provide('addQuestionsToSection', addQuestionsToSection);
provide('addQuestionsToSectionFromResources', addQuestionsToSectionFromResources);
provide('handleReplacement', handleReplacement);
provide('replaceSelectedQuestions', replaceSelectedQuestions);
provide('addSection', addSection);
provide('removeSection', removeSection);
provide('updateQuiz', updateQuiz);
provide('addQuestionToSelection', addQuestionToSelection);
provide('removeQuestionFromSelection', removeQuestionFromSelection);
provide('addQuestionsToSelection', addQuestionsToSelection);
provide('removeQuestionsFromSelection', removeQuestionsFromSelection);
provide('clearSelectedQuestions', clearSelectedQuestions);
provide('allSections', allSections);
provide('activeSectionIndex', activeSectionIndex);
Expand All @@ -460,12 +446,8 @@ export default function useQuizCreation() {
provide('allResourceMap', allResourceMap);
provide('activeQuestions', activeQuestions);
provide('selectedActiveQuestions', selectedActiveQuestions);
provide('allQuestionsSelected', allQuestionsSelected);
provide('selectAllIsIndeterminate', selectAllIsIndeterminate);
provide('replacementQuestionPool', replacementQuestionPool);
provide('selectAllQuestions', selectAllQuestions);
provide('deleteActiveSelectedQuestions', deleteActiveSelectedQuestions);
provide('toggleQuestionInSelection', toggleQuestionInSelection);

return {
// Methods
Expand All @@ -479,8 +461,8 @@ export default function useQuizCreation() {
initializeQuiz,
updateQuiz,
clearSelectedQuestions,
addQuestionToSelection,
removeQuestionFromSelection,
addQuestionsToSelection,
removeQuestionsFromSelection,

// Computed
quizHasChanged,
Expand All @@ -494,10 +476,8 @@ export default function useQuizCreation() {
activeQuestions,
selectedActiveQuestions,
replacementQuestionPool,
selectAllIsIndeterminate,
selectAllLabel,
allSectionsEmpty,
allQuestionsSelected,
noQuestionsSelected,
allQuestionsInQuiz,
};
Expand All @@ -506,14 +486,15 @@ export default function useQuizCreation() {
export function injectQuizCreation() {
const allQuestionsInQuiz = inject('allQuestionsInQuiz');
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');
const addQuestionToSelection = inject('addQuestionToSelection');
const removeQuestionFromSelection = inject('removeQuestionFromSelection');
const addQuestionsToSelection = inject('addQuestionsToSelection');
const removeQuestionsFromSelection = inject('removeQuestionsFromSelection');
const clearSelectedQuestions = inject('clearSelectedQuestions');
const allSections = inject('allSections');
const activeSectionIndex = inject('activeSectionIndex');
Expand All @@ -523,34 +504,27 @@ export function injectQuizCreation() {
const activeResourceMap = inject('activeResourceMap');
const allResourceMap = inject('allResourceMap');
const activeQuestions = inject('activeQuestions');
const allQuestionsSelected = inject('allQuestionsSelected');
const selectAllIsIndeterminate = inject('selectAllIsIndeterminate');
const selectedActiveQuestions = inject('selectedActiveQuestions');
const replacementQuestionPool = inject('replacementQuestionPool');
const selectAllQuestions = inject('selectAllQuestions');
const deleteActiveSelectedQuestions = inject('deleteActiveSelectedQuestions');
const toggleQuestionInSelection = inject('toggleQuestionInSelection');

return {
// Methods
deleteActiveSelectedQuestions,
selectAllQuestions,
updateSection,
addQuestionsToSection,
addQuestionsToSectionFromResources,
handleReplacement,
replaceSelectedQuestions,
addSection,
removeSection,
updateQuiz,
clearSelectedQuestions,
addQuestionToSelection,
removeQuestionFromSelection,
toggleQuestionInSelection,
addQuestionsToSelection,
removeQuestionsFromSelection,

// Computed
allQuestionsSelected,
allQuestionsInQuiz,
selectAllIsIndeterminate,
allSections,
activeSectionIndex,
activeSection,
Expand Down
2 changes: 1 addition & 1 deletion kolibri/plugins/coach/assets/src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const PageNames = {
QUIZ_SELECT_RESOURCES: 'QUIZ_SELECT_RESOURCES',
QUIZ_PREVIEW_RESOURCE: 'QUIZ_PREVIEW_RESOURCE',
QUIZ_PREVIEW_SELECTED_RESOURCES: 'QUIZ_PREVIEW_SELECTED_RESOURCES',
QUIZ_PREVIEW_SELECTED_QUESTIONS: 'QUIZ_PREVIEW_SELECTED_QUESTIONS',
QUIZ_SELECT_RESOURCES_INDEX: 'QUIZ_SELECT_RESOURCES_INDEX',
QUIZ_SELECT_RESOURCES_SEARCH: 'QUIZ_SELECT_RESOURCES_SEARCH',
QUIZ_SELECT_RESOURCES_BOOKMARKS: 'QUIZ_SELECT_RESOURCES_BOOKMARKS',
Expand Down Expand Up @@ -52,7 +53,6 @@ export const PageNames = {
LESSON_SELECT_RESOURCES_TOPIC_TREE: 'LESSON_SELECT_RESOURCES_TOPIC_TREE',
LESSON_SELECT_RESOURCES_SEARCH_RESULTS: 'LESSON_SELECT_RESOURCES_SEARCH_RESULTS',
LESSON_PREVIEW_SELECTED_RESOURCES: 'LESSON_PREVIEW_SELECTED_RESOURCES',
LESSON_PREVIEW_SELECTED_QUESTIONS: 'LESSON_PREVIEW_SELECTED_QUESTIONS',
LESSON_PREVIEW_RESOURCE: 'LESSON_PREVIEW_RESOURCE',
LESSON_LEARNER_REPORT: 'LESSON_LEARNER_REPORT',
LESSON_RESOURCE_LEARNERS_REPORT: 'LESSON_RESOURCE_LEARNERS_REPORT',
Expand Down
6 changes: 6 additions & 0 deletions kolibri/plugins/coach/assets/src/routes/examRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import SelectionIndex from '../views/common/resourceSelection/subPages/Selection
import SelectFromChannels from '../views/common/resourceSelection/subPages/SelectFromTopicTree.vue';
import SelectFromBookmarks from '../views/common/resourceSelection/subPages/SelectFromBookmarks.vue';
import ManageSelectedResources from '../views/common/resourceSelection/subPages/ManageSelectedResources.vue';
import ManageSelectedQuestions from '../views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/ManageSelectedQuestions.vue';
import PreviewSelectedResources from '../views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue';
import {
generateQuestionDetailHandler,
Expand Down Expand Up @@ -114,6 +115,11 @@ export default [
path: 'preview-resources',
component: ManageSelectedResources,
},
{
name: PageNames.QUIZ_PREVIEW_SELECTED_QUESTIONS,
path: 'preview-questions',
component: ManageSelectedQuestions,
},
{
name: PageNames.QUIZ_SELECT_RESOURCES_SETTINGS,
path: 'settings',
Expand Down
6 changes: 0 additions & 6 deletions kolibri/plugins/coach/assets/src/routes/lessonsRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ import PreviewSelectedResources from '../views/common/resourceSelection/subPages
import LessonResourceSelection from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/index.vue';
import SearchFilters from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SearchFilters.vue';
import SelectFromSearchResults from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromSearchResults.vue';
import ManageSelectedQuestions from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/ManageSelectedQuestions.vue';
import { classIdParamRequiredGuard, RouteSegments } from './utils';

const {
Expand Down Expand Up @@ -184,11 +183,6 @@ export default [
};
},
},
{
name: PageNames.LESSON_PREVIEW_SELECTED_QUESTIONS,
path: 'preview-questions',
component: ManageSelectedQuestions,
},
],
},
],
Expand Down
Loading

0 comments on commit f6fceb2

Please sign in to comment.