Source code for superdjango.ui.options.controls.library

# Imports

from django.conf import settings
from django.db import models
from django.contrib.auth import get_user_model
from django.contrib.humanize.templatetags.humanize import intcomma
from django.core.exceptions import FieldDoesNotExist
from django import forms
from django.forms.utils import flatatt
from django.template.defaultfilters import linebreaks, linebreaksbr, truncatechars, yesno
from django.urls import NoReverseMatch
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from json import dumps as json_dumps
import logging
from superdjango.utils import is_bool, is_integer
# import random
from superdjango.assets.library import JavaScript, StyleSheet
from superdjango.assets.utils import python_to_js
from superdjango.forms import AjaxUploadWidget, ChooserWidget, CodeEditorWidget, ColorPickerWidget, MarkdownWidget, ReadOnlyWidget, SlugFromWidget
from superdjango.contrib.i18n.countries import SUB_REGION_COUNTRY_CHOICES, get_country_label, CountryField
from superdjango.contrib.i18n.timezones import COMMON_TIMEZONE_CHOICES
from superdjango.shortcuts import title
from superdjango.ui.constants import USER, VERB
from superdjango.ui.options.utils import Default, HelpText
from superdjango.ui.runtime.data import Datum
import warnings
from .compat import clean_html, markdown

log = logging.getLogger(__name__)

# Exports

__all__ = (
    "factory",
    "AjaxFileControl",
    "BaseControl",
    "BooleanControl",
    "CallableControl",
    "CharControl",
    "ChoiceControl",
    "CountryControl",
    "CodeControl",
    "ColorControl",
    "DateControl",
    "DateTimeControl",
    "DecimalControl",
    "DurationControl",
    "EmailControl",
    "FileControl",
    "FloatControl",
    "ForeignKeyControl",
    "HTMLControl",
    "IconControl",
    "ImageControl",
    "IntegerControl",
    "ManyToManyControl",
    "MarkdownControl",
    "NullBooleanControl",
    "OneToOneControl",
    "PercentageControl",
    "RichTextControl",
    "SlugControl",
    "TextControl",
    "TimeControl",
    "TimeZoneControl",
    "URLControl",
    "UserControl",
)

# Constants

STATIC_URL = settings.STATIC_URL

# Functions


