diff --git a/kolibri/plugins/coach/assets/src/composables/useQuizResources.js b/kolibri/plugins/coach/assets/src/composables/useQuizResources.js index 052ae64add..b243a8ca4e 100644 --- a/kolibri/plugins/coach/assets/src/composables/useQuizResources.js +++ b/kolibri/plugins/coach/assets/src/composables/useQuizResources.js @@ -128,11 +128,17 @@ export default function useQuizResources({ topicId } = {}) { } /** @returns {Boolean} Whether the given node should be displayed with a checkbox - * @description Returns whether the given node is an exercise or not -- although, could be - * extended in the future to permit topic-level selection if desired + * @description Returns true for exercises and for topics that have no topic children and no + * more children to load */ function hasCheckbox(node) { - return node.kind === ContentNodeKinds.EXERCISE; + return ( + node.kind === ContentNodeKinds.EXERCISE || + // Has children, no more to load, and no children are topics + (node.children && + !node.children.more && + !node.children.results.some(c => c.kind === ContentNodeKinds.TOPIC)) + ); } function setResources(r) { diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ResourceSelection.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ResourceSelection.vue index 8ac5bb5af5..d11589743b 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ResourceSelection.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ResourceSelection.vue @@ -50,6 +50,14 @@ +
+ {{ cannotSelectSomeTopicWarning$() }} +
+ + @@ -154,6 +163,7 @@ numberOfSelectedResources$, numberOfResources$, selectedResourcesInformation$, + cannotSelectSomeTopicWarning$, } = enhancedQuizManagementStrings; // TODO let's not use text for this @@ -178,7 +188,29 @@ * a list of unique resources to avoid unnecessary duplication */ function addToWorkingResourcePool(resources = []) { - workingResourcePool.value = uniqWith([...workingResourcePool.value, ...resources], isEqual); + workingResourcePool.value = uniqWith( + [ + ...workingResourcePool.value, + ...resources.filter(r => r.kind === ContentNodeKinds.EXERCISE), + ], + isEqual + ); + } + + /** + * @description Returns the list of Exercises which can possibly be selected from the current + * contentList taking into consideration the logic for whether a topic can be selected or not. + * @returns {QuizExercise[]} - All contents which can be selected + */ + function selectableContentList() { + return contentList.value.reduce((newList, content) => { + if (content.kind === ContentNodeKinds.TOPIC && hasCheckbox(content)) { + newList = [...newList, ...content.children.results]; + } else { + newList.push(content); + } + return newList; + }, []); } /** @@ -202,6 +234,9 @@ */ function contentPresentInWorkingResourcePool(content) { const workingResourceIds = workingResourcePool.value.map(wr => wr.id); + if (content.kind === ContentNodeKinds.TOPIC) { + return content.children.results.every(child => workingResourceIds.includes(child.id)); + } return workingResourceIds.includes(content.id); } @@ -226,6 +261,63 @@ }); } + const selectAllChecked = computed(() => { + // Returns true if all the resources in the topic are in the working resource pool + const workingResourceIds = workingResourcePool.value.map(wr => wr.id); + const selectableIds = selectableContentList().map(content => content.id); + return selectableIds.every(id => workingResourceIds.includes(id)); + }); + + const selectAllIndeterminate = computed(() => { + // Returns true if some, but not all, of the resources in the topic are in the working + // resource + const workingResourceIds = workingResourcePool.value.map(wr => wr.id); + const selectableIds = selectableContentList().map(content => content.id); + return !selectAllChecked.value && selectableIds.some(id => workingResourceIds.includes(id)); + }); + + const showSelectAll = computed(() => { + return contentList.value.every(content => hasCheckbox(content)); + }); + + function handleSelectAll(isChecked) { + if (isChecked) { + this.addToWorkingResourcePool(selectableContentList()); + } else { + this.contentList.forEach(content => { + var contentToRemove = []; + if (content.kind === ContentNodeKinds.TOPIC) { + contentToRemove = content.children.results; + } else { + contentToRemove.push(content); + } + contentToRemove.forEach(c => { + this.removeFromWorkingResourcePool(c); + }); + }); + } + } + + /** + * @param {Object} param + * @param {ContentNode} param.content + * @param {boolean} param.checked + * @affects workingResourcePool - Adds or removes the content from the workingResourcePool + * When given a topic, it adds or removes all the exercises in the topic from the + * workingResourcePool. This assumes that topics which should not be added are not able to + * be checked and does not do any additional checks. + */ + function toggleSelected({ content, checked }) { + content = content.kind === ContentNodeKinds.TOPIC ? content.children.results : [content]; + if (checked) { + this.addToWorkingResourcePool(content); + } else { + content.forEach(c => { + this.removeFromWorkingResourcePool(c); + }); + } + } + const { hasCheckbox, topic, @@ -341,6 +433,11 @@ } return { + selectAllChecked, + selectAllIndeterminate, + showSelectAll, + handleSelectAll, + toggleSelected, topic, topicId, contentList, @@ -353,6 +450,7 @@ resetWorkingResourcePool, contentPresentInWorkingResourcePool, //contentList, + cannotSelectSomeTopicWarning$, sectionSettings$, selectFromBookmarks$, numberOfSelectedBookmarks$, @@ -382,20 +480,6 @@ isTopicIdSet() { return this.$route.params.topic_id; }, - isSelectAllChecked() { - // Returns true if all the resources in the topic are in the working resource pool - const workingResourceIds = this.workingResourcePool.map(wr => wr.id); - return this.contentList.every(content => workingResourceIds.includes(content.id)); - }, - selectAllIndeterminate() { - // Returns true if some, but not all, of the resources in the topic are in the working - // resource - const workingResourceIds = this.workingResourcePool.map(wr => wr.id); - return ( - !this.isSelectAllChecked && - this.contentList.some(content => workingResourceIds.includes(content.id)) - ); - }, getBookmarksLink() { // Inject the showBookmarks parameter so that @@ -423,6 +507,12 @@ }, }, methods: { + showTopicSizeWarningCard(content) { + return !this.hasCheckbox(content) && content.kind === ContentNodeKinds.TOPIC; + }, + showTopicSizeWarning() { + return this.contentList.some(this.showTopicSizeWarningCard); + }, /** @public */ focusFirstEl() { this.$refs.textbox.focus(); @@ -446,22 +536,6 @@ return {}; // or return {} if you prefer an empty object }, - toggleSelected({ content, checked }) { - if (checked) { - this.addToWorkingResourcePool([content]); - } else { - this.removeFromWorkingResourcePool(content); - } - }, - toggleTopicInWorkingResources(isChecked) { - if (isChecked) { - this.addToWorkingResourcePool(this.contentList); - } else { - this.contentList.forEach(content => { - this.removeFromWorkingResourcePool(content); - }); - } - }, topicListingLink({ topicId }) { return this.$router.getRoute( PageNames.QUIZ_SELECT_RESOURCES, @@ -607,4 +681,9 @@ margin-top: 2em; } + .shadow { + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.14), + 0 2px 1px -1px rgba(0, 0, 0, 0.12); + } + diff --git a/kolibri/plugins/coach/assets/src/views/plan/LessonResourceSelectionPage/ContentCardList.vue b/kolibri/plugins/coach/assets/src/views/plan/LessonResourceSelectionPage/ContentCardList.vue index d0dfd643cc..ffc72b8e6c 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/LessonResourceSelectionPage/ContentCardList.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/LessonResourceSelectionPage/ContentCardList.vue @@ -33,7 +33,11 @@ :link="contentCardLink(content)" :numCoachContents="content.num_coach_contents" :isLeaf="content.is_leaf" - /> + > + + diff --git a/kolibri/plugins/coach/assets/src/views/plan/LessonResourceSelectionPage/LessonContentCard/index.vue b/kolibri/plugins/coach/assets/src/views/plan/LessonResourceSelectionPage/LessonContentCard/index.vue index c64bf7f18c..e3ed3035d4 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/LessonResourceSelectionPage/LessonContentCard/index.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/LessonResourceSelectionPage/LessonContentCard/index.vue @@ -52,6 +52,7 @@ :isTopic="isTopic" /> + diff --git a/packages/kolibri-common/strings/enhancedQuizManagementStrings.js b/packages/kolibri-common/strings/enhancedQuizManagementStrings.js index 039a606f14..6cb7297274 100644 --- a/packages/kolibri-common/strings/enhancedQuizManagementStrings.js +++ b/packages/kolibri-common/strings/enhancedQuizManagementStrings.js @@ -154,4 +154,8 @@ export const enhancedQuizManagementStrings = createTranslator('EnhancedQuizManag message: '{count, number, integer} of {total, number, integer} {total, plural, one {resource selected} other {resources selected}}', }, + cannotSelectSomeTopicWarning: { + message: + 'You can only select folders with 12 or less exercises and no subfolders to avoid oversized quizzes.', + }, });