@@ -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)
@@ -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'
@@ -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@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)
@@ -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()
@@ -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(
new file mode 100644
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ `Django 4.1 <https://docs.djangoproject.com/en/dev/releases/4.1/>`_ is
+ now supported.
@@ -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
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@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