From 638e992b657ef8d5e770eea72e68a030e03d7ca0 Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Tue, 11 Apr 2023 09:38:15 -0700 Subject: [PATCH 1/5] Properly handle error responses from remote. --- kolibri/core/content/api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kolibri/core/content/api.py b/kolibri/core/content/api.py index 9e00b93ddf..ca516d1e5d 100644 --- a/kolibri/core/content/api.py +++ b/kolibri/core/content/api.py @@ -154,6 +154,9 @@ def _hande_proxied_request(self, request): response = requests.get( remote_url, params=qs, headers=self._get_request_headers(request) ) + if response.status_code == 404: + raise Http404("Remote resource not found") + response.raise_for_status() # If Etag is set on the response we have returned here, any further Etag will not be modified # by the django etag decorator, so this should allow us to transparently proxy the remote etag. try: From 567d32f44d40297a2dbcf0a2f26d614b05465a6f Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Tue, 11 Apr 2023 09:38:41 -0700 Subject: [PATCH 2/5] Don't proxy Accept-Encoding headers as we need to decode on the backend. --- kolibri/core/content/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kolibri/core/content/api.py b/kolibri/core/content/api.py index ca516d1e5d..1741c0fed6 100644 --- a/kolibri/core/content/api.py +++ b/kolibri/core/content/api.py @@ -121,7 +121,8 @@ def _should_proxy_request(self, request): def _get_request_headers(self, request): return { "Accept": request.META.get("HTTP_ACCEPT"), - "Accept-Encoding": request.META.get("HTTP_ACCEPT_ENCODING"), + # Don't proxy client's accept headers as it may include br for brotli + # that we cannot rely on having decompression for available on the server. "Accept-Language": request.META.get("HTTP_ACCEPT_LANGUAGE"), "Content-Type": request.META.get("CONTENT_TYPE"), "If-None-Match": request.META.get("HTTP_IF_NONE_MATCH", ""), From 59970689a9963c634e26509c6e875fd2f100bbf6 Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Tue, 11 Apr 2023 09:39:00 -0700 Subject: [PATCH 3/5] Return fetched device info from the Studio status endpoint. --- kolibri/core/content/api.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/kolibri/core/content/api.py b/kolibri/core/content/api.py index 1741c0fed6..b824a798f2 100644 --- a/kolibri/core/content/api.py +++ b/kolibri/core/content/api.py @@ -1725,6 +1725,9 @@ def kolibri_studio_status(self, request, **kwargs): if resp.status_code == 404: raise requests.ConnectionError("Kolibri Studio URL is incorrect!") else: - return Response({"status": "online"}) + data = resp.json() + data["available"] = True + data["status"] = "online" + return Response(data) except requests.ConnectionError: - return Response({"status": "offline"}) + return Response({"status": "offline", "available": False}) From 2b4db47cd09ba163230fe5afd8d4a51edbbc2621 Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Tue, 11 Apr 2023 10:39:36 -0700 Subject: [PATCH 4/5] Simplify Studio library display handling. --- .../assets/src/composables/useDevices.js | 37 ++++++++++++- .../src/modules/recommended/handlers.js | 45 ++++------------ .../plugins/learn/assets/src/routes/index.js | 4 +- .../src/views/ExploreLibrariesPage/index.vue | 52 ++++--------------- 4 files changed, 57 insertions(+), 81 deletions(-) diff --git a/kolibri/plugins/learn/assets/src/composables/useDevices.js b/kolibri/plugins/learn/assets/src/composables/useDevices.js index 6e632dc256..1b015122cf 100644 --- a/kolibri/plugins/learn/assets/src/composables/useDevices.js +++ b/kolibri/plugins/learn/assets/src/composables/useDevices.js @@ -3,19 +3,52 @@ */ import { computed, getCurrentInstance, ref } from 'kolibri.lib.vueCompositionApi'; -import { NetworkLocationResource } from 'kolibri.resources'; +import { NetworkLocationResource, RemoteChannelResource } from 'kolibri.resources'; import { get, set } from '@vueuse/core'; +import useMinimumKolibriVersion from 'kolibri.coreVue.composables.useMinimumKolibriVersion'; +import { KolibriStudioId } from '../constants'; +import { learnStrings } from '../views/commonLearnStrings'; +import plugin_data from 'plugin_data'; // The refs are defined in the outer scope so they can be used as a shared store const currentDevice = ref(null); +const KolibriStudioDeviceData = { + id: KolibriStudioId, + instance_id: KolibriStudioId, + base_url: plugin_data.studio_baseurl, + get device_name() { + return learnStrings.$tr('kolibriLibrary'); + }, +}; + +const { isMinimumKolibriVersion } = useMinimumKolibriVersion(0, 16, 0); + function fetchDevices() { - return NetworkLocationResource.list().then(devices => { + return Promise.all([ + RemoteChannelResource.getKolibriStudioStatus(), + NetworkLocationResource.list(), + ]).then(([studioResponse, devices]) => { + const studio = studioResponse.data; + devices = devices.filter(device => isMinimumKolibriVersion(device.kolibri_version)); + if (studio.available && isMinimumKolibriVersion(studio.kolibri_version || '0.15.0')) { + return [ + { + ...studio, + ...KolibriStudioDeviceData, + }, + ...devices, + ]; + } return devices; }); } export function setCurrentDevice(id) { + if (id === KolibriStudioId) { + set(currentDevice, KolibriStudioDeviceData); + return Promise.resolve(KolibriStudioDeviceData); + } return NetworkLocationResource.fetchModel({ id }).then(device => { set(currentDevice, device); return device; diff --git a/kolibri/plugins/learn/assets/src/modules/recommended/handlers.js b/kolibri/plugins/learn/assets/src/modules/recommended/handlers.js index c3c5505c29..cfe1373713 100644 --- a/kolibri/plugins/learn/assets/src/modules/recommended/handlers.js +++ b/kolibri/plugins/learn/assets/src/modules/recommended/handlers.js @@ -1,12 +1,11 @@ import { get } from '@vueuse/core'; -import { ContentNodeResource, RemoteChannelResource } from 'kolibri.resources'; +import { ContentNodeResource } from 'kolibri.resources'; import samePageCheckGenerator from 'kolibri.utils.samePageCheckGenerator'; -import { PageNames, KolibriStudioId } from '../../constants'; +import { PageNames } from '../../constants'; import useChannels from '../../composables/useChannels'; import { setCurrentDevice } from '../../composables/useDevices'; import useLearnerResources from '../../composables/useLearnerResources'; import { searchKeys } from '../../composables/useSearch'; -import plugin_data from 'plugin_data'; const { channels, fetchChannels } = useChannels(); @@ -78,39 +77,13 @@ function _showLibrary(store, query, channels, baseurl) { } export function showLibrary(store, query, deviceId = null) { - /** - * ToDo: remove if block. - * Currently the channels & contentnode browser apis in studio - * are not able to load content using the the studio base url. - * Once studio is updated, this function will need to be refactored - * to use the else block code only. - * - * The if block is meant for UI viualization purposes only - * during development - */ - if (deviceId === KolibriStudioId) { - RemoteChannelResource.getKolibriStudioStatus().then(({ data }) => { - if (data.status === 'online') { - RemoteChannelResource.fetchCollection().then(channels => { - //This is a hack to return kolibri channels. - store.commit('SET_ROOT_NODES', channels); - - store.commit('CORE_SET_PAGE_LOADING', false); - store.commit('CORE_SET_ERROR', null); - store.commit('SET_PAGE_NAME', PageNames.LIBRARY); - return Promise.resolve(); - }); - } - }); - } else { - if (deviceId) { - return setCurrentDevice(deviceId).then(device => { - const baseurl = deviceId === KolibriStudioId ? plugin_data.studio_baseurl : device.base_url; - return fetchChannels({ baseurl }).then(channels => { - return _showLibrary(store, query, channels, baseurl); - }); + if (deviceId) { + return setCurrentDevice(deviceId).then(device => { + const baseurl = device.base_url; + return fetchChannels({ baseurl }).then(channels => { + return _showLibrary(store, query, channels, baseurl); }); - } - return _showLibrary(store, query, get(channels)); + }); } + return _showLibrary(store, query, get(channels)); } diff --git a/kolibri/plugins/learn/assets/src/routes/index.js b/kolibri/plugins/learn/assets/src/routes/index.js index b354a3b3b9..2581572e85 100644 --- a/kolibri/plugins/learn/assets/src/routes/index.js +++ b/kolibri/plugins/learn/assets/src/routes/index.js @@ -9,7 +9,7 @@ import { setClasses, setResumableContentNodes } from '../composables/useLearnerR import { setContentNodeProgress } from '../composables/useContentNodeProgress'; import { showTopicsTopic, showTopicsContent } from '../modules/topicsTree/handlers'; import { showLibrary } from '../modules/recommended/handlers'; -import { PageNames, ClassesPageNames } from '../constants'; +import { PageNames, ClassesPageNames, KolibriStudioId } from '../constants'; import LibraryPage from '../views/LibraryPage'; import HomePage from '../views/HomePage'; import TopicsPage from '../views/TopicsPage'; @@ -45,7 +45,7 @@ function hydrateHomePage() { }); } -const optionalDeviceIdPathSegment = '/:deviceId([a-f0-9]{32}|kolibri-studio)?'; +const optionalDeviceIdPathSegment = `/:deviceId([a-f0-9]{32}|${KolibriStudioId})?`; export default [ { diff --git a/kolibri/plugins/learn/assets/src/views/ExploreLibrariesPage/index.vue b/kolibri/plugins/learn/assets/src/views/ExploreLibrariesPage/index.vue index a4cd0f251f..ead9689689 100644 --- a/kolibri/plugins/learn/assets/src/views/ExploreLibrariesPage/index.vue +++ b/kolibri/plugins/learn/assets/src/views/ExploreLibrariesPage/index.vue @@ -16,17 +16,6 @@ {{ $tr('showingLibraries') }}

