diff --git a/CHANGELOG.md b/CHANGELOG.md index fece59e..c16c287 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Release history of [django-admin-sortable2](https://github.com/jrief/django-admin-sortable2/) +### 2.3.2 +- fix #434: Respect Django's `CSRF_HEADER_NAME` for sortable update requests in the admin changelist. + ### 2.3.1 - fix #370: `django-compress` and `django-sass-processor` raises errors during run of compress or compilescss management command. diff --git a/adminsortable2/admin.py b/adminsortable2/admin.py index f78f80f..c9e7772 100644 --- a/adminsortable2/admin.py +++ b/adminsortable2/admin.py @@ -450,6 +450,7 @@ def _bulk_move(self, request, queryset, method): def changelist_view(self, request, extra_context=None): extra_context = extra_context or {} extra_context['sortable_update_url'] = self.get_update_url(request) + extra_context['sortable_csrf_header_name'] = self.get_csrf_header_name(request) extra_context['base_change_list_template'] = super().change_list_template or 'admin/change_list.html' return super().changelist_view(request, extra_context) @@ -459,6 +460,15 @@ def get_update_url(self, request): """ return reverse(f'{self.admin_site.name}:{self._get_update_url_name()}') + def get_csrf_header_name(self, request): + """ + Returns the HTTP header expected by Django's CSRF middleware. + """ + csrf_header_name = getattr(settings, 'CSRF_HEADER_NAME', 'HTTP_X_CSRFTOKEN') + if csrf_header_name.startswith('HTTP_'): + csrf_header_name = csrf_header_name[5:] + return csrf_header_name.replace('_', '-') + class PolymorphicSortableAdminMixin(SortableAdminMixin): """ diff --git a/adminsortable2/templates/adminsortable2/change_list.html b/adminsortable2/templates/adminsortable2/change_list.html index 8de9c8f..34a1883 100644 --- a/adminsortable2/templates/adminsortable2/change_list.html +++ b/adminsortable2/templates/adminsortable2/change_list.html @@ -5,6 +5,7 @@ ', + content, + re.S, + ) + assert config_match is not None + + config = json.loads(config_match.group(1)) + assert config['csrf_header'] == 'X-AUTH-CSRFTOKEN' + + token_match = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', content) + assert token_match is not None + csrf_token = token_match.group(1) + + book = Book1.objects.order_by('my_order').first() + assert book is not None + + update_payload = json.dumps({'updatedItems': [[book.pk, book.my_order]]}) + update_url = config['update_url'] + + # Default header must fail when CSRF_HEADER_NAME is customized. + default_header_response = client.post( + update_url, + data=update_payload, + content_type='application/json', + HTTP_X_CSRFTOKEN=csrf_token, + ) + assert default_header_response.status_code == 403 + + # Custom header succeeds and keeps the endpoint compatible with Django settings. + custom_header_response = client.post( + update_url, + data=update_payload, + content_type='application/json', + HTTP_X_AUTH_CSRFTOKEN=csrf_token, + ) + assert custom_header_response.status_code == 200