helpers.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. from __future__ import unicode_literals
  2. from django import forms
  3. from django.contrib.admin.util import (flatten_fieldsets, lookup_field,
  4. display_for_field, label_for_field, help_text_for_field)
  5. from django.contrib.admin.templatetags.admin_static import static
  6. from django.contrib.contenttypes.models import ContentType
  7. from django.core.exceptions import ObjectDoesNotExist
  8. from django.db.models.fields.related import ManyToManyRel
  9. from django.forms.util import flatatt
  10. from django.template.defaultfilters import capfirst
  11. from django.utils.encoding import force_text, smart_text
  12. from django.utils.html import conditional_escape, format_html
  13. from django.utils.safestring import mark_safe
  14. from django.utils import six
  15. from django.utils.translation import ugettext_lazy as _
  16. from django.conf import settings
  17. ACTION_CHECKBOX_NAME = '_selected_action'
  18. class ActionForm(forms.Form):
  19. action = forms.ChoiceField(label=_('Action:'))
  20. select_across = forms.BooleanField(label='', required=False, initial=0,
  21. widget=forms.HiddenInput({'class': 'select-across'}))
  22. checkbox = forms.CheckboxInput({'class': 'action-select'}, lambda value: False)
  23. class AdminForm(object):
  24. def __init__(self, form, fieldsets, prepopulated_fields, readonly_fields=None, model_admin=None):
  25. self.form, self.fieldsets = form, normalize_fieldsets(fieldsets)
  26. self.prepopulated_fields = [{
  27. 'field': form[field_name],
  28. 'dependencies': [form[f] for f in dependencies]
  29. } for field_name, dependencies in prepopulated_fields.items()]
  30. self.model_admin = model_admin
  31. if readonly_fields is None:
  32. readonly_fields = ()
  33. self.readonly_fields = readonly_fields
  34. def __iter__(self):
  35. for name, options in self.fieldsets:
  36. yield Fieldset(self.form, name,
  37. readonly_fields=self.readonly_fields,
  38. model_admin=self.model_admin,
  39. **options
  40. )
  41. def first_field(self):
  42. try:
  43. fieldset_name, fieldset_options = self.fieldsets[0]
  44. field_name = fieldset_options['fields'][0]
  45. if not isinstance(field_name, six.string_types):
  46. field_name = field_name[0]
  47. return self.form[field_name]
  48. except (KeyError, IndexError):
  49. pass
  50. try:
  51. return next(iter(self.form))
  52. except StopIteration:
  53. return None
  54. def _media(self):
  55. media = self.form.media
  56. for fs in self:
  57. media = media + fs.media
  58. return media
  59. media = property(_media)
  60. class Fieldset(object):
  61. def __init__(self, form, name=None, readonly_fields=(), fields=(), classes=(),
  62. description=None, model_admin=None):
  63. self.form = form
  64. self.name, self.fields = name, fields
  65. self.classes = ' '.join(classes)
  66. self.description = description
  67. self.model_admin = model_admin
  68. self.readonly_fields = readonly_fields
  69. def _media(self):
  70. if 'collapse' in self.classes:
  71. extra = '' if settings.DEBUG else '.min'
  72. js = ['jquery%s.js' % extra,
  73. 'jquery.init.js',
  74. 'collapse%s.js' % extra]
  75. return forms.Media(js=[static('admin/js/%s' % url) for url in js])
  76. return forms.Media()
  77. media = property(_media)
  78. def __iter__(self):
  79. for field in self.fields:
  80. yield Fieldline(self.form, field, self.readonly_fields, model_admin=self.model_admin)
  81. class Fieldline(object):
  82. def __init__(self, form, field, readonly_fields=None, model_admin=None):
  83. self.form = form # A django.forms.Form instance
  84. if not hasattr(field, "__iter__"):
  85. self.fields = [field]
  86. else:
  87. self.fields = field
  88. self.model_admin = model_admin
  89. if readonly_fields is None:
  90. readonly_fields = ()
  91. self.readonly_fields = readonly_fields
  92. def __iter__(self):
  93. for i, field in enumerate(self.fields):
  94. if field in self.readonly_fields:
  95. yield AdminReadonlyField(self.form, field, is_first=(i == 0),
  96. model_admin=self.model_admin)
  97. else:
  98. yield AdminField(self.form, field, is_first=(i == 0))
  99. def errors(self):
  100. return mark_safe('\n'.join([self.form[f].errors.as_ul() for f in self.fields if f not in self.readonly_fields]).strip('\n'))
  101. class AdminField(object):
  102. def __init__(self, form, field, is_first):
  103. self.field = form[field] # A django.forms.BoundField instance
  104. self.is_first = is_first # Whether this field is first on the line
  105. self.is_checkbox = isinstance(self.field.field.widget, forms.CheckboxInput)
  106. def label_tag(self):
  107. classes = []
  108. contents = conditional_escape(force_text(self.field.label))
  109. if self.is_checkbox:
  110. classes.append('vCheckboxLabel')
  111. else:
  112. contents += ':'
  113. if self.field.field.required:
  114. classes.append('required')
  115. if not self.is_first:
  116. classes.append('inline')
  117. attrs = classes and {'class': ' '.join(classes)} or {}
  118. return self.field.label_tag(contents=mark_safe(contents), attrs=attrs)
  119. def errors(self):
  120. return mark_safe(self.field.errors.as_ul())
  121. class AdminReadonlyField(object):
  122. def __init__(self, form, field, is_first, model_admin=None):
  123. label = label_for_field(field, form._meta.model, model_admin)
  124. # Make self.field look a little bit like a field. This means that
  125. # {{ field.name }} must be a useful class name to identify the field.
  126. # For convenience, store other field-related data here too.
  127. if callable(field):
  128. class_name = field.__name__ != '<lambda>' and field.__name__ or ''
  129. else:
  130. class_name = field
  131. self.field = {
  132. 'name': class_name,
  133. 'label': label,
  134. 'field': field,
  135. 'help_text': help_text_for_field(class_name, form._meta.model)
  136. }
  137. self.form = form
  138. self.model_admin = model_admin
  139. self.is_first = is_first
  140. self.is_checkbox = False
  141. self.is_readonly = True
  142. def label_tag(self):
  143. attrs = {}
  144. if not self.is_first:
  145. attrs["class"] = "inline"
  146. label = self.field['label']
  147. return format_html('<label{0}>{1}:</label>',
  148. flatatt(attrs),
  149. capfirst(force_text(label)))
  150. def contents(self):
  151. from django.contrib.admin.templatetags.admin_list import _boolean_icon
  152. from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
  153. field, obj, model_admin = self.field['field'], self.form.instance, self.model_admin
  154. try:
  155. f, attr, value = lookup_field(field, obj, model_admin)
  156. except (AttributeError, ValueError, ObjectDoesNotExist):
  157. result_repr = EMPTY_CHANGELIST_VALUE
  158. else:
  159. if f is None:
  160. boolean = getattr(attr, "boolean", False)
  161. if boolean:
  162. result_repr = _boolean_icon(value)
  163. else:
  164. result_repr = smart_text(value)
  165. if getattr(attr, "allow_tags", False):
  166. result_repr = mark_safe(result_repr)
  167. else:
  168. if value is None:
  169. result_repr = EMPTY_CHANGELIST_VALUE
  170. elif isinstance(f.rel, ManyToManyRel):
  171. result_repr = ", ".join(map(six.text_type, value.all()))
  172. else:
  173. result_repr = display_for_field(value, f)
  174. return conditional_escape(result_repr)
  175. class InlineAdminFormSet(object):
  176. """
  177. A wrapper around an inline formset for use in the admin system.
  178. """
  179. def __init__(self, inline, formset, fieldsets, prepopulated_fields=None,
  180. readonly_fields=None, model_admin=None):
  181. self.opts = inline
  182. self.formset = formset
  183. self.fieldsets = fieldsets
  184. self.model_admin = model_admin
  185. if readonly_fields is None:
  186. readonly_fields = ()
  187. self.readonly_fields = readonly_fields
  188. if prepopulated_fields is None:
  189. prepopulated_fields = {}
  190. self.prepopulated_fields = prepopulated_fields
  191. def __iter__(self):
  192. for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
  193. yield InlineAdminForm(self.formset, form, self.fieldsets,
  194. self.prepopulated_fields, original, self.readonly_fields,
  195. model_admin=self.opts)
  196. for form in self.formset.extra_forms:
  197. yield InlineAdminForm(self.formset, form, self.fieldsets,
  198. self.prepopulated_fields, None, self.readonly_fields,
  199. model_admin=self.opts)
  200. yield InlineAdminForm(self.formset, self.formset.empty_form,
  201. self.fieldsets, self.prepopulated_fields, None,
  202. self.readonly_fields, model_admin=self.opts)
  203. def fields(self):
  204. fk = getattr(self.formset, "fk", None)
  205. for i, field in enumerate(flatten_fieldsets(self.fieldsets)):
  206. if fk and fk.name == field:
  207. continue
  208. if field in self.readonly_fields:
  209. yield {
  210. 'label': label_for_field(field, self.opts.model, self.opts),
  211. 'widget': {
  212. 'is_hidden': False
  213. },
  214. 'required': False
  215. }
  216. else:
  217. yield self.formset.form.base_fields[field]
  218. def _media(self):
  219. media = self.opts.media + self.formset.media
  220. for fs in self:
  221. media = media + fs.media
  222. return media
  223. media = property(_media)
  224. class InlineAdminForm(AdminForm):
  225. """
  226. A wrapper around an inline form for use in the admin system.
  227. """
  228. def __init__(self, formset, form, fieldsets, prepopulated_fields, original,
  229. readonly_fields=None, model_admin=None):
  230. self.formset = formset
  231. self.model_admin = model_admin
  232. self.original = original
  233. if original is not None:
  234. self.original_content_type_id = ContentType.objects.get_for_model(original).pk
  235. self.show_url = original and hasattr(original, 'get_absolute_url')
  236. super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields,
  237. readonly_fields, model_admin)
  238. def __iter__(self):
  239. for name, options in self.fieldsets:
  240. yield InlineFieldset(self.formset, self.form, name,
  241. self.readonly_fields, model_admin=self.model_admin, **options)
  242. def has_auto_field(self):
  243. if self.form._meta.model._meta.has_auto_field:
  244. return True
  245. # Also search any parents for an auto field.
  246. for parent in self.form._meta.model._meta.get_parent_list():
  247. if parent._meta.has_auto_field:
  248. return True
  249. return False
  250. def field_count(self):
  251. # tabular.html uses this function for colspan value.
  252. num_of_fields = 0
  253. if self.has_auto_field():
  254. num_of_fields += 1
  255. num_of_fields += len(self.fieldsets[0][1]["fields"])
  256. if self.formset.can_order:
  257. num_of_fields += 1
  258. if self.formset.can_delete:
  259. num_of_fields += 1
  260. return num_of_fields
  261. def pk_field(self):
  262. return AdminField(self.form, self.formset._pk_field.name, False)
  263. def fk_field(self):
  264. fk = getattr(self.formset, "fk", None)
  265. if fk:
  266. return AdminField(self.form, fk.name, False)
  267. else:
  268. return ""
  269. def deletion_field(self):
  270. from django.forms.formsets import DELETION_FIELD_NAME
  271. return AdminField(self.form, DELETION_FIELD_NAME, False)
  272. def ordering_field(self):
  273. from django.forms.formsets import ORDERING_FIELD_NAME
  274. return AdminField(self.form, ORDERING_FIELD_NAME, False)
  275. class InlineFieldset(Fieldset):
  276. def __init__(self, formset, *args, **kwargs):
  277. self.formset = formset
  278. super(InlineFieldset, self).__init__(*args, **kwargs)
  279. def __iter__(self):
  280. fk = getattr(self.formset, "fk", None)
  281. for field in self.fields:
  282. if fk and fk.name == field:
  283. continue
  284. yield Fieldline(self.form, field, self.readonly_fields,
  285. model_admin=self.model_admin)
  286. class AdminErrorList(forms.util.ErrorList):
  287. """
  288. Stores all errors for the form/formsets in an add/change stage view.
  289. """
  290. def __init__(self, form, inline_formsets):
  291. if form.is_bound:
  292. self.extend(list(six.itervalues(form.errors)))
  293. for inline_formset in inline_formsets:
  294. self.extend(inline_formset.non_form_errors())
  295. for errors_in_inline_form in inline_formset.errors:
  296. self.extend(list(six.itervalues(errors_in_inline_form)))
  297. def normalize_fieldsets(fieldsets):
  298. """
  299. Make sure the keys in fieldset dictionaries are strings. Returns the
  300. normalized data.
  301. """
  302. result = []
  303. for name, options in fieldsets:
  304. result.append((name, normalize_dictionary(options)))
  305. return result
  306. def normalize_dictionary(data_dict):
  307. """
  308. Converts all the keys in "data_dict" to strings. The keys must be
  309. convertible using str().
  310. """
  311. for key, value in data_dict.items():
  312. if not isinstance(key, str):
  313. del data_dict[key]
  314. data_dict[str(key)] = value
  315. return data_dict