From 102af51afe8dbb5e6113414a69dd8c7cdee9a768 Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Tue, 2 Jul 2024 07:43:46 -0700 Subject: [PATCH] Update how we calculate descendant count display. Return descendant count for current topics, not ancestor counts for current lesson resources. --- .../assets/src/api-resources/contentNode.js | 4 +- kolibri/core/content/api.py | 32 ++---- .../src/modules/lessonResources/handlers.js | 102 +++++++++--------- .../src/modules/lessonResources/index.js | 6 +- .../LessonResourceSelectionPage/index.vue | 15 +-- 5 files changed, 69 insertions(+), 90 deletions(-) diff --git a/kolibri/core/assets/src/api-resources/contentNode.js b/kolibri/core/assets/src/api-resources/contentNode.js index 67e52fa43b0..7809e548b9c 100644 --- a/kolibri/core/assets/src/api-resources/contentNode.js +++ b/kolibri/core/assets/src/api-resources/contentNode.js @@ -83,8 +83,8 @@ export default new Resource({ fetchRandomCollection({ getParams: params }) { return this.getListEndpoint('random', params); }, - fetchDescendants(ids, getParams = {}) { - return this.getListEndpoint('descendants', { ids, ...getParams }); + fetchDescendantCounts(getParams) { + return this.getListEndpoint('descendant_counts', { ...getParams }); }, fetchDescendantsAssessments(ids) { return this.getListEndpoint('descendants_assessments', { ids }); diff --git a/kolibri/core/content/api.py b/kolibri/core/content/api.py index 7f44c67b7e0..b107555c4ba 100644 --- a/kolibri/core/content/api.py +++ b/kolibri/core/content/api.py @@ -861,33 +861,17 @@ def random(self, request, **kwargs): return Response(self.serialize(queryset)) @action(detail=False) - def descendants(self, request): + def descendant_counts(self, request): """ - Returns a slim view all the descendants of a set of content nodes (as designated by the passed in ids). - In addition to id, title, kind, and content_id, each node is also annotated with the ancestor_id of one - of the ids that are passed into the request. - In the case where a node has more than one ancestor in the set of content nodes requested, duplicates of - that content node are returned, each annotated with one of the ancestor_ids for a node. + Return the number of descendants for each node in the queryset. """ - ids = self.request.query_params.get("ids", None) - if not ids: + # Don't allow unfiltered queries + if not self.request.query_params: return Response([]) - kind = self.request.query_params.get("descendant_kind", None) - nodes = self.filter_queryset(self.get_queryset()) - data = [] - for node in nodes: - - def copy_node(new_node): - new_node["ancestor_id"] = node.id - new_node["is_leaf"] = new_node.get("kind") != content_kinds.TOPIC - return new_node - - node_data = node.get_descendants().filter(available=True) - if kind: - node_data = node_data.filter(kind=kind) - data += map( - copy_node, node_data.values("id", "title", "kind", "content_id") - ) + queryset = self.filter_queryset(self.get_queryset()) + + data = queryset.values("id", "on_device_resources") + return Response(data) @action(detail=False) diff --git a/kolibri/plugins/coach/assets/src/modules/lessonResources/handlers.js b/kolibri/plugins/coach/assets/src/modules/lessonResources/handlers.js index 97c1fb41b67..8f0481ec52d 100644 --- a/kolibri/plugins/coach/assets/src/modules/lessonResources/handlers.js +++ b/kolibri/plugins/coach/assets/src/modules/lessonResources/handlers.js @@ -10,7 +10,14 @@ import chunk from 'lodash/chunk'; import { LessonsPageNames } from '../../constants/lessonsConstants'; async function showResourceSelectionPage(store, params) { - const { lessonId, contentList, pageName, bookmarksList, ancestors = [] } = params; + const { + lessonId, + contentList, + pageName, + bookmarksList, + ancestors = [], + descendantCounts = [], + } = params; const pendingSelections = store.state.lessonSummary.workingResources || []; const cache = store.state.lessonSummary.resourceCache || {}; const initClassInfoPromise = store.dispatch('initClassInfo', params.classId); @@ -32,14 +39,17 @@ async function showResourceSelectionPage(store, params) { store.commit('lessonSummary/resources/SET_BOOKMARKS_LIST', bookmarksList); store.commit('lessonSummary/resources/SET_STATE', { contentList: [], - ancestors: [], + ancestors, }); - store.dispatch('notLoading'); if (lessonId) { const loadRequirements = [store.dispatch('lessonSummary/updateCurrentLesson', lessonId)]; return Promise.all(loadRequirements).then(([currentLesson]) => { - // TODO make a state mapper + const resourceIds = currentLesson.resources.map(resourceObj => resourceObj.contentnode_id); + const setResourceCachePromise = store.dispatch( + 'lessonSummary/getResourceCache', + resourceIds, + ); // contains selections that were commited to server prior to opening this page if (!pendingSelections.length) { store.commit('lessonSummary/SET_WORKING_RESOURCES', currentLesson.resources); @@ -49,51 +59,29 @@ async function showResourceSelectionPage(store, params) { store.commit('lessonSummary/resources/SET_ANCESTORS', ancestors); } - const ancestorCounts = {}; + const descendantCountsObject = {}; + for (const descendantCount of descendantCounts.data || descendantCounts) { + descendantCountsObject[descendantCount.id] = descendantCount.on_device_resources; + } - const resourceAncestors = store.state.lessonSummary.workingResources.map( - resource => (cache[resource.contentnode_id] || {}).ancestors || [], - ); - // store ancestor ids to get their descendants later - const ancestorIds = new Set(); + store.commit('lessonSummary/resources/SET_DESCENDANT_COUNTS', descendantCountsObject); - resourceAncestors.forEach(ancestorArray => - ancestorArray.forEach(ancestor => { - ancestorIds.add(ancestor.id); - if (ancestorCounts[ancestor.id]) { - ancestorCounts[ancestor.id].count++; - } else { - ancestorCounts[ancestor.id] = {}; - // total number of working/added resources - ancestorCounts[ancestor.id].count = 1; - // total number of descendants - ancestorCounts[ancestor.id].total = 0; - } - }), - ); - ContentNodeResource.fetchDescendants(Array.from(ancestorIds)).then(nodes => { - nodes.data.forEach(node => { - // exclude topics from total resource calculation - if (node.kind !== ContentNodeKinds.TOPIC) { - ancestorCounts[node.ancestor_id].total++; - } + // carry pendingSelections over from other interactions in this modal + store.commit('lessonSummary/resources/SET_CONTENT_LIST', contentList); + if (params.searchResults) { + store.commit('lessonSummary/resources/SET_SEARCH_RESULTS', params.searchResults); + } + store.commit('SET_PAGE_NAME', pageName); + if (pageName === LessonsPageNames.SELECTION_SEARCH) { + store.commit('SET_TOOLBAR_ROUTE', { + name: LessonsPageNames.SELECTION_ROOT, }); - store.commit('lessonSummary/resources/SET_ANCESTOR_COUNTS', ancestorCounts); - // carry pendingSelections over from other interactions in this modal - store.commit('lessonSummary/resources/SET_CONTENT_LIST', contentList); - if (params.searchResults) { - store.commit('lessonSummary/resources/SET_SEARCH_RESULTS', params.searchResults); - } - store.commit('SET_PAGE_NAME', pageName); - if (pageName === LessonsPageNames.SELECTION_SEARCH) { - store.commit('SET_TOOLBAR_ROUTE', { - name: LessonsPageNames.SELECTION_ROOT, - }); - } else { - store.commit('SET_TOOLBAR_ROUTE', { - name: LessonsPageNames.SUMMARY, - }); - } + } else { + store.commit('SET_TOOLBAR_ROUTE', { + name: LessonsPageNames.SUMMARY, + }); + } + return setResourceCachePromise.then(() => { store.dispatch('notLoading'); }); }); @@ -111,13 +99,17 @@ export function showLessonResourceSelectionRootPage(store, params) { is_leaf: false, }; }); - - return showResourceSelectionPage(store, { - classId: params.classId, - lessonId: params.lessonId, - contentList: channelContentList, - pageName: LessonsPageNames.SELECTION_ROOT, - }); + return ContentNodeResource.fetchDescendantCounts({ parent__isnull: true }).then( + descendantCounts => { + return showResourceSelectionPage(store, { + classId: params.classId, + lessonId: params.lessonId, + contentList: channelContentList, + pageName: LessonsPageNames.SELECTION_ROOT, + descendantCounts, + }); + }, + ); }); } @@ -128,9 +120,10 @@ export function showLessonResourceSelectionTopicPage(store, params) { const loadRequirements = [ ContentNodeResource.fetchModel({ id: topicId }), ContentNodeResource.fetchCollection({ getParams: { parent: topicId } }), + ContentNodeResource.fetchDescendantCounts({ parent: topicId }), ]; - return Promise.all(loadRequirements).then(([topicNode, childNodes]) => { + return Promise.all(loadRequirements).then(([topicNode, childNodes, descendantCounts]) => { const topicContentList = childNodes.map(node => { return { ...node, thumbnail: getContentNodeThumbnail(node) }; }); @@ -140,6 +133,7 @@ export function showLessonResourceSelectionTopicPage(store, params) { lessonId: params.lessonId, contentList: topicContentList, pageName: LessonsPageNames.SELECTION, + descendantCounts, ancestors: [...topicNode.ancestors, topicNode], }); }); diff --git a/kolibri/plugins/coach/assets/src/modules/lessonResources/index.js b/kolibri/plugins/coach/assets/src/modules/lessonResources/index.js index b775fd90699..6865cb8a584 100644 --- a/kolibri/plugins/coach/assets/src/modules/lessonResources/index.js +++ b/kolibri/plugins/coach/assets/src/modules/lessonResources/index.js @@ -5,7 +5,7 @@ import * as actions from './actions'; function defaultState() { return { bookmarksList: [], - ancestorCounts: {}, + descendantCounts: {}, ancestors: [], contentList: [], searchResults: { @@ -44,8 +44,8 @@ export default { SET_BOOKMARKS_LIST(state, bookmarks) { state.bookmarksList = bookmarks; }, - SET_ANCESTOR_COUNTS(state, ancestorCountsObject) { - state.ancestorCounts = ancestorCountsObject; + SET_DESCENDANT_COUNTS(state, descendantCountsObject) { + state.descendantCounts = descendantCountsObject; }, SET_CONTENT_LIST(state, contentList) { state.contentList = contentList; diff --git a/kolibri/plugins/coach/assets/src/views/plan/LessonResourceSelectionPage/index.vue b/kolibri/plugins/coach/assets/src/views/plan/LessonResourceSelectionPage/index.vue index a1496bae9e1..e0a86cf7ddb 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/LessonResourceSelectionPage/index.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/LessonResourceSelectionPage/index.vue @@ -171,9 +171,9 @@ computed: { ...mapState(['pageName']), ...mapState('classSummary', { classId: 'id' }), - ...mapState('lessonSummary', ['currentLesson', 'workingResources']), + ...mapState('lessonSummary', ['currentLesson', 'resourceCache', 'workingResources']), ...mapState('lessonSummary/resources', [ - 'ancestorCounts', + 'descendantCounts', 'contentList', 'bookmarksList', 'searchResults', @@ -485,15 +485,16 @@ }, selectionMetadata(content) { let count = 0; - let total = 0; - if (this.ancestorCounts[content.id]) { - count = this.ancestorCounts[content.id].count; - total = this.ancestorCounts[content.id].total; + for (const wr of this.workingResources) { + const resource = this.resourceCache[wr.contentnode_id]; + if (resource && resource.ancestors.find(ancestor => ancestor.id === content.id)) { + count += 1; + } } if (count) { return this.$tr('selectionInformation', { count, - total, + total: this.descendantCounts[content.id], }); } return '';