+
@@ -48,28 +56,25 @@
-
-
-
diff --git a/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/select-drive-modal.vue b/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/select-drive-modal.vue
new file mode 100644
index 00000000000..20b069c9bb1
--- /dev/null
+++ b/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/select-drive-modal.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+ {{ $tr('findingLocalDrives') }}
+
+
+
+
+ {{ $tr('problemFindingLocalDrives') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/select-import-source-modal.vue b/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/select-import-source-modal.vue
new file mode 100644
index 00000000000..e50f05b55d4
--- /dev/null
+++ b/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/select-import-source-modal.vue
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/wizard-export.vue b/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/wizard-export.vue
deleted file mode 100644
index 930382a63ce..00000000000
--- a/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/wizard-export.vue
+++ /dev/null
@@ -1,160 +0,0 @@
-
-
-
-
-
-
-
- {{ $tr('exportPrompt', { numChannels: channelList.length, exportSize }) }}
-
-
selectedDrive = driveId"
- />
-
-
-
-
-
-
-
-
- {{ wizardState.error }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/wizard-import-local.vue b/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/wizard-import-local.vue
deleted file mode 100644
index 72e646d01a7..00000000000
--- a/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/wizard-import-local.vue
+++ /dev/null
@@ -1,148 +0,0 @@
-
-
-
-
-
-
- selectedDrive = driveId"
- />
-
-
-
-
-
-
-
-
- {{ wizardState.error }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/wizard-import-network.vue b/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/wizard-import-network.vue
deleted file mode 100644
index 38b6bf68d9e..00000000000
--- a/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/wizard-import-network.vue
+++ /dev/null
@@ -1,131 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/wizard-import-source.vue b/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/wizard-import-source.vue
deleted file mode 100644
index 7b27bf94474..00000000000
--- a/kolibri/plugins/management/assets/src/device_management/views/manage-content-page/wizards/wizard-import-source.vue
+++ /dev/null
@@ -1,82 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/kolibri/plugins/management/assets/src/device_management/views/select-content-page/channel-contents-summary.vue b/kolibri/plugins/management/assets/src/device_management/views/select-content-page/channel-contents-summary.vue
new file mode 100644
index 00000000000..d771b19a1db
--- /dev/null
+++ b/kolibri/plugins/management/assets/src/device_management/views/select-content-page/channel-contents-summary.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+ {{ $tr('totalSizeRow') }} |
+ {{ $tr('resourceCount', { count: channel.total_resources || 0 }) }} |
+ {{ bytesForHumans(channel.total_file_size || 0) }} |
+
+
+
+ {{ $tr('onDeviceRow') }} |
+ {{ $tr('resourceCount', { count: channelOnDevice.on_device_resources || 0 }) }} |
+ {{ bytesForHumans(channelOnDevice.on_device_file_size || 0) }} |
+
+
+
+
+
+
+
+
+
+
+
diff --git a/kolibri/plugins/management/assets/src/device_management/views/select-content-page/content-node-row.vue b/kolibri/plugins/management/assets/src/device_management/views/select-content-page/content-node-row.vue
new file mode 100644
index 00000000000..d706a84ff92
--- /dev/null
+++ b/kolibri/plugins/management/assets/src/device_management/views/select-content-page/content-node-row.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+ {{ node.title }}
+
+
+
+
+ {{ message }}
+
+
+
+
+
+
+
+
+
+
diff --git a/kolibri/plugins/management/assets/src/device_management/views/select-content-page/content-tree-viewer.vue b/kolibri/plugins/management/assets/src/device_management/views/select-content-page/content-tree-viewer.vue
new file mode 100644
index 00000000000..9c590e7b7ed
--- /dev/null
+++ b/kolibri/plugins/management/assets/src/device_management/views/select-content-page/content-tree-viewer.vue
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+
+
+
+ {{ $tr('topicHasNoContents') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/kolibri/plugins/management/assets/src/device_management/views/select-content-page/index.vue b/kolibri/plugins/management/assets/src/device_management/views/select-content-page/index.vue
new file mode 100644
index 00000000000..62dade0d08f
--- /dev/null
+++ b/kolibri/plugins/management/assets/src/device_management/views/select-content-page/index.vue
@@ -0,0 +1,218 @@
+
+
+
+
+
+
+
+
+
+ {{ $tr('newVersionAvailableNotification') }}
+
+
+
+
+
+
+ {{ $tr('newVersionAvailable', { version: channel.version }) }}
+
+
+
+ {{ $tr('channelUpToDate') }}
+
+
+
+
+
+
+ {{ $tr('problemFetchingChannel') }}
+
+
+
+
+ {{ $tr('problemTransferringContents') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/kolibri/plugins/management/assets/src/device_management/views/select-content-page/selected-resources-size.vue b/kolibri/plugins/management/assets/src/device_management/views/select-content-page/selected-resources-size.vue
new file mode 100644
index 00000000000..eed50557154
--- /dev/null
+++ b/kolibri/plugins/management/assets/src/device_management/views/select-content-page/selected-resources-size.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
+ {{ $tr('chooseContentToImport') }}
+
+
+ {{ $tr('chooseContentToExport') }}
+
+
+
+
+ {{ $tr('resourcesSelected', { fileSize: bytesForHumans(fileSize), resources: resourceCount }) }}
+
+
+
+
+
+
+ {{ $tr('remainingSpace', { space: bytesForHumans(remainingSpaceAfterTransfer) }) }}
+
+
+
+ {{ $tr('notEnoughSpace') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/kolibri/plugins/management/assets/src/device_management/views/select-content-page/treeViewUtils.js b/kolibri/plugins/management/assets/src/device_management/views/select-content-page/treeViewUtils.js
new file mode 100644
index 00000000000..62e86ec8434
--- /dev/null
+++ b/kolibri/plugins/management/assets/src/device_management/views/select-content-page/treeViewUtils.js
@@ -0,0 +1,162 @@
+import find from 'lodash/find';
+import flip from 'lodash/flip';
+import sumBy from 'lodash/fp/sumBy';
+import { createTranslator } from 'kolibri.utils.i18n';
+import { updateTopicLinkObject } from '../../wizardTransitionRoutes';
+
+const translator = createTranslator('treeViewRowMessages', {
+ alreadyOnYourDevice: 'Already on your device',
+ fractionOfResourcesOnDevice:
+ '{onDevice, number, useGrouping} of {total, number, useGrouping} resources on your device',
+ resourcesSelected:
+ '{total, number, useGrouping} {total, plural, one {resource} other {resources}} selected',
+ fractionOfResourcesSelected:
+ '{selected, number, useGrouping} of {total, number, useGrouping} {total, plural, one {resource} other {resources}} selected',
+ noTitle: 'No title',
+});
+
+export const CheckboxTypes = {
+ CHECKED: 'checked',
+ UNCHECKED: 'unchecked',
+ INDETERMINATE: 'indeterminate',
+};
+
+// One-liner utilities for annotateNode
+// isAncestorOf(a, b) is truthy if a is an ancestor of b
+const isAncestorOf = (a, b) => find(b.path, { pk: a.pk });
+const isDescedantOf = flip(isAncestorOf);
+const sumTotalResources = sumBy('total_resources');
+const sumOnDeviceResources = sumBy('on_device_resources');
+
+/**
+ * Takes a Node, plus contextual data from store, then annotates them with info
+ * needed to correctly display it on tree view.
+ *
+ * @param node {Node}
+ * @param selectedNodes {SelectedNodes}
+ * @param selectedNodes.omitted {Array
}
+ * @param selectedNodes.included {Array}
+ * @returns {AnnotatedNode} - annotations are message, disabled, and checkboxType
+ *
+ */
+export function annotateNode(node, selectedNodes) {
+ const { on_device_resources, total_resources } = node;
+ const isIncluded = find(selectedNodes.included, { pk: node.pk });
+ const isOmitted = find(selectedNodes.omitted, { pk: node.pk });
+ const ancestorIsIncluded = find(selectedNodes.included, iNode => isAncestorOf(iNode, node));
+ const ancestorIsOmitted = find(selectedNodes.omitted, oNode => isAncestorOf(oNode, node));
+
+ // Completely on device -> DISABLED
+ if (on_device_resources === total_resources) {
+ return {
+ ...node,
+ message: translator.$tr('alreadyOnYourDevice'),
+ disabled: true,
+ checkboxType: CheckboxTypes.CHECKED,
+ };
+ }
+
+ if (!(isOmitted || ancestorIsOmitted) && (isIncluded || ancestorIsIncluded)) {
+ const omittedDescendants = selectedNodes.omitted.filter(oNode => isDescedantOf(oNode, node));
+
+ // If any descendants are omitted -> UNCHECKED or INDETERMINATE
+ if (omittedDescendants.length > 0) {
+ const omittedResources =
+ (sumTotalResources(omittedDescendants) || 0) -
+ (sumOnDeviceResources(omittedDescendants) || 0);
+
+ // All descendants are omitted -> UNCHECKED
+ if (omittedResources === total_resources - on_device_resources) {
+ return {
+ ...node,
+ message: '',
+ disabled: false,
+ checkboxType: CheckboxTypes.UNCHECKED,
+ };
+ }
+
+ // Some (but not all) descendants are omitted -> INDETERMINATE
+ return {
+ ...node,
+ message: translator.$tr('fractionOfResourcesSelected', {
+ selected: total_resources - sumTotalResources(omittedDescendants),
+ total: total_resources,
+ }),
+ disabled: false,
+ checkboxType: CheckboxTypes.INDETERMINATE,
+ };
+ }
+
+ // Completely selected -> CHECKED
+ return {
+ ...node,
+ message: translator.$tr('resourcesSelected', { total: total_resources }),
+ disabled: false,
+ checkboxType: CheckboxTypes.CHECKED,
+ };
+ }
+
+ const fullyIncludedDescendants = selectedNodes.included
+ .filter(iNode => isDescedantOf(iNode, node))
+ .filter(iNode => !selectedNodes.omitted.find(oNode => isDescedantOf(oNode, iNode)));
+
+ if (fullyIncludedDescendants.length > 0) {
+ const fullIncludedDescendantsResources =
+ sumTotalResources(fullyIncludedDescendants) - sumOnDeviceResources(fullyIncludedDescendants);
+
+ // Node is not selected, has all children selected -> CHECKED
+ if (fullIncludedDescendantsResources === total_resources - on_device_resources) {
+ return {
+ ...node,
+ message: translator.$tr('resourcesSelected', { total: total_resources }),
+ disabled: false,
+ checkboxType: CheckboxTypes.CHECKED,
+ };
+ }
+
+ // Node is not selected, has some children selected -> INDETERMINATE
+ return {
+ ...node,
+ message: translator.$tr('fractionOfResourcesSelected', {
+ selected: fullIncludedDescendantsResources,
+ total: total_resources,
+ }),
+ disabled: false,
+ checkboxType: CheckboxTypes.INDETERMINATE,
+ };
+ }
+
+ if (on_device_resources > 0) {
+ // Node has some (but not all) resources on device -> UNCHECKED (w/ message).
+ // Node with all resources on device handled at top of this function.
+ return {
+ ...node,
+ message: translator.$tr('fractionOfResourcesOnDevice', {
+ onDevice: on_device_resources,
+ total: total_resources,
+ }),
+ disabled: false,
+ checkboxType: CheckboxTypes.UNCHECKED,
+ };
+ }
+
+ // Node is not selected, has no children, is not on device -> UNCHECKED
+ return {
+ ...node,
+ message: '',
+ disabled: false,
+ checkboxType: CheckboxTypes.UNCHECKED,
+ };
+}
+
+/**
+ * Takes an array of breadcrumb { id, title } objects in state, and converts them
+ * into a form that can be used in k-breadcrumbs props.items { text, link: LinkObject }.
+ *
+ */
+export function transformBreadrumb(node) {
+ return {
+ text: node.title || translator.$tr('noTitle'),
+ link: updateTopicLinkObject(node),
+ };
+}
diff --git a/kolibri/plugins/management/assets/src/device_management/wizardTransitionRoutes.js b/kolibri/plugins/management/assets/src/device_management/wizardTransitionRoutes.js
new file mode 100644
index 00000000000..762b1d3b92f
--- /dev/null
+++ b/kolibri/plugins/management/assets/src/device_management/wizardTransitionRoutes.js
@@ -0,0 +1,80 @@
+import get from 'lodash/get';
+import router from 'kolibri.coreVue.router';
+import store from 'kolibri.coreVue.vuex.store';
+import { ContentWizardPages } from './constants';
+import { transitionWizardPage } from './state/actions/contentWizardActions';
+import { updateTreeViewTopic } from './state/actions/selectContentActions';
+
+export const WizardTransitions = {
+ GOTO_TOPIC_TREEVIEW: 'GOTO_TOPIC_TREEVIEW',
+ GOTO_AVAILABLE_CHANNELS_PAGE: 'GOTO_AVAILABLE_CHANNELS_PAGE',
+};
+
+export function updateTopicLinkObject(node) {
+ return {
+ name: WizardTransitions.GOTO_TOPIC_TREEVIEW,
+ query: {
+ pk: node.pk,
+ },
+ params: {
+ node,
+ },
+ };
+}
+
+// To update the treeview topic programatically
+export function navigateToTopicUrl(node) {
+ router.push(updateTopicLinkObject(node));
+}
+
+// Special fake routes so we can use router-link-dependant components inside
+// the wizard modals/immersive-full-screen
+export default [
+ {
+ name: WizardTransitions.GOTO_TOPIC_TREEVIEW,
+ path: '/content/wizard/topic',
+ handler: ({ params, query }) => {
+ let nextNode;
+ // Redirect to /content if coming into URL directly without initiating workflow
+ if (
+ get(store.state.pageState, 'wizardState.pageName') !== ContentWizardPages.SELECT_CONTENT
+ ) {
+ return router.replace('/content');
+ }
+ if (!params.node) {
+ nextNode = {
+ // Works fine without title at the moment.
+ path: store.state.pageState.wizardState.pathCache[params.pk],
+ pk: query.pk,
+ };
+ } else {
+ nextNode = params.node;
+ }
+ return updateTreeViewTopic(store, nextNode);
+ },
+ },
+ {
+ name: WizardTransitions.GOTO_AVAILABLE_CHANNELS_PAGE,
+ path: '/content/wizard/availablechannels',
+ handler: () => {
+ if (!get(store.state.pageState, 'wizardState.pageName')) {
+ return router.replace('/content');
+ }
+ // This action should maintain most of the state, except for nodesForTransfer
+ store.dispatch('REPLACE_INCLUDE_LIST', []);
+ store.dispatch('REPLACE_OMIT_LIST', []);
+ // TODO adjust tests to account for this flow (they assume fresh start from /content).
+ return store.dispatch('SET_WIZARD_PAGENAME', ContentWizardPages.AVAILABLE_CHANNELS);
+ },
+ },
+ {
+ name: 'wizardtransition',
+ // Wizard transitions don't change the URL
+ path: '',
+ handler: ({ params }) => {
+ if (params.transition === 'cancel') {
+ transitionWizardPage(store, 'cancel');
+ }
+ },
+ },
+];
diff --git a/kolibri/plugins/style_guide/assets/src/views/content/breadcrumbs/index.vue b/kolibri/plugins/style_guide/assets/src/views/content/breadcrumbs/index.vue
index 23be0cb7a29..966d15dd6f2 100644
--- a/kolibri/plugins/style_guide/assets/src/views/content/breadcrumbs/index.vue
+++ b/kolibri/plugins/style_guide/assets/src/views/content/breadcrumbs/index.vue
@@ -1,6 +1,6 @@
-
+
Topic tree breadcrumbs
diff --git a/kolibri/tasks/api.py b/kolibri/tasks/api.py
index bd31f8fb890..3f6837ccae9 100644
--- a/kolibri/tasks/api.py
+++ b/kolibri/tasks/api.py
@@ -27,6 +27,7 @@
logging = logger.getLogger(__name__)
+
NETWORK_ERROR_STRING = _("There was a network error.")
DISK_IO_ERROR_STRING = _("There was a disk access error.")
@@ -69,7 +70,14 @@ def startremotechannelimport(self, request):
"started_by": request.user.pk,
}
- job_id = get_client().schedule(call_command, "importchannel", "network", channel_id, baseurl=baseurl, extra_metadata=job_metadata,)
+ job_id = get_client().schedule(
+ call_command,
+ "importchannel",
+ "network",
+ channel_id,
+ baseurl=baseurl,
+ extra_metadata=job_metadata,
+ )
resp = _job_to_response(get_client().status(job_id))
return Response(resp)