Plain is headed towards 1.0! Subscribe for development updates →

Views

Take a request, return a response.

Plain views are written as classes, with a straightforward API that keeps simple views simple, but gives you the power of a full class to handle more complex cases.

from plain.views import View


class ExampleView(View):
    def get(self):
        return "Hello, world!"

HTTP methods -> class methods

The HTTP methd of the request will map to a class method of the same name on the view.

If a request comes in and there isn't a matching method on the view, Plain will return a 405 Method Not Allowed response.

from plain.views import View


class ExampleView(View):
    def get(self):
        pass

    def post(self):
        pass

    def put(self):
        pass

    def patch(self):
        pass

    def delete(self):
        pass

    def trace(self):
        pass

The base View class defines default options and head behavior, but you can override these too.

Return types

For simple plain text and JSON responses, you don't need to instantiate a Response object.

class TextView(View):
    def get(self):
        return "Hello, world!"


class JsonView(View):
    def get(self):
        return {"message": "Hello, world!"}

Template views

The most common behavior for a view is to render a template.

from plain.views import TemplateView


class ExampleView(TemplateView):
    template_name = "example.html"

    def get_template_context(self):
        context = super().get_template_context()
        context["message"] = "Hello, world!"
        return context

The TemplateView is also the base class for most of the other built-in view classes.

Form views

Standard forms can be rendered and processed by a FormView.

from plain.views import FormView
from .forms import ExampleForm


class ExampleView(FormView):
    template_name = "example.html"
    form_class = ExampleForm
    success_url = "."  # Redirect to the same page

    def form_valid(self, form):
        # Do other successfull form processing here
        return super().form_valid(form)

Rendering forms is done directly in the HTML.

{% extends "base.html" %}

{% block content %}

<form method="post">
    {{ csrf_input }}

    <!-- Render general form errors -->
    {% for error in form.non_field_errors %}
    <div>{{ error }}</div>
    {% endfor %}

    <!-- Render form fields individually (or with Jinja helps or other concepts) -->
    <label for="{{ form.email.html_id }}">Email</label>
    <input
        type="email"
        name="{{ form.email.html_name }}"
        id="{{ form.email.html_id }}"
        value="{{ form.email.value() or '' }}"
        autocomplete="email"
        autofocus
        required>
    {% if form.email.errors %}
    <div>{{ form.email.errors|join(', ') }}</div>
    {% endif %}

    <button type="submit">Save</button>
</form>

{% endblock %}

Object views

The object views support the standard CRUD (create, read/detail, update, delete) operations, plus a list view.

from plain.views import DetailView, CreateView, UpdateView, DeleteView, ListView


class ExampleDetailView(DetailView):
    template_name = "detail.html"

    def get_object(self):
        return MyObjectClass.objects.get(
            pk=self.url_kwargs["pk"],
            user=self.request.user,  # Limit access
        )


class ExampleCreateView(CreateView):
    template_name = "create.html"
    form_class = CustomCreateForm
    success_url = "."


class ExampleUpdateView(UpdateView):
    template_name = "update.html"
    form_class = CustomUpdateForm
    success_url = "."

    def get_object(self):
        return MyObjectClass.objects.get(
            pk=self.url_kwargs["pk"],
            user=self.request.user,  # Limit access
        )


class ExampleDeleteView(DeleteView):
    template_name = "delete.html"
    success_url = "."

    # No form class necessary.
    # Just POST to this view to delete the object.

    def get_object(self):
        return MyObjectClass.objects.get(
            pk=self.url_kwargs["pk"],
            user=self.request.user,  # Limit access
        )


class ExampleListView(ListView):
    template_name = "list.html"

    def get_objects(self):
        return MyObjectClass.objects.filter(
            user=self.request.user,  # Limit access
        )

Response exceptions

At any point in the request handling, a view can raise a ResponseException to immediately exit and return the wrapped response.

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.

from plain.views import DetailView
from plain.views.exceptions import ResponseException
from plain.http import Response


class ExampleView(DetailView):
    def get_object(self):
        if self.request.user.exceeds_rate_limit:
            raise ResponseException(
                Response("Rate limit exceeded", status=429)
            )

        return AnExpensiveObject()

