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 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)