Coverage for apis_core/apis_entities/models.py: 44%

80 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2026-01-07 08:21 +0000

1import functools 

2import logging 

3from collections import defaultdict 

4 

5from django.apps import apps 

6from django.conf import settings 

7from django.contrib.contenttypes.models import ContentType 

8from django.db.models import Case, CharField, F, Q, Value, When 

9from django.db.models.base import ModelBase 

10from django.db.models.functions import Concat 

11from django.urls import NoReverseMatch, reverse 

12 

13from apis_core.apis_metainfo.models import RootObject 

14from apis_core.relations.models import Relation 

15from apis_core.utils.settings import apis_base_uri 

16 

17NEXT_PREV = getattr(settings, "APIS_NEXT_PREV", True) 

18 

19logger = logging.getLogger(__name__) 

20 

21 

22class AbstractEntityModelBase(ModelBase): 

23 def __new__(metacls, name, bases, attrs): 

24 if name == "AbstractEntity": 

25 return super().__new__(metacls, name, bases, attrs) 

26 else: 

27 new_class = super().__new__(metacls, name, bases, attrs) 

28 if not new_class._meta.ordering: 

29 logger.warning( 

30 f"{name} inherits from AbstractEntity but does not specify 'ordering' in its Meta class. " 

31 "Empty ordering could result in inconsitent results with pagination. " 

32 "Set a ordering or inherit the Meta class from AbstractEntity.", 

33 ) 

34 

35 return new_class 

36 

37 

38class AbstractEntity(RootObject, metaclass=AbstractEntityModelBase): 

39 """ 

40 Abstract super class which encapsulates common logic between the 

41 different entity kinds and provides various methods relating to either 

42 all or one specific entity kind. 

43 

44 Most of the class methods are designed to be used in the subclass as they 

45 are considering contexts which depend on the subclass entity type. 

46 So they are to be understood in that dynamic context. 

47 """ 

48 

49 class Meta: 

50 abstract = True 

51 

52 @functools.cached_property 

53 def get_prev_id(self): 

54 if NEXT_PREV: 

55 prev_instance = ( 

56 type(self) 

57 .objects.filter(id__lt=self.id) 

58 .order_by("-id") 

59 .only("id") 

60 .first() 

61 ) 

62 if prev_instance is not None: 

63 return prev_instance.id 

64 return False 

65 

66 @functools.cached_property 

67 def get_next_id(self): 

68 if NEXT_PREV: 

69 next_instance = ( 

70 type(self) 

71 .objects.filter(id__gt=self.id) 

72 .order_by("id") 

73 .only("id") 

74 .first() 

75 ) 

76 if next_instance is not None: 

77 return next_instance.id 

78 return False 

79 

80 def import_data(self, data): 

81 super().import_data(data) 

82 if "same_as" in data: 

83 self._uris = data["same_as"] 

84 self.save() 

85 if "relations" in data: 

86 self.create_relations_to_uris = data["relations"] 

87 self.save() 

88 

89 def get_default_uri(self): 

90 try: 

91 route = reverse("GetEntityGenericRoot", kwargs={"pk": self.pk}) 

92 except NoReverseMatch: 

93 route = reverse("apis_core:GetEntityGeneric", kwargs={"pk": self.pk}) 

94 base = apis_base_uri().strip("/") 

95 return f"{base}{route}" 

96 

97 @classmethod 

98 def get_facet_label(cls): 

99 if "db_string" in cls._meta.get_fields(): 

100 return F("db_string") 

101 return Concat( 

102 Value(f"{cls._meta.verbose_name.title()} "), 

103 F("pk"), 

104 output_field=CharField(), 

105 ) 

106 

107 @classmethod 

108 def get_facets(cls, queryset): 

109 facets = defaultdict(dict) 

110 if getattr(cls, "enable_facets", False): 

111 my_content_type = ContentType.objects.get_for_model(queryset.model) 

112 

113 # we filter all the relations that are somehow connected to instances of this queryset 

114 # we are only interested in the id and the type of the object the relations point to, 

115 # so we store those in `target_id` and `target_content_type` 

116 query_filter = Q( 

117 subj_content_type=my_content_type, subj_object_id__in=queryset 

118 ) | Q( 

119 Q(obj_content_type=my_content_type, obj_object_id__in=queryset), 

120 ~Q(subj_content_type=my_content_type), 

121 ) 

122 rels = ( 

123 Relation.objects.to_content_type_with_targets(my_content_type).values( 

124 "target_id", "target_content_type" 

125 ) 

126 ).filter(query_filter) 

127 

128 # we use the ids to get a the labels of the targets 

129 # and we store those in an id: label dict 

130 target_ids = [rel["target_id"] for rel in rels] 

131 entity_classes = list( 

132 filter(lambda x: issubclass(x, AbstractEntity), apps.get_models()) 

133 ) 

134 

135 # this should be moved to an EntityManager once the entites app is in apis_core 

136 when_clauses_classes = [ 

137 When( 

138 **{f"{cls.__name__.lower()}__isnull": False}, 

139 then=cls.get_facet_label(), 

140 ) 

141 for cls in entity_classes 

142 ] 

143 identifiers = ( 

144 RootObject.objects_inheritance.filter(id__in=target_ids) 

145 .select_subclasses() 

146 .annotate(label=Case(*when_clauses_classes)) 

147 .values("pk", "label") 

148 ) 

149 

150 identifiers = {item["pk"]: item["label"] for item in identifiers} 

151 

152 for rel in rels: 

153 _id = rel["target_id"] 

154 content_type = rel["target_content_type"] 

155 facetname = ( 

156 "relation_to_" + ContentType.objects.get_for_id(content_type).name 

157 ) 

158 if _id in facets[facetname]: 

159 facets[facetname][_id]["count"] += 1 

160 else: 

161 facets[facetname][_id] = { 

162 "count": 1, 

163 "name": identifiers.get(_id, "Unknown"), 

164 } 

165 return facets