[PATCH 04/10] requirements: Bump Django to 3.2.x, djangorestframework to 4.16.0

Stephen Finucane stephen at that.guru
Sat Oct 1 02:19:15 AEST 2022


There are two issues to be addressed:

  RemovedInDjango50Warning: Passing response to assertFormError() is
  deprecated. Use the form object directly:

  RemovedInDjango50Warning: The "default.html" templates for forms and
  formsets will be removed. These were proxies to the equivalent
  "table.html" templates, but the new "div.html" templates will be the
  default from Django 5.0. Transitional renderers are provided to allow
  you to opt-in to the new output style now. See
  https://docs.djangoproject.com/en/4.1/releases/4.1/ for more details

Nothing complicated in fixing either of these. For the former, we must
do as we're told and use the form object directly. For the latter, we
need to configure our own form renderer so we can continue using the
table form renderer for now.

Signed-off-by: Stephen Finucane <stephen at that.guru>
---
 patchwork/forms.py                            |   8 ++
 patchwork/settings/base.py                    |   2 +
 patchwork/tests/views/test_mail.py            |  91 +++++++++++++---
 patchwork/tests/views/test_patch.py           |  23 ++--
 patchwork/tests/views/test_user.py            | 102 ++++++++++++++----
 .../django-4-1-support-bcbe65a71d235b43.yaml  |   5 +
 tox.ini                                       |   9 +-
 7 files changed, 197 insertions(+), 43 deletions(-)
 create mode 100644 releasenotes/notes/django-4-1-support-bcbe65a71d235b43.yaml

diff --git patchwork/forms.py patchwork/forms.py
index 84448586..1c5a29be 100644
--- patchwork/forms.py
+++ patchwork/forms.py
@@ -5,8 +5,10 @@
 
 from django.contrib.auth.models import User
 from django import forms
+from django.forms import renderers
 from django.db.models import Q
 from django.db.utils import ProgrammingError
+from django.template.backends import django as django_template_backend
 
 from patchwork.models import Bundle
 from patchwork.models import Patch
@@ -14,6 +16,12 @@ from patchwork.models import State
 from patchwork.models import UserProfile
 
 
+class PatchworkTableRenderer(renderers.EngineMixin, renderers.BaseRenderer):
+    backend = django_template_backend.DjangoTemplates
+    form_template_name = 'django/forms/table.html'
+    formset_template_name = 'django/forms/formsets/table.html'
+
+
 class RegistrationForm(forms.Form):
     first_name = forms.CharField(max_length=30, required=False)
     last_name = forms.CharField(max_length=30, required=False)
diff --git patchwork/settings/base.py patchwork/settings/base.py
index 045f262f..965c949f 100644
--- patchwork/settings/base.py
+++ patchwork/settings/base.py
@@ -71,6 +71,8 @@ TEMPLATES = [
     },
 ]
 
+FORM_RENDERER = 'patchwork.forms.PatchworkTableRenderer'
+
 # TODO(stephenfin): Consider changing to BigAutoField when we drop support for
 # Django < 3.2
 DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
diff --git patchwork/tests/views/test_mail.py patchwork/tests/views/test_mail.py
index de9df3d2..ae0b2c38 100644
--- patchwork/tests/views/test_mail.py
+++ patchwork/tests/views/test_mail.py
@@ -5,6 +5,7 @@
 
 import re
 
+import django
 from django.core import mail
 from django.test import TestCase
 from django.urls import reverse
@@ -33,15 +34,37 @@ class MailSettingsTest(TestCase):
         response = self.client.post(reverse('mail-settings'), {'email': ''})
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, 'patchwork/mail.html')
-        self.assertFormError(
-            response, 'form', 'email', 'This field is required.'
-        )
+        if django.VERSION >= (4, 1):
+            self.assertFormError(
+                response.context['form'],
+                'email',
+                'This field is required.',
+            )
+        else:
+            self.assertFormError(
+                response,
+                'form',
+                'email',
+                'This field is required.',
+            )
 
     def test_post_invalid(self):
         response = self.client.post(reverse('mail-settings'), {'email': 'foo'})
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, 'patchwork/mail.html')
-        self.assertFormError(response, 'form', 'email', error_strings['email'])
+        if django.VERSION >= (4, 1):
+            self.assertFormError(
+                response.context['form'],
+                'email',
+                error_strings['email'],
+            )
+        else:
+            self.assertFormError(
+                response,
+                'form',
+                'email',
+                error_strings['email'],
+            )
 
     def test_post_optin(self):
         email = 'foo at example.com'
@@ -91,9 +114,19 @@ class OptoutRequestTest(TestCase):
     def test_post_empty(self):
         response = self.client.post(reverse('mail-optout'), {'email': ''})
         self.assertEqual(response.status_code, 200)
