Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
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 18KB

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