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