Files
Sith/core/views/mixins.py
2025-09-25 16:02:56 +02:00

198 lines
7.2 KiB
Python

import copy
import inspect
from typing import Any, ClassVar, LiteralString, Protocol, Unpack
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest, HttpResponse
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."""
current_tab: ClassVar[str | None] = None
list_of_tabs: ClassVar[list | None] = None
tabs_title: ClassVar[str | None] = None
def get_tabs_title(self):
if not self.tabs_title:
raise ImproperlyConfigured("tabs_title is required")
return self.tabs_title
def get_current_tab(self):
if not self.current_tab:
raise ImproperlyConfigured("current_tab is required")
return self.current_tab
def get_list_of_tabs(self):
if not self.list_of_tabs:
raise ImproperlyConfigured("list_of_tabs is required")
return self.list_of_tabs
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 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()),
]
```
"""
reload_on_redirect: bool = False
"""If True, this fragment will trigger a full page reload on redirect."""
@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)
# any POST action on the fragment will be dealt by the fragment itself.
# So, if the view that is rendering this fragment is in a POST context,
# let's pretend anyway it's a GET, in order to be sure the fragment
# won't try to do any POST action (like form validation) on initial render.
self.request = copy.copy(request)
self.request.method = "GET"
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,
)
def dispatch(self, *args, **kwargs):
res: HttpResponse = super().dispatch(*args, **kwargs)
if 300 <= res.status_code < 400 and self.reload_on_redirect:
# HTMX doesn't care about redirection codes (because why not),
# so we must transform the redirection code into a 200.
res.status_code = 200
res.headers["HX-Redirect"] = res["Location"]
return res
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():
is_cls = inspect.isclass(fragment) and issubclass(fragment, FragmentMixin)
_fragment = fragment.as_fragment() if is_cls 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