Просмотр исходного кода

Fixes #23332: Mark request copies as non-HTMX so background bulk edits work (#23334)

Default NetBoxFakeRequest.htmx to False so background views can use
HTMX helpers without raising AttributeError. Define the default on
the class to also support requests queued before the upgrade.

Add regression tests for copied HTMX requests, requests without
HTMX state, and background bulk edits.
bctiemann 1 день назад
Родитель
Сommit
958c38929b
3 измененных файлов с 55 добавлено и 2 удалено
  1. 34 1
      netbox/netbox/tests/test_jobs.py
  2. 4 0
      netbox/utilities/request.py
  3. 17 1
      netbox/utilities/tests/test_request.py

+ 34 - 1
netbox/netbox/tests/test_jobs.py

@@ -2,12 +2,17 @@ import uuid
 from datetime import timedelta
 from datetime import timedelta
 from unittest.mock import patch
 from unittest.mock import patch
 
 
-from django.test import TestCase
+from django.test import RequestFactory, TestCase
+from django.urls import reverse
 from django.utils import timezone
 from django.utils import timezone
 
 
 from core.choices import JobStatusChoices
 from core.choices import JobStatusChoices
 from core.exceptions import JobFailed
 from core.exceptions import JobFailed
 from core.models import DataSource, Job
 from core.models import DataSource, Job
+from dcim.models import Site
+from dcim.views import SiteBulkEditView
+from users.models import User
+from utilities.request import copy_safe_request
 from utilities.testing import disable_warnings
 from utilities.testing import disable_warnings
 from utilities.testing.mixins import RQQueueTestMixin
 from utilities.testing.mixins import RQQueueTestMixin
 
 
@@ -317,3 +322,31 @@ class SystemJobTestCase(BaseJobRunnerTestCase):
             interval=interval,
             interval=interval,
         )
         )
         self.assertEqual(enqueued.count(), 2)
         self.assertEqual(enqueued.count(), 2)
+
+
+class AsyncViewJobTestCase(TestCase):
+    """
+    Test running UI views as background jobs.
+    """
+
+    def test_bulk_edit(self):
+        user = User.objects.create_superuser(username='testuser')
+        sites = (
+            Site(name='Site 1', slug='site-1'),
+            Site(name='Site 2', slug='site-2'),
+        )
+        Site.objects.bulk_create(sites)
+        request = RequestFactory().post(reverse('dcim:site_bulk_edit'), data={
+            '_apply': True,
+            'pk': [site.pk for site in sites],
+            'description': 'Edited in a background job',
+            'background_job': True,
+        })
+        request.user = user
+        request.id = uuid.uuid4()
+        job = Job.objects.create(name='Bulk edit', user=user, job_id=uuid.uuid4())
+
+        # Run the view as process_request_as_job() would have it run in a worker
+        AsyncViewJob(job).run(view_cls=SiteBulkEditView, request=copy_safe_request(request))
+
+        self.assertEqual(Site.objects.filter(description='Edited in a background job').count(), len(sites))

+ 4 - 0
netbox/utilities/request.py

@@ -31,6 +31,10 @@ class NetBoxFakeRequest:
     A fake request object which is explicitly defined at the module level so it is able to be pickled. It simply
     A fake request object which is explicitly defined at the module level so it is able to be pickled. It simply
     takes what is passed to it as kwargs on init and sets them as instance variables.
     takes what is passed to it as kwargs on init and sets them as instance variables.
     """
     """
+    # Fake requests run outside an HTTP exchange and are not HTMX requests.
+    # A class-level default also covers requests pickled before this attribute existed.
+    htmx = False
+
     def __init__(self, _dict):
     def __init__(self, _dict):
         self.__dict__ = _dict
         self.__dict__ = _dict
 
 

+ 17 - 1
netbox/utilities/tests/test_request.py

@@ -1,9 +1,11 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.contrib.auth.models import AnonymousUser
 from django.contrib.auth.models import AnonymousUser
 from django.test import RequestFactory, TestCase, override_settings
 from django.test import RequestFactory, TestCase, override_settings
+from django_htmx.middleware import HtmxDetails
 from netaddr import IPAddress
 from netaddr import IPAddress
 
 
-from utilities.request import copy_safe_request, get_client_ip, get_safe_request_context
+from utilities.htmx import htmx_partial
+from utilities.request import NetBoxFakeRequest, copy_safe_request, get_client_ip, get_safe_request_context
 
 
 User = get_user_model()
 User = get_user_model()
 
 
@@ -51,6 +53,20 @@ class CopySafeRequestTestCase(TestCase):
         self.assertEqual(fake.method, 'GET')
         self.assertEqual(fake.method, 'GET')
         self.assertEqual(fake.GET.get('foo'), 'bar')
         self.assertEqual(fake.GET.get('foo'), 'bar')
 
 
+    def test_copy_is_not_an_htmx_request(self):
+        """Copied requests are non-HTMX, regardless of the original request."""
+        request = self._make_request(HTTP_HX_REQUEST='true')
+        request.htmx = HtmxDetails(request)
+        self.assertTrue(request.htmx)
+        fake = copy_safe_request(request)
+        self.assertFalse(fake.htmx)
+        self.assertFalse(htmx_partial(fake))
+
+    def test_fake_request_without_htmx_key(self):
+        """The HTMX default also applies to requests serialized by older releases."""
+        fake = NetBoxFakeRequest({'path': '/'})
+        self.assertFalse(htmx_partial(fake))
+
 
 
 class GetSafeRequestContextTestCase(TestCase):
 class GetSafeRequestContextTestCase(TestCase):
     def setUp(self):
     def setUp(self):