Coverage for sm/api/serializers.py: 97%

139 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-24 19:06 +0000

1from rest_framework import serializers 

2from rest_framework.exceptions import ValidationError 

3from django.utils.translation import gettext_lazy as _ 

4from datetime import datetime 

5from taggit.serializers import TaggitSerializer, TagListSerializerField 

6from vendor.models import Model as Vendor 

7from status.models import Model as Status 

8from location.models import Model as Location 

9from domain.models import Model as Domain 

10from patchtime.models import Model as Patchtime 

11from operatingsystem.models import Model as OS 

12from servermodel.models import Model as ServerModel 

13from server.models import Model as Server 

14from cluster.models import Model as Cluster 

15from clusterpackage.models import Model as ClusterPackage 

16from clustersoftware.models import Model as ClusterSoftware 

17from clusterpackagetype.models import Model as ClusterPackageType 

18 

19from sm.mixins import filter_queryset_by_tenant 

20 

21 

22class TenantSlugRelatedField(serializers.SlugRelatedField): 

23 """ 

24 A ``SlugRelatedField`` whose queryset is scoped to the requesting user's 

25 accessible groups, so a user can never reference (create/update against) a 

26 related object belonging to another tenant. 

27 """ 

28 

29 def get_queryset(self): 

30 request = self.context.get("request") 

31 if request is not None and not getattr(request.user, "is_superuser", False): 

32 return filter_queryset_by_tenant(self.queryset, request) 

33 return self.queryset 

34 

35 

36class TenantPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField): 

37 """A PK related field scoped to the requesting user's accessible groups.""" 

38 

39 def get_queryset(self): 

40 request = self.context.get("request") 

41 if request is not None and not getattr(request.user, "is_superuser", False): 

42 return filter_queryset_by_tenant(self.queryset, request) 

43 return self.queryset 

44 

45 

46class VendorSerializer(serializers.ModelSerializer): 

47 class Meta: 

48 model = Vendor 

49 fields = ["id", "name", "is_hardware", "is_software"] 

50 # DRF maps the model's partial global-uniqueness constraint to an 

51 # auto UniqueValidator here, which would wrongly reject tenant items 

52 # sharing a global row's name. DB constraints plus the API mixin's 

53 # scope guard remain authoritative. 

54 extra_kwargs = {"name": {"validators": []}} 

55 

56 

57class StatusSerializer(serializers.ModelSerializer): 

58 class Meta: 

59 model = Status 

60 fields = ["id", "name"] 

61 extra_kwargs = {"name": {"validators": []}} 

62 

63 

64class OSSerializer(serializers.ModelSerializer): 

65 vendor = TenantSlugRelatedField(slug_field="name", queryset=Vendor.objects.all()) 

66 

67 class Meta: 

68 model = OS 

69 fields = ["id", "version", "vendor"] 

70 

71 

72class ServerModelSerializer(serializers.ModelSerializer): 

73 vendor = TenantSlugRelatedField(slug_field="name", queryset=Vendor.objects.all()) 

74 

75 class Meta: 

76 model = ServerModel 

77 fields = ["id", "name", "vendor"] 

78 

79 

80class CoercingDateField(serializers.DateField): 

81 """ 

82 Date field that tolerates datetime values. The server model stores 

83 ``default=timezone.now`` for its DateFields, which can yield datetimes. 

84 """ 

85 

86 def to_representation(self, value): 

87 if isinstance(value, datetime): 

88 value = value.date() 

89 return super().to_representation(value) 

90 

91 

92class ServerSerializer(TaggitSerializer, serializers.ModelSerializer): 

93 status = TenantSlugRelatedField(slug_field="name", queryset=Status.objects.all()) 

94 domain = TenantSlugRelatedField(slug_field="name", queryset=Domain.objects.all()) 

95 location = TenantSlugRelatedField( 

96 slug_field="name", 

97 queryset=Location.objects.all(), 

98 required=False, 

99 allow_null=True, 

100 ) 

101 patchtime = TenantSlugRelatedField( 

102 slug_field="name", 

103 queryset=Patchtime.objects.all(), 

104 required=False, 

105 allow_null=True, 

106 ) 

107 operatingsystem = OSSerializer(read_only=True) 

108 servermodel = ServerModelSerializer(read_only=True) 

109 delivery_date = CoercingDateField(required=False) 

110 install_date = CoercingDateField(required=False) 

111 decommission_date = CoercingDateField(required=False, allow_null=True) 

112 tags = TagListSerializerField(required=False) 

113 

114 # "monitoring" is the canonical presentation of the monitoring flag. 

115 # "monitoring_from_puppet" remains as a read-only legacy alias so old 

116 # API consumers that read the field keep working. 

117 monitoring = serializers.BooleanField( 

118 source="monitoring_from_puppet", required=False 

119 ) 

120 monitoring_from_puppet = serializers.BooleanField(read_only=True) 

121 

122 class Meta: 

123 model = Server 

