diff --git a/frontend_build/src/webpack.config.base.js b/frontend_build/src/webpack.config.base.js index 3e5ce395e32..649e976b82e 100644 --- a/frontend_build/src/webpack.config.base.js +++ b/frontend_build/src/webpack.config.base.js @@ -61,12 +61,6 @@ var config = { options: { preserveWhitespace: false, loaders: { - js: { - loader: 'buble-loader', - query: { - objectAssign: 'Object.assign', - }, - }, stylus: ExtractTextPlugin.extract({ allChunks: true, use: vueStylusLoaders, @@ -76,6 +70,9 @@ var config = { use: vueSassLoaders, }), }, + buble: { + objectAssign: 'Object.assign', + }, // handles , , , and svg inlining preLoaders: { html: 'svg-icon-inline-loader' }, }, diff --git a/karma_config/testUtils.js b/karma_config/testUtils.js index 5f0b7068cd1..862f20cbd45 100644 --- a/karma_config/testUtils.js +++ b/karma_config/testUtils.js @@ -21,6 +21,7 @@ class MockResource { } else { fetchable.fetch = () => Promise.resolve(payload); } + sinon.spy(fetchable, 'fetch'); return fetchable; } @@ -35,7 +36,9 @@ class MockResource { } __getModelFetchReturns(payload, willReject = false) { - this.getModel.returns(this.__getFetchable(payload, willReject)); + const fetchable = this.__getFetchable(payload, willReject); + this.getModel.returns(fetchable); + return fetchable; } __getModelSaveReturns(payload, willReject = false) { @@ -45,7 +48,9 @@ class MockResource { } __getCollectionFetchReturns(payload, willReject = false) { - this.getCollection.returns(this.__getFetchable(payload, willReject)); + const fetchable = this.__getFetchable(payload, willReject); + this.getCollection.returns(fetchable); + return fetchable; } } diff --git a/karma_config/vueLocal.js b/karma_config/vueLocal.js index e4b08cc9cb1..63683acfbce 100644 --- a/karma_config/vueLocal.js +++ b/karma_config/vueLocal.js @@ -4,6 +4,9 @@ import router from 'vue-router'; import vueintl from 'vue-intl'; import 'intl'; import 'intl/locale-data/jsonp/en.js'; +import kRouter from 'kolibri.coreVue.router'; + +kRouter.init([]); vue.prototype.Kolibri = {}; vue.config.silent = true; diff --git a/kolibri/content/api.py b/kolibri/content/api.py index 64700802898..f02a1a34cf1 100644 --- a/kolibri/content/api.py +++ b/kolibri/content/api.py @@ -401,6 +401,7 @@ def _studio_response_to_kolibri_response(cls, studioresp): "thumbnail": studioresp.get("icon_encoding"), "public": studioresp.get("public", True), "total_resources": studioresp.get("total_resource_count", 0), + "total_file_size": studioresp.get("published_size"), "version": studioresp.get("version", 0), "included_languages": included_languages, "last_updated": studioresp.get("last_published"), diff --git a/kolibri/content/serializers.py b/kolibri/content/serializers.py index de8673fed38..aa43e08893b 100644 --- a/kolibri/content/serializers.py +++ b/kolibri/content/serializers.py @@ -51,8 +51,18 @@ def get_lang_name(self, instance): class Meta: model = ChannelMetadata - fields = ('root', 'id', 'name', 'description', 'author', 'last_updated', 'version', 'thumbnail', - 'lang_code', 'lang_name') + fields = ( + 'author', + 'description', + 'id', + 'last_updated', + 'lang_code', + 'lang_name', + 'name', + 'root', + 'thumbnail', + 'version', + ) class LowerCaseField(serializers.CharField): diff --git a/kolibri/core/assets/src/api-resources/channel.js b/kolibri/core/assets/src/api-resources/channel.js index 01962009564..747718adb6f 100644 --- a/kolibri/core/assets/src/api-resources/channel.js +++ b/kolibri/core/assets/src/api-resources/channel.js @@ -3,6 +3,9 @@ import { Resource } from '../api-resource'; /** * @example Delete a channel * ChannelResource.getModel(channel_id).delete() + * + * @example Only get the channels that are "available" (i.e. with resources on device) + * ChannelResource.getCollection().fetch({ available: true }) */ export default class ChannelResource extends Resource { static resourceName() { diff --git a/kolibri/core/assets/src/api-resources/contentNodeGranular.js b/kolibri/core/assets/src/api-resources/contentNodeGranular.js new file mode 100644 index 00000000000..d4eeb3b06ca --- /dev/null +++ b/kolibri/core/assets/src/api-resources/contentNodeGranular.js @@ -0,0 +1,28 @@ +import { Resource } from '../api-resource'; + +/** + * @example Get ContentNode from a local USB drive for the purposes of importing from that drive. + * ContentNodeGranular.getModel(pk).fetch({ importing_from_drive_id: 'drive_1' }); + * + * @example Get ContentNode from a remote channel (whose content DB has been downloaded). + * OR exporting to a USB drive. + * ContentNodeGranular.getModel(pk).fetch(); + * + * Note: if the top-level of the channel is desired, then `pk` must be the channels's `root` id. + */ +export default class ContentNodeGranularResource extends Resource { + static resourceName() { + return 'contentnode_granular'; + } + + static idKey() { + return 'pk'; + } + + // Given a node ID, returns the {total_file_size, on_device_file_size} + getFileSizes(pk) { + return this.client({ + path: `${this.urls['contentnodefilesize_list']()}${pk}`, + }); + } +} diff --git a/kolibri/core/assets/src/api-resources/index.js b/kolibri/core/assets/src/api-resources/index.js index 8580eaa7a56..ddc976db0fa 100644 --- a/kolibri/core/assets/src/api-resources/index.js +++ b/kolibri/core/assets/src/api-resources/index.js @@ -1,5 +1,6 @@ import ClassroomResource from './classroom'; import ContentNodeResource from './contentNode'; +import ContentNodeGranular from './contentNodeGranular'; import FacilityUserResource from './facilityUser'; import FacilityUsernameResource from './facilityUsername'; import LearnerGroupResource from './learnerGroup'; @@ -24,6 +25,7 @@ import UserProgressResource from './userProgress'; import ContentNodeProgressResource from './contentNodeProgress'; import DeviceProvisionResource from './deviceProvision'; import DevicePermissionsResource, { NewDevicePermissionsResource } from './devicePermissions'; +import RemoteChannel from './remoteChannel'; const classroomResource = new ClassroomResource(); const contentNodeResource = new ContentNodeResource(); @@ -52,10 +54,14 @@ const contentNodeProgressResource = new ContentNodeProgressResource(); const deviceProvisionResource = new DeviceProvisionResource(); const devicePermissionsResource = new DevicePermissionsResource(); const newDevicePermissionsResource = new NewDevicePermissionsResource(); +const ContentNodeGranularResource = new ContentNodeGranular(); +const RemoteChannelResource = new RemoteChannel(); export { classroomResource as ClassroomResource, contentNodeResource as ContentNodeResource, + ContentNodeGranularResource, + RemoteChannelResource, facilityUserResource as FacilityUserResource, facilityUsernameResource as FacilityUsernameResource, learnerGroupResource as LearnerGroupResource, diff --git a/kolibri/core/assets/src/api-resources/remoteChannel.js b/kolibri/core/assets/src/api-resources/remoteChannel.js new file mode 100644 index 00000000000..5dcfa35c3db --- /dev/null +++ b/kolibri/core/assets/src/api-resources/remoteChannel.js @@ -0,0 +1,7 @@ +import { Resource } from '../api-resource'; + +export default class RemoteChannelResource extends Resource { + static resourceName() { + return 'remotechannel'; + } +} diff --git a/kolibri/core/assets/src/api-resources/task.js b/kolibri/core/assets/src/api-resources/task.js index e23c0d9a5b7..9f099e1458a 100644 --- a/kolibri/core/assets/src/api-resources/task.js +++ b/kolibri/core/assets/src/api-resources/task.js @@ -1,32 +1,109 @@ import { Resource } from '../api-resource'; +import pickBy from 'lodash/pickBy'; export default class TaskResource extends Resource { static resourceName() { return 'task'; } - localExportContent(driveId) { - const clientObj = { - path: this.localExportUrl(), - entity: { drive_id: driveId }, - }; - return this.client(clientObj); + /** + * Initiates a Task that imports a Channel Metadata DB from a remote source + * + * @param {string} channel_id - + * + */ + startRemoteChannelImport({ channel_id }) { + return this.client({ + path: this.urls[`${this.name}_startremotechannelimport`](), + method: 'POST', + entity: { + channel_id, + }, + }); } - localImportContent(driveId) { - const clientObj = { - path: this.localImportUrl(), - entity: { drive_id: driveId }, - }; - return this.client(clientObj); + /** + * Initiates a Task that imports a Channel Metadata DB from a local drive + * + * @param {string} params.channel_id - + * @param {string} params.drive_id - + * + */ + startDiskChannelImport({ channel_id, drive_id }) { + return this.client({ + path: this.urls[`${this.name}_startdiskchannelimport`](), + method: 'POST', + entity: { + channel_id, + drive_id, + }, + }); } - remoteImportContent(channelId) { - const clientObj = { - path: this.remoteImportUrl(), - entity: { channel_id: channelId }, - }; - return this.client(clientObj); + /** + * Initiates a Task that imports Channel Content from a remote source + * + * @param {string} params.channel_id - + * @param {string} [params.baseurl] - URL of remote source (defaults to Kolibri Studio) + * @param {Array} [params.node_ids] - + * @param {Array} [params.exclude_node_ids] - + * @returns {Promise} + * + */ + startRemoteContentImport(params) { + return this.client({ + path: this.urls[`${this.name}_startremotecontentimport`](), + method: 'POST', + entity: pickBy(params), + }); + } + + /** + * Initiates a Task that imports Channel Content from a local drive + * + * @param {string} params.channel_id - + * @param {string} params.drive_id - + * @param {Array} [params.node_ids] - + * @param {Array} [params.exclude_node_ids] - + * @returns {Promise} + * + */ + startDiskContentImport(params) { + return this.client({ + path: this.urls[`${this.name}_startdiskcontentimport`](), + method: 'POST', + entity: pickBy(params), + }); + } + + /** + * Initiates a Task that exports Channel Content to a local drive + * + * @param {string} params.channel_id - + * @param {string} params.drive_id - + * @param {Array} [params.node_ids] - + * @param {Array} [params.exclude_node_ids] - + * @returns {Promise} + * + */ + startDiskContentExport(params) { + // Not naming it after URL to keep internal consistency + return this.client({ + path: this.urls[`${this.name}_startdiskexport`](), + method: 'POST', + entity: pickBy(params), + }); + } + + /** + * Gets all the Tasks outside of the Resource Layer mechanism + * + */ + getTasks() { + return this.client({ + path: this.urls[`${this.name}_list`](), + method: 'GET', + }); } deleteChannel(channelId) { @@ -58,16 +135,6 @@ export default class TaskResource extends Resource { }; return this.client(clientObj); } - - get localExportUrl() { - return this.urls[`${this.name}_startlocalexport`]; - } - get localImportUrl() { - return this.urls[`${this.name}_startlocalimport`]; - } - get remoteImportUrl() { - return this.urls[`${this.name}_startremoteimport`]; - } get deleteChannelUrl() { return this.urls[`${this.name}_startdeletechannel`]; } diff --git a/kolibri/core/assets/src/router.js b/kolibri/core/assets/src/router.js index 6df3bccf187..a19a1426f30 100644 --- a/kolibri/core/assets/src/router.js +++ b/kolibri/core/assets/src/router.js @@ -45,6 +45,10 @@ class Router { replace(location, onComplete, onAbort) { return this._vueRouter.replace(location, onComplete, onAbort); } + + push(location, onComplete, onAbort) { + return this._vueRouter.push(location, onComplete, onAbort); + } } const router = new Router(); diff --git a/kolibri/core/assets/src/views/core-modal/index.vue b/kolibri/core/assets/src/views/core-modal/index.vue index ba5cfa05638..1fcc4b475e3 100644 --- a/kolibri/core/assets/src/views/core-modal/index.vue +++ b/kolibri/core/assets/src/views/core-modal/index.vue @@ -21,7 +21,7 @@ :style="{ width: width, height: height }" > -
+
@@ -100,6 +100,10 @@ type: String, required: false, }, + hideTopButtons: { + type: Boolean, + default: false, + }, }, data() { return { @@ -221,9 +225,6 @@ .btn-close right: -10px - .title - text-align: center - .fade-enter-active, .fade-leave-active transition: all 0.3s ease diff --git a/kolibri/core/assets/src/views/immersive-full-screen/index.vue b/kolibri/core/assets/src/views/immersive-full-screen/index.vue index cdf8ca1d0a4..eacdcfc7c73 100644 --- a/kolibri/core/assets/src/views/immersive-full-screen/index.vue +++ b/kolibri/core/assets/src/views/immersive-full-screen/index.vue @@ -24,6 +24,7 @@ import { validateLinkObject } from 'kolibri.utils.validators'; export default { + name: 'immersiveFullScreen', props: { backPageLink: { type: Object, diff --git a/kolibri/core/assets/src/views/k-breadcrumbs/index.vue b/kolibri/core/assets/src/views/k-breadcrumbs/index.vue index 4653e957354..e640a68e625 100644 --- a/kolibri/core/assets/src/views/k-breadcrumbs/index.vue +++ b/kolibri/core/assets/src/views/k-breadcrumbs/index.vue @@ -1,6 +1,6 @@