Repository: learningequality/kolibri

Issue:
Move task-is-clearable flag to the API layer, instead of using frontend code to derive this from `task.status`
This is a bit of tech debt that we run into when developing UIs for presenting and managing tasks

There is this piece of JS that is used in a lot of places to determine whether a task is clearable

https://github.com/learningequality/kolibri/blob/577c10e9670a10dbc1955dffc6e7dfe6098bbe57/kolibri/plugins/device/assets/src/constants.js#L58-L61

I think this concept of "clearability" belongs on the API layer instead not just because it is so ubiquitous in the UI (and could save us from having to import this utility function all over the place), but because "clearability" is tied to whether a Task ID is a valid ID for the `/api/tasks/cleartask(s)` endpoints. 

This could probably be implemented by adding some code to this function here:

https://github.com/learningequality/kolibri/blob/release-v0.14.x/kolibri/core/tasks/api.py#L1222-L1244

Then most/all of the instances where we call `isTaskClearble(task)` could be replaced by simply reading `task.is_clearable` etc.


Discussion:

Resolving patch:
diff --git a/kolibri/core/tasks/api.py b/kolibri/core/tasks/api.py
--- a/kolibri/core/tasks/api.py
+++ b/kolibri/core/tasks/api.py
@@ -1229,6 +1229,7 @@ def _job_to_response(job):
             "progress": [],
             "id": None,
             "cancellable": False,
+            "clearable": False,
         }
     else:
         output = {
@@ -1238,6 +1239,7 @@ def _job_to_response(job):
             "percentage": job.percentage_progress,
             "id": job.job_id,
             "cancellable": job.cancellable,
+            "clearable": job.state in [State.FAILED, State.CANCELED, State.COMPLETED],
         }
         output.update(job.extra_metadata)
         return output


Test patch:
diff --git a/kolibri/core/tasks/test/test_api.py b/kolibri/core/tasks/test/test_api.py
--- a/kolibri/core/tasks/test/test_api.py
+++ b/kolibri/core/tasks/test/test_api.py
@@ -6,6 +6,7 @@
 from mock import call
 from mock import Mock
 from mock import patch
+from mock import PropertyMock
 from rest_framework.exceptions import AuthenticationFailed
 from rest_framework.exceptions import ParseError
 from rest_framework.exceptions import PermissionDenied
@@ -89,6 +90,34 @@ def test_task_get_no_task(self, queue_mock):
         )
         self.assertEqual(response.status_code, 404)
 
+    def test_tasks_clearable_flag(self, queue_mock):
+        with patch(
+            "kolibri.core.tasks.queue.Queue.jobs", new_callable=PropertyMock
+        ) as jobs_mock:
+            jobs_mock.return_value = [
+                fake_job(state=state)
+                for state in [
+                    # not clearable
+                    State.SCHEDULED,
+                    State.QUEUED,
+                    State.RUNNING,
+                    State.CANCELING,
+                    # clearable
+                    State.FAILED,
+                    State.CANCELED,
+                    State.COMPLETED,
+                ]
+            ]
+            response = self.client.get(reverse("kolibri:core:task-list"))
+
+            def assert_clearable(index, expected):
+                self.assertEqual(response.data[index]["clearable"], expected)
+
+            for i in [0, 1, 2, 3]:
+                assert_clearable(i, False)
+            for i in [4, 5, 6]:
+                assert_clearable(i, True)
+
 
 class TaskAPIPermissionsTestCase(APITestCase):
     def setUp(self):
diff --git a/kolibri/plugins/device/assets/src/views/ManageTasksPage/__test__/TaskPanel.spec.js b/kolibri/plugins/device/assets/src/views/ManageTasksPage/__test__/TaskPanel.spec.js
--- a/kolibri/plugins/device/assets/src/views/ManageTasksPage/__test__/TaskPanel.spec.js
+++ b/kolibri/plugins/device/assets/src/views/ManageTasksPage/__test__/TaskPanel.spec.js
@@ -10,6 +10,7 @@ describe('TaskPanel', () => {
   const exportTask = {
     type: 'DISKCONTENTEXPORT',
     status: 'CANCELED',
+    clearable: true,
     channel_name: 'Canceled disk export channel test',
     started_by_username: 'Tester',
     file_size: 5000,
diff --git a/kolibri/plugins/device/assets/src/views/__test__/syncTaskUtils.spec.js b/kolibri/plugins/device/assets/src/views/__test__/syncTaskUtils.spec.js
--- a/kolibri/plugins/device/assets/src/views/__test__/syncTaskUtils.spec.js
+++ b/kolibri/plugins/device/assets/src/views/__test__/syncTaskUtils.spec.js
@@ -6,6 +6,8 @@ import {
   SyncTaskStatuses,
 } from '../syncTaskUtils';
 
+const CLEARABLE_STATUSES = ['COMPLETED', 'CANCELED', 'FAILED'];
+
 describe('syncTaskUtils.syncFacilityTaskDisplayInfo', () => {
   const CANCELLABLE_STATUSES = [
     SyncTaskStatuses.SESSION_CREATION,
@@ -28,6 +30,7 @@ describe('syncTaskUtils.syncFacilityTaskDisplayInfo', () => {
       bytes_sent: 1000000,
       bytes_received: 500000000,
       cancellable: CANCELLABLE_STATUSES.indexOf(status) >= 0,
+      clearable: CLEARABLE_STATUSES.indexOf(status) >= 0,
     };
   }
 
@@ -134,6 +137,7 @@ describe('syncTaskUtils.removeFacilityTaskDisplayInfo', () => {
     return {
       type: 'DELETEFACILITY',
       status,
+      clearable: CLEARABLE_STATUSES.indexOf(status) >= 0,
       facility_name: 'removed facility',
       facility: 'fac123',
       started_by_username: 'removing user',