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

Search updates #8570

Merged
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5cf317d
Use DjangoJSONEncoder for bootstrapping plugin data to allow datetimes.
rtibbles Oct 30, 2021
f4bad16
Bootstrap full available channel data into the learn page.
rtibbles Oct 30, 2021
e917afb
Update useChannels composable to use bootstrapped channel data.
rtibbles Oct 30, 2021
85688cb
Consolidate search logic in useSearch composable.
rtibbles Oct 30, 2021
80ea591
Don't show channels selector on within channel search.
rtibbles Oct 30, 2021
d0d8065
Fix channel breadcrumbs display.
rtibbles Oct 30, 2021
07bac8d
Generalize channel specificity for search to topic specificity.
rtibbles Oct 30, 2021
dd679dc
Simplify routes. Add topics search route.
rtibbles Oct 30, 2021
661012e
Fix search chips display. Add translation for no categories.
rtibbles Oct 30, 2021
1dd4670
Fix search clearing.
rtibbles Oct 30, 2021
f07bd2e
Consolidate library page handler into a single function.
rtibbles Oct 30, 2021
e9bd98a
Fix breadcrumbs for route changes.
rtibbles Oct 30, 2021
e3f4884
Clean up unused constants, components and functionality.
rtibbles Oct 30, 2021
dcdf284
Upgrade kolibri-constants.
rtibbles Oct 30, 2021
5aed660
Add code comments to convoluted categories nesting code.
rtibbles Nov 1, 2021
67d1f18
Handle search clearing properly in all cases.
rtibbles Nov 1, 2021
0c1ea94
Rename tree viewset parameter to avoid collision with contentnode fil…
rtibbles Nov 1, 2021
9331bfd
Add redirect to handle historic channels page.
rtibbles Nov 1, 2021
d5ffc8e
Add full test coverage to useSearch module. Fix bugs and cleanup.
rtibbles Nov 2, 2021
13d0e19
Don't display chips for null or empty keywords value.
rtibbles Nov 2, 2021
22d330f
Update searchInputValue to always be empty string for no value.
rtibbles Nov 2, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions kolibri/core/assets/src/mixins/commonCoreStrings.js
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,9 @@ const nonconformingKeys = {
MATERIALS: 'NeedsMaterials',
FOR_BEGINNERS: 'ForBeginners',
digitalLiteracy: 'digitialLiteracy',
BASIC_SKILLS: 'allLevelsBasicSkills',
FOUNDATIONS: 'basicSkills',
FOUNDATIONS_LOGIC_AND_CRITICAL_THINKING: 'logicAndCriticalThinking',
};

