from typing import Any, LiteralString, Protocol, Unpack from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.http import HttpRequest from django.template.loader import render_to_string from django.utils.safestring import SafeString from django.views import View from django.views.generic.base import ContextMixin, TemplateResponseMixin class TabedViewMixin(View): """Basic functions for displaying tabs in the template.""" def get_tabs_title(self): if hasattr(self, "tabs_title"): return self.tabs_title raise ImproperlyConfigured("tabs_title is required") def get_current_tab(self): if hasattr(self, "current_tab"): return self.current_tab raise ImproperlyConfigured("current_tab is required") def get_list_of_tabs(self): if hasattr(self, "list_of_tabs"): return self.list_of_tabs raise ImproperlyConfigured("list_of_tabs is required") def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) kwargs["tabs_title"] = self.get_tabs_title() kwargs["current_tab"] = self.get_current_tab() kwargs["list_of_tabs"] = self.get_list_of_tabs() return kwargs class QuickNotifMixin: quick_notif_list = [] def dispatch(self, request, *arg, **kwargs): # In some cases, the class can stay instanciated, so we need to reset the list self.quick_notif_list = [] return super().dispatch(request, *arg, **kwargs) def get_success_url(self): ret = super().get_success_url() if hasattr(self, "quick_notif_url_arg"): if "?" in ret: ret += "&" + self.quick_notif_url_arg else: ret += "?" + self.quick_notif_url_arg return ret def get_context_data(self, **kwargs): """Add quick notifications to context.""" kwargs = super().get_context_data(**kwargs) kwargs["quick_notifs"] = [] for n in self.quick_notif_list: kwargs["quick_notifs"].append(settings.SITH_QUICK_NOTIF[n]) for key, val in settings.SITH_QUICK_NOTIF.items(): for gk in self.request.GET: if key == gk: kwargs["quick_notifs"].append(val) return kwargs class AllowFragment: """Add `is_fragment` to templates. It's only True if the request is emitted by htmx""" def get_context_data(self, **kwargs): kwargs["is_fragment"] = self.request.headers.get("HX-Request", False) return super().get_context_data(**kwargs) class FragmentRenderer(Protocol): def __call__( self, request: HttpRequest, **kwargs: Unpack[dict[str, Any]] ) -> SafeString: ... class FragmentMixin(TemplateResponseMixin, ContextMixin): """Make a view buildable as a fragment that can be embedded in a template. Most fragments are used in two different ways : - in the request/response cycle, like any regular view - in templates, where the rendering is done in another view This mixin aims to simplify the initial fragment rendering. The rendered fragment will then be able to re-render itself through the request/response cycle if it uses HTMX. !!!Example ```python class MyFragment(FragmentMixin, FormView): template_name = "app/fragment.jinja" form_class = MyForm success_url = reverse_lazy("foo:bar") # in another view : def some_view(request): fragment = MyFragment.as_fragment() return render( request, "app/template.jinja", context={"fragment": fragment(request) } # in urls.py urlpatterns = [ path("foo/view", some_view), path("foo/fragment", MyFragment.as_view()), ] ``` """ @classmethod def as_fragment(cls, **initkwargs) -> FragmentRenderer: # the following code is heavily inspired from the base View.as_view method for key in initkwargs: if not hasattr(cls, key): raise TypeError( "%s() received an invalid keyword %r. as_view " "only accepts arguments that are already " "attributes of the class." % (cls.__name__, key) ) def fragment(request: HttpRequest, **kwargs) -> SafeString: self = cls(**initkwargs) self.request = request self.kwargs = kwargs return self.render_fragment(request, **kwargs) fragment.__doc__ = cls.__doc__ fragment.__module__ = cls.__module__ return fragment def render_fragment(self, request, **kwargs) -> SafeString: return render_to_string( self.get_template_names(), context=self.get_context_data(**kwargs), request=request, ) class UseFragmentsMixin(ContextMixin): """Mark a view as using fragments. This mixin is not mandatory (you may as well render manually your fragments in the `get_context_data` method). However, the interface of this class bring some distinction between fragments and other context data, which may reduce boilerplate. !!!Example ```python class FooFragment(FragmentMixin, FormView): ... class BarFragment(FragmentMixin, FormView): ... class AdminFragment(FragmentMixin, FormView): ... class MyView(UseFragmentsMixin, TemplateView) template_name = "app/view.jinja" fragments = { "foo": FooFragment "bar": BarFragment(template_name="some_template.jinja") } fragments_data = { "foo": {"some": "data"} # this will be passed to the FooFragment renderer } def get_fragments(self): res = super().get_fragments() if self.request.user.is_superuser: res["admin_fragment"] = AdminFragment return res ``` """ fragments: dict[LiteralString, type[FragmentMixin] | FragmentRenderer] | None = None fragment_data: dict[LiteralString, dict[LiteralString, Any]] | None = None def get_fragments(self) -> dict[str, type[FragmentMixin] | FragmentRenderer]: return self.fragments if self.fragments is not None else {} def get_fragment_data(self) -> dict[str, dict[str, Any]]: """Return eventual data used to initialize the fragments.""" return self.fragment_data if self.fragment_data is not None else {} def get_fragment_context_data(self) -> dict[str, SafeString]: """Return the rendered fragments as context data.""" res = {} data = self.get_fragment_data() for name, fragment in self.get_fragments().items(): _fragment = ( fragment.as_fragment() if fragment is FragmentMixin else fragment ) fragment_data = data.get(name, {}) res[name] = _fragment(self.request, **fragment_data) return res def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) kwargs.update(self.get_fragment_context_data()) return kwargs