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.

inlines.py 10KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. """
  2. Django Admin support for polymorphic inlines.
  3. Each row in the inline can correspond with a different subclass.
  4. """
  5. from functools import partial
  6. from django.conf import settings
  7. from django.contrib.admin.options import InlineModelAdmin
  8. from django.contrib.admin.utils import flatten_fieldsets
  9. from django.core.exceptions import ImproperlyConfigured
  10. from django.forms import Media
  11. from polymorphic.formsets import (
  12. BasePolymorphicInlineFormSet,
  13. PolymorphicFormSetChild,
  14. UnsupportedChildType,
  15. polymorphic_child_forms_factory,
  16. )
  17. from polymorphic.formsets.utils import add_media
  18. from .helpers import PolymorphicInlineSupportMixin
  19. class PolymorphicInlineModelAdmin(InlineModelAdmin):
  20. """
  21. A polymorphic inline, where each formset row can be a different form.
  22. Note that:
  23. * Permissions are only checked on the base model.
  24. * The child inlines can't override the base model fields, only this parent inline can do that.
  25. """
  26. formset = BasePolymorphicInlineFormSet
  27. #: The extra media to add for the polymorphic inlines effect.
  28. #: This can be redefined for subclasses.
  29. polymorphic_media = Media(
  30. js=(
  31. "admin/js/vendor/jquery/jquery{}.js".format("" if settings.DEBUG else ".min"),
  32. "admin/js/jquery.init.js",
  33. "polymorphic/js/polymorphic_inlines.js",
  34. ),
  35. css={"all": ("polymorphic/css/polymorphic_inlines.css",)},
  36. )
  37. #: The extra forms to show
  38. #: By default there are no 'extra' forms as the desired type is unknown.
  39. #: Instead, add each new item using JavaScript that first offers a type-selection.
  40. extra = 0
  41. #: Inlines for all model sub types that can be displayed in this inline.
  42. #: Each row is a :class:`PolymorphicInlineModelAdmin.Child`
  43. child_inlines = ()
  44. def __init__(self, parent_model, admin_site):
  45. super().__init__(parent_model, admin_site)
  46. # Extra check to avoid confusion
  47. # While we could monkeypatch the admin here, better stay explicit.
  48. parent_admin = admin_site._registry.get(parent_model, None)
  49. if parent_admin is not None: # Can be None during check
  50. if not isinstance(parent_admin, PolymorphicInlineSupportMixin):
  51. raise ImproperlyConfigured(
  52. "To use polymorphic inlines, add the `PolymorphicInlineSupportMixin` mixin "
  53. "to the ModelAdmin that hosts the inline."
  54. )
  55. # While the inline is created per request, the 'request' object is not known here.
  56. # Hence, creating all child inlines unconditionally, without checking permissions.
  57. self.child_inline_instances = self.get_child_inline_instances()
  58. # Create a lookup table
  59. self._child_inlines_lookup = {}
  60. for child_inline in self.child_inline_instances:
  61. self._child_inlines_lookup[child_inline.model] = child_inline
  62. def get_child_inline_instances(self):
  63. """
  64. :rtype List[PolymorphicInlineModelAdmin.Child]
  65. """
  66. instances = []
  67. for ChildInlineType in self.child_inlines:
  68. instances.append(ChildInlineType(parent_inline=self))
  69. return instances
  70. def get_child_inline_instance(self, model):
  71. """
  72. Find the child inline for a given model.
  73. :rtype: PolymorphicInlineModelAdmin.Child
  74. """
  75. try:
  76. return self._child_inlines_lookup[model]
  77. except KeyError:
  78. raise UnsupportedChildType(f"Model '{model.__name__}' not found in child_inlines")
  79. def get_formset(self, request, obj=None, **kwargs):
  80. """
  81. Construct the inline formset class.
  82. This passes all class attributes to the formset.
  83. :rtype: type
  84. """
  85. # Construct the FormSet class
  86. FormSet = super().get_formset(request, obj=obj, **kwargs)
  87. # Instead of completely redefining super().get_formset(), we use
  88. # the regular inlineformset_factory(), and amend that with our extra bits.
  89. # This code line is the essence of what polymorphic_inlineformset_factory() does.
  90. FormSet.child_forms = polymorphic_child_forms_factory(
  91. formset_children=self.get_formset_children(request, obj=obj)
  92. )
  93. return FormSet
  94. def get_formset_children(self, request, obj=None):
  95. """
  96. The formset 'children' provide the details for all child models that are part of this formset.
  97. It provides a stripped version of the modelform/formset factory methods.
  98. """
  99. formset_children = []
  100. for child_inline in self.child_inline_instances:
  101. # TODO: the children can be limited here per request based on permissions.
  102. formset_children.append(child_inline.get_formset_child(request, obj=obj))
  103. return formset_children
  104. def get_fieldsets(self, request, obj=None):
  105. """
  106. Hook for specifying fieldsets.
  107. """
  108. if self.fieldsets:
  109. return self.fieldsets
  110. else:
  111. return [] # Avoid exposing fields to the child
  112. def get_fields(self, request, obj=None):
  113. if self.fields:
  114. return self.fields
  115. else:
  116. return [] # Avoid exposing fields to the child
  117. @property
  118. def media(self):
  119. # The media of the inline focuses on the admin settings,
  120. # whether to expose the scripts for filter_horizontal etc..
  121. # The admin helper exposes the inline + formset media.
  122. base_media = super().media
  123. all_media = Media()
  124. add_media(all_media, base_media)
  125. # Add all media of the child inline instances
  126. for child_instance in self.child_inline_instances:
  127. child_media = child_instance.media
  128. # Avoid adding the same media object again and again
  129. if child_media._css != base_media._css and child_media._js != base_media._js:
  130. add_media(all_media, child_media)
  131. add_media(all_media, self.polymorphic_media)
  132. return all_media
  133. class Child(InlineModelAdmin):
  134. """
  135. The child inline; which allows configuring the admin options
  136. for the child appearance.
  137. Note that not all options will be honored by the parent, notably the formset options:
  138. * :attr:`extra`
  139. * :attr:`min_num`
  140. * :attr:`max_num`
  141. The model form options however, will all be read.
  142. """
  143. formset_child = PolymorphicFormSetChild
  144. extra = 0 # TODO: currently unused for the children.
  145. def __init__(self, parent_inline):
  146. self.parent_inline = parent_inline
  147. super(PolymorphicInlineModelAdmin.Child, self).__init__(
  148. parent_inline.parent_model, parent_inline.admin_site
  149. )
  150. def get_formset(self, request, obj=None, **kwargs):
  151. # The child inline is only used to construct the form,
  152. # and allow to override the form field attributes.
  153. # The formset is created by the parent inline.
  154. raise RuntimeError("The child get_formset() is not used.")
  155. def get_fields(self, request, obj=None):
  156. if self.fields:
  157. return self.fields
  158. # Standard Django logic, use the form to determine the fields.
  159. # The form needs to pass through all factory logic so all 'excludes' are set as well.
  160. # Default Django does: form = self.get_formset(request, obj, fields=None).form
  161. # Use 'fields=None' avoids recursion in the field autodetection.
  162. form = self.get_formset_child(request, obj, fields=None).get_form()
  163. return list(form.base_fields) + list(self.get_readonly_fields(request, obj))
  164. def get_formset_child(self, request, obj=None, **kwargs):
  165. """
  166. Return the formset child that the parent inline can use to represent us.
  167. :rtype: PolymorphicFormSetChild
  168. """
  169. # Similar to the normal get_formset(), the caller may pass fields to override the defaults settings
  170. # in the inline. In Django's GenericInlineModelAdmin.get_formset() this is also used in the same way,
  171. # to make sure the 'exclude' also contains the GFK fields.
  172. #
  173. # Hence this code is almost identical to InlineModelAdmin.get_formset()
  174. # and GenericInlineModelAdmin.get_formset()
  175. #
  176. # Transfer the local inline attributes to the formset child,
  177. # this allows overriding settings.
  178. if "fields" in kwargs:
  179. fields = kwargs.pop("fields")
  180. else:
  181. fields = flatten_fieldsets(self.get_fieldsets(request, obj))
  182. if self.exclude is None:
  183. exclude = []
  184. else:
  185. exclude = list(self.exclude)
  186. exclude.extend(self.get_readonly_fields(request, obj))
  187. # Add forcefully, as Django 1.10 doesn't include readonly fields.
  188. exclude.append("polymorphic_ctype")
  189. if self.exclude is None and hasattr(self.form, "_meta") and self.form._meta.exclude:
  190. # Take the custom ModelForm's Meta.exclude into account only if the
  191. # InlineModelAdmin doesn't define its own.
  192. exclude.extend(self.form._meta.exclude)
  193. # can_delete = self.can_delete and self.has_delete_permission(request, obj)
  194. defaults = {
  195. "form": self.form,
  196. "fields": fields,
  197. "exclude": exclude or None,
  198. "formfield_callback": partial(self.formfield_for_dbfield, request=request),
  199. }
  200. defaults.update(kwargs)
  201. # This goes through the same logic that get_formset() calls
  202. # by passing the inline class attributes to modelform_factory()
  203. FormSetChildClass = self.formset_child
  204. return FormSetChildClass(self.model, **defaults)
  205. class StackedPolymorphicInline(PolymorphicInlineModelAdmin):
  206. """
  207. Stacked inline for django-polymorphic models.
  208. Since tabular doesn't make much sense with changed fields, just offer this one.
  209. """
  210. #: The default template to use.
  211. template = "admin/polymorphic/edit_inline/stacked.html"