PageRenderTime 56ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/dojango/forms/widgets.py

http://dojango.googlecode.com/
Python | 565 lines | 505 code | 41 blank | 19 comment | 11 complexity | e923c89d8175c16a6470bfe6485fe3a7 MD5 | raw file
  1. import datetime
  2. from django.forms import *
  3. from django.utils import formats
  4. from django.utils.encoding import StrAndUnicode, force_unicode
  5. from django.utils.html import conditional_escape
  6. from django.utils.safestring import mark_safe
  7. from django.forms.util import flatatt
  8. from django.utils import datetime_safe
  9. from dojango.util import json_encode
  10. from dojango.util.config import Config
  11. from dojango.util import dojo_collector
  12. __all__ = (
  13. 'Media', 'MediaDefiningClass', # original django classes
  14. 'DojoWidgetMixin', 'Input', 'Widget', 'TextInput', 'PasswordInput',
  15. 'HiddenInput', 'MultipleHiddenInput', 'FileInput', 'Textarea',
  16. 'DateInput', 'DateTimeInput', 'TimeInput', 'CheckboxInput', 'Select',
  17. 'NullBooleanSelect', 'SelectMultiple', 'RadioInput', 'RadioFieldRenderer',
  18. 'RadioSelect', 'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget',
  19. 'SplitHiddenDateTimeWidget', 'SimpleTextarea', 'EditorInput', 'HorizontalSliderInput',
  20. 'VerticalSliderInput', 'ValidationTextInput', 'ValidationPasswordInput',
  21. 'EmailTextInput', 'IPAddressTextInput', 'URLTextInput', 'NumberTextInput',
  22. 'RangeBoundTextInput', 'NumberSpinnerInput', 'RatingInput', 'DateInputAnim',
  23. 'DropDownSelect', 'CheckedMultiSelect', 'FilteringSelect', 'ComboBox',
  24. 'ComboBoxStore', 'FilteringSelectStore', 'ListInput',
  25. )
  26. dojo_config = Config() # initialize the configuration
  27. class DojoWidgetMixin:
  28. """A helper mixin, that is used by every custom dojo widget.
  29. Some dojo widgets can utilize the validation information of a field and here
  30. we mixin those attributes into the widget. Field attributes that are listed
  31. in the 'valid_extra_attrs' will be mixed into the attributes of a widget.
  32. The 'default_field_attr_map' property contains the default mapping of field
  33. attributes to dojo widget attributes.
  34. This mixin also takes care passing the required dojo modules to the collector.
  35. 'dojo_type' defines the used dojo module type of this widget and adds this
  36. module to the collector, if no 'alt_require' property is defined. When
  37. 'alt_require' is set, this module will be passed to the collector. By using
  38. 'extra_dojo_require' it is possible to pass additional dojo modules to the
  39. collector.
  40. """
  41. dojo_type = None # this is the dojoType definition of the widget. also used for generating the dojo.require call
  42. alt_require = None # alternative dojo.require call (not using the dojo_type)
  43. extra_dojo_require = [] # these dojo modules also needs to be loaded for this widget
  44. default_field_attr_map = { # the default map for mapping field attributes to dojo attributes
  45. 'required':'required',
  46. 'help_text':'promptMessage',
  47. 'min_value':'constraints.min',
  48. 'max_value':'constraints.max',
  49. 'max_length':'maxLength',
  50. #'max_digits':'maxDigits',
  51. 'decimal_places':'constraints.places',
  52. 'js_regex':'regExp',
  53. 'multiple':'multiple',
  54. }
  55. field_attr_map = {} # used for overwriting the default attr-map
  56. valid_extra_attrs = [] # these field_attributes are valid for the current widget
  57. def _mixin_attr(self, attrs, key, value):
  58. """Mixes in the passed key/value into the passed attrs and returns that
  59. extended attrs dictionary.
  60. A 'key', that is separated by a dot, e.g. 'constraints.min', will be
  61. added as:
  62. {'constraints':{'min':value}}
  63. """
  64. dojo_field_attr = key.split(".")
  65. inner_dict = attrs
  66. len_fields = len(dojo_field_attr)
  67. count = 0
  68. for i in dojo_field_attr:
  69. count = count+1
  70. if count == len_fields and inner_dict.get(i, None) is None:
  71. if isinstance(value, datetime.datetime):
  72. if isinstance(self, TimeInput):
  73. value = value.strftime('T%H:%M:%S')
  74. if isinstance(self, DateInput):
  75. value = value.strftime('%Y-%m-%d')
  76. value = str(value).replace(' ', 'T') # see dojo.date.stamp
  77. if isinstance(value, datetime.date):
  78. value = str(value)
  79. if isinstance(value, datetime.time):
  80. value = "T" + str(value) # see dojo.date.stamp
  81. inner_dict[i] = value
  82. elif not inner_dict.has_key(i):
  83. inner_dict[i] = {}
  84. inner_dict = inner_dict[i]
  85. return attrs
  86. def build_attrs(self, extra_attrs=None, **kwargs):
  87. """Overwritten helper function for building an attribute dictionary.
  88. This helper also takes care passing the used dojo modules to the
  89. collector. Furthermore it mixes in the used field attributes into the
  90. attributes of this widget.
  91. """
  92. # gathering all widget attributes
  93. attrs = dict(self.attrs, **kwargs)
  94. 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!
  95. field_attr.update(self.field_attr_map) # the field-attribute-mapping can be customzied
  96. if extra_attrs:
  97. attrs.update(extra_attrs)
  98. # assigning dojoType to our widget
  99. dojo_type = getattr(self, "dojo_type", False)
  100. if dojo_type:
  101. attrs["dojoType"] = dojo_type # add the dojoType attribute
  102. # fill the global collector object
  103. if getattr(self, "alt_require", False):
  104. dojo_collector.add_module(self.alt_require)
  105. elif dojo_type:
  106. dojo_collector.add_module(self.dojo_type)
  107. extra_requires = getattr(self, "extra_dojo_require", [])
  108. for i in extra_requires:
  109. dojo_collector.add_module(i)
  110. # mixin those additional field attrs, that are valid for this widget
  111. extra_field_attrs = attrs.get("extra_field_attrs", False)
  112. if extra_field_attrs:
  113. for i in self.valid_extra_attrs:
  114. field_val = extra_field_attrs.get(i, None)
  115. new_attr_name = field_attr.get(i, None)
  116. if field_val is not None and new_attr_name is not None:
  117. attrs = self._mixin_attr(attrs, new_attr_name, field_val)
  118. del attrs["extra_field_attrs"]
  119. # now encode several attributes, e.g. False = false, True = true
  120. for i in attrs:
  121. if isinstance(attrs[i], bool):
  122. attrs[i] = json_encode(attrs[i])
  123. return attrs
  124. #############################################
  125. # ALL OVERWRITTEN DEFAULT DJANGO WIDGETS
  126. #############################################
  127. class Widget(DojoWidgetMixin, widgets.Widget):
  128. dojo_type = 'dijit._Widget'
  129. class Input(DojoWidgetMixin, widgets.Input):
  130. pass
  131. class TextInput(DojoWidgetMixin, widgets.TextInput):
  132. dojo_type = 'dijit.form.TextBox'
  133. valid_extra_attrs = [
  134. 'max_length',
  135. ]
  136. class PasswordInput(DojoWidgetMixin, widgets.PasswordInput):
  137. dojo_type = 'dijit.form.TextBox'
  138. valid_extra_attrs = [
  139. 'max_length',
  140. ]
  141. class HiddenInput(DojoWidgetMixin, widgets.HiddenInput):
  142. dojo_type = 'dijit.form.TextBox' # otherwise dijit.form.Form can't get its values
  143. class MultipleHiddenInput(DojoWidgetMixin, widgets.MultipleHiddenInput):
  144. dojo_type = 'dijit.form.TextBox' # otherwise dijit.form.Form can't get its values
  145. class FileInput(DojoWidgetMixin, widgets.FileInput):
  146. dojo_type = 'dojox.form.FileInput'
  147. class Media:
  148. css = {
  149. 'all': ('%(base_url)s/dojox/form/resources/FileInput.css' % {
  150. 'base_url':dojo_config.dojo_base_url
  151. },)
  152. }
  153. class Textarea(DojoWidgetMixin, widgets.Textarea):
  154. """Auto resizing textarea"""
  155. dojo_type = 'dijit.form.Textarea'
  156. valid_extra_attrs = [
  157. 'max_length'
  158. ]
  159. if DateInput:
  160. class DateInput(DojoWidgetMixin, widgets.DateInput):
  161. dojo_type = 'dijit.form.DateTextBox'
  162. valid_extra_attrs = [
  163. 'required',
  164. 'help_text',
  165. 'min_value',
  166. 'max_value',
  167. ]
  168. else: # fallback for older django versions
  169. class DateInput(TextInput):
  170. """Copy of the implementation in Django 1.1. Before this widget did not exists."""
  171. dojo_type = 'dijit.form.DateTextBox'
  172. valid_extra_attrs = [
  173. 'required',
  174. 'help_text',
  175. 'min_value',
  176. 'max_value',
  177. ]
  178. format = '%Y-%m-%d' # '2006-10-25'
  179. def __init__(self, attrs=None, format=None):
  180. super(DateInput, self).__init__(attrs)
  181. if format:
  182. self.format = format
  183. def render(self, name, value, attrs=None):
  184. if value is None:
  185. value = ''
  186. elif hasattr(value, 'strftime'):
  187. value = datetime_safe.new_date(value)
  188. value = value.strftime(self.format)
  189. return super(DateInput, self).render(name, value, attrs)
  190. if TimeInput:
  191. class TimeInput(DojoWidgetMixin, widgets.TimeInput):
  192. dojo_type = 'dijit.form.TimeTextBox'
  193. valid_extra_attrs = [
  194. 'required',
  195. 'help_text',
  196. 'min_value',
  197. 'max_value',
  198. ]
  199. format = "T%H:%M:%S" # special for dojo: 'T12:12:33'
  200. def __init__(self, attrs=None, format=None):
  201. # always passing the dojo time format
  202. super(TimeInput, self).__init__(attrs, format=self.format)
  203. def _has_changed(self, initial, data):
  204. try:
  205. input_format = self.format
  206. initial = datetime.time(*time.strptime(initial, input_format)[3:6])
  207. except (TypeError, ValueError):
  208. pass
  209. return super(TimeInput, self)._has_changed(self._format_value(initial), data)
  210. else: # fallback for older django versions
  211. class TimeInput(TextInput):
  212. """Copy of the implementation in Django 1.1. Before this widget did not exists."""
  213. dojo_type = 'dijit.form.TimeTextBox'
  214. valid_extra_attrs = [
  215. 'required',
  216. 'help_text',
  217. 'min_value',
  218. 'max_value',
  219. ]
  220. format = "T%H:%M:%S" # special for dojo: 'T12:12:33'
  221. def __init__(self, attrs=None, format=None):
  222. super(TimeInput, self).__init__(attrs)
  223. if format:
  224. self.format = format
  225. def render(self, name, value, attrs=None):
  226. if value is None:
  227. value = ''
  228. elif hasattr(value, 'strftime'):
  229. value = value.strftime(self.format)
  230. return super(TimeInput, self).render(name, value, attrs)
  231. class CheckboxInput(DojoWidgetMixin, widgets.CheckboxInput):
  232. dojo_type = 'dijit.form.CheckBox'
  233. class Select(DojoWidgetMixin, widgets.Select):
  234. dojo_type = dojo_config.version < '1.4' and 'dijit.form.FilteringSelect' or 'dijit.form.Select'
  235. valid_extra_attrs = dojo_config.version < '1.4' and \
  236. ['required', 'help_text',] or \
  237. ['required',]
  238. class NullBooleanSelect(DojoWidgetMixin, widgets.NullBooleanSelect):
  239. dojo_type = dojo_config.version < '1.4' and 'dijit.form.FilteringSelect' or 'dijit.form.Select'
  240. valid_extra_attrs = dojo_config.version < '1.4' and \
  241. ['required', 'help_text',] or \
  242. ['required',]
  243. class SelectMultiple(DojoWidgetMixin, widgets.SelectMultiple):
  244. dojo_type = 'dijit.form.MultiSelect'
  245. RadioInput = widgets.RadioInput
  246. RadioFieldRenderer = widgets.RadioFieldRenderer
  247. class RadioSelect(DojoWidgetMixin, widgets.RadioSelect):
  248. dojo_type = 'dijit.form.RadioButton'
  249. def __init__(self, *args, **kwargs):
  250. if dojo_config.version < '1.3':
  251. self.alt_require = 'dijit.form.CheckBox'
  252. super(RadioSelect, self).__init__(*args, **kwargs)
  253. class CheckboxSelectMultiple(DojoWidgetMixin, widgets.CheckboxSelectMultiple):
  254. dojo_type = 'dijit.form.CheckBox'
  255. class MultiWidget(DojoWidgetMixin, widgets.MultiWidget):
  256. dojo_type = None
  257. class SplitDateTimeWidget(widgets.SplitDateTimeWidget):
  258. "DateTimeInput is using two input fields."
  259. date_format = DateInput.format
  260. time_format = TimeInput.format
  261. def __init__(self, attrs=None, date_format=None, time_format=None):
  262. if date_format:
  263. self.date_format = date_format
  264. if time_format:
  265. self.time_format = time_format
  266. split_widgets = (DateInput(attrs=attrs, format=self.date_format),
  267. TimeInput(attrs=attrs, format=self.time_format))
  268. # Note that we're calling MultiWidget, not SplitDateTimeWidget, because
  269. # we want to define widgets.
  270. widgets.MultiWidget.__init__(self, split_widgets, attrs)
  271. class SplitHiddenDateTimeWidget(DojoWidgetMixin, widgets.SplitHiddenDateTimeWidget):
  272. dojo_type = "dijit.form.TextBox"
  273. DateTimeInput = SplitDateTimeWidget
  274. #############################################
  275. # MORE ENHANCED DJANGO/DOJO WIDGETS
  276. #############################################
  277. class SimpleTextarea(Textarea):
  278. """No autoexpanding textarea"""
  279. dojo_type = "dijit.form.SimpleTextarea"
  280. class EditorInput(Textarea):
  281. dojo_type = 'dijit.Editor'
  282. def render(self, name, value, attrs=None):
  283. if value is None: value = ''
  284. final_attrs = self.build_attrs(attrs, name=name)
  285. # dijit.Editor must be rendered in a div (see dijit/_editor/RichText.js)
  286. return mark_safe(u'<div%s>%s</div>' % (flatatt(final_attrs),
  287. force_unicode(value))) # we don't escape the value for the editor
  288. class HorizontalSliderInput(TextInput):
  289. dojo_type = 'dijit.form.HorizontalSlider'
  290. valid_extra_attrs = [
  291. 'max_value',
  292. 'min_value',
  293. ]
  294. field_attr_map = {
  295. 'max_value': 'maximum',
  296. 'min_value': 'minimum',
  297. }
  298. def __init__(self, attrs=None):
  299. if dojo_config.version < '1.3':
  300. self.alt_require = 'dijit.form.Slider'
  301. super(HorizontalSliderInput, self).__init__(attrs)
  302. class VerticalSliderInput(HorizontalSliderInput):
  303. dojo_type = 'dijit.form.VerticalSlider'
  304. class ValidationTextInput(TextInput):
  305. dojo_type = 'dijit.form.ValidationTextBox'
  306. valid_extra_attrs = [
  307. 'required',
  308. 'help_text',
  309. 'js_regex',
  310. 'max_length',
  311. ]
  312. js_regex_func = None
  313. def render(self, name, value, attrs=None):
  314. if self.js_regex_func:
  315. attrs = self.build_attrs(attrs, regExpGen=self.js_regex_func)
  316. return super(ValidationTextInput, self).render(name, value, attrs)
  317. class ValidationPasswordInput(PasswordInput):
  318. dojo_type = 'dijit.form.ValidationTextBox'
  319. valid_extra_attrs = [
  320. 'required',
  321. 'help_text',
  322. 'js_regex',
  323. 'max_length',
  324. ]
  325. class EmailTextInput(ValidationTextInput):
  326. extra_dojo_require = [
  327. 'dojox.validate.regexp'
  328. ]
  329. js_regex_func = "dojox.validate.regexp.emailAddress"
  330. def __init__(self, attrs=None):
  331. if dojo_config.version < '1.3':
  332. self.js_regex_func = 'dojox.regexp.emailAddress'
  333. super(EmailTextInput, self).__init__(attrs)
  334. class IPAddressTextInput(ValidationTextInput):
  335. extra_dojo_require = [
  336. 'dojox.validate.regexp'
  337. ]
  338. js_regex_func = "dojox.validate.regexp.ipAddress"
  339. def __init__(self, attrs=None):
  340. if dojo_config.version < '1.3':
  341. self.js_regex_func = 'dojox.regexp.ipAddress'
  342. super(IPAddressTextInput, self).__init__(attrs)
  343. class URLTextInput(ValidationTextInput):
  344. extra_dojo_require = [
  345. 'dojox.validate.regexp'
  346. ]
  347. js_regex_func = "dojox.validate.regexp.url"
  348. def __init__(self, attrs=None):
  349. if dojo_config.version < '1.3':
  350. self.js_regex_func = 'dojox.regexp.url'
  351. super(URLTextInput, self).__init__(attrs)
  352. class NumberTextInput(TextInput):
  353. dojo_type = 'dijit.form.NumberTextBox'
  354. valid_extra_attrs = [
  355. 'min_value',
  356. 'max_value',
  357. 'required',
  358. 'help_text',
  359. 'decimal_places',
  360. ]
  361. class RangeBoundTextInput(NumberTextInput):
  362. dojo_type = 'dijit.form.RangeBoundTextBox'
  363. class NumberSpinnerInput(NumberTextInput):
  364. dojo_type = 'dijit.form.NumberSpinner'
  365. class RatingInput(TextInput):
  366. dojo_type = 'dojox.form.Rating'
  367. valid_extra_attrs = [
  368. 'max_value',
  369. ]
  370. field_attr_map = {
  371. 'max_value': 'numStars',
  372. }
  373. class Media:
  374. css = {
  375. 'all': ('%(base_url)s/dojox/form/resources/Rating.css' % {
  376. 'base_url':dojo_config.dojo_base_url
  377. },)
  378. }
  379. class DateInputAnim(DateInput):
  380. dojo_type = 'dojox.form.DateTextBox'
  381. class Media:
  382. css = {
  383. 'all': ('%(base_url)s/dojox/widget/Calendar/Calendar.css' % {
  384. 'base_url':dojo_config.dojo_base_url
  385. },)
  386. }
  387. class DropDownSelect(Select):
  388. dojo_type = 'dojox.form.DropDownSelect'
  389. valid_extra_attrs = []
  390. class Media:
  391. css = {
  392. 'all': ('%(base_url)s/dojox/form/resources/DropDownSelect.css' % {
  393. 'base_url':dojo_config.dojo_base_url
  394. },)
  395. }
  396. class CheckedMultiSelect(SelectMultiple):
  397. dojo_type = 'dojox.form.CheckedMultiSelect'
  398. valid_extra_attrs = []
  399. # TODO: fix attribute multiple=multiple
  400. # seems there is a dependency in dojox.form.CheckedMultiSelect for dijit.form.MultiSelect,
  401. # but CheckedMultiSelect is not extending that
  402. class Media:
  403. css = {
  404. 'all': ('%(base_url)s/dojox/form/resources/CheckedMultiSelect.css' % {
  405. 'base_url':dojo_config.dojo_base_url
  406. },)
  407. }
  408. class ComboBox(DojoWidgetMixin, widgets.Select):
  409. """Nearly the same as FilteringSelect, but ignoring the option value."""
  410. dojo_type = 'dijit.form.ComboBox'
  411. valid_extra_attrs = [
  412. 'required',
  413. 'help_text',
  414. ]
  415. class FilteringSelect(ComboBox):
  416. dojo_type = 'dijit.form.FilteringSelect'
  417. class ComboBoxStore(TextInput):
  418. """A combobox that is receiving data from a given dojo data url.
  419. As default dojo.data.ItemFileReadStore is used. You can overwrite
  420. that behaviour by passing a different store name
  421. (e.g. dojox.data.QueryReadStore).
  422. Usage:
  423. ComboBoxStore("/dojo-data-store-url/")
  424. """
  425. dojo_type = 'dijit.form.ComboBox'
  426. valid_extra_attrs = [
  427. 'required',
  428. 'help_text',
  429. ]
  430. store = 'dojo.data.ItemFileReadStore'
  431. store_attrs = {}
  432. url = None
  433. def __init__(self, url, attrs=None, store=None, store_attrs={}):
  434. self.url = url
  435. if store:
  436. self.store = store
  437. if store_attrs:
  438. self.store_attrs = store_attrs
  439. self.extra_dojo_require.append(self.store)
  440. super(ComboBoxStore, self).__init__(attrs)
  441. def render(self, name, value, attrs=None):
  442. if value is None: value = ''
  443. store_id = self.get_store_id(getattr(attrs, "id", None), name)
  444. final_attrs = self.build_attrs(attrs, type=self.input_type, name=name, store=store_id)
  445. if value != '':
  446. # Only add the 'value' attribute if a value is non-empty.
  447. final_attrs['value'] = force_unicode(self._format_value(value))
  448. self.store_attrs.update({
  449. 'dojoType': self.store,
  450. 'url': self.url,
  451. 'jsId':store_id
  452. })
  453. # TODO: convert store attributes to valid js-format (False => false, dict => {}, array = [])
  454. store_node = '<div%s></div>' % flatatt(self.store_attrs)
  455. return mark_safe(u'%s<input%s />' % (store_node, flatatt(final_attrs)))
  456. def get_store_id(self, id, name):
  457. return "_store_" + (id and id or name)
  458. class FilteringSelectStore(ComboBoxStore):
  459. dojo_type = 'dijit.form.FilteringSelect'
  460. class ListInput(DojoWidgetMixin, widgets.TextInput):
  461. dojo_type = 'dojox.form.ListInput'
  462. class Media:
  463. css = {
  464. 'all': ('%(base_url)s/dojox/form/resources/ListInput.css' % {
  465. 'base_url':dojo_config.dojo_base_url
  466. },)
  467. }
  468. # THE RANGE SLIDER NEEDS A DIFFERENT REPRESENTATION WITHIN HTML
  469. # SOMETHING LIKE:
  470. # <div dojoType="dojox.form.RangeSlider"><input value="5"/><input value="10"/></div>
  471. '''class HorizontalRangeSlider(HorizontalSliderInput):
  472. """This just can be used with a comma-separated-value like: 20,40"""
  473. dojo_type = 'dojox.form.HorizontalRangeSlider'
  474. alt_require = 'dojox.form.RangeSlider'
  475. class Media:
  476. css = {
  477. 'all': ('%(base_url)s/dojox/form/resources/RangeSlider.css' % {
  478. 'base_url':dojo_config.dojo_base_url
  479. },)
  480. }
  481. '''
  482. # TODO: implement
  483. # dojox.form.RangeSlider
  484. # dojox.form.MultiComboBox
  485. # dojox.form.FileUploader