Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

widgets.py 9.1KB

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