]> git.ipfire.org Git - thirdparty/patchwork.git/commitdiff
Bump all the dependencies
authorStephen Finucane <stephen@that.guru>
Sat, 6 Jun 2026 10:25:43 +0000 (11:25 +0100)
committerStephen Finucane <stephenfinucane@hotmail.com>
Sat, 6 Jun 2026 11:58:58 +0000 (12:58 +0100)
We also change our testing matrix so that we only test the upper and
lower bounds of supported Python releases. This should ease pressure on
CI somewhat. We also switch from the deprecated psycopg2 to psycopg
(which actually corresponds to v3 now).

Changes to the source were auto-generated with django-upgrade.

Signed-off-by: Stephen Finucane <stephen@that.guru>
20 files changed:
.github/workflows/ci.yaml
docker-compose-pg.yml
manage.py
patchwork/admin.py
patchwork/forms.py
patchwork/settings/base.py
patchwork/settings/production.example.py
patchwork/tests/views/test_bundles.py
patchwork/tests/views/test_mail.py
patchwork/tests/views/test_patch.py
patchwork/tests/views/test_user.py
patchwork/views/xmlrpc.py
releasenotes/notes/django-5-0-support-923e45ec2dc93117.yaml
releasenotes/notes/django-5-2-support-32a602166789f7f8.yaml [new file with mode: 0644]
releasenotes/notes/django-6-0-support-1295530f992e1158.yaml [new file with mode: 0644]
releasenotes/notes/python-3-14-support-8dc97577ffc4c8a0.yaml [new file with mode: 0644]
requirements-dev.txt
requirements-prod.txt
requirements-test.txt
tox.ini

index 796bf32ae967d8ffd0c662510b601acfbb1190d6..e7c43e711723a561f78084f4d8c2da6bd76b954d 100644 (file)
@@ -12,16 +12,16 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout source code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v6
       - name: Set up Python
-        uses: actions/setup-python@v5
+        uses: actions/setup-python@v6
         with:
-          python-version: "3.13"
+          python-version: "3.14"
           cache: 'pip'
       - name: Set up Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
-          go-version: "1.21"
+          go-version: "1.26"
       - name: Install dependencies
         run: python -m pip install tox
       - name: Run tox
@@ -33,7 +33,7 @@ jobs:
       matrix:
         # NOTE: If you add a version here, don't forget to update the
         # '[gh-actions]' section in tox.ini
-        python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
+        python: ["3.11", "3.14"]
         db: [postgres, mysql, sqlite3]
     env:
       DATABASE_TYPE: "${{ matrix.db }}"
@@ -44,7 +44,7 @@ jobs:
       MYSQL_ROOT_PASSWORD: root-${{ github.run_id }}
     services:
       postgres:
-        image: postgres:latest
+        image: postgres:17
         env:
           POSTGRES_DB: ${{ env.DATABASE_NAME }}
           POSTGRES_PASSWORD: ${{ env.DATABASE_PASSWORD }}
@@ -57,7 +57,7 @@ jobs:
           --health-timeout 5s
           --health-retries 5
       mysql:
-        image: mysql:latest
+        image: mysql:8.4
         env:
           MYSQL_DATABASE: ${{ env.DATABASE_NAME }}
           MYSQL_USER: ${{ env.DATABASE_USER }}
@@ -72,9 +72,9 @@ jobs:
           --health-retries 5
     steps:
       - name: Checkout source code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v6
       - name: Set up Python ${{ matrix.python }}
-        uses: actions/setup-python@v5
+        uses: actions/setup-python@v6
         with:
           python-version: ${{ matrix.python }}
           cache: 'pip'
@@ -104,20 +104,22 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout source code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v6
         with:
           fetch-depth: 0
+      - name: Fetch all branches
+        run: git fetch --all
       - name: Set up Python
-        uses: actions/setup-python@v5
+        uses: actions/setup-python@v6
         with:
-          python-version: "3.13"
+          python-version: "3.14"
           cache: 'pip'
       - name: Install dependencies
         run: python -m pip install tox
       - name: Build docs (via tox)
         run: tox -e docs
       - name: Archive build results
