diff --git a/CHANGELOG.md b/CHANGELOG.md index 40a25dc3ce..46d5fa9ffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ List of the most important changes for each release. +## 0.17.3 + +### Changed +- Improve learn folder contents display by @rtibbles in https://github.com/learningequality/kolibri/pull/12737 +- Update KDS theme token by @AllanOXDi in https://github.com/learningequality/kolibri/pull/12742 +- Final token updates by @marcellamaki in https://github.com/learningequality/kolibri/pull/12754 + +### Added +- Add Fulfulde translations by @radinamatic in https://github.com/learningequality/kolibri/pull/12738 +- Update morango for conditional indexes on store to improve deserialization performance. by @rtibbles in https://github.com/learningequality/kolibri/pull/12747 + +### Fixed +- Settings is not correct after tzlocal update by @jredrejo in https://github.com/learningequality/kolibri/pull/12683 +- Add missing `indeterminate` states on Select All checkboxes by @nucleogenesis in https://github.com/learningequality/kolibri/pull/12587 +- Fix import channel in Postgresql by @jredrejo in https://github.com/learningequality/kolibri/pull/12709 +- Branding tweaks by @rtibbles in https://github.com/learningequality/kolibri/pull/12736 +- Tweaks to CLI and message extraction utility function by @rtibbles in https://github.com/learningequality/kolibri/pull/12320 +- Delete resource from everywhere when force_delete is selected by @thesujai in https://github.com/learningequality/kolibri/pull/12680 +- Dont delete the entire channel when deleting a single content by @thesujai in https://github.com/learningequality/kolibri/pull/12740 +- Prevent access to undefined AttemptLogs while looking at reports by @LianaHarris360 in https://github.com/learningequality/kolibri/pull/12723 +- Update StorageNotification.vue to prevent undefined access if device plugin is disabled by @rtibbles in https://github.com/learningequality/kolibri/pull/12724 +- Update favicon to be available when the default theme is disabled by @marcellamaki in https://github.com/learningequality/kolibri/pull/12760 +- Fix on my own merge account KCheckbox issue by @AlexVelezLl in https://github.com/learningequality/kolibri/pull/12761 + ## 0.17.2 ### Changed diff --git a/kolibri/core/assets/src/core-app/apiSpec.js b/kolibri/core/assets/src/core-app/apiSpec.js index 10486bc230..e74ebb160f 100644 --- a/kolibri/core/assets/src/core-app/apiSpec.js +++ b/kolibri/core/assets/src/core-app/apiSpec.js @@ -70,7 +70,6 @@ import Draggable from '../views/sortable/Draggable'; import DragHandle from '../views/sortable/DragHandle'; import DragContainer from '../views/sortable/DragContainer'; import DragSortWidget from '../views/sortable/DragSortWidget'; -import FocusTrap from '../views/FocusTrap'; import BottomAppBar from '../views/BottomAppBar'; import BaseToolbar from '../views/BaseToolbar'; import GenderSelect from '../views/userAccounts/GenderSelect'; @@ -169,7 +168,6 @@ export default { DragHandle, DragContainer, DragSortWidget, - FocusTrap, BottomAppBar, BaseToolbar, GenderSelect, diff --git a/kolibri/core/assets/src/views/CoreMenu/index.vue b/kolibri/core/assets/src/views/CoreMenu/index.vue index 4fc6202687..c0f6a8986f 100644 --- a/kolibri/core/assets/src/views/CoreMenu/index.vue +++ b/kolibri/core/assets/src/views/CoreMenu/index.vue @@ -16,7 +16,7 @@ - - +
import last from 'lodash/last'; - import FocusTrap from 'kolibri.coreVue.components.FocusTrap'; export default { name: 'CoreMenu', - components: { - FocusTrap, - }, props: { // Whether to show if links are currently active showActive: { diff --git a/kolibri/core/assets/src/views/FocusTrap.vue b/kolibri/core/assets/src/views/FocusTrap.vue deleted file mode 100644 index 28b9176a3d..0000000000 --- a/kolibri/core/assets/src/views/FocusTrap.vue +++ /dev/null @@ -1,78 +0,0 @@ - - - - diff --git a/kolibri/core/assets/src/views/SideNav.vue b/kolibri/core/assets/src/views/SideNav.vue index 5ef65c04d7..ac622d2b75 100644 --- a/kolibri/core/assets/src/views/SideNav.vue +++ b/kolibri/core/assets/src/views/SideNav.vue @@ -17,7 +17,7 @@ backgroundColor: $themeTokens.surface, }" > - @@ -214,7 +214,7 @@ :style="{ color: $themeTokens.text }" >{{ sideNavTitleText }}
- + @@ -269,7 +269,6 @@ import TotalPoints from './TotalPoints.vue'; import SyncStatusDisplay from './SyncStatusDisplay'; import SideNavDivider from './SideNavDivider'; - import FocusTrap from './FocusTrap.vue'; import BottomNavigationBar from './BottomNavigationBar'; // Explicit ordered list of roles for nav item sorting @@ -293,7 +292,6 @@ SyncStatusDisplay, SideNavDivider, PrivacyInfoModal, - FocusTrap, TotalPoints, LanguageSwitcherModal, BottomNavigationBar, diff --git a/kolibri/core/assets/src/views/__tests__/FocusTrap.spec.js b/kolibri/core/assets/src/views/__tests__/FocusTrap.spec.js deleted file mode 100644 index 4efdc94224..0000000000 --- a/kolibri/core/assets/src/views/__tests__/FocusTrap.spec.js +++ /dev/null @@ -1,58 +0,0 @@ -import { render } from '@testing-library/vue'; -import userEvent from '@testing-library/user-event'; -import FocusTrap from '../FocusTrap.vue'; - -const renderComponent = (props = {}) => { - return render(FocusTrap, { - props: { - disabled: false, - ...props, - }, - }); -}; - -describe('FocusTrap', () => { - it('should emit the "shouldFocusFirstEl" element when the tab key is pressed once', async () => { - const { emitted } = renderComponent(); - - await userEvent.tab(); - expect(emitted()).toHaveProperty('shouldFocusFirstEl'); - expect(emitted().shouldFocusFirstEl.length).toBe(1); - }); - - it("should trap the focus and emit 'shouldFocusFirstEl' if the last focusable element is focused and we focus the next element", async () => { - const { emitted } = renderComponent(); - - await userEvent.tab(); - await userEvent.tab(); - - expect(emitted()).toHaveProperty('shouldFocusFirstEl'); - expect(emitted().shouldFocusFirstEl.length).toBe(2); - }); - - it('should trap the focus and emit "shouldFocusLastEl" when the first element is focused and we focus the previous element', async () => { - const { emitted } = renderComponent(); - - await userEvent.tab(); - await userEvent.tab(); - - // Shift + Tab is used to focus on the initial element again - await userEvent.tab({ shift: true }); - - expect(emitted()).toHaveProperty('shouldFocusLastEl'); - expect(emitted().shouldFocusLastEl.length).toBe(1); - }); - - it("should not trap focus when 'disabled' prop is set to true", async () => { - const { emitted } = renderComponent({ disabled: true }); - - await userEvent.tab(); - expect(emitted()).not.toHaveProperty('shouldFocusFirstEl'); - - await userEvent.tab(); - expect(emitted()).not.toHaveProperty('shouldFocusFirstEl'); - - await userEvent.tab({ shift: true }); - expect(emitted()).not.toHaveProperty('shouldFocusLastEl'); - }); -}); diff --git a/kolibri/core/assets/src/views/language-switcher/LanguageSwitcherModal.vue b/kolibri/core/assets/src/views/language-switcher/LanguageSwitcherModal.vue index e1c900b375..7e2ded4b07 100644 --- a/kolibri/core/assets/src/views/language-switcher/LanguageSwitcherModal.vue +++ b/kolibri/core/assets/src/views/language-switcher/LanguageSwitcherModal.vue @@ -1,6 +1,6 @@ @@ -46,12 +46,10 @@ import { currentLanguage } from 'kolibri.utils.i18n'; import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow'; import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings'; - import FocusTrap from 'kolibri.coreVue.components.FocusTrap'; import languageSwitcherMixin from './mixin'; export default { name: 'LanguageSwitcherModal', - components: { FocusTrap }, mixins: [commonCoreStrings, languageSwitcherMixin], setup() { const { windowIsSmall } = useKResponsiveWindow(); diff --git a/kolibri/core/logger/csv_export.py b/kolibri/core/logger/csv_export.py index d63f60acf4..31a529142b 100644 --- a/kolibri/core/logger/csv_export.py +++ b/kolibri/core/logger/csv_export.py @@ -4,7 +4,6 @@ import math import os from collections import OrderedDict -from functools import partial from dateutil import parser from django.core.cache import cache @@ -27,8 +26,39 @@ "summary": "{}_{}_content_summary_logs_from_{}_to_{}.csv", } +CACHE_TIMEOUT = 60 * 10 -def cache_channel_name(obj): + +def add_content_to_cache(content_id, **kwargs): + title_key = "{content_id}_ContentNode_title".format(content_id=content_id) + ancestors_key = "{content_id}_ContentNode_ancestors".format(content_id=content_id) + + cache.set(title_key, kwargs.get("title", ""), CACHE_TIMEOUT) + cache.set(ancestors_key, kwargs.get("ancestors", []), CACHE_TIMEOUT) + + +def get_cached_content_data(content_id): + title_key = f"{content_id}_ContentNode_title" + ancestors_key = f"{content_id}_ContentNode_ancestors" + + title = cache.get(title_key) + ancestors = cache.get(ancestors_key) + + if title is None or ancestors is None: + node = ContentNode.objects.filter(content_id=content_id).first() + if node: + title = node.title + ancestors = node.ancestors + else: + title = "" + ancestors = [] + + add_content_to_cache(content_id, title=title, ancestors=ancestors) + + return title, ancestors + + +def get_cached_channel_name(obj): channel_id = obj["channel_id"] key = "{id}_ChannelMetadata_name".format(id=channel_id) channel_name = cache.get(key) @@ -37,27 +67,24 @@ def cache_channel_name(obj): channel_name = ChannelMetadata.objects.get(id=channel_id) except ChannelMetadata.DoesNotExist: channel_name = "" - cache.set(key, channel_name, 60 * 10) + cache.set(key, channel_name, CACHE_TIMEOUT) return channel_name -def cache_content_title(obj): +def get_cached_content_title(obj): content_id = obj["content_id"] - key = "{id}_ContentNode_title".format(id=content_id) - title = cache.get(key) - if title is None: - node = ContentNode.objects.filter(content_id=content_id).first() - if node: - title = node.title - else: - title = "" - cache.set(key, title, 60 * 10) + title, _ = get_cached_content_data(content_id) return title +def get_cached_ancestors(content_id): + _, ancestors = get_cached_content_data(content_id) + return ancestors + + mappings = { - "channel_name": cache_channel_name, - "content_title": cache_content_title, + "channel_name": get_cached_channel_name, + "content_title": get_cached_content_title, "time_spent": lambda x: "{:.1f}".format(round(x["time_spent"], 1)), "progress": lambda x: "{:.4f}".format(math.floor(x["progress"] * 10000.0) / 10000), } @@ -103,7 +130,39 @@ def cache_content_title(obj): ) ) -map_object = partial(output_mapper, labels=labels, output_mappings=mappings) + +def get_max_ancestor_depth(): + """Returns one less than the maximum depth of the ancestors of all content nodes""" + max_depth = 0 + content_ids = ContentSummaryLog.objects.values_list("content_id", flat=True) + nodes = ContentNode.objects.filter(content_id__in=content_ids).only( + "content_id", "title", "ancestors" + ) + for node in nodes: + ancestors = node.ancestors + # cache it here so the retireival while adding ancestors info into csv is faster + add_content_to_cache(node.content_id, title=node.title, ancestors=ancestors) + max_depth = max(max_depth, len(ancestors)) + return max_depth - 1 + + +def add_ancestors_info(row, ancestors, max_depth): + ancestors = ancestors[1:] + row.update( + { + f"Topic level {level + 1}": ancestors[level]["title"] + if level < len(ancestors) + else "" + for level in range(max_depth) + } + ) + + +def map_object(item): + mapped_item = output_mapper(item, labels=labels, output_mappings=mappings) + ancestors = get_cached_ancestors(item["content_id"]) + add_ancestors_info(mapped_item, ancestors, get_max_ancestor_depth()) + return mapped_item classes_info = { @@ -171,11 +230,21 @@ def csv_file_generator( queryset = queryset.filter(start_timestamp__lte=end) # Exclude completion timestamp for the sessionlog CSV - header_labels = tuple( + header_labels = list( label for label in labels.values() if log_type == "summary" or label != labels["completion_timestamp"] ) + # len of topic headers should be equal to the max depth of the content node + topic_headers = [ + (f"Topic level {i+1}", _(f"Topic level {i+1}")) + for i in range(get_max_ancestor_depth()) + ] + + content_id_index = header_labels.index(labels["content_id"]) + header_labels[content_id_index:content_id_index] = [ + label for _, label in topic_headers + ] csv_file = open_csv_for_writing(filepath) diff --git a/kolibri/core/package.json b/kolibri/core/package.json index ed16db8fcc..875aea33a9 100644 --- a/kolibri/core/package.json +++ b/kolibri/core/package.json @@ -28,7 +28,7 @@ "path-to-regexp": "1.8.0", "screenfull": "^5.2.0", "tinycolor2": "^1.6.0", - "ua-parser-js": "^1.0.38", + "ua-parser-js": "^1.0.39", "vue": "2.6.14", "vue-intl": "3.1.0", "vue-meta": "^2.4.0", diff --git a/kolibri/plugins/default_theme/static/assets/default_theme/logo.ico b/kolibri/core/static/assets/favicons/logo.ico similarity index 100% rename from kolibri/plugins/default_theme/static/assets/default_theme/logo.ico rename to kolibri/core/static/assets/favicons/logo.ico diff --git a/kolibri/core/templatetags/core_tags.py b/kolibri/core/templatetags/core_tags.py index dc385bd08e..3ba6f00020 100644 --- a/kolibri/core/templatetags/core_tags.py +++ b/kolibri/core/templatetags/core_tags.py @@ -65,7 +65,9 @@ def theme_favicon(): # Choose the first available .ico file. It's unlikely there's more than # one specified in the theme. - favicon_url = favicon_urls[0] if favicon_urls else static("assets/logo.ico") + favicon_url = ( + favicon_urls[0] if favicon_urls else static("assets/favicons/logo.ico") + ) return format_html('', favicon_url) diff --git a/kolibri/plugins/coach/assets/src/views/common/assignments/IndividualLearnerSelector.vue b/kolibri/plugins/coach/assets/src/views/common/assignments/IndividualLearnerSelector.vue index 805edcb718..b070400003 100644 --- a/kolibri/plugins/coach/assets/src/views/common/assignments/IndividualLearnerSelector.vue +++ b/kolibri/plugins/coach/assets/src/views/common/assignments/IndividualLearnerSelector.vue @@ -32,7 +32,7 @@