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/.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/.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..d302a43 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -22,13 +22,15 @@ jobs: matrix: toxenv: - isort + - black + - flake8 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/README.md b/README.md index fff8614..b8810cc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -django-regex-redirects [](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! 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/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/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/regex_redirects/actions.py b/regex_redirects/actions.py index 20d7915..1c5ec16 100644 --- a/regex_redirects/actions.py +++ b/regex_redirects/actions.py @@ -1,20 +1,23 @@ -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/ -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. @@ -23,20 +26,27 @@ 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) 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 039599d..7e7e2e6 100644 --- a/regex_redirects/admin.py +++ b/regex_redirects/admin.py @@ -1,18 +1,17 @@ -from __future__ import unicode_literals - from django.contrib import admin 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 43a056b..9c3da79 100644 --- a/regex_redirects/apps.py +++ b/regex_redirects/apps.py @@ -1,8 +1,6 @@ -from __future__ import unicode_literals - from django.apps import AppConfig 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 3aa66b4..9391cbb 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 @@ -14,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 """ @@ -28,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." @@ -45,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 06e8653..37959be 100644 --- a/regex_redirects/migrations/0001_initial.py +++ b/regex_redirects/migrations/0001_initial.py @@ -1,29 +1,71 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models 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 f9bff9b..ea39e2a 100644 --- a/regex_redirects/migrations/0002_auto_20151217_1938.py +++ b/regex_redirects/migrations/0002_auto_20151217_1938.py @@ -1,24 +1,32 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models 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 c5d3cc2..4c7f3eb 100644 --- a/regex_redirects/migrations/0004_auto_20170512_1349.py +++ b/regex_redirects/migrations/0004_auto_20170512_1349.py @@ -1,18 +1,16 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models +from django.db import migrations 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 2971493..470bbc4 100644 --- a/regex_redirects/models.py +++ b/regex_redirects/models.py @@ -1,33 +1,52 @@ -from __future__ import unicode_literals - from django.db import models from django.utils.translation import gettext_lazy as _ 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 6b87244..7461969 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): @@ -18,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=r"/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) @@ -68,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, + ) 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/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 bd3b59c..b029965 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,10 @@ [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 + black + flake8 skip_missing_interpreters = true [gh-actions] @@ -11,16 +13,31 @@ 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 + +[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 .