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.
 
 
 
 

392 lines
14 KiB

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