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.

query_translate.py 11KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. """
  2. PolymorphicQuerySet support functions
  3. """
  4. import copy
  5. from collections import deque
  6. from django.apps import apps
  7. from django.contrib.contenttypes.models import ContentType
  8. from django.core.exceptions import FieldDoesNotExist, FieldError
  9. from django.db import models
  10. from django.db.models import Q
  11. from django.db.models.fields.related import ForeignObjectRel, RelatedField
  12. from django.db.utils import DEFAULT_DB_ALIAS
  13. # These functions implement the additional filter- and Q-object functionality.
  14. # They form a kind of small framework for easily adding more
  15. # functionality to filters and Q objects.
  16. # Probably a more general queryset enhancement class could be made out of them.
  17. from polymorphic import compat
  18. ###################################################################################
  19. # PolymorphicQuerySet support functions
  20. def translate_polymorphic_filter_definitions_in_kwargs(
  21. queryset_model, kwargs, using=DEFAULT_DB_ALIAS
  22. ):
  23. """
  24. Translate the keyword argument list for PolymorphicQuerySet.filter()
  25. Any kwargs with special polymorphic functionality are replaced in the kwargs
  26. dict with their vanilla django equivalents.
  27. For some kwargs a direct replacement is not possible, as a Q object is needed
  28. instead to implement the required functionality. In these cases the kwarg is
  29. deleted from the kwargs dict and a Q object is added to the return list.
  30. Modifies: kwargs dict
  31. Returns: a list of non-keyword-arguments (Q objects) to be added to the filter() query.
  32. """
  33. additional_args = []
  34. for field_path, val in kwargs.copy().items(): # Python 3 needs copy
  35. new_expr = _translate_polymorphic_filter_definition(
  36. queryset_model, field_path, val, using=using
  37. )
  38. if isinstance(new_expr, tuple):
  39. # replace kwargs element
  40. del kwargs[field_path]
  41. kwargs[new_expr[0]] = new_expr[1]
  42. elif isinstance(new_expr, models.Q):
  43. del kwargs[field_path]
  44. additional_args.append(new_expr)
  45. return additional_args
  46. def translate_polymorphic_Q_object(queryset_model, potential_q_object, using=DEFAULT_DB_ALIAS):
  47. def tree_node_correct_field_specs(my_model, node):
  48. "process all children of this Q node"
  49. for i in range(len(node.children)):
  50. child = node.children[i]
  51. if isinstance(child, (tuple, list)):
  52. # this Q object child is a tuple => a kwarg like Q( instance_of=ModelB )
  53. key, val = child
  54. new_expr = _translate_polymorphic_filter_definition(
  55. my_model, key, val, using=using
  56. )
  57. if new_expr:
  58. node.children[i] = new_expr
  59. else:
  60. # this Q object child is another Q object, recursively process this as well
  61. tree_node_correct_field_specs(my_model, child)
  62. if isinstance(potential_q_object, models.Q):
  63. tree_node_correct_field_specs(queryset_model, potential_q_object)
  64. return potential_q_object
  65. def translate_polymorphic_filter_definitions_in_args(queryset_model, args, using=DEFAULT_DB_ALIAS):
  66. """
  67. Translate the non-keyword argument list for PolymorphicQuerySet.filter()
  68. In the args list, we return all kwargs to Q-objects that contain special
  69. polymorphic functionality with their vanilla django equivalents.
  70. We traverse the Q object tree for this (which is simple).
  71. Returns: modified Q objects
  72. """
  73. return [
  74. translate_polymorphic_Q_object(queryset_model, copy.deepcopy(q), using=using) for q in args
  75. ]
  76. def _translate_polymorphic_filter_definition(
  77. queryset_model, field_path, field_val, using=DEFAULT_DB_ALIAS
  78. ):
  79. """
  80. Translate a keyword argument (field_path=field_val), as used for
  81. PolymorphicQuerySet.filter()-like functions (and Q objects).
  82. A kwarg with special polymorphic functionality is translated into
  83. its vanilla django equivalent, which is returned, either as tuple
  84. (field_path, field_val) or as Q object.
  85. Returns: kwarg tuple or Q object or None (if no change is required)
  86. """
  87. # handle instance_of expressions or alternatively,
  88. # if this is a normal Django filter expression, return None
  89. if field_path == "instance_of":
  90. return create_instanceof_q(field_val, using=using)
  91. elif field_path == "not_instance_of":
  92. return create_instanceof_q(field_val, not_instance_of=True, using=using)
  93. elif "___" not in field_path:
  94. return None # no change
  95. # filter expression contains '___' (i.e. filter for polymorphic field)
  96. # => get the model class specified in the filter expression
  97. newpath = translate_polymorphic_field_path(queryset_model, field_path)
  98. return (newpath, field_val)
  99. def translate_polymorphic_field_path(queryset_model, field_path):
  100. """
  101. Translate a field path from a keyword argument, as used for
  102. PolymorphicQuerySet.filter()-like functions (and Q objects).
  103. Supports leading '-' (for order_by args).
  104. E.g.: if queryset_model is ModelA, then "ModelC___field3" is translated
  105. into modela__modelb__modelc__field3.
  106. Returns: translated path (unchanged, if no translation needed)
  107. """
  108. if not isinstance(field_path, str):
  109. raise ValueError(f"Expected field name as string: {field_path}")
  110. classname, sep, pure_field_path = field_path.partition("___")
  111. if not sep:
  112. return field_path
  113. assert classname, "PolymorphicModel: %s: bad field specification" % field_path
  114. negated = False
  115. if classname[0] == "-":
  116. negated = True
  117. classname = classname.lstrip("-")
  118. if "__" in classname:
  119. # the user has app label prepended to class name via __ => use Django's get_model function
  120. appname, sep, classname = classname.partition("__")
  121. model = apps.get_model(appname, classname)
  122. assert model, "PolymorphicModel: model {} (in app {}) not found!".format(
  123. model.__name__,
  124. appname,
  125. )
  126. if not issubclass(model, queryset_model):
  127. e = (
  128. 'PolymorphicModel: queryset filter error: "'
  129. + model.__name__
  130. + '" is not derived from "'
  131. + queryset_model.__name__
  132. + '"'
  133. )
  134. raise AssertionError(e)
  135. else:
  136. # the user has only given us the class name via ___
  137. # => select the model from the sub models of the queryset base model
  138. # Test whether it's actually a regular relation__ _fieldname (the field starting with an _)
  139. # so no tripple ClassName___field was intended.
  140. try:
  141. # This also retreives M2M relations now (including reverse foreign key relations)
  142. field = queryset_model._meta.get_field(classname)
  143. if isinstance(field, (RelatedField, ForeignObjectRel)):
  144. # Can also test whether the field exists in the related object to avoid ambiguity between
  145. # class names and field names, but that never happens when your class names are in CamelCase.
  146. return field_path # No exception raised, field does exist.
  147. except FieldDoesNotExist:
  148. pass
  149. submodels = _get_all_sub_models(queryset_model)
  150. model = submodels.get(classname, None)
  151. assert model, "PolymorphicModel: model {} not found (not a subclass of {})!".format(
  152. classname,
  153. queryset_model.__name__,
  154. )
  155. basepath = _create_base_path(queryset_model, model)
  156. if negated:
  157. newpath = "-"
  158. else:
  159. newpath = ""
  160. newpath += basepath
  161. if basepath:
  162. newpath += "__"
  163. newpath += pure_field_path
  164. return newpath
  165. def _get_all_sub_models(base_model):
  166. """#Collect all sub-models, this should be optimized (cached)"""
  167. result = {}
  168. queue = deque([base_model])
  169. while queue:
  170. model = queue.popleft()
  171. if issubclass(model, models.Model) and model != models.Model:
  172. # model name is occurring twice in submodel inheritance tree => Error
  173. if model.__name__ in result and model != result[model.__name__]:
  174. raise FieldError(
  175. "PolymorphicModel: model name alone is ambiguous: %s.%s and %s.%s match!\n"
  176. "In this case, please use the syntax: applabel__ModelName___field"
  177. % (
  178. model._meta.app_label,
  179. model.__name__,
  180. result[model.__name__]._meta.app_label,
  181. result[model.__name__].__name__,
  182. )
  183. )
  184. result[model.__name__] = model
  185. queue.extend(model.__subclasses__())
  186. return result
  187. def _create_base_path(baseclass, myclass):
  188. # create new field path for expressions, e.g. for baseclass=ModelA, myclass=ModelC
  189. # 'modelb__modelc" is returned
  190. for b in myclass.__bases__:
  191. if b == baseclass:
  192. return _get_query_related_name(myclass)
  193. path = _create_base_path(baseclass, b)
  194. if path:
  195. if b._meta.abstract or b._meta.proxy:
  196. return _get_query_related_name(myclass)
  197. else:
  198. return path + "__" + _get_query_related_name(myclass)
  199. return ""
  200. def _get_query_related_name(myclass):
  201. for f in myclass._meta.local_fields:
  202. if isinstance(f, models.OneToOneField) and f.remote_field.parent_link:
  203. return f.related_query_name()
  204. # Fallback to undetected name,
  205. # this happens on proxy models (e.g. SubclassSelectorProxyModel)
  206. return myclass.__name__.lower()
  207. def create_instanceof_q(modellist, not_instance_of=False, using=DEFAULT_DB_ALIAS):
  208. """
  209. Helper function for instance_of / not_instance_of
  210. Creates and returns a Q object that filters for the models in modellist,
  211. including all subclasses of these models (as we want to do the same
  212. as pythons isinstance() ).
  213. .
  214. We recursively collect all __subclasses__(), create a Q filter for each,
  215. and or-combine these Q objects. This could be done much more
  216. efficiently however (regarding the resulting sql), should an optimization
  217. be needed.
  218. """
  219. if not modellist:
  220. return None
  221. if not isinstance(modellist, (list, tuple)):
  222. from .models import PolymorphicModel
  223. if issubclass(modellist, PolymorphicModel):
  224. modellist = [modellist]
  225. else:
  226. raise TypeError(
  227. "PolymorphicModel: instance_of expects a list of (polymorphic) "
  228. "models or a single (polymorphic) model"
  229. )
  230. contenttype_ids = _get_mro_content_type_ids(modellist, using)
  231. q = Q(polymorphic_ctype__in=sorted(contenttype_ids))
  232. if not_instance_of:
  233. q = ~q
  234. return q
  235. def _get_mro_content_type_ids(models, using):
  236. contenttype_ids = set()
  237. for model in models:
  238. ct = ContentType.objects.db_manager(using).get_for_model(model, for_concrete_model=False)
  239. contenttype_ids.add(ct.pk)
  240. subclasses = model.__subclasses__()
  241. if subclasses:
  242. contenttype_ids.update(_get_mro_content_type_ids(subclasses, using))
  243. return contenttype_ids