-        uses: actions/upload-artifact@v4
+        uses: actions/upload-artifact@v7
         with:
           name: html-docs-build
           path: docs/_build/html
@@ -132,13 +134,13 @@ jobs:
       COMPOSE_FILE: ${{ matrix.db == 'mysql' && 'docker-compose.yml' || (matrix.db == 'postgres' && 'docker-compose-pg.yml') || 'docker-compose-sqlite3.yml' }}
     steps:
       - name: Checkout source code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v6
         with:
           fetch-depth: 0
       - name: Set up Python
-        uses: actions/setup-python@v5
+        uses: actions/setup-python@v6
         with:
-          python-version: "3.13"
+          python-version: "3.14"
           cache: 'pip'
       - name: Build docker-compose service
         run: |
@@ -151,7 +153,7 @@ jobs:
           { echo patchwork; echo patchwork; } | \
               docker compose run -T --rm web \
               python manage.py changepassword patchwork
-      # FIXME(stephenfin): Re-enable this once dbbackup supports Django 4.0
+      # FIXME(stephenfin): Re-enable this once we've update configuration accordingly
       # - name: Test dbbackup/dbrestore
       #   run: |
       #     docker compose run -T --rm web python manage.py dbbackup
index 25cf8ff70f98b88510b630da7e9b6672bb8c21ba..3e277e5cf7f4ca9ce3fbdfc1b4831b6774813a06 100644 (file)
@@ -1,7 +1,7 @@
 ---
 services:
   db:
-    image: postgres:latest
+    image: postgres:17
     volumes:
       - ./tools/docker/db/postdata:/var/lib/postgresql/data
     environment:
index a2f4bd33bd4706897e1d31b282a190ec86c8fbd6..faddbd02979973646e055e7f717438f38b940e47 100755 (executable)
--- a/manage.py
+++ b/manage.py
@@ -7,11 +7,6 @@ if __name__ == '__main__':
         'DJANGO_SETTINGS_MODULE', 'patchwork.settings.production'
     )
 
-    import django
-
-    if django.VERSION < (3, 2):
-        raise Exception('Patchwork requires Django 3.2 or greater')
-
     from django.core.management import execute_from_command_line
 
     execute_from_command_line(sys.argv)
index d1c389a17b9981d8fd71b855dc75217466ae3fb7..d3bdae1bbc237297df643c89ec74d37d5eabbd8b 100644 (file)
@@ -44,6 +44,7 @@ class DelegationRuleInline(admin.TabularInline):
     fields = ('path', 'user', 'priority')
 
 
+@admin.register(Project)
 class ProjectAdmin(admin.ModelAdmin):
     list_display = ('name', 'linkname', 'listid', 'listemail')
     inlines = [
@@ -51,31 +52,26 @@ class ProjectAdmin(admin.ModelAdmin):
     ]
 
 
-admin.site.register(Project, ProjectAdmin)
-
-
+@admin.register(Person)
 class PersonAdmin(admin.ModelAdmin):
     list_display = ('__str__', 'has_account')
     search_fields = ('name', 'email')
 
+    @admin.display(
+        description='Account',
+        boolean=True,
+        ordering='user',
+    )
     def has_account(self, person):
         return bool(person.user)
 
-    has_account.boolean = True
-    has_account.admin_order_field = 'user'
-    has_account.short_description = 'Account'
-
-
-admin.site.register(Person, PersonAdmin)
-
 
+@admin.register(State)
 class StateAdmin(admin.ModelAdmin):
     list_display = ('name', 'action_required')
 
 
-admin.site.register(State, StateAdmin)
-
-
+@admin.register(Cover)
 class CoverAdmin(admin.ModelAdmin):
     list_display = ('name', 'submitter', 'project', 'date')
     list_filter = ('project',)
@@ -83,9 +79,7 @@ class CoverAdmin(admin.ModelAdmin):
     date_hierarchy = 'date'
 
 
