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.

childadmin.py 9.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. """
  2. The child admin displays the change/delete view of the subclass model.
  3. """
  4. import inspect
  5. from django.contrib import admin
  6. from django.urls import resolve
  7. from django.utils.translation import gettext_lazy as _
  8. from polymorphic.utils import get_base_polymorphic_model
  9. from ..admin import PolymorphicParentModelAdmin
  10. class ParentAdminNotRegistered(RuntimeError):
  11. "The admin site for the model is not registered."
  12. class PolymorphicChildModelAdmin(admin.ModelAdmin):
  13. """
  14. The *optional* base class for the admin interface of derived models.
  15. This base class defines some convenience behavior for the admin interface:
  16. * It corrects the breadcrumbs in the admin pages.
  17. * It adds the base model to the template lookup paths.
  18. * It allows to set ``base_form`` so the derived class will automatically include other fields in the form.
  19. * It allows to set ``base_fieldsets`` so the derived class will automatically display any extra fields.
  20. """
  21. #: The base model that the class uses (auto-detected if not set explicitly)
  22. base_model = None
  23. #: By setting ``base_form`` instead of ``form``, any subclass fields are automatically added to the form.
  24. #: This is useful when your model admin class is inherited by others.
  25. base_form = None
  26. #: By setting ``base_fieldsets`` instead of ``fieldsets``,
  27. #: any subclass fields can be automatically added.
  28. #: This is useful when your model admin class is inherited by others.
  29. base_fieldsets = None
  30. #: Default title for extra fieldset
  31. extra_fieldset_title = _("Contents")
  32. #: Whether the child admin model should be visible in the admin index page.
  33. show_in_index = False
  34. def __init__(self, model, admin_site, *args, **kwargs):
  35. super().__init__(model, admin_site, *args, **kwargs)
  36. if self.base_model is None:
  37. self.base_model = get_base_polymorphic_model(model)
  38. def get_form(self, request, obj=None, **kwargs):
  39. # The django admin validation requires the form to have a 'class Meta: model = ..'
  40. # attribute, or it will complain that the fields are missing.
  41. # However, this enforces all derived ModelAdmin classes to redefine the model as well,
  42. # because they need to explicitly set the model again - it will stick with the base model.
  43. #
  44. # Instead, pass the form unchecked here, because the standard ModelForm will just work.
  45. # If the derived class sets the model explicitly, respect that setting.
  46. kwargs.setdefault("form", self.base_form or self.form)
  47. # prevent infinite recursion when this is called from get_subclass_fields
  48. if not self.fieldsets and not self.fields:
  49. kwargs.setdefault("fields", "__all__")
  50. return super().get_form(request, obj, **kwargs)
  51. def get_model_perms(self, request):
  52. match = resolve(request.path_info)
  53. if (
  54. not self.show_in_index
  55. and match.app_name == "admin"
  56. and match.url_name in ("index", "app_list")
  57. ):
  58. return {"add": False, "change": False, "delete": False}
  59. return super().get_model_perms(request)
  60. @property
  61. def change_form_template(self):
  62. opts = self.model._meta
  63. app_label = opts.app_label
  64. # Pass the base options
  65. base_opts = self.base_model._meta
  66. base_app_label = base_opts.app_label
  67. return [
  68. f"admin/{app_label}/{opts.object_name.lower()}/change_form.html",
  69. "admin/%s/change_form.html" % app_label,
  70. # Added:
  71. "admin/%s/%s/change_form.html" % (base_app_label, base_opts.object_name.lower()),
  72. "admin/%s/change_form.html" % base_app_label,
  73. "admin/polymorphic/change_form.html",
  74. "admin/change_form.html",
  75. ]
  76. @property
  77. def delete_confirmation_template(self):
  78. opts = self.model._meta
  79. app_label = opts.app_label
  80. # Pass the base options
  81. base_opts = self.base_model._meta
  82. base_app_label = base_opts.app_label
  83. return [
  84. "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()),
  85. "admin/%s/delete_confirmation.html" % app_label,
  86. # Added:
  87. "admin/%s/%s/delete_confirmation.html"
  88. % (base_app_label, base_opts.object_name.lower()),
  89. "admin/%s/delete_confirmation.html" % base_app_label,
  90. "admin/polymorphic/delete_confirmation.html",
  91. "admin/delete_confirmation.html",
  92. ]
  93. @property
  94. def object_history_template(self):
  95. opts = self.model._meta
  96. app_label = opts.app_label
  97. # Pass the base options
  98. base_opts = self.base_model._meta
  99. base_app_label = base_opts.app_label
  100. return [
  101. f"admin/{app_label}/{opts.object_name.lower()}/object_history.html",
  102. "admin/%s/object_history.html" % app_label,
  103. # Added:
  104. "admin/%s/%s/object_history.html" % (base_app_label, base_opts.object_name.lower()),
  105. "admin/%s/object_history.html" % base_app_label,
  106. "admin/polymorphic/object_history.html",
  107. "admin/object_history.html",
  108. ]
  109. def _get_parent_admin(self):
  110. # this returns parent admin instance on which to call response_post_save methods
  111. parent_model = self.model._meta.get_field("polymorphic_ctype").model
  112. if parent_model == self.model:
  113. # when parent_model is in among child_models, just return super instance
  114. return super()
  115. try:
  116. return self.admin_site._registry[parent_model]
  117. except KeyError:
  118. # Admin is not registered for polymorphic_ctype model, but perhaps it's registered
  119. # for a intermediate proxy model, between the parent_model and this model.
  120. for klass in inspect.getmro(self.model):
  121. if not issubclass(klass, parent_model):
  122. continue # e.g. found a mixin.
  123. # Fetch admin instance for model class, see if it's a possible candidate.
  124. model_admin = self.admin_site._registry.get(klass)
  125. if model_admin is not None and isinstance(
  126. model_admin, PolymorphicParentModelAdmin
  127. ):
  128. return model_admin # Success!
  129. # If we get this far without returning there is no admin available
  130. raise ParentAdminNotRegistered(
  131. f"No parent admin was registered for a '{parent_model}' model."
  132. )
  133. def response_post_save_add(self, request, obj):
  134. return self._get_parent_admin().response_post_save_add(request, obj)
  135. def response_post_save_change(self, request, obj):
  136. return self._get_parent_admin().response_post_save_change(request, obj)
  137. def render_change_form(self, request, context, add=False, change=False, form_url="", obj=None):
  138. context.update({"base_opts": self.base_model._meta})
  139. return super().render_change_form(
  140. request, context, add=add, change=change, form_url=form_url, obj=obj
  141. )
  142. def delete_view(self, request, object_id, context=None):
  143. extra_context = {"base_opts": self.base_model._meta}
  144. return super().delete_view(request, object_id, extra_context)
  145. def history_view(self, request, object_id, extra_context=None):
  146. # Make sure the history view can also display polymorphic breadcrumbs
  147. context = {"base_opts": self.base_model._meta}
  148. if extra_context:
  149. context.update(extra_context)
  150. return super().history_view(request, object_id, extra_context=context)
  151. # ---- Extra: improving the form/fieldset default display ----
  152. def get_base_fieldsets(self, request, obj=None):
  153. return self.base_fieldsets
  154. def get_fieldsets(self, request, obj=None):
  155. base_fieldsets = self.get_base_fieldsets(request, obj)
  156. # If subclass declares fieldsets or fields, this is respected
  157. if self.fieldsets or self.fields or not self.base_fieldsets:
  158. return super().get_fieldsets(request, obj)
  159. # Have a reasonable default fieldsets,
  160. # where the subclass fields are automatically included.
  161. other_fields = self.get_subclass_fields(request, obj)
  162. if other_fields:
  163. return (
  164. base_fieldsets[0],
  165. (self.extra_fieldset_title, {"fields": other_fields}),
  166. ) + base_fieldsets[1:]
  167. else:
  168. return base_fieldsets
  169. def get_subclass_fields(self, request, obj=None):
  170. # Find out how many fields would really be on the form,
  171. # if it weren't restricted by declared fields.
  172. exclude = list(self.exclude or [])
  173. exclude.extend(self.get_readonly_fields(request, obj))
  174. # By not declaring the fields/form in the base class,
  175. # get_form() will populate the form with all available fields.
  176. form = self.get_form(request, obj, exclude=exclude)
  177. subclass_fields = list(form.base_fields.keys()) + list(
  178. self.get_readonly_fields(request, obj)
  179. )
  180. # Find which fields are not part of the common fields.
  181. for fieldset in self.get_base_fieldsets(request, obj):
  182. for field in fieldset[1]["fields"]:
  183. try:
  184. subclass_fields.remove(field)
  185. except ValueError:
  186. pass # field not found in form, Django will raise exception later.
  187. return subclass_fields