helpers.py 16 KB

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