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

Bookmarks feature #8272

Merged
merged 12 commits into from
Sep 14, 2021
4 changes: 4 additions & 0 deletions kolibri/core/assets/src/api-resources/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export const DynamicNetworkLocationResource = new Resource({
name: 'dynamicnetworklocation',
});

export const BookmarksResource = new Resource({
name: 'bookmarks',
});

export { default as ClassroomResource } from './classroom';
export { default as ContentNodeResource } from './contentNode';
export { default as ContentNodeGranularResource } from './contentNodeGranular';
Expand Down
1 change: 1 addition & 0 deletions kolibri/core/assets/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const ContentNodeKinds = {
CLASSROOM: 'CLASSROOM',
ACTIVITY: 'ACTIVITY',
SLIDESHOW: 'slideshow',
BOOKMARK: 'bookmark',
Evgeni998 marked this conversation as resolved.
Show resolved Hide resolved
};

export const LearningActivities = {
Expand Down
2 changes: 2 additions & 0 deletions kolibri/plugins/coach/assets/src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export const PageNames = {
EXAM_CREATION_SELECT_CHANNEL_QUIZ_TOPIC: 'EXAM_CREATION_SELECT_CHANNEL_QUIZ_TOPIC',
EXAM_CREATION_CHANNEL_QUIZ_PREVIEW: 'EXAM_CREATION_CHANNEL_QUIZ_PREVIEW',
EXAM_CREATION_TOPIC: 'EXAM_CREATION_TOPIC',
EXAM_CREATION_BOOKMARKS: 'EXAM_CREATION_BOOKMARKS',
EXAM_CREATION_BOOKMARKS_MAIN: 'EXAM_CREATION_BOOKMARKS_MAIN',
EXAM_CREATION_PREVIEW: 'EXAM_CREATION_PREVIEW',
EXAM_CREATION_SEARCH: 'EXAM_CREATION_SEARCH',
EXAM_CREATION_QUESTION_SELECTION: 'EXAM_CREATION_QUESTION_SELECTION',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export const LessonsPageNames = {
SELECTION_SEARCH: 'SELECTION_SEARCH',
SELECTION_CONTENT_PREVIEW: 'SELECTION_CONTENT_PREVIEW', // exclusively a route name
CONTENT_PREVIEW: 'CONTENT_PREVIEW',
LESSON_SELECTION_BOOKMARKS: 'LESSON_SELECTION_BOOKMARKS',
LESSON_SELECTION_BOOKMARKS_MAIN: 'LESSON_SELECTION_BOOKMARKS_MAIN',
};

export const CollectionTypes = {
Expand Down
63 changes: 61 additions & 2 deletions kolibri/plugins/coach/assets/src/modules/examCreation/handlers.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import pickBy from 'lodash/pickBy';
import uniq from 'lodash/uniq';
import { ContentNodeKinds } from 'kolibri.coreVue.vuex.constants';
import { ContentNodeResource, ContentNodeSearchResource, ChannelResource } from 'kolibri.resources';
import {
ContentNodeResource,
BookmarksResource,
ContentNodeSearchResource,
ChannelResource,
} from 'kolibri.resources';
import { assessmentMetaDataState } from 'kolibri.coreVue.vuex.mappers';
import router from 'kolibri.coreVue.router';
import chunk from 'lodash/chunk';
import { PageNames } from '../../constants';
import { filterAndAnnotateContentList, fetchChannelQuizzes } from './actions';

function showExamCreationPage(store, params) {
const { contentList, pageName, ancestors = [], searchResults = null } = params;
const { contentList, bookmarksList, pageName, ancestors = [], searchResults = null } = params;
return store.dispatch('loading').then(() => {
store.commit('examCreation/SET_ANCESTORS', ancestors);
store.commit('examCreation/SET_CONTENT_LIST', contentList);
store.commit('examCreation/SET_BOOKMARKS_LIST', bookmarksList);
if (searchResults) {
store.commit('examCreation/SET_SEARCH_RESULTS', searchResults);
}
Expand Down Expand Up @@ -118,6 +125,58 @@ export function showExamCreationTopicPage(store, params) {
});
});
}
export function showExamCreationBookmarksPage(store, params) {
return store.dispatch('loading').then(() => {
const { topicId } = params;
const topicNodePromise = ContentNodeResource.fetchModel({ id: topicId });
const childNodesPromise = ContentNodeResource.fetchCollection({
getParams: {
parent: topicId,
kind_in: [ContentNodeKinds.TOPIC, ContentNodeKinds.VIDEO, ContentNodeKinds.EXERCISE],
},
});
const loadRequirements = [topicNodePromise, childNodesPromise];

return Promise.all(loadRequirements).then(([topicNode, childNodes]) => {
return filterAndAnnotateContentList(childNodes).then(() => {
store.commit('SET_TOOLBAR_ROUTE', {
name: PageNames.EXAMS,
});
return showExamCreationPage(store, {
classId: params.classId,
bookmarksList: childNodes,
Evgeni998 marked this conversation as resolved.
Show resolved Hide resolved
pageName: PageNames.EXAM_CREATION_BOOKMARKS,
ancestors: [...topicNode.ancestors, topicNode],
});
});
});
});
}
export function showExamCreationAllBookmarks(store) {
return store.dispatch('loading').then(() => {
getBookmarks().then(bookmarks => {
return showExamCreationPage(store, {
bookmarksList: bookmarks[0],
});
});
});
}
function getBookmarks() {
return BookmarksResource.fetchCollection()
.then(bookmarks => bookmarks.map(bookmark => bookmark.contentnode_id))
.then(contentNodeIds => {
const chunkedContentNodeIds = chunk(contentNodeIds, 50); // Breaking contentNodeIds into lists no more than 50 in length
// Now we will create an array of promises, each of which queries for the 50-id chunk
const fetchPromises = chunkedContentNodeIds.map(idsChunk => {
return ContentNodeResource.fetchCollection({
getParams: {
ids: idsChunk, // This filters only the ids we want
},
});
});
return Promise.all(fetchPromises);
});
}

export function showExamCreationPreviewPage(store, params, query = {}) {
const { classId, contentId } = params;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function defaultState() {
numberOfQuestions: 10,
seed: getRandomInt(), // consistent seed is used for question selection
contentList: [],
bookmarksList: [],
selectedExercises: {},
availableQuestions: 0,
searchResults: {
Expand Down Expand Up @@ -70,6 +71,9 @@ export default {
SET_CONTENT_LIST(state, contentList) {
state.contentList = contentList;
},
SET_BOOKMARKS_LIST(state, bookmarksList) {
state.bookmarksList = bookmarksList;
},
ADD_TO_SELECTED_EXERCISES(state, exercises) {
state.selectedExercises = Object.assign(
{},
Expand Down
177 changes: 117 additions & 60 deletions kolibri/plugins/coach/assets/src/modules/lessonResources/handlers.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import pickBy from 'lodash/pickBy';
import { assessmentMetaDataState } from 'kolibri.coreVue.vuex.mappers';
import { ContentNodeResource, ContentNodeSearchResource } from 'kolibri.resources';
import {
BookmarksResource,
ContentNodeResource,
ContentNodeSearchResource,
} from 'kolibri.resources';
import { ContentNodeKinds } from 'kolibri.coreVue.vuex.constants';
import { getContentNodeThumbnail } from 'kolibri.utils.contentNode';
import chunk from 'lodash/chunk';
import { LessonsPageNames } from '../../constants/lessonsConstants';

function showResourceSelectionPage(store, params) {
const { lessonId, contentList, pageName, ancestors = [] } = params;
const { lessonId, contentList, pageName, bookmarksList, ancestors = [] } = params;
const pendingSelections = store.state.lessonSummary.workingResources || [];
const cache = store.state.lessonSummary.resourceCache || {};
const lessonSummaryState = {
Expand All @@ -18,73 +23,77 @@ function showResourceSelectionPage(store, params) {
return store.dispatch('loading').then(() => {
store.commit('SET_TOOLBAR_ROUTE', {});
store.commit('lessonSummary/SET_STATE', lessonSummaryState);
store.commit('lessonSummary/resources/SET_BOOKMARKS_LIST', bookmarksList);
store.commit('lessonSummary/resources/SET_STATE', {
contentList: [],
ancestors: [],
});
store.dispatch('notLoading');

const loadRequirements = [store.dispatch('lessonSummary/updateCurrentLesson', lessonId)];
return Promise.all(loadRequirements).then(([currentLesson]) => {
// TODO make a state mapper
// contains selections that were commited to server prior to opening this page
if (!pendingSelections.length) {
store.commit('lessonSummary/SET_WORKING_RESOURCES', currentLesson.resources);
}

if (ancestors.length) {
store.commit('lessonSummary/resources/SET_ANCESTORS', ancestors);
}

const ancestorCounts = {};

let resourceAncestors;

resourceAncestors = store.state.lessonSummary.workingResources.map(
resource => (cache[resource.contentnode_id] || {}).ancestors || []
);
// store ancestor ids to get their descendants later
const ancestorIds = new Set();

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;
if (lessonId) {
const loadRequirements = [store.dispatch('lessonSummary/updateCurrentLesson', lessonId)];
return Promise.all(loadRequirements).then(([currentLesson]) => {
// TODO make a state mapper
// contains selections that were commited to server prior to opening this page
if (!pendingSelections.length) {
store.commit('lessonSummary/SET_WORKING_RESOURCES', currentLesson.resources);
}

if (ancestors.length) {
store.commit('lessonSummary/resources/SET_ANCESTORS', ancestors);
}

const ancestorCounts = {};

let resourceAncestors;

resourceAncestors = store.state.lessonSummary.workingResources.map(
resource => (cache[resource.contentnode_id] || {}).ancestors || []
);
// store ancestor ids to get their descendants later
const ancestorIds = new Set();

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++;
}
});
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);
}
})
);
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++;
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,
});
}
store.dispatch('notLoading');
});
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,
});
}
store.dispatch('notLoading');
});
});
}
});
}

