Source code for wildewidgets.views.mixins

from __future__ import annotations

import base64
import json
from typing import TYPE_CHECKING, Any

from django.core.serializers.json import DjangoJSONEncoder
from django.http import HttpResponse
from django.utils.cache import add_never_cache_headers

try:
    from django.utils.encoding import force_text  # type: ignore[import]
except ImportError:
    from django.utils.encoding import force_str as force_text
from django.utils.functional import Promise

if TYPE_CHECKING:
    from django.http import HttpRequest

    from ..widgets import BreadcrumbBlock


class LazyEncoder(DjangoJSONEncoder):
    """
    Custom JSON encoder that handles Django's lazy i18n translation strings.

    This encoder extends Django's built-in JSON encoder to properly serialize
    lazy translation strings (Promise objects) by forcing them to text before
    encoding.

    Examples:
        .. code-block:: python

            data = {'message': _('Hello World')}
            json_data = json.dumps(data, cls=LazyEncoder)

    """

    def default(self, o: Any) -> Any:
        """
        Convert the given object to a JSON serializable type.

        Args:
            o: Object to be serialized

        Returns:
            A JSON serializable version of the object

        """
        if isinstance(o, Promise):
            return force_text(o)
        return super().default(o)


[docs]class WidgetInitKwargsMixin: """ Mixin that preserves widget initialization arguments for AJAX requests. This mixin stores widget initialization arguments and provides methods to encode them for transmission in URLs and decode them from HTTP requests. This allows widgets to preserve their state between initial rendering and subsequent AJAX calls. The mixin stores both positional and keyword arguments and can serialize them to base64-encoded JSON for inclusion in URLs or form data. The way it does it is as follows: - We make a dictionary - The positional arguments are stored in the "args" key. - The keyword arguments are stored in the "kwargs" key. - The resulting dictionary is then serialized to JSON and encoded as base64 for transmission. - The :py:meth:`get_encoded_extra_data` method returns the base64-encoded JSON string that can be included in URLs or form data. - The :py:meth:`get_decoded_extra_data(request)` method decodes the base64 string from the request and returns the original arguments as a dictionary. The encoded data can be used in AJAX requests as the ``extra_data`` query parameter to reconstruct the widget's state without needing to pass all arguments explicitly in the request. This is primarily used by :py:class:`wildewidgets.WildewidgetDispatch` to pass widget initialization arguments to tables and other widgets that require them for rendering or processing AJAX requests. Examples: .. code-block:: python from wildewidgets import WidgetInitKwargsMixin, Widget class MyWidget(WidgetInitKwargsMixin, Widget): def __init__(self, user_id, show_details=False, **kwargs): super().__init__(user_id, show_details=show_details, **kwargs) # The args and kwargs are now stored in self.extra_data """ def __init__(self, *args, **kwargs): """ Initialize the mixin and store initialization arguments. Args: *args: Positional arguments to preserve **kwargs: Keyword arguments to preserve """ super().__init__(*args, **kwargs) self.extra_data = {"args": args, "kwargs": kwargs}
[docs] def get_encoded_extra_data(self): """ Encode widget initialization arguments as base64 for URL transmission. This method serializes the stored arguments to JSON, encodes them as base64, and returns them as a string that can be included in URLs. Returns: str: Base64-encoded JSON string of the initialization arguments """ data_json = json.dumps(self.extra_data) payload_bytes = base64.b64encode(data_json.encode()) return payload_bytes.decode()
[docs] def get_decoded_extra_data(self, request): """ Decode widget initialization arguments from an HTTP request. This method extracts the "extra_data" parameter from the request's GET parameters, decodes it from base64, and parses it as JSON. Args: request: The HTTP request containing encoded widget arguments Returns: dict: Decoded initialization arguments (empty dict if none found) """ encoded_extra_data = request.GET.get("extra_data", None) if not encoded_extra_data: return {} extra_bytes = encoded_extra_data.encode() payload_bytes = base64.b64decode(extra_bytes) return json.loads(payload_bytes.decode())
[docs] def convert_extra(self, extra_item, first=True): """ Convert extra data to URL query string format. This method formats a dictionary as URL query parameters, either starting with "?" (if first=True) or "&" (if first=False). Args: extra_item: Dictionary of parameters to convert first: Whether this is the first set of parameters in a URL Returns: str: Formatted URL query string segment """ start = "?" if first else "&" if isinstance(extra_item, dict): extra_list = [] for k, v in extra_item.items(): extra_list.append(f"{k}={v}") return f"{start}{'&'.join(extra_list)}" return ""
[docs]class JSONResponseMixin: """ Mixin that provides JSON response handling for views. This mixin allows views to return JSON responses by converting the context data from get_context_data() into a JSON string and returning it with the appropriate content type. It also handles adding standard result indicators and removing non-serializable view objects. Attributes: Examples: .. code-block:: python from django.utils import timezone from django.views import View from wildewidgets import JSONResponseMixin def get_some_data(): return {"key": "value", "another_key": 42} class MyJSONView(JSONResponseMixin, View): def get_context_data(self, **kwargs): return { 'data': get_some_data(), 'timestamp': timezone.now().isoformat() } """ #: If True, return context data as-is without adding a #: "result" field. If False, add "result": "ok" or #: "result": "error" based on the presence of error indicators. is_clean: bool = False
[docs] def render_to_response(self, data: str) -> HttpResponse: """ Return a JSON response containing the provided data. Args: data: JSON string to include in the response Returns: HttpResponse: HTTP response with JSON content type """ return self.get_json_response(data)
[docs] def get_json_response(self, content: Any, **httpresponse_kwargs) -> HttpResponse: """ Create a properly configured HttpResponse with JSON content type. This method creates an HttpResponse with the provided content and sets the content type to "application/json". It also adds cache control headers to prevent caching of the response. Args: content: The content to include in the response **httpresponse_kwargs: Additional keyword arguments for HttpResponse Returns: HttpResponse: Configured HTTP response with JSON content """ response = HttpResponse( content, content_type="application/json", **httpresponse_kwargs ) add_never_cache_headers(response) return response
[docs] def post(self, *args, **kwargs) -> HttpResponse: """ Handle POST requests by delegating to the GET handler. This allows the same JSON response logic to be used for both GET and POST requests. Args: *args: Positional arguments to pass to get() **kwargs: Keyword arguments to pass to get() Returns: HttpResponse: JSON response from get() method """ return self.get(*args, **kwargs)
[docs] def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: # noqa: ARG002 """ Handle GET requests by returning a JSON response. This method: 1. Processes the request to extract the CSRF token 2. Gets context data from get_context_data() 3. Adds a "result" field if not in clean mode 4. Removes non-serializable view object 5. Serializes to JSON and returns the response Args: request: The HTTP request *args: Additional positional arguments **kwargs: Additional keyword arguments Returns: HttpResponse: HTTP response with JSON content Raises: AssertionError: If get_context_data() doesn't return a dictionary """ self.request: HttpRequest = request self.csrf_token: str | None = self.request.GET.get("csrf_token", None) response: dict[str, Any] | None = None func_val = self.get_context_data(**kwargs) # type: ignore[attr-defined] assert isinstance(func_val, dict), "get_context_data must return a dict" # noqa: S101 if not self.is_clean: response = dict(func_val) if "error" not in response and "sError" not in response: response["result"] = "ok" else: response["result"] = "error" else: response = func_val # can't have 'view' here, because the view object can't be jsonified response.pop("view", None) dump = json.dumps(response, cls=LazyEncoder) return self.render_to_response(dump)
[docs]class StandardWidgetMixin: """ A mixin for views that use a standard widget-based template structure. This mixin provides a convention for template-less views that render their content using widgets. It automatically adds the content widget and optional breadcrumbs widget to the template context. To use this mixin: 1. Override :py:meth:`get_content` to return the main content widget 2. Optionally override :py:meth:`get_breadcrumbs` to provide breadcrumb navigation 3. Ensure your template includes the necessary blocks and loads the wildewidgets template tags The template used by your derived class should include at least the following:: {% extends "<your_base_template>.html" %} {% load wildewidgets %} {% block title %}{{page_title}}{% endblock %} {% block breadcrumb-items %} {% if breadcrumbs %} {% wildewidgets breadcrumbs %} {% endif %} {% endblock %} {% block content %} {% wildewidgets content %} {% endblock %} Examples: Basic view using the StandardWidgetMixin: .. code-block:: python from django.views.generic import TemplateView from wildewidgets.views.mixins import StandardWidgetMixin from wildewidgets import Block, BreadcrumbBlock class HomeContentWidget(Block): block = "home-content" tag = "h1" def __init__(self, user): super().__init__(f"Welcome, {user.username}!") class AppBreadcrumbs(BreadcrumbBlock): def __init__(self): super().__init__() self.add_breadcrumb('Home', url='/') class HomeView(StandardWidgetMixin, TemplateView): template_name = 'myapp/standard.html' def get_content(self): return HomeContentWidget(user=self.request.user) def get_breadcrumbs(self): breadcrumbs = AppBreadcrumbs() breadcrumbs.add_breadcrumb('Home') return breadcrumbs """
[docs] def get_context_data(self, **kwargs: Any) -> dict[str, Any]: """ Add content widget and breadcrumbs to the template context. This method: 1. Gets content widget from get_content() 2. Gets optional breadcrumbs widget from get_breadcrumbs() 3. If breadcrumbs exist, adds them and sets page_title 4. Delegates to parent class for additional context Args: **kwargs: Additional context variables Returns: dict: The updated template context """ kwargs["content"] = self.get_content() breadcrumbs: BreadcrumbBlock | None = self.get_breadcrumbs() if breadcrumbs: kwargs["breadcrumbs"] = breadcrumbs kwargs["page_title"] = breadcrumbs.flatten() return super().get_context_data(**kwargs) # type: ignore[misc]
[docs] def get_content(self): """ Get the main content widget for the page. This method must be overridden by subclasses to provide the widget that will be rendered in the content block. Returns: Widget: The main content widget Raises: NotImplementedError: If not overridden by subclass """ msg = f"You must override get_content in {self.__class__.__name__}" raise NotImplementedError(msg)
[docs] def get_breadcrumbs(self) -> BreadcrumbBlock | None: """ Get the breadcrumbs widget for the page. Override this method to provide breadcrumb navigation. By default, no breadcrumbs are shown. Returns: BreadcrumbBlock | None: Breadcrumb widget or None if no breadcrumbs """ return None