Development of an internal social media platform with personalised dashboards for students
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.

managers.py 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. from __future__ import unicode_literals
  2. from functools import total_ordering
  3. from operator import attrgetter
  4. from django import VERSION
  5. from django.conf import settings
  6. from django.contrib.contenttypes.fields import GenericRelation
  7. from django.contrib.contenttypes.models import ContentType
  8. from django.db import models, router
  9. from django.db.models import signals
  10. from django.db.models.fields import Field
  11. from django.db.models.fields.related import (ManyToManyRel, OneToOneRel,
  12. RelatedField,
  13. lazy_related_operation)
  14. from django.db.models.query_utils import PathInfo
  15. from django.utils import six
  16. from django.utils.text import capfirst
  17. from django.utils.translation import ugettext_lazy as _
  18. from taggit.forms import TagField
  19. from taggit.models import CommonGenericTaggedItemBase, TaggedItem
  20. from taggit.utils import require_instance_manager
  21. class TaggableRel(ManyToManyRel):
  22. def __init__(self, field, related_name, through, to=None):
  23. self.model = to
  24. self.related_name = related_name
  25. self.related_query_name = None
  26. self.limit_choices_to = {}
  27. self.symmetrical = True
  28. self.multiple = True
  29. self.through = through
  30. self.field = field
  31. self.through_fields = None
  32. def get_joining_columns(self):
  33. return self.field.get_reverse_joining_columns()
  34. def get_extra_restriction(self, where_class, alias, related_alias):
  35. return self.field.get_extra_restriction(where_class, related_alias, alias)
  36. class ExtraJoinRestriction(object):
  37. """
  38. An extra restriction used for contenttype restriction in joins.
  39. """
  40. contains_aggregate = False
  41. def __init__(self, alias, col, content_types):
  42. self.alias = alias
  43. self.col = col
  44. self.content_types = content_types
  45. def as_sql(self, compiler, connection):
  46. qn = compiler.quote_name_unless_alias
  47. if len(self.content_types) == 1:
  48. extra_where = "%s.%s = %%s" % (qn(self.alias), qn(self.col))
  49. else:
  50. extra_where = "%s.%s IN (%s)" % (qn(self.alias), qn(self.col),
  51. ','.join(['%s'] * len(self.content_types)))
  52. return extra_where, self.content_types
  53. def relabel_aliases(self, change_map):
  54. self.alias = change_map.get(self.alias, self.alias)
  55. def clone(self):
  56. return self.__class__(self.alias, self.col, self.content_types[:])
  57. class _TaggableManager(models.Manager):
  58. def __init__(self, through, model, instance, prefetch_cache_name):
  59. self.through = through
  60. self.model = model
  61. self.instance = instance
  62. self.prefetch_cache_name = prefetch_cache_name
  63. self._db = None
  64. def is_cached(self, instance):
  65. return self.prefetch_cache_name in instance._prefetched_objects_cache
  66. def get_queryset(self, extra_filters=None):
  67. try:
  68. return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
  69. except (AttributeError, KeyError):
  70. kwargs = extra_filters if extra_filters else {}
  71. return self.through.tags_for(self.model, self.instance, **kwargs)
  72. def get_prefetch_queryset(self, instances, queryset=None):
  73. if queryset is not None:
  74. raise ValueError("Custom queryset can't be used for this lookup.")
  75. instance = instances[0]
  76. from django.db import connections
  77. db = self._db or router.db_for_read(instance.__class__, instance=instance)
  78. fieldname = ('object_id' if issubclass(self.through, CommonGenericTaggedItemBase)
  79. else 'content_object')
  80. fk = self.through._meta.get_field(fieldname)
  81. query = {
  82. '%s__%s__in' % (self.through.tag_relname(), fk.name): {obj._get_pk_val() for obj in instances}
  83. }
  84. join_table = self.through._meta.db_table
  85. source_col = fk.column
  86. connection = connections[db]
  87. qn = connection.ops.quote_name
  88. qs = self.get_queryset(query).using(db).extra(
  89. select={
  90. '_prefetch_related_val': '%s.%s' % (qn(join_table), qn(source_col))
  91. }
  92. )
  93. if VERSION < (2, 0):
  94. return (
  95. qs,
  96. attrgetter('_prefetch_related_val'),
  97. lambda obj: obj._get_pk_val(),
  98. False,
  99. self.prefetch_cache_name,
  100. )
  101. else:
  102. return (
  103. qs,
  104. attrgetter('_prefetch_related_val'),
  105. lambda obj: obj._get_pk_val(),
  106. False,
  107. self.prefetch_cache_name,
  108. False,
  109. )
  110. def _lookup_kwargs(self):
  111. return self.through.lookup_kwargs(self.instance)
  112. @require_instance_manager
  113. def add(self, *tags):
  114. db = router.db_for_write(self.through, instance=self.instance)
  115. tag_objs = self._to_tag_model_instances(tags)
  116. new_ids = {t.pk for t in tag_objs}
  117. # NOTE: can we hardcode 'tag_id' here or should the column name be got
  118. # dynamically from somewhere?
  119. vals = (self.through._default_manager.using(db)
  120. .values_list('tag_id', flat=True)
  121. .filter(**self._lookup_kwargs()))
  122. new_ids = new_ids - set(vals)
  123. signals.m2m_changed.send(
  124. sender=self.through, action="pre_add",
  125. instance=self.instance, reverse=False,
  126. model=self.through.tag_model(), pk_set=new_ids, using=db,
  127. )
  128. for tag in tag_objs:
  129. self.through._default_manager.using(db).get_or_create(
  130. tag=tag, **self._lookup_kwargs())
  131. signals.m2m_changed.send(
  132. sender=self.through, action="post_add",
  133. instance=self.instance, reverse=False,
  134. model=self.through.tag_model(), pk_set=new_ids, using=db,
  135. )
  136. def _to_tag_model_instances(self, tags):
  137. """
  138. Takes an iterable containing either strings, tag objects, or a mixture
  139. of both and returns set of tag objects.
  140. """
  141. db = router.db_for_write(self.through, instance=self.instance)
  142. str_tags = set()
  143. tag_objs = set()
  144. for t in tags:
  145. if isinstance(t, self.through.tag_model()):
  146. tag_objs.add(t)
  147. elif isinstance(t, six.string_types):
  148. str_tags.add(t)
  149. else:
  150. raise ValueError(
  151. "Cannot add {0} ({1}). Expected {2} or str.".format(
  152. t, type(t), type(self.through.tag_model())))
  153. case_insensitive = getattr(settings, 'TAGGIT_CASE_INSENSITIVE', False)
  154. manager = self.through.tag_model()._default_manager.using(db)
  155. if case_insensitive:
  156. # Some databases can do case-insensitive comparison with IN, which
  157. # would be faster, but we can't rely on it or easily detect it.
  158. existing = []
  159. tags_to_create = []
  160. for name in str_tags:
  161. try:
  162. tag = manager.get(name__iexact=name)
  163. existing.append(tag)
  164. except self.through.tag_model().DoesNotExist:
  165. tags_to_create.append(name)
  166. else:
  167. # If str_tags has 0 elements Django actually optimizes that to not
  168. # do a query. Malcolm is very smart.
  169. existing = manager.filter(name__in=str_tags)
  170. tags_to_create = str_tags - {t.name for t in existing}
  171. tag_objs.update(existing)
  172. for new_tag in tags_to_create:
  173. if case_insensitive:
  174. try:
  175. tag = manager.get(name__iexact=new_tag)
  176. except self.through.tag_model().DoesNotExist:
  177. tag = manager.create(name=new_tag)
  178. else:
  179. tag = manager.create(name=new_tag)
  180. tag_objs.add(tag)
  181. return tag_objs
  182. @require_instance_manager
  183. def names(self):
  184. return self.get_queryset().values_list('name', flat=True)
  185. @require_instance_manager
  186. def slugs(self):
  187. return self.get_queryset().values_list('slug', flat=True)
  188. @require_instance_manager
  189. def set(self, *tags, **kwargs):
  190. """
  191. Set the object's tags to the given n tags. If the clear kwarg is True
  192. then all existing tags are removed (using `.clear()`) and the new tags
  193. added. Otherwise, only those tags that are not present in the args are
  194. removed and any new tags added.
  195. """
  196. db = router.db_for_write(self.through, instance=self.instance)
  197. clear = kwargs.pop('clear', False)
  198. if clear:
  199. self.clear()
  200. self.add(*tags)
  201. else:
  202. # make sure we're working with a collection of a uniform type
  203. objs = self._to_tag_model_instances(tags)
  204. # get the existing tag strings
  205. old_tag_strs = set(self.through._default_manager
  206. .using(db)
  207. .filter(**self._lookup_kwargs())
  208. .values_list('tag__name', flat=True))
  209. new_objs = []
  210. for obj in objs:
  211. if obj.name in old_tag_strs:
  212. old_tag_strs.remove(obj.name)
  213. else:
  214. new_objs.append(obj)
  215. self.remove(*old_tag_strs)
  216. self.add(*new_objs)
  217. @require_instance_manager
  218. def remove(self, *tags):
  219. if not tags:
  220. return
  221. db = router.db_for_write(self.through, instance=self.instance)
  222. qs = (self.through._default_manager.using(db)
  223. .filter(**self._lookup_kwargs())
  224. .filter(tag__name__in=tags))
  225. old_ids = set(qs.values_list('tag_id', flat=True))
  226. signals.m2m_changed.send(
  227. sender=self.through, action="pre_remove",
  228. instance=self.instance, reverse=False,
  229. model=self.through.tag_model(), pk_set=old_ids, using=db,
  230. )
  231. qs.delete()
  232. signals.m2m_changed.send(
  233. sender=self.through, action="post_remove",
  234. instance=self.instance, reverse=False,
  235. model=self.through.tag_model(), pk_set=old_ids, using=db,
  236. )
  237. @require_instance_manager
  238. def clear(self):
  239. db = router.db_for_write(self.through, instance=self.instance)
  240. signals.m2m_changed.send(
  241. sender=self.through, action="pre_clear",
  242. instance=self.instance, reverse=False,
  243. model=self.through.tag_model(), pk_set=None, using=db,
  244. )
  245. self.through._default_manager.using(db).filter(
  246. **self._lookup_kwargs()).delete()
  247. signals.m2m_changed.send(
  248. sender=self.through, action="post_clear",
  249. instance=self.instance, reverse=False,
  250. model=self.through.tag_model(), pk_set=None, using=db,
  251. )
  252. def most_common(self, min_count=None, extra_filters=None):
  253. queryset = self.get_queryset(extra_filters).annotate(
  254. num_times=models.Count(self.through.tag_relname())
  255. ).order_by('-num_times')
  256. if min_count:
  257. queryset = queryset.filter(num_times__gte=min_count)
  258. return queryset
  259. @require_instance_manager
  260. def similar_objects(self):
  261. lookup_kwargs = self._lookup_kwargs()
  262. lookup_keys = sorted(lookup_kwargs)
  263. qs = self.through.objects.values(*six.iterkeys(lookup_kwargs))
  264. qs = qs.annotate(n=models.Count('pk'))
  265. qs = qs.exclude(**lookup_kwargs)
  266. qs = qs.filter(tag__in=self.all())
  267. qs = qs.order_by('-n')
  268. # TODO: This all feels like a bit of a hack.
  269. items = {}
  270. if len(lookup_keys) == 1:
  271. # Can we do this without a second query by using a select_related()
  272. # somehow?
  273. f = self.through._meta.get_field(lookup_keys[0])
  274. remote_field = f.remote_field
  275. rel_model = remote_field.model
  276. objs = rel_model._default_manager.filter(**{
  277. "%s__in" % remote_field.field_name: [r["content_object"] for r in qs]
  278. })
  279. for obj in objs:
  280. items[(getattr(obj, remote_field.field_name),)] = obj
  281. else:
  282. preload = {}
  283. for result in qs:
  284. preload.setdefault(result['content_type'], set())
  285. preload[result["content_type"]].add(result["object_id"])
  286. for ct, obj_ids in preload.items():
  287. ct = ContentType.objects.get_for_id(ct)
  288. for obj in ct.model_class()._default_manager.filter(pk__in=obj_ids):
  289. items[(ct.pk, obj.pk)] = obj
  290. results = []
  291. for result in qs:
  292. obj = items[
  293. tuple(result[k] for k in lookup_keys)
  294. ]
  295. obj.similar_tags = result["n"]
  296. results.append(obj)
  297. return results
  298. @total_ordering
  299. class TaggableManager(RelatedField, Field):
  300. # Field flags
  301. many_to_many = True
  302. many_to_one = False
  303. one_to_many = False
  304. one_to_one = False
  305. _related_name_counter = 0
  306. def __init__(self, verbose_name=_("Tags"),
  307. help_text=_("A comma-separated list of tags."),
  308. through=None, blank=False, related_name=None, to=None,
  309. manager=_TaggableManager):
  310. self.through = through or TaggedItem
  311. self.swappable = False
  312. self.manager = manager
  313. rel = TaggableRel(self, related_name, self.through, to=to)
  314. Field.__init__(
  315. self,
  316. verbose_name=verbose_name,
  317. help_text=help_text,
  318. blank=blank,
  319. null=True,
  320. serialize=False,
  321. rel=rel,
  322. )
  323. # NOTE: `to` is ignored, only used via `deconstruct`.
  324. def __get__(self, instance, model):
  325. if instance is not None and instance.pk is None:
  326. raise ValueError("%s objects need to have a primary key value "
  327. "before you can access their tags." % model.__name__)
  328. manager = self.manager(
  329. through=self.through,
  330. model=model,
  331. instance=instance,
  332. prefetch_cache_name=self.name
  333. )
  334. return manager
  335. def deconstruct(self):
  336. """
  337. Deconstruct the object, used with migrations.
  338. """
  339. name, path, args, kwargs = super(TaggableManager, self).deconstruct()
  340. # Remove forced kwargs.
  341. for kwarg in ('serialize', 'null'):
  342. del kwargs[kwarg]
  343. # Add arguments related to relations.
  344. # Ref: https://github.com/alex/django-taggit/issues/206#issuecomment-37578676
  345. rel = self.remote_field
  346. if isinstance(rel.through, six.string_types):
  347. kwargs['through'] = rel.through
  348. elif not rel.through._meta.auto_created:
  349. kwargs['through'] = "%s.%s" % (rel.through._meta.app_label, rel.through._meta.object_name)
  350. related_model = rel.model
  351. if isinstance(related_model, six.string_types):
  352. kwargs['to'] = related_model
  353. else:
  354. kwargs['to'] = '%s.%s' % (related_model._meta.app_label, related_model._meta.object_name)
  355. return name, path, args, kwargs
  356. def contribute_to_class(self, cls, name):
  357. self.set_attributes_from_name(name)
  358. self.model = cls
  359. self.opts = cls._meta
  360. cls._meta.add_field(self)
  361. setattr(cls, name, self)
  362. if not cls._meta.abstract:
  363. if isinstance(self.remote_field.model, six.string_types):
  364. def resolve_related_class(cls, model, field):
  365. field.remote_field.model = model
  366. lazy_related_operation(
  367. resolve_related_class, cls, self.remote_field.model, field=self
  368. )
  369. if isinstance(self.through, six.string_types):
  370. def resolve_related_class(cls, model, field):
  371. self.through = model
  372. self.remote_field.through = model
  373. self.post_through_setup(cls)
  374. lazy_related_operation(
  375. resolve_related_class, cls, self.through, field=self
  376. )
  377. else:
  378. self.post_through_setup(cls)
  379. def get_internal_type(self):
  380. return 'ManyToManyField'
  381. def __lt__(self, other):
  382. """
  383. Required contribute_to_class as Django uses bisect
  384. for ordered class contribution and bisect requires
  385. a orderable type in py3.
  386. """
  387. return False
  388. def post_through_setup(self, cls):
  389. self.use_gfk = (
  390. self.through is None or issubclass(self.through, CommonGenericTaggedItemBase)
  391. )
  392. if not self.remote_field.model:
  393. self.remote_field.model = self.through._meta.get_field("tag").remote_field.model
  394. if self.use_gfk:
  395. tagged_items = GenericRelation(self.through)
  396. tagged_items.contribute_to_class(cls, 'tagged_items')
  397. for rel in cls._meta.local_many_to_many:
  398. if rel == self or not isinstance(rel, TaggableManager):
  399. continue
  400. if rel.through == self.through:
  401. raise ValueError('You can\'t have two TaggableManagers with the'
  402. ' same through model.')
  403. def save_form_data(self, instance, value):
  404. getattr(instance, self.name).set(*value)
  405. def formfield(self, form_class=TagField, **kwargs):
  406. defaults = {
  407. "label": capfirst(self.verbose_name),
  408. "help_text": self.help_text,
  409. "required": not self.blank
  410. }
  411. defaults.update(kwargs)
  412. return form_class(**defaults)
  413. def value_from_object(self, instance):
  414. if instance.pk:
  415. return self.through.objects.filter(**self.through.lookup_kwargs(instance))
  416. return self.through.objects.none()
  417. def related_query_name(self):
  418. return self.model._meta.model_name
  419. def m2m_reverse_name(self):
  420. return self.through._meta.get_field('tag').column
  421. def m2m_reverse_field_name(self):
  422. return self.through._meta.get_field('tag').name
  423. def m2m_target_field_name(self):
  424. return self.model._meta.pk.name
  425. def m2m_reverse_target_field_name(self):
  426. return self.remote_field.model._meta.pk.name
  427. def m2m_column_name(self):
  428. if self.use_gfk:
  429. return self.through._meta.virtual_fields[0].fk_field
  430. return self.through._meta.get_field('content_object').column
  431. def db_type(self, connection=None):
  432. return None
  433. def m2m_db_table(self):
  434. return self.through._meta.db_table
  435. def bulk_related_objects(self, new_objs, using):
  436. return []
  437. def extra_filters(self, pieces, pos, negate):
  438. if negate or not self.use_gfk:
  439. return []
  440. prefix = "__".join(["tagged_items"] + pieces[:pos - 2])
  441. get = ContentType.objects.get_for_model
  442. cts = [get(obj) for obj in _get_subclasses(self.model)]
  443. if len(cts) == 1:
  444. return [("%s__content_type" % prefix, cts[0])]
  445. return [("%s__content_type__in" % prefix, cts)]
  446. def get_extra_join_sql(self, connection, qn, lhs_alias, rhs_alias):
  447. model_name = self.through._meta.model_name
  448. if rhs_alias == '%s_%s' % (self.through._meta.app_label, model_name):
  449. alias_to_join = rhs_alias
  450. else:
  451. alias_to_join = lhs_alias
  452. extra_col = self.through._meta.get_field('content_type').column
  453. content_type_ids = [ContentType.objects.get_for_model(subclass).pk for
  454. subclass in _get_subclasses(self.model)]
  455. if len(content_type_ids) == 1:
  456. content_type_id = content_type_ids[0]
  457. extra_where = " AND %s.%s = %%s" % (qn(alias_to_join),
  458. qn(extra_col))
  459. params = [content_type_id]
  460. else:
  461. extra_where = " AND %s.%s IN (%s)" % (qn(alias_to_join),
  462. qn(extra_col),
  463. ','.join(['%s'] *
  464. len(content_type_ids)))
  465. params = content_type_ids
  466. return extra_where, params
  467. def _get_mm_case_path_info(self, direct=False, filtered_relation=None):
  468. pathinfos = []
  469. linkfield1 = self.through._meta.get_field('content_object')
  470. linkfield2 = self.through._meta.get_field(self.m2m_reverse_field_name())
  471. if direct:
  472. if VERSION < (2, 0):
  473. join1infos = linkfield1.get_reverse_path_info()
  474. join2infos = linkfield2.get_path_info()
  475. else:
  476. join1infos = linkfield1.get_reverse_path_info(filtered_relation=filtered_relation)
  477. join2infos = linkfield2.get_path_info(filtered_relation=filtered_relation)
  478. else:
  479. if VERSION < (2, 0):
  480. join1infos = linkfield2.get_reverse_path_info()
  481. join2infos = linkfield1.get_path_info()
  482. else:
  483. join1infos = linkfield2.get_reverse_path_info(filtered_relation=filtered_relation)
  484. join2infos = linkfield1.get_path_info(filtered_relation=filtered_relation)
  485. pathinfos.extend(join1infos)
  486. pathinfos.extend(join2infos)
  487. return pathinfos
  488. def _get_gfk_case_path_info(self, direct=False, filtered_relation=None):
  489. pathinfos = []
  490. from_field = self.model._meta.pk
  491. opts = self.through._meta
  492. linkfield = self.through._meta.get_field(self.m2m_reverse_field_name())
  493. if direct:
  494. if VERSION < (2, 0):
  495. join1infos = [PathInfo(self.model._meta, opts, [from_field], self.remote_field, True, False)]
  496. join2infos = linkfield.get_path_info()
  497. else:
  498. join1infos = [PathInfo(self.model._meta, opts, [from_field], self.remote_field, True, False, filtered_relation)]
  499. join2infos = linkfield.get_path_info(filtered_relation=filtered_relation)
  500. else:
  501. if VERSION < (2, 0):
  502. join1infos = linkfield.get_reverse_path_info()
  503. join2infos = [PathInfo(opts, self.model._meta, [from_field], self, True, False)]
  504. else:
  505. join1infos = linkfield.get_reverse_path_info(filtered_relation=filtered_relation)
  506. join2infos = [PathInfo(opts, self.model._meta, [from_field], self, True, False, filtered_relation)]
  507. pathinfos.extend(join1infos)
  508. pathinfos.extend(join2infos)
  509. return pathinfos
  510. def get_path_info(self, filtered_relation=None):
  511. if self.use_gfk:
  512. return self._get_gfk_case_path_info(direct=True, filtered_relation=filtered_relation)
  513. else:
  514. return self._get_mm_case_path_info(direct=True, filtered_relation=filtered_relation)
  515. def get_reverse_path_info(self, filtered_relation=None):
  516. if self.use_gfk:
  517. return self._get_gfk_case_path_info(direct=False, filtered_relation=filtered_relation)
  518. else:
  519. return self._get_mm_case_path_info(direct=False, filtered_relation=filtered_relation)
  520. def get_joining_columns(self, reverse_join=False):
  521. if reverse_join:
  522. return ((self.model._meta.pk.column, "object_id"),)
  523. else:
  524. return (("object_id", self.model._meta.pk.column),)
  525. def get_extra_restriction(self, where_class, alias, related_alias):
  526. extra_col = self.through._meta.get_field('content_type').column
  527. content_type_ids = [ContentType.objects.get_for_model(subclass).pk
  528. for subclass in _get_subclasses(self.model)]
  529. return ExtraJoinRestriction(related_alias, extra_col, content_type_ids)
  530. def get_reverse_joining_columns(self):
  531. return self.get_joining_columns(reverse_join=True)
  532. @property
  533. def related_fields(self):
  534. return [(self.through._meta.get_field('object_id'), self.model._meta.pk)]
  535. @property
  536. def foreign_related_fields(self):
  537. return [self.related_fields[0][1]]
  538. def _get_subclasses(model):
  539. subclasses = [model]
  540. for field in model._meta.get_fields():
  541. if isinstance(field, OneToOneRel) and getattr(field.field.remote_field, "parent_link", None):
  542. subclasses.extend(_get_subclasses(field.related_model))
  543. return subclasses