This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import json
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin, UserPassesTestMixin
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
|
||||
DAISYUI_WIDGET_CLASSES = (
|
||||
(forms.CheckboxInput, 'checkbox'),
|
||||
(forms.CheckboxSelectMultiple, 'checkbox'),
|
||||
(forms.ClearableFileInput, 'file-input file-input-bordered w-full'),
|
||||
(forms.Textarea, 'textarea textarea-bordered w-full'),
|
||||
(forms.Select, 'select select-bordered w-full'),
|
||||
)
|
||||
DAISYUI_DEFAULT_WIDGET_CLASS = 'input input-bordered w-full'
|
||||
|
||||
|
||||
class BackofficeSectionMixin:
|
||||
section = None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['section'] = self.section
|
||||
return context
|
||||
|
||||
|
||||
class BackofficeAccessMixin(BackofficeSectionMixin, LoginRequiredMixin, UserPassesTestMixin):
|
||||
login_url = 'users:login'
|
||||
|
||||
def test_func(self):
|
||||
return self.request.user.is_staff
|
||||
|
||||
|
||||
class BackofficeCRUDMixin(BackofficeAccessMixin, PermissionRequiredMixin):
|
||||
raise_exception = True
|
||||
|
||||
|
||||
class BackofficeStyledFormMixin:
|
||||
"""Aplica clases de DaisyUI a los widgets del formulario según su tipo,
|
||||
sin necesitar declarar un ModelForm explícito por modelo (a diferencia de
|
||||
web.mixins.StylingMixin, que requiere listar `styled_fields` a mano)."""
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
|
||||
for field in form.fields.values():
|
||||
css_class = DAISYUI_DEFAULT_WIDGET_CLASS
|
||||
|
||||
for widget_type, widget_css_class in DAISYUI_WIDGET_CLASSES:
|
||||
if isinstance(field.widget, widget_type):
|
||||
css_class = widget_css_class
|
||||
break
|
||||
|
||||
existing = field.widget.attrs.get('class', '')
|
||||
field.widget.attrs['class'] = f'{existing} {css_class}'.strip()
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class BackofficeHtmxMixin:
|
||||
"""Toda vista de backoffice puede responder con la página completa (navegación
|
||||
directa) o con un fragmento (carga en el modal, o autorefresco de una lista/detalle)."""
|
||||
|
||||
fragment_template_name = None
|
||||
|
||||
def is_htmx(self):
|
||||
return self.request.headers.get('HX-Request') == 'true'
|
||||
|
||||
def get_template_names(self):
|
||||
if self.is_htmx() and self.fragment_template_name:
|
||||
return [self.fragment_template_name]
|
||||
return super().get_template_names()
|
||||
|
||||
|
||||
class BackofficeModalFormMixin(BackofficeStyledFormMixin, BackofficeHtmxMixin):
|
||||
"""Create/Update pensadas para abrirse en modal. Al guardar con éxito vía htmx
|
||||
no redirige (un <dialog> no navega): responde 204 + HX-Trigger para cerrar el
|
||||
modal y avisar a quien lo abrió (lista o detalle) de que se refresque."""
|
||||
|
||||
trigger_event = 'backoffice:list-changed'
|
||||
|
||||
def form_valid(self, form):
|
||||
self.object = form.save()
|
||||
|
||||
if self.is_htmx():
|
||||
response = HttpResponse(status=204)
|
||||
response['HX-Trigger'] = json.dumps({'backoffice:modal-close': True, self.trigger_event: True})
|
||||
return response
|
||||
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
|
||||
class BackofficeModalDeleteMixin(BackofficeHtmxMixin):
|
||||
trigger_event = 'backoffice:list-changed'
|
||||
|
||||
def perform_delete(self):
|
||||
self.object.delete()
|
||||
|
||||
def form_valid(self, form):
|
||||
success_url = self.get_success_url()
|
||||
self.perform_delete()
|
||||
|
||||
if self.is_htmx():
|
||||
response = HttpResponse(status=204)
|
||||
response['HX-Trigger'] = json.dumps({'backoffice:modal-close': True, self.trigger_event: True})
|
||||
return response
|
||||
|
||||
return HttpResponseRedirect(success_url)
|
||||
Reference in New Issue
Block a user