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