Plain is headed towards 1.0! Subscribe for development updates →

  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```