Error views

By default, HTTP errors will be rendered by templates/<status_code>.html or templates/error.html.

You can define your own error views by pointing the HTTP_ERROR_VIEWS setting to a dictionary of status codes and view classes.

# app/settings.py
HTTP_ERROR_VIEWS = {
    404: "errors.NotFoundView",
}
# app/errors.py
from plain.views import View


class NotFoundView(View):
    def get(self):
        # A custom implementation or error view handling
        pass

Redirect views

from plain.views import RedirectView


class ExampleRedirectView(RedirectView):
    url = "/new-location/"
    permanent = True

CSRF exemption

from plain.views import View
from plain.views.csrf import CsrfExemptViewMixin


class ExemptView(CsrfExemptViewMixin, View):
    def post(self):
        return "Hello, world!"
  1import logging
  2
  3from plain.http import (
  4    HttpRequest,
  5    JsonResponse,
  6    Response,
  7    ResponseBase,
  8    ResponseNotAllowed,
  9)
 10from plain.utils.decorators import classonlymethod
 11
 12from .exceptions import ResponseException
 13
 14logger = logging.getLogger("plain.request")
 15
 16
 17class View:
 18    request: HttpRequest
 19    url_args: tuple
 20    url_kwargs: dict
 21
 22    # By default, any of these are allowed if a method is defined for it.
 23    allowed_http_methods = [
 24        "get",
 25        "post",
 26        "put",
 27        "patch",
 28        "delete",
 29        "head",
 30        "options",
 31        "trace",
 32    ]
 33
 34    def __init__(self, *args, **kwargs) -> None:
 35        # Views can customize their init, which receives
 36        # the args and kwargs from as_view()
 37        pass
 38
 39    def setup(self, request: HttpRequest, *args, **kwargs) -> None:
 40        if hasattr(self, "get") and not hasattr(self, "head"):
 41            self.head = self.get
 42
 43        self.request = request
 44        self.url_args = args
 45        self.url_kwargs = kwargs
 46
 47    @classonlymethod
 48    def as_view(cls, *init_args, **init_kwargs):
 49        def view(request, *args, **kwargs):
 50            v = cls(*init_args, **init_kwargs)
 51            v.setup(request, *args, **kwargs)
 52            try:
 53                return v.get_response()
 54            except ResponseException as e:
 55                return e.response
 56
 57        view.view_class = cls
 58
 59        return view
 60
 61    def get_request_handler(self) -> callable:
 62        """Return the handler for the current request method."""
 63
 64        if not self.request.method:
 65            raise AttributeError("HTTP method is not set")
 66
 67        handler = getattr(self, self.request.method.lower(), None)
 68
 69        if not handler or self.request.method.lower() not in self.allowed_http_methods:
 70            logger.warning(
 71                "Method Not Allowed (%s): %s",
 72                self.request.method,
 73                self.request.path,
 74                extra={"status_code": 405, "request": self.request},
 75            )
 76            raise ResponseException(ResponseNotAllowed(self._allowed_methods()))
 77
 78        return handler
 79
 80    def get_response(self) -> ResponseBase:
 81        handler = self.get_request_handler()
 82
 83        result = handler()
 84
 85        if isinstance(result, ResponseBase):
 86            return result
 87
 88        if isinstance(result, str):
 89            return Response(result)
 90
 91        if isinstance(result, list):
 92            return JsonResponse(result, safe=False)
 93
 94        if isinstance(result, dict):
 95            return JsonResponse(result)
 96
 97        if isinstance(result, int):
 98            return Response(status=result)
 99
100        # Allow tuple for (status_code, content)?
101
102        raise ValueError(f"Unexpected view return type: {type(result)}")
103
104    def options(self) -> Response:
105        """Handle responding to requests for the OPTIONS HTTP verb."""
106        response = Response()
107        response.headers["Allow"] = ", ".join(self._allowed_methods())
108        response.headers["Content-Length"] = "0"
109        return response
110
111    def _allowed_methods(self) -> list[str]:
112        return [m.upper() for m in self.allowed_http_methods if hasattr(self, m)]