Repository: nautobot/nautobot

Issue:
Job Buttons do not honor the Job task_queue
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reporting reproducible bugs. If you need assistance
    with Nautobot installation, or if you have a general question, please start a
    discussion instead: https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Nautobot version (Docker tag too if applicable): 1.6.11
* Python version: 3.11
* Database platform, version: NA
* Middleware(s): NA

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of Nautobot. Begin with the
    creation of any necessary database objects and call out every operation
    being performed explicitly. If reporting a bug in the REST API, be sure to
    reconstruct the raw HTTP request(s) being made: Don't rely on a client
    library such as pynautobot.
-->
### Steps to Reproduce
1. Create a JobButtonReciever that uses a queue other than default
2. Create a Job Button that points to the JobButtonReciever and connect it to a contenttype (like dcim.devices)
3. Click the Job Button on the contenttype from above.

<!-- What did you expect to happen? -->
### Expected Behavior
The Job runs under the queue that is configured for JobButtonReceiver.

<!-- What happened instead? -->
### Observed Behavior
The Job runs under the default queue (which in my case causes an error, but may not in other use cases).


Discussion:
Thanks!

We can’t add (in 1.6.x) an option to the JobButton data model to let you configure a queue (as that’d be a database migration) but I agree that we could at least pass a `_task_queue = JobButtonReceiverClass.task_queues[0]` as part of the form submission.


Resolving patch:
diff --git a/nautobot/extras/templatetags/job_buttons.py b/nautobot/extras/templatetags/job_buttons.py
--- a/nautobot/extras/templatetags/job_buttons.py
+++ b/nautobot/extras/templatetags/job_buttons.py
@@ -1,6 +1,7 @@
 from collections import OrderedDict
 
 from django import template
+from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.urls import reverse
 from django.utils.html import format_html
@@ -27,6 +28,7 @@
 <input type="hidden" name="object_pk" value="{object_pk}">
 <input type="hidden" name="object_model_name" value="{object_model_name}">
 <input type="hidden" name="_schedule_type" value="immediately">
+<input type="hidden" name="_task_queue" value="{task_queue}">
 <input type="hidden" name="_return_url" value="{redirect_path}">
 """
 
@@ -105,12 +107,17 @@ def _render_job_button_for_obj(job_button, obj, context, content_type):
 
     # Disable buttons if the user doesn't have permission to run the underlying Job.
     has_run_perm = Job.objects.check_perms(context["user"], instance=job_button.job, action="run")
+    try:
+        _task_queue = job_button.job.task_queues[0]
+    except IndexError:
+        _task_queue = settings.CELERY_TASK_DEFAULT_QUEUE
     hidden_inputs = format_html(
         HIDDEN_INPUTS,
         csrf_token=context["csrf_token"],
         object_pk=obj.pk,
         object_model_name=f"{content_type.app_label}.{content_type.model}",
         redirect_path=context["request"].path,
+        task_queue=_task_queue,
     )
     template_args = {
         "button_id": job_button.pk,


Test patch:
diff --git a/nautobot/extras/tests/test_views.py b/nautobot/extras/tests/test_views.py
--- a/nautobot/extras/tests/test_views.py
+++ b/nautobot/extras/tests/test_views.py
@@ -3,6 +3,7 @@
 import urllib.parse
 import uuid
 
+from django.conf import settings
 from django.contrib.auth import get_user_model
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
@@ -2019,10 +2020,11 @@ class JobButtonRenderingTestCase(TestCase):
 
     def setUp(self):
         super().setUp()
+        self.job = Job.objects.get(job_class_name="TestJobButtonReceiverSimple")
         self.job_button_1 = JobButton(
             name="JobButton 1",
             text="JobButton {{ obj.name }}",
-            job=Job.objects.get(job_class_name="TestJobButtonReceiverSimple"),
+            job=self.job,
             confirmation=False,
         )
         self.job_button_1.validated_save()
@@ -2047,6 +2049,26 @@ def test_view_object_with_job_button(self):
         self.assertIn(f"JobButton {self.location_type.name}", content, content)
         self.assertIn("Click me!", content, content)
 
+    def test_task_queue_hidden_input_is_present(self):
+        """
+        Ensure that the job button respects the job class' task_queues and the job class task_queues[0]/default is passed as a hidden form input.
+        """
+        self.job.task_queues_override = True
+        self.job.task_queues = ["overriden_queue", "default", "priority"]
+        self.job.save()
+        response = self.client.get(self.location_type.get_absolute_url(), follow=True)
+        self.assertEqual(response.status_code, 200)
+        content = extract_page_body(response.content.decode(response.charset))
+        self.assertIn(f'<input type="hidden" name="_task_queue" value="{self.job.task_queues[0]}">', content, content)
+        self.job.task_queues_override = False
+        self.job.save()
+        response = self.client.get(self.location_type.get_absolute_url(), follow=True)
+        self.assertEqual(response.status_code, 200)
+        content = extract_page_body(response.content.decode(response.charset))
+        self.assertIn(
+            f'<input type="hidden" name="_task_queue" value="{settings.CELERY_TASK_DEFAULT_QUEUE}">', content, content
+        )
+
     def test_view_object_with_unsafe_text(self):
         """Ensure that JobButton text can't be used as a vector for XSS."""
         self.job_button_1.text = '<script>alert("Hello world!")</script>'