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

/src/oscar/forms/widgets.py

https://gitlab.com/kreativedev/django-oscar
Python | 344 lines | 323 code | 4 blank | 17 comment | 2 complexity | 587b14860e4d968c2d0e114468332ca4 MD5 | raw file
  1. import re
  2. from django import forms
  3. from django.core.files.uploadedfile import InMemoryUploadedFile
  4. from django.forms.utils import flatatt
  5. from django.forms.widgets import FileInput
  6. from django.template import Context
  7. from django.template.loader import render_to_string
  8. from django.utils import formats, six
  9. from django.utils.encoding import force_text
  10. from django.utils.html import format_html
  11. from django.utils.safestring import mark_safe
  12. from django.utils.six.moves import filter, map
  13. class ImageInput(FileInput):
  14. """
  15. Widget providing a input element for file uploads based on the
  16. Django ``FileInput`` element. It hides the actual browser-specific
  17. input element and shows the available image for images that have
  18. been previously uploaded. Selecting the image will open the file
  19. dialog and allow for selecting a new or replacing image file.
  20. """
  21. template_name = 'partials/image_input_widget.html'
  22. attrs = {'accept': 'image/*'}
  23. def render(self, name, value, attrs=None):
  24. """
  25. Render the ``input`` field based on the defined ``template_name``. The
  26. image URL is take from *value* and is provided to the template as
  27. ``image_url`` context variable relative to ``MEDIA_URL``. Further
  28. attributes for the ``input`` element are provide in ``input_attrs`` and
  29. contain parameters specified in *attrs* and *name*.
  30. If *value* contains no valid image URL an empty string will be provided
  31. in the context.
  32. """
  33. final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
  34. if not value or isinstance(value, InMemoryUploadedFile):
  35. # can't display images that aren't stored
  36. image_url = ''
  37. else:
  38. image_url = final_attrs['value'] = force_text(
  39. self._format_value(value))
  40. return render_to_string(self.template_name, Context({
  41. 'input_attrs': flatatt(final_attrs),
  42. 'image_url': image_url,
  43. 'image_id': "%s-image" % final_attrs['id'],
  44. }))
  45. class WYSIWYGTextArea(forms.Textarea):
  46. def __init__(self, *args, **kwargs):
  47. kwargs.setdefault('attrs', {})
  48. kwargs['attrs'].setdefault('class', '')
  49. kwargs['attrs']['class'] += ' wysiwyg'
  50. super(WYSIWYGTextArea, self).__init__(*args, **kwargs)
  51. def datetime_format_to_js_date_format(format):
  52. """
  53. Convert a Python datetime format to a date format suitable for use with
  54. the JS date picker we use.
  55. """
  56. format = format.split()[0]
  57. return datetime_format_to_js_datetime_format(format)
  58. def datetime_format_to_js_time_format(format):
  59. """
  60. Convert a Python datetime format to a time format suitable for use with the
  61. JS time picker we use.
  62. """
  63. try:
  64. format = format.split()[1]
  65. except IndexError:
  66. pass
  67. converted = format
  68. replacements = {
  69. '%H': 'hh',
  70. '%I': 'HH',
  71. '%M': 'ii',
  72. '%S': 'ss',
  73. }
  74. for search, replace in replacements.items():
  75. converted = converted.replace(search, replace)
  76. return converted.strip()
  77. def datetime_format_to_js_datetime_format(format):
  78. """
  79. Convert a Python datetime format to a time format suitable for use with
  80. the datetime picker we use, http://www.malot.fr/bootstrap-datetimepicker/.
  81. """
  82. converted = format
  83. replacements = {
  84. '%Y': 'yyyy',
  85. '%y': 'yy',
  86. '%m': 'mm',
  87. '%d': 'dd',
  88. '%H': 'hh',
  89. '%I': 'HH',
  90. '%M': 'ii',
  91. '%S': 'ss',
  92. }
  93. for search, replace in replacements.items():
  94. converted = converted.replace(search, replace)
  95. return converted.strip()
  96. def datetime_format_to_js_input_mask(format):
  97. # taken from
  98. # http://stackoverflow.com/questions/15175142/how-can-i-do-multiple-substitutions-using-regex-in-python # noqa
  99. def multiple_replace(dict, text):
  100. # Create a regular expression from the dictionary keys
  101. regex = re.compile("(%s)" % "|".join(map(re.escape, dict.keys())))
  102. # For each match, look-up corresponding value in dictionary
  103. return regex.sub(lambda mo: dict[mo.string[mo.start():mo.end()]], text)
  104. replacements = {
  105. '%Y': 'y',
  106. '%y': '99',
  107. '%m': 'm',
  108. '%d': 'd',
  109. '%H': 'h',
  110. '%I': 'h',
  111. '%M': 's',
  112. '%S': 's',
  113. }
  114. return multiple_replace(replacements, format).strip()
  115. class DateTimeWidgetMixin(object):
  116. def get_format(self):
  117. format = self.format
  118. if hasattr(self, 'manual_format'):
  119. # For django <= 1.6.5, see
  120. # https://code.djangoproject.com/ticket/21173
  121. if self.is_localized and not self.manual_format:
  122. format = force_text(formats.get_format(self.format_key)[0])
  123. else:
  124. # For django >= 1.7
  125. format = format or formats.get_format(self.format_key)[0]
  126. return format
  127. def gett_attrs(self, attrs, format):
  128. if not attrs:
  129. attrs = {}
  130. attrs['data-inputmask'] = "'mask': '{mask}'".format(
  131. mask=datetime_format_to_js_input_mask(format))
  132. return attrs
  133. class TimePickerInput(DateTimeWidgetMixin, forms.TimeInput):
  134. """
  135. A widget that passes the date format to the JS date picker in a data
  136. attribute.
  137. """
  138. format_key = 'TIME_INPUT_FORMATS'
  139. def render(self, name, value, attrs=None):
  140. format = self.get_format()
  141. input = super(TimePickerInput, self).render(
  142. name, value, self.gett_attrs(attrs, format))
  143. attrs = {'data-oscarWidget': 'time',
  144. 'data-timeFormat':
  145. datetime_format_to_js_time_format(format),
  146. }
  147. div = format_html('<div class="input-group date"{}>', flatatt(attrs))
  148. return mark_safe('<div class="form-inline">'
  149. ' {div}'
  150. ' {input}'
  151. ' <span class="input-group-addon">'
  152. ' <i class="icon-time glyphicon-time"></i>'
  153. ' </span>'
  154. ' </div>'
  155. '</div>'
  156. .format(div=div, input=input))
  157. class DatePickerInput(DateTimeWidgetMixin, forms.DateInput):
  158. """
  159. A widget that passes the date format to the JS date picker in a data
  160. attribute.
  161. """
  162. format_key = 'DATE_INPUT_FORMATS'
  163. def render(self, name, value, attrs=None):
  164. format = self.get_format()
  165. input = super(DatePickerInput, self).render(
  166. name, value, self.gett_attrs(attrs, format))
  167. attrs = {'data-oscarWidget': 'date',
  168. 'data-dateFormat':
  169. datetime_format_to_js_date_format(format),
  170. }
  171. div = format_html('<div class="input-group date"{}>', flatatt(attrs))
  172. return mark_safe('<div class="form-inline">'
  173. ' {div}'
  174. ' {input}'
  175. ' <span class="input-group-addon">'
  176. ' <i class="icon-calendar glyphicon-calendar"></i>'
  177. ' </span>'
  178. ' </div>'
  179. '</div>'
  180. .format(div=div, input=input))
  181. class DateTimePickerInput(DateTimeWidgetMixin, forms.DateTimeInput):
  182. """
  183. A widget that passes the datetime format to the JS datetime picker in a
  184. data attribute.
  185. It also removes seconds by default. However this only works with widgets
  186. without localize=True.
  187. For localized widgets refer to
  188. https://docs.djangoproject.com/en/1.6/topics/i18n/formatting/#creating-custom-format-files # noqa
  189. instead to override the format.
  190. """
  191. format_key = 'DATETIME_INPUT_FORMATS'
  192. def __init__(self, *args, **kwargs):
  193. include_seconds = kwargs.pop('include_seconds', False)
  194. super(DateTimePickerInput, self).__init__(*args, **kwargs)
  195. if not include_seconds and self.format:
  196. self.format = re.sub(':?%S', '', self.format)
  197. def render(self, name, value, attrs=None):
  198. format = self.get_format()
  199. input = super(DateTimePickerInput, self).render(
  200. name, value, self.gett_attrs(attrs, format))
  201. attrs = {'data-oscarWidget': 'datetime',
  202. 'data-datetimeFormat':
  203. datetime_format_to_js_datetime_format(format),
  204. }
  205. div = format_html('<div class="input-group date"{}>', flatatt(attrs))
  206. return mark_safe('<div class="form-inline">'
  207. ' {div}'
  208. ' {input}'
  209. ' <span class="input-group-addon">'
  210. ' <i class="icon-calendar glyphicon-calendar"></i>'
  211. ' </span>'
  212. ' </div>'
  213. '</div>'
  214. .format(div=div, input=input))
  215. class AdvancedSelect(forms.Select):
  216. """
  217. Customised Select widget that allows a list of disabled values to be passed
  218. to the constructor. Django's default Select widget doesn't allow this so
  219. we have to override the render_option method and add a section that checks
  220. for whether the widget is disabled.
  221. """
  222. def __init__(self, attrs=None, choices=(), disabled_values=()):
  223. self.disabled_values = set(force_text(v) for v in disabled_values)
  224. super(AdvancedSelect, self).__init__(attrs, choices)
  225. def render_option(self, selected_choices, option_value, option_label):
  226. option_value = force_text(option_value)
  227. if option_value in self.disabled_values:
  228. selected_html = mark_safe(' disabled="disabled"')
  229. elif option_value in selected_choices:
  230. selected_html = mark_safe(' selected="selected"')
  231. if not self.allow_multiple_selected:
  232. # Only allow for a single selection.
  233. selected_choices.remove(option_value)
  234. else:
  235. selected_html = ''
  236. return format_html(u'<option value="{0}"{1}>{2}</option>',
  237. option_value,
  238. selected_html,
  239. force_text(option_label))
  240. class RemoteSelect(forms.Widget):
  241. """
  242. Somewhat reusable widget that allows AJAX lookups in combination with
  243. select2.
  244. Requires setting the URL of a lookup view either as class attribute or when
  245. constructing
  246. """
  247. is_multiple = False
  248. lookup_url = None
  249. def __init__(self, *args, **kwargs):
  250. if 'lookup_url' in kwargs:
  251. self.lookup_url = kwargs.pop('lookup_url')
  252. if self.lookup_url is None:
  253. raise ValueError(
  254. "RemoteSelect requires a lookup ULR")
  255. super(RemoteSelect, self).__init__(*args, **kwargs)
  256. def format_value(self, value):
  257. return six.text_type(value or '')
  258. def value_from_datadict(self, data, files, name):
  259. value = data.get(name, None)
  260. if value is None:
  261. return value
  262. else:
  263. return six.text_type(value)
  264. def render(self, name, value, attrs=None, choices=()):
  265. attrs = self.build_attrs(attrs, **{
  266. 'type': 'hidden',
  267. 'name': name,
  268. 'data-ajax-url': self.lookup_url,
  269. 'data-multiple': 'multiple' if self.is_multiple else '',
  270. 'value': self.format_value(value),
  271. 'data-required': 'required' if self.is_required else '',
  272. })
  273. return mark_safe(u'<input %s>' % flatatt(attrs))
  274. class MultipleRemoteSelect(RemoteSelect):
  275. is_multiple = True
  276. def format_value(self, value):
  277. if value:
  278. return ','.join(map(six.text_type, filter(bool, value)))
  279. else:
  280. return ''
  281. def value_from_datadict(self, data, files, name):
  282. value = data.get(name, None)
  283. if value is None:
  284. return []
  285. else:
  286. return list(filter(bool, value.split(',')))