adm-ntuh-net/ntuh/dojango/forms/widgets.py
2024-12-12 10:19:16 +08:00

565 lines
20 KiB
Python
Executable file

import datetime
from django.forms import *
from django.utils import formats
from django.utils.encoding import StrAndUnicode, force_unicode
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
from django.forms.util import flatatt
from django.utils import datetime_safe
from dojango.util import json_encode
from dojango.util.config import Config
from dojango.util import dojo_collector
__all__ = (
'Media', 'MediaDefiningClass', # original django classes
'DojoWidgetMixin', 'Input', 'Widget', 'TextInput', 'PasswordInput',
'HiddenInput', 'MultipleHiddenInput', 'FileInput', 'Textarea',
'DateInput', 'DateTimeInput', 'TimeInput', 'CheckboxInput', 'Select',
'NullBooleanSelect', 'SelectMultiple', 'RadioInput', 'RadioFieldRenderer',
'RadioSelect', 'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget',
'SplitHiddenDateTimeWidget', 'SimpleTextarea', 'EditorInput', 'HorizontalSliderInput',
'VerticalSliderInput', 'ValidationTextInput', 'ValidationPasswordInput',
'EmailTextInput', 'IPAddressTextInput', 'URLTextInput', 'NumberTextInput',
'RangeBoundTextInput', 'NumberSpinnerInput', 'RatingInput', 'DateInputAnim',
'DropDownSelect', 'CheckedMultiSelect', 'FilteringSelect', 'ComboBox',
'ComboBoxStore', 'FilteringSelectStore', 'ListInput',
)
dojo_config = Config() # initialize the configuration
class DojoWidgetMixin:
"""A helper mixin, that is used by every custom dojo widget.
Some dojo widgets can utilize the validation information of a field and here
we mixin those attributes into the widget. Field attributes that are listed
in the 'valid_extra_attrs' will be mixed into the attributes of a widget.
The 'default_field_attr_map' property contains the default mapping of field
attributes to dojo widget attributes.
This mixin also takes care passing the required dojo modules to the collector.
'dojo_type' defines the used dojo module type of this widget and adds this
module to the collector, if no 'alt_require' property is defined. When
'alt_require' is set, this module will be passed to the collector. By using
'extra_dojo_require' it is possible to pass additional dojo modules to the
collector.
"""
dojo_type = None # this is the dojoType definition of the widget. also used for generating the dojo.require call
alt_require = None # alternative dojo.require call (not using the dojo_type)
extra_dojo_require = [] # these dojo modules also needs to be loaded for this widget
default_field_attr_map = { # the default map for mapping field attributes to dojo attributes
'required':'required',
'help_text':'promptMessage',
'min_value':'constraints.min',
'max_value':'constraints.max',
'max_length':'maxLength',
#'max_digits':'maxDigits',
'decimal_places':'constraints.places',
'js_regex':'regExp',
'multiple':'multiple',
}
field_attr_map = {} # used for overwriting the default attr-map
valid_extra_attrs = [] # these field_attributes are valid for the current widget
def _mixin_attr(self, attrs, key, value):
"""Mixes in the passed key/value into the passed attrs and returns that
extended attrs dictionary.
A 'key', that is separated by a dot, e.g. 'constraints.min', will be
added as:
{'constraints':{'min':value}}
"""
dojo_field_attr = key.split(".")
inner_dict = attrs
len_fields = len(dojo_field_attr)
count = 0
for i in dojo_field_attr:
count = count+1
if count == len_fields and inner_dict.get(i, None) is None:
if isinstance(value, datetime.datetime):
if isinstance(self, TimeInput):
value = value.strftime('T%H:%M:%S')
if isinstance(self, DateInput):
value = value.strftime('%Y-%m-%d')
value = str(value).replace(' ', 'T') # see dojo.date.stamp
if isinstance(value, datetime.date):
value = str(value)
if isinstance(value, datetime.time):
value = "T" + str(value) # see dojo.date.stamp
inner_dict[i] = value
elif not inner_dict.has_key(i):
inner_dict[i] = {}
inner_dict = inner_dict[i]
return attrs
def build_attrs(self, extra_attrs=None, **kwargs):
"""Overwritten helper function for building an attribute dictionary.
This helper also takes care passing the used dojo modules to the
collector. Furthermore it mixes in the used field attributes into the
attributes of this widget.
"""
# gathering all widget attributes
attrs = dict(self.attrs, **kwargs)
field_attr = self.default_field_attr_map.copy() # use a copy of that object. otherwise changed field_attr_map would overwrite the default-map for all widgets!
field_attr.update(self.field_attr_map) # the field-attribute-mapping can be customzied
if extra_attrs:
attrs.update(extra_attrs)
# assigning dojoType to our widget
dojo_type = getattr(self, "dojo_type", False)
if dojo_type:
attrs["dojoType"] = dojo_type # add the dojoType attribute
# fill the global collector object
if getattr(self, "alt_require", False):
dojo_collector.add_module(self.alt_require)
elif dojo_type:
dojo_collector.add_module(self.dojo_type)
extra_requires = getattr(self, "extra_dojo_require", [])
for i in extra_requires:
dojo_collector.add_module(i)
# mixin those additional field attrs, that are valid for this widget
extra_field_attrs = attrs.get("extra_field_attrs", False)
if extra_field_attrs:
for i in self.valid_extra_attrs:
field_val = extra_field_attrs.get(i, None)
new_attr_name = field_attr.get(i, None)
if field_val is not None and new_attr_name is not None:
attrs = self._mixin_attr(attrs, new_attr_name, field_val)
del attrs["extra_field_attrs"]
# now encode several attributes, e.g. False = false, True = true
for i in attrs:
if isinstance(attrs[i], bool):
attrs[i] = json_encode(attrs[i])
return attrs
#############################################
# ALL OVERWRITTEN DEFAULT DJANGO WIDGETS
#############################################
class Widget(DojoWidgetMixin, widgets.Widget):
dojo_type = 'dijit._Widget'
class Input(DojoWidgetMixin, widgets.Input):
pass
class TextInput(DojoWidgetMixin, widgets.TextInput):
dojo_type = 'dijit.form.TextBox'
valid_extra_attrs = [
'max_length',
]
class PasswordInput(DojoWidgetMixin, widgets.PasswordInput):
dojo_type = 'dijit.form.TextBox'
valid_extra_attrs = [
'max_length',
]
class HiddenInput(DojoWidgetMixin, widgets.HiddenInput):
dojo_type = 'dijit.form.TextBox' # otherwise dijit.form.Form can't get its values
class MultipleHiddenInput(DojoWidgetMixin, widgets.MultipleHiddenInput):
dojo_type = 'dijit.form.TextBox' # otherwise dijit.form.Form can't get its values
class FileInput(DojoWidgetMixin, widgets.FileInput):
dojo_type = 'dojox.form.FileInput'
class Media:
css = {
'all': ('%(base_url)s/dojox/form/resources/FileInput.css' % {
'base_url':dojo_config.dojo_base_url
},)
}
class Textarea(DojoWidgetMixin, widgets.Textarea):
"""Auto resizing textarea"""
dojo_type = 'dijit.form.Textarea'
valid_extra_attrs = [
'max_length'
]
if DateInput:
class DateInput(DojoWidgetMixin, widgets.DateInput):
dojo_type = 'dijit.form.DateTextBox'
valid_extra_attrs = [
'required',
'help_text',
'min_value',
'max_value',
]
else: # fallback for older django versions
class DateInput(TextInput):
"""Copy of the implementation in Django 1.1. Before this widget did not exists."""
dojo_type = 'dijit.form.DateTextBox'
valid_extra_attrs = [
'required',
'help_text',
'min_value',
'max_value',
]
format = '%Y-%m-%d' # '2006-10-25'
def __init__(self, attrs=None, format=None):
super(DateInput, self).__init__(attrs)
if format:
self.format = format
def render(self, name, value, attrs=None):
if value is None:
value = ''
elif hasattr(value, 'strftime'):
value = datetime_safe.new_date(value)
value = value.strftime(self.format)
return super(DateInput, self).render(name, value, attrs)
if TimeInput:
class TimeInput(DojoWidgetMixin, widgets.TimeInput):
dojo_type = 'dijit.form.TimeTextBox'
valid_extra_attrs = [
'required',
'help_text',
'min_value',
'max_value',
]
format = "T%H:%M:%S" # special for dojo: 'T12:12:33'
def __init__(self, attrs=None, format=None):
# always passing the dojo time format
super(TimeInput, self).__init__(attrs, format=self.format)
def _has_changed(self, initial, data):
try:
input_format = self.format
initial = datetime.time(*time.strptime(initial, input_format)[3:6])
except (TypeError, ValueError):
pass
return super(TimeInput, self)._has_changed(self._format_value(initial), data)
else: # fallback for older django versions
class TimeInput(TextInput):
"""Copy of the implementation in Django 1.1. Before this widget did not exists."""
dojo_type = 'dijit.form.TimeTextBox'
valid_extra_attrs = [
'required',
'help_text',
'min_value',
'max_value',
]
format = "T%H:%M:%S" # special for dojo: 'T12:12:33'
def __init__(self, attrs=None, format=None):
super(TimeInput, self).__init__(attrs)
if format:
self.format = format
def render(self, name, value, attrs=None):
if value is None:
value = ''
elif hasattr(value, 'strftime'):
value = value.strftime(self.format)
return super(TimeInput, self).render(name, value, attrs)
class CheckboxInput(DojoWidgetMixin, widgets.CheckboxInput):
dojo_type = 'dijit.form.CheckBox'
class Select(DojoWidgetMixin, widgets.Select):
dojo_type = dojo_config.version < '1.4' and 'dijit.form.FilteringSelect' or 'dijit.form.Select'
valid_extra_attrs = dojo_config.version < '1.4' and \
['required', 'help_text',] or \
['required',]
class NullBooleanSelect(DojoWidgetMixin, widgets.NullBooleanSelect):
dojo_type = dojo_config.version < '1.4' and 'dijit.form.FilteringSelect' or 'dijit.form.Select'
valid_extra_attrs = dojo_config.version < '1.4' and \
['required', 'help_text',] or \
['required',]
class SelectMultiple(DojoWidgetMixin, widgets.SelectMultiple):
dojo_type = 'dijit.form.MultiSelect'
RadioInput = widgets.RadioInput
RadioFieldRenderer = widgets.RadioFieldRenderer
class RadioSelect(DojoWidgetMixin, widgets.RadioSelect):
dojo_type = 'dijit.form.RadioButton'
def __init__(self, *args, **kwargs):
if dojo_config.version < '1.3':
self.alt_require = 'dijit.form.CheckBox'
super(RadioSelect, self).__init__(*args, **kwargs)
class CheckboxSelectMultiple(DojoWidgetMixin, widgets.CheckboxSelectMultiple):
dojo_type = 'dijit.form.CheckBox'
class MultiWidget(DojoWidgetMixin, widgets.MultiWidget):
dojo_type = None
class SplitDateTimeWidget(widgets.SplitDateTimeWidget):
"DateTimeInput is using two input fields."
date_format = DateInput.format
time_format = TimeInput.format
def __init__(self, attrs=None, date_format=None, time_format=None):
if date_format:
self.date_format = date_format
if time_format:
self.time_format = time_format
split_widgets = (DateInput(attrs=attrs, format=self.date_format),
TimeInput(attrs=attrs, format=self.time_format))
# Note that we're calling MultiWidget, not SplitDateTimeWidget, because
# we want to define widgets.
widgets.MultiWidget.__init__(self, split_widgets, attrs)
class SplitHiddenDateTimeWidget(DojoWidgetMixin, widgets.SplitHiddenDateTimeWidget):
dojo_type = "dijit.form.TextBox"
DateTimeInput = SplitDateTimeWidget
#############################################
# MORE ENHANCED DJANGO/DOJO WIDGETS
#############################################
class SimpleTextarea(Textarea):
"""No autoexpanding textarea"""
dojo_type = "dijit.form.SimpleTextarea"
class EditorInput(Textarea):
dojo_type = 'dijit.Editor'
def render(self, name, value, attrs=None):
if value is None: value = ''
final_attrs = self.build_attrs(attrs, name=name)
# dijit.Editor must be rendered in a div (see dijit/_editor/RichText.js)
return mark_safe(u'<div%s>%s</div>' % (flatatt(final_attrs),
force_unicode(value))) # we don't escape the value for the editor
class HorizontalSliderInput(TextInput):
dojo_type = 'dijit.form.HorizontalSlider'
valid_extra_attrs = [
'max_value',
'min_value',
]
field_attr_map = {
'max_value': 'maximum',
'min_value': 'minimum',
}
def __init__(self, attrs=None):
if dojo_config.version < '1.3':
self.alt_require = 'dijit.form.Slider'
super(HorizontalSliderInput, self).__init__(attrs)
class VerticalSliderInput(HorizontalSliderInput):
dojo_type = 'dijit.form.VerticalSlider'
class ValidationTextInput(TextInput):
dojo_type = 'dijit.form.ValidationTextBox'
valid_extra_attrs = [
'required',
'help_text',
'js_regex',
'max_length',
]
js_regex_func = None
def render(self, name, value, attrs=None):
if self.js_regex_func:
attrs = self.build_attrs(attrs, regExpGen=self.js_regex_func)
return super(ValidationTextInput, self).render(name, value, attrs)
class ValidationPasswordInput(PasswordInput):
dojo_type = 'dijit.form.ValidationTextBox'
valid_extra_attrs = [
'required',
'help_text',
'js_regex',
'max_length',
]
class EmailTextInput(ValidationTextInput):
extra_dojo_require = [
'dojox.validate.regexp'
]
js_regex_func = "dojox.validate.regexp.emailAddress"
def __init__(self, attrs=None):
if dojo_config.version < '1.3':
self.js_regex_func = 'dojox.regexp.emailAddress'
super(EmailTextInput, self).__init__(attrs)
class IPAddressTextInput(ValidationTextInput):
extra_dojo_require = [
'dojox.validate.regexp'
]
js_regex_func = "dojox.validate.regexp.ipAddress"
def __init__(self, attrs=None):
if dojo_config.version < '1.3':
self.js_regex_func = 'dojox.regexp.ipAddress'
super(IPAddressTextInput, self).__init__(attrs)
class URLTextInput(ValidationTextInput):
extra_dojo_require = [
'dojox.validate.regexp'
]
js_regex_func = "dojox.validate.regexp.url"
def __init__(self, attrs=None):
if dojo_config.version < '1.3':
self.js_regex_func = 'dojox.regexp.url'
super(URLTextInput, self).__init__(attrs)
class NumberTextInput(TextInput):
dojo_type = 'dijit.form.NumberTextBox'
valid_extra_attrs = [
'min_value',
'max_value',
'required',
'help_text',
'decimal_places',
]
class RangeBoundTextInput(NumberTextInput):
dojo_type = 'dijit.form.RangeBoundTextBox'
class NumberSpinnerInput(NumberTextInput):
dojo_type = 'dijit.form.NumberSpinner'
class RatingInput(TextInput):
dojo_type = 'dojox.form.Rating'
valid_extra_attrs = [
'max_value',
]
field_attr_map = {
'max_value': 'numStars',
}
class Media:
css = {
'all': ('%(base_url)s/dojox/form/resources/Rating.css' % {
'base_url':dojo_config.dojo_base_url
},)
}
class DateInputAnim(DateInput):
dojo_type = 'dojox.form.DateTextBox'
class Media:
css = {
'all': ('%(base_url)s/dojox/widget/Calendar/Calendar.css' % {
'base_url':dojo_config.dojo_base_url
},)
}
class DropDownSelect(Select):
dojo_type = 'dojox.form.DropDownSelect'
valid_extra_attrs = []
class Media:
css = {
'all': ('%(base_url)s/dojox/form/resources/DropDownSelect.css' % {
'base_url':dojo_config.dojo_base_url
},)
}
class CheckedMultiSelect(SelectMultiple):
dojo_type = 'dojox.form.CheckedMultiSelect'
valid_extra_attrs = []
# TODO: fix attribute multiple=multiple
# seems there is a dependency in dojox.form.CheckedMultiSelect for dijit.form.MultiSelect,
# but CheckedMultiSelect is not extending that
class Media:
css = {
'all': ('%(base_url)s/dojox/form/resources/CheckedMultiSelect.css' % {
'base_url':dojo_config.dojo_base_url
},)
}
class ComboBox(DojoWidgetMixin, widgets.Select):
"""Nearly the same as FilteringSelect, but ignoring the option value."""
dojo_type = 'dijit.form.ComboBox'
valid_extra_attrs = [
'required',
'help_text',
]
class FilteringSelect(ComboBox):
dojo_type = 'dijit.form.FilteringSelect'
class ComboBoxStore(TextInput):
"""A combobox that is receiving data from a given dojo data url.
As default dojo.data.ItemFileReadStore is used. You can overwrite
that behaviour by passing a different store name
(e.g. dojox.data.QueryReadStore).
Usage:
ComboBoxStore("/dojo-data-store-url/")
"""
dojo_type = 'dijit.form.ComboBox'
valid_extra_attrs = [
'required',
'help_text',
]
store = 'dojo.data.ItemFileReadStore'
store_attrs = {}
url = None
def __init__(self, url, attrs=None, store=None, store_attrs={}):
self.url = url
if store:
self.store = store
if store_attrs:
self.store_attrs = store_attrs
self.extra_dojo_require.append(self.store)
super(ComboBoxStore, self).__init__(attrs)
def render(self, name, value, attrs=None):
if value is None: value = ''
store_id = self.get_store_id(getattr(attrs, "id", None), name)
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name, store=store_id)
if value != '':
# Only add the 'value' attribute if a value is non-empty.
final_attrs['value'] = force_unicode(self._format_value(value))
self.store_attrs.update({
'dojoType': self.store,
'url': self.url,
'jsId':store_id
})
# TODO: convert store attributes to valid js-format (False => false, dict => {}, array = [])
store_node = '<div%s></div>' % flatatt(self.store_attrs)
return mark_safe(u'%s<input%s />' % (store_node, flatatt(final_attrs)))
def get_store_id(self, id, name):
return "_store_" + (id and id or name)
class FilteringSelectStore(ComboBoxStore):
dojo_type = 'dijit.form.FilteringSelect'
class ListInput(DojoWidgetMixin, widgets.TextInput):
dojo_type = 'dojox.form.ListInput'
class Media:
css = {
'all': ('%(base_url)s/dojox/form/resources/ListInput.css' % {
'base_url':dojo_config.dojo_base_url
},)
}
# THE RANGE SLIDER NEEDS A DIFFERENT REPRESENTATION WITHIN HTML
# SOMETHING LIKE:
# <div dojoType="dojox.form.RangeSlider"><input value="5"/><input value="10"/></div>
'''class HorizontalRangeSlider(HorizontalSliderInput):
"""This just can be used with a comma-separated-value like: 20,40"""
dojo_type = 'dojox.form.HorizontalRangeSlider'
alt_require = 'dojox.form.RangeSlider'
class Media:
css = {
'all': ('%(base_url)s/dojox/form/resources/RangeSlider.css' % {
'base_url':dojo_config.dojo_base_url
},)
}
'''
# TODO: implement
# dojox.form.RangeSlider
# dojox.form.MultiComboBox
# dojox.form.FileUploader