Repository: django-guardian/django-guardian (language: python)

Issue:
Add support for official Python/Django versions
Hi, 

This PR

* Add support for Django 4.1 & 4.2
* Add support for Python 3.10 & 3.11
* Drop support for Django 2.2, 3.0, 3.1 & 4.0
* Drop support for Python 3.5, 3.6 & 3.7

Please can we have a new release after this PR is merged.

Thanks,
I've added a small patch that supports generation of RPMs for this package



Discussion / hints:

Thanks for this one, nvie.


Fix patch:
diff --git a/CHANGES b/CHANGES
index 19344cd1..a8cd36c4 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,11 @@
+Unreleased
+==========
+
+* Add support for Django 4.1, 4.2 & 5.0
+* Add support for Python 3.10, 3.11 & 3.12
+* Drop support for Django 2.2, 3.0, 3.1 & 4.0
+* Drop support for Python 3.5, 3.6 & 3.7
+
 Release 2.4.0 (May 24, 2021)
 ============================
 
diff --git a/README.rst b/README.rst
index 244521f7..d4c65576 100644
--- a/README.rst
+++ b/README.rst
@@ -22,10 +22,10 @@ Online documentation is available at https://django-guardian.readthedocs.io/.
 Requirements
 ------------
 
-* Python 3.5+
-* A supported version of Django (currently 2.2+)
+* Python 3.8+
+* A supported version of Django (currently 3.2+)
 
-GitHub Actions run tests against Django versions 2.2, 3.0, 3.1, 3.2, 4.0, and main.
+GitHub Actions run tests against Django versions 3.2, 4.1, 4.2, 5.0 and main.
 
 Installation
 ------------
diff --git a/docs/develop/supported-versions.rst b/docs/develop/supported-versions.rst
index 958311bd..25a5d72a 100644
--- a/docs/develop/supported-versions.rst
+++ b/docs/develop/supported-versions.rst
@@ -3,11 +3,11 @@
 Supported versions
 ==================
 
-``django-guardian`` supports Python 3.5+ and Django 2.2+.
+``django-guardian`` supports Python 3.8+ and Django 3.2+.
 
 Rules
 -----
 
-* We support Python 3.5+.
-* We support Django 2.2+. This is due to many simplifications in code we could
+* We support Python 3.8+.
+* We support Django 3.2+. This is due to many simplifications in code we could
   do.
diff --git a/docs/installation.rst b/docs/installation.rst
index 494a8fa1..c54a0b9a 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -3,7 +3,7 @@
 Installation
 ============
 
-This application requires Django_ 2.2 or higher and it is the only prerequisite
+This application requires Django_ 3.2 or higher and it is the only prerequisite
 before ``django-guardian`` may be used.
 
 In order to install ``django-guardian`` simply use ``pip``::
diff --git a/example_project/articles/admin.py b/example_project/articles/admin.py
index 96966abb..34a43b90 100644
--- a/example_project/articles/admin.py
+++ b/example_project/articles/admin.py
@@ -2,9 +2,9 @@
 from .models import Article
 
 
+@admin.register(Article)
 class ArticleAdmin(admin.ModelAdmin):
     list_display = ('title', 'slug', 'created_at')
     list_filter = ('created_at',)
     search_fields = ('title',)
 
-admin.site.register(Article, ArticleAdmin)
diff --git a/example_project/articles/migrations/0003_auto_20230727_0659.py b/example_project/articles/migrations/0003_auto_20230727_0659.py
new file mode 100644
index 00000000..3bab7a78
--- /dev/null
+++ b/example_project/articles/migrations/0003_auto_20230727_0659.py
@@ -0,0 +1,28 @@
+# Generated by Django 3.2.20 on 2023-07-27 06:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('articles', '0002_custom_generic_permissions'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='article',
+            name='id',
+            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+        ),
+        migrations.AlterField(
+            model_name='articlegroupobjectpermission',
+            name='id',
+            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+        ),
+        migrations.AlterField(
+            model_name='articleuserobjectpermission',
+            name='id',
+            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+        ),
+    ]
diff --git a/example_project/core/migrations/0003_auto_20230727_0659.py b/example_project/core/migrations/0003_auto_20230727_0659.py
new file mode 100644
index 00000000..8378ba28
--- /dev/null
+++ b/example_project/core/migrations/0003_auto_20230727_0659.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2.20 on 2023-07-27 06:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0002_auto_20190629_0848'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='customuser',
+            name='first_name',
+            field=models.CharField(blank=True, max_length=150, verbose_name='first name'),
+        ),
+        migrations.AlterField(
+            model_name='customuser',
+            name='id',
+            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+        ),
+    ]
diff --git a/example_project/posts/admin.py b/example_project/posts/admin.py
index 99121509..acc7e113 100644
--- a/example_project/posts/admin.py
+++ b/example_project/posts/admin.py
@@ -5,6 +5,7 @@
 from guardian.admin import GuardedModelAdmin
 
 