Expand Down Expand Up @@ -132,7 +141,54 @@ export function showLessonResourceSelectionTopicPage(store, params) {
});
});
}
export function showLessonResourceBookmarks(store, params) {
return store.dispatch('loading').then(() => {
const { topicId } = params;
const loadRequirements = [
ContentNodeResource.fetchModel({ id: topicId }),
ContentNodeResource.fetchCollection({ getParams: { parent: topicId } }),
];

return Promise.all(loadRequirements).then(([topicNode, childNodes]) => {
const topicContentList = childNodes.map(node => {
return { ...node, thumbnail: getContentNodeThumbnail(node) };
});

return showResourceSelectionPage(store, {
classId: params.classId,
lessonId: params.lessonId,
bookmarksList: topicContentList,
pageName: LessonsPageNames.SELECTION,
ancestors: [...topicNode.ancestors, topicNode],
});
});
});
}
export function showLessonResourceBookmarksMain(store) {
return store.dispatch('loading').then(() => {
getBookmarks().then(bookmarks => {
return showResourceSelectionPage(store, {
bookmarksList: bookmarks[0],
});
});
});
}
function getBookmarks() {
return BookmarksResource.fetchCollection()
.then(bookmarks => bookmarks.map(bookmark => bookmark.contentnode_id))
.then(contentNodeIds => {
const chunkedContentNodeIds = chunk(contentNodeIds, 50); // Breaking contentNodeIds into lists no more than 50 in length
// Now we will create an array of promises, each of which queries for the 50-id chunk
const fetchPromises = chunkedContentNodeIds.map(idsChunk => {
return ContentNodeResource.fetchCollection({
getParams: {
ids: idsChunk, // This filters only the ids we want
},
});
});
return Promise.all(fetchPromises);
});
}
export function showLessonResourceContentPreview(store, params) {
const { classId, lessonId, contentId } = params;
return store.dispatch('loading').then(() => {
Expand All @@ -143,6 +199,7 @@ export function showLessonResourceContentPreview(store, params) {
}

export function showLessonSelectionContentPreview(store, params, query = {}) {
console.log('in prview', params);
const { classId, lessonId, contentId } = params;
return store.dispatch('loading').then(() => {
const pendingSelections = store.state.lessonSummary.workingResources || [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as actions from './actions';

function defaultState() {
return {
bookmarksList: [],
ancestorCounts: {},
ancestors: [],
contentList: [],
Expand Down Expand Up @@ -40,6 +41,9 @@ export default {
SET_ANCESTORS(state, ancestors) {
state.ancestors = [...ancestors];
},
SET_BOOKMARKS_LIST(state, bookmarks) {
state.bookmarksList = bookmarks;
},
SET_ANCESTOR_COUNTS(state, ancestorCountsObject) {
state.ancestorCounts = ancestorCountsObject;
},
Expand Down
Loading