Coverage for netbox/graphql/types.py: 75%

53 statements  

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

1import strawberry 

2import strawberry_django 

3from django.contrib.contenttypes.models import ContentType 

4from django.db.models import ExpressionWrapper, F, Func, IntegerField, Value 

5from strawberry.types import Info 

6 

7from core.graphql.mixins import ChangelogMixin 

8from core.models import ObjectType as ObjectType_ 

9from extras.graphql.mixins import CustomFieldsMixin, JournalEntriesMixin, TagsMixin 

10from netbox.graphql.utils import register_model_graphql_type 

11from users.graphql.mixins import OwnerMixin 

12 

13__all__ = ( 

14 'BaseObjectType', 

15 'ContentTypeType', 

16 'LtreeNodeMixin', 

17 'NestedGroupObjectType', 

18 'NestedLtreeGroupObjectType', 

19 'NetBoxObjectType', 

20 'ObjectType', 

21 'OrganizationalObjectType', 

22 'PrimaryObjectType', 

23 'register_type', 

24) 

25 

26 

27def register_type(model, **kwargs): 

28 """ 

29 Drop-in replacement for `strawberry_django.type()` for model-bound NetBox GraphQL output types. Before delegating 

30 to `strawberry_django.type()`, any plugin-registered output-type mixins for the given model are spliced into the 

31 decorated class's bases. With no extensions registered this is an exact pass-through, leaving schema output 

32 unchanged. See `register_model_graphql_type` for the registry-timing contract. 

33 """ 

34 return register_model_graphql_type(model, strawberry_django.type, 'graphql_type_extensions', **kwargs) 

35 

36 

37# 

38# Base types 

39# 

40 

41@strawberry.type 

42class BaseObjectType: 

43 """ 

44 Base GraphQL object type for all NetBox objects. Restricts the model queryset to enforce object permissions. 

45 """ 

46 

47 @classmethod 

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

49 # Enforce object permissions on the queryset 

50 if hasattr(queryset, 'restrict'): 

51 return queryset.restrict(info.context.request.user, 'view') 

52 return queryset 

53 

54 @strawberry_django.field 

55 def display(self) -> str: 

56 return str(self) 

57 

58 @strawberry_django.field 

59 def class_type(self) -> str: 

60 return self.__class__.__name__ 

61 

62 

63class ObjectType( 

64 ChangelogMixin, 

65 BaseObjectType 

66): 

67 """ 

68 Base GraphQL object type for unclassified models which support change logging 

69 """ 

70 pass 

71 

72 

73class PrimaryObjectType( 

74 ChangelogMixin, 

75 CustomFieldsMixin, 

76 JournalEntriesMixin, 

77 TagsMixin, 

78 OwnerMixin, 

79 BaseObjectType 

80): 

81 """ 

82 Base GraphQL type for models which inherit from PrimaryModel. 

83 """ 

84 pass 

85 

86 

87class OrganizationalObjectType( 

88 ChangelogMixin, 

89 CustomFieldsMixin, 

90 JournalEntriesMixin, 

91 TagsMixin, 

92 OwnerMixin, 

93 BaseObjectType 

94): 

95 """ 

96 Base GraphQL type for models which inherit from OrganizationalModel. 

97 """ 

98 pass 

99 

100 

101@strawberry.type 

102class LtreeNodeMixin: 

103 """ 

104 Exposes the ltree-backed tree depth as a `level` field, preserving the `level` 

105 field MPTT-based types previously surfaced automatically as a real column. 

106 

107 The depth is computed in the database as `nlevel(path) - 1` (root = 0) and 

108 annotated onto the queryset. We prefer the annotation over the `path` column, 

109 which is excluded from the schema. When a resolution path does not apply the 

110 annotation (e.g. a nested relation), `ltree_level` is absent; fall back to the 

111 loaded `path` string (the same depth the LtreeModel.level property computes) 

112 so the field never raises AttributeError. 

113 """ 

114 @strawberry_django.field(annotate={ 

115 'ltree_level': ExpressionWrapper( 

116 Func(F('path'), function='nlevel', output_field=IntegerField()) - Value(1), 

117 output_field=IntegerField(), 

118 ) 

119 }) 

120 def level(self) -> int: 

121 ltree_level = getattr(self, 'ltree_level', None) 

122 if ltree_level is not None: 

123 return ltree_level 

124 path = getattr(self, 'path', '') or '' 

125 return str(path).count('.') 

126 

127 

128class NestedGroupObjectType( 

129 ChangelogMixin, 

130 CustomFieldsMixin, 

131 JournalEntriesMixin, 

132 TagsMixin, 

133 OwnerMixin, 

134 BaseObjectType 

135): 

136 """ 

137 Base GraphQL type for the deprecated MPTT-backed NestedGroupModel, kept for 

138 plugin compatibility. MPTT exposes `level` as a real column, so no annotation 

139 mixin is needed. New code should use NestedLtreeGroupObjectType. 

140 """ 

141 pass 

142 

143 

144class NestedLtreeGroupObjectType( 

145 LtreeNodeMixin, 

146 ChangelogMixin, 

147 CustomFieldsMixin, 

148 JournalEntriesMixin, 

149 TagsMixin, 

150 OwnerMixin, 

151 BaseObjectType 

152): 

153 """ 

154 Base GraphQL type for models which inherit from NestedLtreeGroupModel. 

155 Adds a `level` field annotated via `nlevel(path)`. 

156 """ 

157 pass 

158 

159 

160class NetBoxObjectType( 

161 ChangelogMixin, 

162 CustomFieldsMixin, 

163 JournalEntriesMixin, 

164 TagsMixin, 

165 BaseObjectType 

166): 

167 pass 

168 

169 

170# 

171# Miscellaneous types 

172# 

173 

174@register_type( 

175 ContentType, 

176 fields=['id', 'app_label', 'model'], 

177 pagination=True 

178) 

179class ContentTypeType: 

180 pass 

181 

182 

183@register_type( 

184 ObjectType_, 

185 fields=['id', 'app_label', 'model'], 

186 pagination=True 

187) 

188class ObjectTypeType: 

189 pass