-
@@ -54,7 +43,7 @@ :deviceIcon="getDeviceIcon(device)" :channels="device.channels" :totalChannels="device['total_channels']" - :pinIcon="getPinIcon(isPinned(device['instance_id']))" + :pinIcon="getPinIcon(false)" @togglePin="handlePinToggle" /> 0 && this.moreDevices.length < this.unpinnedDevices?.length ); }, - kolibriStudioId() { - return KolibriStudioId; - }, networkDevicesWithChannels() { - return this.networkDevices.filter(device => device.channels?.length > 0); + return this.networkDevices.filter( + device => + device.channels?.length > 0 && + (device.instance_id !== KolibriStudioId || this.isSuperuser) + ); }, pageHeaderStyle() { return { @@ -150,12 +136,12 @@ }, pinnedDevices() { return this.networkDevicesWithChannels.filter(netdev => { - return this.usersPinsDeviceIds.includes(netdev.instance_id); + return ( + this.usersPinsDeviceIds.includes(netdev.instance_id) || + netdev.instance_id === KolibriStudioId + ); }); }, - showKolibriLibrary() { - return this.isSuperuser && this.isKolibriLibraryLoaded; - }, unpinnedDevices() { return this.networkDevicesWithChannels.filter(netdev => { return !this.usersPinsDeviceIds.includes(netdev.instance_id); @@ -171,19 +157,6 @@ }); }); - RemoteChannelResource.getKolibriStudioStatus().then(({ data }) => { - if (data.status === 'online') { - RemoteChannelResource.fetchCollection() - .then(channels => { - this.isKolibriLibraryLoaded = true; - this.kolibriLibraryChannels = channels.slice(0, 4); - this.totalChannels = channels.length; - }) - .catch(() => { - this.isKolibriLibraryLoaded = true; - }); - } - }); this.fetchDevices().then(devices => { this.networkDevices = devices; for (const device of this.networkDevices) { @@ -199,9 +172,6 @@ }); }, methods: { - isPinned(instance_id) { - return this.usersPinsDeviceIds.includes(instance_id); - }, createPin(instance_id) { return this.createPinForUser(instance_id).then(response => { const id = response.id; From 5001d3544b7985ab03375581285a4a4642c9177b Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Tue, 11 Apr 2023 10:40:06 -0700 Subject: [PATCH 5/5] Add navigation guards for explore libraries page to prevent access when not logged in. --- kolibri/plugins/learn/assets/src/routes/index.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/kolibri/plugins/learn/assets/src/routes/index.js b/kolibri/plugins/learn/assets/src/routes/index.js index 2581572e85..f1e2f922c9 100644 --- a/kolibri/plugins/learn/assets/src/routes/index.js +++ b/kolibri/plugins/learn/assets/src/routes/index.js @@ -205,6 +205,15 @@ export default [ name: PageNames.EXPLORE_LIBRARIES, path: '/explore_libraries', component: ExploreLibrariesPage, + handler: () => { + if (!get(isUserLoggedIn)) { + router.replace({ name: PageNames.LIBRARY }); + return; + } + if (unassignedContentGuard()) { + return unassignedContentGuard(); + } + }, }, { path: '*',