-        self.assertFormError(
-            response, 'form', 'email', 'This field is required.'
-        )
+        if django.VERSION >= (4, 1):
+            self.assertFormError(
+                response.context['form'],
+                'email',
+                'This field is required.',
+            )
+        else:
+            self.assertFormError(
+                response,
+                'form',
+                'email',
+                'This field is required.',
+            )
         self.assertTrue(response.context['error'])
         self.assertNotIn('confirmation', response.context)
         self.assertEqual(len(mail.outbox), 0)
@@ -101,7 +134,19 @@ class OptoutRequestTest(TestCase):
     def test_post_non_email(self):
         response = self.client.post(reverse('mail-optout'), {'email': 'foo'})
         self.assertEqual(response.status_code, 200)
-        self.assertFormError(response, 'form', 'email', error_strings['email'])
+        if django.VERSION >= (4, 1):
+            self.assertFormError(
+                response.context['form'],
+                'email',
+                error_strings['email'],
+            )
+        else:
+            self.assertFormError(
+                response,
+                'form',
+                'email',
+                error_strings['email'],
+            )
         self.assertTrue(response.context['error'])
         self.assertNotIn('confirmation', response.context)
         self.assertEqual(len(mail.outbox), 0)
@@ -172,9 +217,19 @@ class OptinRequestTest(TestCase):
     def test_post_empty(self):
         response = self.client.post(reverse('mail-optin'), {'email': ''})
         self.assertEqual(response.status_code, 200)
-        self.assertFormError(
-            response, 'form', 'email', 'This field is required.'
-        )
+        if django.VERSION >= (4, 1):
+            self.assertFormError(
+                response.context['form'],
+                'email',
+                'This field is required.',
+            )
+        else:
+            self.assertFormError(
+                response,
+                'form',
+                'email',
+                'This field is required.',
+            )
         self.assertTrue(response.context['error'])
         self.assertNotIn('confirmation', response.context)
         self.assertEqual(len(mail.outbox), 0)
@@ -182,7 +237,19 @@ class OptinRequestTest(TestCase):
     def test_post_non_email(self):
         response = self.client.post(reverse('mail-optin'), {'email': 'foo'})
         self.assertEqual(response.status_code, 200)
-        self.assertFormError(response, 'form', 'email', error_strings['email'])
+        if django.VERSION >= (4, 1):
+            self.assertFormError(
+                response.context['form'],
+                'email',
+                error_strings['email'],
+            )
+        else:
+            self.assertFormError(
+                response,
+                'form',
+                'email',
+                error_strings['email'],
+            )
         self.assertTrue(response.context['error'])
         self.assertNotIn('confirmation', response.context)
         self.assertEqual(len(mail.outbox), 0)
diff --git patchwork/tests/views/test_patch.py patchwork/tests/views/test_patch.py
index 9cea8a0b..d1de8ec9 100644
--- patchwork/tests/views/test_patch.py
+++ patchwork/tests/views/test_patch.py
@@ -8,6 +8,7 @@ from datetime import timedelta
 import re
 import unittest
 
+import django
 from django.conf import settings
 from django.test import TestCase
 from django.urls import reverse
@@ -460,13 +461,21 @@ class PatchUpdateTest(TestCase):
 
         new_states = [Patch.objects.get(pk=p.pk).state for p in self.patches]
         self.assertEqual(new_states, orig_states)
-        self.assertFormError(
-            response,
-            'patchform',
-            'state',
-            'Select a valid choice. That choice is not one '
-            'of the available choices.',
-        )
+        if django.VERSION >= (4, 1):
+            self.assertFormError(
+                response.context['patchform'],
+                'state',
+                'Select a valid choice. That choice is not one '
+                'of the available choices.',
+            )
+        else:
+            self.assertFormError(
+                response,
+                'patchform',
+                'state',
+                'Select a valid choice. That choice is not one '
+                'of the available choices.',
+            )
 
     def _test_delegate_change(self, delegate_str):
         data = self.base_data.copy()
diff --git patchwork/tests/views/test_user.py patchwork/tests/views/test_user.py
index 1f74ad50..8ab91670 100644
--- patchwork/tests/views/test_user.py
+++ patchwork/tests/views/test_user.py
@@ -3,6 +3,7 @@
 #
 # SPDX-License-Identifier: GPL-2.0-or-later
 
+import django
 from django.contrib.auth.models import User
 from django.core import mail
 from django.test.client import Client
@@ -70,14 +71,38 @@ class RegistrationTest(TestCase):
             del data[field]
             response = self.client.post('/register/', data)
             self.assertEqual(response.status_code, 200)
-            self.assertFormError(response, 'form', field, self.required_error)
+            if django.VERSION >= (4, 1):
+                self.assertFormError(
+                    response.context['form'],
+                    field,
+                    self.required_error,
+                )
+            else:
+                self.assertFormError(
+                    response,
+                    'form',
+                    field,
+                    self.required_error,
+                )
 
     def test_invalid_username(self):
         data = self.default_data.copy()
         data['username'] = 'invalid user'
         response = self.client.post('/register/', data)
         self.assertEqual(response.status_code, 200)
