diff --git a/kolibri/core/assets/src/views/sync/ConfirmationRegisterModal.vue b/kolibri/core/assets/src/views/sync/ConfirmationRegisterModal.vue index 753252df6bb..8017003c5ad 100644 --- a/kolibri/core/assets/src/views/sync/ConfirmationRegisterModal.vue +++ b/kolibri/core/assets/src/views/sync/ConfirmationRegisterModal.vue @@ -5,7 +5,7 @@ :submitText="registerText" :cancelText="cancelText" @submit="registerFacility" - @cancel="$emit('cancel')" + @cancel="cancel" > diff --git a/kolibri/core/assets/src/views/sync/SyncFacilityModalGroup.vue b/kolibri/core/assets/src/views/sync/SyncFacilityModalGroup.vue index b75514d5ee0..727c4c0bcdf 100644 --- a/kolibri/core/assets/src/views/sync/SyncFacilityModalGroup.vue +++ b/kolibri/core/assets/src/views/sync/SyncFacilityModalGroup.vue @@ -15,6 +15,23 @@ @submit="handleAddressSubmit" @cancel="closeModal()" /> + + + @@ -26,15 +43,21 @@ import commonSyncElements from 'kolibri.coreVue.mixins.commonSyncElements'; import SelectDeviceModalGroup from './SelectDeviceModalGroup'; import SelectSyncSourceModal from './SelectSyncSourceModal'; + import RegisterFacilityModal from './RegisterFacilityModal'; + import ConfirmationRegisterModal from './ConfirmationRegisterModal'; const Steps = Object.freeze({ SELECT_SOURCE: 'SELECT_SOURCE', SELECT_ADDRESS: 'SELECT_ADDRESS', + REGISTER_FACILITY: 'REGISTER_FACILITY', + CONFIRMATION_REGISTER: 'CONFIRMATION_REGISTER', }); export default { name: 'SyncFacilityModalGroup', components: { + ConfirmationRegisterModal, + RegisterFacilityModal, SelectSyncSourceModal, SelectDeviceModalGroup, }, @@ -49,18 +72,24 @@ }, data() { return { - step: null, + step: Steps.SELECT_SOURCE, syncSubmitDisabled: false, - displaySkipOption: true, + kdpProject: null, // { name, token } }; }, computed: { atSelectSource() { - return !this.step; + return this.step === Steps.SELECT_SOURCE; }, atSelectAddress() { return this.step === Steps.SELECT_ADDRESS; }, + atRegister() { + return this.step === Steps.REGISTER_FACILITY; + }, + atConfirmation() { + return this.step === Steps.CONFIRMATION_REGISTER; + }, }, methods: { handleSourceSubmit(data) { @@ -68,9 +97,9 @@ this.step = Steps.SELECT_ADDRESS; } else { if (this.facilityForSync.dataset.registered) { - this.$emit('syncKDP', this.facilityForSync); + this.emitKdpSync(); } else { - this.$emit('register', this.displaySkipOption, this.facilityForSync); + this.step = Steps.REGISTER_FACILITY; } } }, @@ -80,9 +109,16 @@ } this.$emit('syncPeer', data, this.facilityForSync); }, + handleValidateSuccess({ name, token }) { + this.kdpProject = { name, token }; + this.step = Steps.CONFIRMATION_REGISTER; + }, closeModal() { this.$emit('close'); }, + emitKdpSync() { + this.$emit('syncKDP', this.facilityForSync); + }, }, }; diff --git a/kolibri/plugins/device/assets/src/views/FacilitiesPage/FacilitiesTasksPage.vue b/kolibri/plugins/device/assets/src/views/FacilitiesPage/FacilitiesTasksPage.vue index fddbe2a9f36..ffca0caee51 100644 --- a/kolibri/plugins/device/assets/src/views/FacilitiesPage/FacilitiesTasksPage.vue +++ b/kolibri/plugins/device/assets/src/views/FacilitiesPage/FacilitiesTasksPage.vue @@ -22,7 +22,7 @@

task.clearable)); + return Boolean(this.activeFacilityTasks.find(task => task.clearable)); }, }, methods: { diff --git a/kolibri/plugins/device/assets/src/views/FacilitiesPage/facilityTasksQueue.js b/kolibri/plugins/device/assets/src/views/FacilitiesPage/facilityTasksQueue.js index 85b35acbb3b..52c29817de8 100644 --- a/kolibri/plugins/device/assets/src/views/FacilitiesPage/facilityTasksQueue.js +++ b/kolibri/plugins/device/assets/src/views/FacilitiesPage/facilityTasksQueue.js @@ -1,7 +1,7 @@ // Mixin that can be used for a component to view and manage // the task queue import { TaskResource } from 'kolibri.resources'; -import { TaskTypes } from 'kolibri.utils.syncTaskUtils'; +import { TaskStatuses, TaskTypes } from 'kolibri.utils.syncTaskUtils'; function isSyncTask(task) { return task.type === TaskTypes.SYNCDATAPORTAL || task.type === TaskTypes.SYNCPEERFULL; @@ -11,6 +11,13 @@ function taskFacilityMatch(task, facility) { return task.facility_id === facility.id; } +function isActiveTask(task) { + // Helper function filter tasks by whether they are 'active' + // i.e. has a user just queued a non-repeating task, or is a repeating task + // that is currently running. + return task.repeat !== null || task.status === TaskStatuses.RUNNING; +} + export default { data() { return { @@ -41,10 +48,15 @@ export default { this.isPolling = false; }, computed: { + activeFacilityTasks() { + return this.facilityTasks.filter(isActiveTask); + }, facilityIsSyncing() { return function isSyncing(facility) { - const syncTasks = this.facilityTasks.filter(t => isSyncTask(t) && !t.clearable); - return Boolean(syncTasks.find(task => taskFacilityMatch(task, facility))); + const inProcessSyncTasks = this.activeFacilityTasks.filter( + t => isSyncTask(t) && !t.clearable + ); + return Boolean(inProcessSyncTasks.find(task => taskFacilityMatch(task, facility))); }; }, facilityIsDeleting() { diff --git a/kolibri/plugins/device/assets/src/views/FacilitiesPage/index.vue b/kolibri/plugins/device/assets/src/views/FacilitiesPage/index.vue index ec932c4c157..be425f654aa 100644 --- a/kolibri/plugins/device/assets/src/views/FacilitiesPage/index.vue +++ b/kolibri/plugins/device/assets/src/views/FacilitiesPage/index.vue @@ -27,8 +27,8 @@ @@ -153,7 +153,7 @@ @@ -246,8 +245,6 @@ facilityForRegister: null, kdpProject: null, // { name, token } taskIdsToWatch: [], - displaySkipOption: false, - // (facilityTaskQueue) facilityTasks }; }, computed: { @@ -306,7 +303,6 @@ if (option === Options.REMOVE) { this.facilityForRemoval = facility; } else if (option === Options.REGISTER) { - this.displaySkipOption = false; this.facilityForRegister = facility; } else if (option === Options.MANAGESYNC) { const route = this.manageSync(facility.id); @@ -367,11 +363,6 @@ this.taskIdsToWatch.push(taskId); this.facilityForRemoval = null; }, - handleRegister(displaySkipOption, facility) { - this.displaySkipOption = displaySkipOption; - this.facilityForRegister = facility; - this.facilityForSync = null; - }, handleKDPSync(facility) { this.facilityForSync = null; this.facilityForRegister = null; diff --git a/kolibri/plugins/facility/assets/src/routes.js b/kolibri/plugins/facility/assets/src/routes.js index 50103aaa6ca..b0e5ce96892 100644 --- a/kolibri/plugins/facility/assets/src/routes.js +++ b/kolibri/plugins/facility/assets/src/routes.js @@ -157,7 +157,10 @@ export default [ const facilityId = route.params.facility_id || store.getters.currentFacilityId; return { facilityId, - goBackRoute: { name: PageNames.DATA_EXPORT_PAGE }, + goBackRoute: { + name: PageNames.DATA_EXPORT_PAGE, + params: { facility_id: route.params.facility_id }, + }, editSyncRoute: function(deviceId) { return { name: SyncPageNames.EDIT_SYNC_SCHEDULE, @@ -180,7 +183,10 @@ export default [ return { facilityId: route.params.facility_id || store.getters.currentFacilityId, deviceId: route.params.deviceId, - goBackRoute: { name: SyncPageNames.MANAGE_SYNC_SCHEDULE }, + goBackRoute: { + name: SyncPageNames.MANAGE_SYNC_SCHEDULE, + params: { facility_id: route.params.facility_id }, + }, }; }, }, diff --git a/kolibri/plugins/facility/assets/src/views/DataPage/SyncInterface/index.vue b/kolibri/plugins/facility/assets/src/views/DataPage/SyncInterface/index.vue index cc94ff516f0..85cf7ccced1 100644 --- a/kolibri/plugins/facility/assets/src/views/DataPage/SyncInterface/index.vue +++ b/kolibri/plugins/facility/assets/src/views/DataPage/SyncInterface/index.vue @@ -84,7 +84,7 @@ @@ -169,7 +168,6 @@ syncHasFailed: false, Modals, isMenuOpen: false, - displaySkipOption: false, }; }, computed: { @@ -265,9 +263,8 @@ handleSyncFacilityFailure() { this.syncHasFailed = true; }, - handleRegister(displaySkipOption) { + handleRegister() { this.closeMenu(); - this.displaySkipOption = Boolean(displaySkipOption); this.displayModal(Modals.REGISTER_FACILITY); }, handleKDPSync() { diff --git a/packages/kolibri-common/components/SyncSchedule/EditDeviceSyncSchedule.vue b/packages/kolibri-common/components/SyncSchedule/EditDeviceSyncSchedule.vue index 6c4363283d2..78a7ff366d5 100644 --- a/packages/kolibri-common/components/SyncSchedule/EditDeviceSyncSchedule.vue +++ b/packages/kolibri-common/components/SyncSchedule/EditDeviceSyncSchedule.vue @@ -28,6 +28,7 @@ - + {{ $tr('serverTime') }} - {{ now }} + {{ + $formatTime(now, { + year: "numeric", + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }) + }}

- + {{ $tr('checkboxLabel') }}

@@ -134,10 +148,7 @@ :layout12="{ span: 12 }" >

{{ $tr('removeDeviceWarning') }}

-

- -

-

+

{{ $tr('deviceNotConnected') }}

@@ -152,10 +163,38 @@ import ImmersivePage from 'kolibri.coreVue.components.ImmersivePage'; import BottomAppBar from 'kolibri.coreVue.components.BottomAppBar'; - import { NetworkLocationResource, FacilityResource, TaskResource } from 'kolibri.resources'; + import { NetworkLocationResource, TaskResource } from 'kolibri.resources'; import { now } from 'kolibri.utils.serverClock'; import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings'; - import { TaskTypes } from 'kolibri.utils.syncTaskUtils'; + import { TaskStatuses, TaskTypes } from 'kolibri.utils.syncTaskUtils'; + import { KDP_ID, oneHour, oneDay, oneWeek, twoWeeks, oneMonth } from './constants'; + import { kdpNameTranslator } from './i18n'; + + const today = new Date(); + const daysOfWeek = []; + const date = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate() + (7 - today.getDay()) + ); + for (let i = 0; i < 7; i++) { + daysOfWeek.push({ value: i, date: new Date(date) }); + date.setDate(date.getDate() + 1); + } + + const endTime = new Date(); + endTime.setHours(24, 0, 0, 0); + const interval = 30; + + const times = []; + var i = 0; + const time = new Date(); + time.setHours(0, 0, 0, 0); + + while (time < endTime) { + times.push({ value: i++, time: new Date(time) }); + time.setMinutes(time.getMinutes() + interval); + } export default { name: 'EditDeviceSyncSchedule', @@ -185,16 +224,13 @@ data() { return { removeDeviceModal: false, - deviceName: null, - device: [], - now: now(), + retryFlag: false, + device: null, + now: null, selectedItem: {}, tasks: [], selectedDay: {}, selectedTime: {}, - removeBtn: false, - serverTime: null, - baseurl: null, }; }, computed: { @@ -217,55 +253,79 @@ }, selectArray() { return [ - { label: this.$tr('everyHour'), value: 3600 }, - { label: this.$tr('everyDay'), value: 86400 }, - { label: this.$tr('everyWeek'), value: 604800 }, - { label: this.$tr('everyTwoWeeks'), value: 1209600 }, - { label: this.$tr('everyMonth'), value: 2592000 }, + { label: this.$tr('everyHour'), value: oneHour }, + { label: this.$tr('everyDay'), value: oneDay }, + { label: this.$tr('everyWeek'), value: oneWeek }, + { label: this.$tr('everyTwoWeeks'), value: twoWeeks }, + { label: this.$tr('everyMonth'), value: oneMonth }, ]; }, getDays() { - const today = new Date(); - const daysOfWeek = []; - const date = new Date( - today.getFullYear(), - today.getMonth(), - today.getDate() + (7 - today.getDay()) - ); - for (let i = 0; i < 7; i++) { - daysOfWeek.push({ label: this.$formatDate(date, { weekday: 'long' }), value: i }); - date.setDate(date.getDate() + 1); - } - return daysOfWeek; + return daysOfWeek.map(day => { + return { + label: this.$formatDate(day.date, { weekday: 'long' }), + value: day.value, + }; + }); }, SyncTime() { - const endTime = new Date(); - endTime.setHours(24, 0, 0, 0); - const interval = 30; - - const times = []; - var i = 0; - const time = new Date(); - time.setHours(0, 0, 0, 0); - - while (time < endTime) { - times.push({ label: this.$formatTime(time), value: i++ }); - time.setMinutes(time.getMinutes() + interval); - } - return times; + return times.map(time => { + return { + label: this.$formatTime(time.time), + value: time.value, + hours: time.time.getHours(), + minutes: time.time.getMinutes(), + }; + }); + }, + deviceName() { + return this.device && this.device.nickname && this.device.nickname.length + ? this.device.nickname + : this.device.device_name; + }, + currentTask() { + return this.tasks && this.tasks.length ? this.tasks[0] : null; + }, + currentTaskRunning() { + return this.currentTask && this.currentTask.status === TaskStatuses.RUNNING; + }, + timeRequired() { + return this.selectedItem.value > oneHour; + }, + timeIsSet() { + return this.selectedTime && times[this.selectedTime.value]; + }, + dayRequired() { + return this.selectedItem.value > oneDay; + }, + dayIsSet() { + return this.selectedDay && daysOfWeek[this.selectedDay.value]; + }, + isKdp() { + return this.deviceId === KDP_ID; + }, + taskType() { + return this.isKdp ? TaskTypes.SYNCDATAPORTAL : TaskTypes.SYNCPEERFULL; + }, + saveDisabled() { + return ( + this.currentTaskRunning || + (!this.timeIsSet && this.timeRequired) || + (!this.dayIsSet && this.dayRequired) || + !this.selectedItem.value + ); }, }, - beforeMount() { + created() { this.fetchDevice(); - }, - mounted() { - this.serverTime = setInterval(() => { + this.now = now(); + this.serverTimeInterval = setInterval(() => { this.now = now(); }, 10000); }, beforeDestroy() { - clearInterval(this.serverTime); + clearInterval(this.serverTimeInterval); }, methods: { closeModal() { @@ -273,57 +333,154 @@ }, handleDeleteDevice() { this.removeDeviceModal = false; - NetworkLocationResource.deleteModel({ id: this.deviceId }) + TaskResource.deleteModel({ id: this.currentTask.id }) .then(() => { this.showSnackbarNotification('deviceRemove'); - history.back(); + this.goBack(); }) .catch(() => { this.showSnackbarNotification('deviceNotRemove'); }); }, + computeNextSync() { + const date = new Date(this.now); + if (this.timeRequired) { + if (!this.timeIsSet) { + throw new ReferenceError('Time is not set and is required'); + } + const hours = this.selectedTime.hours; + const minutes = this.selectedTime.minutes; + if ( + hours < date.getHours() || + (hours === date.getHours() && minutes < date.getMinutes()) + ) { + date.setDate(date.getDate() + 1); + } + date.setHours(hours); + date.setMinutes(minutes); + } + if (this.dayRequired) { + if (!this.dayIsSet) { + throw new ReferenceError('Day is not set and is required'); + } + const diff = this.selectedDay.value - date.getDay(); + if (date.getDay() > this.selectedDay.value) { + date.setDate(date.getDate() + 7 - Math.abs(diff)); + } else if (date.getDay() < this.selectedDay.value) { + date.setDate(date.getDate() + Math.abs(diff)); + } + } + return date; + }, handleSaveSchedule() { - FacilityResource.fetchModel({ id: this.facilityId, force: true }).then(facility => { - this.facility = { ...facility }; - const date = new Date(this.serverTime); - const equeue_param = date.toISOString(); - TaskResource.startTask({ - type: TaskTypes.SYNCPEERFULL, - facility: this.facility.id, - device_id: this.deviceId, - baseurl: this.baseurl, - enqueue_args: { - enqueue_at: equeue_param, - repeat_interval: this.selectedItem.value, - repeat: 2, - }, + const enqueue_param = this.computeNextSync().toISOString(); + const enqueue_args = { + enqueue_at: enqueue_param, + repeat_interval: this.selectedItem.value, + repeat: null, + retry_interval: this.retryFlag ? 60 * 5 : null, + }; + let promise; + if (this.currentTask) { + promise = TaskResource.saveModel({ + id: this.currentTask.id, + data: { enqueue_args }, + exists: true, + }); + } else { + const taskParams = { + type: this.taskType, + facility: this.facilityId, + enqueue_args, + }; + if (!this.isKdp) { + taskParams.device_id = this.deviceId; + taskParams.baseurl = this.device.base_url; + } + promise = TaskResource.startTask(taskParams); + } + promise + .then(() => { + this.goBack(); + this.showSnackbarNotification('syncAdded'); }) - .then(() => { - history.back(); - this.showSnackbarNotification('syncAdded'); - }) - .catch(() => { - this.createTaskFailedSnackbar(); - }); - }); + .catch(() => { + this.createTaskFailedSnackbar(); + if (this.currentTask) { + this.fetchSyncTasks(); + } + }); }, - cancelBtn() { + goBack() { this.$router.push(this.goBackRoute); }, + pollFetchSyncTasks() { + this.pollInterval = setInterval(() => { + this.fetchSyncTasks(); + }, 10000); + }, + fetchSyncTasks() { + TaskResource.list({ queue: 'facility_task' }).then(tasks => { + this.tasks = tasks.filter( + task => + (this.isKdp || task.extra_metadata.device_id === this.device.id) && + task.facility_id === this.facilityId && + task.type === this.taskType && + // Only show tasks that are repeating indefinitely + task.repeat === null + ); + this.$nextTick(() => { + if (this.currentTask) { + const enqueueAt = new Date(Date.parse(this.currentTask.scheduled_datetime)); + const day = enqueueAt.getDay(); + const hours = enqueueAt.getHours(); + const minutes = enqueueAt.getMinutes(); + this.selectedItem = + this.selectArray.find(item => item.value === this.currentTask.repeat_interval) || + {}; + this.selectedDay = this.getDays.find(item => item.value === day) || {}; + for (const time of this.SyncTime) { + // Because there can be some drift in the task scheduling process, + // we round the 'scheduled' time to the nearest 30 minutes + if ( + time.minutes === 0 && + ((time.hours === hours && minutes < 15) || + (time.hours === hours + 1 && minutes >= 45)) + ) { + this.selectedTime = time; + break; + } + if (time.minutes === 30 && time.hours === hours && minutes >= 15 && minutes < 45) { + this.selectedTime = time; + break; + } + } + this.retryFlag = Boolean(this.currentTask.retry_interval); + if (this.currentTaskRunning) { + this.pollFetchSyncTasks(); + } else { + clearInterval(this.pollInterval); + this.pollInterval = null; + } + } + }); + }); + }, fetchDevice() { + if (this.isKdp) { + this.device = { + id: KDP_ID, + // eslint-disable-next-line kolibri/vue-no-undefined-string-uses + device_name: kdpNameTranslator.$tr('syncToKDP'), + base_url: '', + }; + this.fetchSyncTasks(); + return; + } NetworkLocationResource.fetchModel({ id: this.deviceId }).then(device => { this.device = device; - this.baseurl = device.base_url; - TaskResource.list({ queue: 'facility_task' }).then(tasks => { - this.tasks = tasks.filter( - task => - task.extra_metadata.device_id === device.id && task.facility_id === this.facilityId - ); - if (this.tasks && this.tasks.length) { - this.removeBtn = true; - } - }); + this.fetchSyncTasks(); }); }, }, diff --git a/packages/kolibri-common/components/SyncSchedule/ManageSyncSchedule.vue b/packages/kolibri-common/components/SyncSchedule/ManageSyncSchedule.vue index 7b6973c3958..fed92e47c70 100644 --- a/packages/kolibri-common/components/SyncSchedule/ManageSyncSchedule.vue +++ b/packages/kolibri-common/components/SyncSchedule/ManageSyncSchedule.vue @@ -40,7 +40,7 @@ - - - - - - - - - -
-
-
- - {{ btn.base_url }} -
-
-
-
-
- -
-
-
- - - - - -
-
- + :facilityForSync="facility" + @close="closeModal" + @syncKDP="handleKDPSync" + @syncPeer="handlePeerSync" + /> @@ -192,22 +118,52 @@