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.
Files changed (169) hide show
  1. plain/README.md +33 -0
  2. plain/__main__.py +5 -0
  3. plain/assets/README.md +56 -0
  4. plain/assets/__init__.py +6 -0
  5. plain/assets/finders.py +233 -0
  6. plain/assets/preflight.py +14 -0
  7. plain/assets/storage.py +916 -0
  8. plain/assets/utils.py +52 -0
  9. plain/assets/whitenoise/__init__.py +5 -0
  10. plain/assets/whitenoise/base.py +259 -0
  11. plain/assets/whitenoise/compress.py +189 -0
  12. plain/assets/whitenoise/media_types.py +137 -0
  13. plain/assets/whitenoise/middleware.py +197 -0
  14. plain/assets/whitenoise/responders.py +286 -0
  15. plain/assets/whitenoise/storage.py +178 -0
  16. plain/assets/whitenoise/string_utils.py +13 -0
  17. plain/cli/README.md +123 -0
  18. plain/cli/__init__.py +3 -0
  19. plain/cli/cli.py +439 -0
  20. plain/cli/formatting.py +61 -0
  21. plain/cli/packages.py +73 -0
  22. plain/cli/print.py +9 -0
  23. plain/cli/startup.py +33 -0
  24. plain/csrf/README.md +3 -0
  25. plain/csrf/middleware.py +466 -0
  26. plain/csrf/views.py +10 -0
  27. plain/debug.py +23 -0
  28. plain/exceptions.py +242 -0
  29. plain/forms/README.md +14 -0
  30. plain/forms/__init__.py +8 -0
  31. plain/forms/boundfield.py +58 -0
  32. plain/forms/exceptions.py +11 -0
  33. plain/forms/fields.py +1030 -0
  34. plain/forms/forms.py +297 -0
  35. plain/http/README.md +1 -0
  36. plain/http/__init__.py +51 -0
  37. plain/http/cookie.py +20 -0
  38. plain/http/multipartparser.py +743 -0
  39. plain/http/request.py +754 -0
  40. plain/http/response.py +719 -0
  41. plain/internal/__init__.py +0 -0
  42. plain/internal/files/README.md +3 -0
  43. plain/internal/files/__init__.py +3 -0
  44. plain/internal/files/base.py +161 -0
  45. plain/internal/files/locks.py +127 -0
  46. plain/internal/files/move.py +102 -0
  47. plain/internal/files/temp.py +79 -0
  48. plain/internal/files/uploadedfile.py +150 -0
  49. plain/internal/files/uploadhandler.py +254 -0
  50. plain/internal/files/utils.py +78 -0
  51. plain/internal/handlers/__init__.py +0 -0
  52. plain/internal/handlers/base.py +133 -0
  53. plain/internal/handlers/exception.py +145 -0
  54. plain/internal/handlers/wsgi.py +216 -0
  55. plain/internal/legacy/__init__.py +0 -0
  56. plain/internal/legacy/__main__.py +12 -0
  57. plain/internal/legacy/management/__init__.py +414 -0
  58. plain/internal/legacy/management/base.py +692 -0
  59. plain/internal/legacy/management/color.py +113 -0
  60. plain/internal/legacy/management/commands/__init__.py +0 -0
  61. plain/internal/legacy/management/commands/collectstatic.py +297 -0
  62. plain/internal/legacy/management/sql.py +67 -0
  63. plain/internal/legacy/management/utils.py +175 -0
  64. plain/json.py +40 -0
  65. plain/logs/README.md +24 -0
  66. plain/logs/__init__.py +5 -0
  67. plain/logs/configure.py +39 -0
  68. plain/logs/loggers.py +74 -0
  69. plain/logs/utils.py +46 -0
  70. plain/middleware/README.md +3 -0
  71. plain/middleware/__init__.py +0 -0
  72. plain/middleware/clickjacking.py +52 -0
  73. plain/middleware/common.py +87 -0
  74. plain/middleware/gzip.py +64 -0
  75. plain/middleware/security.py +64 -0
  76. plain/packages/README.md +41 -0
  77. plain/packages/__init__.py +4 -0
  78. plain/packages/config.py +259 -0
  79. plain/packages/registry.py +438 -0
  80. plain/paginator.py +187 -0
  81. plain/preflight/README.md +3 -0
  82. plain/preflight/__init__.py +38 -0
  83. plain/preflight/compatibility/__init__.py +0 -0
  84. plain/preflight/compatibility/django_4_0.py +20 -0
  85. plain/preflight/files.py +19 -0
  86. plain/preflight/messages.py +88 -0
  87. plain/preflight/registry.py +72 -0
  88. plain/preflight/security/__init__.py +0 -0
  89. plain/preflight/security/base.py +268 -0
  90. plain/preflight/security/csrf.py +40 -0
  91. plain/preflight/urls.py +117 -0
  92. plain/runtime/README.md +75 -0
  93. plain/runtime/__init__.py +61 -0
  94. plain/runtime/global_settings.py +199 -0
  95. plain/runtime/user_settings.py +353 -0
  96. plain/signals/README.md +14 -0
  97. plain/signals/__init__.py +5 -0
  98. plain/signals/dispatch/__init__.py +9 -0
  99. plain/signals/dispatch/dispatcher.py +320 -0
  100. plain/signals/dispatch/license.txt +35 -0
  101. plain/signing.py +299 -0
  102. plain/templates/README.md +20 -0
  103. plain/templates/__init__.py +6 -0
  104. plain/templates/core.py +24 -0
  105. plain/templates/jinja/README.md +227 -0
  106. plain/templates/jinja/__init__.py +22 -0
  107. plain/templates/jinja/defaults.py +119 -0
  108. plain/templates/jinja/extensions.py +39 -0
  109. plain/templates/jinja/filters.py +28 -0
  110. plain/templates/jinja/globals.py +19 -0
  111. plain/test/README.md +3 -0
  112. plain/test/__init__.py +16 -0
  113. plain/test/client.py +985 -0
  114. plain/test/utils.py +255 -0
  115. plain/urls/README.md +3 -0
  116. plain/urls/__init__.py +40 -0
  117. plain/urls/base.py +118 -0
  118. plain/urls/conf.py +94 -0
  119. plain/urls/converters.py +66 -0
  120. plain/urls/exceptions.py +9 -0
  121. plain/urls/resolvers.py +731 -0
  122. plain/utils/README.md +3 -0
  123. plain/utils/__init__.py +0 -0
  124. plain/utils/_os.py +52 -0
  125. plain/utils/cache.py +327 -0
  126. plain/utils/connection.py +84 -0
  127. plain/utils/crypto.py +76 -0
  128. plain/utils/datastructures.py +345 -0
  129. plain/utils/dateformat.py +329 -0
  130. plain/utils/dateparse.py +154 -0
  131. plain/utils/dates.py +76 -0
  132. plain/utils/deconstruct.py +54 -0
  133. plain/utils/decorators.py +90 -0
  134. plain/utils/deprecation.py +6 -0
  135. plain/utils/duration.py +44 -0
  136. plain/utils/email.py +12 -0
  137. plain/utils/encoding.py +235 -0
  138. plain/utils/functional.py +456 -0
  139. plain/utils/hashable.py +26 -0
  140. plain/utils/html.py +401 -0
  141. plain/utils/http.py +374 -0
  142. plain/utils/inspect.py +73 -0
  143. plain/utils/ipv6.py +46 -0
  144. plain/utils/itercompat.py +8 -0
  145. plain/utils/module_loading.py +69 -0
  146. plain/utils/regex_helper.py +353 -0
  147. plain/utils/safestring.py +72 -0
  148. plain/utils/termcolors.py +221 -0
  149. plain/utils/text.py +518 -0
  150. plain/utils/timesince.py +138 -0
  151. plain/utils/timezone.py +244 -0
  152. plain/utils/tree.py +126 -0
  153. plain/validators.py +603 -0
  154. plain/views/README.md +268 -0
  155. plain/views/__init__.py +18 -0
  156. plain/views/base.py +107 -0
  157. plain/views/csrf.py +24 -0
  158. plain/views/errors.py +25 -0
  159. plain/views/exceptions.py +4 -0
  160. plain/views/forms.py +76 -0
  161. plain/views/objects.py +229 -0
  162. plain/views/redirect.py +72 -0
  163. plain/views/templates.py +66 -0
  164. plain/wsgi.py +11 -0
  165. plain-0.1.0.dist-info/LICENSE +85 -0
  166. plain-0.1.0.dist-info/METADATA +51 -0
  167. plain-0.1.0.dist-info/RECORD +169 -0
  168. plain-0.1.0.dist-info/WHEEL +4 -0
  169. 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
+ ```
@@ -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
@@ -0,0 +1,4 @@
1
+ class ResponseException(Exception):
2
+ def __init__(self, response):
3
+ self.response = response
4
+ super().__init__(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)