Plain is headed towards 1.0! Subscribe for development updates →

  1from abc import ABC, abstractmethod
  2from functools import cached_property
  3from typing import Any
  4
  5from plain.exceptions import ImproperlyConfigured
  6
  7try:
  8    from plain.models.exceptions import ObjectDoesNotExist
  9except ImportError:
 10    ObjectDoesNotExist = None  # type: ignore[assignment]
 11
 12from plain.forms import BaseForm, Form
 13from plain.http import Http404
 14
 15from .forms import FormView
 16from .templates import TemplateView
 17
 18
 19class CreateView(FormView):
 20    """
 21    View for creating a new object, with a response rendered by a template.
 22    """
 23
 24    # TODO? would rather you have to specify this...
 25    def get_success_url(self, form: BaseForm) -> str:
 26        """Return the URL to redirect to after processing a valid form."""
 27        if self.success_url:
 28            url = str(self.success_url).format(**self.object.__dict__)
 29        else:
 30            try:
 31                url = self.object.get_absolute_url()
 32            except AttributeError:
 33                raise ImproperlyConfigured(
 34                    "No URL to redirect to.  Either provide a url or define"
 35                    " a get_absolute_url method on the Model."
 36                )
 37        return url
 38
 39    def form_valid(self, form: BaseForm) -> Any:
 40        """If the form is valid, save the associated model."""
 41        self.object = form.save()  # type: ignore[attr-defined]
 42        return super().form_valid(form)
 43
 44
 45class ObjectTemplateViewMixin(ABC):
 46    context_object_name = ""
 47
 48    @cached_property
 49    def object(self) -> Any:
 50        try:
 51            obj = self.get_object()
 52        except Exception as e:
 53            # If ObjectDoesNotExist is available and this is that exception, raise 404
 54            if ObjectDoesNotExist and isinstance(e, ObjectDoesNotExist):
 55                raise Http404
 56            # Otherwise, let other exceptions bubble up
 57            raise
 58
 59        # Also raise 404 if get_object() returns None
 60        if not obj:
 61            raise Http404
 62
 63        return obj
 64
 65    @abstractmethod
 66    def get_object(self) -> Any: ...
 67
 68    def get_template_context(self) -> dict[str, Any]:
 69        """Insert the single object into the context dict."""
 70        context = super().get_template_context()  # type: ignore
 71        context["object"] = (
 72            self.object
 73        )  # Some templates can benefit by always knowing a primary "object" can be present
 74        if self.context_object_name:
 75            context[self.context_object_name] = self.object
 76        return context
 77
 78
 79class DetailView(ObjectTemplateViewMixin, TemplateView):
 80    """
 81    Render a "detail" view of an object.
 82
 83    By default this is a model instance looked up from `self.queryset`, but the
 84    view will support display of *any* object by overriding `self.get_object()`.
 85    """
 86
 87    pass
 88
 89
 90class UpdateView(ObjectTemplateViewMixin, FormView):
 91    """View for updating an object, with a response rendered by a template."""
 92
 93    def get_success_url(self, form: BaseForm) -> str:
 94        """Return the URL to redirect to after processing a valid form."""
 95        if self.success_url:
 96            url = str(self.success_url).format(**self.object.__dict__)
 97        else:
 98            try:
 99                url = self.object.get_absolute_url()
100            except AttributeError:
101                raise ImproperlyConfigured(
102                    "No URL to redirect to.  Either provide a url or define"
103                    " a get_absolute_url method on the Model."
104                )
105        return url
106
107    def form_valid(self, form: BaseForm) -> Any:
108        """If the form is valid, save the associated model."""
109        form.save()  # type: ignore[attr-defined]
110        return super().form_valid(form)
111
112    def get_form_kwargs(self) -> dict[str, Any]:
113        """Return the keyword arguments for instantiating the form."""
114        kwargs = super().get_form_kwargs()
115        kwargs.update({"instance": self.object})
116        return kwargs
117
118
119class DeleteView(ObjectTemplateViewMixin, FormView):
120    """
121    View for deleting an object retrieved with self.get_object(), with a
122    response rendered by a template.
123    """
124
125    class EmptyDeleteForm(Form):
126        def __init__(self, instance: Any, **kwargs: Any) -> None:
127            self.instance = instance
128            super().__init__(**kwargs)
129
130        def save(self) -> None:
131            self.instance.delete()
132
133    form_class = EmptyDeleteForm
134
135    def get_form_kwargs(self) -> dict[str, Any]:
136        """Return the keyword arguments for instantiating the form."""
137        kwargs = super().get_form_kwargs()
138        kwargs.update({"instance": self.object})
139        return kwargs
140
141    def form_valid(self, form: BaseForm) -> Any:
142        """If the form is valid, save the associated model."""
143        form.save()  # type: ignore[attr-defined]
144        return super().form_valid(form)
145
146
147class ListView(TemplateView, ABC):
148    """
149    Render some list of objects, set by `self.get_queryset()`, with a response
150    rendered by a template.
151    """
152
153    context_object_name = ""
154
155    @cached_property
156    def objects(self) -> Any:
157        return self.get_objects()
158
159    @abstractmethod
160    def get_objects(self) -> Any: ...
161
162    def get_template_context(self) -> dict[str, Any]:
163        """Insert the single object into the context dict."""
164        context = super().get_template_context()  # type: ignore
165        context["objects"] = self.objects
166        if self.context_object_name:
167            context[self.context_object_name] = self.objects
168        return context