-admin.site.register(Cover, CoverAdmin)
-
-
+@admin.register(Patch)
 class PatchAdmin(admin.ModelAdmin):
     list_display = (
         'name',
@@ -101,40 +95,35 @@ class PatchAdmin(admin.ModelAdmin):
     search_fields = ('name', 'submitter__name', 'submitter__email')
     date_hierarchy = 'date'
 
+    @admin.display(
+        description='Pull',
+        boolean=True,
+        ordering='pull_url',
+    )
     def is_pull_request(self, patch):
         return bool(patch.pull_url)
 
-    is_pull_request.boolean = True
-    is_pull_request.admin_order_field = 'pull_url'
-    is_pull_request.short_description = 'Pull'
-
-
-admin.site.register(Patch, PatchAdmin)
-
 
+@admin.register(CoverComment)
 class CoverCommentAdmin(admin.ModelAdmin):
     list_display = ('cover', 'submitter', 'date')
     search_fields = ('cover__name', 'submitter__name', 'submitter__email')
     date_hierarchy = 'date'
 
 
-admin.site.register(CoverComment, CoverCommentAdmin)
-
-
+@admin.register(PatchComment)
 class PatchCommentAdmin(admin.ModelAdmin):
     list_display = ('patch', 'submitter', 'date')
     search_fields = ('patch__name', 'submitter__name', 'submitter__email')
     date_hierarchy = 'date'
 
 
-admin.site.register(PatchComment, PatchCommentAdmin)
-
-
 class PatchInline(admin.StackedInline):
     model = Patch
     extra = 0
 
 
+@admin.register(Series)
 class SeriesAdmin(admin.ModelAdmin):
     list_display = (
         'name',
@@ -154,11 +143,10 @@ class SeriesAdmin(admin.ModelAdmin):
     filter_horizontal = ('dependencies',)
     inlines = (PatchInline,)
 
+    @admin.display(boolean=True)
     def received_all(self, series):
         return series.received_all
 
-    received_all.boolean = True
-
     def get_queryset(self, request):
         qs = super(SeriesAdmin, self).get_queryset(request)
         return qs.prefetch_related(
@@ -171,16 +159,12 @@ class SeriesAdmin(admin.ModelAdmin):
         )
 
 
-admin.site.register(Series, SeriesAdmin)
-
-
+@admin.register(SeriesReference)
 class SeriesReferenceAdmin(admin.ModelAdmin):
     model = SeriesReference
 
 
-admin.site.register(SeriesReference, SeriesReferenceAdmin)
-
-
+@admin.register(Check)
 class CheckAdmin(admin.ModelAdmin):
     list_display = (
         'patch',
@@ -195,27 +179,18 @@ class CheckAdmin(admin.ModelAdmin):
     date_hierarchy = 'date'
 
 
-admin.site.register(Check, CheckAdmin)
-
-
+@admin.register(Bundle)
 class BundleAdmin(admin.ModelAdmin):
     list_display = ('name', 'owner', 'project', 'public')
     list_filter = ('public', 'project')
     search_fields = ('name', 'owner')
 
 
-admin.site.register(Bundle, BundleAdmin)
-
-
+@admin.register(Tag)
 class TagAdmin(admin.ModelAdmin):
     list_display = ('name',)
 
 
-admin.site.register(Tag, TagAdmin)
-
-
+@admin.register(PatchRelation)
 class PatchRelationAdmin(admin.ModelAdmin):
     model = PatchRelation
-
-
-admin.site.register(PatchRelation, PatchRelationAdmin)
index cf77bdcc4a3b6a3cbc92db388c8617067de53452..efedfa9763b8ac07de220bc50b9966996c72d806 100644 (file)
@@ -3,7 +3,6 @@
 #
 # SPDX-License-Identifier: GPL-2.0-or-later
 
-import django
 from django.contrib.auth.models import User
 from django import forms
 from django.forms import renderers
@@ -179,10 +178,7 @@ class OptionalModelChoiceField(forms.ModelChoiceField):
         choices.append(self.no_change_choice)
         return choices
 
-    if django.VERSION >= (5, 0):
-        choices = property(_get_choices, forms.ChoiceField.choices.fset)
-    else:
-        choices = property(_get_choices, forms.ChoiceField._set_choices)
+    choices = property(_get_choices, forms.ChoiceField.choices.fset)
 
     def is_no_change(self, value):
         return value == self.no_change_choice[0]
index dccab6c36dbafc43029bcf702c03e01743d36f41..2b557fc1c016f684a632a57a80395f56a9b04d75 100644 (file)
@@ -119,7 +119,7 @@ STATICFILES_DIRS = [
 if os.getenv('DATABASE_TYPE') == 'postgres':
     DATABASES = {
         'default': {
-            'ENGINE': 'django.db.backends.postgresql_psycopg2',
+            'ENGINE': 'django.db.backends.postgresql',
             'HOST': os.environ.get('DATABASE_HOST', 'localhost'),
             'PORT': os.environ.get('DATABASE_PORT', ''),
             'NAME': os.environ.get('DATABASE_NAME', 'patchwork'),
index 8955a204bc21e9aeb7e1af55c736673c471a3dc1..65cb14556e1607f287c0b7e34125db88724b3590 100644 (file)
@@ -56,6 +56,11 @@ ADMINS = (
 
 STATIC_ROOT = os.environ.get('STATIC_ROOT', '/srv/patchwork/htdocs/static')
 
-STATICFILES_STORAGE = (
-    'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
-)
+STORAGES = {
+    'default': {
+        'BACKEND': 'django.core.files.storage.FileSystemStorage',
+    },
+    'staticfiles': {
+        'BACKEND': 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage',
+    },
+}
index 1d317c3059c7312e68fdbe09f88d985432343e1d..722abd88049ccd432f2b77f04ef207fef1e6254e 100644 (file)
@@ -331,14 +331,18 @@ class BundlePrivateViewMboxTest(BundlePrivateViewTest):
 
         # Check we can view as owner
         auth_string = _get_auth_string(self.user)
-        response = self.client.get(self.url, HTTP_AUTHORIZATION=auth_string)
+        response = self.client.get(
+            self.url, headers={'authorization': auth_string}
+        )
 
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, self.patches[0].name)
 
         # Check we can't view as another user
         auth_string = _get_auth_string(self.other_user)
-        response = self.client.get(self.url, HTTP_AUTHORIZATION=auth_string)
+        response = self.client.get(
+            self.url, headers={'authorization': auth_string}
+        )
         self.assertEqual(response.status_code, 404)
 
     def test_private_bundle_mbox_token_auth(self):
@@ -353,14 +357,18 @@ class BundlePrivateViewMboxTest(BundlePrivateViewTest):
 
         # Check we can view as owner
         auth_string = _get_auth_string(self.user)
-        response = self.client.get(self.url, HTTP_AUTHORIZATION=auth_string)
+        response = self.client.get(
+            self.url, headers={'authorization': auth_string}
+        )
 
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, self.patches[0].name)
 
         # Check we can't view as another user
         auth_string = _get_auth_string(self.other_user)
-        response = self.client.get(self.url, HTTP_AUTHORIZATION=auth_string)
+        response = self.client.get(
+            self.url, headers={'authorization': auth_string}
+        )
         self.assertEqual(response.status_code, 404)
 
 
index 798b83f486349a37eea1a274ee233fdb6654259b..b8545dc7aeee5dbd6e2eb8dc0eec396819776c6f 100644 (file)
@@ -5,7 +5,6 @@
 
 import re
 
-import django
 from django.core import mail
 from django.test import TestCase
 from django.urls import reverse
@@ -34,37 +33,21 @@ class MailSettingsTest(TestCase):
         response = self.client.post(reverse('mail-settings'), {'email': ''})
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, 'patchwork/mail.html')
-        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.assertFormError(
+            response.context['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')
-        if django.VERSION >= (4, 1):
-            self.assertFormError(
-                response.context['form'],
-                'email',
-                error_strings['email'],
-            )
-        else:
-            self.assertFormError(
-                response,
-                'form',
-                'email',
-                error_strings['email'],
-            )
+        self.assertFormError(
+            response.context['form'],
+            'email',
+            error_strings['email'],
+        )
 
     def test_post_optin(self):
         email = 'foo@example.com'
@@ -114,19 +97,11 @@ class OptoutRequestTest(TestCase):
     def test_post_empty(self):
         response = self.client.post(reverse('mail-optout'), {'email': ''})
         self.assertEqual(response.status_code, 200)
-        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.assertFormError(
+            response.context['form'],
+            'email',
+            'This field is required.',
+        )
         self.assertTrue(response.context['error'])
         self.assertNotIn('confirmation', response.context)
         self.assertEqual(len(mail.outbox), 0)
@@ -134,19 +109,11 @@ class OptoutRequestTest(TestCase):
     def test_post_non_email(self):
         response = self.client.post(reverse('mail-optout'), {'email': 'foo'})
         self.assertEqual(response.status_code, 200)
-        if django.VERSION >= (4, 1):
-            self.assertFormError(
-                response.context['form'],
-                'email',
-                error_strings['email'],
-            )
-        else:
-            self.assertFormError(
-                response,
-                'form',
-                'email',
-                error_strings['email'],
-            )
+        self.assertFormError(
+            response.context['form'],
+            'email',
+            error_strings['email'],
+        )
         self.assertTrue(response.context['error'])
         self.assertNotIn('confirmation', response.context)
         self.assertEqual(len(mail.outbox), 0)
@@ -216,19 +183,11 @@ class OptinRequestTest(TestCase):
     def test_post_empty(self):
         response = self.client.post(reverse('mail-optin'), {'email': ''})
         self.assertEqual(response.status_code, 200)
-        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.assertFormError(
+            response.context['form'],
+            'email',
+            'This field is required.',
+        )
         self.assertTrue(response.context['error'])
         self.assertNotIn('confirmation', response.context)
         self.assertEqual(len(mail.outbox), 0)
@@ -236,19 +195,11 @@ class OptinRequestTest(TestCase):
     def test_post_non_email(self):
         response = self.client.post(reverse('mail-optin'), {'email': 'foo'})
         self.assertEqual(response.status_code, 200)
-        if django.VERSION >= (4, 1):
-            self.assertFormError(
-                response.context['form'],
-                'email',
-                error_strings['email'],
-            )
-        else:
-            self.assertFormError(
-                response,
-                'form',
-                'email',
-                error_strings['email'],
-            )
+        self.assertFormError(
+            response.context['form'],
+            'email',
+            error_strings['email'],
+        )
         self.assertTrue(response.context['error'])
         self.assertNotIn('confirmation', response.context)
         self.assertEqual(len(mail.outbox), 0)
index 3de558f0b4efb3eef106653bf41ec884d6ca9ab9..133bae366a36ede646da3efb3fa6812195d005fe 100644 (file)
@@ -8,7 +8,6 @@ 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,21 +459,12 @@ class PatchUpdateTest(TestCase):
 
         new_states = [Patch.objects.get(pk=p.pk).state for p in self.patches]
         self.assertEqual(new_states, orig_states)
-        if django.VERSION >= (4, 1):
-            self.assertFormError(
-                response.context['patch_form'],
-                'state',
-                'Select a valid choice. That choice is not one '
-                'of the available choices.',
-            )
-        else:
-            self.assertFormError(
-                response,
-                'patch_form',
-                'state',
-                'Select a valid choice. That choice is not one '
-                'of the available choices.',
-            )
+        self.assertFormError(
+            response.context['patch_form'],
+            '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()
index 3c3b0c5d0c401ee0e20e05155845c996fdfd75e3..5fe4a82f57f679e5a0e053223ad63a7140ea8bb4 100644 (file)
@@ -6,7 +6,6 @@
 import string
 import secrets
 
-import django
 from django.contrib.auth.models import User
 from django.core import mail
 from django.test.client import Client
@@ -80,38 +79,22 @@ class RegistrationTest(TestCase):
             del data[field]
             response = self.client.post('/register/', data)
             self.assertEqual(response.status_code, 200)
-            if django.VERSION >= (4, 1):
-                self.assertFormError(
-                    response.context['form'],
-                    field,
-                    self.required_error,
-                )
-            else:
-                self.assertFormError(
-                    response,
-                    'form',
-                    field,
-                    self.required_error,
-                )
+            self.assertFormError(
+                response.context['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)
-        if django.VERSION >= (4, 1):
-            self.assertFormError(
-                response.context['form'],
-                'username',
-                self.invalid_error,
-            )
-        else:
-            self.assertFormError(
-                response,
-                'form',
-                'username',
-                self.invalid_error,
-            )
+        self.assertFormError(
+            response.context['form'],
+            'username',
+            self.invalid_error,
+        )
 
     def test_existing_username(self):
         user = create_user()
@@ -119,19 +102,11 @@ class RegistrationTest(TestCase):
         data['username'] = user.username
         response = self.client.post('/register/', data)
         self.assertEqual(response.status_code, 200)
-        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.',
-            )
+        self.assertFormError(
+            response.context['form'],
+            'username',
+            'This username is already taken. Please choose another.',
+        )
 
     def test_existing_email(self):
         user = create_user()
@@ -139,21 +114,12 @@ class RegistrationTest(TestCase):
         data['email'] = user.email
         response = self.client.post('/register/', data)
         self.assertEqual(response.status_code, 200)
-        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,
-            )
+        self.assertFormError(
+            response.context['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)
@@ -304,37 +270,21 @@ class UserLinkTest(_UserTestCase):
         response = self.client.post(reverse('user-link'), {'email': ''})
         self.assertEqual(response.status_code, 200)
         self.assertTrue(response.context['linkform'])
-        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.',
-            )
+        self.assertFormError(
+            response.context['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'])
-        if django.VERSION >= (4, 1):
-            self.assertFormError(
-                response.context['linkform'],
-                'email',
-                error_strings['email'],
-            )
-        else:
-            self.assertFormError(
-                response,
-                'linkform',
-                'email',
-                error_strings['email'],
-            )
+        self.assertFormError(
+            response.context['linkform'],
+            'email',
+            error_strings['email'],
+        )
 
     def test_user_person_request_valid(self):
         response = self.client.post(
index a936798e116ce930c6b632ea9cd7be210d00e99b..8137720a730ad9ebef87a0f7fef057d388a08ba0 100644 (file)
@@ -49,8 +49,8 @@ class PatchworkXMLRPCDispatcher(SimpleXMLRPCDispatcher, XMLRPCDocGenerator):
     def _user_for_request(self, request):
         auth_header = None
 
-        if 'HTTP_AUTHORIZATION' in request.META:
-            auth_header = request.META.get('HTTP_AUTHORIZATION')
+        if 'authorization' in request.headers:
+            auth_header = request.headers.get('authorization')
         elif 'Authorization' in request.META:
             auth_header = request.META.get('Authorization')
 
index 2e7aa35e5fdfab9291b3db004263e5e42a3c3d1e..09bfcc51f57452ed059ed70d9da1861ee2ad2704 100644 (file)
@@ -5,5 +5,5 @@ features:
     now supported.
 upgrade:
   - |
-    Django 3.2 and 4.1 are no longer supported. Bother releases are no longer
+    Django 3.2 and 4.1 are no longer supported. Both releases are no longer
     supported upstream and most distributions provide a newer version.
diff --git a/releasenotes/notes/django-5-2-support-32a602166789f7f8.yaml b/releasenotes/notes/django-5-2-support-32a602166789f7f8.yaml
new file mode 100644 (file)
index 0000000..d6adec8
--- /dev/null
@@ -0,0 +1,9 @@
+---
+features:
+  - |
+    `Django 5.2 <https://docs.djangoproject.com/en/dev/releases/5.2/>`_ is
+    now supported.
+upgrade:
+  - |
+    Django 4.2, 5.0 and 5.1 are no longer supported. These releases are no
+    longer supported upstream and most distributions provide a newer version.
diff --git a/releasenotes/notes/django-6-0-support-1295530f992e1158.yaml b/releasenotes/notes/django-6-0-support-1295530f992e1158.yaml
new file mode 100644 (file)
index 0000000..4d131e6
--- /dev/null
@@ -0,0 +1,5 @@
+---
+features:
+  - |
+    `Django 6.0 <https://docs.djangoproject.com/en/dev/releases/6.0/>`_ is
+    now supported.
diff --git a/releasenotes/notes/python-3-14-support-8dc97577ffc4c8a0.yaml b/releasenotes/notes/python-3-14-support-8dc97577ffc4c8a0.yaml
new file mode 100644 (file)
index 0000000..3cf353e
--- /dev/null
@@ -0,0 +1,9 @@
+---
+features:
+  - |
+    `Python 3.14 <https://www.python.org/downloads/release/python-3140/>`_ is
+    now supported.
+upgrade:
+  - |
+    Python 3.9 and 3.10 are no longer supported. These releases are no longer
+    supported upstream and most distributions provide a newer version.
index 90bc4c31331f2b422abd691aaac1c12529369694..a1d0e03e78763cc087c56ffa8c035eec081732e8 100644 (file)
@@ -1,6 +1,6 @@
-Django~=5.1.0
-djangorestframework~=3.15.2
-django-filter~=24.2.0
-django-debug-toolbar~=5.0.1
-django-dbbackup~=4.2.0
+Django~=6.0.0
+djangorestframework~=3.17.1
+django-filter~=25.2.0
+django-debug-toolbar~=6.3.0
+django-dbbackup~=5.3.0
 -r requirements-test.txt
index 4bd824a7bc8f78ae8ef1fde408315c0c7440228f..9797311354b6a140b16cb8447eb7c4cb2d2e9ad8 100644 (file)
@@ -1,5 +1,5 @@
-Django~=5.1.0
-djangorestframework~=3.15.0
-django-filter~=24.3.0
-psycopg2~=2.9.10
-sqlparse~=0.5.1
+Django~=6.0.0
+djangorestframework~=3.17.1
+django-filter~=25.2.0
+psycopg~=3.3.4
+sqlparse~=0.5.5
index 1371abd39029740b203fe44e80c7ebbde7aa64eb..8c88ae0f647c9412ee4da3bcffd3dbaef8ade17a 100644 (file)
@@ -1,8 +1,8 @@
-mysqlclient~=2.2.5
-psycopg2-binary~=2.9.9
-sqlparse~=0.5.1
+mysqlclient~=2.2.8
+psycopg~=3.3.4
+sqlparse~=0.5.5
 python-dateutil~=2.9.0
-tblib~=3.0.0
+tblib~=3.2.2
 openapi-core~=0.19.2
 jsonschema-path~=0.3.3
-termcolor~=2.5.0
+termcolor~=3.3.0
diff --git a/tox.ini b/tox.ini
index f1385fda7d4e5b6967c51a20e796534b3226f195..5fe0e7539e9612fe7962490affcb3cfffe2d521a 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -1,23 +1,19 @@
 [tox]
-minversion = 3.2
-envlist = pep8,docs,py{39,310,311}-django42,py{310,311,312}-django{50,51},py313-django51
+minversion = 4.6
+envlist = pep8,docs,py{311,312,313,314}-django52,py{312,313,314}-django60
 
 [testenv]
 skip_install = true
 deps =
     -r{toxinidir}/requirements-test.txt
-    django42: django~=4.2.0
-    django42: djangorestframework~=3.15.0
-    django42: django-filter~=24.3.0
-    django50: django~=5.0.0
-    django50: djangorestframework~=3.15.0
-    django50: django-filter~=24.3.0
-    django51: django~=5.1.0
-    django51: djangorestframework~=3.15.0
-    django51: django-filter~=24.3.0
+    django52: django~=5.2.0
+    django52: djangorestframework~=3.16.0
+    django52: django-filter~=25.1.0
+    django60: django~=6.0.0
+    django60: djangorestframework~=3.16.0
+    django60: django-filter~=25.2.0
 setenv =
     DJANGO_SETTINGS_MODULE = patchwork.settings.dev
-    PYTHONDONTWRITEBYTECODE = 1
     PYTHONDEVMODE = 1
     PYTHONWARNINGS = once
 passenv =
@@ -74,8 +70,7 @@ commands =
 
 [gh-actions]
 python =
-    3.9: py39
-    3.10: py310
     3.11: py311
     3.12: py312
     3.13: py313
+    3.14: py314