helpers.py 18 KB

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