124 fields = [ 

125 "id", 

126 "hostname", 

127 "domain", 

128 "status", 

129 "location", 

130 "operatingsystem", 

131 "servermodel", 

132 "patchtime", 

133 "primary_ip", 

134 "delivery_date", 

135 "install_date", 

136 "decommission_date", 

137 "description", 

138 "application", 

139 "rack", 

140 "monitoring", 

141 "monitoring_from_puppet", 

142 "management_ip", 

143 "management_hostname", 

144 "tags", 

145 ] 

146 

147 def to_internal_value(self, data): 

148 # If an API client sends the legacy "monitoring_from_puppet" key 

149 # without the new "monitoring" key, honour it. 

150 if "monitoring_from_puppet" in data and "monitoring" not in data: 

151 data = {**data, "monitoring": data["monitoring_from_puppet"]} 

152 return super().to_internal_value(data) 

153 

154 

155class LocationSerializer(TaggitSerializer, serializers.ModelSerializer): 

156 tags = TagListSerializerField(required=False) 

157 

158 class Meta: 

159 model = Location 

160 fields = "__all__" 

161 

162 

163class DomainSerializer(TaggitSerializer, serializers.ModelSerializer): 

164 tags = TagListSerializerField(required=False) 

165 

166 class Meta: 

167 model = Domain 

168 fields = "__all__" 

169 extra_kwargs = {"name": {"validators": []}} 

170 

171 

172class PatchtimeSerializer(TaggitSerializer, serializers.ModelSerializer): 

173 tags = TagListSerializerField(required=False) 

174 

175 class Meta: 

176 model = Patchtime 

177 fields = "__all__" 

178 extra_kwargs = {"name": {"validators": []}} 

179 

180 

181class ClusterSerializer(TaggitSerializer, serializers.ModelSerializer): 

182 clustersoftware = TenantPrimaryKeyRelatedField( 

183 queryset=ClusterSoftware.objects.all(), 

184 required=False, 

185 allow_null=True, 

186 ) 

187 tags = TagListSerializerField(required=False) 

188 

189 class Meta: 

190 model = Cluster 

191 fields = "__all__" 

192 

193 

194class ClusterPackageSerializer(TaggitSerializer, serializers.ModelSerializer): 

195 status_name = serializers.CharField(source="status.name", read_only=True) 

196 package_type_name = serializers.CharField( 

197 source="package_type.name", read_only=True 

198 ) 

199 cluster_name = serializers.CharField(source="cluster.name", read_only=True) 

200 clustersoftware = serializers.CharField( 

201 source="cluster.clustersoftware.name", read_only=True, allow_null=True 

202 ) 

203 clustersoftwareversion = serializers.CharField( 

204 source="cluster.clustersoftware.version", read_only=True, allow_null=True 

205 ) 

206 cluster = TenantPrimaryKeyRelatedField(queryset=Cluster.objects.all()) 

207 status = TenantPrimaryKeyRelatedField(queryset=Status.objects.all()) 

208 package_type = TenantPrimaryKeyRelatedField( 

209 queryset=ClusterPackageType.objects.all() 

210 ) 

211 tags = TagListSerializerField(required=False) 

212 

213 class Meta: 

214 model = ClusterPackage 

215 fields = "__all__" 

216 

217 def validate(self, attrs): 

218 attrs = super().validate(attrs) 

219 cluster = attrs.get("cluster", getattr(self.instance, "cluster", None)) 

220 name = attrs.get("name", getattr(self.instance, "name", None)) 

221 status = attrs.get("status", getattr(self.instance, "status", None)) 

222 package_type = attrs.get( 

223 "package_type", getattr(self.instance, "package_type", None) 

224 ) 

225 if None in (cluster, name, status, package_type): 

226 return attrs 

227 request = self.context.get("request") 

228 group = None 

229 if request and request.user and not request.user.is_superuser: 

230 group = request.user.groups.first() 

231 qs = ClusterPackage.objects.filter( 

232 cluster=cluster, 

233 name=name, 

234 status=status, 

235 package_type=package_type, 

236 group=group, 

237 ) 

238 if self.instance is not None: 

239 qs = qs.exclude(pk=self.instance.pk) 

240 if qs.exists(): 

241 raise ValidationError( 

242 _( 

243 "A cluster package with this cluster, name, status and " 

244 "package type already exists." 

245 ) 

246 ) 

247 return attrs 

248 

249 

250class ClusterSoftwareSerializer(serializers.ModelSerializer): 

251 vendor = TenantSlugRelatedField(slug_field="name", queryset=Vendor.objects.all()) 

252 

253 class Meta: 

254 model = ClusterSoftware 

255 fields = ["id", "name", "version", "vendor"] 

256 

257 

258class ClusterPackageTypeSerializer(serializers.ModelSerializer): 

259 class Meta: 

260 model = ClusterPackageType 

261 fields = ["id", "name"] 

262 extra_kwargs = {"name": {"validators": []}}