1# Views
2
3**Take a request, return a response.**
4
5- [Overview](#overview)
6- [HTTP methods -> class methods](#http-methods---class-methods)
7- [Return types](#return-types)
8- [Template views](#template-views)
9- [Form views](#form-views)
10- [Object views](#object-views)
11- [Response exceptions](#response-exceptions)
12- [Error views](#error-views)
13- [Redirect views](#redirect-views)
14- [CSRF exempt views](#csrf-exempt-views)
15
16## Overview
17
18Plain views are written as classes,
19with a straightforward API that keeps simple views simple,
20but gives you the power of a full class to handle more complex cases.
21
22```python
23from plain.views import View
24
25
26class ExampleView(View):
27 def get(self):
28 return "<html><body>Hello, world!</body></html>"
29```
30
31## HTTP methods -> class methods
32
33The HTTP method of the request will map to a class method of the same name on the view.
34
35If a request comes in and there isn't a matching method on the view,
36Plain will return a `405 Method Not Allowed` response.
37
38```python
39from plain.views import View
40
41
42class ExampleView(View):
43 def get(self):
44 pass
45
46 def post(self):
47 pass
48
49 def put(self):
50 pass
51
52 def patch(self):
53 pass
54
55 def delete(self):
56 pass
57
58 def trace(self):
59 pass
60```
61
62The [base `View` class](./base.py#View) defines default `options` and `head` behavior,
63but you can override these too.
64
65## Return types
66
67For simple JSON responses, HTML, or status code responses,
68you don't need to instantiate a `Response` object.
69
70```python
71class JsonView(View):
72 def get(self):
73 return {"message": "Hello, world!"}
74
75
76class HtmlView(View):
77 def get(self):
78 return "<html><body>Hello, world!</body></html>"
79
80
81class StatusCodeView(View):
82 def get(self):
83 return 204 # No content
84```
85
86## Template views
87
88The most common behavior for a view is to render a template.
89
90```python
91from plain.views import TemplateView
92
93
94class ExampleView(TemplateView):
95 template_name = "example.html"
96
97 def get_template_context(self):
98 context = super().get_template_context()
99 context["message"] = "Hello, world!"
100 return context
101```
102
103The [`TemplateView`](./templates.py#TemplateView) is also the base class for _most_ of the other built-in view classes.
104
105Template views that don't need any custom context can use `TemplateView.as_view()` directly in the URL route.
106
107```python
108from plain.views import TemplateView
109from plain.urls import path, Router
110
111
112class AppRouter(Router):
113 routes = [
114 path("/example/", TemplateView.as_view(template_name="example.html")),
115 ]
116```
117
118## Form views
119
120Standard [forms](../forms) can be rendered and processed by a [`FormView`](./forms.py#FormView).
121
122```python
123from plain.views import FormView
124from .forms import ExampleForm
125
126
127class ExampleView(FormView):
128 template_name = "example.html"
129 form_class = ExampleForm
130 success_url = "." # Redirect to the same page
131
132 def form_valid(self, form):
133 # Do other successfull form processing here
134 return super().form_valid(form)
135```
136
137Rendering forms is done directly in the HTML.
138
139```html
140{% extends "base.html" %}
141
142{% block content %}
143
144<form method="post">
145 <!-- Render general form errors -->
146 {% for error in form.non_field_errors %}
147 <div>{{ error }}</div>
148 {% endfor %}
149
150 <!-- Render form fields individually (or with Jinja helps or other concepts) -->
151 <label for="{{ form.email.html_id }}">Email</label>
152 <input
153 type="email"
154 name="{{ form.email.html_name }}"
155 id="{{ form.email.html_id }}"
156 value="{{ form.email.value() or '' }}"
157 autocomplete="email"
158 autofocus
159 required>
160 {% if form.email.errors %}
161 <div>{{ form.email.errors|join(', ') }}</div>
162 {% endif %}
163
164 <button type="submit">Save</button>
165</form>
166
167{% endblock %}
168```
169
170## Object views
171
172The object views support the standard CRUD (create, read/detail, update, delete) operations, plus a list view.
173
174```python
175from plain.views import DetailView, CreateView, UpdateView, DeleteView, ListView
176
177
178class ExampleDetailView(DetailView):
179 template_name = "detail.html"
180
181 def get_object(self):
182 return MyObjectClass.query.get(
183 id=self.url_kwargs["id"],
184 user=self.user, # Limit access
185 )
186
187
188class ExampleCreateView(CreateView):
189 template_name = "create.html"
190 form_class = CustomCreateForm
191 success_url = "."
192
193
194class ExampleUpdateView(UpdateView):
195 template_name = "update.html"
196 form_class = CustomUpdateForm
197 success_url = "."
198
199 def get_object(self):
200 return MyObjectClass.query.get(
201 id=self.url_kwargs["id"],
202 user=self.user, # Limit access
203 )
204
205
206class ExampleDeleteView(DeleteView):
207 template_name = "delete.html"
208 success_url = "."
209
210 # No form class necessary.
211 # Just POST to this view to delete the object.
212
213 def get_object(self):
214 return MyObjectClass.query.get(
215 id=self.url_kwargs["id"],
216 user=self.user, # Limit access
217 )
218
219
220class ExampleListView(ListView):
221 template_name = "list.html"
222
223 def get_objects(self):
224 return MyObjectClass.query.filter(
225 user=self.user, # Limit access
226 )
227```
228
229## Response exceptions
230
231At any point in the request handling,
232a view can raise a [`ResponseException`](./exceptions.py#ResponseException) to immediately exit and return the wrapped response.
233
234This 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.
235
236```python
237from plain.views import DetailView
238from plain.views.exceptions import ResponseException
239from plain.http import Response
240
241
242class ExampleView(DetailView):
243 def get_object(self):
244 if self.user and self.user.exceeds_rate_limit:
245 raise ResponseException(
246 Response("Rate limit exceeded", status_code=429)
247 )
248
249 return AnExpensiveObject()
250```
251
252## Error views
253
254HTTP errors are rendered using templates. Create templates for the errors users actually see:
255
256- `templates/404.html` - Page not found
257- `templates/403.html` - Forbidden
258- `templates/500.html` - Server error
259
260Plain looks for `{status_code}.html` templates, then returns a plain HTTP response if not found. Most apps only need the three specific templates above.
261
262Templates receive `status_code` and `exception` in context.
263
264**Note:** `500.html` should be self-contained - avoid extending base templates or accessing database/session, since server errors can occur during middleware or template rendering. `404.html` and `403.html` can safely extend base templates since they occur during view execution after middleware runs.
265
266## Redirect views
267
268```python
269from plain.views import RedirectView
270
271
272class ExampleRedirectView(RedirectView):
273 url = "/new-location/"
274 permanent = True
275```
276
277Redirect views can also be used in the URL router.
278
279```python
280from plain.views import RedirectView
281from plain.urls import path, Router
282
283
284class AppRouter(Router):
285 routes = [
286 path("/old-location/", RedirectView.as_view(url="/new-location/", permanent=True)),
287 ]
288```
289
290## CSRF exempt views
291
292```python
293from plain.views import View
294from plain.views.csrf import CsrfExemptViewMixin
295
296
297class ExemptView(CsrfExemptViewMixin, View):
298 def post(self):
299 return "Hello, world!"
300```