Source code for superdjango.ui.options.utils

# Imports

from django.contrib.humanize.templatetags.humanize import intcomma
from django.db import models
from django.urls import reverse, NoReverseMatch
from django.utils.translation import gettext_lazy as _
import logging
import os
from superdjango.assets.library import JavaScript, StyleSheet
from superdjango.forms import RequestEnabledModelForm
from urllib.parse import quote_plus
from ..constants import FILTER, LOCATION, ORIENTATION, PAGINATION

log = logging.getLogger(__name__)

# Exports

__all__ = (
    "Actions",
    "Aggregate",
    "BaseUtil",
    "BlankFooter",
    "ChainedLookup",
    "Choice",
    "ContextMenu",
    "DateIncrement",
    "Default",
    "Dropdown",
    "FieldGroup",
    "Fieldset",
    "Filtering",
    "FooterRow",
    "FormStep",
    "Help",
    "HelpText",
    "Limit",
    "ListTypes",
    "OnSelect",
    "Ordering",
    "Pagination",
    "Tab",
    "Toggle",
)

# Classes


[docs]class BaseUtil(object): """Base for utility classes, the primary purpose of which is to capture any additional kwargs and assign them to the ``attributes`` attribute. These may be - flattened in templates to assign attributes to the element, or - used programmatically using the getter. """
[docs] def __init__(self, **kwargs): """Initialize dynamic attributes.""" self.attributes = kwargs
def __getattr__(self, item): return self.attributes.get(item) def __repr__(self): return "<%s>" % self.__class__.__name__
[docs]class Actions(object): """A collection of actions used to output buttons, links, or context menus. This class may be used instead of a list of action names. """
[docs] def __init__(self, *items, enabled=True, label=None, location=LOCATION.DEFAULT): """Initialize the instance. :param items: A list of action (verb) names to be included in the set. These may also be supplied as instances that extend the :py:class:`BaseAction` class. :param enabled: Indicates the actions are available to users. This may be toggled programmatically. :type enabled: bool :param label: The label for this group of actions. Defaults to ``_("Actions")`` but may be disabled by passing an empty string. :type label: str :param location: The desired location of the actions. :type location: str """ self.enabled = enabled self.items = list(items) self.location = location if label is None: self.label = _("Actions") else: self.label = label
def __iter__(self): return iter(self.items) def __repr__(self): return "<%s %s (%s)>" % (self.__class__.__name__, self.label, len(self.items))
[docs]class Aggregate(object): """Specify options for loading aggregate data.""" AVG = "avg" COUNT = "count" MAX = "max" MIN = "min" STDDEV = "stddev" SUM = "sum" VARIANCE = "variance"
[docs] def __init__(self, field, callback=None, criteria=None, formatter=None, label=None): """Initialize an aggregate option. :param field: The name of the field to be aggregated. :type field: str :param callback: The callback to use for aggregation. This may be a built-in name (``str``) or a callable. See notes. :type callback: callable | str :param criteria: Optional criteria for filtering aggregate data. :type criteria: dict :param formatter: A callable that may be used to format the aggregate result. If should accept a single parameter, which is the return value of query, and return a ``str``. Note that the value may be ``None``. If omitted, a default formatter is applied. :type formatter: callable :param label: Optional label for the aggregated data. :type label: str The built-in callbacks are: - avg - count - max - min - stddev - sum - variance When providing a callback, it should exhibit the following signature: ``callback(field, queryset, request, ui, criteria=None)`` """ self.callback = callback self.criteria = criteria self.field = field self.formatter = formatter or self._default_formatter self.label = label self.value = None
def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.field) def __str__(self): return self.formatter(self.value)
[docs] def load(self, queryset, request, ui): """Load the aggregate data. Sets the ``value`` attribute of the Aggregate instance. :param queryset: The queryset to use for aggregation. :type queryset: QuerySet :param request: The current HTTP request instance. This is not used by default. :param ui: The current model UI instance. :type ui: ModelUI :rtype: bool :returns: ``True`` if a value was acquired. If ``False``, check the logs. """ ''' try: if self.callback in ("average", "avg"): return self._avg(queryset, request, ui) elif self.callback == "count": return self._count(queryset, request, ui) elif self.callback in ("max", "maximum"): return self._max(queryset, request, ui) elif self.callback in ("min", "minimum"): return self._min(queryset, request, ui) elif self.callback in ("stddev", "standard_deviation"): return self._stddev(queryset, request, ui) elif self.callback in ("total", "sum"): return self._sum(queryset, request, ui) elif self.callback in ("var", "variance"): return self._variance(queryset, request, ui) elif callable(self.callback): self.value = self.callback(self.field, queryset, request, ui, criteria=self.criteria) return self.value is not None else: log.warning("Expected a built-in or custom callable for Aggregate callback: %s" % self.field) return False except AssertionError as e: log.warning("%s Aggregate callback: %s" % (str(e), self.field)) return False ''' # BUG: Utilizing criteria on a paginated queryset results in an AssertionError (Cannot filter a query once a # slice has been taken). One workaround might be to provide a queryset as an keyword argument to __init__. if self.callback in ("average", "avg"): return self._avg(queryset, request, ui) elif self.callback == "count": return self._count(queryset, request, ui) elif self.callback in ("max", "maximum"): return self._max(queryset, request, ui) elif self.callback in ("min", "minimum"): return self._min(queryset, request, ui) elif self.callback in ("stddev", "standard_deviation"): return self._stddev(queryset, request, ui) elif self.callback in ("total", "sum"): return self._sum(queryset, request, ui) elif self.callback in ("var", "variance"): return self._variance(queryset, request, ui) elif callable(self.callback): self.value = self.callback(self.field, queryset, request, ui, criteria=self.criteria) return self.value is not None else: log.warning("Expected a built-in or custom callable for Aggregate callback: %s" % self.field) return False
# noinspection PyUnusedLocal def _avg(self, queryset, request, ui): output_key = "%s__avg" % self.field if self.criteria is not None: self.value = queryset.filter(**self.criteria).aggregate(models.Avg(self.field))[output_key] else: self.value = queryset.all().aggregate(models.Avg(self.field))[output_key] return self.value is not None # noinspection PyUnusedLocal def _count(self, queryset, request, ui): if self.criteria is not None: self.value = queryset.filter(**self.criteria).count() else: self.value = queryset.all().count() return self.value is not None # noinspection PyMethodMayBeStatic def _default_formatter(self, value): """The default formatter. :param value: The (aggregate) value to be formatted. :rtype: str """ if value is None: return "" if "." in str(value): number, places = str(value).split(".") if places == "0": places = "00" return "%s.%s" % (intcomma(number), places) return intcomma(value) # noinspection PyUnusedLocal def _max(self, queryset, request, ui): output_key = "%s__max" % self.field if self.criteria is not None: self.value = queryset.filter(**self.criteria).aggregate(models.Max(self.field))[output_key] else: self.value = queryset.all().aggregate(models.Max(self.field))[output_key] return self.value is not None # noinspection PyUnusedLocal def _min(self, queryset, request, ui): output_key = "%s__min" % self.field if self.criteria is not None: self.value = queryset.filter(**self.criteria).aggregate(models.Min(self.field))[output_key] else: self.value = queryset.all().aggregate(models.Min(self.field))[output_key] return self.value is not None # noinspection PyUnusedLocal def _stddev(self, queryset, request, ui): output_key = "%s__stddev" % self.field if self.criteria is not None: self.value = queryset.filter(**self.criteria).aggregate(models.StdDev(self.field))[output_key] else: self.value = queryset.all().aggregate(models.StdDev(self.field))[output_key] return self.value is not None # noinspection PyUnusedLocal def _sum(self, queryset, request, ui): output_key = "%s__sum" % self.field if self.criteria is not None: self.value = queryset.filter(**self.criteria).aggregate(models.Sum(self.field))[output_key] else: self.value = queryset.all().aggregate(models.Sum(self.field))[output_key] return self.value is not None # noinspection PyUnusedLocal def _variance(self, queryset, request, ui): output_key = "%s__variance" % self.field if self.criteria is not None: self.value = queryset.filter(**self.criteria).aggregate(models.Variance(self.field))[output_key] else: self.value = queryset.all().aggregate(models.Variance(self.field))[output_key] return self.value is not None
[docs]class BlankFooter(object): """A blank footer that simulates the DBC for :py:class:`Aggregate`. See :py:class:`FooterRow`.""" def __str__(self): return ""
[docs] def load(self, queryset, request, ui): """Does nothing. Maintains the DBC for :py:class:`Aggregate`.""" pass
[docs]class ChainedLookup(object): """Connects the possible values of a reference field to the value of another field."""
[docs] def __init__(self, source_field, target_field, empty_value=None, pattern_name=None): """Initialize the chained lookup. :param source_field: The field name whose value is used to filter the possible values of the target field. :type source_field: str :param target_field: The field name whose values depend upon the selected value of the source field. :type target_field: str :param empty_value: The value to display when no ``source_field`` value is selected. Defaults to "Select a <source_field>" :type empty_value: str :param pattern_name: The pattern name of the UIAjaxChainedLookupView (or compatible view) that is used for acquiring the ``target_field`` values. In most cases, automatic resolution does not require this parameter to be specified. :type pattern_name: str """ self.empty_value = empty_value or _("Select %s" % source_field.replace("_", " ").title()) self.pattern_name = pattern_name self.source_field = source_field self.target_field = target_field
[docs] def get_form_js(self, ui, record=None): """Get the JavaScript to be used in the form. :param ui: The current model UI instance that is utilizing the lookup. :type ui: ModelUI :param record: The record (model instance), if any. :rtype: JavaScript | None """ url = self.get_url(ui) if url is None: message = "Could not find URL lookup for ChainedLookup: %s, %s" log.debug(message % (self.source_field, self.target_field)) return None js = JavaScript() js.append("chained", url="bundled/chained/jquery.chained.remote.js") js.from_template( "%s-%s chain" % (self.source_field, self.target_field), "superdjango/ui/js/chained_lookup.js", context={ 'empty_value': self.empty_value, 'record': record, 'source_selector': "#id_%s" % self.source_field, 'target_is_required': not ui.meta.get_field(self.target_field).null, 'target_selector': "#id_%s" % self.target_field, 'url': url, } ) return js
[docs] def get_url(self, ui): """Get the AJAX URL that provides the lookup. :param ui: The current model UI instance that is utilizing the lookup. :type ui: ModelUI :rtype: str | None """ url = None if self.pattern_name is not None: pattern_name = self.pattern_name try: url = reverse(pattern_name) except NoReverseMatch: pass elif ui.site is not None: dotted = ui.get_remote_model(self.target_field, dotted=True) url = ui.site.get_url(dotted, "ajax_chained_lookup") else: remote = ui.get_control(self.target_field).remote_field # noinspection PyProtectedMember pattern_name = "%s_%s_ajax_chained_lookup" % ( remote.model._meta.app_label, remote.model._meta.model_name ) try: url = reverse(pattern_name) except NoReverseMatch: pass return url
[docs]class Choice(BaseUtil): """A user choice."""
[docs] def __init__(self, label, value, abbr=None, description=None, icon=None, is_enabled=True, url=None, **kwargs): self.abbr = abbr self.description = description self.icon = icon self.is_enabled = is_enabled self.label = label self._url = url self.value = value super().__init__(**kwargs)
@property def url(self): """Get the encoded URL. :rtype: str """ return quote_plus(self._url)
[docs]class ContextMenu(object): """Actions to be displayed with a context (right-click) menu for a record."""
[docs] def __init__(self, *items, base_url=None, selector=".ui-record"): """Initialize a context menu. :param items: The actions (verbs) to be included in the menu. These must be converted into an Action instance prior to template rendering. See the ``get_context_menu()`` of t he :py:class:`UIListView` for an example. :param base_url: The base URL of the actions. Defaults to the list view of the model. :type base_url: str :param selector: The CSS selector that identifiers record. The default is used across all UI list types, so don't override it unless you are prepared for the additional work involved. :type selector: str """ self.base_url = base_url self.items = list(items) self.selector = selector
def __iter__(self): return iter(self.items) def __repr__(self): return "<%s %s (%s)>" % (self.__class__.__name__, self.base_url, len(self.items)) # noinspection PyMethodMayBeStatic
[docs] def get_css(self): """Get the CSS for the context menu. :rtype: StyleSheet """ css = StyleSheet() css.append( "context-menu", url="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.css" ) return css
[docs] def get_js(self): """Get the JavaScript for the context menu. :rtype: JavaScript """ # TODO: Consider bundling jquery-contextmenu resources. js = JavaScript() js.append( "jquery-contextmenu", url="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.js" ) js.append( "jquery.ui.position", url="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.ui.position.js", ) js.from_template( "%s context-menu" % self.base_url, "superdjango/ui/js/context-menu.js", context={'menu': self} ) return js
[docs]class DateIncrement(object): """Automatically increment a date field based upon the value entered in another date field. .. code-block:: python from superdjango import ui class ProjectUI(ui.ModelUI): # ... controls = { 'end_date': ui.controls.DateControl(increment=ui.DateIncrement(source_field="start_date", days=60)), } # ... JavaScript is produced to perform the increment as soon as the source field is changed. """
[docs] def __init__(self, source_field, always=False, days=None, target_field=None): """Initialize an increment. :param source_field: The field whose value is used to set the value of the target field. :type source_field: str :param always: Indicates the target field should always be updated, even if it already has a value. :type always: bool :param days: The number of days by which the source field value should be incremented. Default: ``30``. :type days: int :param target_field: The target field to be updated. This defaults to the field name to which the ``DateIncrement`` is attached. :type target_field: str """ self.always = always self.days = days or 30 self.source_field = source_field self.target_field = target_field
def __repr__(self): return "<%s %s <- %s>" % (self.__class__.__name__, self.source_field, self.target_field) @property def source_selector(self): """The jQuery selector for the source field. :rtype: str """ return "#id_%s" % self.source_field @property def target_selector(self): """The jQuery selector for the target field. :rtype: str """ return "#id_%s" % self.target_field
# def get_form_js(self): # """Get the JavaScript needed to perform the increment. # # :rtype: JavaScript # # """ # js = JavaScript() # # js.append("moment.js", url="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment-with-locales.js") # js.append("datetime.js", url="superdjango/ui/js/datetime.js") # # identifier = "increment %s from %s" % (self.target_field, self.source_field) # template = os.path.join("superdjango", "ui", "js", "increment_date.js") # js.from_template(identifier, template, context={'increment': self}) # # return js
[docs]class Default(object): """Programmatically define a default value."""
[docs] def __init__(self, callback=None, takes_request=False, value=None): """Initialize the default. :param callback: A function used to acquire the value. :param takes_request: Indicates the callback utilizes the current request. :type takes_request: bool :param value: The default value. The callback signature should be ``callback(record=None)``. However, when ``takes_request`` is ``True``, the callback signature should be: ``callback(request, record=None)`` Additionally, the callback is expected to return the correct Python data type for the intended use of the default value. """ self.callback = callback self.takes_request = takes_request self.value = value
def __repr__(self): if self.callback is not None: return "<%s callback:%s>" % (self.__class__.__name__, self.callback) return "<%s value:%s>" % (self.__class__.__name__, self.value)
[docs] def get(self, request, record=None): """Get the default value. :param request: The current HTTP request instance. :param record: The record (model instance), if any, with which the default is associated. """ if self.callback is not None: if self.takes_request: return self.callback(request, record=record) return self.callback(record=record) return self.value
class Dropdown(object): def __init__(self, *events, control_field=None): self.control_field = control_field self.events = events def __iter__(self): return iter(self.events) def __repr__(self): return "<%s %s (%s)>" % (self.__class__.__name__, self.control_field, len(self.events)) def get_form_js(self): """Get the JavaScript that provides the on select functionality. :rtype: JavaScript """ js = JavaScript() identifier = "%s dropdown events" % self.control_field template = os.path.join("superdjango", "ui", "js", "on_select.js") js.from_template(identifier, template, context={'dropdown': self}) return js def get_values(self): a = list() for e in self.events: if type(e.value) in (list, tuple): for v in e.value: if v not in a: a.append(v) elif e.value not in a: a.append(e.value) else: pass return a
[docs]class FieldGroup(object): """A grouping of fields."""
[docs] def __init__(self, *fields, label=None, size=None): """Initialize a field group. :param fields: List of field names to include in the group. :param label: A label for the group. :type label: str :param size: The size of the group's columns. :type size: int """ self.fields = list(fields) self.label = label self.size = size
[docs]class Fieldset(BaseUtil): """A field set within a form."""
[docs] def __init__(self, *fields, classes=None, description=None, legend=None, **kwargs): """Initialize a fieldset. :param fields: A list of field names to include in the fieldset. :param classes: The CSS classes to be applied to the fieldset element. :type classes: str :param description: The description appears before fields. :type description: str :param legend: The fieldset legend. :type legend: str """ self.description = description self.fields = list(fields) self.legend = legend self._fields = list() if classes is not None: kwargs['class'] = classes super().__init__(**kwargs)
def __iter__(self): return iter(self._fields) def __len__(self): """Required for iteration in templates.""" return len(self._fields) def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.legend)
[docs]class Filtering(object): """A utility class for holding filters and filter options."""
[docs] def __init__(self, *fields, default=None, location=LOCATION.DEFAULT, orientation=None, template=None): """Initialize filtering options for a list. :param fields: A list of field names to be included as filters. :param default: Establish the default filtering of the list. The criteria included here need not be related to filter fields. Any request made to the list that does not include filtering will use this criteria instead. :type default: dict :param location: The location of the filter on the page. :type location: str :param orientation: The orientation of the filter; ``ORIENTATION.HORIZONTAL`` or ``ORIENTATION.VERTICAL``. By default, the orientation is selected based on the location. :type orientation: str :param template: The template to use for the filters. By default, the template is based upon the location and orientation of the filters. :type template: str """ self.default = default self.fields = fields self.filters = None self.location = location if orientation is not None: self.orientation = orientation elif location in (LOCATION.LEFT, LOCATION.RIGHT): self.orientation = ORIENTATION.VERTICAL else: self.orientation = ORIENTATION.HORIZONTAL if template is not None: self.template = template elif location in (LOCATION.LEFT, LOCATION.RIGHT): self.template = "model_filter_" + FILTER.LISTGROUP else: self.template = "model_filter_" + FILTER.SELECT
[docs]class FooterRow(object): """A collection of footer data to be displayed with a table. .. code-block:: python from superdjango import ui class TodoUI(ui.ModelUI): # ... list_options = ui.ListOptions( ui.lists.Table( "title", "priority", "stage", "estimated_cost", "estimated_hours", "is_complete", footers=[ ui.lists.FooterRow( ui.lists.BlankFooter(), ui.lists.BlankFooter(), ui.lists.BlankFooter(), ui.Aggregate("estimated_cost", callback="sum"), ui.Aggregate("estimated_hours", callback="sum"), ui.lists.BlankFooter(), label=_("Totals") ), ui.lists.FooterRow( ui.lists.BlankFooter(), ui.lists.BlankFooter(), ui.lists.BlankFooter(), ui.Aggregate("estimated_cost", callback="avg"), ui.Aggregate("estimated_hours", callback="avg"), ui.lists.BlankFooter(), label=_("Average") ), ], link_field="title" ) ) # ... """
[docs] def __init__(self, *data, label=None): """Initialize the footer row. :param data: A list of :py:class`Aggregate` instances. :param label: Optional label for the row. :type label: str """ self.data = list(data) self.label = label
def __iter__(self): return iter(self.data) def __len__(self): return len(self.data) def __repr__(self): return "<%s %s (%s)>" % (self.__class__.__name__, self.label, len(self.data))
[docs]class FormStep(object): """Define a step within multi-step form, e.g. a form wizard."""
[docs] def __init__(self, *fields, callback=None, description=None, form_class=None, label=None): self.callback = callback self.description = description self.fields = list(fields) self.form_class = form_class or RequestEnabledModelForm self.label = label
[docs]class Help(object): """Encapsulate help resources."""
[docs] def __init__(self, articles=None, icon="fas fa-life-ring", include_titles=True, path=None, snippets=None, terms=None, title=None): self.icon = icon self.include_titles = include_titles self.path = path self.snippets = snippets or list() self.terms = terms or list() self.title = title or _("Help") self._articles = articles or list()
@property def articles(self): """Get the article instances included in the help. :rtype: list[superdjango.contrib.support.library.Article] """ from superdjango.contrib.support.loader import content a = list() for slug in self._articles: article = content.fetch(slug) if article is not None: a.append(article) return a
[docs]class HelpText(object): """Manage help text programmatically. .. code-block:: python from django.utils.translation import ugettext_lazy as _ from superdjango import ui from .models import MemberApplication class MemberApplicationUI(ui.ModelUI): model = MemberApplication controls = { 'name': ui.controls.CharControl( help_text={ ui.USER.NORMAL: ui.HelpText(_("Please enter your full name.")), ui.USER.ROOT: ui.HelpText(_("The user's full name.")), } ), } """
[docs] def __init__(self, form_text, detail_text=None, list_text=None): """Initialize help text. :param form_text: The text to display with form elements. Used for all other text when no other options are provided. :type form_text: str :param list_text: The text to use for list display. :type list_text: str :param detail_text: The text to use for detail display. :type detail_text: str """ self.detail_text = detail_text or form_text self.form_text = form_text self.list_text = list_text or form_text
def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.form_text) def __str__(self): return str(self.form_text) @property def column_text(self): """An alias for ``list_text``.""" return self.list_text @property def help_text(self): """An alias for ``form_text``.""" return self.form_text
[docs]class Limit(object): """A utility for capturing options for results per page. Used by :py:class:`Pagination`, but may also be used to exercise programmatic control over limit and limit display. """ WIDGET_BUTTON = "button" WIDGET_DISABLED = None WIDGET_LINKS = "links" WIDGET_PILLS = "pills" WIDGET_SELECT = "select" WIDGET_TOOLBAR = "toolbar"
[docs] def __init__(self, increments=None, label=None, location=LOCATION.DEFAULT, value=PAGINATION.LIMIT, widget=WIDGET_LINKS): """Initialize pagination limit configuration. :param increments: A list of integers the user may select for results per page. Default: ``[10, 25, 50, 100]`` :type increments: list[int] :param label: The label for the widget. Default: ``_("Results Per Page")``. Different widgets use the label in different ways. :type label: str :param location: The desired location of the widget. :type location: str :param value: The initial results per page. :type value: int :param widget: The widget to use that allows the user to change the limit. Set to ``Limit.WIDGET_DISABLED`` (``None``) to disallow this feature. :type widget: str | None """ self.increments = increments or [10, 25, 50, 100] self.label = label or _("Results Per Page") self.location = location self.value = value self.widget = widget
def __eq__(self, other): return self.value == other def __int__(self): return self.value def __str__(self): return "%s" % self.value
[docs]class ListTypes(object): """Manage list type options."""
[docs] def __init__(self, *items, label=None, location=LOCATION.DEFAULT): """Initialize list type options. :param items: A list of instances that extend :py:class:`superdjango.ui.options.lists.BaseList`. :param label: The label for the list type switcher. Default: ``_("View As")``. :type label: str :param location: The desired location of the list type switcher. :type location: str """ self.items = list(items) self.label = label or _("View As") self.location = location
def __contains__(self, item): for i in self.items: if i.get_context_name() == item: return True return False def __getitem__(self, item): if item == "label": return self.label if item == "location": return self.location for i in self.items: if i.get_context_name() == item: return i return None def __iter__(self): return iter(self.items) def __len__(self): return len(self.items) def __repr__(self): return "<%s %s (%s)>" % (self.__class__.__name__, self.label, len(self.items)) def values(self): return self.items
class OnSelect(object): def __init__(self, value, target_field=None, default_value=None, required=None, visible=None): """Define what happens when an option is selected from a dropdown menu. :param value: The selected value. :param target_field: The field to be updated based on the selected value. See notes below. :type target_field: dict | list | str | tuple :param default_value: Set the target field value to this default. :param required: Indicates the target field is required. :type required: bool | None :param visible: Indicates the target field should be made visible. :type visible: bool | None When the ``target_field`` is supplied as a `list` or `tuple`, the provided options are applied to all of the named target fields. This is generally not what you want when providing a ``default_value``. It is also possible to supply a dictionary of target fields where each target field name is the key and the values are the options for that field. For example, TODO: Document usage on OnSelect with dictionary. Otherwise ``target_field`` is assumed to be a string, and the ``target_fields`` attribute is set to a list of one element. """ self.default_value = default_value self.required = required self.target_field = target_field self.target_values = None self.value = value self.visible = visible if type(target_field) in (list, tuple): self.target_fields = target_field elif type(target_field) is dict: self.target_fields = target_field.keys() self.target_values = target_field else: self.target_fields = [target_field] def __repr__(self): return "<%s %s (%s)>" % (self.__class__.__name__, self.value, self.target_field)
[docs]class Ordering(object): """Encapsulates ordering options for a list."""
[docs] def __init__(self, direction="asc", fields=None, initial_field=None): """Initialize ordering options. :param direction: The default ordering direction; ``asc`` or ``desc``. :type direction: str :param fields: A list of field names that may be used for ordering a list. :type fields: list[str] :param initial_field: The name of the field to by which ordering should be initially provided. :type initial_field: str """ self.direction = direction self.fields = fields or list() self.initial_field = initial_field
[docs] def get_keyword(self, field): """Get the GET keyword and value for the field. :rtype: str """ if field not in self.fields: return "" return "%s=%s" % (field, self.direction)
[docs]class Tab(BaseUtil): """Create tabbed interface for controls."""
[docs] def __init__(self, *fields, classes=None, description=None, icon=None, identifier=None, label=None, **kwargs): """Initialize a tab interface. :param fields: A list of field names to include in the tab. :param classes: The CSS classes to be applied to the fieldset element. :type classes: str :param description: The description appears before fields. :type description: str :param icon: An icon to display with the tab. :type icon: str :param identifier: The unique identifier for the tab. If omitted, it is derived from the label. :type identifier: str :param label: The label of the tab. :type label: str .. tip:: You may supply an class extending :py:class:`InlineModelUI` as the only field. This will cause the inline formset to display as tabbed content. The ``identifier`` is used to match against the formset's prefix, so you may need to specify this manually if the formset is not displayed. """ self.description = description self.fields = list(fields) self.icon = icon self.identifier = identifier or label.replace(" ", "-").lower() self.label = label self._fields = list() if classes is not None: kwargs['class'] = classes super().__init__(**kwargs)
def __iter__(self): return iter(self._fields) def __len__(self): """Required for iteration in templates.""" return len(self._fields) def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.label)
[docs]class Toggle(object): """Toggle the attributes of another field based on the value of a checkbox. For example, imagine a to-do has a notes field that may be populated when *is complete* is checked: .. code-block:: python class TodoUI(ui.ModelUI): # ... controls = { 'is_complete': ui.controls.BooleanControl(toggle=ui.Toggle("close_notes", visible=True)), } # ... """
[docs] def __init__(self, target_field, control_field=None, default_value=None, inverse=False, required=None, visible=None): """Initialize a toggle. :param target_field: The field that is controlled by the toggle. :param control_field: The name of the checkbox field that controls the toggle. This is assigned automatically when using ModelUI. :type control_field: str :param default_value: Set a default value on the target field. :param inverse: For checkboxes that are checked by default, toggle target fields when the box is *unchecked*. :type inverse: bool :param required: Indicates the target field should be required when the checkbox is checked. :type required: bool :param visible: Indicates the target field should be displayed when the checkbox is checked. This attribute may also be given as a string; either "disabled" or "readonly". For the difference between these two, see: https://stackoverflow.com/a/7730719/241720 :type visible: bool | str """ self.control_field = control_field self.default_value = default_value self.inverse = inverse self.required = required self.target_field = target_field self.visible = visible
# def get_form_js(self): # """Get the JavaScript that provides the toggle functionality. # # :rtype: JavaScript # # """ # js = JavaScript() # # identifier = "%s toggle" % self.control_field # template = os.path.join("superdjango", "ui", "js", "toggle.js") # # js.from_template(identifier, template, context={'toggle': self}) # # return js