1from __future__ import annotations
2
3import re
4from datetime import datetime
5from typing import TYPE_CHECKING
6
7from plain import models
8from plain.models import types
9
10if TYPE_CHECKING:
11 from plain.http import Request
12
13__all__ = ["NotFoundLog", "Redirect", "RedirectLog"]
14
15
16@models.register_model
17class Redirect(models.Model):
18 from_pattern: str = types.CharField(max_length=255)
19 to_pattern: str = types.CharField(max_length=255)
20 http_status: int = types.PositiveSmallIntegerField(
21 default=301
22 ) # Default to permanent - could be choices?
23 created_at: datetime = types.DateTimeField(auto_now_add=True)
24 updated_at: datetime = types.DateTimeField(auto_now=True)
25 order: int = types.PositiveSmallIntegerField(default=0)
26 enabled: bool = types.BooleanField(default=True)
27 is_regex: bool = types.BooleanField(default=False)
28
29 # query params?
30 # logged in or not? auth not required necessarily...
31 # headers?
32
33 query: models.QuerySet[Redirect] = models.QuerySet()
34
35 model_options = models.Options(
36 ordering=["order", "-created_at"],
37 indexes=[
38 models.Index(fields=["order"]),
39 models.Index(fields=["created_at"]),
40 ],
41 constraints=[
42 models.UniqueConstraint(
43 fields=["from_pattern"],
44 name="plainredirects_redirect_unique_from_pattern",
45 ),
46 ],
47 )
48
49 def __str__(self) -> str:
50 return f"{self.from_pattern}"
51
52 def matches_request(self, request: Request) -> bool:
53 """
54 Decide whether a request matches this Redirect,
55 automatically checking whether the pattern is path based or full URL based.
56 """
57
58 if self.from_pattern.startswith("http"):
59 # Full url with query params
60 url = request.build_absolute_uri()
61 else:
62 # Doesn't include query params or host
63 url = request.path
64
65 if self.is_regex:
66 return bool(re.match(self.from_pattern, url))
67 else:
68 return url == self.from_pattern
69
70 def get_redirect_url(self, request: Request) -> str:
71 if not self.is_regex:
72 return self.to_pattern
73
74 # Replace any regex groups in the to_pattern
75 if self.from_pattern.startswith("http"):
76 url = request.build_absolute_uri()
77 else:
78 url = request.path
79
80 return re.sub(self.from_pattern, self.to_pattern, url)
81
82
83@models.register_model
84class RedirectLog(models.Model):
85 redirect: Redirect = types.ForeignKeyField(Redirect, on_delete=models.CASCADE)
86
87 # The actuals that were used to redirect
88 from_url: str = types.URLField(max_length=512)
89 to_url: str = types.URLField(max_length=512)
90 http_status: int = types.PositiveSmallIntegerField(default=301)
91
92 # Request metadata
93 ip_address: str = types.GenericIPAddressField()
94 user_agent: str = types.CharField(required=False, max_length=512)
95 referrer: str = types.CharField(required=False, max_length=512)
96
97 created_at: datetime = types.DateTimeField(auto_now_add=True)
98
99 query: models.QuerySet[RedirectLog] = models.QuerySet()
100
101 model_options = models.Options(
102 ordering=["-created_at"],
103 indexes=[
104 models.Index(fields=["created_at"]),
105 ],
106 )
107
108 @classmethod
109 def from_redirect(cls, redirect: Redirect, request: Request) -> RedirectLog:
110 from_url = request.build_absolute_uri()
111 to_url = redirect.get_redirect_url(request)
112
113 if not to_url.startswith("http"):
114 to_url = request.build_absolute_uri(to_url)
115
116 if from_url == to_url:
117 raise ValueError("Redirecting to the same URL")
118
119 return cls.query.create(
120 redirect=redirect,
121 from_url=from_url,
122 to_url=to_url,
123 http_status=redirect.http_status,
124 ip_address=request.client_ip,
125 user_agent=request.headers.get("User-Agent", ""),
126 referrer=request.headers.get("Referer", ""),
127 )
128
129
130@models.register_model
131class NotFoundLog(models.Model):
132 url: str = types.URLField(max_length=512)
133
134 # Request metadata
135 ip_address: str = types.GenericIPAddressField()
136 user_agent: str = types.CharField(required=False, max_length=512)
137 referrer: str = types.CharField(required=False, max_length=512)
138
139 created_at: datetime = types.DateTimeField(auto_now_add=True)
140
141 query: models.QuerySet[NotFoundLog] = models.QuerySet()
142
143 model_options = models.Options(
144 ordering=["-created_at"],
145 indexes=[
146 models.Index(fields=["created_at"]),
147 ],
148 )
149
150 @classmethod
151 def from_request(cls, request: Request) -> NotFoundLog:
152 return cls.query.create(
153 url=request.build_absolute_uri(),
154 ip_address=request.client_ip,
155 user_agent=request.headers.get("User-Agent", ""),
156 referrer=request.headers.get("Referer", ""),
157 )