# 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
# noinspection PyMethodMayBeStatic
# noinspection PyMethodMayBeStatic
[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
@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
def get_options(self, record, request):
d = super().get_options(record, request)
d['class'] = "text-center"
return d
[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_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]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]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]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_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]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]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_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
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
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]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_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]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