You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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