plain 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/README.md +33 -0
- plain/__main__.py +5 -0
- plain/assets/README.md +56 -0
- plain/assets/__init__.py +6 -0
- plain/assets/finders.py +233 -0
- plain/assets/preflight.py +14 -0
- plain/assets/storage.py +916 -0
- plain/assets/utils.py +52 -0
- plain/assets/whitenoise/__init__.py +5 -0
- plain/assets/whitenoise/base.py +259 -0
- plain/assets/whitenoise/compress.py +189 -0
- plain/assets/whitenoise/media_types.py +137 -0
- plain/assets/whitenoise/middleware.py +197 -0
- plain/assets/whitenoise/responders.py +286 -0
- plain/assets/whitenoise/storage.py +178 -0
- plain/assets/whitenoise/string_utils.py +13 -0
- plain/cli/README.md +123 -0
- plain/cli/__init__.py +3 -0
- plain/cli/cli.py +439 -0
- plain/cli/formatting.py +61 -0
- plain/cli/packages.py +73 -0
- plain/cli/print.py +9 -0
- plain/cli/startup.py +33 -0
- plain/csrf/README.md +3 -0
- plain/csrf/middleware.py +466 -0
- plain/csrf/views.py +10 -0
- plain/debug.py +23 -0
- plain/exceptions.py +242 -0
- plain/forms/README.md +14 -0
- plain/forms/__init__.py +8 -0
- plain/forms/boundfield.py +58 -0
- plain/forms/exceptions.py +11 -0
- plain/forms/fields.py +1030 -0
- plain/forms/forms.py +297 -0
- plain/http/README.md +1 -0
- plain/http/__init__.py +51 -0
- plain/http/cookie.py +20 -0
- plain/http/multipartparser.py +743 -0
- plain/http/request.py +754 -0
- plain/http/response.py +719 -0
- plain/internal/__init__.py +0 -0
- plain/internal/files/README.md +3 -0
- plain/internal/files/__init__.py +3 -0
- plain/internal/files/base.py +161 -0
- plain/internal/files/locks.py +127 -0
- plain/internal/files/move.py +102 -0
- plain/internal/files/temp.py +79 -0
- plain/internal/files/uploadedfile.py +150 -0
- plain/internal/files/uploadhandler.py +254 -0
- plain/internal/files/utils.py +78 -0
- plain/internal/handlers/__init__.py +0 -0
- plain/internal/handlers/base.py +133 -0
- plain/internal/handlers/exception.py +145 -0
- plain/internal/handlers/wsgi.py +216 -0
- plain/internal/legacy/__init__.py +0 -0
- plain/internal/legacy/__main__.py +12 -0
- plain/internal/legacy/management/__init__.py +414 -0
- plain/internal/legacy/management/base.py +692 -0
- plain/internal/legacy/management/color.py +113 -0
- plain/internal/legacy/management/commands/__init__.py +0 -0
- plain/internal/legacy/management/commands/collectstatic.py +297 -0
- plain/internal/legacy/management/sql.py +67 -0
- plain/internal/legacy/management/utils.py +175 -0
- plain/json.py +40 -0
- plain/logs/README.md +24 -0
- plain/logs/__init__.py +5 -0
- plain/logs/configure.py +39 -0
- plain/logs/loggers.py +74 -0
- plain/logs/utils.py +46 -0
- plain/middleware/README.md +3 -0
- plain/middleware/__init__.py +0 -0
- plain/middleware/clickjacking.py +52 -0
- plain/middleware/common.py +87 -0
- plain/middleware/gzip.py +64 -0
- plain/middleware/security.py +64 -0
- plain/packages/README.md +41 -0
- plain/packages/__init__.py +4 -0
- plain/packages/config.py +259 -0
- plain/packages/registry.py +438 -0
- plain/paginator.py +187 -0
- plain/preflight/README.md +3 -0
- plain/preflight/__init__.py +38 -0
- plain/preflight/compatibility/__init__.py +0 -0
- plain/preflight/compatibility/django_4_0.py +20 -0
- plain/preflight/files.py +19 -0
- plain/preflight/messages.py +88 -0
- plain/preflight/registry.py +72 -0
- plain/preflight/security/__init__.py +0 -0
- plain/preflight/security/base.py +268 -0
- plain/preflight/security/csrf.py +40 -0
- plain/preflight/urls.py +117 -0
- plain/runtime/README.md +75 -0
- plain/runtime/__init__.py +61 -0
- plain/runtime/global_settings.py +199 -0
- plain/runtime/user_settings.py +353 -0
- plain/signals/README.md +14 -0
- plain/signals/__init__.py +5 -0
- plain/signals/dispatch/__init__.py +9 -0
- plain/signals/dispatch/dispatcher.py +320 -0
- plain/signals/dispatch/license.txt +35 -0
- plain/signing.py +299 -0
- plain/templates/README.md +20 -0
- plain/templates/__init__.py +6 -0
- plain/templates/core.py +24 -0
- plain/templates/jinja/README.md +227 -0
- plain/templates/jinja/__init__.py +22 -0
- plain/templates/jinja/defaults.py +119 -0
- plain/templates/jinja/extensions.py +39 -0
- plain/templates/jinja/filters.py +28 -0
- plain/templates/jinja/globals.py +19 -0
- plain/test/README.md +3 -0
- plain/test/__init__.py +16 -0
- plain/test/client.py +985 -0
- plain/test/utils.py +255 -0
- plain/urls/README.md +3 -0
- plain/urls/__init__.py +40 -0
- plain/urls/base.py +118 -0
- plain/urls/conf.py +94 -0
- plain/urls/converters.py +66 -0
- plain/urls/exceptions.py +9 -0
- plain/urls/resolvers.py +731 -0
- plain/utils/README.md +3 -0
- plain/utils/__init__.py +0 -0
- plain/utils/_os.py +52 -0
- plain/utils/cache.py +327 -0
- plain/utils/connection.py +84 -0
- plain/utils/crypto.py +76 -0
- plain/utils/datastructures.py +345 -0
- plain/utils/dateformat.py +329 -0
- plain/utils/dateparse.py +154 -0
- plain/utils/dates.py +76 -0
- plain/utils/deconstruct.py +54 -0
- plain/utils/decorators.py +90 -0
- plain/utils/deprecation.py +6 -0
- plain/utils/duration.py +44 -0
- plain/utils/email.py +12 -0
- plain/utils/encoding.py +235 -0
- plain/utils/functional.py +456 -0
- plain/utils/hashable.py +26 -0
- plain/utils/html.py +401 -0
- plain/utils/http.py +374 -0
- plain/utils/inspect.py +73 -0
- plain/utils/ipv6.py +46 -0
- plain/utils/itercompat.py +8 -0
- plain/utils/module_loading.py +69 -0
- plain/utils/regex_helper.py +353 -0
- plain/utils/safestring.py +72 -0
- plain/utils/termcolors.py +221 -0
- plain/utils/text.py +518 -0
- plain/utils/timesince.py +138 -0
- plain/utils/timezone.py +244 -0
- plain/utils/tree.py +126 -0
- plain/validators.py +603 -0
- plain/views/README.md +268 -0
- plain/views/__init__.py +18 -0
- plain/views/base.py +107 -0
- plain/views/csrf.py +24 -0
- plain/views/errors.py +25 -0
- plain/views/exceptions.py +4 -0
- plain/views/forms.py +76 -0
- plain/views/objects.py +229 -0
- plain/views/redirect.py +72 -0
- plain/views/templates.py +66 -0
- plain/wsgi.py +11 -0
- plain-0.1.0.dist-info/LICENSE +85 -0
- plain-0.1.0.dist-info/METADATA +51 -0
- plain-0.1.0.dist-info/RECORD +169 -0
- plain-0.1.0.dist-info/WHEEL +4 -0
- plain-0.1.0.dist-info/entry_points.txt +3 -0
plain/views/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Views
|
|
2
|
+
|
|
3
|
+
Take a request, return a response.
|
|
4
|
+
|
|
5
|
+
Plain views are written as classes,
|
|
6
|
+
with a straightforward API that keeps simple views simple,
|
|
7
|
+
but gives you the power of a full class to handle more complex cases.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from plain.views import View
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExampleView(View):
|
|
14
|
+
def get(self):
|
|
15
|
+
return "Hello, world!"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## HTTP methods -> class methods
|
|
19
|
+
|
|
20
|
+
The HTTP methd of the request will map to a class method of the same name on the view.
|
|
21
|
+
|
|
22
|
+
If a request comes in and there isn't a matching method on the view,
|
|
23
|
+
Plain will return a `405 Method Not Allowed` response.
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from plain.views import View
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ExampleView(View):
|
|
30
|
+
def get(self):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def post(self):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
def put(self):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
def patch(self):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def delete(self):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def trace(self):
|
|
46
|
+
pass
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The [base `View` class](./base.py) defines default `options` and `head` behavior,
|
|
50
|
+
but you can override these too.
|
|
51
|
+
|
|
52
|
+
## Return types
|
|
53
|
+
|
|
54
|
+
For simple plain text and JSON responses,
|
|
55
|
+
you don't need to instantiate a `Response` object.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
class TextView(View):
|
|
59
|
+
def get(self):
|
|
60
|
+
return "Hello, world!"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class JsonView(View):
|
|
64
|
+
def get(self):
|
|
65
|
+
return {"message": "Hello, world!"}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Template views
|
|
69
|
+
|
|
70
|
+
The most common behavior for a view is to render a template.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from plain.views import TemplateView
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ExampleView(TemplateView):
|
|
77
|
+
template_name = "example.html"
|
|
78
|
+
|
|
79
|
+
def get_template_context(self):
|
|
80
|
+
context = super().get_template_context()
|
|
81
|
+
context["message"] = "Hello, world!"
|
|
82
|
+
return context
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The `TemplateView` is also the base class for *most* of the other built-in view classes.
|
|
86
|
+
|
|
87
|
+
## Form views
|
|
88
|
+
|
|
89
|
+
Standard [forms](../forms) can be rendered and processed by a `FormView`.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from plain.views import FormView
|
|
93
|
+
from .forms import ExampleForm
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ExampleView(FormView):
|
|
97
|
+
template_name = "example.html"
|
|
98
|
+
form_class = ExampleForm
|
|
99
|
+
success_url = "." # Redirect to the same page
|
|
100
|
+
|
|
101
|
+
def form_valid(self, form):
|
|
102
|
+
# Do other successfull form processing here
|
|
103
|
+
return super().form_valid(form)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Rendering forms is done directly in the HTML.
|
|
107
|
+
|
|
108
|
+
```html
|
|
109
|
+
{% extends "base.html" %}
|
|
110
|
+
|
|
111
|
+
{% block content %}
|
|
112
|
+
|
|
113
|
+
<form method="post">
|
|
114
|
+
{{ csrf_input }}
|
|
115
|
+
|
|
116
|
+
<!-- Render general form errors -->
|
|
117
|
+
{% for error in form.non_field_errors %}
|
|
118
|
+
<div>{{ error }}</div>
|
|
119
|
+
{% endfor %}
|
|
120
|
+
|
|
121
|
+
<!-- Render form fields individually (or with Jinja helps or other concepts) -->
|
|
122
|
+
<label for="{{ form.email.html_id }}">Email</label>
|
|
123
|
+
<input
|
|
124
|
+
type="email"
|
|
125
|
+
name="{{ form.email.html_name }}"
|
|
126
|
+
id="{{ form.email.html_id }}"
|
|
127
|
+
value="{{ form.email.value() or '' }}"
|
|
128
|
+
autocomplete="email"
|
|
129
|
+
autofocus
|
|
130
|
+
required>
|
|
131
|
+
{% if form.email.errors %}
|
|
132
|
+
<div>{{ form.email.errors|join(', ') }}</div>
|
|
133
|
+
{% endif %}
|
|
134
|
+
|
|
135
|
+
<button type="submit">Save</button>
|
|
136
|
+
</form>
|
|
137
|
+
|
|
138
|
+
{% endblock %}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Object views
|
|
142
|
+
|
|
143
|
+
The object views support the standard CRUD (create, read/detail, update, delete) operations, plus a list view.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from plain.views import DetailView, CreateView, UpdateView, DeleteView, ListView
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class ExampleDetailView(DetailView):
|
|
150
|
+
template_name = "detail.html"
|
|
151
|
+
|
|
152
|
+
def get_object(self):
|
|
153
|
+
return MyObjectClass.objects.get(
|
|
154
|
+
pk=self.url_kwargs["pk"],
|
|
155
|
+
user=self.request.user, # Limit access
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class ExampleCreateView(CreateView):
|
|
160
|
+
template_name = "create.html"
|
|
161
|
+
form_class = CustomCreateForm
|
|
162
|
+
success_url = "."
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class ExampleUpdateView(UpdateView):
|
|
166
|
+
template_name = "update.html"
|
|
167
|
+
form_class = CustomUpdateForm
|
|
168
|
+
success_url = "."
|
|
169
|
+
|
|
170
|
+
def get_object(self):
|
|
171
|
+
return MyObjectClass.objects.get(
|
|
172
|
+
pk=self.url_kwargs["pk"],
|
|
173
|
+
user=self.request.user, # Limit access
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class ExampleDeleteView(DeleteView):
|
|
178
|
+
template_name = "delete.html"
|
|
179
|
+
success_url = "."
|
|
180
|
+
|
|
181
|
+
# No form class necessary.
|
|
182
|
+
# Just POST to this view to delete the object.
|
|
183
|
+
|
|
184
|
+
def get_object(self):
|
|
185
|
+
return MyObjectClass.objects.get(
|
|
186
|
+
pk=self.url_kwargs["pk"],
|
|
187
|
+
user=self.request.user, # Limit access
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class ExampleListView(ListView):
|
|
192
|
+
template_name = "list.html"
|
|
193
|
+
|
|
194
|
+
def get_objects(self):
|
|
195
|
+
return MyObjectClass.objects.filter(
|
|
196
|
+
user=self.request.user, # Limit access
|
|
197
|
+
)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Response exceptions
|
|
201
|
+
|
|
202
|
+
At any point in the request handling,
|
|
203
|
+
a view can raise a `ResponseException` to immediately exit and return the wrapped response.
|
|
204
|
+
|
|
205
|
+
This isn't always necessary, but can be useful for raising rate limits or authorization errors when you're a couple layers deep in the view handling or helper functions.
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from plain.views import DetailView
|
|
209
|
+
from plain.views.exceptions import ResponseException
|
|
210
|
+
from plain.http import Response
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class ExampleView(DetailView):
|
|
214
|
+
def get_object(self):
|
|
215
|
+
if self.request.user.exceeds_rate_limit:
|
|
216
|
+
raise ResponseException(
|
|
217
|
+
Response("Rate limit exceeded", status=429)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return AnExpensiveObject()
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Error views
|
|
224
|
+
|
|
225
|
+
By default, HTTP errors will be rendered by `templates/<status_code>.html` or `templates/error.html`.
|
|
226
|
+
|
|
227
|
+
You can define your own error views by pointing the `HTTP_ERROR_VIEWS` setting to a dictionary of status codes and view classes.
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
# app/settings.py
|
|
231
|
+
HTTP_ERROR_VIEWS = {
|
|
232
|
+
404: "errors.NotFoundView",
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
# app/errors.py
|
|
238
|
+
from plain.views import View
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class NotFoundView(View):
|
|
242
|
+
def get(self):
|
|
243
|
+
# A custom implementation or error view handling
|
|
244
|
+
pass
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Redirect views
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from plain.views import RedirectView
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class ExampleRedirectView(RedirectView):
|
|
254
|
+
url = "/new-location/"
|
|
255
|
+
permanent = True
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## CSRF exemption
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from plain.views import View
|
|
262
|
+
from plain.views.csrf import CsrfExemptViewMixin
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class ExemptView(CsrfExemptViewMixin, View):
|
|
266
|
+
def post(self):
|
|
267
|
+
return "Hello, world!"
|
|
268
|
+
```
|
plain/views/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .base import View
|
|
2
|
+
from .forms import FormView
|
|
3
|
+
from .objects import CreateView, DeleteView, DetailView, ListView, UpdateView
|
|
4
|
+
from .redirect import RedirectView
|
|
5
|
+
from .templates import TemplateView
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"View",
|
|
9
|
+
"TemplateView",
|
|
10
|
+
"RedirectView",
|
|
11
|
+
"FormView",
|
|
12
|
+
"DetailView",
|
|
13
|
+
"CreateView",
|
|
14
|
+
"UpdateView",
|
|
15
|
+
"DeleteView",
|
|
16
|
+
"ListView",
|
|
17
|
+
"AuthViewMixin",
|
|
18
|
+
]
|
plain/views/base.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from plain.http import (
|
|
4
|
+
HttpRequest,
|
|
5
|
+
JsonResponse,
|
|
6
|
+
Response,
|
|
7
|
+
ResponseBase,
|
|
8
|
+
ResponseNotAllowed,
|
|
9
|
+
)
|
|
10
|
+
from plain.utils.decorators import classonlymethod
|
|
11
|
+
|
|
12
|
+
from .exceptions import ResponseException
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("plain.request")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class View:
|
|
18
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
19
|
+
# Views can customize their init, which receives
|
|
20
|
+
# the args and kwargs from as_view()
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def setup(self, request: HttpRequest, *args, **kwargs) -> None:
|
|
24
|
+
if hasattr(self, "get") and not hasattr(self, "head"):
|
|
25
|
+
self.head = self.get
|
|
26
|
+
|
|
27
|
+
self.request = request
|
|
28
|
+
self.url_args = args
|
|
29
|
+
self.url_kwargs = kwargs
|
|
30
|
+
|
|
31
|
+
@classonlymethod
|
|
32
|
+
def as_view(cls, *init_args, **init_kwargs):
|
|
33
|
+
def view(request, *args, **kwargs):
|
|
34
|
+
v = cls(*init_args, **init_kwargs)
|
|
35
|
+
v.setup(request, *args, **kwargs)
|
|
36
|
+
try:
|
|
37
|
+
return v.get_response()
|
|
38
|
+
except ResponseException as e:
|
|
39
|
+
return e.response
|
|
40
|
+
|
|
41
|
+
# Copy possible attributes set by decorators, e.g. @csrf_exempt, from
|
|
42
|
+
# the dispatch method.
|
|
43
|
+
view.__dict__.update(cls.get_response.__dict__)
|
|
44
|
+
view.view_class = cls
|
|
45
|
+
|
|
46
|
+
return view
|
|
47
|
+
|
|
48
|
+
def get_request_handler(self) -> callable:
|
|
49
|
+
"""Return the handler for the current request method."""
|
|
50
|
+
|
|
51
|
+
if not self.request.method:
|
|
52
|
+
raise AttributeError("HTTP method is not set")
|
|
53
|
+
|
|
54
|
+
handler = getattr(self, self.request.method.lower(), None)
|
|
55
|
+
|
|
56
|
+
if not handler:
|
|
57
|
+
logger.warning(
|
|
58
|
+
"Method Not Allowed (%s): %s",
|
|
59
|
+
self.request.method,
|
|
60
|
+
self.request.path,
|
|
61
|
+
extra={"status_code": 405, "request": self.request},
|
|
62
|
+
)
|
|
63
|
+
raise ResponseException(ResponseNotAllowed(self._allowed_methods()))
|
|
64
|
+
|
|
65
|
+
return handler
|
|
66
|
+
|
|
67
|
+
def get_response(self) -> ResponseBase:
|
|
68
|
+
handler = self.get_request_handler()
|
|
69
|
+
|
|
70
|
+
result = handler()
|
|
71
|
+
|
|
72
|
+
if isinstance(result, ResponseBase):
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
# Allow return of an int (status code)
|
|
76
|
+
# or tuple (status code, content)?
|
|
77
|
+
|
|
78
|
+
if isinstance(result, str):
|
|
79
|
+
return Response(result)
|
|
80
|
+
|
|
81
|
+
if isinstance(result, list):
|
|
82
|
+
return JsonResponse(result, safe=False)
|
|
83
|
+
|
|
84
|
+
if isinstance(result, dict):
|
|
85
|
+
return JsonResponse(result)
|
|
86
|
+
|
|
87
|
+
raise ValueError(f"Unexpected view return type: {type(result)}")
|
|
88
|
+
|
|
89
|
+
def options(self) -> Response:
|
|
90
|
+
"""Handle responding to requests for the OPTIONS HTTP verb."""
|
|
91
|
+
response = Response()
|
|
92
|
+
response.headers["Allow"] = ", ".join(self._allowed_methods())
|
|
93
|
+
response.headers["Content-Length"] = "0"
|
|
94
|
+
return response
|
|
95
|
+
|
|
96
|
+
def _allowed_methods(self) -> list[str]:
|
|
97
|
+
known_http_method_names = [
|
|
98
|
+
"get",
|
|
99
|
+
"post",
|
|
100
|
+
"put",
|
|
101
|
+
"patch",
|
|
102
|
+
"delete",
|
|
103
|
+
"head",
|
|
104
|
+
"options",
|
|
105
|
+
"trace",
|
|
106
|
+
]
|
|
107
|
+
return [m.upper() for m in known_http_method_names if hasattr(self, m)]
|
plain/views/csrf.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
|
|
3
|
+
from plain.utils.decorators import method_decorator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def csrf_exempt(view_func):
|
|
7
|
+
"""Mark a view function as being exempt from the CSRF view protection."""
|
|
8
|
+
|
|
9
|
+
# view_func.csrf_exempt = True would also work, but decorators are nicer
|
|
10
|
+
# if they don't have side effects, so return a new function.
|
|
11
|
+
@wraps(view_func)
|
|
12
|
+
def wrapper_view(*args, **kwargs):
|
|
13
|
+
return view_func(*args, **kwargs)
|
|
14
|
+
|
|
15
|
+
wrapper_view.csrf_exempt = True
|
|
16
|
+
return wrapper_view
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@method_decorator(csrf_exempt, name="get_response")
|
|
20
|
+
class CsrfExemptViewMixin:
|
|
21
|
+
"""CsrfExemptViewMixin needs to come before View in the class definition"""
|
|
22
|
+
|
|
23
|
+
def get_response(self):
|
|
24
|
+
return super().get_response()
|
plain/views/errors.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from plain.http import ResponseBase
|
|
2
|
+
|
|
3
|
+
from .templates import TemplateView
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ErrorView(TemplateView):
|
|
7
|
+
status_code: int
|
|
8
|
+
|
|
9
|
+
def __init__(self, status_code=None) -> None:
|
|
10
|
+
# Allow creating an ErrorView with a status code
|
|
11
|
+
# e.g. ErrorView.as_view(status_code=404)
|
|
12
|
+
if status_code is not None:
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
|
|
15
|
+
def get_template_names(self) -> list[str]:
|
|
16
|
+
return [f"{self.status_code}.html", "error.html"]
|
|
17
|
+
|
|
18
|
+
def get_request_handler(self):
|
|
19
|
+
return self.get # All methods (post, patch, etc.) will use the get()
|
|
20
|
+
|
|
21
|
+
def get_response(self) -> ResponseBase:
|
|
22
|
+
response = super().get_response()
|
|
23
|
+
# Set the status code we want
|
|
24
|
+
response.status_code = self.status_code
|
|
25
|
+
return response
|
plain/views/forms.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from plain.exceptions import ImproperlyConfigured
|
|
5
|
+
from plain.http import Response, ResponseRedirect
|
|
6
|
+
|
|
7
|
+
from .templates import TemplateView
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from plain.forms import BaseForm
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FormView(TemplateView):
|
|
14
|
+
"""A view for displaying a form and rendering a template response."""
|
|
15
|
+
|
|
16
|
+
form_class: type["BaseForm"] | None = None
|
|
17
|
+
success_url: Callable | str | None = None
|
|
18
|
+
|
|
19
|
+
def get_form(self) -> "BaseForm":
|
|
20
|
+
"""Return an instance of the form to be used in this view."""
|
|
21
|
+
if not self.form_class:
|
|
22
|
+
raise ImproperlyConfigured(
|
|
23
|
+
"No form class provided. Define {cls}.form_class or override "
|
|
24
|
+
"{cls}.get_form().".format(cls=self.__class__.__name__)
|
|
25
|
+
)
|
|
26
|
+
return self.form_class(**self.get_form_kwargs())
|
|
27
|
+
|
|
28
|
+
def get_form_kwargs(self) -> dict:
|
|
29
|
+
"""Return the keyword arguments for instantiating the form."""
|
|
30
|
+
kwargs: dict = {
|
|
31
|
+
"initial": {}, # Make it easier to set keys in subclasses
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if hasattr(self, "request") and self.request.method in ("POST", "PUT"):
|
|
35
|
+
kwargs.update(
|
|
36
|
+
{
|
|
37
|
+
"data": self.request.POST,
|
|
38
|
+
"files": self.request.FILES,
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
return kwargs
|
|
42
|
+
|
|
43
|
+
def get_success_url(self) -> str:
|
|
44
|
+
"""Return the URL to redirect to after processing a valid form."""
|
|
45
|
+
if not self.success_url:
|
|
46
|
+
raise ImproperlyConfigured("No URL to redirect to. Provide a success_url.")
|
|
47
|
+
return str(self.success_url) # success_url may be lazy
|
|
48
|
+
|
|
49
|
+
def form_valid(self, form: "BaseForm") -> Response:
|
|
50
|
+
"""If the form is valid, redirect to the supplied URL."""
|
|
51
|
+
return ResponseRedirect(self.get_success_url())
|
|
52
|
+
|
|
53
|
+
def form_invalid(self, form: "BaseForm") -> Response:
|
|
54
|
+
"""If the form is invalid, render the invalid form."""
|
|
55
|
+
context = {
|
|
56
|
+
**self.get_template_context(),
|
|
57
|
+
"form": form,
|
|
58
|
+
}
|
|
59
|
+
return self.get_template().render(context)
|
|
60
|
+
|
|
61
|
+
def get_template_context(self) -> dict:
|
|
62
|
+
"""Insert the form into the context dict."""
|
|
63
|
+
context = super().get_template_context()
|
|
64
|
+
context["form"] = self.get_form()
|
|
65
|
+
return context
|
|
66
|
+
|
|
67
|
+
def post(self) -> Response:
|
|
68
|
+
"""
|
|
69
|
+
Handle POST requests: instantiate a form instance with the passed
|
|
70
|
+
POST variables and then check if it's valid.
|
|
71
|
+
"""
|
|
72
|
+
form = self.get_form()
|
|
73
|
+
if form.is_valid():
|
|
74
|
+
return self.form_valid(form)
|
|
75
|
+
else:
|
|
76
|
+
return self.form_invalid(form)
|