+@admin.register(Post)
 class PostAdmin(GuardedModelAdmin):
     prepopulated_fields = {"slug": ("title",)}
     list_display = ('title', 'slug', 'created_at')
@@ -12,4 +13,3 @@ class PostAdmin(GuardedModelAdmin):
     ordering = ('-created_at',)
     date_hierarchy = 'created_at'
 
-admin.site.register(Post, PostAdmin)
diff --git a/example_project/posts/migrations/0003_alter_post_id.py b/example_project/posts/migrations/0003_alter_post_id.py
new file mode 100644
index 00000000..8ceb8f81
--- /dev/null
+++ b/example_project/posts/migrations/0003_alter_post_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.20 on 2023-07-27 06:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('posts', '0002_auto_20190629_0848'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='post',
+            name='id',
+            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+        ),
+    ]
diff --git a/example_project/requirements.txt b/example_project/requirements.txt
index de83bcea..7b87f113 100644
--- a/example_project/requirements.txt
+++ b/example_project/requirements.txt
@@ -1,5 +1,5 @@
-Django~=2.2.9
-django-environ==0.4.5
-django-rosetta==0.9.3
+Django~=3.2.20
+django-environ==0.10.0
+django-rosetta==0.9.9
 microsofttranslator==0.8
-polib==1.1.0
+polib==1.2.0
diff --git a/example_project/settings.py b/example_project/settings.py
index 8e0c0bd9..1841cf56 100644
--- a/example_project/settings.py
+++ b/example_project/settings.py
@@ -23,11 +23,11 @@
     'django.contrib.sites',
     'django.contrib.admin',
     'django.contrib.messages',
+    'django.contrib.staticfiles',
     'guardian',
     'posts',
     'articles',
     'core',
-    'django.contrib.staticfiles',
 )
 
 if 'GRAPPELLI' in os.environ:
@@ -108,3 +108,5 @@
         },
     },
 ]
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
diff --git a/guardian/__init__.py b/guardian/__init__.py
index ad011267..93c788d3 100644
--- a/guardian/__init__.py
+++ b/guardian/__init__.py
@@ -4,8 +4,6 @@
 from . import checks
 import django
 
-if django.VERSION < (3, 2):
-    default_app_config = 'guardian.apps.GuardianConfig'
 
 # PEP 396: The __version__ attribute's value SHOULD be a string.
 __version__ = '2.4.0'
diff --git a/requirements.txt b/requirements.txt
index deeecf21..8be7946b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-Django>=2.2
+Django>=3.2
 django-environ
 bumpversion
 mock
diff --git a/setup.cfg b/setup.cfg
index be97bbdd..b512558b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -10,7 +10,7 @@ all_files = 1
 upload-dir = docs/build/html
 
 [bdist_rpm]
-requires = Django >= 2.2
+requires = Django >= 3.2
 
 [aliases]
 test = pytest
diff --git a/setup.py b/setup.py
index a9e32fb4..cf7f3e2c 100644
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@
 setup(
     name='django-guardian',
     version=version,
-    python_requires='>=3.5',
+    python_requires='>=3.8',
     url='http://github.com/django-guardian/django-guardian',
     author='Lukasz Balcerzak',
     author_email='lukaszbalcerzak@gmail.com',
@@ -28,16 +28,15 @@
     ],
     include_package_data=True,
     license='BSD',
-    install_requires=["Django>=2.2"],
+    install_requires=["Django>=3.2"],
     tests_require=['mock', 'django-environ', 'pytest', 'pytest-django'],
     classifiers=['Development Status :: 5 - Production/Stable',
                  'Environment :: Web Environment',
                  'Framework :: Django',
-                 'Framework :: Django :: 2.2',
-                 'Framework :: Django :: 3.0',
-                 'Framework :: Django :: 3.1',
                  'Framework :: Django :: 3.2',
-                 'Framework :: Django :: 4.0',
+                 'Framework :: Django :: 4.1',
+                 'Framework :: Django :: 4.2',
+                 'Framework :: Django :: 5.0',
                  'Intended Audience :: Developers',
                  'License :: OSI Approved :: BSD License',
                  'Operating System :: OS Independent',
@@ -45,13 +44,11 @@
                  'Topic :: Security',
                  'Programming Language :: Python :: 3',
                  'Programming Language :: Python :: 3 :: Only',
-                 'Programming Language :: Python :: 3.5',
-                 'Programming Language :: Python :: 3.6',
-                 'Programming Language :: Python :: 3.7',
                  'Programming Language :: Python :: 3.8',
                  'Programming Language :: Python :: 3.9',
-                 'Programming Language :: Python :: 3.9',
                  'Programming Language :: Python :: 3.10',
+                 'Programming Language :: Python :: 3.11',
+                 'Programming Language :: Python :: 3.12',
                  ],
     test_suite='tests.main',
     cmdclass={'flakes': RunFlakesCommand},
