From a238eb841b8f7137c36d180f6a2556710ebb9d8d Mon Sep 17 00:00:00 2001
From: Viicos <65306057+Viicos@users.noreply.github.com>
Date: Fri, 8 Mar 2024 12:26:56 +0100
Subject: [PATCH 1/6] Switch to `pyproject.toml`, add support for Python 3.12

Actually run tests in CI
Update actions versions
Update gitignore
---
 .coveragerc                                   |  11 --
 .github/workflows/ci.yml                      |  29 +++--
 .github/workflows/code-quality.yml            |   8 +-
 .gitignore                                    | 108 +++++++++++++++++-
 .travis.yml                                   |  40 -------
 django_regex_redirects.egg-info/PKG-INFO      |   9 --
 django_regex_redirects.egg-info/SOURCES.txt   |  19 ---
 .../dependency_links.txt                      |   1 -
 django_regex_redirects.egg-info/top_level.txt |   1 -
 pyproject.toml                                |  85 ++++++++++++++
 setup.py                                      |  16 ---
 tox.ini                                       |  12 +-
 12 files changed, 213 insertions(+), 126 deletions(-)
 delete mode 100644 .coveragerc
 delete mode 100644 .travis.yml
 delete mode 100644 django_regex_redirects.egg-info/PKG-INFO
 delete mode 100644 django_regex_redirects.egg-info/SOURCES.txt
 delete mode 100644 django_regex_redirects.egg-info/dependency_links.txt
 delete mode 100644 django_regex_redirects.egg-info/top_level.txt
 create mode 100644 pyproject.toml
 delete mode 100644 setup.py

diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index 0a6b118..0000000
--- a/.coveragerc
+++ /dev/null
@@ -1,11 +0,0 @@
-[run]
-branch = True
-source = regex_redirects
-
-[report]
-omit = */tests/*
-exclude_lines =
-    pragma: no cover
-
-[html]
-directory = cover
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ab14533..932b789 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,15 +15,17 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python: ['3.8', '3.9', '3.10', '3.11']
-        django: ['3.2', '4.1', '4.2']
+        python: ["3.8", "3.9", "3.10", "3.11", "3.12"]
+        django: ["3.2", "4.2"]
         exclude:
-          - python: '3.11'
-            django: '3.2'
+          - python: "3.11"
+            django: "3.2"
+          - python: "3.12"
+            django: "3.2"
 
     steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python }}
 
@@ -48,18 +50,15 @@ jobs:
     if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
 
     steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
         with:
-          python-version: '3.10'
+          python-version: "3.10"
 
-      - name: Build sdist and wheel
+      - name: Build wheel
         run: |
-          pip install pip setuptools wheel --upgrade
-          python setup.py sdist bdist_wheel
+          pip install build --upgrade
+          python -m build
 
       - name: Publish a Python distribution to PyPI
         uses: pypa/gh-action-pypi-publish@release/v1
-        with:
-          user: __token__
-          password: ${{ secrets.PYPI_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml
index f8aa87e..030c95b 100644
--- a/.github/workflows/code-quality.yml
+++ b/.github/workflows/code-quality.yml
@@ -23,12 +23,12 @@ jobs:
         toxenv:
           - isort
     steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
         with:
-          python-version: '3.10'
+          python-version: "3.10"
       - name: Install dependencies
         run: pip install tox
       - run: tox
         env:
-          TOXENV: ${{ matrix.toxenv }}
\ No newline at end of file
+          TOXENV: ${{ matrix.toxenv }}
diff --git a/.gitignore b/.gitignore
index 118a0a9..ddfad43 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,106 @@
-*.log
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+reports/
+
+# Translations
+# *.mo -> we want to package these along
 *.pot
-*.pyc
+
+# Django stuff:
+*.log
 local_settings.py
-.tox
-.coverage
+db.sqlite3
+testapp/log_outgoing_requests.db
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index d3d2c90..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-language: python
-sudo: false
-cache: pip
-
-matrix:
-  include:
-    - python: 2.7
-      env: TOXENV=py27-django-1.10
-    - python: 2.7
-      env: TOXENV=py27-django-1.11
-    - python: 3.4
-      env: TOXENV=py34-django-1.10
-    - python: 3.4
-      env: TOXENV=py34-django-1.11
-    - python: 3.5
-      env: TOXENV=py35-django-1.10
-    - python: 3.5
-      env: TOXENV=py35-django-1.11
-    - python: 3.5
-      env: TOXENV=py35-django-2.0
-    - python: 3.5
-      env: TOXENV=py35-master
-    - python: 3.6
-      env: TOXENV=py36-django-1.11
-    - python: 3.6
-      env: TOXENV=py36-django-2.0
-    - python: 3.6
-      env: TOXENV=py36-master
-  allow_failures:
-    - python: 3.5
-      env: TOXENV=py35-master
-    - python: 3.6
-      env: TOXENV=py36-master
-
-install:
-  - pip install coverage coveralls tox
-script:
-  - tox
-after_success:
-  - coveralls
diff --git a/django_regex_redirects.egg-info/PKG-INFO b/django_regex_redirects.egg-info/PKG-INFO
deleted file mode 100644
index bf75e98..0000000
--- a/django_regex_redirects.egg-info/PKG-INFO
+++ /dev/null
@@ -1,9 +0,0 @@
-Metadata-Version: 2.1
-Name: django-regex-redirects
-Version: 0.4.0
-Summary: Django redirects, with regular expressions
-Home-page: https://github.com/maykinmedia/django-regex-redirects
-Author: Alex de Landgraaf
-Author-email: alex@maykinmedia.nl
-License: BSD licence, see LICENCE.txt
-License-File: LICENSE
diff --git a/django_regex_redirects.egg-info/SOURCES.txt b/django_regex_redirects.egg-info/SOURCES.txt
deleted file mode 100644
index 964adfc..0000000
--- a/django_regex_redirects.egg-info/SOURCES.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-LICENSE
-README.md
-setup.py
-django_regex_redirects.egg-info/PKG-INFO
-django_regex_redirects.egg-info/SOURCES.txt
-django_regex_redirects.egg-info/dependency_links.txt
-django_regex_redirects.egg-info/top_level.txt
-regex_redirects/__init__.py
-regex_redirects/actions.py
-regex_redirects/admin.py
-regex_redirects/apps.py
-regex_redirects/middleware.py
-regex_redirects/models.py
-regex_redirects/tests.py
-regex_redirects/migrations/0001_initial.py
-regex_redirects/migrations/0002_auto_20151217_1938.py
-regex_redirects/migrations/0004_auto_20170512_1349.py
-regex_redirects/migrations/0005_auto_20210425_1321.py
-regex_redirects/migrations/__init__.py
\ No newline at end of file
diff --git a/django_regex_redirects.egg-info/dependency_links.txt b/django_regex_redirects.egg-info/dependency_links.txt
deleted file mode 100644
index 8b13789..0000000
--- a/django_regex_redirects.egg-info/dependency_links.txt
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/django_regex_redirects.egg-info/top_level.txt b/django_regex_redirects.egg-info/top_level.txt
deleted file mode 100644
index 69193c5..0000000
--- a/django_regex_redirects.egg-info/top_level.txt
+++ /dev/null
@@ -1 +0,0 @@
-regex_redirects
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..404a4a1
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,85 @@
+[build-system]
+requires = ["setuptools>=61.0.0"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "django-regex-redirects"
+version = "0.4.0"
+description = "Django redirects, with regular expressions"
+authors = [
+    {name = "Alex de Landgraaf", email = "alex@maykinmedia.nl"}
+]
+readme = "README.md"
+license = {file = "LICENSE"}
+keywords = ["Django", "regex"]
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Framework :: Django",
+    "Framework :: Django :: 3.2",
+    "Framework :: Django :: 4.2",
+    "Intended Audience :: Developers",
+    "Operating System :: Unix",
+    "Operating System :: MacOS",
+    "Operating System :: Microsoft :: Windows",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Topic :: Software Development :: Libraries :: Python Modules",
+]
+requires-python = ">=3.8"
+dependencies = [
+    "django>=3.2",
+]
+
+[project.urls]
+Homepage = "https://github.com/maykinmedia/django-regex-redirects"
+"Bug Tracker" = "https://github.com/maykinmedia/django-regex-redirects/issues"
+"Source Code" = "https://github.com/maykinmedia/django-regex-redirects"
+
+[project.optional-dependencies]
+tests = [
+    "tox",
+    "isort",
+    "black",
+    "flake8",
+]
+
+[tool.setuptools.packages.find]
+include = ["regex_redirects*"]
+namespaces = false
+
+[tool.isort]
+profile = "black"
+combine_as_imports = true
+known_django = "django"
+known_first_party="regex_redirects"
+sections=["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]
+skip = ["env", ".tox", ".history"]
+
+[tool.coverage.run]
+branch = true
+source = [
+    "regex_redirects"
+]
+omit = [
+    "regex_redirects/migrations/*",
+]
+
+[tool.coverage.report]
+exclude_also = [
+    "if (typing\\.)?TYPE_CHECKING:",
+    "@(typing\\.)?overload",
+    "class .*\\(.*Protocol.*\\):",
+    "@(abc\\.)?abstractmethod",
+    "raise NotImplementedError",
+    "\\.\\.\\.",
+    "pass",
+]
+omit = [
+    "regex_redirects/migrations/*",
+]
+
+[tool.coverage.html]
+directory = "cover"
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 88684ba..0000000
--- a/setup.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# -*- coding: utf-8 -*-
-from distutils.core import setup
-
-from setuptools import find_packages
-
-setup(
-    name='django-regex-redirects',
-    version='0.4.0',
-    author=u'Alex de Landgraaf',
-    author_email='alex@maykinmedia.nl',
-    packages=find_packages(),
-    url='https://github.com/maykinmedia/django-regex-redirects',
-    license='BSD licence, see LICENCE.txt',
-    description='Django redirects, with regular expressions',
-    include_package_data=True,
-)
diff --git a/tox.ini b/tox.ini
index bd3b59c..a530551 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,7 @@
 [tox]
-envlist = 
-    py{38,39,310}-django{32,41,42}
-    py311-django{41,42}
+envlist =
+    py{38,39,310}-django{32,42}
+    py3{11,12}-django{41,42}
     isort
 skip_missing_interpreters = true
 
@@ -11,16 +11,16 @@ python =
     3.9: py39
     3.10: py310
     3.11: py311
+    3.12: py312
 
 [gh-actions:env]
 DJANGO =
     3.2: django32
-    4.1: django41
     4.2: django42
 
 [testenv]
 deps=
   django32: Django~=3.2.0
-  django41: Django~=4.1.0
   django42: Django~=4.2.0
-  master: https://github.com/django/django/archive/master.tar.gz
+commands =
+  python manage.py test regex_redirects

From c43fa7fd8d83f57a1a3c1b842d88a9275d06d89f Mon Sep 17 00:00:00 2001
From: Viicos <65306057+Viicos@users.noreply.github.com>
Date: Fri, 8 Mar 2024 12:33:25 +0100
Subject: [PATCH 2/6] lint: isort, remove old import

---
 regex_redirects/actions.py                           |  3 ---
 regex_redirects/admin.py                             |  2 --
 regex_redirects/apps.py                              |  2 --
 regex_redirects/middleware.py                        |  4 +---
 regex_redirects/migrations/0001_initial.py           |  2 --
 .../migrations/0002_auto_20151217_1938.py            |  2 --
 .../migrations/0004_auto_20170512_1349.py            |  4 +---
 regex_redirects/models.py                            |  2 --
 regex_redirects/tests.py                             | 12 +++++-------
 9 files changed, 7 insertions(+), 26 deletions(-)

diff --git a/regex_redirects/actions.py b/regex_redirects/actions.py
index 20d7915..caf9124 100644
--- a/regex_redirects/actions.py
+++ b/regex_redirects/actions.py
@@ -1,10 +1,7 @@
-from __future__ import unicode_literals
-
 import csv
 
 from django.http import HttpResponse
 
-
 # Admin action for a generic "CSV Export"
 # Django snippets: http://djangosnippets.org/snippets/2369/
 
diff --git a/regex_redirects/admin.py b/regex_redirects/admin.py
index 039599d..ad08ed0 100644
--- a/regex_redirects/admin.py
+++ b/regex_redirects/admin.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
 from django.contrib import admin
 
 from .actions import export_as_csv_action
diff --git a/regex_redirects/apps.py b/regex_redirects/apps.py
index 43a056b..fad4374 100644
--- a/regex_redirects/apps.py
+++ b/regex_redirects/apps.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
 from django.apps import AppConfig
 
 
diff --git a/regex_redirects/middleware.py b/regex_redirects/middleware.py
index 3aa66b4..3e1ed3e 100644
--- a/regex_redirects/middleware.py
+++ b/regex_redirects/middleware.py
@@ -1,11 +1,9 @@
-from __future__ import unicode_literals
-
 import re
 
 from django import http
 from django.conf import settings
-from django.core.exceptions import ImproperlyConfigured
 from django.core.cache import cache
+from django.core.exceptions import ImproperlyConfigured
 
 from .models import Redirect
 
diff --git a/regex_redirects/migrations/0001_initial.py b/regex_redirects/migrations/0001_initial.py
index 06e8653..f9d3a48 100644
--- a/regex_redirects/migrations/0001_initial.py
+++ b/regex_redirects/migrations/0001_initial.py
@@ -1,6 +1,4 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
 from django.db import migrations, models
 
 
diff --git a/regex_redirects/migrations/0002_auto_20151217_1938.py b/regex_redirects/migrations/0002_auto_20151217_1938.py
index f9bff9b..81d9753 100644
--- a/regex_redirects/migrations/0002_auto_20151217_1938.py
+++ b/regex_redirects/migrations/0002_auto_20151217_1938.py
@@ -1,6 +1,4 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
 from django.db import migrations, models
 
 
diff --git a/regex_redirects/migrations/0004_auto_20170512_1349.py b/regex_redirects/migrations/0004_auto_20170512_1349.py
index c5d3cc2..e3c0840 100644
--- a/regex_redirects/migrations/0004_auto_20170512_1349.py
+++ b/regex_redirects/migrations/0004_auto_20170512_1349.py
@@ -1,7 +1,5 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from django.db import migrations, models
+from django.db import migrations
 
 
 class Migration(migrations.Migration):
diff --git a/regex_redirects/models.py b/regex_redirects/models.py
index 2971493..2ed7ad4 100644
--- a/regex_redirects/models.py
+++ b/regex_redirects/models.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
 from django.db import models
 from django.utils.translation import gettext_lazy as _
 
diff --git a/regex_redirects/tests.py b/regex_redirects/tests.py
index 6b87244..78ce4da 100644
--- a/regex_redirects/tests.py
+++ b/regex_redirects/tests.py
@@ -1,14 +1,12 @@
-from __future__ import unicode_literals
-
-from unittest.case import skipUnless
-
-from django.conf import settings
+from django.core.cache import cache
 from django.test import TestCase
 from django.test.utils import override_settings
-from django.core.cache import cache
 
+from .middleware import (
+    DJANGO_REGEX_REDIRECTS_CACHE_KEY,
+    DJANGO_REGEX_REDIRECTS_CACHE_REGEX_KEY,
+)
 from .models import Redirect
-from .middleware import DJANGO_REGEX_REDIRECTS_CACHE_KEY, DJANGO_REGEX_REDIRECTS_CACHE_REGEX_KEY
 
 
 class RegexRedirectTests(TestCase):

From 7cf8152b1c64df775095f608d3467ce9e8071dff Mon Sep 17 00:00:00 2001
From: Viicos <65306057+Viicos@users.noreply.github.com>
Date: Fri, 8 Mar 2024 12:33:45 +0100
Subject: [PATCH 3/6] lint: black

---
 regex_redirects/actions.py                    |  24 ++-
 regex_redirects/admin.py                      |   9 +-
 regex_redirects/apps.py                       |   4 +-
 regex_redirects/middleware.py                 |  73 +++++---
 regex_redirects/migrations/0001_initial.py    |  68 +++++--
 .../migrations/0002_auto_20151217_1938.py     |  24 ++-
 .../migrations/0004_auto_20170512_1349.py     |   4 +-
 .../migrations/0005_auto_20210425_1321.py     |  13 +-
 regex_redirects/models.py                     |  59 ++++--
 regex_redirects/tests.py                      | 171 +++++++++---------
 10 files changed, 285 insertions(+), 164 deletions(-)

diff --git a/regex_redirects/actions.py b/regex_redirects/actions.py
index caf9124..1caf134 100644
--- a/regex_redirects/actions.py
+++ b/regex_redirects/actions.py
@@ -5,13 +5,19 @@
 # Admin action for a generic "CSV Export"
 # Django snippets: http://djangosnippets.org/snippets/2369/
 
-def export_as_csv_action(description="Export selected objects as CSV file",
-                         fields=None, exclude=None, header=True):
+
+def export_as_csv_action(
+    description="Export selected objects as CSV file",
+    fields=None,
+    exclude=None,
+    header=True,
+):
     """
     This function returns an export csv action
     'fields' and 'exclude' work like in django ModelForm
     'header' is whether or not to output the column names as the first row
     """
+
     def export_as_csv(modeladmin, request, queryset):
         """
         Generic csv export admin action.
@@ -26,14 +32,22 @@ def export_as_csv(modeladmin, request, queryset):
             excludeset = set(exclude)
             field_names = field_names - excludeset
 
-        response = HttpResponse(content_type='text/csv')
-        response['Content-Disposition'] = 'attachment; filename=%s.csv' % opts.replace('.', '_')
+        response = HttpResponse(content_type="text/csv")
+        response["Content-Disposition"] = "attachment; filename=%s.csv" % opts.replace(
+            ".", "_"
+        )
 
         writer = csv.writer(response)
         if header:
             writer.writerow(list(field_names))
         for obj in queryset:
-            writer.writerow([getattr(obj, field).encode('utf-8', 'replace') for field in field_names])
+            writer.writerow(
+                [
+                    getattr(obj, field).encode("utf-8", "replace")
+                    for field in field_names
+                ]
+            )
         return response
+
     export_as_csv.short_description = description
     return export_as_csv
diff --git a/regex_redirects/admin.py b/regex_redirects/admin.py
index ad08ed0..7e7e2e6 100644
--- a/regex_redirects/admin.py
+++ b/regex_redirects/admin.py
@@ -3,14 +3,15 @@
 from .actions import export_as_csv_action
 from .models import Redirect
 
-FIELD_LIST = ('old_path', 'new_path', 'regular_expression', 'fallback_redirect')
+FIELD_LIST = ("old_path", "new_path", "regular_expression", "fallback_redirect")
 
 
 class RedirectAdmin(admin.ModelAdmin):
     list_display = FIELD_LIST
-    list_filter = ('regular_expression',)
-    search_fields = ('old_path', 'new_path')
+    list_filter = ("regular_expression",)
+    search_fields = ("old_path", "new_path")
+
+    actions = [export_as_csv_action("Export to CSV", fields=FIELD_LIST)]
 
-    actions = [export_as_csv_action('Export to CSV', fields=FIELD_LIST)]
 
 admin.site.register(Redirect, RedirectAdmin)
diff --git a/regex_redirects/apps.py b/regex_redirects/apps.py
index fad4374..9c3da79 100644
--- a/regex_redirects/apps.py
+++ b/regex_redirects/apps.py
@@ -2,5 +2,5 @@
 
 
 class RegexRedirectsConfig(AppConfig):
-    name = 'regex_redirects'
-    verbose_name = 'Regex Redirects'
+    name = "regex_redirects"
+    verbose_name = "Regex Redirects"
diff --git a/regex_redirects/middleware.py b/regex_redirects/middleware.py
index 3e1ed3e..9391cbb 100644
--- a/regex_redirects/middleware.py
+++ b/regex_redirects/middleware.py
@@ -12,8 +12,8 @@
 except ImportError:
     MiddlewareMixin = object  # fallback for Django < 1.10
 
-DJANGO_REGEX_REDIRECTS_CACHE_KEY = 'django-regex-redirects-regular'
-DJANGO_REGEX_REDIRECTS_CACHE_REGEX_KEY = 'django-regex-redirects-regex'
+DJANGO_REGEX_REDIRECTS_CACHE_KEY = "django-regex-redirects-regular"
+DJANGO_REGEX_REDIRECTS_CACHE_REGEX_KEY = "django-regex-redirects-regex"
 DJANGO_REGEX_REDIRECTS_CACHE_TIMEOUT = 60
 
 """
@@ -26,7 +26,7 @@
 
 class RedirectFallbackMiddleware(MiddlewareMixin):
     def __init__(self, *args, **kwargs):
-        if 'django.contrib.sites' not in settings.INSTALLED_APPS:
+        if "django.contrib.sites" not in settings.INSTALLED_APPS:
             raise ImproperlyConfigured(
                 "You cannot use RedirectFallbackMiddleware when "
                 "django.contrib.sites is not installed."
@@ -43,59 +43,76 @@ def process_response(self, request, response):
             return response  # No need to check for a redirect for non-404 responses.
 
         full_path = request.get_full_path()
-        http_host = request.META.get('HTTP_HOST', '')
+        http_host = request.META.get("HTTP_HOST", "")
         if http_host:
             if request.is_secure():
-                http_host = 'https://' + http_host
+                http_host = "https://" + http_host
             else:
-                http_host = 'http://' + http_host
+                http_host = "http://" + http_host
 
         redirects = cache.get(DJANGO_REGEX_REDIRECTS_CACHE_KEY)
         if redirects is None:
-            redirects = list(Redirect.objects.all().order_by('fallback_redirect').values())
-            cache.set(DJANGO_REGEX_REDIRECTS_CACHE_KEY, redirects, DJANGO_REGEX_REDIRECTS_CACHE_TIMEOUT)
+            redirects = list(
+                Redirect.objects.all().order_by("fallback_redirect").values()
+            )
+            cache.set(
+                DJANGO_REGEX_REDIRECTS_CACHE_KEY,
+                redirects,
+                DJANGO_REGEX_REDIRECTS_CACHE_TIMEOUT,
+            )
 
         for redirect in redirects:
             # Attempt a regular match
-            if redirect['old_path'] == full_path:
-                self.increment_redirect(redirect['id'])
-                if redirect['new_path'].startswith('http'):
-                    return http.HttpResponsePermanentRedirect(redirect['new_path'])
+            if redirect["old_path"] == full_path:
+                self.increment_redirect(redirect["id"])
+                if redirect["new_path"].startswith("http"):
+                    return http.HttpResponsePermanentRedirect(redirect["new_path"])
                 else:
-                    return http.HttpResponsePermanentRedirect(http_host + redirect['new_path'])
+                    return http.HttpResponsePermanentRedirect(
+                        http_host + redirect["new_path"]
+                    )
 
-            if settings.APPEND_SLASH and not request.path.endswith('/'):
+            if settings.APPEND_SLASH and not request.path.endswith("/"):
                 # Try appending a trailing slash.
                 path_len = len(request.path)
-                slashed_full_path = full_path[:path_len] + '/' + full_path[path_len:]
+                slashed_full_path = full_path[:path_len] + "/" + full_path[path_len:]
 
-                if redirect['old_path'] == slashed_full_path:
-                    self.increment_redirect(redirect['id'])
-                    if redirect['new_path'].startswith('http'):
-                        return http.HttpResponsePermanentRedirect(redirect['new_path'])
+                if redirect["old_path"] == slashed_full_path:
+                    self.increment_redirect(redirect["id"])
+                    if redirect["new_path"].startswith("http"):
+                        return http.HttpResponsePermanentRedirect(redirect["new_path"])
                     else:
-                        return http.HttpResponsePermanentRedirect(http_host + redirect['new_path'])
+                        return http.HttpResponsePermanentRedirect(
+                            http_host + redirect["new_path"]
+                        )
 
         reg_redirects = cache.get(DJANGO_REGEX_REDIRECTS_CACHE_REGEX_KEY)
         if reg_redirects is None:
-            reg_redirects = list(Redirect.objects.filter(regular_expression=True).order_by('fallback_redirect').values())
-            cache.set(DJANGO_REGEX_REDIRECTS_CACHE_REGEX_KEY, reg_redirects,
-                      DJANGO_REGEX_REDIRECTS_CACHE_TIMEOUT)
+            reg_redirects = list(
+                Redirect.objects.filter(regular_expression=True)
+                .order_by("fallback_redirect")
+                .values()
+            )
+            cache.set(
+                DJANGO_REGEX_REDIRECTS_CACHE_REGEX_KEY,
+                reg_redirects,
+                DJANGO_REGEX_REDIRECTS_CACHE_TIMEOUT,
+            )
 
         for redirect in reg_redirects:
             try:
-                old_path = re.compile(redirect['old_path'], re.IGNORECASE)
+                old_path = re.compile(redirect["old_path"], re.IGNORECASE)
             except re.error:
                 # old_path does not compile into regex, ignore it and move on to the next one
                 continue
 
-            if re.match(redirect['old_path'], full_path):
-                self.increment_redirect(redirect['id'])
+            if re.match(redirect["old_path"], full_path):
+                self.increment_redirect(redirect["id"])
                 # Convert $1 into \1 (otherwise users would have to enter \1 via the admin
                 # which would have to be escaped)
-                new_path = redirect['new_path'].replace('$', '\\')
+                new_path = redirect["new_path"].replace("$", "\\")
                 replaced_path = re.sub(old_path, new_path, full_path)
-                if redirect['new_path'].startswith('http'):
+                if redirect["new_path"].startswith("http"):
                     return http.HttpResponsePermanentRedirect(replaced_path)
                 else:
                     return http.HttpResponsePermanentRedirect(http_host + replaced_path)
diff --git a/regex_redirects/migrations/0001_initial.py b/regex_redirects/migrations/0001_initial.py
index f9d3a48..37959be 100644
--- a/regex_redirects/migrations/0001_initial.py
+++ b/regex_redirects/migrations/0001_initial.py
@@ -4,24 +4,68 @@
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-    ]
+    dependencies = []
 
     operations = [
         migrations.CreateModel(
-            name='Redirect',
+            name="Redirect",
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
-                ('old_path', models.CharField(help_text="This should be an absolute path, excluding the domain name. Example: '/events/search/'.", unique=True, max_length=200, verbose_name='redirect from', db_index=True)),
-                ('new_path', models.CharField(help_text="This can be either an absolute path (as above) or a full URL starting with 'http://'.", max_length=200, verbose_name='redirect to', blank=True)),
-                ('regular_expression', models.BooleanField(default=False, help_text='If checked, the redirect-from and redirect-to fields will also be processed using regular expressions when matching incoming requests.<br>Example: <strong>/projects/.* -> /#!/projects</strong> will redirect everyone visiting a page starting with /projects/<br>Example: <strong>/projects/(.*) -> /#!/projects/$1</strong> will turn /projects/myproject into /#!/projects/myproject<br><br>Invalid regular expressions will be ignored.', verbose_name='Match using regular expressions')),
-                ('fallback_redirect', models.BooleanField(default=False, help_text="This redirect is only matched after all other redirects have failed to match.<br>This allows us to define a general 'catch-all' that is only used as a fallback after more specific redirects have been attempted.", verbose_name='Fallback redirect')),
-                ('nr_times_visited', models.IntegerField(default=0, help_text='Is incremented each time a visitor hits this redirect')),
+                (
+                    "id",
+                    models.AutoField(
+                        verbose_name="ID",
+                        serialize=False,
+                        auto_created=True,
+                        primary_key=True,
+                    ),
+                ),
+                (
+                    "old_path",
+                    models.CharField(
+                        help_text="This should be an absolute path, excluding the domain name. Example: '/events/search/'.",
+                        unique=True,
+                        max_length=200,
+                        verbose_name="redirect from",
+                        db_index=True,
+                    ),
+                ),
+                (
+                    "new_path",
+                    models.CharField(
+                        help_text="This can be either an absolute path (as above) or a full URL starting with 'http://'.",
+                        max_length=200,
+                        verbose_name="redirect to",
+                        blank=True,
+                    ),
+                ),
+                (
+                    "regular_expression",
+                    models.BooleanField(
+                        default=False,
+                        help_text="If checked, the redirect-from and redirect-to fields will also be processed using regular expressions when matching incoming requests.<br>Example: <strong>/projects/.* -> /#!/projects</strong> will redirect everyone visiting a page starting with /projects/<br>Example: <strong>/projects/(.*) -> /#!/projects/$1</strong> will turn /projects/myproject into /#!/projects/myproject<br><br>Invalid regular expressions will be ignored.",
+                        verbose_name="Match using regular expressions",
+                    ),
+                ),
+                (
+                    "fallback_redirect",
+                    models.BooleanField(
+                        default=False,
+                        help_text="This redirect is only matched after all other redirects have failed to match.<br>This allows us to define a general 'catch-all' that is only used as a fallback after more specific redirects have been attempted.",
+                        verbose_name="Fallback redirect",
+                    ),
+                ),
+                (
+                    "nr_times_visited",
+                    models.IntegerField(
+                        default=0,
+                        help_text="Is incremented each time a visitor hits this redirect",
+                    ),
+                ),
             ],
             options={
-                'ordering': ('fallback_redirect', 'regular_expression', 'old_path'),
-                'verbose_name': 'redirect',
-                'verbose_name_plural': 'redirects',
+                "ordering": ("fallback_redirect", "regular_expression", "old_path"),
+                "verbose_name": "redirect",
+                "verbose_name_plural": "redirects",
             },
         ),
     ]
diff --git a/regex_redirects/migrations/0002_auto_20151217_1938.py b/regex_redirects/migrations/0002_auto_20151217_1938.py
index 81d9753..ea39e2a 100644
--- a/regex_redirects/migrations/0002_auto_20151217_1938.py
+++ b/regex_redirects/migrations/0002_auto_20151217_1938.py
@@ -5,18 +5,28 @@
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('regex_redirects', '0001_initial'),
+        ("regex_redirects", "0001_initial"),
     ]
 
     operations = [
         migrations.AlterField(
-            model_name='redirect',
-            name='new_path',
-            field=models.CharField(help_text="This can be either an absolute path (as above) or a full URL starting with 'http://'.", max_length=2000, verbose_name='redirect to', blank=True),
+            model_name="redirect",
+            name="new_path",
+            field=models.CharField(
+                help_text="This can be either an absolute path (as above) or a full URL starting with 'http://'.",
+                max_length=2000,
+                verbose_name="redirect to",
+                blank=True,
+            ),
         ),
         migrations.AlterField(
-            model_name='redirect',
-            name='old_path',
-            field=models.CharField(help_text="This should be an absolute path, excluding the domain name. Example: '/events/search/'.", max_length=512, verbose_name='redirect from', db_index=True),
+            model_name="redirect",
+            name="old_path",
+            field=models.CharField(
+                help_text="This should be an absolute path, excluding the domain name. Example: '/events/search/'.",
+                max_length=512,
+                verbose_name="redirect from",
+                db_index=True,
+            ),
         ),
     ]
diff --git a/regex_redirects/migrations/0004_auto_20170512_1349.py b/regex_redirects/migrations/0004_auto_20170512_1349.py
index e3c0840..4c7f3eb 100644
--- a/regex_redirects/migrations/0004_auto_20170512_1349.py
+++ b/regex_redirects/migrations/0004_auto_20170512_1349.py
@@ -5,12 +5,12 @@
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('regex_redirects', '0002_auto_20151217_1938'),
+        ("regex_redirects", "0002_auto_20151217_1938"),
     ]
 
     operations = [
         migrations.AlterUniqueTogether(
-            name='redirect',
+            name="redirect",
             unique_together=set([]),
         ),
     ]
diff --git a/regex_redirects/migrations/0005_auto_20210425_1321.py b/regex_redirects/migrations/0005_auto_20210425_1321.py
index dc7043a..cd2f440 100644
--- a/regex_redirects/migrations/0005_auto_20210425_1321.py
+++ b/regex_redirects/migrations/0005_auto_20210425_1321.py
@@ -5,13 +5,18 @@
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('regex_redirects', '0004_auto_20170512_1349'),
+        ("regex_redirects", "0004_auto_20170512_1349"),
     ]
 
     operations = [
         migrations.AlterField(
-            model_name='redirect',
-            name='old_path',
-            field=models.CharField(db_index=True, help_text="This should be an absolute path, excluding the domain name. Example: '/events/search/'.", max_length=512, verbose_name='redirect from'),
+            model_name="redirect",
+            name="old_path",
+            field=models.CharField(
+                db_index=True,
+                help_text="This should be an absolute path, excluding the domain name. Example: '/events/search/'.",
+                max_length=512,
+                verbose_name="redirect from",
+            ),
         ),
     ]
diff --git a/regex_redirects/models.py b/regex_redirects/models.py
index 2ed7ad4..470bbc4 100644
--- a/regex_redirects/models.py
+++ b/regex_redirects/models.py
@@ -3,29 +3,50 @@
 
 
 class Redirect(models.Model):
-    old_path = models.CharField(_('redirect from'),
-                                max_length=512,
-                                db_index=True,
-                                help_text=_("This should be an absolute path, excluding the domain name. Example: '/events/search/'."))
-    new_path = models.CharField(_('redirect to'),
-                                max_length=2000,
-                                blank=True,
-                                help_text=_("This can be either an absolute path (as above) or a full URL starting with 'http://'."))
-    regular_expression = models.BooleanField(_('Match using regular expressions'),
-                                             default=False,
-                                             help_text=_("If checked, the redirect-from and redirect-to fields will also be processed using regular expressions when matching incoming requests.<br>Example: <strong>/projects/.* -> /#!/projects</strong> will redirect everyone visiting a page starting with /projects/<br>Example: <strong>/projects/(.*) -> /#!/projects/$1</strong> will turn /projects/myproject into /#!/projects/myproject<br><br>Invalid regular expressions will be ignored."))
+    old_path = models.CharField(
+        _("redirect from"),
+        max_length=512,
+        db_index=True,
+        help_text=_(
+            "This should be an absolute path, excluding the domain name. Example: '/events/search/'."
+        ),
+    )
+    new_path = models.CharField(
+        _("redirect to"),
+        max_length=2000,
+        blank=True,
+        help_text=_(
+            "This can be either an absolute path (as above) or a full URL starting with 'http://'."
+        ),
+    )
+    regular_expression = models.BooleanField(
+        _("Match using regular expressions"),
+        default=False,
+        help_text=_(
+            "If checked, the redirect-from and redirect-to fields will also be processed using regular expressions when matching incoming requests.<br>Example: <strong>/projects/.* -> /#!/projects</strong> will redirect everyone visiting a page starting with /projects/<br>Example: <strong>/projects/(.*) -> /#!/projects/$1</strong> will turn /projects/myproject into /#!/projects/myproject<br><br>Invalid regular expressions will be ignored."
+        ),
+    )
 
-    fallback_redirect = models.BooleanField(_("Fallback redirect"),
-                                            default=False,
-                                            help_text=_("This redirect is only matched after all other redirects have failed to match.<br>This allows us to define a general 'catch-all' that is only used as a fallback after more specific redirects have been attempted."))
+    fallback_redirect = models.BooleanField(
+        _("Fallback redirect"),
+        default=False,
+        help_text=_(
+            "This redirect is only matched after all other redirects have failed to match.<br>This allows us to define a general 'catch-all' that is only used as a fallback after more specific redirects have been attempted."
+        ),
+    )
 
-    nr_times_visited = models.IntegerField(default=0,
-                                           help_text=_("Is incremented each time a visitor hits this redirect"))
+    nr_times_visited = models.IntegerField(
+        default=0, help_text=_("Is incremented each time a visitor hits this redirect")
+    )
 
     class Meta:
-        verbose_name = _('redirect')
-        verbose_name_plural = _('redirects')
-        ordering = ('fallback_redirect', 'regular_expression', 'old_path',)
+        verbose_name = _("redirect")
+        verbose_name_plural = _("redirects")
+        ordering = (
+            "fallback_redirect",
+            "regular_expression",
+            "old_path",
+        )
 
     def __str__(self):
         return "%s ---> %s" % (self.old_path, self.new_path)
diff --git a/regex_redirects/tests.py b/regex_redirects/tests.py
index 78ce4da..591e4d3 100644
--- a/regex_redirects/tests.py
+++ b/regex_redirects/tests.py
@@ -16,48 +16,50 @@ def setUp(self):
         cache.delete(DJANGO_REGEX_REDIRECTS_CACHE_REGEX_KEY)
 
     def test_model(self):
-        r1 = Redirect.objects.create(
-            old_path='/initial', new_path='/new_target')
-        self.assertEqual(str(r1) , "/initial ---> /new_target")
+        r1 = Redirect.objects.create(old_path="/initial", new_path="/new_target")
+        self.assertEqual(str(r1), "/initial ---> /new_target")
 
     def test_redirect(self):
-        redirect = Redirect.objects.create(
-            old_path='/initial', new_path='/new_target')
+        redirect = Redirect.objects.create(old_path="/initial", new_path="/new_target")
         self.assertEqual(redirect.nr_times_visited, 0)
-        response = self.client.get('/initial')
-        self.assertRedirects(response,
-            '/new_target', status_code=301, target_status_code=404)
+        response = self.client.get("/initial")
+        self.assertRedirects(
+            response, "/new_target", status_code=301, target_status_code=404
+        )
         redirect.refresh_from_db()
         self.assertEqual(redirect.nr_times_visited, 1)
 
     @override_settings(APPEND_SLASH=True)
     def test_redirect_with_append_slash(self):
         redirect = Redirect.objects.create(
-            old_path='/initial/', new_path='/new_target/')
+            old_path="/initial/", new_path="/new_target/"
+        )
         self.assertEqual(redirect.nr_times_visited, 0)
-        response = self.client.get('/initial')
-        self.assertRedirects(response,
-            '/new_target/', status_code=301, target_status_code=404)
+        response = self.client.get("/initial")
+        self.assertRedirects(
+            response, "/new_target/", status_code=301, target_status_code=404
+        )
         redirect.refresh_from_db()
         self.assertEqual(redirect.nr_times_visited, 1)
 
     @override_settings(APPEND_SLASH=True)
     def test_redirect_with_append_slash_and_query_string(self):
-        Redirect.objects.create(
-            old_path='/initial/?foo', new_path='/new_target/')
-        response = self.client.get('/initial?foo')
-        self.assertRedirects(response,
-            '/new_target/', status_code=301, target_status_code=404)
+        Redirect.objects.create(old_path="/initial/?foo", new_path="/new_target/")
+        response = self.client.get("/initial?foo")
+        self.assertRedirects(
+            response, "/new_target/", status_code=301, target_status_code=404
+        )
 
     def test_regular_expression(self):
         redirect = Redirect.objects.create(
-            old_path='/news/index/(\d+)/(.*)/',
-            new_path='/my/news/$2/',
-            regular_expression=True)
-        response = self.client.get('/news/index/12345/foobar/')
-        self.assertRedirects(response,
-                             '/my/news/foobar/',
-                             status_code=301, target_status_code=404)
+            old_path="/news/index/(\d+)/(.*)/",
+            new_path="/my/news/$2/",
+            regular_expression=True,
+        )
+        response = self.client.get("/news/index/12345/foobar/")
+        self.assertRedirects(
+            response, "/my/news/foobar/", status_code=301, target_status_code=404
+        )
         redirect = Redirect.objects.get(regular_expression=True)
         redirect.refresh_from_db()
         self.assertEqual(redirect.nr_times_visited, 1)
@@ -66,71 +68,78 @@ def test_fallback_redirects(self):
         """
         Ensure redirects with fallback_redirect set are the last evaluated
         """
-        Redirect.objects.create(
-            old_path='/project/foo',
-            new_path='/my/project/foo')
+        Redirect.objects.create(old_path="/project/foo", new_path="/my/project/foo")
 
         Redirect.objects.create(
-            old_path='/project/foo/(.*)',
-            new_path='/my/project/foo/$1',
-            regular_expression=True)
+            old_path="/project/foo/(.*)",
+            new_path="/my/project/foo/$1",
+            regular_expression=True,
+        )
 
         Redirect.objects.create(
-            old_path='/project/(.*)',
-            new_path='/projects',
+            old_path="/project/(.*)",
+            new_path="/projects",
             regular_expression=True,
-            fallback_redirect=True)
+            fallback_redirect=True,
+        )
 
         Redirect.objects.create(
-            old_path='/project/bar/(.*)',
-            new_path='/my/project/bar/$1',
-            regular_expression=True)
+            old_path="/project/bar/(.*)",
+            new_path="/my/project/bar/$1",
+            regular_expression=True,
+        )
 
-        Redirect.objects.create(
-            old_path='/project/bar',
-            new_path='/my/project/bar')
+        Redirect.objects.create(old_path="/project/bar", new_path="/my/project/bar")
 
         Redirect.objects.create(
-            old_path='/second_project/.*',
-            new_path='http://example.com/my/second_project/bar/',
-            regular_expression=True)
-        
+            old_path="/second_project/.*",
+            new_path="http://example.com/my/second_project/bar/",
+            regular_expression=True,
+        )
+
         Redirect.objects.create(
-            old_path='/third_project/(.*)',
-            new_path='http://example.com/my/third_project/bar/$1',
-            regular_expression=True)
-
-        response = self.client.get('/project/foo')
-        self.assertRedirects(response,
-                             '/my/project/foo',
-                             status_code=301, target_status_code=404)
-
-        response = self.client.get('/project/bar')
-        self.assertRedirects(response,
-                             '/my/project/bar',
-                             status_code=301, target_status_code=404)
-
-        response = self.client.get('/project/bar/details')
-        self.assertRedirects(response,
-                             '/my/project/bar/details',
-                             status_code=301, target_status_code=404)
-
-        response = self.client.get('/project/foobar')
-        self.assertRedirects(response,
-                             '/projects',
-                             status_code=301, target_status_code=404)
-
-        response = self.client.get('/project/foo/details')
-        self.assertRedirects(response,
-                             '/my/project/foo/details',
-                             status_code=301, target_status_code=404)
-
-        response = self.client.get('/second_project/details')
-        self.assertRedirects(response,
-                             'http://example.com/my/second_project/bar/',
-                             status_code=301, target_status_code=404)
-
-        response = self.client.get('/third_project/details')
-        self.assertRedirects(response,
-                             'http://example.com/my/third_project/bar/details',
-                             status_code=301, target_status_code=404)
+            old_path="/third_project/(.*)",
+            new_path="http://example.com/my/third_project/bar/$1",
+            regular_expression=True,
+        )
+
+        response = self.client.get("/project/foo")
+        self.assertRedirects(
+            response, "/my/project/foo", status_code=301, target_status_code=404
+        )
+
+        response = self.client.get("/project/bar")
+        self.assertRedirects(
+            response, "/my/project/bar", status_code=301, target_status_code=404
+        )
+
+        response = self.client.get("/project/bar/details")
+        self.assertRedirects(
+            response, "/my/project/bar/details", status_code=301, target_status_code=404
+        )
+
+        response = self.client.get("/project/foobar")
+        self.assertRedirects(
+            response, "/projects", status_code=301, target_status_code=404
+        )
+
+        response = self.client.get("/project/foo/details")
+        self.assertRedirects(
+            response, "/my/project/foo/details", status_code=301, target_status_code=404
+        )
+
+        response = self.client.get("/second_project/details")
+        self.assertRedirects(
+            response,
+            "http://example.com/my/second_project/bar/",
+            status_code=301,
+            target_status_code=404,
+        )
+
+        response = self.client.get("/third_project/details")
+        self.assertRedirects(
+            response,
+            "http://example.com/my/third_project/bar/details",
+            status_code=301,
+            target_status_code=404,
+        )

From 058b94c71ed27240f98e4c0d68d5242e6f783d04 Mon Sep 17 00:00:00 2001
From: Viicos <65306057+Viicos@users.noreply.github.com>
Date: Fri, 8 Mar 2024 12:34:53 +0100
Subject: [PATCH 4/6] lint: flake8

---
 .flake8                    | 4 ++++
 regex_redirects/actions.py | 1 -
 regex_redirects/tests.py   | 2 +-
 3 files changed, 5 insertions(+), 2 deletions(-)
 create mode 100644 .flake8

diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..03d67fa
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,4 @@
+[flake8]
+max-line-length=88
+exclude=env,.tox,doc
+ignore=E203,W503,E501
diff --git a/regex_redirects/actions.py b/regex_redirects/actions.py
index 1caf134..1c5ec16 100644
--- a/regex_redirects/actions.py
+++ b/regex_redirects/actions.py
@@ -26,7 +26,6 @@ def export_as_csv(modeladmin, request, queryset):
         opts = modeladmin.model._meta
         field_names = set([field.name for field in opts.fields])
         if fields:
-            fieldset = set(fields)
             field_names = fields
         elif exclude:
             excludeset = set(exclude)
diff --git a/regex_redirects/tests.py b/regex_redirects/tests.py
index 591e4d3..7461969 100644
--- a/regex_redirects/tests.py
+++ b/regex_redirects/tests.py
@@ -52,7 +52,7 @@ def test_redirect_with_append_slash_and_query_string(self):
 
     def test_regular_expression(self):
         redirect = Redirect.objects.create(
-            old_path="/news/index/(\d+)/(.*)/",
+            old_path=r"/news/index/(\d+)/(.*)/",
             new_path="/my/news/$2/",
             regular_expression=True,
         )

From 3ecea398ed61efbdb6d17571196af4cbc94c35a7 Mon Sep 17 00:00:00 2001
From: Viicos <65306057+Viicos@users.noreply.github.com>
Date: Fri, 8 Mar 2024 12:40:17 +0100
Subject: [PATCH 5/6] Add linting to CI

---
 .github/workflows/code-quality.yml |  2 ++
 manage.py                          |  4 ++--
 testsettings.py                    | 17 +++++++++--------
 tox.ini                            | 17 +++++++++++++++++
 4 files changed, 30 insertions(+), 10 deletions(-)

diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml
index 030c95b..d302a43 100644
--- a/.github/workflows/code-quality.yml
+++ b/.github/workflows/code-quality.yml
@@ -22,6 +22,8 @@ jobs:
       matrix:
         toxenv:
           - isort
+          - black
+          - flake8
     steps:
       - uses: actions/checkout@v4
       - uses: actions/setup-python@v5
diff --git a/manage.py b/manage.py
index 7520d73..819d703 100755
--- a/manage.py
+++ b/manage.py
@@ -2,8 +2,8 @@
 import os
 import sys
 
-if __name__ == '__main__':
-    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testsettings')
+if __name__ == "__main__":
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testsettings")
 
     from django.core.management import execute_from_command_line
 
diff --git a/testsettings.py b/testsettings.py
index c54e376..e7e1c0c 100644
--- a/testsettings.py
+++ b/testsettings.py
@@ -1,23 +1,24 @@
-
 DATABASES = {
-    'default': {
-        'ENGINE': 'django.db.backends.sqlite3',
-        'NAME': ':memory:',
+    "default": {
+        "ENGINE": "django.db.backends.sqlite3",
+        "NAME": ":memory:",
     }
 }
 
-ALLOWED_HOSTS = ['example.com',]
+ALLOWED_HOSTS = [
+    "example.com",
+]
 
 INSTALLED_APPS = [
-    'django.contrib.sites',
-    'regex_redirects',
+    "django.contrib.sites",
+    "regex_redirects",
 ]
 
 SECRET_KEY = "notimportant"
 
 APPEND_SLASH = False
 
-MIDDLEWARE = ['regex_redirects.middleware.RedirectFallbackMiddleware']
+MIDDLEWARE = ["regex_redirects.middleware.RedirectFallbackMiddleware"]
 
 SITE_ID = 1
 
diff --git a/tox.ini b/tox.ini
index a530551..b029965 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,6 +3,8 @@ envlist =
     py{38,39,310}-django{32,42}
     py3{11,12}-django{41,42}
     isort
+    black
+    flake8
 skip_missing_interpreters = true
 
 [gh-actions]
@@ -24,3 +26,18 @@ deps=
   django42: Django~=4.2.0
 commands =
   python manage.py test regex_redirects
+
+[testenv:isort]
+extras = tests
+skipsdist = True
+commands = isort --check-only --diff .
+
+[testenv:black]
+extras = tests
+skipsdist = True
+commands = black --check regex_redirects testsettings.py manage.py
+
+[testenv:flake8]
+extras = tests
+skipsdist = True
+commands = flake8 .

From ba89d4ada2f9976e2a34eb58132ba0f6ad8f8a8e Mon Sep 17 00:00:00 2001
From: Viicos <65306057+Viicos@users.noreply.github.com>
Date: Fri, 8 Mar 2024 12:43:14 +0100
Subject: [PATCH 6/6] Update readme

---
 README.md | 24 ++++++++++--------------
 1 file changed, 10 insertions(+), 14 deletions(-)

diff --git a/README.md b/README.md
index fff8614..b8810cc 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-django-regex-redirects [![Build Status](https://travis-ci.org/maykinmedia/django-regex-redirects.svg?branch=master)](https://travis-ci.org/maykinmedia/django-regex-redirects)
+django-regex-redirects
 ======================
 
 Django redirects, with regular expressions. It is a modified version of django.contrib.redirects.
@@ -10,34 +10,30 @@ Features
  * Configurable via the admin
  * Redirects are exportable as .csv
  
-https://pypi.python.org/pypi/django-regex-redirects
+https://pypi.org/pypi/django-regex-redirects
 
 Install
 =======
 
-```pip install django-regex-redirects``` or ```python setup.py install```
+`pip install django-regex-redirects`
 
-Add regex_redirects to your INSTALLED_APPS:
+Add `regex_redirects` to your `INSTALLED_APPS`:
 
-```
+```python
 INSTALLED_APPS = (
   ...
-  'regex_redirects',
+  "regex_redirects",
   ...
 )
 ```
 
-Add the middleware to your MIDDLEWARE:
+Add the middleware to your `MIDDLEWARE`:
 
-```
+```python
 MIDDLEWARE = [
-  'regex_redirects.middleware.RedirectFallbackMiddleware'
+  "regex_redirects.middleware.RedirectFallbackMiddleware"
   ...
 ]
 ```
 
-Run manage.py migrate and you're good to go!
-
-
-
-
+Run `manage.py migrate` and you're good to go!