helpers.py 18 KB

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