Skip to content

Commit 06eb8a4

Browse files
authored
feat: Add metadata fields to core entities (API) (#3315)
1 parent 95b14d1 commit 06eb8a4

File tree

12 files changed

+634
-75
lines changed

12 files changed

+634
-75
lines changed

api/conftest.py

Lines changed: 146 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -659,23 +659,23 @@ def task_processor_synchronously(settings):
659659

660660

661661
@pytest.fixture()
662-
def a_metadata_field(organisation):
662+
def a_metadata_field(organisation: Organisation) -> MetadataField:
663663
return MetadataField.objects.create(name="a", type="int", organisation=organisation)
664664

665665

666666
@pytest.fixture()
667-
def b_metadata_field(organisation):
667+
def b_metadata_field(organisation: Organisation) -> MetadataField:
668668
return MetadataField.objects.create(name="b", type="str", organisation=organisation)
669669

670670

671671
@pytest.fixture()
672672
def required_a_environment_metadata_field(
673-
organisation,
674-
a_metadata_field,
675-
environment,
676-
project,
677-
project_content_type,
678-
):
673+
organisation: Organisation,
674+
a_metadata_field: MetadataField,
675+
environment: Environment,
676+
project: Project,
677+
project_content_type: ContentType,
678+
) -> MetadataModelField:
679679
environment_type = ContentType.objects.get_for_model(environment)
680680
model_field = MetadataModelField.objects.create(
681681
field=a_metadata_field,
@@ -689,7 +689,119 @@ def required_a_environment_metadata_field(
689689

690690

691691
@pytest.fixture()
692-
def optional_b_environment_metadata_field(organisation, b_metadata_field, environment):
692+
def required_a_feature_metadata_field(
693+
organisation: Organisation,
694+
a_metadata_field: MetadataField,
695+
feature_content_type: ContentType,
696+
project: Project,
697+
project_content_type: ContentType,
698+
) -> MetadataModelField:
699+
model_field = MetadataModelField.objects.create(
700+
field=a_metadata_field,
701+
content_type=feature_content_type,
702+
)
703+
704+
MetadataModelFieldRequirement.objects.create(
705+
content_type=project_content_type, object_id=project.id, model_field=model_field
706+
)
707+
708+
return model_field
709+
710+
711+
@pytest.fixture()
712+
def required_a_feature_metadata_field_using_organisation_content_type(
713+
organisation: Organisation,
714+
a_metadata_field: MetadataField,
715+
feature_content_type: ContentType,
716+
project: Project,
717+
organisation_content_type: ContentType,
718+
) -> MetadataModelField:
719+
model_field = MetadataModelField.objects.create(
720+
field=a_metadata_field,
721+
content_type=feature_content_type,
722+
)
723+
724+
MetadataModelFieldRequirement.objects.create(
725+
content_type=organisation_content_type,
726+
object_id=organisation.id,
727+
model_field=model_field,
728+
)
729+
730+
return model_field
731+
732+
733+
@pytest.fixture()
734+
def required_a_segment_metadata_field(
735+
organisation: Organisation,
736+
a_metadata_field: MetadataField,
737+
segment_content_type: ContentType,
738+
project: Project,
739+
project_content_type: ContentType,
740+
) -> MetadataModelField:
741+
model_field = MetadataModelField.objects.create(
742+
field=a_metadata_field,
743+
content_type=segment_content_type,
744+
)
745+
746+
MetadataModelFieldRequirement.objects.create(
747+
content_type=project_content_type, object_id=project.id, model_field=model_field
748+
)
749+
750+
return model_field
751+
752+
753+
@pytest.fixture()
754+
def required_a_segment_metadata_field_using_organisation_content_type(
755+
organisation: Organisation,
756+
a_metadata_field: MetadataField,
757+
segment_content_type: ContentType,
758+
project: Project,
759+
organisation_content_type: ContentType,
760+
) -> MetadataModelField:
761+
model_field = MetadataModelField.objects.create(
762+
field=a_metadata_field,
763+
content_type=segment_content_type,
764+
)
765+
766+
MetadataModelFieldRequirement.objects.create(
767+
content_type=organisation_content_type,
768+
object_id=organisation.id,
769+
model_field=model_field,
770+
)
771+
772+
return model_field
773+
774+
775+
@pytest.fixture()
776+
def optional_b_feature_metadata_field(
777+
organisation: Organisation, b_metadata_field: MetadataField, feature: Feature
778+
) -> MetadataModelField:
779+
feature_type = ContentType.objects.get_for_model(feature)
780+
781+
return MetadataModelField.objects.create(
782+
field=b_metadata_field,
783+
content_type=feature_type,
784+
)
785+
786+
787+
@pytest.fixture()
788+
def optional_b_segment_metadata_field(
789+
organisation: Organisation, b_metadata_field: MetadataField, segment: Segment
790+
) -> MetadataModelField:
791+
segment_type = ContentType.objects.get_for_model(segment)
792+
793+
return MetadataModelField.objects.create(
794+
field=b_metadata_field,
795+
content_type=segment_type,
796+
)
797+
798+
799+
@pytest.fixture()
800+
def optional_b_environment_metadata_field(
801+
organisation: Organisation,
802+
b_metadata_field: MetadataField,
803+
environment: Environment,
804+
) -> MetadataModelField:
693805
environment_type = ContentType.objects.get_for_model(environment)
694806

695807
return MetadataModelField.objects.create(
@@ -699,7 +811,10 @@ def optional_b_environment_metadata_field(organisation, b_metadata_field, enviro
699811

700812

701813
@pytest.fixture()
702-
def environment_metadata_a(environment, required_a_environment_metadata_field):
814+
def environment_metadata_a(
815+
environment: Environment,
816+
required_a_environment_metadata_field: MetadataModelField,
817+
) -> Metadata:
703818
environment_type = ContentType.objects.get_for_model(environment)
704819
return Metadata.objects.create(
705820
object_id=environment.id,
@@ -710,7 +825,10 @@ def environment_metadata_a(environment, required_a_environment_metadata_field):
710825

711826

712827
@pytest.fixture()
713-
def environment_metadata_b(environment, optional_b_environment_metadata_field):
828+
def environment_metadata_b(
829+
environment: Environment,
830+
optional_b_environment_metadata_field: MetadataModelField,
831+
) -> Metadata:
714832
environment_type = ContentType.objects.get_for_model(environment)
715833
return Metadata.objects.create(
716834
object_id=environment.id,
@@ -721,15 +839,30 @@ def environment_metadata_b(environment, optional_b_environment_metadata_field):
721839

722840

723841
@pytest.fixture()
724-
def environment_content_type():
842+
def environment_content_type() -> ContentType:
725843
return ContentType.objects.get_for_model(Environment)
726844

727845

728846
@pytest.fixture()
729-
def project_content_type():
847+
def feature_content_type() -> ContentType:
848+
return ContentType.objects.get_for_model(Feature)
849+
850+
851+
@pytest.fixture()
852+
def segment_content_type() -> ContentType:
853+
return ContentType.objects.get_for_model(Segment)
854+
855+
856+
@pytest.fixture()
857+
def project_content_type() -> ContentType:
730858
return ContentType.objects.get_for_model(Project)
731859

732860

861+
@pytest.fixture()
862+
def organisation_content_type() -> ContentType:
863+
return ContentType.objects.get_for_model(Organisation)
864+
865+
733866
@pytest.fixture
734867
def manage_user_group_permission(db):
735868
return OrganisationPermissionModel.objects.get(key=MANAGE_USER_GROUPS)

api/features/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
SoftDeleteExportableModel,
1313
abstract_base_auditable_model_factory,
1414
)
15+
from django.contrib.contenttypes.fields import GenericRelation
1516
from django.core.exceptions import (
1617
NON_FIELD_ERRORS,
1718
ObjectDoesNotExist,
@@ -75,6 +76,7 @@
7576
)
7677
from features.versioning.models import EnvironmentFeatureVersion
7778
from integrations.github.models import GithubConfiguration
79+
from metadata.models import Metadata
7880
from projects.models import Project
7981
from projects.tags.models import Tag
8082

@@ -129,6 +131,8 @@ class Feature(
129131

130132
objects = FeatureManager()
131133

134+
metadata = GenericRelation(Metadata)
135+
132136
class Meta:
133137
# Note: uniqueness index is added in explicit SQL in the migrations (See 0005, 0050)
134138
# TODO: after upgrade to Django 4.0 use UniqueConstraint()

api/features/serializers.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from environments.sdk.serializers_mixins import (
1313
HideSensitiveFieldsSerializerMixin,
1414
)
15+
from metadata.serializers import MetadataSerializer, SerializerWithMetadata
1516
from projects.models import Project
1617
from users.serializers import (
1718
UserIdsSerializer,
@@ -296,17 +297,44 @@ def get_last_modified_in_current_environment(
296297
return getattr(instance, "last_modified_in_current_environment", None)
297298

298299

299-
class ListFeatureSerializer(CreateFeatureSerializer):
300+
class FeatureSerializerWithMetadata(SerializerWithMetadata, CreateFeatureSerializer):
301+
metadata = MetadataSerializer(required=False, many=True)
302+
303+
class Meta(CreateFeatureSerializer.Meta):
304+
fields = CreateFeatureSerializer.Meta.fields + ("metadata",)
305+
306+
def get_project(self, validated_data: dict = None) -> Project:
307+
project = self.context.get("project")
308+
if project:
309+
return project
310+
else:
311+
raise serializers.ValidationError(
312+
"Unable to retrieve project for metadata validation."
313+
)
314+
315+
316+
class UpdateFeatureSerializerWithMetadata(FeatureSerializerWithMetadata):
317+
"""prevent users from changing certain values after creation"""
318+
319+
class Meta(FeatureSerializerWithMetadata.Meta):
320+
read_only_fields = FeatureSerializerWithMetadata.Meta.read_only_fields + (
321+
"default_enabled",
322+
"initial_value",
323+
"name",
324+
)
325+
326+
327+
class ListFeatureSerializer(FeatureSerializerWithMetadata):
300328
# This exists purely to reduce the conflicts for the EE repository
301329
# which has some extra behaviour here to support Oracle DB.
302330
pass
303331

304332

305-
class UpdateFeatureSerializer(CreateFeatureSerializer):
333+
class UpdateFeatureSerializer(ListFeatureSerializer):
306334
"""prevent users from changing certain values after creation"""
307335

308-
class Meta(CreateFeatureSerializer.Meta):
309-
read_only_fields = CreateFeatureSerializer.Meta.read_only_fields + (
336+
class Meta(ListFeatureSerializer.Meta):
337+
read_only_fields = ListFeatureSerializer.Meta.read_only_fields + (
310338
"default_enabled",
311339
"initial_value",
312340
"name",

api/features/views.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ class FeatureViewSet(viewsets.ModelViewSet):
107107
def get_serializer_class(self):
108108
return {
109109
"list": ListFeatureSerializer,
110-
"retrieve": CreateFeatureSerializer,
111-
"create": CreateFeatureSerializer,
110+
"retrieve": ListFeatureSerializer,
111+
"create": ListFeatureSerializer,
112112
"update": UpdateFeatureSerializer,
113113
"partial_update": UpdateFeatureSerializer,
114114
}.get(self.action, ProjectFeatureSerializer)
@@ -131,7 +131,9 @@ def get_queryset(self):
131131
),
132132
),
133133
)
134-
.prefetch_related("multivariate_options", "owners", "tags", "group_owners")
134+
.prefetch_related(
135+
"multivariate_options", "owners", "tags", "group_owners", "metadata"
136+
)
135137
)
136138

137139
query_serializer = FeatureQuerySerializer(data=self.request.query_params)

api/metadata/models.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,16 @@
66
from django.db import models
77

88
from organisations.models import Organisation
9-
from projects.models import Project
109

1110
from .fields import GenericObjectID
1211

1312
FIELD_VALUE_MAX_LENGTH = 2000
1413

15-
METADATA_SUPPORTED_MODELS = ["environment"]
16-
1714
# A map of model name to a function that takes the object id and returns the organisation_id
1815
SUPPORTED_REQUIREMENTS_MAPPING = {
19-
"environment": {
20-
"organisation": lambda org_id: org_id,
21-
"project": lambda project_id: Project.objects.get(
22-
id=project_id
23-
).organisation_id,
24-
}
16+
"environment": ["organisation", "project"],
17+
"feature": ["organisation", "project"],
18+
"segment": ["organisation", "project"],
2519
}
2620

2721

api/metadata/serializers.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
)
1010

1111
from .models import (
12-
SUPPORTED_REQUIREMENTS_MAPPING,
1312
Metadata,
1413
MetadataField,
1514
MetadataModelField,
@@ -55,21 +54,13 @@ class Meta:
5554
def validate(self, data):
5655
data = super().validate(data)
5756
for requirement in data.get("is_required_for", []):
58-
try:
59-
get_org_id_func = SUPPORTED_REQUIREMENTS_MAPPING[
60-
data["content_type"].model
61-
][requirement["content_type"].model]
62-
except KeyError:
63-
raise serializers.ValidationError(
64-
"Invalid requirement for model {}".format(
65-
data["content_type"].model
66-
)
67-
)
68-
69-
if (
70-
get_org_id_func(requirement["object_id"])
71-
!= data["field"].organisation_id
72-
):
57+
org_id = (
58+
requirement["content_type"]
59+
.model_class()
60+
.objects.get(id=requirement["object_id"])
61+
.organisation_id
62+
)
63+
if org_id != data["field"].organisation_id:
7364
raise serializers.ValidationError(
7465
"The requirement organisation does not match the field organisation"
7566
)

0 commit comments

Comments
 (0)