Source code for wildewidgets.viewsets

from __future__ import annotations

from copy import deepcopy
from typing import TYPE_CHECKING, Any

from django.core.exceptions import ImproperlyConfigured
from django.urls import path, reverse_lazy
from django.utils.text import slugify

from wildewidgets.views.generic import (
    CreateView,
    DeleteView,
    IndexView,
    TableAJAXView,
    TableBulkActionView,
    UpdateView,
)
from wildewidgets.widgets import (
    BaseDataTable,
    BreadcrumbBlock,
    LookupModelTable,
    Navbar,
)

from .models import ViewSetMixin

if TYPE_CHECKING:
    from django.views import View


[docs]class ModelViewSet: """ A viewset that provides CRUD operations for a Django model. This class simplifies the creation of a complete set of views for a model, automatically generating URLs and configuring views for listing, creating, updating, and deleting model instances. It follows the Django REST Framework ViewSet pattern but for traditional Django views. The viewset handles: - Index/list view with a data table - AJAX endpoint for table data loading - Bulk action processing (e.g., bulk delete) - Create view with a form - Update view with a form - Delete view To use this class, create a subclass and set at minimum the `model` attribute: Example: .. code-block:: python from wildewidgets import ( BreadcrumbBlock, TablerVerticalNavbar, Menu, MenuItem, ) from wildewidgets.viewsets import ModelViewSet from myapp.models import Book class MainMenu(Menu): items = [ MenuItem( text="Home", icon="house", url=reverse_lazy("core:home"), ), MenuItem( text="Books", icon="mailbox", url=reverse_lazy("core:books--index"), ), ] class MyNavbar(TablerVerticalNavbar): hide_below_viewport: str = "xl" branding = Block( LinkedImage( image_src=static("images/logo.png"), image_width="150px", image_alt="MyOrg", css_class="d-flex justify-content-center ms-3", url="https://www.example.com/", ), ) contents = [ MainMenu(), ] class MyBreadcrumbs(BreadcrumbBlock): def __init__(self): super().__init__() self.add_breadcrumb(title="Home", url="/") class BookViewSet(ModelViewSet): model = Book url_prefix = "books" url_namespace = "myapp" breadcrumbs_class = MyBreadcrumbs # In urls.py urlpatterns += BookViewSet().get_urlpatterns() Keyword Args: model: The model class to use (must be a :py:class:`wildewidgets.ViewSetMixin` subclass) url_prefix: Optional prefix for all URLs (defaults to model name) url_namespace: Optional namespace for URL names **kwargs: Additional attributes to set on the viewset Raises: ImproperlyConfigured: If model is not provided or not a ViewSetMixin subclass, or if model is missing required attributes """ #: The model for which we are building this viewset model: type[ViewSetMixin] | None = None #: The path prefix to use to organize our views. url_prefix: str | None = None #: The URL namespace to use for our view names. This should be set to #: the ``app_name`` set in the ``urls.py`` where this viewset's patterns #: will be added to ``urlpatterns`` url_namespace: str | None = None #: Set this to your :py:class:`wildewidgets.widgets.navigation.BreadcrumbBlock` #: class to manage breadcrumbs automatically. breadcrumbs_class: type[BreadcrumbBlock] | None = None #: The navbar to use. We'll highlight our verbose name in any of the #: menus within the navbar navbar_class: type[Navbar] | None = None #: The template name to use for all views template_name: str | None = None #: The view class to use for the index view; must be a subclass of #: :py:class:`wildewidgets.views.generic.IndexView`. index_view_class: type[IndexView] = IndexView #: The :py:class:`wildewidgets.widgets.tables.base.BaseDataTable` subclass #: to use for the listing table index_table_class: type[BaseDataTable] = LookupModelTable #: A dictionary that we will use as the ``**kwargs`` for the constructor of #: :py:attr:`table_class` index_table_kwargs: dict[str, Any] = { # noqa: RUF012 "striped": True, "page_length": 50, "buttons": True, } #: The view class to use as the AJAX JSON endpoint for the dataTable; must #: be a subclass of :py:class:`wildewidgets.views.generic.TableAJAXView`. table_ajax_view_class: type[View] = TableAJAXView #: The view class to use for the index view; must be a subclass of #: :py:class:`wildewidgets.views.generic.CreateView`. table_bulk_action_view_class: type[View] = TableBulkActionView #: The view class to use for the index view; must be a subclass of #: :py:class:`wildewidgets.views.generic.CreateView`. create_view_class: type[View] = CreateView #: The view class to use for the index view; must be a subclass of #: :py:class:`wildewidgets.views.generic.UpdateView`. update_view_class: type[View] = UpdateView #: The view class to use for the index view; must be a subclass of #: :py:class:`wildewidgets.views.generic.DeleteView`. delete_view_class: type[View] = DeleteView def __init__( self, model: type[ViewSetMixin] | None = None, url_prefix: str | None = None, url_namespace: str | None = None, **kwargs, ): #: The model we're describing in this view. It must have #: :py:class:`wildewidgets.models.ViewSetMixin` in its class heirarchy self.model: type[ViewSetMixin] = model or self.model if not self.model or not issubclass(self.model, ViewSetMixin): msg = ( "model must be either set as a class attribute or passed in as " "a kwarg, and it also must be a subclass of ViewSetMixin" ) raise ImproperlyConfigured(msg) if not self.model._meta.verbose_name_plural: msg = "Model must have a verbose_name_plural set to use breadcrumbs" raise ImproperlyConfigured(msg) if not self.model._meta.verbose_name: msg = "model must have a verbose_name" raise ImproperlyConfigured(msg) # Tell the model we are its viewset self.model.viewset = self self.name = slugify(self.model._meta.verbose_name.lower()) self.url_prefix = url_prefix or self.name self.url_namespace = url_namespace for key, value in kwargs.items(): setattr(self, key, value)
[docs] def get_url_name(self, view_name: str, namespace: bool = True) -> str: """ Generate a URL name for a specific view. This method creates a URL name by combining the model name with the view name. If namespace is True and a URL namespace is set, the namespace is prepended. Args: view_name: The base name of the view (e.g., "index", "create") namespace: Whether to include the namespace prefix if available Returns: str: The complete URL name for the view Example: >>> viewset.get_url_name("create") 'myapp:book--create' # (if namespace is 'myapp' and model name is 'book') """ url_name = f"{self.name}--{view_name}" if namespace and self.url_namespace: url_name = f"{self.url_namespace}:{url_name}" return url_name
[docs] def get_breadcrumbs(self, view_name: str) -> BreadcrumbBlock | None: """ Create breadcrumb navigation for the specified view. This method returns a configured breadcrumb block for the current view. For views other than "index", it adds a breadcrumb link back to the index view. Args: view_name: The name of the view ("index", "create", "update", etc.) Returns: Configured breadcrumb block, or None if :py:attr:`breadcrumbs_class` is not set """ breadcrumbs: BreadcrumbBlock | None = None if self.breadcrumbs_class: breadcrumbs = self.breadcrumbs_class() # pylint: disable=not-callable if view_name != "index": breadcrumbs.add_breadcrumb( title=self.model.model_verbose_name_plural(), # type: ignore[union-attr] url=reverse_lazy(self.get_url_name("index")), ) return breadcrumbs
[docs] def get_index_table_kwargs(self) -> dict[str, Any]: """ Get keyword arguments for configuring the index table. This method creates a dictionary of configuration options for the data table on the index page. It automatically sets: - Default form actions if none are specified - The AJAX URL for fetching data - The form URL for bulk actions Returns: dict[str, Any]: Configuration options for the data table """ table_kwargs = deepcopy(self.index_table_kwargs) if "form_actions" not in table_kwargs: table_kwargs["form_actions"] = [("delete", "Delete selected")] table_kwargs["ajax_url_name"] = self.get_url_name("table-ajax") table_kwargs["form_url"] = reverse_lazy(self.get_url_name("table-bulk-actions")) return table_kwargs
@property def index_view(self): """ Get the configured index view. This property returns a Django view function for listing model instances in a data table, with all necessary configuration. Returns: View callable: Configured index view """ return self.index_view_class.as_view( model=self.model, template_name=self.template_name, table_class=self.index_table_class, table_kwargs=self.get_index_table_kwargs(), breadcrumbs=self.get_breadcrumbs("index"), navbar_class=self.navbar_class, ) @property def table_ajax_view(self): """ Get the configured AJAX data endpoint view. This property returns a Django view function that serves as the AJAX endpoint for the data table, providing data in JSON format. Returns: View callable: Configured AJAX view """ return self.table_ajax_view_class.as_view( model=self.model, table_class=self.index_table_class, table_kwargs=self.get_index_table_kwargs(), ) @property def table_bulk_action_view(self): """ Get the configured bulk action view. This property returns a Django view function for processing bulk actions on multiple model instances, such as bulk deletion. Returns: View callable: Configured bulk action view """ return self.table_bulk_action_view_class.as_view( model=self.model, url=reverse_lazy(self.get_url_name("index")), table_class=self.index_table_class, table_kwargs=self.get_index_table_kwargs(), ) @property def create_view(self): """ Get the configured create view. This property returns a Django view function for creating new model instances, with form handling and success/error redirects. Returns: View callable: Configured create view """ return self.create_view_class.as_view( model=self.model, template_name=self.template_name, breadcrumbs=self.get_breadcrumbs("create"), navbar_class=self.navbar_class, success_url=reverse_lazy(self.get_url_name("index")), ) @property def update_view(self): """ Get the configured update view. This property returns a Django view function for updating existing model instances, with form handling and success/error redirects. Returns: View callable: Configured update view """ return self.update_view_class.as_view( model=self.model, template_name=self.template_name, breadcrumbs=self.get_breadcrumbs("update"), navbar_class=self.navbar_class, success_url=reverse_lazy(self.get_url_name("index")), ) @property def delete_view(self): """ Get the configured delete view. This property returns a Django view function for deleting model instances, with success redirect after deletion. Returns: View callable: Configured delete view """ return self.delete_view_class.as_view( model=self.model, success_url=reverse_lazy(self.get_url_name("index")) )
[docs] def get_urlpatterns(self): """ Generate URL patterns for all views. This method creates a list of URL patterns for all CRUD operations: - Index/list view - AJAX endpoint for table data - Bulk action processing - Create view - Update view - Delete view The URLs follow a consistent pattern based on the model name and any prefix. Returns: list: Django URL patterns for all views Example: >>> urlpatterns = BookViewSet().get_urlpatterns() >>> # URLs will include patterns like 'books/', 'books/create/', etc. """ root = f"{slugify(self.model.model_logger_name())}/" if self.url_prefix: root = f"{self.url_prefix}/{root}" return [ path( root, self.index_view, name=self.get_url_name("index", namespace=False) ), path( f"{root}table/ajax/", self.table_ajax_view, name=self.get_url_name("table-ajax", namespace=False), ), path( f"{root}table/bulk/", self.table_bulk_action_view, name=self.get_url_name("table-bulk-actions", namespace=False), ), path( f"{root}create/", self.create_view, name=self.get_url_name("create", namespace=False), ), path( f"{root}<int:pk>/", self.update_view, name=self.get_url_name("update", namespace=False), ), path( f"{root}<int:pk>/delete/", self.delete_view, name=self.get_url_name("delete", namespace=False), ), ]