Newer
Older
__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
__license__ = "AGPL v3"
from typing import Any, Dict
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.db.models.query import QuerySet
from django.forms import formset_factory, modelformset_factory
from django.forms.formsets import ManagementForm
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.html import format_html
from django.views import View
from django.views.generic import FormView, ListView
from django.views.generic.detail import SingleObjectMixin
from scipost.permissions import HTMXResponse
from .forms import HTMXInlineCRUDModelForm
def empty(request):
return HttpResponse("")
class HTMXInlineCRUDModelFormView(FormView):
template_name = "htmx/htmx_inline_crud_form.html"
form_class = HTMXInlineCRUDModelForm
instance_li_template_name = None
target_element_id = "htmx-crud-{instance_type}-{instance_id}"
edit = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.instance_type = self.form_class.Meta.model.__name__.lower()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["target_element_id"] = self.get_target_element_id()
context["instance_li_template_name"] = self.instance_li_template_name
context["instance_type"] = self.instance_type
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
context[self.instance_type] = context["instance"] = self.instance
return context
def post(self, request, *args, **kwargs):
self.instance = get_object_or_404(self.form_class.Meta.model, pk=kwargs["pk"])
self.edit = True
return super().post(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
self.instance = get_object_or_404(self.form_class.Meta.model, pk=kwargs["pk"])
self.edit = bool(request.GET.get("edit", None))
super().get(request, *args, **kwargs)
return render(request, self.template_name, self.get_context_data(**kwargs))
def delete(self, request, *args, **kwargs):
self.instance = get_object_or_404(self.form_class.Meta.model, pk=kwargs["pk"])
self.instance.delete()
messages.success(
self.request, f"{self.instance_type.title()} deleted successfully"
)
return empty(request)
def get_form(self) -> BaseForm:
if self.request.method == "GET" and not self.edit:
return None
return super().get_form()
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.update({"instance": self.instance})
return kwargs
def get_target_element_id(self) -> str:
return self.target_element_id.format(
instance_type=self.instance_type,
instance_id=self.instance.id,
)
def get_success_url(self) -> str:
return self.get_context_data()["view"].request.path
def form_valid(self, form: BaseForm) -> HttpResponse:
form.save()
messages.success(
self.request, f"{self.instance_type.title()} saved successfully"
)
return super().form_valid(form)
class HTMXInlineCRUDModelListView(ListView):
template_name = "htmx/htmx_inline_crud_list.html"
add_form_class = None
model = None
model_form_view_url = None
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.instance_type = self.model.__name__.lower()
def _append_model_form_view_url(self, queryset: QuerySet, **kwargs) -> QuerySet:
kwargs.update({"pk": object.pk})
self.model_form_view_url, kwargs=kwargs
return queryset
def post(self, request, *args, **kwargs):
"""
Post requests to the list view are treated as new object creation requests.
"""
if self.add_form_class is None:
return empty(request)
add_form = self.add_form_class(request.POST or None, **kwargs)
object = add_form.save()
kwargs.update({"pk": object.pk})
messages.success(self.request, f"{self.instance_type.title()} successfully")
return redirect(reverse(self.model_form_view_url, kwargs=kwargs))
else:
response = TemplateResponse(
request,
"htmx/htmx_inline_crud_new_form.html",
{
"list_url": request.path,
"add_form": add_form,
"instance_type": self.instance_type,
},
)
# Modify headers to swap in place with "HX-Reswap": "outerHTML"
# This will avoid duplication of the form if errors are present
response["HX-Reswap"] = "outerHTML"
return response
def get_context_data(self, **kwargs: Any):
context = super().get_context_data(**kwargs)
context["list_url"] = self.request.path
context["instance_type"] = self.instance_type
return context
class HXDynselSelectOptionView(View):
def get(self, request, content_type_id, object_id):
obj = self.get_object(content_type_id, object_id)
format_html('<option value="{}" selected>{}</option>', obj.pk, str(obj))
def get_object(self, content_type_id, object_id):
model = ContentType.objects.get_for_id(content_type_id).model_class()
if model is None:
raise ValueError("Model not found")
return get_object_or_404(model, pk=object_id)
model = None
template_name = "htmx/dynsel_list_page.html"
paginate_by = 16
def get(self, request):
self.q = request.GET.get("q", "")
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
context = self.get_context_data()
return self.render_to_response(context)
def get_page_obj(self, page_nr):
paginator = Paginator(self.get_queryset(), self.paginate_by)
page_obj = paginator.get_page(page_nr)
return page_obj
def get_queryset(self):
result = self.search(
self.model.objects.all(),
self.q,
)
return result
def render_to_response(self, context):
return TemplateResponse(
self.request,
self.template_name,
context,
)
def search(self, queryset, q):
return queryset
def get_context_data(self, **kwargs):
context = {}
context["model_name"] = self.model._meta.verbose_name_plural
context["q"] = self.q
context["page_obj"] = self.get_page_obj(self.page_nr)
return context
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
class HXFormSetView(View):
"""
Class-based view for handling formsets with HTMX.
"""
form_class = None
formset_prefix = "formset"
template_name = "htmx/formset_form.html"
template_name_form = "htmx/crispy_form.html"
def get_initial(self):
"""
Return the initial form instances to be used in the formset if pre-existing data is available.
Does not set the initial data for each new form.
"""
return []
def get_form_kwargs(self):
return {"initial": {}}
def get_factory_kwargs(self):
return {}
def get_formset_kwargs(self):
kwargs: dict[str, Any] = {
"form_kwargs": self.get_form_kwargs(),
}
if self.request.method in ("POST", "PUT"):
kwargs.update(
{
"data": self.request.POST,
"files": self.request.FILES,
}
)
return kwargs
def get_formset(self, data=None):
# Determine if the formset is modelformset or regular formset
if hasattr(self.form_class, "Meta") and hasattr(self.form_class.Meta, "model"):
factory = modelformset_factory(
self.form_class.Meta.model,
form=self.form_class,
**self.get_factory_kwargs(),
)
else:
factory = formset_factory(self.form_class, **self.get_factory_kwargs())
formset = factory(**self.get_formset_kwargs())
# This sets up the initial forms, not the (same) initial data for each (new) form
formset.initial = self.get_initial()
# Remove form tag if using crispy forms
for form in formset:
if getattr(form, "helper", None):
form.helper.form_tag = False
return formset
def get_context_data(self, **kwargs: Any):
context = {}
context["formset"] = self.get_formset()
return context
def formset_invalid(self):
return render(self.request, self.template_name, self.get_context_data())
def formset_valid(self):
response = HTMXResponse("Formset saved successfully", tag="success")
return response
def get(self, request, **kwargs):
self.request = request
self.kwargs = kwargs
return render(request, self.template_name, self.get_context_data())
def post(self, request, **kwargs):
self.request = request
self.kwargs = kwargs
formset = self.get_formset()
# If the "add extra form" button was pressed, add an extra form to the formset
if request.POST.get("add-extra-form", False):
return self._hx_add_extra_form(request, formset)
# formset = self.get_formset()
else:
formset.full_clean()
if formset.is_valid():
formset.save()
return self.formset_valid()
else:
return self.formset_invalid()
def _hx_add_extra_form(self, request, formset):
"""
Creates a new form and adds it to the formset.
Also updates the formset's total form count to reflect the addition.
Returns the updated formset to be replaced in the DOM.
"""
# Create a new form and add it to the formset
# omit the form tag if using crispy forms
form = formset.empty_form
if getattr(form, "helper", None):
form.helper.form_tag = False
# add prefix to the form
form.prefix = formset.add_prefix(formset.total_form_count())
management_form = ManagementForm(
auto_id=formset.auto_id,
prefix=formset.prefix,
initial={
"TOTAL_FORMS": formset.total_form_count() + 1,
"INITIAL_FORMS": formset.initial_form_count(),
"MIN_NUM_FORMS": formset.min_num,
"MAX_NUM_FORMS": formset.max_num,
},
renderer=formset.renderer,
)
response = render(
request,
self.template_name_form,
{
"form": form,
"formset_prefix": formset.prefix,
"management_form": management_form,
},
)
response["HX-Retarget"] = f"#{formset.prefix}-formset-forms"
response["HX-Reswap"] = "beforeend"
return response