From f56fc2325905a2d9c6bc189c49f27c7a924ccac6 Mon Sep 17 00:00:00 2001
From: Blaine Jester
Date: Tue, 7 Jun 2022 15:03:57 -0700
Subject: [PATCH 1/4] Retry, but resume, a sync
---
kolibri/core/auth/management/utils.py | 7 +++
kolibri/core/auth/tasks.py | 21 ++++++++
kolibri/core/tasks/api.py | 15 +++---
kolibri/core/tasks/exceptions.py | 4 --
kolibri/core/tasks/job.py | 12 -----
kolibri/core/tasks/registry.py | 18 +++++++
kolibri/core/tasks/storage.py | 28 -----------
.../tasks/test/taskrunner/test_storage.py | 26 ----------
kolibri/core/tasks/test/test_api.py | 22 ++++++++-
kolibri/core/tasks/test/test_job.py | 24 +++++++++-
kolibri/core/tasks/test/test_validation.py | 48 +++++++++++++++++++
kolibri/core/tasks/validation.py | 29 +++++++++++
12 files changed, 174 insertions(+), 80 deletions(-)
create mode 100644 kolibri/core/tasks/test/test_validation.py
diff --git a/kolibri/core/auth/management/utils.py b/kolibri/core/auth/management/utils.py
index 5d36b0d04d5..58101ada923 100644
--- a/kolibri/core/auth/management/utils.py
+++ b/kolibri/core/auth/management/utils.py
@@ -418,6 +418,13 @@ def _sync(self, sync_session_client, **options): # noqa: C901
dataset_cache.clear()
dataset_cache.activate()
+ # add the sync session ID to the job (task) if it exists for retrying it
+ if self.job:
+ self.job.extra_metadata.update(
+ sync_session_id=sync_session_client.sync_session.id
+ )
+ self.job.save_meta()
+
if not noninteractive:
# output session ID for CLI user
logger.info("Session ID: {}".format(sync_session_client.sync_session.id))
diff --git a/kolibri/core/auth/tasks.py b/kolibri/core/auth/tasks.py
index b0cb8ab2677..c7df2621d47 100644
--- a/kolibri/core/auth/tasks.py
+++ b/kolibri/core/auth/tasks.py
@@ -13,6 +13,7 @@
from django.utils import timezone
from morango.errors import MorangoResumeSyncError
from morango.models import InstanceIDModel
+from morango.models.core import SyncSession
from requests.exceptions import ConnectionError
from rest_framework import serializers
from rest_framework import status
@@ -261,6 +262,26 @@ def validate(self, data):
"args": [data["command"]],
}
+ def validate_for_restart(self, job):
+ sync_session_id = job.extra_metadata.get("sync_session_id")
+ if sync_session_id:
+ try:
+ SyncSession.objects.get(pk=sync_session_id, active=True)
+ except SyncSession.DoesNotExist:
+ sync_session_id = None
+
+ data = super(SyncJobValidator, self).validate_for_restart(job)
+
+ # if we didn't get an existing active sync_session_id,
+ # we'll fall back to default functionality
+ if sync_session_id:
+ kwargs = data.get("kwargs")
+ kwargs.pop("facility")
+ kwargs.update(id=sync_session_id)
+ data.update(args=("resumesync",), kwargs=kwargs)
+
+ return data
+
facility_task_queue = "facility_task"
diff --git a/kolibri/core/tasks/api.py b/kolibri/core/tasks/api.py
index ea7bb27ca70..53c90720674 100644
--- a/kolibri/core/tasks/api.py
+++ b/kolibri/core/tasks/api.py
@@ -9,7 +9,6 @@
from six import string_types
from kolibri.core.tasks.exceptions import JobNotFound
-from kolibri.core.tasks.exceptions import JobNotRestartable
from kolibri.core.tasks.job import State
from kolibri.core.tasks.main import job_storage
from kolibri.core.tasks.registry import TaskRegistry
@@ -185,13 +184,15 @@ def restart(self, request, pk=None):
job_to_restart = self._get_job_for_pk(request, pk)
- try:
- restarted_job_id = job_storage.restart_job(job_id=job_to_restart.job_id)
- except JobNotRestartable:
- raise serializers.ValidationError(
- "Cannot restart job with state: {}".format(job_to_restart.state)
- )
+ registered_task = TaskRegistry[job_to_restart.func]
+ job = registered_task.validate_job_restart(request.user, job_to_restart)
+ # delete existing task after validation
+ job_storage.clear(job_id=job_to_restart.job_id, force=False)
+
+ restarted_job_id = job_storage.enqueue_job(
+ job, queue=registered_task.queue, priority=registered_task.priority
+ )
job_response = self._job_to_response(
job_storage.get_job(job_id=restarted_job_id)
)
diff --git a/kolibri/core/tasks/exceptions.py b/kolibri/core/tasks/exceptions.py
index 10bfbb168ab..8f23bd541f3 100644
--- a/kolibri/core/tasks/exceptions.py
+++ b/kolibri/core/tasks/exceptions.py
@@ -11,7 +11,3 @@ class UserCancelledError(CancelledError):
class JobNotFound(Exception):
pass
-
-
-class JobNotRestartable(Exception):
- pass
diff --git a/kolibri/core/tasks/job.py b/kolibri/core/tasks/job.py
index 882eaac407f..bcb4470c15b 100644
--- a/kolibri/core/tasks/job.py
+++ b/kolibri/core/tasks/job.py
@@ -182,18 +182,6 @@ def from_json(cls, json_string):
return Job(func, **working_dictionary)
- @classmethod
- def from_job(cls, job, **kwargs):
- if not isinstance(job, cls):
- raise TypeError("job must be an instance of {}".format(cls))
- kwargs["args"] = copy.copy(job.args)
- kwargs["kwargs"] = copy.copy(job.kwargs)
- kwargs["track_progress"] = job.track_progress
- kwargs["cancellable"] = job.cancellable
- kwargs["extra_metadata"] = job.extra_metadata.copy()
- kwargs["facility_id"] = job.facility_id
- return cls(job.func, **kwargs)
-
def __init__(
self,
func,
diff --git a/kolibri/core/tasks/registry.py b/kolibri/core/tasks/registry.py
index 8cb76fdbb57..2a45ae518cd 100644
--- a/kolibri/core/tasks/registry.py
+++ b/kolibri/core/tasks/registry.py
@@ -229,6 +229,24 @@ def validate_job_data(self, user, data):
return job
+ def validate_job_restart(self, user, job):
+ """
+ :type user: kolibri.core.auth.models.FacilityUser
+ :type job: kolibri.core.tasks.job.Job
+ :return: A new job object for restarting
+ :rtype: kolibri.core.tasks.job.Job
+ """
+ validator = self.validator(instance=job, context={"user": user})
+
+ try:
+ job = self._ready_job(**validator.data)
+ except TypeError:
+ raise serializers.ValidationError(
+ "Invalid job data returned from validator."
+ )
+
+ return job
+
def enqueue(self, job=None, **job_kwargs):
"""
Enqueue the function with arguments passed to this method.
diff --git a/kolibri/core/tasks/storage.py b/kolibri/core/tasks/storage.py
index 589f0a04ee4..3f3e0934ee9 100644
--- a/kolibri/core/tasks/storage.py
+++ b/kolibri/core/tasks/storage.py
@@ -16,7 +16,6 @@
from kolibri.core.tasks.constants import DEFAULT_QUEUE
from kolibri.core.tasks.exceptions import JobNotFound
-from kolibri.core.tasks.exceptions import JobNotRestartable
from kolibri.core.tasks.job import Job
from kolibri.core.tasks.job import Priority
from kolibri.core.tasks.job import State
@@ -241,33 +240,6 @@ def get_job(self, job_id):
job, _ = self._get_job_and_orm_job(job_id, session)
return job
- def restart_job(self, job_id):
- """
- First deletes the job with id = job_id then enqueues a new job with the same
- job_id as the one we deleted, with same args and kwargs.
-
- Returns the job_id of enqueued job.
-
- Raises `JobNotRestartable` exception if the job with id = job_id state is
- not in CANCELED or FAILED.
- """
- with self.session_scope() as session:
- job_to_restart, orm_job = self._get_job_and_orm_job(job_id, session)
- queue = orm_job.queue
- priority = orm_job.priority
-
- if job_to_restart.state in [State.CANCELED, State.FAILED]:
- self.clear(job_id=job_to_restart.job_id, force=False)
- job = Job.from_job(
- job_to_restart,
- job_id=job_to_restart.job_id,
- )
- return self.enqueue_job(job, queue=queue, priority=priority)
- else:
- raise JobNotRestartable(
- "Cannot restart job with state={}".format(job_to_restart.state)
- )
-
def check_job_canceled(self, job_id):
job = self.get_job(job_id)
return job.state == State.CANCELED or job.state == State.CANCELING
diff --git a/kolibri/core/tasks/test/taskrunner/test_storage.py b/kolibri/core/tasks/test/taskrunner/test_storage.py
index 0a2da1d3f91..416c0fc34c2 100644
--- a/kolibri/core/tasks/test/taskrunner/test_storage.py
+++ b/kolibri/core/tasks/test/taskrunner/test_storage.py
@@ -2,10 +2,8 @@
import time
import pytest
-from mock import patch
from kolibri.core.tasks.decorators import register_task
-from kolibri.core.tasks.exceptions import JobNotRestartable
from kolibri.core.tasks.job import Job
from kolibri.core.tasks.job import Priority
from kolibri.core.tasks.job import State
@@ -142,27 +140,3 @@ def test_gets_oldest_high_priority_job_first(self, defaultbackend, simplejob):
defaultbackend.enqueue_job(simplejob, QUEUE, Priority.HIGH)
assert defaultbackend.get_next_queued_job().job_id == job_id
-
- def test_restart_job(self, defaultbackend, simplejob):
- with patch("kolibri.core.tasks.main.job_storage", wraps=defaultbackend):
- job_id = defaultbackend.enqueue_job(simplejob, QUEUE)
-
- for state in [
- State.COMPLETED,
- State.RUNNING,
- State.QUEUED,
- State.SCHEDULED,
- State.CANCELING,
- ]:
- defaultbackend._update_job(job_id, state)
- with pytest.raises(JobNotRestartable):
- defaultbackend.restart_job(job_id)
-
- for state in [State.CANCELED, State.FAILED]:
- defaultbackend._update_job(job_id, state)
-
- restarted_job_id = defaultbackend.restart_job(job_id)
- restarted_job = defaultbackend.get_job(restarted_job_id)
-
- assert restarted_job_id == job_id
- assert restarted_job.state == State.QUEUED
diff --git a/kolibri/core/tasks/test/test_api.py b/kolibri/core/tasks/test/test_api.py
index 3d8d12b250b..4f8f961dfef 100644
--- a/kolibri/core/tasks/test/test_api.py
+++ b/kolibri/core/tasks/test/test_api.py
@@ -726,15 +726,33 @@ def test_retrieval_404(self, mock_job_storage):
def test_restart_task(self, mock_job_storage):
self.client.login(username=self.facility2user.username, password=DUMMY_PASSWORD)
- mock_job_storage.restart_job.return_value = self.jobs[2].job_id
+ self.jobs[2].state = State.FAILED
mock_job_storage.get_job.return_value = self.jobs[2]
+ def _clear(**kwargs):
+ self.jobs[2].state = State.QUEUED
+
+ mock_job_storage.clear.side_effect = _clear
+
response = self.client.post(
reverse("kolibri:core:task-restart", kwargs={"pk": "2"}), format="json"
)
self.assertEqual(response.data, self.jobs_response[2])
- mock_job_storage.restart_job.assert_called_once_with(job_id="2")
+ mock_job_storage.clear.assert_called_once_with(job_id="2", force=False)
+
+ def test_restart_task__not_restartable(self, mock_job_storage):
+ self.client.login(username=self.facility2user.username, password=DUMMY_PASSWORD)
+
+ mock_job_storage.get_job.return_value = self.jobs[2]
+
+ response = self.client.post(
+ reverse("kolibri:core:task-restart", kwargs={"pk": "2"}), format="json"
+ )
+
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(str(response.data[0]), "Cannot restart job with state=QUEUED")
+ mock_job_storage.clear.assert_not_called()
def test_restart_task_respect_permissions(self, mock_job_storage):
self.client.login(username=self.facility2user.username, password=DUMMY_PASSWORD)
diff --git a/kolibri/core/tasks/test/test_job.py b/kolibri/core/tasks/test/test_job.py
index fceeed5e315..e1147947f79 100644
--- a/kolibri/core/tasks/test/test_job.py
+++ b/kolibri/core/tasks/test/test_job.py
@@ -53,10 +53,15 @@ def test_job_save_as_cancellable__no_storage(self):
self.job.save_as_cancellable(cancellable=cancellable)
+class TestingJobValidator(JobValidator):
+ pass
+
+
class TestRegisteredTask(TestCase):
def setUp(self):
self.registered_task = RegisteredTask(
int,
+ validator=TestingJobValidator,
priority=Priority.HIGH,
queue="test",
permission_classes=[IsSuperAdmin],
@@ -67,7 +72,7 @@ def setUp(self):
def test_constructor_sets_required_params(self):
self.assertEqual(self.registered_task.func, int)
- self.assertEqual(self.registered_task.validator, JobValidator)
+ self.assertEqual(self.registered_task.validator, TestingJobValidator)
self.assertEqual(self.registered_task.priority, Priority.HIGH)
self.assertTrue(isinstance(self.registered_task.permissions[0], IsSuperAdmin))
self.assertEqual(self.registered_task.job_id, "test")
@@ -153,3 +158,20 @@ def test_enqueue(self, job_storage_mock, _ready_job_mock):
queue=self.registered_task.queue,
priority=self.registered_task.priority,
)
+
+ @mock.patch("kolibri.core.tasks.registry.RegisteredTask._ready_job")
+ def test_validate_job_restart(self, _ready_job_mock):
+ mock_user = mock.MagicMock(spec="kolibri.core.auth.models.FacilityUser")
+ mock_job = mock.MagicMock(spec="kolibri.core.tasks.registry.Job")
+
+ _ready_job_mock.return_value = "job"
+
+ with mock.patch.object(
+ TestingJobValidator, "validate_for_restart"
+ ) as mock_validate_for_restart:
+ mock_validate_for_restart.return_value = {"test": True}
+ result = self.registered_task.validate_job_restart(mock_user, mock_job)
+ mock_validate_for_restart.assert_called_once_with(mock_job)
+
+ self.assertEqual(result, "job")
+ _ready_job_mock.assert_called_once_with(test=True)
diff --git a/kolibri/core/tasks/test/test_validation.py b/kolibri/core/tasks/test/test_validation.py
new file mode 100644
index 00000000000..a5244ca9247
--- /dev/null
+++ b/kolibri/core/tasks/test/test_validation.py
@@ -0,0 +1,48 @@
+from django.test import SimpleTestCase
+from rest_framework import serializers
+
+from kolibri.core.tasks.job import Job
+from kolibri.core.tasks.job import State
+from kolibri.core.tasks.validation import JobValidator
+
+
+class JobValidatorTestCase(SimpleTestCase):
+ def setUp(self):
+ def add(x, y):
+ return x + y
+
+ self.job = Job(
+ add,
+ job_id="123",
+ state=State.PENDING,
+ args=("test",),
+ kwargs={"test": True},
+ track_progress=True,
+ cancellable=False,
+ extra_metadata={"extra": True},
+ )
+
+ def test_validate_for_restart(self):
+ for state in [State.CANCELED, State.FAILED]:
+ self.job.state = state
+ validator = JobValidator(instance=self.job)
+ self.assertEqual(
+ validator.data,
+ dict(
+ job_id="123",
+ args=("test",),
+ kwargs={"test": True},
+ track_progress=True,
+ cancellable=False,
+ extra_metadata={"extra": True},
+ facility_id=None,
+ ),
+ )
+
+ def test_validate_for_restart__not_restartable(self):
+ for state in [State.QUEUED, State.COMPLETED, State.SCHEDULED, State.RUNNING]:
+ self.job.state = state
+ validator = JobValidator(instance=self.job)
+
+ with self.assertRaises(serializers.ValidationError):
+ self.assertFalse(validator.data)
diff --git a/kolibri/core/tasks/validation.py b/kolibri/core/tasks/validation.py
index af9186c72e5..a99046019dc 100644
--- a/kolibri/core/tasks/validation.py
+++ b/kolibri/core/tasks/validation.py
@@ -1,5 +1,9 @@
+import copy
+
from rest_framework import serializers
+from kolibri.core.tasks.job import State
+
class JobValidator(serializers.Serializer):
"""
@@ -19,6 +23,31 @@ def validate(self, data):
"extra_metadata": {},
}
+ def validate_for_restart(self, job):
+ """
+ :param job: The job for which to restart
+ :type job: kolibri.core.tasks.job.Job
+ :return: A dictionary of data for instantiating a new job
+ """
+ if job.state not in [State.CANCELED, State.FAILED]:
+ raise serializers.ValidationError(
+ "Cannot restart job with state={}".format(job.state)
+ )
+
+ return {
+ # default behavior is to retain the same job ID, so the existing job requires deletion
+ "job_id": job.job_id,
+ "args": copy.copy(job.args),
+ "kwargs": copy.copy(job.kwargs),
+ "track_progress": job.track_progress,
+ "cancellable": job.cancellable,
+ "extra_metadata": job.extra_metadata.copy(),
+ "facility_id": job.facility_id,
+ }
+
+ def to_representation(self, instance):
+ return self.validate_for_restart(instance or self.instance)
+
def run_validation(self, data):
value = super(JobValidator, self).run_validation(data)
if not isinstance(value, dict):
From a31da5936a2788c8631b91730e06c3167546cb7a Mon Sep 17 00:00:00 2001
From: Blaine Jester
Date: Wed, 8 Jun 2022 13:09:45 -0700
Subject: [PATCH 2/4] Rename syncsession ID argument for resumesync + tests
---
.../auth/management/commands/resumesync.py | 6 ++-
kolibri/core/auth/tasks.py | 8 ++--
kolibri/core/auth/test/test_auth_tasks.py | 43 +++++++++++++++++++
3 files changed, 51 insertions(+), 6 deletions(-)
diff --git a/kolibri/core/auth/management/commands/resumesync.py b/kolibri/core/auth/management/commands/resumesync.py
index c95000c188c..d1fefd0b9f4 100644
--- a/kolibri/core/auth/management/commands/resumesync.py
+++ b/kolibri/core/auth/management/commands/resumesync.py
@@ -8,7 +8,9 @@ class Command(MorangoSyncCommand):
def add_arguments(self, parser):
parser.add_argument(
- "--id", type=str, help="ID of an incomplete session to resume sync"
+ "--sync-session-id",
+ type=str,
+ help="ID of an incomplete session to resume sync",
)
parser.add_argument(
"--baseurl", type=str, default=DATA_PORTAL_SYNCING_BASE_URL, dest="baseurl"
@@ -45,7 +47,7 @@ def add_arguments(self, parser):
def handle_async(self, *args, **options):
(baseurl, sync_session_id, chunk_size,) = (
options["baseurl"],
- options["id"],
+ options["sync_session_id"],
options["chunk_size"],
)
diff --git a/kolibri/core/auth/tasks.py b/kolibri/core/auth/tasks.py
index c7df2621d47..163b023460c 100644
--- a/kolibri/core/auth/tasks.py
+++ b/kolibri/core/auth/tasks.py
@@ -263,6 +263,9 @@ def validate(self, data):
}
def validate_for_restart(self, job):
+ data = super(SyncJobValidator, self).validate_for_restart(job)
+
+ # find the sync_session_id the command added to the job metadata when it ran
sync_session_id = job.extra_metadata.get("sync_session_id")
if sync_session_id:
try:
@@ -270,14 +273,11 @@ def validate_for_restart(self, job):
except SyncSession.DoesNotExist:
sync_session_id = None
- data = super(SyncJobValidator, self).validate_for_restart(job)
-
# if we didn't get an existing active sync_session_id,
# we'll fall back to default functionality
if sync_session_id:
kwargs = data.get("kwargs")
- kwargs.pop("facility")
- kwargs.update(id=sync_session_id)
+ kwargs.update(sync_session_id=sync_session_id)
data.update(args=("resumesync",), kwargs=kwargs)
return data
diff --git a/kolibri/core/auth/test/test_auth_tasks.py b/kolibri/core/auth/test/test_auth_tasks.py
index 2720e04bd30..403b50c6b7a 100644
--- a/kolibri/core/auth/test/test_auth_tasks.py
+++ b/kolibri/core/auth/test/test_auth_tasks.py
@@ -5,6 +5,7 @@
from django.urls import reverse
from mock import Mock
from mock import patch
+from morango.models.core import SyncSession
from requests.exceptions import ConnectionError
from rest_framework import serializers
from rest_framework.exceptions import AuthenticationFailed
@@ -29,6 +30,7 @@
from kolibri.core.public.constants.user_sync_statuses import QUEUED
from kolibri.core.public.constants.user_sync_statuses import SYNC
from kolibri.core.tasks.job import Job
+from kolibri.core.tasks.job import State
DUMMY_PASSWORD = "password"
@@ -41,6 +43,7 @@
traceback="",
percentage_progress=0,
cancellable=False,
+ track_progress=True,
extra_metadata={},
func="",
)
@@ -668,6 +671,46 @@ def test_validate_and_create_sync_credentials_no_credentials(
with self.assertRaises(PermissionDenied):
PeerFacilitySyncJobValidator(data=data).is_valid(raise_exception=True)
+ def test_validate_for_restart__not_restartable(self):
+ job = fake_job(state=State.RUNNING)
+ with self.assertRaises(serializers.ValidationError):
+ PeerFacilitySyncJobValidator(instance=job).data
+
+ def test_validate_for_restart__missing_sync_session(self):
+ job = fake_job(state=State.FAILED, args=("sync",), kwargs={"test": True})
+ new_job_data = PeerFacilitySyncJobValidator(instance=job).data
+ self.assertEqual(new_job_data["args"], ("sync",))
+ self.assertEqual(new_job_data["kwargs"], {"test": True})
+
+ @patch("kolibri.core.auth.tasks.SyncSession.objects.get")
+ def test_validate_for_restart__inactive_sync_session(self, mock_get):
+ job = fake_job(
+ state=State.FAILED,
+ args=("sync",),
+ kwargs={"test": True},
+ extra_metadata={"sync_session_id": "abc123"},
+ )
+ mock_get.side_effect = SyncSession.DoesNotExist
+ new_job_data = PeerFacilitySyncJobValidator(instance=job).data
+ mock_get.assert_called_once_with(pk="abc123", active=True)
+ self.assertEqual(new_job_data["args"], ("sync",))
+ self.assertEqual(new_job_data["kwargs"], {"test": True})
+
+ @patch("kolibri.core.auth.tasks.SyncSession.objects.get")
+ def test_validate_for_restart__resume(self, mock_get):
+ job = fake_job(
+ state=State.FAILED,
+ args=("sync",),
+ kwargs={"test": True},
+ extra_metadata={"sync_session_id": "abc123"},
+ )
+ new_job_data = PeerFacilitySyncJobValidator(instance=job).data
+ mock_get.assert_called_once_with(pk="abc123", active=True)
+ self.assertEqual(new_job_data["args"], ("resumesync",))
+ self.assertEqual(
+ new_job_data["kwargs"], {"test": True, "sync_session_id": "abc123"}
+ )
+
class TestRequestSoUDSync(TestCase):
def setUp(self):
From 7627154eba2afcb8857231040098e82c0e9a057a Mon Sep 17 00:00:00 2001
From: Blaine Jester
Date: Wed, 8 Jun 2022 17:15:59 -0700
Subject: [PATCH 3/4] Move facility task string generation
---
kolibri/core/auth/management/utils.py | 2 +-
.../plugins/device/assets/src/constants.js | 15 ++
.../FacilitiesPage/FacilityTaskPanel.vue | 181 +++++++++++++++---
.../device/assets/src/views/syncTaskUtils.js | 180 -----------------
4 files changed, 172 insertions(+), 206 deletions(-)
delete mode 100644 kolibri/plugins/device/assets/src/views/syncTaskUtils.js
diff --git a/kolibri/core/auth/management/utils.py b/kolibri/core/auth/management/utils.py
index 58101ada923..c64529e75f9 100644
--- a/kolibri/core/auth/management/utils.py
+++ b/kolibri/core/auth/management/utils.py
@@ -442,7 +442,7 @@ def _sync(self, sync_session_client, **options): # noqa: C901
noninteractive,
pull_filter,
)
- # and push our own data to server
+ # and push our own data to server
if not no_push:
self._push(
sync_session_client,
diff --git a/kolibri/plugins/device/assets/src/constants.js b/kolibri/plugins/device/assets/src/constants.js
index b9d57a09f3a..d67d7d41f57 100644
--- a/kolibri/plugins/device/assets/src/constants.js
+++ b/kolibri/plugins/device/assets/src/constants.js
@@ -55,6 +55,21 @@ export const TaskStatuses = Object.freeze({
CANCELING: 'CANCELING',
});
+export const SyncTaskStatuses = {
+ SESSION_CREATION: 'SESSION_CREATION',
+ REMOTE_QUEUING: 'REMOTE_QUEUING',
+ PULLING: 'PULLING',
+ LOCAL_DEQUEUING: 'LOCAL_DEQUEUING',
+ LOCAL_QUEUING: 'LOCAL_QUEUING',
+ PUSHING: 'PUSHING',
+ REMOTE_DEQUEUING: 'REMOTE_DEQUEUING',
+ REMOVING_FACILITY: 'REMOVING_FACILITY',
+ PENDING: 'PENDING',
+ COMPLETED: 'COMPLETED',
+ CANCELLED: 'CANCELLED',
+ FAILED: 'FAILED',
+};
+
export const TransferTypes = {
LOCALEXPORT: 'localexport',
LOCALIMPORT: 'localimport',
diff --git a/kolibri/plugins/device/assets/src/views/FacilitiesPage/FacilityTaskPanel.vue b/kolibri/plugins/device/assets/src/views/FacilitiesPage/FacilityTaskPanel.vue
index 500c42f5726..6eba98f3b45 100644
--- a/kolibri/plugins/device/assets/src/views/FacilitiesPage/FacilityTaskPanel.vue
+++ b/kolibri/plugins/device/assets/src/views/FacilitiesPage/FacilityTaskPanel.vue
@@ -1,13 +1,13 @@
import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings';
- import {
- SyncTaskStatuses,
- syncFacilityTaskDisplayInfo,
- removeFacilityTaskDisplayInfo,
- importFacilityTaskDisplayInfo,
- } from '../syncTaskUtils';
- import { TaskTypes } from '../../constants';
+ import commonTaskStrings from 'kolibri.coreVue.mixins.commonTaskStrings';
+ import bytesForHumans from 'kolibri.utils.bytesForHumans';
+
+ import { SyncTaskStatuses, TaskStatuses, TaskTypes } from '../../constants';
import FacilityTaskPanelDetails from './FacilityTaskPanelDetails';
+ const PUSH_PULL_STEPS = 7;
+ const PULL_STEPS = 4;
+
const indeterminateSyncStatuses = [
SyncTaskStatuses.SESSION_CREATION,
SyncTaskStatuses.LOCAL_QUEUING,
@@ -38,12 +38,42 @@
SyncTaskStatuses.PENDING,
];
+ const syncStatusToStepMap = {
+ [SyncTaskStatuses.SESSION_CREATION]: 1,
+ [SyncTaskStatuses.REMOTE_QUEUING]: 2,
+ [SyncTaskStatuses.PULLING]: 3,
+ [SyncTaskStatuses.LOCAL_DEQUEUING]: 4,
+ [SyncTaskStatuses.LOCAL_QUEUING]: 5,
+ [SyncTaskStatuses.PUSHING]: 6,
+ [SyncTaskStatuses.REMOTE_DEQUEUING]: 7,
+ };
+
+ const taskStatusToMessageMap = {
+ [TaskStatuses.PENDING]: 'taskWaitingStatus',
+ [TaskStatuses.QUEUED]: 'taskWaitingStatus',
+ [TaskStatuses.COMPLETED]: 'taskFinishedStatus',
+ [TaskStatuses.CANCELED]: 'taskCanceledStatus',
+ [TaskStatuses.CANCELING]: 'taskCancelingStatus',
+ [TaskStatuses.FAILED]: 'taskFailedStatus',
+ };
+
+ const syncStatusToMessageMap = {
+ ...taskStatusToMessageMap,
+ [SyncTaskStatuses.SESSION_CREATION]: 'establishingConnectionStatus',
+ [SyncTaskStatuses.REMOTE_QUEUING]: 'remotelyPreparingDataStatus',
+ [SyncTaskStatuses.PULLING]: 'receivingDataStatus',
+ [SyncTaskStatuses.LOCAL_DEQUEUING]: 'locallyIntegratingDataStatus',
+ [SyncTaskStatuses.LOCAL_QUEUING]: 'locallyPreparingDataStatus',
+ [SyncTaskStatuses.PUSHING]: 'sendingDataStatus',
+ [SyncTaskStatuses.REMOTE_DEQUEUING]: 'remotelyIntegratingDataStatus',
+ };
+
export default {
name: 'FacilityTaskPanel',
components: {
FacilityTaskPanelDetails,
},
- mixins: [commonCoreStrings],
+ mixins: [commonCoreStrings, commonTaskStrings],
props: {
task: {
type: Object,
@@ -69,34 +99,135 @@
isImportTask() {
return this.task.type === TaskTypes.SYNCPEERPULL;
},
- taskInfo() {
+ isRunning() {
+ return this.task.status === TaskStatuses.RUNNING;
+ },
+ isCompleted() {
+ return this.task.status === TaskStatuses.COMPLETED;
+ },
+ isFailed() {
+ return this.task.status === TaskStatuses.FAILED;
+ },
+ canCancel() {
if (this.isSetupImportTask) {
- return importFacilityTaskDisplayInfo(this.task);
+ return false;
}
- if (this.isSyncTask || this.isImportTask) {
- return syncFacilityTaskDisplayInfo(this.task);
+ return !(this.isCompleted || this.isFailed) && this.task.cancellable;
+ },
+ canClear() {
+ if (this.isSetupImportTask) {
+ return false;
}
- if (this.isDeleteTask) {
- return removeFacilityTaskDisplayInfo(this.task);
+ return this.task.clearable;
+ },
+ canRetry() {
+ if (this.isSetupImportTask) {
+ return false;
}
- return {};
+ return this.isFailed;
},
loaderType() {
const { sync_state = '' } = this.task;
if (indeterminateSyncStatuses.find(s => s === sync_state)) {
return 'indeterminate';
}
-
return 'determinate';
},
buttonSet() {
- if (this.taskInfo.canCancel) {
+ if (this.canCancel) {
return 'cancel';
- } else if (this.taskInfo.canClear) {
- return this.taskInfo.canRetry ? 'retry' : 'clear';
- } else {
+ } else if (this.canClear) {
+ return this.canRetry ? 'retry' : 'clear';
+ }
+ return '';
+ },
+ deviceName() {
+ if (this.task.type === TaskTypes.SYNCDATAPORTAL) {
+ return 'Kolibri Data Portal';
+ }
+ return this.formatNameWithId(
+ this.task.extra_metadata.device_name,
+ this.task.extra_metadata.device_id
+ );
+ },
+ facilityName() {
+ return this.formatNameWithId(this.task.extra_metadata.facility_name, this.task.facility_id);
+ },
+ headingMsg() {
+ if (this.isSetupImportTask) {
return '';
}
+ if (this.isImportTask) {
+ return this.getTaskString('importFacilityTaskLabel', { facilityName: this.facilityName });
+ }
+ if (this.isSyncTask) {
+ return this.getTaskString('syncFacilityTaskLabel', { facilityName: this.facilityName });
+ }
+ if (this.isDeleteTask) {
+ return this.getTaskString('removeFacilityTaskLabel', { facilityName: this.facilityName });
+ }
+ return '';
+ },
+ underHeadingMsg() {
+ if (this.isSetupImportTask) {
+ if (this.isCompleted) {
+ return this.getTaskString('importSuccessStatus', {
+ facilityName: this.task.extra_metadata.facility_name,
+ });
+ } else if (this.isFailed) {
+ return this.getTaskString('importFailedStatus', {
+ facilityName: this.task.extra_metadata.facility_name,
+ });
+ } else {
+ return '';
+ }
+ } else if (this.isSyncTask) {
+ return this.deviceName;
+ }
+ return '';
+ },
+ statusMsg() {
+ let statusMsg = this.getTaskString(
+ taskStatusToMessageMap[this.task.status] || 'taskUnknownStatus'
+ );
+ if (this.isSyncTask) {
+ if (!this.isCompleted) {
+ const syncStageMsgKey = syncStatusToMessageMap[this.task.extra_metadata.sync_state];
+ if (syncStageMsgKey) {
+ statusMsg = this.getTaskString(syncStageMsgKey);
+ }
+ }
+ if (this.isFailed) {
+ statusMsg = `${statusMsg}: ${this.task.exception}`;
+ } else if (!this.isCompleted) {
+ const syncStep = syncStatusToStepMap[this.task.extra_metadata.sync_state];
+ if (syncStep) {
+ statusMsg = this.getTaskString('syncStepAndDescription', {
+ step: syncStep,
+ total: this.isImportTask ? PULL_STEPS : PUSH_PULL_STEPS,
+ description: statusMsg,
+ });
+ }
+ }
+ } else if (this.isDeleteTask) {
+ statusMsg = this.getTaskString('removingFacilityStatus');
+ }
+
+ return statusMsg;
+ },
+ underProgressMsg() {
+ if (this.isSyncTask && this.isCompleted) {
+ return this.getTaskString('syncBytesSentAndReceived', {
+ bytesReceived: bytesForHumans(this.task.extra_metadata.bytes_received),
+ bytesSent: bytesForHumans(this.task.extra_metadata.bytes_sent),
+ });
+ }
+ return '';
+ },
+ },
+ methods: {
+ formatNameWithId(name, id) {
+ return this.coreString('nameWithIdInParens', { name, id: id.slice(0, 4) });
},
},
};
diff --git a/kolibri/plugins/device/assets/src/views/syncTaskUtils.js b/kolibri/plugins/device/assets/src/views/syncTaskUtils.js
deleted file mode 100644
index 51027ab820d..00000000000
--- a/kolibri/plugins/device/assets/src/views/syncTaskUtils.js
+++ /dev/null
@@ -1,180 +0,0 @@
-import coreStrings from 'kolibri.coreVue.mixins.commonCoreStrings';
-import taskStrings from 'kolibri.coreVue.mixins.commonTaskStrings';
-import bytesForHumans from 'kolibri.utils.bytesForHumans';
-import { TaskStatuses, TaskTypes } from '../constants';
-
-export const SyncTaskStatuses = {
- SESSION_CREATION: 'SESSION_CREATION',
- REMOTE_QUEUING: 'REMOTE_QUEUING',
- PULLING: 'PULLING',
- LOCAL_DEQUEUING: 'LOCAL_DEQUEUING',
- LOCAL_QUEUING: 'LOCAL_QUEUING',
- PUSHING: 'PUSHING',
- REMOTE_DEQUEUING: 'REMOTE_DEQUEUING',
- REMOVING_FACILITY: 'REMOVING_FACILITY',
- PENDING: 'PENDING',
- COMPLETED: 'COMPLETED',
- CANCELLED: 'CANCELLED',
- FAILED: 'FAILED',
-};
-
-const { getTaskString } = taskStrings.methods;
-const { coreString } = coreStrings.methods;
-
-const syncTaskStatusToStepMap = {
- [SyncTaskStatuses.SESSION_CREATION]: 1,
- [SyncTaskStatuses.REMOTE_QUEUING]: 2,
- [SyncTaskStatuses.PULLING]: 3,
- [SyncTaskStatuses.LOCAL_DEQUEUING]: 4,
- [SyncTaskStatuses.LOCAL_QUEUING]: 5,
- [SyncTaskStatuses.PUSHING]: 6,
- [SyncTaskStatuses.REMOTE_DEQUEUING]: 7,
-};
-
-const genericStatusToDescriptionMap = {
- [TaskStatuses.PENDING]: getTaskString('taskWaitingStatus'),
- [TaskStatuses.QUEUED]: getTaskString('taskWaitingStatus'),
- [TaskStatuses.COMPLETED]: getTaskString('taskFinishedStatus'),
- [TaskStatuses.CANCELED]: getTaskString('taskCanceledStatus'),
- [TaskStatuses.CANCELING]: getTaskString('taskCancelingStatus'),
- [TaskStatuses.FAILED]: getTaskString('taskFailedStatus'),
-};
-
-export const syncStatusToDescriptionMap = {
- ...genericStatusToDescriptionMap,
- [SyncTaskStatuses.SESSION_CREATION]: getTaskString('establishingConnectionStatus'),
- [SyncTaskStatuses.REMOTE_QUEUING]: getTaskString('remotelyPreparingDataStatus'),
- [SyncTaskStatuses.PULLING]: getTaskString('receivingDataStatus'),
- [SyncTaskStatuses.LOCAL_DEQUEUING]: getTaskString('locallyIntegratingDataStatus'),
- [SyncTaskStatuses.LOCAL_QUEUING]: getTaskString('locallyPreparingDataStatus'),
- [SyncTaskStatuses.PUSHING]: getTaskString('sendingDataStatus'),
- [SyncTaskStatuses.REMOTE_DEQUEUING]: getTaskString('remotelyIntegratingDataStatus'),
-};
-
-function formatNameWithId(name, id) {
- return coreString('nameWithIdInParens', { name, id: id.slice(0, 4) });
-}
-
-const PUSHPULLSTEPS = 7;
-const PULLSTEPS = 4;
-
-// Consolidates logic on how Sync-Facility Tasks should be displayed
-export function syncFacilityTaskDisplayInfo(task) {
- let statusMsg;
- let bytesTransferredMsg = '';
- let deviceNameMsg = '';
- let headingMsg = '';
-
- const facilityName = formatNameWithId(task.extra_metadata.facility_name, task.facility_id);
-
- if (task.type === TaskTypes.SYNCPEERPULL) {
- headingMsg = getTaskString('importFacilityTaskLabel', { facilityName });
- } else {
- headingMsg = getTaskString('syncFacilityTaskLabel', { facilityName });
- }
- // Device info isn't shown on the Setup Wizard version of panel
- if (task.type === TaskTypes.SYNCDATAPORTAL) {
- deviceNameMsg = 'Kolibri Data Portal';
- } else if (task.extra_metadata.device_name) {
- deviceNameMsg = formatNameWithId(
- task.extra_metadata.device_name,
- task.extra_metadata.device_id
- );
- }
- const syncStep = syncTaskStatusToStepMap[task.extra_metadata.sync_state];
- const statusDescription =
- syncStatusToDescriptionMap[task.extra_metadata.sync_state] ||
- syncStatusToDescriptionMap[task.status] ||
- getTaskString('taskUnknownStatus');
-
- if (task.status === TaskStatuses.COMPLETED) {
- statusMsg = getTaskString('taskFinishedStatus');
- } else if (syncStep) {
- statusMsg = getTaskString('syncStepAndDescription', {
- step: syncStep,
- total: task.type === TaskTypes.SYNCPEERPULL ? PULLSTEPS : PUSHPULLSTEPS,
- description: statusDescription,
- });
- } else {
- if (task.type === TaskTypes.SYNCLOD && task.status === TaskStatuses.FAILED)
- statusMsg = `${statusDescription}: ${task.exception}`;
- else statusMsg = statusDescription;
- }
-
- if (task.status === TaskStatuses.COMPLETED) {
- bytesTransferredMsg = getTaskString('syncBytesSentAndReceived', {
- bytesReceived: bytesForHumans(task.extra_metadata.bytes_received),
- bytesSent: bytesForHumans(task.extra_metadata.bytes_sent),
- });
- }
-
- const canClear = task.clearable;
-
- return {
- headingMsg,
- statusMsg,
- startedByMsg: getTaskString('taskStartedByLabel', {
- username: task.extra_metadata.started_by_username,
- }),
- bytesTransferredMsg,
- deviceNameMsg,
- isRunning: Boolean(syncStep) && !canClear,
- canClear,
- canCancel: !canClear && task.cancellable,
- canRetry: task.status === TaskStatuses.FAILED,
- };
-}
-
-export const removeStatusToDescriptionMap = {
- ...genericStatusToDescriptionMap,
- [TaskStatuses.RUNNING]: getTaskString('removingFacilityStatus'),
-};
-
-// Consolidates logic on how Remove-Facility Tasks should be displayed
-export function removeFacilityTaskDisplayInfo(task) {
- const facilityName = formatNameWithId(
- task.extra_metadata.facility_name,
- task.extra_metadata.facility
- );
- const statusDescription =
- removeStatusToDescriptionMap[task.status] || getTaskString('taskUnknownStatus');
-
- return {
- headingMsg: getTaskString('removeFacilityTaskLabel', { facilityName }),
- statusMsg: statusDescription,
- startedByMsg: getTaskString('taskStartedByLabel', {
- username: task.extra_metadata.started_by_username,
- }),
- isRunning: task.status === TaskStatuses.RUNNING,
- canClear: task.clearable,
- canCancel: !task.clearable && task.status !== TaskStatuses.RUNNING,
- canRetry: task.status === TaskStatuses.FAILED,
- };
-}
-
-// For the SetupWizard Import Task
-export function importFacilityTaskDisplayInfo(task) {
- // Basically takes the sync output and removes things
- const info = syncFacilityTaskDisplayInfo(task);
- info.bytesTransferredMsg = '';
- info.headingMsg = '';
-
- if (task.status === TaskStatuses.FAILED) {
- info.deviceNameMsg = getTaskString('importFailedStatus', {
- facilityName: task.extra_metadata.facility_name,
- });
- info.statusMsg = getTaskString('taskFailedStatus');
- info.isRunning = false;
- } else if (task.status === TaskStatuses.COMPLETED) {
- info.deviceNameMsg = getTaskString('importSuccessStatus', {
- facilityName: task.extra_metadata.facility_name,
- });
- info.statusMsg = getTaskString('taskFinishedStatus');
- info.isRunning = false;
- } else {
- info.deviceNameMsg = '';
- }
- info.canRetry = false;
- info.canClear = false;
- return info;
-}
From ea5f6305103e98b20cc296ff1735eff8ff05f2e1 Mon Sep 17 00:00:00 2001
From: Blaine Jester
Date: Wed, 8 Jun 2022 17:45:20 -0700
Subject: [PATCH 4/4] Translate sync utils test into component test
---
.../FacilitiesPage/FacilityTaskPanel.vue | 17 +-
.../FacilityTaskPanelDetails.vue | 2 +-
.../views/__test__/FacilityTaskPanel.spec.js | 195 +++++++++++++++
.../src/views/__test__/syncTaskUtils.spec.js | 225 ------------------
4 files changed, 211 insertions(+), 228 deletions(-)
create mode 100644 kolibri/plugins/device/assets/src/views/__test__/FacilityTaskPanel.spec.js
delete mode 100644 kolibri/plugins/device/assets/src/views/__test__/syncTaskUtils.spec.js
diff --git a/kolibri/plugins/device/assets/src/views/FacilitiesPage/FacilityTaskPanel.vue b/kolibri/plugins/device/assets/src/views/FacilitiesPage/FacilityTaskPanel.vue
index 6eba98f3b45..5bdabdd2592 100644
--- a/kolibri/plugins/device/assets/src/views/FacilitiesPage/FacilityTaskPanel.vue
+++ b/kolibri/plugins/device/assets/src/views/FacilitiesPage/FacilityTaskPanel.vue
@@ -112,7 +112,20 @@
if (this.isSetupImportTask) {
return false;
}
- return !(this.isCompleted || this.isFailed) && this.task.cancellable;
+ if (
+ [
+ TaskStatuses.CANCELING,
+ TaskStatuses.CANCELED,
+ TaskStatuses.COMPLETED,
+ TaskStatuses.FAILED,
+ ].includes(this.task.status)
+ ) {
+ return false;
+ }
+ if (this.isDeleteTask && this.isRunning) {
+ return false;
+ }
+ return this.task.cancellable;
},
canClear() {
if (this.isSetupImportTask) {
@@ -209,7 +222,7 @@
});
}
}
- } else if (this.isDeleteTask) {
+ } else if (this.isDeleteTask && this.isRunning) {
statusMsg = this.getTaskString('removingFacilityStatus');
}
diff --git a/kolibri/plugins/device/assets/src/views/FacilitiesPage/FacilityTaskPanelDetails.vue b/kolibri/plugins/device/assets/src/views/FacilitiesPage/FacilityTaskPanelDetails.vue
index c351cf1562e..50a6d123f91 100644
--- a/kolibri/plugins/device/assets/src/views/FacilitiesPage/FacilityTaskPanelDetails.vue
+++ b/kolibri/plugins/device/assets/src/views/FacilitiesPage/FacilityTaskPanelDetails.vue
@@ -72,7 +72,7 @@
-
+
= 0,
+ clearable: CLEARABLE_STATUSES.indexOf(status) >= 0,
+ ...overrides,
+ };
+}
+
+describe('FacilityTaskPanel component', () => {
+ let wrapper;
+ let task;
+
+ describe('when provided a sync task', () => {
+ beforeAll(() => {
+ task = makeTask(TaskStatuses.PENDING);
+ wrapper = makeWrapper(task);
+ });
+
+ it('should produce the proper heading message', () => {
+ ALL_STATUSES.forEach(status => {
+ wrapper.setProps({ task: makeTask(status) });
+ expect(wrapper.vm.headingMsg).toEqual("Sync 'generic facility' (fac1)");
+ });
+ });
+
+ it('should produce the proper under-heading message', () => {
+ ALL_STATUSES.forEach(status => {
+ wrapper.setProps({ task: makeTask(status) });
+ expect(wrapper.vm.underHeadingMsg).toEqual("'generic device' (dev1)");
+ });
+ });
+
+ it('should produce a message describing the bandwidth used when FINISHED', () => {
+ ALL_STATUSES.forEach(status => {
+ wrapper.setProps({ task: makeTask(status) });
+ if (status === TaskStatuses.COMPLETED) {
+ expect(wrapper.vm.underProgressMsg).toEqual('1 MB sent • 500 MB received');
+ } else {
+ expect(wrapper.vm.underProgressMsg).toEqual('');
+ }
+ });
+ });
+
+ const simpleStatusesMsgTests = [
+ [TaskStatuses.PENDING, 'Waiting'],
+ [TaskStatuses.COMPLETED, 'Finished'],
+ [TaskStatuses.CANCELED, 'Canceled'],
+ [TaskStatuses.CANCELING, 'Canceling'],
+ [TaskStatuses.FAILED, 'Failed: RuntimeError'],
+ ];
+ test.each(simpleStatusesMsgTests)(
+ 'should produce the proper status message with task status %s',
+ (status, msg) => {
+ wrapper.setProps({ task: makeTask(status) });
+ expect(wrapper.vm.statusMsg).toEqual(msg);
+ }
+ );
+
+ const orderedStatusesMsgTests = [
+ [SyncTaskStatuses.SESSION_CREATION, '1 of 7: Establishing connection'],
+ [SyncTaskStatuses.REMOTE_QUEUING, '2 of 7: Remotely preparing data'],
+ [SyncTaskStatuses.PULLING, '3 of 7: Receiving data'],
+ [SyncTaskStatuses.LOCAL_DEQUEUING, '4 of 7: Locally integrating received data'],
+ [SyncTaskStatuses.LOCAL_QUEUING, '5 of 7: Locally preparing data to send'],
+ [SyncTaskStatuses.PUSHING, '6 of 7: Sending data'],
+ [SyncTaskStatuses.REMOTE_DEQUEUING, '7 of 7: Remotely integrating data'],
+ ];
+
+ test.each(orderedStatusesMsgTests)(
+ 'should produce the proper status message with sync-specific status %s',
+ (status, msg) => {
+ wrapper.setProps({ task: makeTask(status) });
+ expect(wrapper.vm.statusMsg).toEqual(msg);
+ }
+ );
+
+ const controlAndStatusTests = [
+ // [status, canClear/hideCancel, isRunning, canCancel]
+ [TaskStatuses.PENDING, false, false, false],
+ [TaskStatuses.CANCELED, true, false, false],
+ [TaskStatuses.CANCELING, false, false, false],
+ [TaskStatuses.FAILED, true, false, false],
+ [SyncTaskStatuses.SESSION_CREATION, false, true, true],
+ [SyncTaskStatuses.REMOTE_QUEUING, false, true, true],
+ [SyncTaskStatuses.PULLING, false, true, true],
+ [SyncTaskStatuses.LOCAL_DEQUEUING, false, true, false],
+ [SyncTaskStatuses.LOCAL_QUEUING, false, true, false],
+ [SyncTaskStatuses.PUSHING, false, true, true],
+ [SyncTaskStatuses.REMOTE_DEQUEUING, false, true, false],
+ [SyncTaskStatuses.COMPLETED, true, false, false],
+ ];
+
+ test.each(controlAndStatusTests)(
+ 'it should produce proper clear/cancel/retry conditions for status %s',
+ (status, canClear, isRunning, canCancel) => {
+ wrapper.setProps({ task: makeTask(status) });
+ expect(wrapper.vm.canClear).toBe(canClear);
+ expect(wrapper.vm.isRunning).toBe(isRunning);
+ expect(wrapper.vm.canCancel).toBe(canCancel);
+ }
+ );
+ });
+
+ describe('when provided a facility deletion task', () => {
+ beforeAll(() => {
+ task = makeTask(TaskStatuses.PENDING, TaskTypes.DELETEFACILITY);
+ wrapper = makeWrapper(task);
+ });
+
+ it('should produce the proper heading message', () => {
+ ALL_STATUSES.forEach(status => {
+ wrapper.setProps({ task: makeTask(status, task.type) });
+ expect(wrapper.vm.headingMsg).toEqual("Remove 'generic facility' (fac1)");
+ });
+ });
+
+ const simpleStatusesMsgTests = [
+ [TaskStatuses.PENDING, 'Waiting'],
+ [TaskStatuses.COMPLETED, 'Finished'],
+ [TaskStatuses.CANCELED, 'Canceled'],
+ [TaskStatuses.CANCELING, 'Canceling'],
+ [TaskStatuses.FAILED, 'Failed'],
+ [TaskStatuses.RUNNING, 'Removing facility'],
+ ];
+
+ test.each(simpleStatusesMsgTests)(
+ 'should produce the proper status message with task status %s',
+ (status, msg) => {
+ wrapper.setProps({ task: makeTask(status, task.type) });
+ expect(wrapper.vm.statusMsg).toEqual(msg);
+ }
+ );
+
+ const controlAndStatusTests = [
+ // [status, canCancel, canClear, canRetry]
+ [TaskStatuses.PENDING, true, false, false],
+ [TaskStatuses.CANCELED, false, true, false],
+ [TaskStatuses.CANCELING, false, false, false],
+ [TaskStatuses.FAILED, false, true, true],
+ [TaskStatuses.RUNNING, false, false, false],
+ [TaskStatuses.COMPLETED, false, true, false],
+ ];
+
+ test.each(controlAndStatusTests)(
+ 'it should produce proper clear/cancel/retry conditions for status %s',
+ (status, canCancel, canClear, canRetry) => {
+ wrapper.setProps({ task: makeTask(status, task.type, { cancellable: true }) });
+ expect(wrapper.vm.canClear).toBe(canClear);
+ expect(wrapper.vm.canCancel).toBe(canCancel);
+ expect(wrapper.vm.canRetry).toBe(canRetry);
+ }
+ );
+ });
+});
diff --git a/kolibri/plugins/device/assets/src/views/__test__/syncTaskUtils.spec.js b/kolibri/plugins/device/assets/src/views/__test__/syncTaskUtils.spec.js
deleted file mode 100644
index 4a837742e4d..00000000000
--- a/kolibri/plugins/device/assets/src/views/__test__/syncTaskUtils.spec.js
+++ /dev/null
@@ -1,225 +0,0 @@
-import {
- syncFacilityTaskDisplayInfo,
- syncStatusToDescriptionMap,
- removeStatusToDescriptionMap,
- removeFacilityTaskDisplayInfo,
- SyncTaskStatuses,
-} from '../syncTaskUtils';
-import { TaskStatuses, TaskTypes } from '../../constants';
-
-const CLEARABLE_STATUSES = ['COMPLETED', 'CANCELED', 'FAILED'];
-
-describe('syncTaskUtils.syncFacilityTaskDisplayInfo', () => {
- const CANCELLABLE_SYNC_STATES = [
- SyncTaskStatuses.SESSION_CREATION,
- SyncTaskStatuses.PULLING,
- SyncTaskStatuses.PUSHING,
- SyncTaskStatuses.REMOTE_QUEUING,
- ];
-
- function makeTask(sync_state) {
- let status;
- if (!TaskStatuses[sync_state]) {
- status = TaskStatuses.RUNNING;
- } else {
- status = sync_state;
- sync_state = undefined;
- }
- return {
- type: TaskTypes.SYNCPEERFULL,
- status,
- facility_id: 'fac123',
- extra_metadata: {
- sync_state,
- device_name: 'generic device',
- device_id: 'dev123',
- facility_name: 'generic facility',
- facility: 'fac123',
- started_by_username: 'generic user',
- bytes_sent: 1000000,
- bytes_received: 500000000,
- },
- cancellable: CANCELLABLE_SYNC_STATES.indexOf(sync_state) >= 0,
- clearable: CLEARABLE_STATUSES.indexOf(status) >= 0,
- };
- }
-
- const ALL_STATUSES = Object.keys(syncStatusToDescriptionMap);
-
- it('displays the correct header for facility-sync tasks', () => {
- const task = makeTask('RUNNING');
- const displayInfo = syncFacilityTaskDisplayInfo(task);
- expect(displayInfo.headingMsg).toEqual("Sync 'generic facility' (fac1)");
- });
-
- it('displays the correct header for facility-import tasks', () => {
- const task = makeTask('RUNNING');
- task.type = TaskTypes.SYNCPEERPULL;
- const displayInfo = syncFacilityTaskDisplayInfo(task);
- expect(displayInfo.headingMsg).toEqual("Import 'generic facility' (fac1)");
- });
-
- it('display title, started by username, and device name are invariant wrt status', () => {
- const task = {
- status: null,
- facility_id: 'fac123',
- extra_metadata: {
- device_name: 'invariant device',
- device_id: 'dev123',
- facility: 'fac123',
- facility_name: 'invariant facility',
- started_by_username: 'invariant user',
- },
- };
-
- ALL_STATUSES.forEach(status => {
- task.status = status;
- expect(syncFacilityTaskDisplayInfo(task)).toMatchObject({
- headingMsg: "Sync 'invariant facility' (fac1)",
- startedByMsg: "Started by 'invariant user'",
- deviceNameMsg: "'invariant device' (dev1)",
- });
- });
- });
-
- const simpleStatusesMsgTests = [
- ['PENDING', 'Waiting'],
- ['COMPLETED', 'Finished'],
- ['CANCELED', 'Canceled'],
- ['CANCELING', 'Canceling'],
- ['FAILED', 'Failed'],
- ];
- test.each(simpleStatusesMsgTests)('statusMsg is correct with %s status', (status, msg) => {
- const task = makeTask(status);
- expect(syncFacilityTaskDisplayInfo(task)).toMatchObject({
- statusMsg: msg,
- });
- });
-
- const orderedStatusesMsgTests = [
- ['SESSION_CREATION', '1 of 7: Establishing connection'],
- ['REMOTE_QUEUING', '2 of 7: Remotely preparing data'],
- ['PULLING', '3 of 7: Receiving data'],
- ['LOCAL_DEQUEUING', '4 of 7: Locally integrating received data'],
- ['LOCAL_QUEUING', '5 of 7: Locally preparing data to send'],
- ['PUSHING', '6 of 7: Sending data'],
- ['REMOTE_DEQUEUING', '7 of 7: Remotely integrating data'],
- ];
-
- test.each(orderedStatusesMsgTests)(
- 'messages are correct with sync-specific status %s',
- (status, msg) => {
- const task = makeTask(status);
- expect(syncFacilityTaskDisplayInfo(task)).toMatchObject({
- statusMsg: msg,
- });
- }
- );
-
- it('if task is FINISHED, it has a bytesTransferredMsg', () => {
- ALL_STATUSES.forEach(status => {
- const task = makeTask(status);
- const { bytesTransferredMsg } = syncFacilityTaskDisplayInfo(task);
- if (status === 'COMPLETED') {
- expect(bytesTransferredMsg).toEqual('1 MB sent • 500 MB received');
- } else {
- expect(bytesTransferredMsg).toEqual('');
- }
- });
- });
-
- const controlAndStatusTests = [
- // [status, canClear/hideCancel, isRunning, canCancel]
- ['PENDING', false, false, false],
- ['CANCELED', true, false, false],
- ['CANCELING', false, false, false],
- ['FAILED', true, false, false],
- ['SESSION_CREATION', false, true, true],
- ['REMOTE_QUEUING', false, true, true],
- ['PULLING', false, true, true],
- ['LOCAL_DEQUEUING', false, true, false],
- ['LOCAL_QUEUING', false, true, false],
- ['PUSHING', false, true, true],
- ['REMOTE_DEQUEUING', false, true, false],
- ['COMPLETED', true, false, false],
- ];
-
- test.each(controlAndStatusTests)(
- 'flags for showing clear/cancel/retry buttons are correct for status %s',
- (status, canClear, isRunning, canCancel) => {
- const task = makeTask(status);
- expect(syncFacilityTaskDisplayInfo(task)).toMatchObject({
- canClear,
- canCancel,
- canRetry: status === 'FAILED',
- isRunning,
- });
- }
- );
-});
-
-describe('syncTaskUtils.removeFacilityTaskDisplayInfo', () => {
- function makeTask(status) {
- return {
- type: TaskTypes.DELETEFACILITY,
- status,
- clearable: CLEARABLE_STATUSES.indexOf(status) >= 0,
- facility_id: 'fac123',
- extra_metadata: {
- facility: 'fac123',
- facility_name: 'removed facility',
- started_by_username: 'removing user',
- },
- };
- }
-
- const ALL_STATUSES = Object.keys(removeStatusToDescriptionMap);
-
- it('title and started by username is invariant wrt status', () => {
- ALL_STATUSES.forEach(status => {
- const task = makeTask(status);
- expect(removeFacilityTaskDisplayInfo(task)).toMatchObject({
- headingMsg: "Remove 'removed facility' (fac1)",
- startedByMsg: "Started by 'removing user'",
- });
- });
- });
-
- const simpleStatusesMsgTests = [
- ['PENDING', 'Waiting'],
- ['COMPLETED', 'Finished'],
- ['CANCELED', 'Canceled'],
- ['CANCELING', 'Canceling'],
- ['FAILED', 'Failed'],
- ['RUNNING', 'Removing facility'],
- ];
-
- test.each(simpleStatusesMsgTests)('statusMsg is correct with %s status', (status, msg) => {
- const task = makeTask(status);
- expect(removeFacilityTaskDisplayInfo(task)).toMatchObject({
- statusMsg: msg,
- });
- });
-
- const controlAndStatusTests = [
- // [status, canCancel, canClear, canRetry]
- ['PENDING', true, false, false],
- ['CANCELED', false, true, false],
- ['CANCELING', true, false, false],
- ['FAILED', false, true, true],
- ['RUNNING', false, false, false],
- ['COMPLETED', false, true, false],
- ];
-
- test.each(controlAndStatusTests)(
- 'flags for showing clear/cancel/retry buttons are correct for status %s',
- (status, canCancel, canClear, canRetry) => {
- const task = makeTask(status);
- expect(removeFacilityTaskDisplayInfo(task)).toMatchObject({
- canClear,
- canCancel,
- canRetry,
- });
- }
- );
-});