diff --git a/tox.ini b/tox.ini
index 65d19ae7..0e4540f7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,30 +1,27 @@
 [tox]
-downloadcache = {toxworkdir}/cache/
 envlist = # sort by django version, next by python version
-    {core,example,docs}-py{35,36,37,38}-django22,
-    {core,example,docs}-py{36,37,38}-django30,
-    {core,example,docs}-py{36,37,38,39}-django31,
-    {core,example,docs}-py{36,37,38,39,310}-django32,
-    {core,example,docs}-py{38,39,310}-django40,
-    {core,example,docs}-py{310}-djangomain,
+    {core,example,docs}-py{38,39,310}-django32,
+    {core,example,docs}-py{38,39,310,311}-django41,
+    {core,example,docs}-py{38,39,310,311}-django42,
+    {core,example,docs}-py{310,311,312}-django50,
+    {core,example,docs}-py{310,311,312}-djangomain,
 
 [testenv]
 passenv = DATABASE_URL
 basepython =
-    py35: python3.5
-    py36: python3.6
-    py37: python3.7
     py38: python3.8
     py39: python3.9
     py310: python3.10
+    py311: python3.11
+    py312: python3.12
 changedir =
     example: example_project
     docs: docs
 commands =
-    django22: python {toxinidir}/manage.py makemigrations --check --dry-run
-    django30: python {toxinidir}/manage.py makemigrations --check --dry-run
-    django31: python {toxinidir}/manage.py makemigrations --check --dry-run
     django32: python {toxinidir}/manage.py makemigrations --check --dry-run
+    django41: python {toxinidir}/manage.py makemigrations --check --dry-run
+    django42: python {toxinidir}/manage.py makemigrations --check --dry-run
+    django50: python {toxinidir}/manage.py makemigrations --check --dry-run
     djangomain: python {toxinidir}/manage.py makemigrations --check --dry-run
     core: py.test --cov=guardian
     docs: sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
@@ -41,9 +38,8 @@ deps =
     docs: sphinx
     docs: sphinx_rtd_theme
     docs: setuptools_scm
-    django22: django~=2.2.17
-    django30: django~=3.0.11
-    django31: django~=3.1.3
-    django32: django~=3.2.0
-    django40: django~=4.0.0
+    django32: Django>=3.2,<3.3
+    django41: Django>=4.1,<4.2
+    django42: Django>=4.2,<4.3
+    django50: Django>=5.0,<5.1
     djangomain: https://github.com/django/django/archive/main.tar.gz


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 3970a8ef..e0b03699 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -10,33 +10,37 @@ on:
 
 jobs:
   tests:
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-24.04
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["3.6", "3.7", "3.8", "3.9"]
-        django: [2.2.17, 3.0.11, 3.1.3, 3.2.0]
-        include:
-          - python-version: "3.10"
-            django: 3.2.0
-          - python-version: "3.8"
-            django: 4.0.0
+        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
+        django: ["3.2.25", "4.1.13", "4.2.16", "5.0.9", "main"]
+        exclude:
+          - python-version: "3.12"
+            django: "4.1.13"
+          - python-version: "3.12"
+            django: "3.2.25"
+          - python-version: "3.11"
+            django: "3.2.25"
+          - python-version: "3.9"
+            django: "5.0.9"
           - python-version: "3.9"
-            django: 4.0.0
-          - python-version: "3.10"
-            django: 4.0.0
-          - python-version: "3.10"
-            django: main
+            django: "main"
+          - python-version: "3.8"
+            django: "5.0.9"
+          - python-version: "3.8"
+            django: "main"
 
     services:
       mariadb:
-        image: mariadb:10.5
+        image: mariadb:11.4.2
         env:
           MYSQL_ROOT_PASSWORD: mariadb
         ports:
           - 3306:3306
       postgres:
-        image: postgres:14-alpine
+        image: postgres:16-alpine
         env:
           PGUSER: postgres
           POSTGRES_PASSWORD: postgres
@@ -49,9 +53,9 @@ jobs:
           - 5432:5432
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
       - name: Install latest pip, setuptools, and wheel