[docs]def factory(field, ui): """Get a control instance for the given field. :param field: The field for which a control is provided. :type field: str | BaseType[BaseControl] | BaseType[Field] :param ui: The model UI instance. :type ui: ModelUI :rtype: BaseType[BaseControl] | None The ``field`` may be given as a field name (string), field instance, or control instance. If the latter is provided, the namespace of the ui is simply assigned to the control. """ from django.contrib.contenttypes.fields import GenericForeignKey if isinstance(field, BaseControl): field.ui = ui return field elif isinstance(field, models.Field): control_class = guess_factory(field) return control_class( default=field.default, field_name=field.name, help_text=field.help_text, label=field.verbose_name, max_length=field.max_length, remote_field=field.remote_field, ui=ui ) elif type(field) is str: try: _field = ui.meta.get_field(field) control_class = guess_factory(_field) return control_class( default=_field.default, field_name=field, help_text=_field.help_text, label=_field.verbose_name, max_length=_field.max_length, remote_field=_field.remote_field, ui=ui ) except FieldDoesNotExist: raise ValueError("%s.%s does not exist." % (ui.meta.model_name, field)) elif isinstance(field, (models.ManyToManyRel, models.ManyToOneRel, models.OneToOneRel)): return None elif isinstance(field, GenericForeignKey): return None else: message = "A field for %s must be a str or instance/class that extends ui.controls.BaseControl, not: %s" raise TypeError(message % (ui.meta.model_name, type(field)))
def guess_factory(field_instance): """Guess the descriptor to use based on the model field. :param field_instance: The field from a model instance. :rtype: class """ from django.contrib.contenttypes.fields import GenericForeignKey # noinspection PyPep8Naming UserModel = get_user_model() # TODO: Provide control for CommaSeparatedIntegerField. # TODO: Provide control for FilePathField. # TODO: Provide control for BinaryField? # TODO: Provide control for UUIDField. # TODO: Provide control for postgres ArrayField. # TODO: Provide control for postgres HStoreField. # TODO: Provide control for postgres JSONField. # TODO: Provide control for postgres RangeField(s). # TODO: Provide support and control for phone number field. # The order in which field classes are checked is significant. if isinstance(field_instance, models.NullBooleanField): warnings.warn("Support for models.NullBooleanField will be removed in SuperDjango 4.2 because Django " "has deprecated NullBooleanField.") return NullBooleanControl elif isinstance(field_instance, models.BooleanField): if field_instance.null: return NullBooleanControl return BooleanControl elif isinstance(field_instance, models.DateTimeField): return DateTimeControl elif isinstance(field_instance, models.DateField): return DateControl elif isinstance(field_instance, models.DecimalField): return DecimalControl elif isinstance(field_instance, models.DurationField): return DurationControl elif isinstance(field_instance, models.EmailField): return EmailControl elif isinstance(field_instance, models.FloatField): return FloatControl elif isinstance(field_instance, models.ImageField): return ImageControl elif isinstance(field_instance, models.FileField): return FileControl elif isinstance(field_instance, (models.GenericIPAddressField, models.IPAddressField)): return IPAddressControl elif isinstance(field_instance, models.OneToOneField): return OneToOneControl elif isinstance(field_instance, models.ForeignKey): if field_instance.remote_field.model == UserModel: return UserControl return ForeignKeyControl elif isinstance(field_instance, GenericForeignKey): return None elif isinstance(field_instance, models.ManyToManyField): return ManyToManyControl elif isinstance(field_instance, models.SlugField): return SlugControl elif isinstance(field_instance, models.TimeField): return TimeControl elif isinstance(field_instance, models.TextField): return TextControl elif isinstance(field_instance, models.URLField): return URLControl elif field_instance.choices: return ChoiceControl elif isinstance(field_instance, (models.IntegerField, models.BigIntegerField, models.PositiveIntegerField, models.PositiveSmallIntegerField)): return IntegerControl elif isinstance(field_instance, models.CharField): return CharControl else: return BaseControl # Classes
[docs]class BaseControl(object): """Base class for describing field/column data.""" # TODO: Distinguish between help_text for form, column/list header, or detail view? # TODO: Include support for disabled fields?
[docs] def __init__(self, align=None, auto_title=True, default=None, empty_value=None, field_name=None, filter_in_place=False, help_text=None, initial=None, input_prefix=None, input_suffix=None, label=None, max_length=None, on_select=None, read_only=False, remote_field=None, ui=None, widget=None): """Initialize a control. :param align: Request that the output be aligned. Requires supporting CSS. :type align: str :param auto_title: Automatically convert the label to title format. :type auto_title: bool :param default: The default value from the model field instance. A :py:class:`Default` instance may also be provided which applies the default post-submit. See ``ModelUI.save_record()``. Note: If the model specifies a default, this is always used. :param empty_value: The default value when a display value, value, or field default is not available. This is useful in views that display data that isn't required and the model field has no default. For example, you might set this to "NA" or "-". :type empty_value: str :param field_name: The name of the field. NOTE: This is auto-populated by the :py:func:`factory` or when the ModelUI initializes controls. :type field_name: str :param help_text: The help text to display for the field. This will override help text set on the model field. This text may be displayed in forms and list, and detail views. When provided as a dictionary the keys should be from the ``USER`` constant and the values should be HelpText instances. See ``get_help_text()``. :type help_text: str | HelpText | dict :param initial: The initial value to use when acquiring the form field. May be specified as any valid Python type for the field, or as a :py:class:`Default` instance. :param input_prefix: The prefix to be displayed as part of the beginning of the form input. :type input_prefix: str :param input_suffix: The suffix to be displayed as part of the end of the form input. :type input_suffix: str :param label: The label for the field. This will override the ``verbose_name`` set on the model field. :type label: str :param read_only: Indicates the value is read only in forms. :type read_only: bool :param remote_field: The value of ``remote_field`` on reference field instances. :param ui: The model UI instance utilizing the control. :type ui: ModelUI :param widget: The form field widget class (or instance) used to display the field in forms. """ self.align = align self.auto_title = auto_title self.default = default self.empty_value = empty_value or "" self.filter_in_place = filter_in_place self.help_text = help_text self.initial = initial self.input_prefix = input_prefix self.input_suffix = input_suffix self.max_length = max_length self.name = field_name self.on_select = on_select self.read_only = read_only self.remote_field = remote_field self.ui = ui self.widget = widget self._label = None default_label = None if field_name is not None: default_label = field_name.replace("_", " ") if label is not None: self._label = label else: self._label = default_label if read_only and widget is None: self.widget = ReadOnlyWidget # Sortation attributes are set automatically. self.sort_enabled = False self.sort_icon = None self.sort_key = None
def __repr__(self): return "<%s %s:%s>" % (self.__class__.__name__, self.name, self.label)
[docs] def get_datum(self, record, request, default=None, url=None): """Get the value of the field in a class wrapper that carries additional attributes. :param record: The current model instance. :param request: The current HTTP request instance. :param default: A default value to display. :param url: The URL to which the value should be linked, if any. :type url: str :rtype: Datum """ value = self.get_value(record, request) return Datum( self.name, value, default=self.empty_value, display_value=self.get_display_value(record, request, url=url), field_type=self.get_type(), help_text=self.get_help_text(request), label=self.label, preview_value=self.get_preview_value(record, request, url=url), **self.get_options(record, request) )
[docs] def get_display_value(self, record, request, url=None): """Get the human-friendliest value that may displayed for the field. :param record: The current model instance. :param request: The current HTTP request instance. :param url: The URL to which the value should be linked, if any. :type url: str :rtype: str """ value = self.get_value(record, request) if url is not None and value is not None: return mark_safe('<a href="%s">%s</a>' % (url, value)) if value is None: value = self.empty_value return str(value)
[docs] def get_field_kwargs(self, model_field, request, record=None): """Get the keyword arguments used to instantiate a field instance for use in a form. :rtype: dict """ kwargs = dict() # noinspection PyNoneFunctionAssignment form_class = self.get_form_class(model_field, request, record=record) if form_class is not None: kwargs['form_class'] = form_class # Deal with help text that is overridden by the control. The str() of HelpText instance is that of form_text, # but this makes it more obvious and explicit. # help_text = self.get_help_text(request) # if str(self.help_text.form_text) != str(model_field.help_text): # kwargs['help_text'] = self.help_text.form_text if self.help_text != model_field.help_text: kwargs['help_text'] = self.help_text if self.initial is not None: if isinstance(self.initial, Default): kwargs['initial'] = self.initial.get(request, record=record) else: kwargs['initial'] = self.initial if self.label != model_field.verbose_name: kwargs['label'] = self.label if self.widget is not None: kwargs['widget'] = self.widget return kwargs
# noinspection PyMethodMayBeStatic
[docs] def get_form_class(self, model_field, request, record=None): """Get the class used for instantiating a field instance for use in a form. Returns ``None`` by default, so child classes should override as needed. """ return None
# noinspection PyMethodMayBeStatic
[docs] def get_form_css(self): """Get the CSS, if any, used for this field in forms. :rtype: StyleSheet | None """ if self.widget is not None: try: # noinspection PyUnresolvedReferences return self.widget.get_css(field_name=self.name) except AttributeError: pass return StyleSheet()
[docs] def get_form_field(self, model_field, request, record=None, **kwargs): """Get the form field instance for a given model field. :param model_field: The model field instance. :param request: The current HTTP request instance. :param record: The current model instance. :returns: A field instance. ``kwargs`` are updated and passed to the ``formfield()`` method of the ``model_field``. This method is called from the ModelUI and performs the following tasks: 1. ``get_field_kwargs()`` is called to populate or update the keyword arguments given to the ``model_field.formfield()`` method. 2. The input prefix and suffix (if present) is added to the form field instance. 3. If ``get_widget_attributes()`` returns a dictionary, the widget attributes of form field instance are updated. """ _kwargs = self.get_field_kwargs(model_field, request, record=record) kwargs.update(_kwargs) form_field = model_field.formfield(**kwargs) if self.input_prefix is not None: form_field.input_prefix = self.input_prefix if self.input_suffix is not None: form_field.input_suffix = self.input_suffix widget_attrs = self.get_widget_attributes(request, record=record) if widget_attrs is not None: form_field.widget.attrs.update(widget_attrs) return form_field
# noinspection PyMethodMayBeStatic
[docs] def get_form_js(self): """Get the JavaScript, if any, used for this field in forms. :rtype: JavaScript | None """ js = JavaScript() if self.widget is not None: try: # noinspection PyUnresolvedReferences js.merge(self.widget.get_js(field_name=self.name)) except AttributeError: pass # if self.toggle is not None: # js.merge(self.toggle.get_form_js()) return js
[docs] def get_help_text(self, request): """Get the help text to display. :param request: The current HTTP request. :rtype: HelpText | None """ if isinstance(self.help_text, HelpText): return self.help_text # When help text is a dictionary, it is expected to be a dictionary of HelpText instances. if type(self.help_text) is dict: if hasattr(request, "is_customer") and request.is_customer and USER.CUSTOMER in self.help_text: return self.help_text[USER.CUSTOMER] if request.user.is_staff and USER.STAFF in self.help_text: return self.help_text[USER.STAFF] if request.user.is_superuser and USER.ROOT in self.help_text: return self.help_text[USER.ROOT] if USER.NORMAL in self.help_text: return self.help_text[USER.NORMAL] if self.help_text is not None: return HelpText(self.help_text) return None
def get_options(self, record, request): if self.align is not None: return {'class': "text-%s" % self.align} return dict() # noinspection PyMethodMayBeStatic,PyUnusedLocal
[docs] def get_preview_value(self, record, request, url=None): """Get the "preview" of the value. By default this ``None`` so templates may test for ``preview_value`` and use it if it exists. Child classes may override to provide a preview if appropriate. :rtype: str | None """ return None
[docs] def get_type(self): """Get the type of control. Used for programmatic or template-based identification. :rtype: str """ base_name = self.__class__.__name__.replace("Control", "").lower() if base_name == "base": base_name = "control" return base_name
[docs] def get_value(self, record, request): """Get the current value of the field from the given record. :param record: The model instance. :param request: The current HTTP request instance. """ try: return getattr(record, self.name) except AttributeError: message = "%s record does not have field: %s" % (self.ui.meta.model_name, self.name) if self.ui.raise_exception: raise AttributeError(message) else: log.warning(message) return None
# noinspection PyMethodMayBeStatic,PyUnusedLocal
[docs] def get_widget_attributes(self, request, record=None): """Get widget attributes based on the configuration of the control. :param record: The model instance. :param request: The current HTTP request instance. :rtype: dict() | None """ if self.on_select is not None: events = list() for event in self.on_select: if len(event.target_fields) > 1: for target_field in event.target_fields: _event = { 'name': target_field, 'value': event.value, } if event.target_values: _event = { 'default_value': event.target_values[target_field].get("default_value", None), 'required': event.target_values[target_field].get("required", None), 'visible': event.target_values[target_field].get("visible", None), } else: _event = { 'name': target_field, 'default_value': event.default_value, 'required': event.required, 'value': event.value, 'visible': event.visible, } events.append(_event) else: _event = { 'name': event.target_field, 'default_value': event.default_value, 'required': event.required, 'value': event.value, 'visible': event.visible, } events.append(_event) return {'data-ui-on-select': json_dumps(events)} return None
@property def is_special(self): """Indicates this is a special control. :rtype: bool """ return False @property def label(self): """Get the label for the control. :rtype: str """ # It is possible (with developer-specified controls) that _label is None at runtime. If so, the field name is # converted to a label. if self._label is not None: label = self._label else: label = self.name.replace("_", " ") if self.auto_title: return title(label) return label @property def type(self): """An alias for ``get_type``.""" return self.get_type()
[docs]class BooleanControl(BaseControl):
[docs] def __init__(self, css_icon=False, graphical_icon=False, toggle=None, **kwargs): self.css_icon = css_icon self.graphical_icon = graphical_icon if type(toggle) in (list, tuple): self.toggle = toggle elif toggle is not None: self.toggle = [toggle] else: self.toggle = None super().__init__(**kwargs)
[docs] def get_display_value(self, record, request, url=None): _value = self.get_value(record, request) if self.css_icon: if _value is True: value = mark_safe('<span class="green"><i class="fas fa-check-circle"></i></span>') else: value = mark_safe('<span class="red"><i class="far fa-times-circle"></i></span>') elif self.graphical_icon: if _value is True: value = mark_safe('<img src="%ssuperdjango/ui/images/true.png" width="20">' % STATIC_URL) else: value = mark_safe('<img src="%ssuperdjango/ui/images/false.png" width="20">' % STATIC_URL) else: value = yesno(self.get_value(record, request)) if url is not None: return mark_safe('<a href="%s">%s</a>' % (url, value)) return value
[docs] def get_form_js(self): js = super().get_form_js() if self.toggle is not None: js.append("js-ui-toggle", url="superdjango/ui/js/toggle.js") return js
def get_options(self, record, request): d = super().get_options(record, request) d['class'] = "text-center" return d
[docs] def get_widget_attributes(self, request, record=None): if self.toggle is None: return None events = list() for toggle in self.toggle: _event = { 'name': toggle.target_field, 'default_value': toggle.default_value, 'inverse': toggle.inverse, 'required': toggle.required, 'visible': toggle.visible, } events.append(_event) return {'data-ui-toggle': json_dumps(events)}
[docs]class CallableControl(BaseControl):
[docs] def get_value(self, record, request): if hasattr(record, self.name): attr = getattr(record, self.name) # It's a method. if callable(attr): method = getattr(record, self.name) try: return method(request) except TypeError: return method() # It's probably a property. return attr log.debug("%s is not callable or does not exist on record: %s" % (self.name, record.__class__.__name__)) return None
@property def is_special(self): return True
[docs]class CharControl(BaseControl): """A control for varchar fields."""
[docs] def __init__(self, counter_enabled=False, truncate=None, **kwargs): """Initialize a char field control. :param counter_enabled: Indicates a character/word counter should be displayed. :type counter_enabled: bool :param truncate: The number of characters to which the text should be truncated for the field's preview value. :type truncate: int """ self.counter_enabled = counter_enabled self.truncate = truncate super().__init__(**kwargs)
[docs] def get_form_js(self): js = super().get_form_js() if self.counter_enabled: js.append("js-ui-character-counter", url="superdjango/ui/js/character_counter.js") # if self.counter_enabled: # context = { # 'field_name': self.name, # 'max_length': self.max_length, # } # template = "superdjango/ui/js/max_length.js" # js.from_template("%s counter" % self.name, template, context=context) return js
[docs] def get_preview_value(self, record, request, url=None): """Get the preview value of the rendered value.""" value = self.get_value(record, request) if not value: return None if self.truncate is not None: value = truncatechars(value, self.truncate) if url is not None: return mark_safe('<a href="%s">%s</a>' % (url, value)) return value
[docs] def get_widget_attributes(self, request, record=None): if self.counter_enabled: return {'data-ui-character-counter': 1} return None
[docs]class ChoiceControl(BaseControl):
[docs] def get_display_value(self, record, request, url=None): # noinspection PyProtectedMember field = record._meta.get_field(self.name) # noinspection PyProtectedMember value = record._get_FIELD_display(field) if url is not None: return mark_safe('<a href="%s">%s</a>' % (url, value)) return value
[docs] def get_form_js(self): js = super().get_form_js() # if self.on_select is not None: # js.merge(self.on_select.get_form_js()) js.append("on select", url="superdjango/ui/js/on_select.js") return js
[docs]class ColorControl(BaseControl): """A control for implementing a color-picker."""
[docs] def __init__(self, input_format="css", input_type="wcp", **kwargs): self.input_format = input_format self.input_type = input_type kwargs.setdefault("widget", ColorPickerWidget) super().__init__(**kwargs)
# def get_form_css(self): # css = super().get_form_css() # if self.input_type == "wcp": # css.append("jquery-wheelcolorpicker", url="bundled/wcp/css/wheelcolorpicker.css") # # return css # # def get_form_js(self): # js = super().get_form_js() # if self.input_type == "wcp": # js.append("jquery-wheelcolorpicker", url="bundled/wcp/jquery.wheelcolorpicker.js") # content = '$("#id_%s").wheelColorPicker({format: "%s"});' % (self.name, self.input_format) # js.append("jquery-wheelcolorpicker %s" % self.name, content=content) # # return js
[docs] def get_widget_attributes(self, request, record=None): return { 'data-ui-color-picker': self.input_format, }
[docs]class DateControl(BaseControl):
[docs] def __init__(self, increment=None, mask="%b %d, %Y", **kwargs): """Initialize a date control :param increment: The increment options for the date, which uses JavaScript to populate the field based on the value entered in another field. :type increment: DateIncrement :param mask: The `strftime() <https://docs.python.org/3.7/library/datetime.html#strftime-strptime-behavior>_` specification which should be used to format dates. :type mask: str """ self.increment = increment self.mask = mask super().__init__(**kwargs)
[docs] def get_form_css(self): """Get style for the datepicker.""" css = super().get_form_css() css.append("datepicker", url="bundled/datepicker/datepicker.css") return css
[docs] def get_display_value(self, record, request, url=None): value = self.get_value(record, request) if value is not None: # noinspection PyUnresolvedReferences return value.strftime(self.mask) return ""
[docs] def get_form_js(self): """Get JavaScript for the datepicker.""" js = super().get_form_js() js.append("datepicker", url="bundled/datepicker/datepicker.min.js") if self.increment is not None: js.append("moment", url="bundled/momentjs/moment.js") js.append("ui-datetime", url="superdjango/ui/js/datetime.js") # content = "$('[data-toggle=\"datepicker\"]').datepicker({format: 'yyyy-mm-dd'});" # js.append("datepicker-data-toggle", content=content) # # if self.increment is not None: # if self.increment.target_field is None: # self.increment.target_field = self.name # # js.merge(self.increment.get_form_js()) return js
[docs] def get_widget_attributes(self, request, record=None): d = { 'data-toggle': "datepicker", 'data-ui-datepicker-format': "yyyy-mm-dd", } if self.increment is not None: d['data-ui-date-increment'] = json_dumps({ 'always': self.increment.always, 'days': self.increment.days, 'source_field': self.increment.source_field, }) # print(self.name, "widget attrs", d) return d
[docs]class DateTimeControl(BaseControl): """BaseControl over datetime fields. For forms, this control makes use of Django's ``forms.SplitDateTimeWidget`` to create two separate inputs; one for the date, the other for the time. Two jQuery plugins are used to provide date and time lookups. .. warning:: Changing the ``date_format`` and ``time_format`` is programmatically possible, but not currently supported by the underlying template or the JavaScript. """
[docs] def __init__(self, date_format="%Y-%m-%d", increment=None, mask="%b %d, %Y %I:%M %p", time_format="%I:%M%p", **kwargs): self.date_format = date_format self.increment = increment self.mask = mask self.time_format = time_format super().__init__(**kwargs)
[docs] def get_display_value(self, record, request, url=None): value = self.get_value(record, request) # TODO: Date/time display with mask support does not account for user's timezone an requests are't currently # passed to control instances. if value is not None: # noinspection PyUnresolvedReferences return value.strftime(self.mask) return ""
[docs] def get_field_kwargs(self, model_field, request, record=None): """Override to provide input date and time formats.""" kwargs = super().get_field_kwargs(model_field, request, record=record) kwargs['input_date_formats'] = [self.date_format] kwargs['input_time_formats'] = [self.time_format] return kwargs
[docs] def get_form_class(self, model_field, request, record=None): """Override to return ``forms.SplitDateTimeField``.""" return forms.SplitDateTimeField
[docs] def get_form_css(self): """Get style for the datepicker.""" css = StyleSheet() css.append("datepicker", url="bundled/datepicker/datepicker.css") css.append("timepicker", url="bundled/timepicker/jquery.timepicker.css") return css
[docs] def get_form_js(self): """Get JavaScript for the datepicker.""" js = super().get_form_js() js.append("datepicker", url="bundled/datepicker/datepicker.min.js") # content = "$('[data-toggle=\"datepicker\"]').datepicker({format: 'yyyy-mm-dd'});" # js.append("datepicker-data-toggle", content=content) js.append("timepicker", url="bundled/timepicker/jquery.timepicker.js") # content = "$('[data-toggle=\"timepicker\"]').timepicker({'scrollDefault': 'now'});" # js.append("timepicker-data-toggle", content=content) if self.increment is not None: js.append("moment", url="bundled/momentjs/moment.js") js.append("ui-datetime", url="superdjango/ui/js/datetime.js") return js
[docs] def get_widget_attributes(self, request, record=None): # widget attributes are not output by widget rendering. data-toggle added by splitdatetime template. d = { 'data-ui-datepicker-format': "yyyy-mm-dd", 'data-ui-timepicker-scroll': "now", } if self.increment is not None: # Source field is part of split date time which adds a _0 suffix. source_field = self.increment.source_field + "_0" d['data-ui-date-increment'] = json_dumps({ 'always': self.increment.always, 'days': self.increment.days, 'format': "YYYY-MM-DD", 'source_field': source_field, }) return d
[docs]class DecimalControl(BaseControl): """Control for decimal fields."""
[docs] def get_display_value(self, record, request, url=None): value = self.get_value(record, request) if not value: return "" number, places = str(value).split(".") return "%s.%s" % (intcomma(number), places)
[docs]class DurationControl(BaseControl): """Describes a duration field."""
[docs] def get_display_value(self, record, request, url=None): value = self.get_value(record, request) if not value: return "" total_seconds = value.total_seconds() hours, remainder = divmod(total_seconds, 3600) minutes, seconds = divmod(remainder, 60) s = list() if hours > 0: s.append(str(int(hours))) if hours == 1: s.append(str(_("hour"))) else: s.append(str(_("hours"))) if minutes > 0: s.append(str(int(minutes))) if minutes == 1: s.append(str(_("minute"))) else: s.append(str(_("minutes"))) return " ".join(s)
[docs]class EmailControl(BaseControl): """Describes an email field."""
[docs] def __init__(self, link_enabled=True, obfuscate=False, **kwargs): self.link_enabled = link_enabled self.obfuscate = obfuscate super().__init__(**kwargs)
[docs] def get_display_value(self, record, request, url=None): value = self.get_value(record, request) if value is None: return str(self.empty_value) if self.link_enabled: mailto = "mailto:%s" % value if self.obfuscate: mailto = self._obfuscate(mailto) return mark_safe('<a href="%s">%s</a>' % (mailto, value)) return value
@staticmethod def _obfuscate(value): # ascii_char_list = list(ord(char) for char in list(value)) # transform = lambda s: random.choice(('&#' + str(s) + ';', str(hex(s)).replace('0', '&#', 1) + ';')) # return "".join(list(map(transform, ascii_char_list))) return ''.join('&#{};'.format(ord(c)) for c in value)
class FileControl(BaseControl): """A control for file fields.""" def __init__(self, download_enabled=False, extensions=None, **kwargs): """Initialize a file control. :param download_enabled: Indicates the file may be downloaded by users. :type download_enabled: bool :param extensions: A list of allowed extensions. ``None`` indicates all extensions are allowed. :type extensions: list[str] | None """ self.download_enabled = download_enabled self.extensions = extensions super().__init__(**kwargs) class FloatControl(BaseControl): """A control for a float field.""" def __init__(self, rounding=None, **kwargs): """Initialize the control. :param rounding: Round the number to the given decimal places. :type rounding: int """ self.rounding = rounding super().__init__(**kwargs) def get_display_value(self, record, request, url=None): """Display the float as a rounded number, if provided.""" value = self.get_value(record, request) if not value: return self.empty_value if self.rounding: value = round(value, self.rounding) if url is not None: return mark_safe('<a href="%s">%s</a>' % (url, value)) return str(value)
[docs]class ForeignKeyControl(BaseControl): """Describes a foreign key field."""
[docs] def __init__(self, input_type="select2", limit_choices_to=None, link_enabled=True, link_to=VERB.DETAIL, **kwargs): """Initialize a control for a foreign key field. :param input_type: The type of input to display; ``chooser``, ``select2``, or ``None`` for a standard dropdown menu. Note that ``chooser`` and ``select2`` requires additional options on the *target* model UI. :type input_type: str | None :param limit: The query limit to be applied when acquiring the display value. :type limit: int :param limit_choices_to: Used when building the model form, this is a dictionary or callable that may be used to filter the queryset. When a callable is supplied it must accept a ``request`` arg and ``record=None`` kwarg. It must also return a dictionary or ``None``. :type limit_choices_to: callable | dict :param link_enabled: Indicates links to the should be enabled to the referenced record. :type link_enabled: bool :param link_to: The verb (view name) to which a link should be included. :type link_to: str """ # :param criteria: Criteria to be applied when acquiring the display value. # :type criteria: dict # :param utility: The display utility to use; ``multiselectjs`` or ``select2. # :type utility: str self.input_type = input_type self.limit_choices_to = limit_choices_to self.link_enabled = link_enabled self.link_to = link_to if self.input_type == "chooser": kwargs['widget'] = ChooserWidget super().__init__(**kwargs)
[docs] def get_field_kwargs(self, model_field, request, record=None): kwargs = super().get_field_kwargs(model_field, request, record=record) criteria = self.ui.get_limit_choices_to(self, request, record=record) kwargs['queryset'] = self.ui.get_field_queryset(model_field, request, criteria=criteria) return kwargs
[docs] def get_form_css(self): """Provide support for chooser and select2.""" css = super().get_form_css() if self.input_type == "chooser": pass elif self.input_type == "select2": css.append("select2", url="bundled/select2/css/select2.css") else: pass return css
[docs] def get_display_value(self, record, request, url=None): value = self.get_value(record, request) if not value: return "" try: display_name = value.get_display_name() except AttributeError: display_name = str(value) if self.link_enabled: if url is None: url = self._get_url(value) if url is not None: return mark_safe('<a href="%s">%s</a>' % (url, display_name)) return display_name
[docs] def get_form_js(self): """Provide support for chooser and select2.""" js = super().get_form_js() # if self.on_select is not None: # js.merge(self.on_select.get_form_js()) if self.input_type == "chooser": js.merge(self._get_chooser_js()) elif self.input_type == "select2": js.merge(self._get_select2_js()) else: pass return js
def _get_chooser_js(self): """Provide support for chooser.""" js = super().get_form_js() # The chooser URL may be local or remote to the current app. url = None if self.ui.site is not None: url = self.ui.site.get_url(self._get_dotted_path(), VERB.AJAX_CHOOSER) if url is None: url = self.ui.get_url(VERB.AJAX_CHOOSER) if url is None: return js if self.limit_choices_to: if "?" in url: url += "&criteria=" else: url += "?criteria=" criteria = list() for key, value in self.limit_choices_to.items(): criteria.append("%s:%s" % (key, value)) url += ",".join(criteria) if "?" in url: url += "&output_format=html" else: url += "?output_format=html" context = { 'field': self.name, 'url': url, } content = """ $(document).ready(function() { $("#id_%(field)s_link").on("click", function() { var url = "%(url)s"; $("#id_%(field)s_modal_body").load(url, function() { $("#id_%(field)s_modal").modal({show: true}); }); return false; }); }); $("body").on("click", ".choose-record", function(){ var pk = $(this).attr("data-id"); //alert("picked " + pk); $("#id_%(field)s_modal").modal("hide"); $("#id_%(field)s").val(pk); return false; }); """ % context js.append("%s chooser" % self.name, content=content) return js def _get_dotted_path(self): """Get the dotted path of the model. :rtype: str """ # noinspection PyProtectedMember return "%s.%s" % (self.remote_field.model._meta.app_label, self.remote_field.model._meta.model_name) def _get_model(self): """Get the app label and model name for the remote field. :rtype: tuple(str, str) """ # noinspection PyProtectedMember return self.remote_field.model._meta.app_label, self.remote_field.model._meta.model_name def _get_select2_js(self): """Provide support for select2.""" js = super().get_form_js() # The auto-complete URL may be local or remote to the current app. url = None if self.ui.site is not None: # url = self.ui.site.get_url("%s.%s" % (remote_app_label, remote_model_name), "ajax_auto_complete") url = self.ui.site.get_url(self._get_dotted_path(), "ajax_auto_complete") # noinspection DuplicatedCode if url is None: url = self.ui.get_url("ajax_auto_complete") if url is None: return js if self.limit_choices_to: if "?" in url: url += "&criteria=" else: url += "?criteria=" criteria = list() for key, value in self.limit_choices_to.items(): criteria.append("%s:%s" % (key, value)) url += ",".join(criteria) js.append("select2", url="bundled/select2/js/select2.js") context = { 'selector': "#id_%s" % self.name, 'url': url, } template = "superdjango/ui/js/auto_complete.js" js.from_template("%s select2" % self.name, template, context=context) return js def _get_url(self, record): dotted = ".".join(self._get_model()) if self.ui.site is not None: return self.ui.site.get_url(dotted, self.link_to, record=record) try: return record.get_absolute_url() except AttributeError: pass return None
[docs]class ImageControl(BaseControl): """A control for image fields."""
[docs] def __init__(self, height=None, width=None, **kwargs): self.height = height self.width = width super().__init__(**kwargs)
[docs] def get_datum(self, record, request, default=None, url=None): value = self.get_value(record, request) if not value: return Datum( self.name, None, default=self.empty_value, display_value="", field_type=self.get_type(), help_text=self.help_text, label=self.label, preview_value="", **self.get_options(record, request) ) datum = super().get_datum(record, request, default=default, url=url) datum.attributes['url'] = datum.value.url return datum
[docs] def get_display_value(self, record, request, url=None): value = self.get_value(record, request) if not value: return "" try: attrs = { 'src': value.url, } except ValueError: return "" if self.align is not None: attrs['class'] = self.align if self.height is not None: attrs['height'] = self.height if self.width is not None: attrs['width'] = self.width html = '<img %s>' % flatatt(attrs) if url is not None: html = '<a href="%s" target="_blank">%s</a>' % (url, html) return mark_safe(html)
[docs]class IntegerControl(BaseControl): """A control for integer fields."""
[docs] def get_display_value(self, record, request, url=None): value = self.get_value(record, request) if not value: return "" return str(value)
class IPAddressControl(BaseControl): """A control for IP addresses.""" def get_form_js(self): """Add jquery and ui mask.""" js = super().get_form_js() js.append("jquery-mask", url="bundled/mask/jquery.mask.min.js") js.append("ui-mask", url="superdjango/ui/js/mask.js") return js def get_widget_attributes(self, request, record=None): """Add data-mask.""" return {'data-mask': "ip-address"}
[docs]class ManyToManyControl(BaseControl): """Describes a many to many field."""
[docs] def __init__(self, criteria=None, input_type=None, limit=3, limit_choices_to=None, limit_continuation="...", link_enabled=True, link_to="detail", **kwargs): """Initialize a control for a many to many field. :param criteria: Criteria to be applied when acquiring the display value. :type criteria: dict :param input_type: Specify an advanced input type: ``multiselectjs`` or ``select2``. :type input_type: str :param limit: The query limit to be applied when acquiring the display value. :type limit: int :param limit_choices_to: Used when building the model form, this is a dictionary or callable that may be used to filter the queryset. When a callable is supplied it must accept a ``request`` arg and ``record=None`` kwarg. It must also return a dictionary or ``None``. :type limit_choices_to: callable | dict :param limit_continuation: The string to display when the display limit is applied. :type limit_continuation: str :param link_enabled: Indicates links to the should be enabled to the referenced record. :type link_enabled: bool :param link_to: The verb (view name) to which a link should be included. :type link_to: str """ self.criteria = criteria self.input_type = input_type self.limit = limit self.limit_choices_to = limit_choices_to self.limit_continuation = limit_continuation self.link_enabled = link_enabled self.link_to = link_to super().__init__(**kwargs)
[docs] def get_datum(self, record, request, default=None, url=None): """Override to deal with empty queries.""" datum = super().get_datum(record, request, default=default, url=url) if datum.display_value == "": datum.value = None datum.preview_value = "-" return datum
[docs] def get_display_value(self, record, request, url=None): # Note: The value is a ManyRelatedManager. value = self.get_value(record, request) if self.criteria is not None: qs = value.filter(**self.criteria) else: qs = value.all() if not qs.exists(): return "" a = list() count = 0 for foreign_record in qs: count += 1 if self.limit is not None and count > self.limit: a.append(self.limit_continuation) break try: display_name = foreign_record.get_display_name() except AttributeError: display_name = str(foreign_record) if self.link_enabled: url = self._get_url(foreign_record) if url is not None: link = '<a href="%s">%s</a>' % (url, display_name) a.append(link) else: a.append(display_name) else: a.append(display_name) return mark_safe((", ".join(a)))
[docs] def get_field_kwargs(self, model_field, request, record=None): kwargs = super().get_field_kwargs(model_field, request, record=record) criteria = self.ui.get_limit_choices_to(self, request, record=record) kwargs['queryset'] = self.ui.get_field_queryset(model_field, request, criteria=criteria) return kwargs
[docs] def get_form_css(self): if self.input_type == "select2": return self._get_select2_css() elif self.input_type == "multiselectjs": return self._get_multiselectjs_css() else: return None
[docs] def get_form_js(self): if self.input_type == "select2": return self._get_select2_js() elif self.input_type == "multiselectjs": return self._get_multiselectjs_js() else: return None
def _get_model(self): """Get the app label and model name for the remote field. :rtype: tuple(str, str) """ # noinspection PyProtectedMember return self.remote_field.model._meta.app_label, self.remote_field.model._meta.model_name # noinspection PyMethodMayBeStatic def _get_multiselectjs_css(self): """Provide support for multiselectjs.""" css = super().get_form_css() css.append("multiselect.js", url="bundled/multiselectjs/css/multi-select.css") return css def _get_multiselectjs_js(self): js = super().get_form_js() js.append("multiselect.js", url="bundled/multiselectjs/js/jquery.multi-select.js") # BUG: multiselect with search fails with a js error. # content = """$('#id_%s').multiSelect({ # selectableHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='try \"12\"'>", # selectionHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='try \"4\"'>", # afterInit: function(ms){ # var that = this, # $selectableSearch = that.$selectableUl.prev(), # $selectionSearch = that.$selectionUl.prev(), # selectableSearchString = '#'+that.$container.attr('id')+' .ms-elem-selectable:not(.ms-selected)', # selectionSearchString = '#'+that.$container.attr('id')+' .ms-elem-selection.ms-selected'; # # that.qs1 = $selectableSearch.quicksearch(selectableSearchString) # .on('keydown', function(e){ # if (e.which === 40){ # that.$selectableUl.focus(); # return false; # } # }); # # that.qs2 = $selectionSearch.quicksearch(selectionSearchString) # .on('keydown', function(e){ # if (e.which == 40){ # that.$selectionUl.focus(); # return false; # } # }); # }, # afterSelect: function(){ # this.qs1.cache(); # this.qs2.cache(); # }, # afterDeselect: function(){ # this.qs1.cache(); # this.qs2.cache(); # } # }); # """ % self.name content = '$("#id_%s").multiSelect();' % self.name js.append("multiselect.js %s" % self.name, content=content) return js # noinspection PyMethodMayBeStatic def _get_select2_css(self): """Provide support for select2.""" css = super().get_form_css() css.append("select2", url="bundled/select2/css/select2.css") return css def _get_select2_js(self): """Provide support for select2.""" js = super().get_form_js() # noinspection PyProtectedMember remote_app_label, remote_model_name = self._get_model() # The auto-complete URL may be local or remote to the current app. url = None if self.ui.site is not None: url = self.ui.site.get_url("%s.%s" % (remote_app_label, remote_model_name), "ajax_auto_complete") # noinspection DuplicatedCode if url is None: url = self.ui.get_url("ajax_auto_complete") if url is None: return js js.append("select2", url="bundled/select2/js/select2.js") context = { 'selector': "#id_%s" % self.name, 'url': url, } template = "superdjango/ui/js/auto_complete.js" js.from_template("%s select2" % self.name, template, context=context) return js def _get_url(self, record): dotted = ".".join(self._get_model()) if self.ui.site is not None: return self.ui.site.get_url(dotted, self.link_to, record=record) try: return record.get_absolute_url() except AttributeError: pass return None
[docs]class NullBooleanControl(BaseControl): """Describes a null-boolean field."""
[docs] def __init__(self, css_icon=False, graphical_icon=False, **kwargs): self.css_icon = css_icon self.graphical_icon = graphical_icon super().__init__(**kwargs)
[docs] def get_display_value(self, record, request, url=None): _value = self.get_value(record, request) if self.css_icon: if _value is True: value = mark_safe('<span class="green"><i class="fas fa-check-circle"></i></span>') elif _value is False: value = mark_safe('<span class="red"><i class="far fa-times-circle"></i></span>') else: value = mark_safe('<span class="orange"><i class="far fa-minus-square"></i></span>') elif self.graphical_icon: if _value is True: value = mark_safe('<img src="%ssuperdjango/ui/images/true.png" width="20">' % STATIC_URL) elif _value is False: value = mark_safe('<img src="%ssuperdjango/ui/images/false.png" width="20">' % STATIC_URL) else: value = mark_safe('<img src="%ssuperdjango/ui/images/maybe.png" width="20">' % STATIC_URL) else: value = yesno(self.get_value(record, request)) if url is not None: return mark_safe('<a href="%s">%s</a>' % (url, value)) return value
[docs]class OneToOneControl(BaseControl): """Describes a one to one field."""
[docs] def __init__(self, link_enabled=False, **kwargs): self.link_enabled = link_enabled super().__init__(**kwargs)
[docs] def get_display_value(self, record, request, url=None): value = self.get_value(record, request) if not value: return "" try: display_name = value.get_display_name() except AttributeError: display_name = str(value) if self.link_enabled: if url is None: try: url = value.get_absolute_url() except (AttributeError, NoReverseMatch): pass if url is not None: return mark_safe('<a href="%s">%s</a>' % (url, display_name)) return display_name
[docs]class SlugControl(BaseControl):
[docs] def __init__(self, from_field="title", **kwargs): self.from_field = from_field kwargs['widget'] = SlugFromWidget(from_field=from_field) super().__init__(**kwargs)
# def get_form_js(self): # js = super().get_form_js() # # if self.from_field is None: # return js # # # noinspection PyProtectedMember # if not self.ui._field_exists(self.from_field): # return js # # js.append("slugify", url="superdjango/ui/js/slugify.js") # # context = { # 'from_selector': "#id_%s" % self.from_field, # 'to_selector': "#id_%s" % self.name, # } # identifier = "%s slugify" % self.name # template = "superdjango/ui/js/slugify.js" # js.from_template(identifier, path=template, context=context) # # return js class TagControl(BaseControl): pass
[docs]class TextControl(BaseControl): """Additional control over text fields.""" OUTPUT_LINE_BREAKS = "br" OUTPUT_PLAIN = "plain" OUTPUT_PARAGRAPHS = "paragraph"
[docs] def __init__(self, collapse=False, collapse_label=None, counter_enabled=False, output_format=OUTPUT_PLAIN, preview_lines=1, truncate=None, uncollapse_label=None, **kwargs): """Initialize a text field control. :param collapse: Indicates whether collapse functionality should be enabled. :type collapse: bool :param collapse_label: The label for showing less text. Default: ``_("Less")``. Not necessarily supported by all frameworks. :type collapse_label: str :param counter_enabled: Indicates a character/word counter should be displayed. :type counter_enabled: bool :param output_format: The output format of the text; ``OUTPUT_LINE_BREAKS``, ``OUTPUT_PLAIN``, ``OUTPUT_PARAGRAPHS``. Default: ``OUTPUT_PLAIN`` :type output_format: str :param preview_lines: The number of lines to display for the field's preview value. :type preview_lines: int :param programming_language: For ``OUTPUT_CODE``, the programming language to use for highlighting. :type programming_language: str :param truncate: For ``OUTPUT_PLAIN``, the number of characters to which the text should be truncated for the field's preview value. :type truncate: int :param uncollapse_label: The label for showing less text. Default: ``_("More")``. Not necessarily supported by all frameworks. :type uncollapse_label: str """ self.collapse = collapse self.collapse_label = collapse_label or _("less") self.counter_enabled = counter_enabled self.output_format = output_format self.preview_lines = preview_lines self.truncate = truncate self.uncollapse_label = uncollapse_label or _("more") super().__init__(**kwargs)
[docs] def get_display_value(self, record, request, url=None): """Get rendered value of the text field.""" value = self.get_value(record, request) if not value: return "" if self.output_format == self.OUTPUT_LINE_BREAKS: return linebreaksbr(value) elif self.output_format == self.OUTPUT_PARAGRAPHS: html = linebreaks(value) return mark_safe(clean_html(html, tags=['p'])) else: return value
[docs] def get_form_js(self): js = super().get_form_js() if self.counter_enabled: js.append("js-ui-character-counter", url="superdjango/ui/js/character_counter.js") # context = { # 'field_name': self.name, # 'max_length': self.max_length, # } # # if self.max_length: # template = "superdjango/ui/js/max_length.js" # else: # template = "superdjango/ui/js/character_counter.js" # # js.from_template("%s counter" % self.name, template, context=context) return js
[docs] def get_preview_value(self, record, request, url=None): """Get the preview of the rendered value.""" value = self.get_value(record, request) if not value: return None lines = value.split("\n") line = "\n".join(lines[:self.preview_lines]) if self.truncate is not None: return truncatechars(line, self.truncate) return line
[docs] def get_widget_attributes(self, request, record=None): if self.counter_enabled: return {'data-ui-character-counter': 1} return None
[docs]class TimeControl(BaseControl): """Control over time fields. For forms, this control makes use of Django's ``forms.SplitDateTimeWidget`` to create two separate inputs; one for the date, the other for the time. Two jQuery plugins are used to provide date and time lookups. .. warning:: Changing the ``time_format`` is programmatically possible, but not currently supported by the underlying template or the JavaScript. """
[docs] def __init__(self, mask="%I:%M %p", time_format="%I:%M%p", **kwargs): self.mask = mask self.time_format = time_format # self.widget = forms.TimeInput(format=time_format) super().__init__(**kwargs)
[docs] def get_display_value(self, record, request, url=None): value = self.get_value(record, request) if value is not None: # noinspection PyUnresolvedReferences return value.strftime(self.mask) return ""
[docs] def get_field_kwargs(self, model_field, request, record=None): kwargs = super().get_field_kwargs(model_field, request, record=record) kwargs['input_formats'] = [self.time_format] return kwargs
[docs] def get_form_css(self): """Get style for the datepicker.""" css = StyleSheet() css.append("timepicker", url="bundled/timepicker/jquery.timepicker.css") return css
[docs] def get_form_js(self): """Get JavaScript for the datepicker.""" js = super().get_form_js() js.append("timepicker", url="bundled/timepicker/jquery.timepicker.js") content = "$('[data-toggle=\"timepicker\"]').timepicker({'scrollDefault': 'now'});" js.append("timepicker-data-toggle", content=content) return js
[docs]class URLControl(BaseControl): """Describes a URL field."""
[docs] def __init__(self, link_enabled=True, link_text=None, target="_blank", **kwargs): self.link_enabled = link_enabled self.link_text = link_text self.target = target super().__init__(**kwargs)
[docs] def get_display_value(self, record, request, url=None): value = self.get_value(record, request) if not value: return "" if self.link_enabled: link_text = value if self.link_text is not None: link_text = self.link_text return mark_safe('<a href="%s" target="%s">%s</a>' % (value, self.target, link_text)) return value
# Extended Controls class AjaxFileControl(FileControl): """A file control which facilitates AJAX uploads.""" def __init__(self, **kwargs): kwargs['widget'] = AjaxUploadWidget super().__init__(**kwargs) def get_form_css(self): css = super().get_form_css() css.append("%s ajax upload" % self.name, url="/_assets/bundled/fileuploader/css/fileuploader.css") return css def get_form_js(self): js = super().get_form_js() js.append("%s ajax upload" % self.name, url="/_assets/bundled/fileuploader/js/fileuploader.js") # This won't work because csrf_token is not available here. # content = """ # $(function(){ # var uploader = new qq.FileUploader({ # action: "/ajax/upload/", # element: $('#file-uploader-%s')[0], # multiple: true, # onComplete: function(id, fileName, responseJSON) { # if (responseJSON.success) { # alert("success!"); # $("#file-uploader-%s-field").value = responseJSON['path']; # } # else { # alert("upload failed!"); # } # }, # onAllComplete: function(uploads) { # // uploads is an array of maps # // the maps look like this: {file: FileObject, response: JSONServerResponse} # alert("All complete!"); # }, # params: { # 'csrf_token': '{{ csrf_token }}', # 'csrf_name': 'csrfmiddlewaretoken', # 'csrf_xname': 'X-CSRFToken', # }, # }); # }); # """ % (self.name, self.name) # # js.append("%s ajax upload element" % self.name, content=content) return js
[docs]class CodeControl(TextControl): """Display a text field as highlighted source code."""
[docs] def __init__(self, language="python", theme="monokai", **kwargs): self.language = language self.theme = theme kwargs['widget'] = CodeEditorWidget super().__init__(**kwargs)
[docs] def get_display_value(self, record, request, url=None): value = self.get_value(record, request) if not value: return "" # TODO: Utilize programming_language and pygments to render code output. # http://pygments.org/docs/ return format_html('<pre><code class="highlight {}">{}</code></pre>', self.language, value)
[docs] def get_form_js(self): js = super().get_form_js() # js.append("ace editor", url="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.6/ace.js") # js.append("ace editor", content='<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.6/ace.js" # type="text/javascript" charset="utf-8"</script>', wrap=False) js.append( "ace editor", content='<script src="/_assets/bundled/ace/ace.js" type="text/javascript" charset="utf-8"></script>', wrap=False ) a = list() # a.append('var %s_textarea = $("#id_%s").hide();' % (self.name, self.name)) a.append('var %s_editor = ace.edit("%s_editor");' % (self.name, self.name)) a.append('%s_editor.setTheme("ace/theme/%s");' % (self.name, self.theme)) a.append('%s_editor.session.setMode("ace/mode/%s");' % (self.name, self.language)) a.append("%s_editor.getSession().on('change', function(){" % self.name) a.append(' $("#id_%s").val(%s_editor.getSession().getValue());' % (self.name, self.name)) a.append('});') # var editor = ace.edit("editor"); # editor.setTheme("ace/theme/monokai"); # editor.session.setMode("ace/mode/javascript"); # editor.getSession().on('change', function(){ # textarea.val(editor.getSession().getValue()); # }); js.append("%s editor" % self.name, content="\n".join(a)) return js
[docs] def get_preview_value(self, record, request, url=None): value = self.get_value(record, request) if not value: return None lines = value.split("\n") line = "\n".join(lines[:self.preview_lines]) return format_html('<pre><code class="highlight {}">{}</code></pre>', self.language, line)
class CountryControl(ChoiceControl): """A control for selecting a country.""" def __init__(self, choices=SUB_REGION_COUNTRY_CHOICES, **kwargs): self.choices = choices super().__init__(**kwargs) def get_display_value(self, record, request, url=None): """We assume that country may be added to the form without choices, so we override ChoiceControl's acquisition of the value to manually lookup the country label based on the code provided to the record. """ value = self.get_value(record, request) if not value: return self.empty_value _value = get_country_label(value) if url is not None: return mark_safe('<a href="%s">%s</a>' % (url, _value)) return _value def get_field_kwargs(self, model_field, request, record=None): """Override to include choices.""" kwargs = super().get_field_kwargs(model_field, request, record=record) kwargs['choices'] = self.choices return kwargs def get_form_class(self, model_field, request, record=None): """Override to return ``CountryField``.""" return CountryField
[docs]class HTMLControl(TextControl): """Display a text field as HTML.""" ALLOWED_ATTRIBUTES = { 'a': ["href", "title"], 'img': ["alt", "src", "title"], } ALLOWED_TAGS = [ 'a', 'b', 'blockquote', 'em', 'h1', 'h2', 'h3', 'h4', 'i', 'img', 'li', 'ol', 'p', 'strong', 'u', 'ul', ]
[docs] def get_display_value(self, record, request, url=None): value = self.get_value(record, request) if not value: return "" return mark_safe(clean_html(value, tags=self.ALLOWED_TAGS, attributes=self.ALLOWED_ATTRIBUTES))
[docs] def get_preview_value(self, record, request, url=None): value = self.get_value(record, request) if not value: return None lines = value.split("\n") line = "\n".join(lines[:self.preview_lines]) # TODO: Bleach supposedly closes tags, so it should work for partial output. This needs to be tested. return mark_safe(clean_html(line, tags=self.ALLOWED_TAGS, attributes=self.ALLOWED_ATTRIBUTES))
[docs]class IconControl(CharControl): """Display an icon."""
[docs] def __init__(self, library="fontawesome", **kwargs): self.library = library super().__init__(**kwargs)
[docs] def get_display_value(self, record, request, url=None): value = self.get_value(record, request) if not value: return "" if self.library == "fontawesome": if url: return mark_safe('<a href="%s"><i class="%s"></i></a>' % (url, value)) else: return mark_safe('<i class="%s"></i>' % value) return value
[docs] def get_preview_value(self, record, request, url=None): value = self.get_value(record, request) if not value: return "" if self.library == "fontawesome": if url: return mark_safe('<a href="%s"><i class="%s"></i></a>' % (url, value)) else: return mark_safe('<i class="%s"></i>' % value) return value
[docs]class MarkdownControl(TextControl): """Display a text field as Markdown.""" ALLOWED_ATTRIBUTES = { 'a': ["href", "title"], 'img': ["alt", "src", "title"], } ALLOWED_TAGS = [ 'a', 'b', 'blockquote', 'em', 'h1', 'h2', 'h3', 'h4', 'i', 'img', 'li', 'ol', 'p', 'strong', 'u', 'ul', ]
[docs] def __init__(self, **kwargs): kwargs.setdefault("widget", MarkdownWidget) super().__init__(**kwargs)
[docs] def get_display_value(self, record, request, url=None): value = self.get_value(record, request) if not value: return "" html = markdown(value) return mark_safe(clean_html(html, tags=self.ALLOWED_TAGS, attributes=self.ALLOWED_ATTRIBUTES))
[docs] def get_preview_value(self, record, request, url=None): value = self.get_value(record, request) if not value: return None lines = value.split("\n") line = "\n".join(lines[:self.preview_lines]) html = markdown(line) return mark_safe(clean_html(html, tags=self.ALLOWED_TAGS, attributes=self.ALLOWED_ATTRIBUTES))
[docs]class PercentageControl(BaseControl): """Represents a percentage number (float or int)."""
[docs] def __init__(self, input_suffix="%", rounding=None, **kwargs): self.rounding = rounding kwargs.setdefault("input_suffix", input_suffix) super().__init__(**kwargs)
[docs] def get_display_value(self, record, request, url=None): value = self.get_value(record, request) if not value: return "" if self.rounding: value = round(value, self.rounding) return "%s%%" % value
[docs] def get_preview_value(self, record, request, url=None): value = self.get_value(record, request) if not value: return "" if self.rounding: value = round(value, self.rounding) return "%s%%" % value
[docs]class RichTextControl(HTMLControl):
[docs] def __init__(self, input_options=None, input_type="summernote", **kwargs): self.input_options = input_options self.input_type = input_type super().__init__(**kwargs)
[docs] def get_form_css(self): css = super().get_form_css() if self.input_type == "nicedit": pass elif self.input_type == "ckeditor": pass else: # css.append("quill.js", url="https://cdn.quilljs.com/1.3.6/quill.snow.css") css.append("summernote.js", url="http://cdnjs.cloudflare.com/ajax/libs/summernote/0.8.12/summernote.css") return css
[docs] def get_form_js(self): js = super().get_form_js() # Both quill and trix are nice-looking editors but require additional JS stupidity before submitting the form. # JS must update a hidden text field using the content of the editor. So for now, we are using Summernote which # looks nice but appears to be limited to Bootstrap. # https://github.com/basecamp/trix # js.append("quill.js", url="https://cdn.quilljs.com/1.3.6/quill.js") # # script = """ # var quill_%s = new Quill("#id_%s_parent", { # theme: 'snow', # }); # """ % ( # self.name, # self.name # ) # js.append("%s quill" % self.name, content=script) if self.input_type == "nicedit": js.append("nicedit.js", url="%sbundled/nicedit/nicEdit.js" % settings.STATIC_URL) _options = [ 'iconsPath: "%sbundled/nicedit/nicEditorIcons.gif"' % settings.STATIC_URL, ] if self.input_options: for key, value in self.input_options.items(): if type(value) is list: _options.append("%s: %s" % (key, str(value))) elif is_bool(value) or is_integer(value, cast=True): _options.append("%s: %s" % (key, value)) else: _options.append("%s: '%s'" % (key, value)) options = "{%s}" % ", ".join(_options) script = """ //<![CDATA[ bkLib.onDomLoaded(function() { new nicEditor(%s).panelInstance('id_%s'); }); //]]> """ % (options, self.name) js.append("%s nicedit" % self.name, content=script) elif self.input_type == "ckeditor": js.append("ckeditor.js", url="https://cdn.ckeditor.com/ckeditor5/19.0.0/classic/ckeditor.js") if self.input_options: script = list() # if 'alignment' in self.input_options or \ # ('toolbar' in self.input_options and 'alignment' in self.input_options['toolbar']): # script.append("import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment';") script.append("ClassicEditor.create(document.querySelector('#id_%s'), {" % self.name) script.append(python_to_js(self.input_options)) # for key, value in self.input_options.items(): # if type(value) is dict: # script.append("%s: {" % key) # for subkey, subvalue in value.items(): # script.append(" %s: %s" % (subkey, subvalue)) # script.append("},") # else: # script.append(" %s: %s," % (key, str(value))) script.append(" })") script.append(".catch(error => {console.error(error);});") js.append("%s ckeditor" % self.name, content="\n".join(script)) else: script = """ ClassicEditor.create(document.querySelector("#id_%s")).catch(error => {console.error(error);}); """ % self.name js.append("%s ckeditor" % self.name, content=script) else: js.append("summernote.js", url="http://cdnjs.cloudflare.com/ajax/libs/summernote/0.8.12/summernote.js") script = """ $(document).ready(function() { $('#id_%s').summernote(); }); """ % self.name js.append("%s summernote" % self.name, content=script) return js
[docs]class TimeZoneControl(TextControl):
[docs] def __init__(self, choices=None, **kwargs): super().__init__(**kwargs) self.choices = choices or COMMON_TIMEZONE_CHOICES self.widget = forms.Select(choices=self.choices)
[docs] def get_form_css(self): """Provide support for select2.""" css = super().get_form_css() css.append("select2", url="bundled/select2/css/select2.css") return css
[docs] def get_form_js(self): """Provide support for select2.""" js = super().get_form_js() js.append("select2", url="bundled/select2/js/select2.js") content = '$(document).ready(function() { $("#id_%s").select2(); });' % self.name js.append("%s select2" % self.name, content=content) return js
[docs]class UserControl(ForeignKeyControl): """Extend :py:class:`ForeignKeyControl` to provide first/last name when possible."""
[docs] def get_display_value(self, record, request, url=None): # noinspection PyPep8Naming UserModel = get_user_model() value = self.get_value(record, request) if not value: return "" try: display_name = value.get_full_name() except AttributeError: display_name = getattr(value, UserModel.USERNAME_FIELD) if self.link_enabled: if url is None: url = self._get_url(value) if url is not None: return mark_safe('<a href="%s">%s</a>' % (url, display_name)) return display_name