Source code for wildewidgets.widgets.tables.actions

from __future__ import annotations

from copy import copy, deepcopy
from typing import TYPE_CHECKING, Any, Literal, cast

from django.core.exceptions import ImproperlyConfigured
from django.urls import reverse

from ..base import Block
from ..buttons import InputButton
from ..forms import HiddenInputBlock
from ..structure import HorizontalLayoutBlock

if TYPE_CHECKING:
    from collections.abc import Callable

    from django.contrib.auth.models import AbstractUser


# -------------------------------
# Buttons
# -------------------------------


[docs]class RowActionButton(Block): """ Base class for action buttons displayed in table rows. This class provides the foundation for creating buttons that appear in the "Actions" column of tables. Each button is bound to a specific row and can perform actions based on that row's data. Note that all keyword arguments/class attributes from :py:class:`wildewidgets.Block` are also accepted. You can use this class directly with keyword arguments to create buttons that link to row-specific URLs. In order for this to work, you'll need to pass a callable into `url` that accepts a single argument, which will be the row data. This callable should return a string that is the URL for the button. You will more usually subclass this, setting class attributes to create specific model-based buttons, and overriding the :py:meth:`get_url` method to return the URL for the button based on the row data. Attributes: tag: HTML tag for the button, defaults to "a" for links text: The text displayed on the button url: The URL the button links to, a Django URL name to reverse, or a callable that accepts a ``row`` arg and returns a URL string color: Bootstrap color class for the button (e.g., "primary", "secondary") size: Bootstrap size class for the button (e.g., "sm", "lg") permission: Optional Django permission string required to see the button row: Reference to the row data this button is bound to (set by bind method) Example: Direct usage with keyword arguments: .. code-block:: python from django.urls import reverse from wildewidgets import RowActionButton def get_home_url(row: Any) -> str: return reverse("app:home", kwargs={"id": row.id}) button = RowActionButton( text="Home", url=get_home_url, color="primary", permission="app.view_item" ) Subclassing for specific actions: .. code-block:: python class RowViewButton(RowActionButton): text = "View" color = "info" size = "sm" url = "core:item-view" def get_url(self, row: Any) -> str: return reverse(self.url, kwargs={"id": row.id}) Keyword Args: text: The text to display on the button url: The URL the button should link to color: The Bootstrap color class for the button (default: "secondary") size: The Bootstrap size class for the button (default: None) permission: Optional Django permission string required to see this button, e.g. "app.view_item" **kwargs: Additional attributes passed to the parent Block class Raises: ImproperlyConfigured: If 'text' or 'url' is not provided """ tag: str = "a" text: str | None = None url: str | Callable[[Any], str] | None = None color: str = "secondary" size: str | None = None permission: str | None = None def __init__( self, text: str | None = None, url: str | Callable[[Any], str] | None = None, color: str | None = None, size: str | None = None, permission: str | None = None, **kwargs, ): self.text = text or self.text self.url = url or self.url self.color = color or self.color self.size = size or self.size self.permission = permission or self.permission super().__init__(cast("str", self.text), **kwargs) #: The table will set this self.row: Any = None
[docs] def is_visible(self, row: Any, user: AbstractUser | None) -> bool: # type: ignore[override] # noqa: ARG002 """ Determine if this button should be visible to the given user. Args: row: The row data this button is associated with user: The current Django user Returns: bool: True if the button should be visible, False otherwise Note: If a permission is specified, the user must have that permission for the button to be visible. """ if self.permission is not None and user is not None: return bool(user.has_perm(self.permission)) return True
[docs] def get_url(self, row: Any) -> str: """ Get the URL for this button, potentially based on row data. This base implementation simply returns the static URL property. Subclasses can override this to generate dynamic URLs based on row data. Args: row: The row data to use for URL generation Returns: str: The URL for the button """ if callable(self.url): # If the URL is a callable, call it with the row data return self.url(row) return self.url # type: ignore[return-value]
[docs] def bind( self, row: Any, table: ActionButtonBlockMixin, # noqa: ARG002 size: str | None = None, ) -> RowActionButton: """ Bind this button to a specific row and return a copy configured for that row. This method creates a copy of the button configured specifically for the given row, including setting appropriate URLs, colors and sizes. Args: row: The row data to bind this button to table: The table instance that contains this row size: Optional size override for the button Returns: RowActionButton: A new button instance bound to the specified row Note: The table parameter provides access to table-level properties like CSRF tokens needed for form submissions. """ action = deepcopy(self) action.row = row action.add_class(f"btn-{self.color}") action._attributes["href"] = self.get_url(row) if not size: size = self.size if self.size: action.add_class(f"btn-{self.size}") return action
[docs]class RowLinkButton(RowActionButton): """ A row action button that renders as a link with appropriate attributes. This subclass of :py:class:`RowActionButton` ensures the rendered button has the appropriate attributes for accessibility and behavior as a link. Either subclass this, setting class attributes to create specific model-based buttons, or use it directly with keyword arguments to create buttons that link to model-specific URLs. Note that all keyword arguments/class attributes from :py:class:`wildewidgets.Block` and :py:class:`RowActionButton` are also accepted. Note: If ``row`` is going to be a Django model instance, you're better off using :py:class:`RowModelUrlButton` or :py:class:`RowDjangoUrlButton` instead. Honestly, the only difference between this and :py:class:`RowActionButton` is that we set the ``role`` attribute on the `<a>` tag to "button". Attributes: Inherits all attributes from :py:class:`RowActionButton`. Example: Direct usage with keyword arguments: .. code-block:: python from django.urls import reverse from wildewidgets import RowLinkButton def get_details_url(row: Any) -> str: return reverse("app:item-details", kwargs={"id": row.id}) button = RowLinkButton( text="Details", url=get_details_url, color="info" ) Subclassing for specific actions: .. code-block:: python from django.urls import reverse_lazy from wildewidgets import RowLinkButton class RowDetailsButton(RowLinkButton): text = "Details" color = "info" def get_url(self, row: Any) -> str: return reverse("app:item-details", kwargs={"id": row.id}) """
[docs] def get_context_data(self, *args, **kwargs) -> dict[str, Any]: """ Prepare the context data for template rendering. Sets appropriate attributes for accessibility including href and role. Args: *args: Positional arguments passed to the parent method **kwargs: Keyword arguments passed to the parent method Returns: dict: The context dictionary with updated attributes """ self._attributes["href"] = self.get_url(self.row) self._attributes["role"] = "button" return super().get_context_data(*args, **kwargs)
[docs]class RowModelUrlButton(RowActionButton): """ A row action button that gets its URL from a model instance attribute or method. This button uses a specified attribute or method of the row's model instance to determine its URL, making it ideal for standard model actions like view or edit. Either subclass this, setting class attributes to create specific model-based buttons, or use it directly with keyword arguments to create buttons that link to model-specific URLs. Note that all keyword arguments/class attributes from :py:class:`wildewidgets.Block` and :py:class:`RowActionButton` are also accepted. Attributes: block: CSS class for styling ("btn") modifier: CSS modifier class ("auto") attribute: The model attribute or method to use for URL generation. Your model must implement this method or attribute to return the URL. text: Default button text ("View") Example: Direct usage with keyword arguments: .. code-block:: python from wildewidgets import RowModelUrlButton # Uses row.get_absolute_url() to determine URL button = RowModelUrlButton(text="View Details") # Uses row.get_edit_url() to determine URL button = RowModelUrlButton( text="Edit", attribute="get_edit_url", color="primary" ) Subclassing for specific actions: .. code-block:: python class RowViewButton(RowModelUrlButton): attribute = "get_absolute_url" text = "View" color = "info" size = "sm" Keyword Args: attribute: The model attribute or method to use for URL generation. Your model must implement this method or attribute to return the URL. **kwargs: Additional keyword arguments for the *:py:class:`RowActionButton` class """ block: str = "btn" modifier: str = "auto" attribute: str = "get_absolute_url" text: str | None = "View" def __init__(self, attribute: str | None = None, **kwargs): self.attribute = attribute or self.attribute super().__init__(**kwargs)
[docs] def get_url(self, row: Any) -> str: """ Get the URL from the specified model attribute or method. Args: row: The model instance to get the URL from Returns: str: The URL for the button Raises: ValueError: If the specified attribute doesn't exist on the model """ if not hasattr(row, self.attribute): msg = f'{row} has no "{self.attribute}" attribute or method' raise ValueError(msg) attr = getattr(row, self.attribute) if callable(attr): return attr() return attr
[docs]class RowDjangoUrlButton(RowActionButton): """ A row action button that constructs its URL using Django's URL resolver. This button uses Django's URL resolution system with either positional or keyword arguments to construct its URL dynamically based on row data. Either subclass this, setting class attributes to create specific model-based buttons, or use it directly with keyword arguments to create buttons that link to model-specific URLs. Note that all keyword arguments/class attributes from :py:class:`wildewidgets.Block` and :py:class:`RowActionButton` are also accepted. Attributes: block: CSS class for styling ("btn") url_path: The Django URL pattern name to reverse, e.g., "app:item-edit" url_args: List of row attribute names to use as positional arguments url_kwargs: Dictionary mapping URL param names to row attribute names Example: Direct usage with keyword arguments: .. code-block:: python from wildewidgets import RowDjangoUrlButton # Using positional arguments (e.g., 'item/1/') button = RowDjangoUrlButton( text="Edit", url_path="app:item-edit", url_args=["id"] ) # Using keyword arguments (e.g., 'item/edit/?pk=1') button = RowDjangoUrlButton( text="Edit", url_path="app:item-edit", url_kwargs={"pk": "id"} ) Subclassing for specific actions: .. code-block:: python class RowEditButton(RowDjangoUrlButton): url_path = "app:item-edit" text = "Edit" color = "primary" url_args = ["id"] Note: You must specify either ``url_args`` or ``url_kwargs``, but not both. Keyword Args: url_path: The Django URL pattern name to reverse url_args: List of row attribute names to use as positional arguments url_kwargs: Dictionary mapping URL param names to row attribute names Raises: ImproperlyConfigured: If both ``url_args`` and ``url_kwargs`` are provided, or if url_path is not set RowActionButton.RequiredAttrOrKwarg: If url_path is not set """ block: str = "btn" url_path: str | None = None url_args: list[str] = [] # noqa: RUF012 url_kwargs: dict[str, str] = {} # noqa: RUF012 def __init__( self, url_path: str | None = None, url_args: list[str] | None = None, url_kwargs: dict[str, str] | None = None, **kwargs, ): self.url_path = url_path or self.url_path self.url_args = url_args or copy(self.url_args) self.url_kwargs = url_kwargs or copy(self.url_kwargs) if not self.url_path: msg = "url_path" raise self.RequiredAttrOrKwarg(msg) if self.url_args and self.url_kwargs: msg = ( 'Either "url_args" or "url_kwargs" must be provided as keyword ' "arguments or class attributes, but not both" ) raise ImproperlyConfigured(msg) super().__init__(**kwargs)
[docs] def get_url(self, row: Any) -> str: """ Construct a URL using Django's URL resolver with row data. Args: row: The row data to use for URL parameters Returns: str: The resolved URL for the button """ if not (self.url_args and self.url_kwargs): url = reverse(self.url_path) if self.url_args: args = [getattr(row, arg) for arg in self.url_args] url = reverse(self.url_path, args=args) if self.url_kwargs: kwargs = {kwarg: getattr(self.row, attr) for kwarg, attr in self.url_kwargs} url = reverse(self.url_path, kwargs=kwargs) return url
[docs]class RowFormButton(RowModelUrlButton): """ A row action button that renders as a form, submitting via POST. This button renders as an HTML form that submits via POST when clicked. It's ideal for actions that modify data, like delete or status change operations. You do not need to provide a form -- it will be created automatically. Either subclass this, setting class attributes to create specific model-based buttons, or use it directly with keyword arguments to create buttons that link to model-specific URLs. Note that all keyword arguments/class attributes from :py:class:`wildewidgets.Block` and :py:class:`RowModelUrlButton` are also accepted. Attributes: block: CSS class for styling ("action-form") tag: HTML tag for the container ("form") form_fields: List of row attribute names to include as hidden fields confirm_text: Confirmation message to show before submission Example: Direct usage with keyword arguments: .. code-block:: python from wildewidgets import RowFormButton button = RowFormButton( text="Delete", attribute="get_delete_url", form_fields=["id"], confirm_text="Are you sure you want to delete this item?" ) Subclassing for specific actions: .. code-block:: python class RowDeleteButton(RowFormButton): attribute = "get_delete_url" text = "Delete" color = "outline-secondary" form_fields = ["id"] confirm_text = "Are you sure you want to delete this item?" Note: - Requires both form_fields and confirm_text to be specified - Will include CSRF token automatically - The form action URL is determined by the :py:attr:`attribute` method or attribute, which must be present on the model class. If you need more sophisticated URL handling, subclass and override the :py:meth:`get_url` method. Keyword Args: form_fields: List of row attribute names to include as hidden fields confirm_text: Confirmation message to show before submission Raises: ImproperlyConfigured: If form_fields or confirm_text are not set """ block: str = "action-form" tag: str = "form" form_fields: list[str] = [] # noqa: RUF012 confirm_text: str | None = None def __init__( self, form_fields: list[str] | None = None, confirm_text: str | None = None, **kwargs, ): self.form_fields = form_fields or deepcopy(self.form_fields) self.confirm_text = confirm_text or self.confirm_text if not self.confirm_text: msg = "RowFormButton requires a 'confirm_text' argument or class attribute" raise ImproperlyConfigured(msg) if not self.form_fields: msg = "RowFormButton requires a 'form_fields' argument or class attribute" raise ImproperlyConfigured(msg) super().__init__(**kwargs) # Our superclass added self.text as a block; but that will not work for us, # since we want self.text to be the button text, so remove it. self.blocks = [] self._attributes["method"] = "post"
[docs] def get_confirm_text(self, row: Any) -> str: # noqa: ARG002 """ Get the confirmation text for this button. Args: row: The row data this button is associated with Returns: str: The confirmation text to display """ return self.confirm_text # type: ignore[return-value]
[docs] def bind(self, row: Any, table: Any, size: str | None = None) -> RowFormButton: """ Bind this button to a specific row and create a form. Creates a complete form with: - CSRF token - Hidden fields with row data - Submit button with confirmation dialog Args: row: The row data to bind this button to table: The table instance that contains this row size: Optional size override for the button Returns: RowFormButton: A configured form button for the specified row """ if not size: size = self.size action = deepcopy(self) action.add_block( HiddenInputBlock(input_name="csrfmiddlewaretoken", value=table.csrf_token) ) action._attributes["action"] = self.get_url(row) for field in self.form_fields: action.add_block( HiddenInputBlock(input_name=field, value=getattr(row, field)) ) action.add_block( InputButton( text=self.text, color=self.color, size=size, confirm_text=self.get_confirm_text(row), ) ) return action
[docs]class RowEditButton(RowModelUrlButton): """ A pre-configured button for editing a row's model instance. This is a convenience subclass of RowModelUrlButton configured specifically for edit operations. It looks for a get_update_url method on the model. Either subclass this, setting class attributes to create specific model-based buttons, or use it directly with keyword arguments to create buttons that link to model-specific URLs. Note that all keyword arguments/class attributes from :py:class:`wildewidgets.Block` and :py:class:`RowModelUrlButton` are also accepted. Attributes: attribute: The model method to call for URL ("get_update_url"). Your model must implement this method or attribute to return the URL for editing. text: Default button text ("Edit") color: Default Bootstrap color ("primary") Example: Direct usage with keyword arguments: .. code-block:: python from wildewidgets import RowEditButton # Simple usage - requires model.get_update_url() to exist button = RowEditButton() # Custom text button = RowEditButton(text="Modify") Subclassing for specific actions: class RowCustomEditButton(RowEditButton): attribute = "get_custom_update_url" text = "Modify" color = "warning" url_args = ["id"] """ attribute: str = "get_update_url" text: str = "Edit" color: str = "primary"
[docs]class RowDeleteButton(RowFormButton): """ A pre-configured button for deleting a row's model instance. This is a convenience subclass of RowFormButton configured specifically for delete operations. It submits the row's ID via POST to the URL returned by the model's ``get_delete_url`` method by default. Either subclass this, setting class attributes to create specific model-based buttons, or use it directly with keyword arguments to create buttons that link to model-specific URLs. Note that all keyword arguments/class attributes from :py:class:`wildewidgets.Block` and :py:class:`RowModelUrlButton` are also accepted. Important: Your model must implement a method or attribute named :py:attr:`attribute`, which must return the URL to submit the form to for deletion. By default, this is :py:attr:`get_delete_url`. The form submitted will have by default the following fields: - ``csrf_token``: CSRF token for security - ``id``: The ID of the row's model instance Thus, your delete view should expect a POST request with these fields. Note: The confirmation text is generated using the row's string representation, so ensure your model has a meaningful `__str__` method. If you need a custom confirmation message, subclass and override :py:meth:`get_confirm_text`. Attributes: attribute: The model method or attribute to call/access for the URL ("get_delete_url") text: Default button text ("Delete") color: Default Bootstrap color ("outline-secondary") form_fields: Default field to submit (["id"]) Example: Direct usage with keyword arguments: .. code-block:: python from wildewidgets import RowDeleteButton # Simple usage - requires model.get_delete_url() to exist button = RowDeleteButton() Subclassing for specific actions: class RowCustomDeleteButton(RowDeleteButton): attribute = "get_custom_delete_url" text = "Remove" color = "danger" form_fields = ["id", "name"] # Submit additional fields """ attribute: str = "get_delete_url" text: str = "Delete" color: str = "outline-secondary" form_fields: list[str] = ["id"] # noqa: RUF012
[docs] def get_confirm_text(self, row: Any) -> str: """ Generate a confirmation message for deleting this row. Creates a message like 'Delete "Item Name"?' using the row's string representation. Args: row: The row data to create a confirmation message for Returns: str: The confirmation message to display """ return f'Delete "{row!s}"?'
# ------------------------------- # Mixins # -------------------------------
[docs]class ActionsButtonsBySpecMixin: """ A mixin for :py:class:`wildewidgets.DataTable` classes and subclasses that adds action buttons based on specifications. This mixin allows you to add action buttons to the rightmost column of a table by specifying them as tuples that define their behavior. It provides a simple way to create standard actions without needing to define button classes. Actions are specified as tuples with these elements, in this order: 1. Label (str): The button text 2. URL name (str): Django URL pattern name, e.g., "app:item-view". We'll reverse this to get the URL. 3. Method (str, optional): "get" or "post" (default: "get") 4. Color (str, optional): Bootstrap color class (default: "secondary") 5. Field name (str, optional): ID field name (default: "id") 6. JS function (str, optional): JavaScript function to call Example: .. code-block:: python from wildewidgets import ActionsButtonsBySpecMixin class MyDataTable(ActionsButtonsBySpecMixin, DataTable): actions = [ ("View", "app:item-view"), ("Edit", "app:item-edit", "get", "primary"), ("Delete", "app:item-delete", "post", "danger", "id", "confirmDelete") ] This will add an "Actions" column with buttons for each specified action. Each button will be rendered with the appropriate URL and method based on the row data. Args: *args: Positional arguments for the parent class Keyword Args: actions: List of action specifications to add to the table action_button_size: Size of the action buttons (default: "normal") default_action_button_label: Default label for the action button (default: "View") default_action_button_color_class: Default color class for the action button (default: "secondary") **kwargs: Keyword arguments for the parent class """ #: Per row action buttons. If not ``False``, this will simply add a #: rightmost column named ``Actions`` with a button named #: :py:attr:`default_action_button_label` which when clicked will take the #: user to the actions: Any = False #: How big should each action button be? One of ``normal``, ``btn-lg``, or #: ``btn-sm``. action_button_size: str = "normal" #: The label to use for the default action button default_action_button_label: str = "View" #: The Bootstrap color class to use for the default action buttons default_action_button_color_class: str = "secondary" def __init__( self, *args, actions: Any = None, action_button_size: str | None = None, default_action_button_label: str | None = None, default_action_button_color_class: str | None = None, **kwargs, ): self.actions = actions if actions is not None else self.actions self.action_button_size = action_button_size or self.action_button_size self.default_action_button_label = ( default_action_button_label or self.default_action_button_label ) self.default_action_button_color_class = ( default_action_button_color_class or self.default_action_button_color_class ) if self.action_button_size != "normal": self.action_button_size_class = f"btn-{self.action_button_size}" else: self.action_button_size_class = "" super().__init__(*args, **kwargs)
[docs] def get_template_context_data(self, **kwargs) -> dict[str, Any]: """ Add action column information to the template context. Args: **kwargs: Keyword arguments to update Returns: dict: Updated context dictionary with action column information """ if self.actions: self.add_column(field="actions", searchable=False, sortable=False) kwargs["has_actions"] = True kwargs["action_column"] = len(self.column_fields) - 1 else: kwargs["has_actions"] = False return super().get_template_context_data(**kwargs) # type: ignore[misc]
[docs] def get_content(self, **kwargs) -> str: """ Get the rendered content for the table, including action columns. Args: **kwargs: Keyword arguments for content generation Returns: str: The HTML content for the table """ if self.actions: self.add_column(field="actions", searchable=False, sortable=False) return super().get_content(**kwargs) # type: ignore[misc]
[docs] def get_action_button( self, row: Any, label: str, url_name: str, method: str = "get", color_class: str = "secondary", attr: str = "id", js_function_name: str | None = None, ) -> str: """ Create an action button for a specific row using a Django URL name. Args: row: The row data this button is for label: Text to display on the button url_name: Django URL pattern name to link to method: HTTP method to use ("get" or "post") color_class: Bootstrap color class attr: Row attribute to use as ID parameter js_function_name: Optional JavaScript function to call on click Returns: str: HTML for the rendered button """ if url_name: base = reverse(url_name) # TODO: This assumes we're using QueryStringKwargsMixin, which people # outside our group don't use url = f"{base}?{attr}={row.id}" if method == "get" else base else: url = "javascript:void(0)" return self.get_action_button_with_url( row, label, url, method, color_class, attr, js_function_name )
[docs] def get_action_button_url_extra_attributes(self, row: Any) -> str: # noqa: ARG002 """ Get additional URL query parameters for action buttons. Override this in subclasses to add custom query parameters. Args: row: The row data this button is for Returns: str: Additional URL query parameters """ return ""
[docs] def get_action_button_with_url( self, row: Any, label: str, url: str, method: str = "get", color_class: str = "secondary", attr: str = "id", js_function_name: str | None = None, ) -> str: """ Create an action button for a row with a specific URL. Args: row: The row data this button is for label: Text to display on the button url: Complete URL to link to method: HTTP method to use ("get" or "post") color_class: Bootstrap color class attr: Row attribute to use as ID parameter for POST forms js_function_name: Optional JavaScript function to call on click Returns: str: HTML for the rendered button """ url_extra = self.get_action_button_url_extra_attributes(row) if url_extra: url = f"{url}&{url_extra}" if method == "get": if js_function_name: link_extra = f'onclick="{js_function_name}({row.id});"' else: link_extra = "" return f'<a href="{url}" class="btn btn-{color_class} {self.action_button_size_class} me-2" {link_extra}>{label}</a>' # noqa: E501 token_input = f'<input type="hidden" name="csrfmiddlewaretoken" value="{self.csrf_token}">' # noqa: E501 id_input = f'<input type="hidden" name="{attr}" value="{row.id}">' button = f'<input type=submit value="{label}" class="btn btn-{color_class} {self.action_button_size_class} me-2">' # noqa: E501 return f'<form class="form form-inline" action={url} method="post">{token_input}{id_input}{button}</form>' # noqa: E501
[docs] def get_conditional_action_buttons(self, row: Any) -> str: # noqa: ARG002 """ Get additional action buttons based on row data. Override this in subclasses to add buttons conditionally based on row attributes. Args: row: The row data to evaluate Returns: str: HTML for additional buttons """ return ""
[docs] def render_actions_column(self, row: Any, column: str) -> str: # noqa: ARG002 """ Render all action buttons for a specific row. This method generates the complete HTML for the actions column, including: 1. The default view button if the model has get_absolute_url 2. All buttons defined in the actions specification 3. Any conditional buttons from get_conditional_action_buttons Args: row: The row data to render buttons for column: The column name (ignored, always "actions") Returns: str: Complete HTML for the actions column """ response = '<div class="d-flex flex-row justify-content-end">' if hasattr(row, "get_absolute_url"): if callable(row.get_absolute_url): url = row.get_absolute_url() else: url = row.get_absolute_url view_button = self.get_action_button_with_url( row, self.default_action_button_label, url, color_class=self.default_action_button_color_class, ) response += view_button if not isinstance(self.actions, bool): for action in self.actions: if not len(action) > 1: continue label = action[0] url_name = action[1] method = action[2] if len(action) > 2 else "get" # noqa: PLR2004 color_class = action[3] if len(action) > 3 else "secondary" # noqa: PLR2004 attr = action[4] if len(action) > 4 else "id" # noqa: PLR2004 js_function_name = action[5] if len(action) > 5 else "" # noqa: PLR2004 response += self.get_action_button( row, label, url_name, method, color_class, attr, js_function_name, ) response += self.get_conditional_action_buttons(row) response += "</div>" return response
[docs]class ActionButtonBlockMixin: """ A mixin for :py:class:`wildwidgets.DataTable` classes that adds action buttons using :py:class:`RowActionButton` classes. This mixin provides a more object-oriented approach to adding action buttons compared to :py:class:`ActionsButtonsBySpecMixin`. It uses :py:class:`RowActionButton` instances for greater flexibility and extensibility. Important: Typically, you will not use this directly, but rather use :py:class:`wildewidgets.StandardActionButtonModelTable` or :py:class:`wildewidgets.LookupModelTable`, which already has this built in as a mixin. Example: .. code-block:: python from wildewidgets import ActionButtonBlockMixin, DataTable class MyTable(ActionButtonBlockMixin, DataTable): actions = [ RowEditButton(), RowDeleteButton() ] ... Args: *args: Positional arguments for the parent class Keyword Args: actions: List of :py:class:`RowActionButton` instances to add to the "Actions" column. If not provided, a default "View" button is used. button_size: Size of the action buttons (default: None) justify: Justification of the action buttons in the "Actions" column (default: "end") """ #: If not ``None``, make all per-row action buttons be this size. button_size: str | None = None #: The justification of the action buttons in the "Actions" column. justify: Literal["start", "center", "end"] = "end" #: A list of :py:class:`RowActionButton` subclasses to display in the #: "Actions" column. actions: list[RowActionButton] = [RowModelUrlButton(text="View", color="secondary")] # noqa: RUF012 def __init__( self, *args, actions: list[RowActionButton] | None = None, button_size: str | None = None, justify: Literal["start", "center", "end"] | None = None, **kwargs, ): self.actions = actions if actions is not None else deepcopy(self.actions) self.button_size = button_size or self.button_size self.justify = justify or self.justify super().__init__(*args, **kwargs)
[docs] def get_actions(self) -> list[RowActionButton]: """ Get the list of action buttons to display. Override this in subclasses to dynamically determine which buttons to show. Returns: list[RowActionButton]: List of action button instances """ return self.actions
[docs] def get_content(self, **kwargs) -> str: """ Get the rendered content for the table, including action columns. Args: **kwargs: Keyword arguments for content generation Returns: str: The HTML content for the table """ if self.get_actions(): self.add_column(field="actions", searchable=False, sortable=False) return super().get_content(**kwargs) # type: ignore[misc]
[docs] def render_actions_column(self, row: Any, column: str) -> str: # noqa: ARG002 """ Render all action buttons for a specific row. Creates a horizontal layout containing all visible action buttons for the given row. Args: row: The row data to render buttons for column: The column name (ignored, always "actions") Returns: str: Complete HTML for the actions column Note: Buttons are only shown if their is_visible method returns True for the current user. """ container = HorizontalLayoutBlock(justify=self.justify) for action in self.get_actions(): button = action.bind(row, self, size=self.button_size) user = None if hasattr(self, "request") and self.request is not None: user = self.request.user if not button.is_visible(row, user): continue button.add_class("me-2") container.add_block(button) button.remove_class("me-2") return str(container)
[docs]class StandardModelActionButtonBlockMixin(ActionButtonBlockMixin): """ A ready-to-use mixin providing standard "Edit" and "Delete" buttons for model tables. This mixin provides a convenient configuration of :py:class:`ActionButtonBlockMixin` with "Edit" and "Delete" buttons pre-configured. It's a quick way to add these common actions to any DataTable. Note: This is used by :py:class:`wildewidgets.LookupModelTable`. Requirements: - Your model must implement ``get_absolute_url``, ``get_update_url`` and ``get_delete_url`` methods Example: .. code-block:: python from wildewidgets import StandardModelActionButtonBlockMixin, DataTable from myapp.models import MyModel class MyTable(StandardModelActionButtonBlockMixin, DataTable): model = MyModel fields = ["name", "description"] # No additional configuration needed for basic edit/delete buttons """ actions: list[RowActionButton] = [ # noqa: RUF012 RowModelUrlButton(text="Edit", color="primary", attribute="get_update_url"), # TODO: change this to RowFormButton RowModelUrlButton( text="delete", color="outline-secondary", attribute="get_delete_url", ), ]