-        self.assertFormError(response, 'form', 'username', self.invalid_error)
+        if django.VERSION >= (4, 1):
+            self.assertFormError(
+                response.context['form'],
+                'username',
+                self.invalid_error,
+            )
+        else:
+            self.assertFormError(
+                response,
+                'form',
+                'username',
+                self.invalid_error,
+            )
 
     def test_existing_username(self):
         user = create_user()
@@ -85,12 +110,19 @@ class RegistrationTest(TestCase):
         data['username'] = user.username
         response = self.client.post('/register/', data)
         self.assertEqual(response.status_code, 200)
-        self.assertFormError(
-            response,
-            'form',
-            'username',
-            'This username is already taken. Please choose another.',
-        )
+        if django.VERSION >= (4, 1):
+            self.assertFormError(
+                response.context['form'],
+                'username',
+                'This username is already taken. Please choose another.',
+            )
+        else:
+            self.assertFormError(
+                response,
+                'form',
+                'username',
+                'This username is already taken. Please choose another.',
+            )
 
     def test_existing_email(self):
         user = create_user()
@@ -98,13 +130,21 @@ class RegistrationTest(TestCase):
         data['email'] = user.email
         response = self.client.post('/register/', data)
         self.assertEqual(response.status_code, 200)
-        self.assertFormError(
-            response,
-            'form',
-            'email',
-            'This email address is already in use for the account '
-            '"%s".\n' % user.username,
-        )
+        if django.VERSION >= (4, 1):
+            self.assertFormError(
+                response.context['form'],
+                'email',
+                'This email address is already in use for the account '
+                '"%s".\n' % user.username,
+            )
+        else:
+            self.assertFormError(
+                response,
+                'form',
+                'email',
+                'This email address is already in use for the account '
+                '"%s".\n' % user.username,
+            )
 
     def test_valid_registration(self):
         response = self.client.post('/register/', self.default_data)
@@ -255,17 +295,37 @@ class UserLinkTest(_UserTestCase):
         response = self.client.post(reverse('user-link'), {'email': ''})
         self.assertEqual(response.status_code, 200)
         self.assertTrue(response.context['linkform'])
-        self.assertFormError(
-            response, 'linkform', 'email', 'This field is required.'
-        )
+        if django.VERSION >= (4, 1):
+            self.assertFormError(
+                response.context['linkform'],
+                'email',
+                'This field is required.',
+            )
+        else:
+            self.assertFormError(
+                response,
+                'linkform',
+                'email',
+                'This field is required.',
+            )
 
     def test_user_person_request_invalid(self):
         response = self.client.post(reverse('user-link'), {'email': 'foo'})
         self.assertEqual(response.status_code, 200)
         self.assertTrue(response.context['linkform'])
-        self.assertFormError(
-            response, 'linkform', 'email', error_strings['email']
-        )
+        if django.VERSION >= (4, 1):
+            self.assertFormError(
+                response.context['linkform'],
+                'email',
+                error_strings['email'],
+            )
+        else:
+            self.assertFormError(
+                response,
+                'linkform',
+                'email',
+                error_strings['email'],
+            )
 
     def test_user_person_request_valid(self):
         response = self.client.post(
diff --git releasenotes/notes/django-4-1-support-bcbe65a71d235b43.yaml releasenotes/notes/django-4-1-support-bcbe65a71d235b43.yaml
new file mode 100644
index 00000000..3dcab1bd
--- /dev/null
+++ releasenotes/notes/django-4-1-support-bcbe65a71d235b43.yaml
@@ -0,0 +1,5 @@
+---
+features:
+  - |
+    `Django 4.1 <https://docs.djangoproject.com/en/dev/releases/4.1/>`_ is
+    now supported.
diff --git tox.ini tox.ini
index aa434a72..f243faca 100644
--- tox.ini
+++ tox.ini
@@ -1,6 +1,6 @@
 [tox]
 minversion = 3.2
-envlist = pep8,docs,py{37,38,39}-django32,py{38,39,310}-django{40}
+envlist = pep8,docs,py{37,38,39}-django32,py{38,39,310}-django{40,41}
 skipsdist = true
 ignore_basepython_conflict = true
 
@@ -9,11 +9,14 @@ basepython = python3
 deps =
     -r{toxinidir}/requirements-test.txt
     django32: django~=3.2.0
-    django32: djangorestframework~=3.13.0
+    django32: djangorestframework~=3.14.0
     django32: django-filter~=22.1.0
     django40: django~=4.0.0
-    django40: djangorestframework~=3.13.0
+    django40: djangorestframework~=3.14.0
     django40: django-filter~=22.1.0
+    django41: django~=4.1.0
+    django41: djangorestframework~=3.14.0
+    django41: django-filter~=22.1.0
 setenv =
     DJANGO_SETTINGS_MODULE = patchwork.settings.dev
     PYTHONDONTWRITEBYTECODE = 1
-- 
2.37.3



More information about the Patchwork mailing list