redirects/models.py
import re
from django.contrib.sites.models import Site
from django.db import models
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
from redirects.http import (
HttpResponseGone,
HttpResponsePermanentRedirect,
HttpResponseSeeOtherRedirect,
HttpResponseStrictPermanentRedirect,
HttpResponseStrictTemporaryRedirect,
HttpResponseTemporaryRedirect,
)
class Redirect(models.Model):
TYPE_301 = 301
TYPE_302 = 302
TYPE_303 = 303
TYPE_307 = 307
TYPE_308 = 308
TYPE_CHOICES = (
(TYPE_301, _("301 - Permanent")),
(TYPE_302, _("302 - Found")),
(TYPE_303, _("303 - See Other")),
(TYPE_307, _("307 - Temporary")),
(TYPE_308, _("308 - Permanent")),
)
TYPE_RESPONSE_CLASS = {
TYPE_301: HttpResponsePermanentRedirect,
TYPE_302: HttpResponseTemporaryRedirect,
TYPE_303: HttpResponseSeeOtherRedirect,
TYPE_307: HttpResponseStrictTemporaryRedirect,
TYPE_308: HttpResponseStrictPermanentRedirect,
}
MATCH_EXACT = "exact"
MATCH_PREFIX = "prefix"
MATCH_REGEX = "regex"
MATCH_CHOICES = (
(MATCH_EXACT, _("Exact")),
(MATCH_PREFIX, _("Prefix")),
(MATCH_REGEX, _("Regex")),
)
site = models.ForeignKey(
Site, blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_("Site")
)
old_path = models.CharField(
db_index=True,
max_length=255,
verbose_name=_("Old path"),
help_text=_(
"This can be either an absolute path or a regex "
"(excluding the domain name). "
),
)
new_path = models.CharField(
max_length=255,
blank=True,
verbose_name=_("New path"),
help_text=_(
"This can be either an absolute path, an absolute URL, or a regex. "
'If empty a "410 Gone" response will be returned.'
),
)
match = models.CharField(
db_index=True,
max_length=20,
choices=MATCH_CHOICES,
default=MATCH_EXACT,
verbose_name=_("Match"),
help_text=_("The redirect match condition."),
)
type_status_code = models.PositiveSmallIntegerField(
db_index=True,
choices=TYPE_CHOICES,
default=TYPE_301,
verbose_name=_("Type"),
help_text=_("The redirect http status code."),
)
priority = models.PositiveSmallIntegerField(
blank=True,
default=0,
verbose_name=_("Priority"),
help_text=_("Higher priority redirects are evaluated first."),
)
counter = models.PositiveIntegerField(
blank=True,
default=0,
verbose_name=_("Counter"),
help_text=_("The redirect requests/responses count."),
)
status_code = models.PositiveSmallIntegerField(
db_index=True,
blank=True,
null=True,
default=None,
verbose_name=_("Status code"),
)
created_at = models.DateTimeField(
auto_now_add=True,
editable=False,
null=True,
verbose_name=_("Created at"),
)
updated_at = models.DateTimeField(
auto_now=True,
editable=False,
null=True,
verbose_name=_("Updated at"),
)
def _get_response_path_with_match_exact(self, path):
if self.old_path.lower() == path.lower():
return self.new_path
return None
def _get_response_path_with_match_prefix(self, path):
if path.lower().startswith(self.old_path.lower()):
return self.new_path
return None
def _get_response_path_with_match_regex(self, path):
try:
old_path_re = re.compile(self.old_path, re.IGNORECASE)
except re.error:
# invalid regex
return None
old_path_match = re.match(old_path_re, path)
if old_path_match:
new_path_repl = self.new_path.replace("$", "\\")
return re.sub(old_path_re, new_path_repl, path)
return None
def get_response_path(self, path):
if self.match == Redirect.MATCH_EXACT:
return self._get_response_path_with_match_exact(path)
elif self.match == Redirect.MATCH_PREFIX:
return self._get_response_path_with_match_prefix(path)
elif self.match == Redirect.MATCH_REGEX:
return self._get_response_path_with_match_regex(path)
return None
def get_response(self, path):
response_path = self.get_response_path(path)
if response_path is None:
return None
self.counter += 1
self.save()
if response_path == "":
return HttpResponseGone()
response_class = Redirect.TYPE_RESPONSE_CLASS.get(self.type_status_code)
return response_class(response_path)
class Meta:
app_label = "redirects"
ordering = ["-priority", "old_path"]
unique_together = [["site", "old_path"]]
verbose_name = _("Redirect")
verbose_name_plural = _("Redirects")
def __str__(self):
verbose_name = _("Redirect")
return force_str(
f"{verbose_name} {self.type_status_code}: "
f"{self.old_path} \n---> {self.new_path}"
)