Coverage for extras/graphql/mixins.py: 70%

42 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1from typing import TYPE_CHECKING, Annotated 

2 

3import strawberry 

4import strawberry_django 

5from strawberry.types import Info 

6 

7from extras.models import CustomField, ImageAttachment, JournalEntry 

8from utilities.querysets import RestrictedPrefetch 

9 

10__all__ = ( 

11 'ConfigContextMixin', 

12 'ContactsMixin', 

13 'CustomFieldsMixin', 

14 'ImageAttachmentsMixin', 

15 'JournalEntriesMixin', 

16 'TagsMixin', 

17) 

18 

19if TYPE_CHECKING: 19 ↛ 20line 19 didn't jump to line 20 because the condition on line 19 was never true

20 from tenancy.graphql.types import ContactAssignmentType 

21 

22 from .types import ImageAttachmentType, JournalEntryType, TagType 

23 

24 

25@strawberry.type 

26class ConfigContextMixin: 

27 

28 @classmethod 

29 def get_queryset(cls, queryset, info: Info, **kwargs): 

30 queryset = super().get_queryset(queryset, info, **kwargs) 

31 

32 # When `config_context` is requested, annotate the aggregated context data — but only for 

33 # rows whose pre-rendered cache (`_config_context_data`) is invalidated (NULL). Warm rows 

34 # are served from the cache by get_config_context() and skip the subquery entirely 

35 # (PostgreSQL short-circuits the CASE), so resolving config_context across a list of objects 

36 # with cold caches no longer incurs a per-object fallback query. 

37 selected = {f.name for f in info.selected_fields[0].selections} 

38 if 'config_context' in selected and hasattr(queryset, 'annotate_config_context_data'): 

39 return queryset.annotate_config_context_data(only_invalidated=True) 

40 

41 return queryset 

42 

43 # Ensure both the pre-rendered cache and `local_context_data` are fetched when `config_context` 

44 # is requested, so the warm-cache read path requires no additional queries. 

45 @strawberry_django.field(only=['_config_context_data', 'local_context_data']) 

46 def config_context(self) -> strawberry.scalars.JSON: 

47 return self.get_config_context() 

48 

49 

50@strawberry.type 

51class CustomFieldsMixin: 

52 

53 @strawberry_django.field(only=['custom_field_data']) 

54 def custom_fields(self) -> strawberry.scalars.JSON: 

55 # Emit a key for every custom field assigned to the model, as the REST API does, rather than 

56 # returning the stored data verbatim. A key is materialized only once a value is assigned 

57 # (see CustomField.populate_initial_data()), so an object which predates a field carries no 

58 # key for it; without this, such a field would be absent from the response instead of null. 

59 # CustomFieldManager.get_for_model() is served from the per-request cache, so this costs one 

60 # query per model rather than one per object. 

61 return { 

62 cf.name: cf.resolve_selection_value(self.custom_field_data.get(cf.name)) 

63 for cf in CustomField.objects.get_for_model(self) 

64 } 

65 

66 

67@strawberry.type 

68class ImageAttachmentsMixin: 

69 

70 @strawberry_django.field( 

71 prefetch_related=lambda info: RestrictedPrefetch( 

72 'images', info.context.request.user, 'view', queryset=ImageAttachment.objects.all() 

73 ), 

74 ) 

75 def image_attachments(self) -> list[Annotated['ImageAttachmentType', strawberry.lazy('.types')]]: 

76 return self.images.all() 

77 

78 

79@strawberry.type 

80class JournalEntriesMixin: 

81 

82 @strawberry_django.field( 

83 prefetch_related=lambda info: RestrictedPrefetch( 

84 'journal_entries', info.context.request.user, 'view', queryset=JournalEntry.objects.all() 

85 ), 

86 ) 

87 def journal_entries(self) -> list[Annotated['JournalEntryType', strawberry.lazy('.types')]]: 

88 return self.journal_entries.all() 

89 

90 

91@strawberry.type 

92class TagsMixin: 

93 

94 tags: list[Annotated['TagType', strawberry.lazy('.types')]] = strawberry_django.field( 

95 prefetch_related=['tags'], 

96 ) 

97 

98 

99@strawberry.type 

100class ContactsMixin: 

101 

102 contacts: list[Annotated['ContactAssignmentType', strawberry.lazy('tenancy.graphql.types')]]