565 lines
20 KiB
Python
Executable file
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
|