108 lines
3.6 KiB
Python
108 lines
3.6 KiB
Python
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)
|