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
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from typing import TYPE_CHECKING, Annotated
3import strawberry
4import strawberry_django
5from strawberry.types import Info
7from extras.models import CustomField, ImageAttachment, JournalEntry
8from utilities.querysets import RestrictedPrefetch
10__all__ = (
11 'ConfigContextMixin',
12 'ContactsMixin',
13 'CustomFieldsMixin',
14 'ImageAttachmentsMixin',
15 'JournalEntriesMixin',
16 'TagsMixin',
17)
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
22 from .types import ImageAttachmentType, JournalEntryType, TagType
25@strawberry.type
26class ConfigContextMixin:
28 @classmethod
29 def get_queryset(cls, queryset, info: Info, **kwargs):
30 queryset = super().get_queryset(queryset, info, **kwargs)
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)
41 return queryset
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()
50@strawberry.type
51class CustomFieldsMixin:
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 }
67@strawberry.type
68class ImageAttachmentsMixin:
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()
79@strawberry.type
80class JournalEntriesMixin:
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()
91@strawberry.type
92class TagsMixin:
94 tags: list[Annotated['TagType', strawberry.lazy('.types')]] = strawberry_django.field(
95 prefetch_related=['tags'],
96 )
99@strawberry.type
100class ContactsMixin:
102 contacts: list[Annotated['ContactAssignmentType', strawberry.lazy('tenancy.graphql.types')]]