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.

helpers.py 15KB

5 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. import json
  2. from django import forms
  3. from django.conf import settings
  4. from django.contrib.admin.utils import (
  5. display_for_field, flatten_fieldsets, help_text_for_field, label_for_field,
  6. lookup_field,
  7. )
  8. from django.core.exceptions import ObjectDoesNotExist
  9. from django.db.models.fields.related import ManyToManyRel
  10. from django.forms.utils import flatatt
  11. from django.template.defaultfilters import capfirst, linebreaksbr
  12. from django.utils.html import conditional_escape, format_html
  13. from django.utils.safestring import mark_safe
  14. from django.utils.translation import gettext, gettext_lazy as _
  15. ACTION_CHECKBOX_NAME = '_selected_action'
  16. class ActionForm(forms.Form):
  17. action = forms.ChoiceField(label=_('Action:'))
  18. select_across = forms.BooleanField(
  19. label='',
  20. required=False,
  21. initial=0,
  22. widget=forms.HiddenInput({'class': 'select-across'}),
  23. )
  24. checkbox = forms.CheckboxInput({'class': 'action-select'}, lambda value: False)
  25. class AdminForm:
  26. def __init__(self, form, fieldsets, prepopulated_fields, readonly_fields=None, model_admin=None):
  27. self.form, self.fieldsets = form, fieldsets
  28. self.prepopulated_fields = [{
  29. 'field': form[field_name],
  30. 'dependencies': [form[f] for f in dependencies]
  31. } for field_name, dependencies in prepopulated_fields.items()]
  32. self.model_admin = model_admin
  33. if readonly_fields is None:
  34. readonly_fields = ()
  35. self.readonly_fields = readonly_fields
  36. def __iter__(self):
  37. for name, options in self.fieldsets:
  38. yield Fieldset(
  39. self.form, name,
  40. readonly_fields=self.readonly_fields,
  41. model_admin=self.model_admin,
  42. **options
  43. )
  44. @property
  45. def errors(self):
  46. return self.form.errors
  47. @property
  48. def non_field_errors(self):
  49. return self.form.non_field_errors
  50. @property
  51. def media(self):
  52. media = self.form.media
  53. for fs in self:
  54. media = media + fs.media
  55. return media
  56. class Fieldset:
  57. def __init__(self, form, name=None, readonly_fields=(), fields=(), classes=(),
  58. description=None, model_admin=None):
  59. self.form = form
  60. self.name, self.fields = name, fields
  61. self.classes = ' '.join(classes)
  62. self.description = description
  63. self.model_admin = model_admin
  64. self.readonly_fields = readonly_fields
  65. @property
  66. def media(self):
  67. if 'collapse' in self.classes:
  68. extra = '' if settings.DEBUG else '.min'
  69. return forms.Media(js=['admin/js/collapse%s.js' % extra])
  70. return forms.Media()
  71. def __iter__(self):
  72. for field in self.fields:
  73. yield Fieldline(self.form, field, self.readonly_fields, model_admin=self.model_admin)
  74. class Fieldline:
  75. def __init__(self, form, field, readonly_fields=None, model_admin=None):
  76. self.form = form # A django.forms.Form instance
  77. if not hasattr(field, "__iter__") or isinstance(field, str):
  78. self.fields = [field]
  79. else:
  80. self.fields = field
  81. self.has_visible_field = not all(
  82. field in self.form.fields and self.form.fields[field].widget.is_hidden
  83. for field in self.fields
  84. )
  85. self.model_admin = model_admin
  86. if readonly_fields is None:
  87. readonly_fields = ()
  88. self.readonly_fields = readonly_fields
  89. def __iter__(self):
  90. for i, field in enumerate(self.fields):
  91. if field in self.readonly_fields:
  92. yield AdminReadonlyField(self.form, field, is_first=(i == 0), model_admin=self.model_admin)
  93. else:
  94. yield AdminField(self.form, field, is_first=(i == 0))
  95. def errors(self):
  96. return mark_safe(
  97. '\n'.join(
  98. self.form[f].errors.as_ul() for f in self.fields if f not in self.readonly_fields
  99. ).strip('\n')
  100. )
  101. class AdminField:
  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. self.is_readonly = False
  107. def label_tag(self):
  108. classes = []
  109. contents = conditional_escape(self.field.label)
  110. if self.is_checkbox:
  111. classes.append('vCheckboxLabel')
  112. if self.field.field.required:
  113. classes.append('required')
  114. if not self.is_first:
  115. classes.append('inline')
  116. attrs = {'class': ' '.join(classes)} if classes else {}
  117. # checkboxes should not have a label suffix as the checkbox appears
  118. # to the left of the label.
  119. return self.field.label_tag(
  120. contents=mark_safe(contents), attrs=attrs,
  121. label_suffix='' if self.is_checkbox else None,
  122. )
  123. def errors(self):
  124. return mark_safe(self.field.errors.as_ul())
  125. class AdminReadonlyField:
  126. def __init__(self, form, field, is_first, model_admin=None):
  127. # Make self.field look a little bit like a field. This means that
  128. # {{ field.name }} must be a useful class name to identify the field.
  129. # For convenience, store other field-related data here too.
  130. if callable(field):
  131. class_name = field.__name__ if field.__name__ != '<lambda>' else ''
  132. else:
  133. class_name = field
  134. if form._meta.labels and class_name in form._meta.labels:
  135. label = form._meta.labels[class_name]
  136. else:
  137. label = label_for_field(field, form._meta.model, model_admin, form=form)
  138. if form._meta.help_texts and class_name in form._meta.help_texts:
  139. help_text = form._meta.help_texts[class_name]
  140. else:
  141. help_text = help_text_for_field(class_name, form._meta.model)
  142. self.field = {
  143. 'name': class_name,
  144. 'label': label,
  145. 'help_text': help_text,
  146. 'field': field,
  147. }
  148. self.form = form
  149. self.model_admin = model_admin
  150. self.is_first = is_first
  151. self.is_checkbox = False
  152. self.is_readonly = True
  153. self.empty_value_display = model_admin.get_empty_value_display()
  154. def label_tag(self):
  155. attrs = {}
  156. if not self.is_first:
  157. attrs["class"] = "inline"
  158. label = self.field['label']
  159. return format_html('<label{}>{}:</label>', flatatt(attrs), capfirst(label))
  160. def contents(self):
  161. from django.contrib.admin.templatetags.admin_list import _boolean_icon
  162. field, obj, model_admin = self.field['field'], self.form.instance, self.model_admin
  163. try:
  164. f, attr, value = lookup_field(field, obj, model_admin)
  165. except (AttributeError, ValueError, ObjectDoesNotExist):
  166. result_repr = self.empty_value_display
  167. else:
  168. if field in self.form.fields:
  169. widget = self.form[field].field.widget
  170. # This isn't elegant but suffices for contrib.auth's
  171. # ReadOnlyPasswordHashWidget.
  172. if getattr(widget, 'read_only', False):
  173. return widget.render(field, value)
  174. if f is None:
  175. if getattr(attr, 'boolean', False):
  176. result_repr = _boolean_icon(value)
  177. else:
  178. if hasattr(value, "__html__"):
  179. result_repr = value
  180. else:
  181. result_repr = linebreaksbr(value)
  182. else:
  183. if isinstance(f.remote_field, ManyToManyRel) and value is not None:
  184. result_repr = ", ".join(map(str, value.all()))
  185. else:
  186. result_repr = display_for_field(value, f, self.empty_value_display)
  187. result_repr = linebreaksbr(result_repr)
  188. return conditional_escape(result_repr)
  189. class InlineAdminFormSet:
  190. """
  191. A wrapper around an inline formset for use in the admin system.
  192. """
  193. def __init__(self, inline, formset, fieldsets, prepopulated_fields=None,
  194. readonly_fields=None, model_admin=None, has_add_permission=True,
  195. has_change_permission=True, has_delete_permission=True,
  196. has_view_permission=True):
  197. self.opts = inline
  198. self.formset = formset
  199. self.fieldsets = fieldsets
  200. self.model_admin = model_admin
  201. if readonly_fields is None:
  202. readonly_fields = ()
  203. self.readonly_fields = readonly_fields
  204. if prepopulated_fields is None:
  205. prepopulated_fields = {}
  206. self.prepopulated_fields = prepopulated_fields
  207. self.classes = ' '.join(inline.classes) if inline.classes else ''
  208. self.has_add_permission = has_add_permission
  209. self.has_change_permission = has_change_permission
  210. self.has_delete_permission = has_delete_permission
  211. self.has_view_permission = has_view_permission
  212. def __iter__(self):
  213. if self.has_change_permission:
  214. readonly_fields_for_editing = self.readonly_fields
  215. else:
  216. readonly_fields_for_editing = self.readonly_fields + flatten_fieldsets(self.fieldsets)
  217. for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
  218. view_on_site_url = self.opts.get_view_on_site_url(original)
  219. yield InlineAdminForm(
  220. self.formset, form, self.fieldsets, self.prepopulated_fields,
  221. original, readonly_fields_for_editing, model_admin=self.opts,
  222. view_on_site_url=view_on_site_url,
  223. )
  224. for form in self.formset.extra_forms:
  225. yield InlineAdminForm(
  226. self.formset, form, self.fieldsets, self.prepopulated_fields,
  227. None, self.readonly_fields, model_admin=self.opts,
  228. )
  229. if self.has_add_permission:
  230. yield InlineAdminForm(
  231. self.formset, self.formset.empty_form,
  232. self.fieldsets, self.prepopulated_fields, None,
  233. self.readonly_fields, model_admin=self.opts,
  234. )
  235. def fields(self):
  236. fk = getattr(self.formset, "fk", None)
  237. empty_form = self.formset.empty_form
  238. meta_labels = empty_form._meta.labels or {}
  239. meta_help_texts = empty_form._meta.help_texts or {}
  240. for i, field_name in enumerate(flatten_fieldsets(self.fieldsets)):
  241. if fk and fk.name == field_name:
  242. continue
  243. if not self.has_change_permission or field_name in self.readonly_fields:
  244. yield {
  245. 'name': field_name,
  246. 'label': meta_labels.get(field_name) or label_for_field(field_name, self.opts.model, self.opts),
  247. 'widget': {'is_hidden': False},
  248. 'required': False,
  249. 'help_text': meta_help_texts.get(field_name) or help_text_for_field(field_name, self.opts.model),
  250. }
  251. else:
  252. form_field = empty_form.fields[field_name]
  253. label = form_field.label
  254. if label is None:
  255. label = label_for_field(field_name, self.opts.model, self.opts)
  256. yield {
  257. 'name': field_name,
  258. 'label': label,
  259. 'widget': form_field.widget,
  260. 'required': form_field.required,
  261. 'help_text': form_field.help_text,
  262. }
  263. def inline_formset_data(self):
  264. verbose_name = self.opts.verbose_name
  265. return json.dumps({
  266. 'name': '#%s' % self.formset.prefix,
  267. 'options': {
  268. 'prefix': self.formset.prefix,
  269. 'addText': gettext('Add another %(verbose_name)s') % {
  270. 'verbose_name': capfirst(verbose_name),
  271. },
  272. 'deleteText': gettext('Remove'),
  273. }
  274. })
  275. @property
  276. def forms(self):
  277. return self.formset.forms
  278. @property
  279. def non_form_errors(self):
  280. return self.formset.non_form_errors
  281. @property
  282. def media(self):
  283. media = self.opts.media + self.formset.media
  284. for fs in self:
  285. media = media + fs.media
  286. return media
  287. class InlineAdminForm(AdminForm):
  288. """
  289. A wrapper around an inline form for use in the admin system.
  290. """
  291. def __init__(self, formset, form, fieldsets, prepopulated_fields, original,
  292. readonly_fields=None, model_admin=None, view_on_site_url=None):
  293. self.formset = formset
  294. self.model_admin = model_admin
  295. self.original = original
  296. self.show_url = original and view_on_site_url is not None
  297. self.absolute_url = view_on_site_url
  298. super().__init__(form, fieldsets, prepopulated_fields, readonly_fields, model_admin)
  299. def __iter__(self):
  300. for name, options in self.fieldsets:
  301. yield InlineFieldset(
  302. self.formset, self.form, name, self.readonly_fields,
  303. model_admin=self.model_admin, **options
  304. )
  305. def needs_explicit_pk_field(self):
  306. return (
  307. # Auto fields are editable, so check for auto or non-editable pk.
  308. self.form._meta.model._meta.auto_field or not self.form._meta.model._meta.pk.editable or
  309. # Also search any parents for an auto field. (The pk info is
  310. # propagated to child models so that does not need to be checked
  311. # in parents.)
  312. any(parent._meta.auto_field or not parent._meta.model._meta.pk.editable
  313. for parent in self.form._meta.model._meta.get_parent_list())
  314. )
  315. def pk_field(self):
  316. return AdminField(self.form, self.formset._pk_field.name, False)
  317. def fk_field(self):
  318. fk = getattr(self.formset, "fk", None)
  319. if fk:
  320. return AdminField(self.form, fk.name, False)
  321. else:
  322. return ""
  323. def deletion_field(self):
  324. from django.forms.formsets import DELETION_FIELD_NAME
  325. return AdminField(self.form, DELETION_FIELD_NAME, False)
  326. def ordering_field(self):
  327. from django.forms.formsets import ORDERING_FIELD_NAME
  328. return AdminField(self.form, ORDERING_FIELD_NAME, False)
  329. class InlineFieldset(Fieldset):
  330. def __init__(self, formset, *args, **kwargs):
  331. self.formset = formset
  332. super().__init__(*args, **kwargs)
  333. def __iter__(self):
  334. fk = getattr(self.formset, "fk", None)
  335. for field in self.fields:
  336. if not fk or fk.name != field:
  337. yield Fieldline(self.form, field, self.readonly_fields, model_admin=self.model_admin)
  338. class AdminErrorList(forms.utils.ErrorList):
  339. """Store errors for the form/formsets in an add/change view."""
  340. def __init__(self, form, inline_formsets):
  341. super().__init__()
  342. if form.is_bound:
  343. self.extend(form.errors.values())
  344. for inline_formset in inline_formsets:
  345. self.extend(inline_formset.non_form_errors())
  346. for errors_in_inline_form in inline_formset.errors:
  347. self.extend(errors_in_inline_form.values())