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
« 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
19from sm.mixins import filter_queryset_by_tenant
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 """
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
36class TenantPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
37 """A PK related field scoped to the requesting user's accessible groups."""
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
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": []}}
57class StatusSerializer(serializers.ModelSerializer):
58 class Meta:
59 model = Status
60 fields = ["id", "name"]
61 extra_kwargs = {"name": {"validators": []}}
64class OSSerializer(serializers.ModelSerializer):
65 vendor = TenantSlugRelatedField(slug_field="name", queryset=Vendor.objects.all())
67 class Meta:
68 model = OS
69 fields = ["id", "version", "vendor"]
72class ServerModelSerializer(serializers.ModelSerializer):
73 vendor = TenantSlugRelatedField(slug_field="name", queryset=Vendor.objects.all())
75 class Meta:
76 model = ServerModel
77 fields = ["id", "name", "vendor"]
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 """
86 def to_representation(self, value):
87 if isinstance(value, datetime):
88 value = value.date()
89 return super().to_representation(value)
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)
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)
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 ]
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)
155class LocationSerializer(TaggitSerializer, serializers.ModelSerializer):
156 tags = TagListSerializerField(required=False)
158 class Meta:
159 model = Location
160 fields = "__all__"
163class DomainSerializer(TaggitSerializer, serializers.ModelSerializer):
164 tags = TagListSerializerField(required=False)
166 class Meta:
167 model = Domain
168 fields = "__all__"
169 extra_kwargs = {"name": {"validators": []}}
172class PatchtimeSerializer(TaggitSerializer, serializers.ModelSerializer):
173 tags = TagListSerializerField(required=False)
175 class Meta:
176 model = Patchtime
177 fields = "__all__"
178 extra_kwargs = {"name": {"validators": []}}
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)
189 class Meta:
190 model = Cluster
191 fields = "__all__"
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)
213 class Meta:
214 model = ClusterPackage
215 fields = "__all__"
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
250class ClusterSoftwareSerializer(serializers.ModelSerializer):
251 vendor = TenantSlugRelatedField(slug_field="name", queryset=Vendor.objects.all())
253 class Meta:
254 model = ClusterSoftware
255 fields = ["id", "name", "version", "vendor"]
258class ClusterPackageTypeSerializer(serializers.ModelSerializer):
259 class Meta:
260 model = ClusterPackageType
261 fields = ["id", "name"]
262 extra_kwargs = {"name": {"validators": []}}