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.

base.py 8.7KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. """
  2. PolymorphicModel Meta Class
  3. """
  4. import inspect
  5. import os
  6. import sys
  7. import warnings
  8. import django
  9. from django.core.exceptions import ImproperlyConfigured
  10. from django.db import models
  11. from django.db.models.base import ModelBase
  12. from django.db.models.manager import ManagerDescriptor
  13. from .managers import PolymorphicManager
  14. from .query import PolymorphicQuerySet
  15. # PolymorphicQuerySet Q objects (and filter()) support these additional key words.
  16. # These are forbidden as field names (a descriptive exception is raised)
  17. POLYMORPHIC_SPECIAL_Q_KWORDS = ["instance_of", "not_instance_of"]
  18. DUMPDATA_COMMAND = os.path.join("django", "core", "management", "commands", "dumpdata.py")
  19. class ManagerInheritanceWarning(RuntimeWarning):
  20. pass
  21. ###################################################################################
  22. # PolymorphicModel meta class
  23. class PolymorphicModelBase(ModelBase):
  24. """
  25. Manager inheritance is a pretty complex topic which may need
  26. more thought regarding how this should be handled for polymorphic
  27. models.
  28. In any case, we probably should propagate 'objects' and 'base_objects'
  29. from PolymorphicModel to every subclass. We also want to somehow
  30. inherit/propagate _default_manager as well, as it needs to be polymorphic.
  31. The current implementation below is an experiment to solve this
  32. problem with a very simplistic approach: We unconditionally
  33. inherit/propagate any and all managers (using _copy_to_model),
  34. as long as they are defined on polymorphic models
  35. (the others are left alone).
  36. Like Django ModelBase, we special-case _default_manager:
  37. if there are any user-defined managers, it is set to the first of these.
  38. We also require that _default_manager as well as any user defined
  39. polymorphic managers produce querysets that are derived from
  40. PolymorphicQuerySet.
  41. """
  42. def __new__(self, model_name, bases, attrs, **kwargs):
  43. # print; print '###', model_name, '- bases:', bases
  44. # Workaround compatibility issue with six.with_metaclass() and custom Django model metaclasses:
  45. if not attrs and model_name == "NewBase":
  46. return super().__new__(self, model_name, bases, attrs, **kwargs)
  47. # Make sure that manager_inheritance_from_future is set, since django-polymorphic 1.x already
  48. # simulated that behavior on the polymorphic manager to all subclasses behave like polymorphics
  49. if django.VERSION < (2, 0):
  50. if "Meta" in attrs:
  51. if not hasattr(attrs["Meta"], "manager_inheritance_from_future"):
  52. attrs["Meta"].manager_inheritance_from_future = True
  53. else:
  54. attrs["Meta"] = type("Meta", (object,), {"manager_inheritance_from_future": True})
  55. # create new model
  56. new_class = self.call_superclass_new_method(model_name, bases, attrs, **kwargs)
  57. # check if the model fields are all allowed
  58. self.validate_model_fields(new_class)
  59. # validate resulting default manager
  60. if not new_class._meta.abstract and not new_class._meta.swapped:
  61. self.validate_model_manager(new_class.objects, model_name, "objects")
  62. # for __init__ function of this class (monkeypatching inheritance accessors)
  63. new_class.polymorphic_super_sub_accessors_replaced = False
  64. # determine the name of the primary key field and store it into the class variable
  65. # polymorphic_primary_key_name (it is needed by query.py)
  66. for f in new_class._meta.fields:
  67. if f.primary_key and type(f) != models.OneToOneField:
  68. new_class.polymorphic_primary_key_name = f.name
  69. break
  70. return new_class
  71. @classmethod
  72. def call_superclass_new_method(self, model_name, bases, attrs, **kwargs):
  73. """call __new__ method of super class and return the newly created class.
  74. Also work around a limitation in Django's ModelBase."""
  75. # There seems to be a general limitation in Django's app_label handling
  76. # regarding abstract models (in ModelBase). See issue 1 on github - TODO: propose patch for Django
  77. # We run into this problem if polymorphic.py is located in a top-level directory
  78. # which is directly in the python path. To work around this we temporarily set
  79. # app_label here for PolymorphicModel.
  80. meta = attrs.get("Meta", None)
  81. do_app_label_workaround = (
  82. meta
  83. and attrs["__module__"] == "polymorphic"
  84. and model_name == "PolymorphicModel"
  85. and getattr(meta, "app_label", None) is None
  86. )
  87. if do_app_label_workaround:
  88. meta.app_label = "poly_dummy_app_label"
  89. new_class = super().__new__(self, model_name, bases, attrs, **kwargs)
  90. if do_app_label_workaround:
  91. del meta.app_label
  92. return new_class
  93. @classmethod
  94. def validate_model_fields(self, new_class):
  95. "check if all fields names are allowed (i.e. not in POLYMORPHIC_SPECIAL_Q_KWORDS)"
  96. for f in new_class._meta.fields:
  97. if f.name in POLYMORPHIC_SPECIAL_Q_KWORDS:
  98. e = 'PolymorphicModel: "%s" - field name "%s" is not allowed in polymorphic models'
  99. raise AssertionError(e % (new_class.__name__, f.name))
  100. @classmethod
  101. def validate_model_manager(self, manager, model_name, manager_name):
  102. """check if the manager is derived from PolymorphicManager
  103. and its querysets from PolymorphicQuerySet - throw AssertionError if not"""
  104. if not issubclass(type(manager), PolymorphicManager):
  105. if django.VERSION < (2, 0):
  106. extra = "\nConsider using Meta.manager_inheritance_from_future = True for Django 1.x projects"
  107. else:
  108. extra = ""
  109. e = (
  110. 'PolymorphicModel: "{0}.{1}" manager is of type "{2}", but must be a subclass of'
  111. " PolymorphicManager.{extra} to support retrieving subclasses".format(
  112. model_name, manager_name, type(manager).__name__, extra=extra
  113. )
  114. )
  115. warnings.warn(e, ManagerInheritanceWarning, stacklevel=3)
  116. return manager
  117. if not getattr(manager, "queryset_class", None) or not issubclass(
  118. manager.queryset_class, PolymorphicQuerySet
  119. ):
  120. e = (
  121. 'PolymorphicModel: "{}.{}" has been instantiated with a queryset class '
  122. "which is not a subclass of PolymorphicQuerySet (which is required)".format(
  123. model_name, manager_name
  124. )
  125. )
  126. warnings.warn(e, ManagerInheritanceWarning, stacklevel=3)
  127. return manager
  128. @property
  129. def base_objects(self):
  130. warnings.warn(
  131. "Using PolymorphicModel.base_objects is deprecated.\n"
  132. "Use {}.objects.non_polymorphic() instead.".format(self.__class__.__name__),
  133. DeprecationWarning,
  134. stacklevel=2,
  135. )
  136. return self._base_objects
  137. @property
  138. def _base_objects(self):
  139. # Create a manager so the API works as expected. Just don't register it
  140. # anymore in the Model Meta, so it doesn't substitute our polymorphic
  141. # manager as default manager for the third level of inheritance when
  142. # that third level doesn't define a manager at all.
  143. manager = models.Manager()
  144. manager.name = "base_objects"
  145. manager.model = self
  146. return manager
  147. @property
  148. def _default_manager(self):
  149. if len(sys.argv) > 1 and sys.argv[1] == "dumpdata":
  150. # TODO: investigate Django how this can be avoided
  151. # hack: a small patch to Django would be a better solution.
  152. # Django's management command 'dumpdata' relies on non-polymorphic
  153. # behaviour of the _default_manager. Therefore, we catch any access to _default_manager
  154. # here and return the non-polymorphic default manager instead if we are called from 'dumpdata.py'
  155. # Otherwise, the base objects will be upcasted to polymorphic models, and be outputted as such.
  156. # (non-polymorphic default manager is 'base_objects' for polymorphic models).
  157. # This way we don't need to patch django.core.management.commands.dumpdata
  158. # for all supported Django versions.
  159. frm = inspect.stack()[1] # frm[1] is caller file name, frm[3] is caller function name
  160. if DUMPDATA_COMMAND in frm[1]:
  161. return self._base_objects
  162. manager = super()._default_manager
  163. if not isinstance(manager, PolymorphicManager):
  164. warnings.warn(
  165. "{}._default_manager is not a PolymorphicManager".format(self.__class__.__name__),
  166. ManagerInheritanceWarning,
  167. )
  168. return manager