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",
),
]