Repository: learningequality/kolibri

Issue:
Can't set task priority via the API
<!--
Instructions:
 * Fill out the sections below, replace …'s with information about your issue
 * Use the 'preview' function above this text box to verify formatting before submitting
-->

## Observed behavior
<!--
Description of the behavior that was observed, including screenshots or other references when applicable
-->

If you enqueue a task from the backend, you can specify its priority. However, if you enqueue a task via the tasks API you can't. It would be nice to be able to do this so that the frontend can prioritize the tasks it's scheduling.

## Errors and logs
<!--
Relevant logs from:
 * the command line
 * ~/.kolibri/logs/kolibri.txt
 * the browser console

Please wrap errors in triple backticks for clean formatting like this:
```
01:10 info: something happened
01:12 error: something bad happened
```
-->

There are no errors. Tasks scheduled via the API use the priority declared in the `register_task` decorator.

## Expected behavior
<!--
Description of what behavior was expected but did not occur
-->

The priority of each task can be specified in the list sent to the tasks API.

## User-facing consequences
<!--
Implications and real-world consequences for learners, coaches, admins, and other users of the application
-->

The way most users schedule tasks is via the API as called by the frontend. User scheduled tasks are likely of higher importance than other non-user scheduled tasks, so it would be nice for the frontend to specify a higher priority. Alternatively, the user may know that the task they're scheduling isn't that important to them and can be run at some later time. The frontend would specify a lower priority in that case.

## Steps to reproduce
<!--
Precise steps that someone else can follow in order to see this behavior
-->

Start Kolibri with 0 regular priority workers and 1 high priority worker. Go to the device content management page and start several tasks of different types. Then go the device task panel and note that the order that they start is not the same as the order they were scheduled.

## Context
<!--
Tell us about your environment, including:
 * Kolibri version
 * Operating system
 * Browser
-->

0.16.0-beta1, Linux


Discussion:
Happy to receive a PR for this.
Hello! 
Tasks can either have a regular priority of 10 or a high priority of 5.
Here, would we just have to make it so that all tasks enqueued by the user have a priority of 5 (high)?
No, that's not quite what is desirable here, rather that tasks initated via the API should default to the priority set on the task itself, but that the payload sent to the API endpoint _can_ specify the priority if so desired. 

This would be best achieved by adding an optional `priority` field to the EnqueueArgs serializer: https://github.com/learningequality/kolibri/blob/release-v0.16.x/kolibri/core/tasks/validation.py#L6

and then ensuring that the priority field is passed in if set during job queueing: https://github.com/learningequality/kolibri/blob/release-v0.16.x/kolibri/core/tasks/api.py#L144

This will need to be done a little bit carefully to ensure that the default priority of tasks does not get overridden when no priority is passed in the API payload.
Just to confirm, the priority can only be either 5 or 10? And the default priority of the task will be used but an option must be added to have a `priority` argument in the payload of the request to the tasks api.

thank you in advance 
The possible priorities are enumerated here, the definition of the serializer field should reference this, so that if new priorities are added it will get updated in step.

https://github.com/learningequality/kolibri/blob/release-v0.16.x/kolibri/core/tasks/job.py#L92

Yes, the priority field would be added to the enqueue args, but it would be optional, so if it isn't specified the priority is the default for the task. 

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
@@ -150,7 +150,7 @@ def _enqueue_job_based_on_enqueue_args(self, registered_task, job, enqueue_args)
                 enqueue_args["enqueue_at"],
                 job,
                 queue=registered_task.queue,
-                priority=registered_task.priority,
+                priority=enqueue_args.get("priority", registered_task.priority),
                 interval=enqueue_args.get("repeat_interval", 0),
                 repeat=enqueue_args.get("repeat", 0),
                 retry_interval=enqueue_args.get("retry_interval", None),
@@ -160,7 +160,7 @@ def _enqueue_job_based_on_enqueue_args(self, registered_task, job, enqueue_args)
                 enqueue_args["enqueue_in"],
                 job,
                 queue=registered_task.queue,
-                priority=registered_task.priority,
+                priority=enqueue_args.get("priority", registered_task.priority),
                 interval=enqueue_args.get("repeat_interval", 0),
                 repeat=enqueue_args.get("repeat", 0),
                 retry_interval=enqueue_args.get("retry_interval", None),
@@ -168,7 +168,7 @@ def _enqueue_job_based_on_enqueue_args(self, registered_task, job, enqueue_args)
         return job_storage.enqueue_job(
             job,
             queue=registered_task.queue,
-            priority=registered_task.priority,
+            priority=enqueue_args.get("priority", registered_task.priority),
             retry_interval=enqueue_args.get("retry_interval", None),
         )
 
diff --git a/kolibri/core/tasks/validation.py b/kolibri/core/tasks/validation.py
--- a/kolibri/core/tasks/validation.py
+++ b/kolibri/core/tasks/validation.py
@@ -2,6 +2,8 @@
 from rest_framework import serializers
 from rest_framework.exceptions import ValidationError
 
+from .job import Priority
+
 
 class EnqueueArgsSerializer(serializers.Serializer):
     """
@@ -15,6 +17,7 @@ class EnqueueArgsSerializer(serializers.Serializer):
     retry_interval = serializers.IntegerField(
         required=False, allow_null=True, min_value=0
     )
+    priority = serializers.ChoiceField(choices=Priority.Priorities, required=False)
 
     def validate(self, data):
         if data.get("enqueue_at") and data.get("enqueue_in"):


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
@@ -644,6 +644,14 @@ def test_erroneous_request(self, mock_job_storage):
                 "enqueue_at": self.enqueue_at_datetime,
                 "repeat": None,
             },
+            {  # Invalid priority specified.
+                "enqueue_at": self.enqueue_at_datetime,
+                "priority": 1,
+            },
+            {  # Priority set to None
+                "enqueue_at": self.enqueue_at_datetime,
+                "priority": None,
+            },
         ]
 
         for err_enq_arg in erroneous_enqueue_args:
@@ -694,6 +702,10 @@ def test_acceptable_request(self, mock_job_storage):
             {
                 "retry_interval": 900,
             },
+            {
+                "enqueue_at": self.enqueue_at_datetime,
+                "priority": 10,
+            },
         ]
 
         mock_job_storage.get_job.return_value = fake_job(state=State.QUEUED)