From e27b2a904a9a81e2ffee5fc3298fde9fd335533b Mon Sep 17 00:00:00 2001 From: ozer550 Date: Thu, 13 Feb 2025 13:24:08 +0530 Subject: [PATCH 1/8] implement workingQuestions --- .../PreviewExercise.vue | 28 +++++++++++++++- .../PreviewSelectedResources/index.vue | 31 +++++++++++++++++ .../QuizResourceSelection/index.vue | 33 +++++++++++++++++-- 3 files changed, 89 insertions(+), 3 deletions(-) diff --git a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/PreviewExercise.vue b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/PreviewExercise.vue index 2b288fa5de..0f46467dea 100644 --- a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/PreviewExercise.vue +++ b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/PreviewExercise.vue @@ -4,7 +4,12 @@ @@ -30,6 +35,27 @@ type: Array, required: true, }, + settings: { + type: Object, + required: false, + default: null, + }, + selectedQuestions: { + type: Array, + required: false, + default: () => [], + }, + }, + computed: { + selectAllIsChecked() { + return this.questions.every(question => this.selectedQuestions.includes(question.item)); + }, + selectAllIsIndeterminate() { + return ( + this.questions.some(question => this.selectedQuestions.includes(question.item)) && + !this.selectAllIsChecked + ); + }, }, }; diff --git a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue index 60acc027a7..62b8899edc 100644 --- a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue +++ b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue @@ -56,6 +56,10 @@ v-if="isExercise" :contentNode="contentNode" :questions="exerciseQuestions" + :settings="settings" + :selectedQuestions="selectedQuestionItems" + @select="handleSelectQuestion" + @selectAll="handleSelectAllQuestions" /> [], + }, }, computed: { isSelected() { @@ -238,6 +250,9 @@ isExercise() { return this.contentNode.kind === ContentNodeKinds.EXERCISE; }, + selectedQuestionItems() { + return this.selectedQuestions.map(q => q.item); + }, }, beforeRouteEnter(to, from, next) { next(vm => { @@ -267,6 +282,22 @@ }, }; }, + handleSelectQuestion(questionItem, value) { + //Map the string of questionids to actual question object + const question = this.exerciseQuestions.find(q => q.item === questionItem); + if (value) { + this.$emit('selectQuestions', [question]); + } else { + this.$emit('deselectQuestions', [question]); + } + }, + handleSelectAllQuestions(value) { + if (value) { + this.$emit('selectQuestions', this.exerciseQuestions); + } else { + this.$emit('deselectQuestions', this.exerciseQuestions); + } + }, }, }; diff --git a/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue b/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue index ad5703781d..1532f0b3d5 100644 --- a/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue +++ b/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue @@ -43,6 +43,7 @@ :setContinueAction="value => (continueAction = value)" :sectionTitle="sectionTitle" :selectedResources="workingResourcePool" + :selectedQuestions="workingQuestions" :topic="topic" :treeFetch="treeFetch" :channelsFetch="channelsFetch" @@ -54,7 +55,9 @@ :contentCardMessage="contentCardMessage" :getResourceLink="getResourceLink" @selectResources="addToWorkingResourcePool" + @selectQuestions="addToWorkingQuestions" @deselectResources="removeFromWorkingResourcePool" + @deselectQuestions="removeFromWorkingQuestions" @setSelectedResources="setWorkingResourcePool" /> } + */ + const workingQuestions = ref([]); + /** * @param {QuizExercise[]} resources * @affects workingResourcePool -- Updates it with the given resources and is ensured to have @@ -233,6 +240,25 @@ workingResourcePool.value = resources; } + /** + * @param {QuizQuestions[]} questions + * @affects workingQuestions -- Updates it with the given questions and is ensured to have + * a list of unique questions to avoid unnecessary duplication + */ + function addToWorkingQuestions(questions) { + workingQuestions.value = uniqWith([...workingQuestions.value, ...questions], isEqual); + } + + /** + * @param {QuizQuestions[]} questions + * @affects workingQuestions -- Removes the given questions from the workingQuestions + */ + function removeFromWorkingQuestions(questions) { + workingQuestions.value = workingQuestions.value.filter( + obj => !questions.some(r => r.item === obj.item), + ); + } + const { annotateTopicsWithDescendantCounts } = useQuizResources(); const unusedQuestionsCount = useMemoize(content => { @@ -378,6 +404,8 @@ maximumContentSelectedWarning, addToWorkingResourcePool, removeFromWorkingResourcePool, + addToWorkingQuestions, + removeFromWorkingQuestions, setWorkingResourcePool, settings, disableSave, @@ -388,6 +416,7 @@ updateSection, addQuestionsToSectionFromResources, workingResourcePool, + workingQuestions, selectQuiz$, addNumberOfQuestions$, numberOfSelectedResources$, From bee66983eb864ed6bdeacf6101857953df400351 Mon Sep 17 00:00:00 2001 From: ozer550 Date: Fri, 14 Feb 2025 23:12:19 +0530 Subject: [PATCH 2/8] migrate shopping cart from lessons in quzzies --- kolibri/plugins/coach/assets/src/app.js | 2 +- .../coach/assets/src/constants/index.js | 2 +- .../coach/assets/src/routes/examRoutes.js | 6 + .../coach/assets/src/routes/lessonsRoutes.js | 6 - .../PreviewSelectedResources/index.vue | 2 +- .../subPages/ManageSelectedQuestions.vue | 159 ------------------ .../QuizResourceSelection/index.vue | 29 +++- .../subPages/ManageSelectedQuestions.vue | 134 +++++++++++++++ .../strings/searchAndFilterStrings.js | 5 + 9 files changed, 174 insertions(+), 171 deletions(-) delete mode 100644 kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/ManageSelectedQuestions.vue create mode 100644 kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/ManageSelectedQuestions.vue diff --git a/kolibri/plugins/coach/assets/src/app.js b/kolibri/plugins/coach/assets/src/app.js index b9516fa727..c963882e30 100644 --- a/kolibri/plugins/coach/assets/src/app.js +++ b/kolibri/plugins/coach/assets/src/app.js @@ -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, @@ -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 ( diff --git a/kolibri/plugins/coach/assets/src/constants/index.js b/kolibri/plugins/coach/assets/src/constants/index.js index 4cf7046ac7..189960c609 100644 --- a/kolibri/plugins/coach/assets/src/constants/index.js +++ b/kolibri/plugins/coach/assets/src/constants/index.js @@ -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', @@ -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', diff --git a/kolibri/plugins/coach/assets/src/routes/examRoutes.js b/kolibri/plugins/coach/assets/src/routes/examRoutes.js index 3ab7773bbd..82deb52a7d 100644 --- a/kolibri/plugins/coach/assets/src/routes/examRoutes.js +++ b/kolibri/plugins/coach/assets/src/routes/examRoutes.js @@ -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, @@ -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', diff --git a/kolibri/plugins/coach/assets/src/routes/lessonsRoutes.js b/kolibri/plugins/coach/assets/src/routes/lessonsRoutes.js index 48bd2a85de..7a43b3c159 100644 --- a/kolibri/plugins/coach/assets/src/routes/lessonsRoutes.js +++ b/kolibri/plugins/coach/assets/src/routes/lessonsRoutes.js @@ -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 { @@ -184,11 +183,6 @@ export default [ }; }, }, - { - name: PageNames.LESSON_PREVIEW_SELECTED_QUESTIONS, - path: 'preview-questions', - component: ManageSelectedQuestions, - }, ], }, ], diff --git a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue index 62b8899edc..45219ea88d 100644 --- a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue +++ b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue @@ -286,7 +286,7 @@ //Map the string of questionids to actual question object const question = this.exerciseQuestions.find(q => q.item === questionItem); if (value) { - this.$emit('selectQuestions', [question]); + this.$emit('selectQuestions', [question], this.contentNode); } else { this.$emit('deselectQuestions', [question]); } diff --git a/kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/ManageSelectedQuestions.vue b/kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/ManageSelectedQuestions.vue deleted file mode 100644 index 2afbc755bb..0000000000 --- a/kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/ManageSelectedQuestions.vue +++ /dev/null @@ -1,159 +0,0 @@ - - - - - - - diff --git a/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue b/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue index 1532f0b3d5..9806ae73a9 100644 --- a/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue +++ b/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue @@ -91,7 +91,8 @@ + + {{ + numberOfSelectedQuestions$({ + count: workingQuestions.length, + }) + }} +
r.id === resource.id)) { + addToWorkingResourcePool([resource]); + } } /** @@ -257,6 +275,10 @@ workingQuestions.value = workingQuestions.value.filter( obj => !questions.some(r => r.item === obj.item), ); + const resourcesToRemove = workingResourcePool.value.filter( + r => !workingQuestions.value.some(q => q.exercise_id === r.id), + ); + removeFromWorkingResourcePool(resourcesToRemove); } const { annotateTopicsWithDescendantCounts } = useQuizResources(); @@ -377,7 +399,7 @@ return maximumResourcesSelectedWarning$(); }); - const { numberOfSelectedResources$ } = searchAndFilterStrings; + const { numberOfSelectedResources$, numberOfSelectedQuestions$ } = searchAndFilterStrings; return { title, @@ -420,6 +442,7 @@ selectQuiz$, addNumberOfQuestions$, numberOfSelectedResources$, + numberOfSelectedQuestions$, }; }, beforeRouteLeave(_, __, next) { diff --git a/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/ManageSelectedQuestions.vue b/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/ManageSelectedQuestions.vue new file mode 100644 index 0000000000..4065ac1789 --- /dev/null +++ b/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/ManageSelectedQuestions.vue @@ -0,0 +1,134 @@ + + + + + + + diff --git a/packages/kolibri-common/strings/searchAndFilterStrings.js b/packages/kolibri-common/strings/searchAndFilterStrings.js index e60564dc36..75e3f40fe5 100644 --- a/packages/kolibri-common/strings/searchAndFilterStrings.js +++ b/packages/kolibri-common/strings/searchAndFilterStrings.js @@ -44,6 +44,11 @@ export const searchAndFilterStrings = createTranslator('SearchAndFilterStrings', '{count, number, integer} {count, plural, one {resource selected} other {resources selected}}', context: 'Indicates the number of resources selected', }, + numberOfSelectedQuestions: { + message: + '{count, number, integer} {count, plural, one {question selected} other {questions selected}}', + context: 'Indicates the number of questions selected', + }, openParentFolderLabel: { message: 'Open parent folder', context: 'Button label to open the parent folder of a resource', From d898f8bbba76ac34d334d46a71247e3b5e74c995 Mon Sep 17 00:00:00 2001 From: Alex Velez Date: Tue, 18 Feb 2025 06:41:39 -0500 Subject: [PATCH 3/8] Save manual selected questions --- .../assets/src/composables/useQuizCreation.js | 29 +++++++++++++++++++ .../QuizResourceSelection/index.vue | 8 +++++ 2 files changed, 37 insertions(+) diff --git a/kolibri/plugins/coach/assets/src/composables/useQuizCreation.js b/kolibri/plugins/coach/assets/src/composables/useQuizCreation.js index d28586e2c0..130a53d435 100644 --- a/kolibri/plugins/coach/assets/src/composables/useQuizCreation.js +++ b/kolibri/plugins/coach/assets/src/composables/useQuizCreation.js @@ -143,6 +143,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)) { @@ -441,6 +467,7 @@ export default function useQuizCreation() { provide('allQuestionsInQuiz', allQuestionsInQuiz); provide('updateSection', updateSection); + provide('addQuestionsToSection', addQuestionsToSection); provide('addQuestionsToSectionFromResources', addQuestionsToSectionFromResources); provide('handleReplacement', handleReplacement); provide('replaceSelectedQuestions', replaceSelectedQuestions); @@ -506,6 +533,7 @@ 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'); @@ -536,6 +564,7 @@ export function injectQuizCreation() { deleteActiveSelectedQuestions, selectAllQuestions, updateSection, + addQuestionsToSection, addQuestionsToSectionFromResources, handleReplacement, replaceSelectedQuestions, diff --git a/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue b/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue index 9806ae73a9..6119110365 100644 --- a/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue +++ b/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue @@ -174,6 +174,7 @@ activeSection, activeSectionIndex, updateSection, + addQuestionsToSection, addQuestionsToSectionFromResources, allQuestionsInQuiz, activeQuestions, @@ -436,6 +437,7 @@ tooManyQuestions$, questionsUnusedInSection$, updateSection, + addQuestionsToSection, addQuestionsToSectionFromResources, workingResourcePool, workingQuestions, @@ -474,6 +476,12 @@ }); sectionIndex++; } + } else if (this.settings.isChoosingManually) { + this.addQuestionsToSection({ + sectionIndex: this.activeSectionIndex, + questions: this.workingQuestions, + resources: this.workingResourcePool, + }); } else { this.addQuestionsToSectionFromResources({ sectionIndex: this.activeSectionIndex, From f0454bb9119c623a7dea8408c0ec2f325a065781 Mon Sep 17 00:00:00 2001 From: Alex Velez Date: Tue, 18 Feb 2025 07:15:38 -0500 Subject: [PATCH 4/8] Adapt quizzes to manual resource selection --- .../PreviewExercise.vue | 2 +- .../PreviewSelectedResources/index.vue | 14 +++++++++-- .../QuizResourceSelection/index.vue | 22 +++++++++++++---- .../subPages/ManageSelectedQuestions.vue | 24 ++++++++++++++++++- .../strings/searchAndFilterStrings.js | 4 ++++ 5 files changed, 57 insertions(+), 9 deletions(-) diff --git a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/PreviewExercise.vue b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/PreviewExercise.vue index 0f46467dea..6091c3f8a0 100644 --- a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/PreviewExercise.vue +++ b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/PreviewExercise.vue @@ -4,7 +4,7 @@
-
+

{{ coreString('selectFromChannels') }}

@@ -33,7 +36,11 @@ />
- + { + const workingPoolQuestionsCount = computed(() => { + if (settings.value.isChoosingManually) { + return workingQuestions.value.length; + } + return workingResourcePool.value.reduce((acc, content) => { return acc + unusedQuestionsCount(content); }, 0); }); const tooManyQuestions = computed(() => { + if (settings.value.isChoosingManually) { + return workingQuestions.value.length > settings.value.questionCount; + } + return workingResourcePool.value.length > settings.value.questionCount; }); @@ -366,7 +374,7 @@ } return ( !workingPoolHasChanged.value || - workingPoolUnusedQuestions.value < settings.value.questionCount || + workingPoolQuestionsCount.value < settings.value.questionCount || settings.value.questionCount < 1 || tooManyQuestions.value || settings.value.questionCount.value > settings.value.maxQuestions @@ -380,9 +388,13 @@ displaySectionTitle(activeSection.value, activeSectionIndex.value), ); - const remainingSelectableContent = computed( - () => settings.value.questionCount - workingResourcePool.value.length, - ); + const remainingSelectableContent = computed(() => { + if (settings.value.isChoosingManually) { + return settings.value.questionCount - workingQuestions.value.length; + } + + return settings.value.questionCount - workingResourcePool.value.length; + }); const selectAllRules = computed(() => [ contentList => contentList.length <= remainingSelectableContent.value, diff --git a/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/ManageSelectedQuestions.vue b/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/ManageSelectedQuestions.vue index 4065ac1789..1bee7df45b 100644 --- a/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/ManageSelectedQuestions.vue +++ b/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/ManageSelectedQuestions.vue @@ -24,7 +24,12 @@ {{ getQuestionContent(question).title }}
- +
@@ -35,7 +40,9 @@ diff --git a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/ResourceActionButton.vue b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/ResourceActionButton.vue new file mode 100644 index 0000000000..69e712deaa --- /dev/null +++ b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/ResourceActionButton.vue @@ -0,0 +1,74 @@ + + + + + + + diff --git a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue index 60f2aa00f4..80b0d7c9c1 100644 --- a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue +++ b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue @@ -10,37 +10,30 @@

{{ coreString('selectFromChannels') }}

- -
- - - {{ addedIndicator$() }} - - - - -
+ + > + + - import { getCurrentInstance, onMounted, ref } from 'vue'; - import commonCoreStrings from 'kolibri/uiText/commonCoreStrings'; import { enhancedQuizManagementStrings } from 'kolibri-common/strings/enhancedQuizManagementStrings.js'; import LearningActivityIcon from 'kolibri-common/components/ResourceDisplayAndSearch/LearningActivityIcon.vue'; import { ContentNodeKinds } from 'kolibri/constants'; - import { searchAndFilterStrings } from 'kolibri-common/strings/searchAndFilterStrings'; import { SelectionTarget } from '../../contants.js'; import { coachStrings } from '../../../commonCoachStrings.js'; import { PageNames } from '../../../../../constants/index.js'; import QuizResourceSelectionHeader from '../../QuizResourceSelectionHeader.vue'; import ResourceSelectionBreadcrumbs from '../../../../lessons/LessonResourceSelectionPage/SearchTools/ResourceSelectionBreadcrumbs.vue'; import useFetchContentNode from '../../../../../composables/useFetchContentNode'; + import QuestionsAccordion from '../../../QuestionsAccordion.vue'; import PreviewContent from './PreviewContent'; - import PreviewExercise from './PreviewExercise.vue'; + import ResourceActionButton from './ResourceActionButton.vue'; export default { name: 'PreviewSelectedResources', components: { PreviewContent, - PreviewExercise, + QuestionsAccordion, LearningActivityIcon, + ResourceActionButton, QuizResourceSelectionHeader, ResourceSelectionBreadcrumbs, }, - mixins: [commonCoreStrings], setup(props) { const prevRoute = ref(null); const instance = getCurrentInstance(); @@ -121,8 +114,6 @@ const { selectResourcesDescription$, selectPracticeQuizLabel$ } = enhancedQuizManagementStrings; - const { addText$, addedIndicator$ } = searchAndFilterStrings; - const getTitle = () => { if (props.target === SelectionTarget.LESSON) { return manageLessonResourcesTitle$(); @@ -162,8 +153,6 @@ redirectBack, // eslint-disable-next-line vue/no-unused-properties prevRoute, - addText$, - addedIndicator$, exerciseQuestions, }; }, @@ -241,7 +230,7 @@ if (this.disabled) { return true; } - return this.unselectableResourceIds?.includes(this.contentId); + return !!this.unselectableResourceIds?.includes(this.contentId); }, channelsLink() { return { @@ -292,21 +281,14 @@ }, }; }, - handleSelectQuestion(questionItem, value) { + handleSelectQuestions(questionsItem) { //Map the string of questionids to actual question object - const question = this.exerciseQuestions.find(q => q.item === questionItem); - if (value) { - this.$emit('selectQuestions', [question], this.contentNode); - } else { - this.$emit('deselectQuestions', [question]); - } + const questions = questionsItem.map(q => this.exerciseQuestions.find(eq => eq.item === q)); + this.$emit('selectQuestions', questions, this.contentNode); }, - handleSelectAllQuestions(value) { - if (value) { - this.$emit('selectQuestions', this.exerciseQuestions); - } else { - this.$emit('deselectQuestions', this.exerciseQuestions); - } + handleDeselectQuestionss(questionsItem) { + const questions = questionsItem.map(q => this.exerciseQuestions.find(eq => eq.item === q)); + this.$emit('deselectQuestions', questions); }, }, }; @@ -326,13 +308,4 @@ font-weight: 600; } - .mr-16 { - margin-right: 16px; - } - - .d-flex-center { - display: flex; - align-items: center; - } - diff --git a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/SelectFromBookmarks.vue b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/SelectFromBookmarks.vue index a290742770..4b943a8bb6 100644 --- a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/SelectFromBookmarks.vue +++ b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/SelectFromBookmarks.vue @@ -9,6 +9,7 @@ /> - import { getCurrentInstance } from 'vue'; + import { computed, getCurrentInstance } from 'vue'; import { now } from 'kolibri/utils/serverClock'; import { coreStrings } from 'kolibri/uiText/commonCoreStrings'; import UpdatedResourceSelection from '../UpdatedResourceSelection.vue'; @@ -86,12 +87,21 @@ return bookmarkedTimeAgoLabel$({ time }); }; + const isSelectable = computed(() => { + if (props.target === SelectionTarget.LESSON) { + return true; + } + // if choosing manually for quizzes, dont allow selecting resources + return !props.settings.isChoosingManually; + }); + return { channelsLink, contentList: data, hasMore, fetchMore, loadingMore, + isSelectable, contentCardMessage, SelectionTarget, }; diff --git a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/SelectFromTopicTree.vue b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/SelectFromTopicTree.vue index e9ad4fd1a8..59535ce37b 100644 --- a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/SelectFromTopicTree.vue +++ b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/SelectFromTopicTree.vue @@ -52,6 +52,7 @@ { + if (props.target === SelectionTarget.LESSON) { + return true; + } + // if choosing manually for quizzes, dont allow selecting resources + return !props.settings.isChoosingManually; + }); + const { data, hasMore, fetchMore, loadingMore } = props.treeFetch; return { contentList: data, @@ -180,6 +189,7 @@ fetchMore, loadingMore, SelectionTarget, + isSelectable, computedTopic, isTopicFromSearchResult, searchLabel$, diff --git a/kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromSearchResults.vue b/kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromSearchResults.vue index 7f45d2f018..827a576301 100644 --- a/kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromSearchResults.vue +++ b/kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromSearchResults.vue @@ -24,6 +24,7 @@ /> - import { getCurrentInstance } from 'vue'; + import { computed, getCurrentInstance } from 'vue'; import { coreStrings } from 'kolibri/uiText/commonCoreStrings'; import SearchChips from 'kolibri-common/components/SearchChips'; @@ -51,6 +52,7 @@ import { PageNames } from '../../../../../../constants'; import { coachStrings } from '../../../../../common/commonCoachStrings'; import UpdatedResourceSelection from '../../../../../common/resourceSelection/UpdatedResourceSelection.vue'; + import { SelectionTarget } from '../../../../../common/resourceSelection/contants'; /** * @typedef {import('../../../../../../composables/useFetch').FetchObject} FetchObject @@ -89,12 +91,21 @@ props.setTitle(manageLessonResourcesTitle$()); props.setGoBack(null); + const isSelectable = computed(() => { + if (props.target === SelectionTarget.LESSON) { + return true; + } + // if choosing manually for quizzes, dont allow selecting resources + return !props.settings.isChoosingManually; + }); + const { data, hasMore, fetchMore, loadingMore } = props.searchFetch; return { contentList: data, hasMore, fetchMore, loadingMore, + isSelectable, searchLabel$, selectFromChannels$, redirectBack, @@ -146,6 +157,22 @@ type: Function, required: true, }, + /** + * The target entity for the selection. + * It can be either 'quiz' or 'lesson'. + */ + target: { + type: String, + required: true, + }, + /** + * Selection settings used for quizzes. + */ + settings: { + type: Object, + required: false, + default: null, + }, }, computed: { resultsCountMessage() { diff --git a/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/CreateQuizSection.vue b/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/CreateQuizSection.vue index bb13ca39a5..26a513a1b0 100644 --- a/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/CreateQuizSection.vue +++ b/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/CreateQuizSection.vue @@ -172,10 +172,8 @@ :questions="activeQuestions" :selectedQuestions="selectedActiveQuestions" :getQuestionContent="question => activeResourceMap[question.exercise_id]" - :selectAllIsChecked="allQuestionsSelected" - :selectAllIsIndeterminate="selectAllIsIndeterminate" - @select="toggleQuestionInSelection" - @selectAll="selectAllQuestions" + @selectQuestions="addQuestionsToSelection" + @deselectQuestions="removeQuestionsFromSelection" @error="err => $emit('error', err)" @sort="handleQuestionOrderChange" > @@ -264,15 +262,13 @@ const { // Methods updateSection, - allQuestionsSelected, - selectAllIsIndeterminate, deleteActiveSelectedQuestions, addSection, removeSection, - selectAllQuestions, replacementQuestionPool, // Computed - toggleQuestionInSelection, + addQuestionsToSelection, + removeQuestionsFromSelection, allSections, activeSectionIndex, activeSection, @@ -299,11 +295,9 @@ deleteConfirmation$, questionsDeletedNotification$, - toggleQuestionInSelection, - selectAllQuestions, + addQuestionsToSelection, + removeQuestionsFromSelection, updateSection, - allQuestionsSelected, - selectAllIsIndeterminate, deleteActiveSelectedQuestions, addSection, removeSection, diff --git a/kolibri/plugins/coach/assets/test/useQuizCreation.spec.js b/kolibri/plugins/coach/assets/test/useQuizCreation.spec.js index 5df30237c4..e9c1d9dbed 100644 --- a/kolibri/plugins/coach/assets/test/useQuizCreation.spec.js +++ b/kolibri/plugins/coach/assets/test/useQuizCreation.spec.js @@ -56,8 +56,8 @@ describe('useQuizCreation', () => { removeSection, initializeQuiz, updateQuiz, - addQuestionToSelection, - removeQuestionFromSelection, + addQuestionsToSelection, + removeQuestionsFromSelection, saveQuiz, quiz, allSections, @@ -75,8 +75,8 @@ describe('useQuizCreation', () => { removeSection, initializeQuiz, updateQuiz, - addQuestionToSelection, - removeQuestionFromSelection, + addQuestionsToSelection, + removeQuestionsFromSelection, saveQuiz, // Computed @@ -215,21 +215,21 @@ describe('useQuizCreation', () => { }); it('Can add a question to the selected questions', () => { const { question_id } = get(activeQuestions)[0]; - addQuestionToSelection(question_id); + addQuestionsToSelection([question_id]); expect(get(selectedActiveQuestions)).toHaveLength(1); }); it("Can remove a question from the active section's selected questions", () => { const { question_id } = get(activeQuestions)[0]; - addQuestionToSelection(question_id); + addQuestionsToSelection([question_id]); expect(get(selectedActiveQuestions)).toHaveLength(1); - removeQuestionFromSelection(question_id); + removeQuestionsFromSelection([question_id]); expect(get(selectedActiveQuestions)).toHaveLength(0); }); it('Does not hold duplicates, so adding an existing question does nothing', () => { const { question_id } = get(activeQuestions)[0]; - addQuestionToSelection(question_id); + addQuestionsToSelection([question_id]); expect(get(selectedActiveQuestions)).toHaveLength(1); - addQuestionToSelection(question_id); + addQuestionsToSelection([question_id]); expect(get(selectedActiveQuestions)).toHaveLength(1); }); }); From 42fe3449235029a39596a26b4dfa653c7f6c1cf3 Mon Sep 17 00:00:00 2001 From: Alex Velez Date: Tue, 18 Feb 2025 12:05:42 -0500 Subject: [PATCH 6/8] Add unselectable resources when all questions have been selected --- .../PreviewSelectedResources/index.vue | 14 ++++++++ .../QuizResourceSelection/index.vue | 35 ++++++++++++++----- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue index 80b0d7c9c1..0b972ee7fb 100644 --- a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue +++ b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue @@ -59,6 +59,7 @@ :isSelectable="!!settings?.isChoosingManually" :maxSelectableQuestions="settings?.questionCount" :selectedQuestions="selectedQuestionItems" + :unselectableQuestionItems="unselectableQuestionItems" @selectQuestions="handleSelectQuestions" @deselectQuestions="handleDeselectQuestionss" /> @@ -173,11 +174,24 @@ type: Array, required: true, }, + /** + * Array of resource ids that already belongs to the quiz, + * and should not be selectable. + */ unselectableResourceIds: { type: Array, required: false, default: null, }, + /** + * Array of question ids that already belongs to the quiz, + * and should not be selectable. + */ + unselectableQuestionItems: { + type: Array, + required: false, + default: null, + }, disabled: { type: Boolean, default: false, diff --git a/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue b/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue index 1ac9c6e261..69b2593eec 100644 --- a/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue +++ b/kolibri/plugins/coach/assets/src/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue @@ -54,6 +54,8 @@ :target="SelectionTarget.QUIZ" :contentCardMessage="contentCardMessage" :getResourceLink="getResourceLink" + :unselectableResourceIds="unselectableResourceIds" + :unselectableQuestionItems="unselectableQuestionItems" @selectResources="addToWorkingResourcePool" @selectQuestions="addToWorkingQuestions" @deselectResources="removeFromWorkingResourcePool" @@ -143,7 +145,6 @@ import get from 'lodash/get'; import uniqWith from 'lodash/uniqWith'; import isEqual from 'lodash/isEqual'; - import { useMemoize } from '@vueuse/core'; import { displaySectionTitle, enhancedQuizManagementStrings, @@ -177,6 +178,7 @@ addQuestionsToSection, addQuestionsToSectionFromResources, allQuestionsInQuiz, + allResourceMap, activeQuestions, addSection, } = injectQuizCreation(); @@ -284,16 +286,15 @@ const { annotateTopicsWithDescendantCounts } = useQuizResources(); - const unusedQuestionsCount = useMemoize(content => { + const unusedQuestionsCount = content => { const questionItems = content.assessmentmetadata.assessment_item_ids.map( aid => `${content.id}:${aid}`, ); - const questionsItemsAlreadyUsed = allQuestionsInQuiz.value - .map(q => q.item) - .filter(i => questionItems.includes(i)); - const questionItemsAvailable = questionItems.length - questionsItemsAlreadyUsed.length; - return questionItemsAvailable; - }); + const questionsItemsUnused = questionItems + .filter(questionItem => !allQuestionsInQuiz.value.some(q => q.item === questionItem)) + .filter(questionItem => !workingQuestions.value.some(q => q.item === questionItem)); + return questionsItemsUnused.length; + }; const isPracticeQuiz = item => !selectPracticeQuiz || get(item, ['options', 'modality'], false) === 'QUIZ'; @@ -402,6 +403,22 @@ const selectionRules = computed(() => [() => remainingSelectableContent.value > 0]); + const unselectableResourceIds = computed(() => { + return Object.keys(allResourceMap.value).filter(exerciseId => { + const exercise = allResourceMap.value[exerciseId]; + const questionItems = exercise.assessmentmetadata.assessment_item_ids.map( + questionId => `${exerciseId}:${questionId}`, + ); + return questionItems.every(questionItem => + allQuestionsInQuiz.value.some(q => q.item === questionItem), + ); + }); + }); + + const unselectableQuestionItems = computed(() => { + return allQuestionsInQuiz.value.map(q => q.item); + }); + const maximumContentSelectedWarning = computed(() => { if (settings.value.questionCount <= 0 || remainingSelectableContent.value > 0) { return null; @@ -436,6 +453,8 @@ loading, selectionRules, selectAllRules, + unselectableResourceIds, + unselectableQuestionItems, maximumContentSelectedWarning, addToWorkingResourcePool, removeFromWorkingResourcePool, From 95ae8d6cf6339744bcc48bc3ac42ffbf949ea604 Mon Sep 17 00:00:00 2001 From: Alex Velez Date: Tue, 18 Feb 2025 12:06:16 -0500 Subject: [PATCH 7/8] Fix questions shopping cart and add empty questions notice --- .../src/views/common/QuestionsAccordion.vue | 18 +++- .../subPages/SelectFromSearchResults.vue | 14 +-- .../QuizResourceSelection/index.vue | 2 +- .../subPages/ManageSelectedQuestions.vue | 85 ++++++++++--------- .../strings/searchAndFilterStrings.js | 4 + 5 files changed, 70 insertions(+), 53 deletions(-) diff --git a/kolibri/plugins/coach/assets/src/views/common/QuestionsAccordion.vue b/kolibri/plugins/coach/assets/src/views/common/QuestionsAccordion.vue index 458ca40b88..8093a2cf7d 100644 --- a/kolibri/plugins/coach/assets/src/views/common/QuestionsAccordion.vue +++ b/kolibri/plugins/coach/assets/src/views/common/QuestionsAccordion.vue @@ -57,7 +57,7 @@ > + {{ emptyQuestionsList$() }} +

@@ -39,6 +41,8 @@