widgets.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. """
  2. Form Widget classes specific to the Django admin site.
  3. """
  4. from __future__ import unicode_literals
  5. import copy
  6. from django import forms
  7. from django.contrib.admin.templatetags.admin_static import static
  8. from django.core.urlresolvers import reverse
  9. from django.forms.widgets import RadioFieldRenderer
  10. from django.forms.utils import flatatt
  11. from django.utils.html import escape, format_html, format_html_join, smart_urlquote
  12. from django.utils.text import Truncator
  13. from django.utils.translation import ugettext as _
  14. from django.utils.safestring import mark_safe
  15. from django.utils.encoding import force_text
  16. from django.utils import six
  17. class FilteredSelectMultiple(forms.SelectMultiple):
  18. """
  19. A SelectMultiple with a JavaScript filter interface.
  20. Note that the resulting JavaScript assumes that the jsi18n
  21. catalog has been loaded in the page
  22. """
  23. @property
  24. def media(self):
  25. js = ["core.js", "SelectBox.js", "SelectFilter2.js"]
  26. return forms.Media(js=[static("admin/js/%s" % path) for path in js])
  27. def __init__(self, verbose_name, is_stacked, attrs=None, choices=()):
  28. self.verbose_name = verbose_name
  29. self.is_stacked = is_stacked
  30. super(FilteredSelectMultiple, self).__init__(attrs, choices)
  31. def render(self, name, value, attrs=None, choices=()):
  32. if attrs is None:
  33. attrs = {}
  34. attrs['class'] = 'selectfilter'
  35. if self.is_stacked:
  36. attrs['class'] += 'stacked'
  37. output = [super(FilteredSelectMultiple, self).render(name, value, attrs, choices)]
  38. output.append('<script type="text/javascript">addEvent(window, "load", function(e) {')
  39. # TODO: "id_" is hard-coded here. This should instead use the correct
  40. # API to determine the ID dynamically.
  41. output.append('SelectFilter.init("id_%s", "%s", %s, "%s"); });</script>\n'
  42. % (name, self.verbose_name.replace('"', '\\"'), int(self.is_stacked), static('admin/')))
  43. return mark_safe(''.join(output))
  44. class AdminDateWidget(forms.DateInput):
  45. @property
  46. def media(self):
  47. js = ["calendar.js", "admin/DateTimeShortcuts.js"]
  48. return forms.Media(js=[static("admin/js/%s" % path) for path in js])
  49. def __init__(self, attrs=None, format=None):
  50. final_attrs = {'class': 'vDateField', 'size': '10'}
  51. if attrs is not None:
  52. final_attrs.update(attrs)
  53. super(AdminDateWidget, self).__init__(attrs=final_attrs, format=format)
  54. class AdminTimeWidget(forms.TimeInput):
  55. @property
  56. def media(self):
  57. js = ["calendar.js", "admin/DateTimeShortcuts.js"]
  58. return forms.Media(js=[static("admin/js/%s" % path) for path in js])
  59. def __init__(self, attrs=None, format=None):
  60. final_attrs = {'class': 'vTimeField', 'size': '8'}
  61. if attrs is not None:
  62. final_attrs.update(attrs)
  63. super(AdminTimeWidget, self).__init__(attrs=final_attrs, format=format)
  64. class AdminSplitDateTime(forms.SplitDateTimeWidget):
  65. """
  66. A SplitDateTime Widget that has some admin-specific styling.
  67. """
  68. def __init__(self, attrs=None):
  69. widgets = [AdminDateWidget, AdminTimeWidget]
  70. # Note that we're calling MultiWidget, not SplitDateTimeWidget, because
  71. # we want to define widgets.
  72. forms.MultiWidget.__init__(self, widgets, attrs)
  73. def format_output(self, rendered_widgets):
  74. return format_html('<p class="datetime">{0} {1}<br />{2} {3}</p>',
  75. _('Date:'), rendered_widgets[0],
  76. _('Time:'), rendered_widgets[1])
  77. class AdminRadioFieldRenderer(RadioFieldRenderer):
  78. def render(self):
  79. """Outputs a <ul> for this set of radio fields."""
  80. return format_html('<ul{0}>\n{1}\n</ul>',
  81. flatatt(self.attrs),
  82. format_html_join('\n', '<li>{0}</li>',
  83. ((force_text(w),) for w in self)))
  84. class AdminRadioSelect(forms.RadioSelect):
  85. renderer = AdminRadioFieldRenderer
  86. class AdminFileWidget(forms.ClearableFileInput):
  87. template_with_initial = ('<p class="file-upload">%s</p>'
  88. % forms.ClearableFileInput.template_with_initial)
  89. template_with_clear = ('<span class="clearable-file-input">%s</span>'
  90. % forms.ClearableFileInput.template_with_clear)
  91. def url_params_from_lookup_dict(lookups):
  92. """
  93. Converts the type of lookups specified in a ForeignKey limit_choices_to
  94. attribute to a dictionary of query parameters
  95. """
  96. params = {}
  97. if lookups and hasattr(lookups, 'items'):
  98. items = []
  99. for k, v in lookups.items():
  100. if callable(v):
  101. v = v()
  102. if isinstance(v, (tuple, list)):
  103. v = ','.join(str(x) for x in v)
  104. elif isinstance(v, bool):
  105. # See django.db.fields.BooleanField.get_prep_lookup
  106. v = ('0', '1')[v]
  107. else:
  108. v = six.text_type(v)
  109. items.append((k, v))
  110. params.update(dict(items))
  111. return params
  112. class ForeignKeyRawIdWidget(forms.TextInput):
  113. """
  114. A Widget for displaying ForeignKeys in the "raw_id" interface rather than
  115. in a <select> box.
  116. """
  117. def __init__(self, rel, admin_site, attrs=None, using=None):
  118. self.rel = rel
  119. self.admin_site = admin_site
  120. self.db = using
  121. super(ForeignKeyRawIdWidget, self).__init__(attrs)
  122. def render(self, name, value, attrs=None):
  123. rel_to = self.rel.to
  124. if attrs is None:
  125. attrs = {}
  126. extra = []
  127. if rel_to in self.admin_site._registry:
  128. # The related object is registered with the same AdminSite
  129. related_url = reverse(
  130. 'admin:%s_%s_changelist' % (
  131. rel_to._meta.app_label,
  132. rel_to._meta.model_name,
  133. ),
  134. current_app=self.admin_site.name,
  135. )
  136. params = self.url_parameters()
  137. if params:
  138. url = '?' + '&amp;'.join('%s=%s' % (k, v) for k, v in params.items())
  139. else:
  140. url = ''
  141. if "class" not in attrs:
  142. attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript code looks for this hook.
  143. # TODO: "lookup_id_" is hard-coded here. This should instead use
  144. # the correct API to determine the ID dynamically.
  145. extra.append('<a href="%s%s" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> ' %
  146. (related_url, url, name))
  147. extra.append('<img src="%s" width="16" height="16" alt="%s" /></a>' %
  148. (static('admin/img/selector-search.gif'), _('Lookup')))
  149. output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)] + extra
  150. if value:
  151. output.append(self.label_for_value(value))
  152. return mark_safe(''.join(output))
  153. def base_url_parameters(self):
  154. limit_choices_to = self.rel.limit_choices_to
  155. if callable(limit_choices_to):
  156. limit_choices_to = limit_choices_to()
  157. return url_params_from_lookup_dict(limit_choices_to)
  158. def url_parameters(self):
  159. from django.contrib.admin.views.main import TO_FIELD_VAR
  160. params = self.base_url_parameters()
  161. params.update({TO_FIELD_VAR: self.rel.get_related_field().name})
  162. return params
  163. def label_for_value(self, value):
  164. key = self.rel.get_related_field().name
  165. try:
  166. obj = self.rel.to._default_manager.using(self.db).get(**{key: value})
  167. return '&nbsp;<strong>%s</strong>' % escape(Truncator(obj).words(14, truncate='...'))
  168. except (ValueError, self.rel.to.DoesNotExist):
  169. return ''
  170. class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
  171. """
  172. A Widget for displaying ManyToMany ids in the "raw_id" interface rather than
  173. in a <select multiple> box.
  174. """
  175. def render(self, name, value, attrs=None):
  176. if attrs is None:
  177. attrs = {}
  178. if self.rel.to in self.admin_site._registry:
  179. # The related object is registered with the same AdminSite
  180. attrs['class'] = 'vManyToManyRawIdAdminField'
  181. if value:
  182. value = ','.join(force_text(v) for v in value)
  183. else:
  184. value = ''
  185. return super(ManyToManyRawIdWidget, self).render(name, value, attrs)
  186. def url_parameters(self):
  187. return self.base_url_parameters()
  188. def label_for_value(self, value):
  189. return ''
  190. def value_from_datadict(self, data, files, name):
  191. value = data.get(name)
  192. if value:
  193. return value.split(',')
  194. class RelatedFieldWidgetWrapper(forms.Widget):
  195. """
  196. This class is a wrapper to a given widget to add the add icon for the
  197. admin interface.
  198. """
  199. def __init__(self, widget, rel, admin_site, can_add_related=None):
  200. self.needs_multipart_form = widget.needs_multipart_form
  201. self.attrs = widget.attrs
  202. self.choices = widget.choices
  203. self.widget = widget
  204. self.rel = rel
  205. # Backwards compatible check for whether a user can add related
  206. # objects.
  207. if can_add_related is None:
  208. can_add_related = rel.to in admin_site._registry
  209. self.can_add_related = can_add_related
  210. # so we can check if the related object is registered with this AdminSite
  211. self.admin_site = admin_site
  212. def __deepcopy__(self, memo):
  213. obj = copy.copy(self)
  214. obj.widget = copy.deepcopy(self.widget, memo)
  215. obj.attrs = self.widget.attrs
  216. memo[id(self)] = obj
  217. return obj
  218. @property
  219. def is_hidden(self):
  220. return self.widget.is_hidden
  221. @property
  222. def media(self):
  223. return self.widget.media
  224. def render(self, name, value, *args, **kwargs):
  225. from django.contrib.admin.views.main import TO_FIELD_VAR
  226. rel_to = self.rel.to
  227. info = (rel_to._meta.app_label, rel_to._meta.model_name)
  228. self.widget.choices = self.choices
  229. output = [self.widget.render(name, value, *args, **kwargs)]
  230. if self.can_add_related:
  231. related_url = reverse('admin:%s_%s_add' % info, current_app=self.admin_site.name)
  232. url_params = '?%s=%s' % (TO_FIELD_VAR, self.rel.get_related_field().name)
  233. # TODO: "add_id_" is hard-coded here. This should instead use the
  234. # correct API to determine the ID dynamically.
  235. output.append('<a href="%s%s" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> '
  236. % (related_url, url_params, name))
  237. output.append('<img src="%s" width="10" height="10" alt="%s"/></a>'
  238. % (static('admin/img/icon_addlink.gif'), _('Add Another')))
  239. return mark_safe(''.join(output))
  240. def build_attrs(self, extra_attrs=None, **kwargs):
  241. "Helper function for building an attribute dictionary."
  242. self.attrs = self.widget.build_attrs(extra_attrs=None, **kwargs)
  243. return self.attrs
  244. def value_from_datadict(self, data, files, name):
  245. return self.widget.value_from_datadict(data, files, name)
  246. def id_for_label(self, id_):
  247. return self.widget.id_for_label(id_)
  248. class AdminTextareaWidget(forms.Textarea):
  249. def __init__(self, attrs=None):
  250. final_attrs = {'class': 'vLargeTextField'}
  251. if attrs is not None:
  252. final_attrs.update(attrs)
  253. super(AdminTextareaWidget, self).__init__(attrs=final_attrs)
  254. class AdminTextInputWidget(forms.TextInput):
  255. def __init__(self, attrs=None):
  256. final_attrs = {'class': 'vTextField'}
  257. if attrs is not None:
  258. final_attrs.update(attrs)
  259. super(AdminTextInputWidget, self).__init__(attrs=final_attrs)
  260. class AdminEmailInputWidget(forms.EmailInput):
  261. def __init__(self, attrs=None):
  262. final_attrs = {'class': 'vTextField'}
  263. if attrs is not None:
  264. final_attrs.update(attrs)
  265. super(AdminEmailInputWidget, self).__init__(attrs=final_attrs)
  266. class AdminURLFieldWidget(forms.URLInput):
  267. def __init__(self, attrs=None):
  268. final_attrs = {'class': 'vURLField'}
  269. if attrs is not None:
  270. final_attrs.update(attrs)
  271. super(AdminURLFieldWidget, self).__init__(attrs=final_attrs)
  272. def render(self, name, value, attrs=None):
  273. html = super(AdminURLFieldWidget, self).render(name, value, attrs)
  274. if value:
  275. value = force_text(self._format_value(value))
  276. final_attrs = {'href': smart_urlquote(value)}
  277. html = format_html(
  278. '<p class="url">{0} <a{1}>{2}</a><br />{3} {4}</p>',
  279. _('Currently:'), flatatt(final_attrs), value,
  280. _('Change:'), html
  281. )
  282. return html
  283. class AdminIntegerFieldWidget(forms.TextInput):
  284. class_name = 'vIntegerField'
  285. def __init__(self, attrs=None):
  286. final_attrs = {'class': self.class_name}
  287. if attrs is not None:
  288. final_attrs.update(attrs)
  289. super(AdminIntegerFieldWidget, self).__init__(attrs=final_attrs)
  290. class AdminBigIntegerFieldWidget(AdminIntegerFieldWidget):
  291. class_name = 'vBigIntegerField'
  292. class AdminCommaSeparatedIntegerFieldWidget(forms.TextInput):
  293. def __init__(self, attrs=None):
  294. final_attrs = {'class': 'vCommaSeparatedIntegerField'}
  295. if attrs is not None:
  296. final_attrs.update(attrs)
  297. super(AdminCommaSeparatedIntegerFieldWidget, self).__init__(attrs=final_attrs)