diff --git a/guardian/testapp/migrations/0006_auto_20230727_0658.py b/guardian/testapp/migrations/0006_auto_20230727_0658.py
new file mode 100644
index 00000000..352f3f3a
--- /dev/null
+++ b/guardian/testapp/migrations/0006_auto_20230727_0658.py
@@ -0,0 +1,68 @@
+# Generated by Django 3.2.20 on 2023-07-27 06:58
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('testapp', '0005_uuidpkmodel'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='customuser',
+            name='first_name',
+            field=models.CharField(blank=True, max_length=150, verbose_name='first name'),
+        ),
+        migrations.AlterField(
+            model_name='customusernameuser',
+            name='id',
+            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+        ),
+        migrations.AlterField(
+            model_name='mixed',
+            name='id',
+            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+        ),
+        migrations.AlterField(
+            model_name='mixedgroupobjectpermission',
+            name='id',
+            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+        ),
+        migrations.AlterField(
+            model_name='parenttestmodel',
+            name='id',
+            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+        ),
+        migrations.AlterField(
+            model_name='post',
+            name='id',
+            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+        ),
+        migrations.AlterField(
+            model_name='project',
+            name='id',
+            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+        ),
+        migrations.AlterField(
+            model_name='projectgroupobjectpermission',
+            name='id',
+            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+        ),
+        migrations.AlterField(
+            model_name='projectuserobjectpermission',
+            name='id',
+            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+        ),
+        migrations.AlterField(
+            model_name='reversemixed',
+            name='id',
+            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+        ),
+        migrations.AlterField(
+            model_name='reversemixeduserobjectpermission',
+            name='id',
+            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+        ),
+    ]
diff --git a/guardian/testapp/tests/test_decorators.py b/guardian/testapp/tests/test_decorators.py
index 1a527f8e..5599205f 100644
--- a/guardian/testapp/tests/test_decorators.py
+++ b/guardian/testapp/tests/test_decorators.py
@@ -384,12 +384,8 @@ def dummy_view(request, project_name):
             pass
         response = dummy_view(request, project_name='foobar')
         self.assertTrue(isinstance(response, HttpResponseRedirect))
-        if django.VERSION >= (3, 2):
-            self.assertTrue(response.headers['location'].startswith(
-                '/foobar/'))
-        else:
-            self.assertTrue(response._headers['location'][1].startswith(
-                '/foobar/'))
+        self.assertTrue(response.headers['location'].startswith(
+            '/foobar/'))
 
     @override_settings(LOGIN_URL='django.contrib.auth.views.login')
     def test_redirection_class(self):
diff --git a/guardian/testapp/tests/test_shortcuts.py b/guardian/testapp/tests/test_shortcuts.py
index aa04e17c..792320e5 100644
--- a/guardian/testapp/tests/test_shortcuts.py
+++ b/guardian/testapp/tests/test_shortcuts.py
@@ -550,8 +550,7 @@ def test_direct_perms_only(self):
         self.assertEqual(set(get_group_perms(self.group2, self.obj1)), set())
         self.assertEqual(set(get_group_perms(admin, self.obj1)), set())
         expected_permissions = ['add_contenttype', 'change_contenttype', 'delete_contenttype']
-        if django.VERSION >= (2, 1):
-            expected_permissions.append('view_contenttype')
+        expected_permissions.append('view_contenttype')
         self.assertEqual(set(get_perms(admin, self.obj1)), set(expected_permissions))
         self.assertEqual(set(get_perms(self.user1, self.obj1)), {'change_contenttype', 'delete_contenttype'})
         self.assertEqual(set(get_perms(self.user2, self.obj1)), {'delete_contenttype'})
@@ -570,8 +569,7 @@ def test_direct_perms_only_perms_attached(self):
             admin: ["add_contenttype", "change_contenttype", "delete_contenttype"],
             self.user2: ["delete_contenttype"]
         }
-        if django.VERSION >= (2, 1):
-            expected[admin].append("view_contenttype")
+        expected[admin].append("view_contenttype")
         result = get_users_with_perms(self.obj1, attach_perms=True,
             with_superusers=False, with_group_users=True)
         self.assertEqual(result.keys(), expected.keys())
diff --git a/guardian/testapp/testsettings.py b/guardian/testapp/testsettings.py
index 57134006..9c9630b0 100644
--- a/guardian/testapp/testsettings.py
+++ b/guardian/testapp/testsettings.py
@@ -63,7 +63,10 @@
                 'django.template.context_processors.static',
                 'django.template.context_processors.tz',
                 'django.contrib.messages.context_processors.messages',
+                'django.template.context_processors.request',
             ],
         },
     },
 ]
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'