/**
Expand Down Expand Up @@ -1080,8 +1083,8 @@ export default {
* string mapping to the values to be passed for those arguments.
*/
coreString(key, args) {
if (key === 'None of the above') {
return noneOfTheAboveTranslator.$tr(key, args);
if (key === 'None of the above' || key === METADATA.NoCategories) {
return noneOfTheAboveTranslator.$tr('None of the above', args);
}

const metadataKey = get(MetadataLookup, key, null);
Expand Down
6 changes: 6 additions & 0 deletions kolibri/core/content/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from django_filters.rest_framework import ChoiceFilter
from django_filters.rest_framework import DjangoFilterBackend
from django_filters.rest_framework import FilterSet
from django_filters.rest_framework import NumberFilter
from django_filters.rest_framework import UUIDFilter
from le_utils.constants import content_kinds
from le_utils.constants import languages
Expand Down Expand Up @@ -185,6 +186,8 @@ class ContentNodeFilter(IdFilter):
channels = UUIDInFilter(name="channel_id")
languages = CharInFilter(name="lang_id")
categories__isnull = BooleanFilter(field_name="categories", lookup_expr="isnull")
lft__gt = NumberFilter(field_name="lft", lookup_expr="gt")
rght__lt = NumberFilter(field_name="rght", lookup_expr="lt")

class Meta:
model = models.ContentNode
Expand All @@ -211,6 +214,9 @@ class Meta:
"keywords",
"channels",
"languages",
"tree_id",
"lft__gt",
"rght__lt",
]

def filter_kind(self, queryset, name, value):
Expand Down
2 changes: 1 addition & 1 deletion kolibri/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"hammerjs": "^2.0.8",
"intl": "^1.2.4",
"knuth-shuffle-seeded": "^1.0.6",
"kolibri-constants": "^0.1.32-beta1",
"kolibri-constants": "^0.1.34",
"kolibri-design-system": "git://github.com/learningequality/kolibri-design-system#632240f0b11f37c8b18de5ed930a4bf66b7c3678",
"lockr": "0.8.4",
"lodash": "^4.17.21",
Expand Down
6 changes: 5 additions & 1 deletion kolibri/core/webpack/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from django.conf import settings
from django.contrib.staticfiles.finders import find as find_staticfiles
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.serializers.json import DjangoJSONEncoder
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from django.utils.six.moves.urllib.request import url2pathname
Expand Down Expand Up @@ -276,7 +277,10 @@ def plugin_data_tag(self):
bundle=self.unique_id,
plugin_data=json.dumps(
json.dumps(
self.plugin_data, separators=(",", ":"), ensure_ascii=False
self.plugin_data,
separators=(",", ":"),
ensure_ascii=False,
cls=DjangoJSONEncoder,
)
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

const MOCK_DEFAULTS = {
channels: [],
fetchChannels: jest.fn(),
channelsMap: {},
};

export function useChannelsMock(overrides = {}) {
Expand Down
30 changes: 11 additions & 19 deletions kolibri/plugins/learn/assets/src/composables/useChannels.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,22 @@
*/

import { ref } from 'kolibri.lib.vueCompositionApi';
import { set } from '@vueuse/core';
import { ChannelResource } from 'kolibri.resources';
import plugin_data from 'plugin_data';

const channelsArray = plugin_data.channels ? plugin_data.channels : [];
const chanMap = {};

for (let channel of channelsArray) {
chanMap[channel.id] = channel;
}

// The refs are defined in the outer scope so they can be used as a shared store
const channels = ref([]);
const channels = ref(channelsArray);
const channelsMap = ref(chanMap);

export default function useChannels() {
/**
* Fetches channels and saves data to this composable's store
*
* @param {Boolean} available only get the channels that are "available"
* (i.e. with resources on device) when `true`
* @returns {Promise}
* @public
*/
function fetchChannels({ available = true } = {}) {
return ChannelResource.fetchCollection({ getParams: { available } }).then(collection => {
set(channels, collection);
return collection;
});
}

return {
channels,
fetchChannels,
channelsMap,
};
}
26 changes: 26 additions & 0 deletions kolibri/plugins/learn/assets/src/composables/useLanguages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* A composable function containing logic related to languages
*/

import sortBy from 'lodash/sortBy';
import { computed, ref } from 'kolibri.lib.vueCompositionApi';
import { get } from '@vueuse/core';
import plugin_data from 'plugin_data';

const langArray = plugin_data.languages ? plugin_data.languages : [];
const langMap = {};

for (let lang of langArray) {
langMap[lang.id] = lang;
}

// The refs are defined in the outer scope so they can be used as a shared store
const languagesMap = ref(langMap);

export default function useLanguages() {
const languages = computed(() => sortBy(Object.values(get(languagesMap)), 'id'));
return {
languages,
languagesMap,
};
}
184 changes: 184 additions & 0 deletions kolibri/plugins/learn/assets/src/composables/useSearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { get, set } from '@vueuse/core';
import { computed, ref, watch } from 'kolibri.lib.vueCompositionApi';
import { ContentNodeResource } from 'kolibri.resources';
import { AllCategories, NoCategories } from 'kolibri.coreVue.vuex.constants';
import router from 'kolibri.coreVue.router';
import store from 'kolibri.coreVue.vuex.store';
import { normalizeContentNode } from '../modules/coreLearn/utils';

const searchKeys = [
'learning_activities',
'categories',
'learner_needs',
'channels',
'accessibility_labels',
'languages',
'grade_levels',
];

export default function useSearch() {
const route = computed(() => store.state.route);

const searchLoading = ref(true);
const moreLoading = ref(false);
const results = ref([]);
const more = ref(null);
const labels = ref(null);

let descendant;

function setSearchWithinDescendant(d) {
descendant = d;
}

const searchTerms = computed({
get() {
const searchTerms = {};
const query = get(route).query;
for (let key of searchKeys) {
const obj = {};
if (query[key]) {
for (let value of query[key].split(',')) {
obj[value] = true;
}
}
searchTerms[key] = obj;
}
if (query.keywords) {
searchTerms.keywords = query.keywords;
}
return searchTerms;
},
set(value) {
const query = { ...get(route).query };
for (let key of searchKeys) {
const val = Object.keys(value[key] || {})
.filter(Boolean)
.join(',');
if (val.length) {
query[key] = Object.keys(value[key]).join(',');
} else {
delete query[key];
}
}
if (value.keywords && value.keywords.length) {
query.keywords = value.keywords;
} else {
delete query.keywords;
}
// Just catch an error from making a redundant navigation rather
// than try to precalculate this.
router.push({ ...get(route), query }).catch(() => {});
},
});

const displayingSearchResults = computed(() =>
Object.values(get(searchTerms)).some(v => Object.keys(v).length)
);

function search() {
const getParams = {};
if (descendant) {
getParams.tree_id = descendant.tree_id;
getParams.lft__gt = descendant.lft;
getParams.rght__lt = descendant.rght;
}
if (get(displayingSearchResults)) {
getParams.max_results = 25;
const terms = get(searchTerms);
set(searchLoading, true);
for (let key of searchKeys) {
if (key === 'categories') {
if (terms[key][AllCategories]) {
getParams['categories__isnull'] = false;
continue;
} else if (terms[key][NoCategories]) {
getParams['categories__isnull'] = true;
continue;
}
if (key === 'channels' && descendant) {
continue;
}
}
const keys = Object.keys(terms[key]);
if (keys.length) {
getParams[key] = keys;
}
}
if (terms.keywords) {
getParams.keywords = terms.keywords;
}
ContentNodeResource.fetchCollection({ getParams }).then(data => {
set(results, data.results.map(normalizeContentNode));
set(more, data.more);
set(labels, data.labels);
set(searchLoading, false);
});
} else if (descendant) {
getParams.max_results = 1;
ContentNodeResource.fetchCollection({ getParams }).then(data => {
set(labels, data.labels);
set(more, null);
});
} else {
// Clear labels if no search results displaying
// and we're not gathering labels from the descendant
set(more, null);
set(labels, null);
}
}

function searchMore() {
if (get(displayingSearchResults) && get(more) && !get(moreLoading)) {
set(moreLoading, true);
ContentNodeResource.fetchCollection({ getParams: get(more) }).then(data => {
set(results, [...get(results), ...data.results.map(normalizeContentNode)]);
set(more, data.more);
set(labels, data.labels);
set(moreLoading, false);
});
}
}

function removeFilterTag({ value, key }) {
if (key === 'keywords') {
set(searchTerms, {
...get(searchTerms),
[key]: '',
});
} else {
const keyObject = get(searchTerms)[key];
delete keyObject[value];
set(searchTerms, {
...get(searchTerms),
[key]: keyObject,
});
}
}

function clearSearch() {
set(searchTerms, {});
}

function setCategory(category) {
set(searchTerms, { ...this.searchTerms, categories: { [category]: true } });
}

watch(searchTerms, search);

return {
rtibbles marked this conversation as resolved.
Show resolved Hide resolved
searchTerms,
displayingSearchResults,
searchLoading,
moreLoading,
results,
more,
labels,
search,
searchMore,
removeFilterTag,
clearSearch,
setCategory,
setSearchWithinDescendant,
};
}
8 changes: 2 additions & 6 deletions kolibri/plugins/learn/assets/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
export const PageNames = {
ROOT: 'ROOT',
HOME: 'HOME',
TOPICS_ROOT: 'TOPICS_ROOT',
TOPICS_CHANNEL: 'TOPICS_CHANNEL',
TOPICS_TOPIC: 'TOPICS_TOPIC',
TOPICS_TOPIC_SEARCH: 'TOPICS_TOPIC_SEARCH',
TOPICS_CONTENT: 'TOPICS_CONTENT',
LIBRARY: 'LIBRARY',
RECOMMENDED_POPULAR: 'RECOMMENDED_POPULAR',
Expand Down Expand Up @@ -42,10 +41,7 @@ export const pageNameToModuleMap = {
[ClassesPageNames.EXAM_REPORT_VIEWER]: 'examReportViewer',
[ClassesPageNames.LESSON_PLAYLIST]: 'lessonPlaylist',
[ClassesPageNames.LESSON_RESOURCE_VIEWER]: 'lessonPlaylist/resource',
[PageNames.TOPICS_ROOT]: 'topicsRoot',
[PageNames.LIBRARY]: 'library',
[PageNames.TOPICS_CHANNEL]: 'topicsTree',
[PageNames.TOPICS_CONTENT]: 'topicsTree',
[PageNames.TOPICS_TOPIC]: 'topicsTree',
[PageNames.RECOMMENDED_CONTENT]: 'topicsTree',
[PageNames.TOPICS_TOPIC_SEARCH]: 'topicsTree',
};