33from django .contrib .contenttypes .models import ContentType
44from django .core .exceptions import ObjectDoesNotExist
55from django .db import transaction
6- from django .db .models import Model
6+ from django .db .models import Model , Q
77from rest_framework import serializers
88
99from metadata .models import (
1313 MetadataModelFieldRequirement ,
1414)
1515from organisations .models import Organisation
16+ from projects .models import Project
1617from util .drf_writable_nested .serializers import (
1718 DeleteBeforeUpdateWritableNestedModelSerializer ,
1819)
@@ -24,14 +25,22 @@ class MetadataFieldQuerySerializer(serializers.Serializer): # type: ignore[type
2425 )
2526
2627
27- class SupportedRequiredForModelQuerySerializer (serializers .Serializer ): # type: ignore[type-arg]
28- model_name = serializers .CharField (required = True )
28+ class ProjectMetadataFieldQuerySerializer (serializers .Serializer ): # type: ignore[type-arg]
29+ include_organisation = serializers .BooleanField (
30+ required = False ,
31+ default = False ,
32+ help_text = "Include inherited organisation-level fields. "
33+ "Project-level fields override same-named org fields." ,
34+ )
35+ entity = serializers .ChoiceField (
36+ required = False ,
37+ choices = ["feature" , "segment" , "environment" ],
38+ help_text = "Filter by entity type (feature, segment, or environment)." ,
39+ )
2940
3041
31- class MetadataFieldSerializer (serializers .ModelSerializer ): # type: ignore[type-arg]
32- class Meta :
33- model = MetadataField
34- fields = ("id" , "name" , "type" , "description" , "organisation" )
42+ class SupportedRequiredForModelQuerySerializer (serializers .Serializer ): # type: ignore[type-arg]
43+ model_name = serializers .CharField (required = True )
3544
3645
3746class MetadataModelFieldQuerySerializer (serializers .Serializer ): # type: ignore[type-arg]
@@ -46,6 +55,71 @@ class Meta:
4655 fields = ("content_type" , "object_id" )
4756
4857
58+ class MetadataModelFieldNestedSerializer (serializers .ModelSerializer ): # type: ignore[type-arg]
59+ is_required_for = MetadataModelFieldRequirementSerializer (many = True , read_only = True )
60+
61+ class Meta :
62+ model = MetadataModelField
63+ fields = ("id" , "content_type" , "is_required_for" )
64+
65+
66+ class MetadataFieldSerializer (serializers .ModelSerializer ): # type: ignore[type-arg]
67+ project = serializers .IntegerField (
68+ required = False , allow_null = True , default = None , source = "project_id"
69+ )
70+ model_fields = MetadataModelFieldNestedSerializer (
71+ source = "metadatamodelfield_set" , many = True , read_only = True
72+ )
73+
74+ class Meta :
75+ model = MetadataField
76+ fields = (
77+ "id" ,
78+ "name" ,
79+ "type" ,
80+ "description" ,
81+ "organisation" ,
82+ "project" ,
83+ "model_fields" ,
84+ )
85+ # Disable auto-generated unique validators — DRF can't generate
86+ # them for conditional UniqueConstraints. Uniqueness is validated
87+ # manually in validate() below.
88+ validators : list [object ] = []
89+
90+ def validate (self , data : dict [str , Any ]) -> dict [str , Any ]:
91+ data = super ().validate (data )
92+ project_id = data .get ("project_id" )
93+ organisation = data .get ("organisation" )
94+
95+ if (
96+ project_id is not None
97+ and not Project .objects .filter (
98+ id = project_id , organisation = organisation
99+ ).exists ()
100+ ):
101+ raise serializers .ValidationError (
102+ {"project" : "Project must belong to the specified organisation." }
103+ )
104+
105+ # Replicate uniqueness checks that DRF can't auto-generate
106+ # from conditional UniqueConstraints.
107+ qs = MetadataField .objects .filter (
108+ name = data .get ("name" ),
109+ organisation = organisation ,
110+ project_id = project_id ,
111+ )
112+ if self .instance is not None :
113+ assert isinstance (self .instance , MetadataField )
114+ qs = qs .exclude (pk = self .instance .pk )
115+ if qs .exists ():
116+ raise serializers .ValidationError (
117+ {"name" : "A metadata field with this name already exists." }
118+ )
119+
120+ return data
121+
122+
49123class MetaDataModelFieldSerializer (DeleteBeforeUpdateWritableNestedModelSerializer ):
50124 is_required_for = MetadataModelFieldRequirementSerializer (many = True , required = False )
51125
@@ -109,20 +183,50 @@ class MetadataSerializerMixin:
109183 """
110184
111185 def _validate_required_metadata (
112- self , organisation : Organisation , metadata : list [dict [str , Any ]]
186+ self ,
187+ organisation : Organisation ,
188+ metadata : list [dict [str , Any ]],
189+ project : Project | None = None ,
113190 ) -> None :
114191 content_type = ContentType .objects .get_for_model (self .Meta .model ) # type: ignore[attr-defined]
115- requirements = MetadataModelFieldRequirement .objects .filter (
192+ org_ct = ContentType .objects .get_for_model (Organisation )
193+
194+ # Field scoping: org-level fields + this project's fields
195+ field_scope = Q (
116196 model_field__content_type = content_type ,
117197 model_field__field__organisation = organisation ,
198+ model_field__field__project__isnull = True ,
199+ )
200+ # Requirement scoping: org-level + this project's requirements
201+ req_scope = Q (content_type = org_ct , object_id = organisation .id )
202+
203+ overridden_names : set [str ] = set ()
204+ if project is not None :
205+ field_scope |= Q (
206+ model_field__content_type = content_type ,
207+ model_field__field__organisation = organisation ,
208+ model_field__field__project = project ,
209+ )
210+ project_ct = ContentType .objects .get_for_model (Project )
211+ req_scope |= Q (content_type = project_ct , object_id = project .id )
212+ overridden_names = set (
213+ MetadataField .objects .filter (
214+ organisation = organisation , project = project
215+ ).values_list ("name" , flat = True )
216+ )
217+
218+ requirements = MetadataModelFieldRequirement .objects .filter (
219+ field_scope & req_scope ,
118220 ).select_related ("model_field__field" )
119221
120222 metadata_fields = {field ["model_field" ] for field in metadata }
121223 for requirement in requirements :
224+ field = requirement .model_field .field
225+ if field .project is None and field .name in overridden_names :
226+ continue
122227 if requirement .model_field not in metadata_fields :
123- field_name = requirement .model_field .field .name
124228 raise serializers .ValidationError (
125- {"metadata" : f"Missing required metadata field: { field_name } " }
229+ {"metadata" : f"Missing required metadata field: { field . name } " }
126230 )
127231
128232 def _update_metadata (
0 commit comments