# -*- coding: utf-8 -*-
"""View support classes and functions for htmx-views."""
# Python imports
import logging
import re
from contextlib import contextmanager
# Django imports
from django.conf import settings
from django.views import View
logger = logging.getLogger(__name__)
[docs]
@contextmanager
def temp_attr(obj, attr, value):
"""Temporarily set the value of an attribute and restore it afterwards."""
# Check if the attribute originally exists on the object
has_attr = hasattr(obj, attr)
original_value = getattr(obj, attr, None)
# Set the attribute to the new value
setattr(obj, attr, value)
try:
# Yield control back to the caller
yield
finally:
if has_attr:
# Restore the original value if the attribute existed
setattr(obj, attr, original_value)
else:
# Delete the attribute if it didn't exist originally
delattr(obj, attr)
[docs]
def dispatch(self, request, *args, **kwargs):
"""Dispatch method that becomes htmx aware.
If the =`htmx` request attribute is set (by django-htmx) and the method name is in either
`self.http_method_names` or `self.htmx_method_names` then try to locate the method
`htmx_<http_verb>` method and call that or else fall back to the regular `<http_verb>` method.
If an approrpiate method can't be located, call `self.http_method_bot_allowed` for error handline.
If the `htmx` request attrobute is not set or is False, then fall back to the original dispatch.
"""
if not getattr(request, "htmx", False): # Not an HTMX aware request
return self._non_htmx_dispatch(request, *args, **kwargs)
# Allow different allowed methods for htmx
allowed_names = getattr(self, "htmx_http_method_names", self.http_method_names)
if request.method.lower() in allowed_names:
handler = getattr(
self, f"htmx_{request.method.lower()}", getattr(self, request.method.lower(), self.http_method_not_allowed)
)
else:
handler = self.http_method_not_allowed
if not callable(handler):
handler = self.http_method_not_allowed
return handler(request, *args, **kwargs)
[docs]
class HTMXProcessMixin:
"""Provide versions of the htmx_`<http_verb>` methods that will delegate to trigger specific methods.
Each http verb DELETE,GET,PATCH,POST,PUT's htmx_<verb> method will look at the request.htmx attriobute
to see if htmx_<verb>_<trigger_name>, htmx_<verb>_<trigger>, htmx_<verb>_<target> is a method and then pass
on to the first matching method. If no match is foumd, the `http_method_not_allowed` method is used instead.
"""
def __init__(self, *args, **kwargs):
"""Initialise HTMX process mixin with context flags.
Keyword Parameters:
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
"""
super().__init__(*args, **kwargs)
self._htmx_get_context_data = False
self._htmx_get_context_object_name = False
self._htmx_get_template_names = False
[docs]
def htmx_elements(self):
"""Iterate over possible htmx element sources."""
for attr in ["trigger_name", "trigger", "target"]:
if elem := getattr(self.request.htmx, attr, None):
elem = re.sub(r"[^A-Za-z0-9_]", "", elem).lower()
if settings.DEBUG:
logger.debug(elem)
yield elem
[docs]
def get_context_data_function(self, **kwargs):
"""Return the first callable element-specific context handler."""
del kwargs
for elem in self.htmx_elements():
handler = getattr(self, f"get_context_data_{elem}", None)
if callable(handler):
return handler
return None
[docs]
def get_context_data(self, **kwargs):
"""Get context data being aware of htmx views."""
if not getattr(self.request, "htmx", False) or self._htmx_get_context_data: # Default behaviour
return super().get_context_data(**kwargs)
# Look for a request specific to the element involved.
handler = self.get_context_data_function(**kwargs)
if handler is not None:
with temp_attr(self, "_htmx_get_context_data", True):
return handler(**kwargs)
return super().get_context_data(**kwargs)
[docs]
def get_context_object_name(self, object_list):
"""Get context object name being aware of htmx elements.
If the get_context_name_<element> method needs to call usper, it should set a keyword
argument, _default to be True to avoid a recursive loop.
"""
if not getattr(self.request, "htmx", False) or self._htmx_get_context_object_name: # Default behaviour
return super().get_context_object_name(object_list)
# Look for a request specific to the element involved.
for elem in self.htmx_elements():
for handler_name in (f"get_context_object_name_{elem}", f"get_context_object_name{elem}"):
if callable(handler := getattr(self, handler_name, None)):
with temp_attr(self, "_htmx_get_context_object_name", True):
return handler(object_list)
if sub_name := getattr(self, f"context_object_{elem}", False):
return sub_name
logger.debug("Super")
return super().get_context_object_name(object_list)
[docs]
def get_template_names(self):
"""Look for htmx specific templates."""
if not getattr(self.request, "htmx", False) or self._htmx_get_template_names: # Default behaviour
return super().get_template_names()
# Look for a request specific to the element involved.
for elem in self.htmx_elements():
handler = getattr(self, f"get_template_names_{elem}", None)
if callable(handler):
if settings.DEBUG:
logger.debug(f"Template_handler: {handler.__name__}")
with temp_attr(self, "_htmx_get_template_names", True):
return handler()
sub_name = getattr(self, f"template_name_{elem}", False)
if sub_name:
if settings.DEBUG:
logger.debug(f"Template_name: {sub_name}")
return sub_name
return super().get_template_names()
[docs]
def htmx_delete(self, request, *args, **kwargs):
"""Delegate HTMX DELETE requests.
Looks for the element that is related to the request by inspecting the `request.htmx` `trigger_name`, trigger
and target attributes in turn. If no matching `htmx_delete_<name>` methods are found, return the
`method_not_allowed` result instrad.
"""
for elem in self.htmx_elements():
handler = getattr(self, f"htmx_delete_{elem}", None)
if callable(handler):
break
else:
handler = getattr(self, "delete", self.http_method_not_allowed)
if not callable(handler):
handler = self.http_method_not_allowed
if settings.DEBUG:
logger.debug(f"HTMX Method handler: {handler.__name__}")
return handler(request, *args, **kwargs)
[docs]
def htmx_get(self, request, *args, **kwargs):
"""Delegate HTMX GET requests.
Looks for the element that is related to the request by inspecting the `request.htmx` `trigger_name`, trigger
and target attributes in turn. If no matching `htmx_get_<name>` methods are found, return the
`method_not_allowed` result instrad.
"""
for elem in self.htmx_elements():
handler = getattr(self, f"htmx_get_{elem}", None)
if callable(handler):
break
else:
handler = getattr(self, "get", self.http_method_not_allowed)
if not callable(handler):
handler = self.http_method_not_allowed
if settings.DEBUG:
logger.debug(f"HTMX Method handler: {handler.__name__}")
return handler(request, *args, **kwargs)
[docs]
def htmx_patch(self, request, *args, **kwargs):
"""Delegate HTMX PATCH requests.
Looks for the element that is related to the request by inspecting the `request.htmx` `trigger_name`, trigger
and target attributes in turn. If no matching `htmx_patch_<name>` methods are found, return the
`method_not_allowed` result instrad.
"""
for elem in self.htmx_elements():
handler = getattr(self, f"htmx_patch_{elem}", None)
if callable(handler):
break
else:
handler = getattr(self, "patch", self.http_method_not_allowed)
if not callable(handler):
handler = self.http_method_not_allowed
if settings.DEBUG:
logger.debug(f"HTMX Method handler: {handler.__name__}")
return handler(request, *args, **kwargs)
[docs]
def htmx_post(self, request, *args, **kwargs):
"""Delegate HTMX POST requests.
Looks for the element that is related to the request by inspecting the `request.htmx` `trigger_name`, trigger
and target attributes in turn. If no matching `htmx_post_<name>` methods are found, return the
`method_not_allowed` result instrad.
"""
for elem in self.htmx_elements():
handler = getattr(self, f"htmx_post_{elem}", None)
if callable(handler):
break
else:
handler = getattr(self, "post", self.http_method_not_allowed)
if not callable(handler):
handler = self.http_method_not_allowed
if settings.DEBUG:
logger.debug(f"HTMX Method handler: {handler.__name__}")
return handler(request, *args, **kwargs)
[docs]
def htmx_put(self, request, *args, **kwargs):
"""Delegate HTMX PUT requests.
Looks for the element that is related to the request by inspecting the `request.htmx` `trigger_name`, trigger
and target attributes in turn. If no matching `htmx_put_<name>` methods are found, return the
`method_not_allowed` result instrad.
"""
for elem in self.htmx_elements():
handler = getattr(self, f"htmx_put_{elem}", None)
if callable(handler):
break
else:
handler = getattr(self, "put", self.http_method_not_allowed)
if not callable(handler):
handler = self.http_method_not_allowed
if settings.DEBUG:
logger.debug(f"HTMX Method handler: {handler.__name__}")
return handler(request, *args, **kwargs)
def __getattr__(name):
"""Lazily preserve the former linked-select view import path."""
if name == "LinkedSelectEndpointView":
# app imports
from .linked_selects import LinkedSelectEndpointView
return LinkedSelectEndpointView
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
def _install_htmx_dispatch(view_class=View):
"""Install the HTMX-aware dispatch function idempotently."""
if not hasattr(view_class, "_non_htmx_dispatch"):
setattr(view_class, "_non_htmx_dispatch", view_class.dispatch)
setattr(view_class, "dispatch", dispatch)
_install_htmx_dispatch()