Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add metadata fields to core entities (API) #3315

Merged
merged 32 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ed6ffda
feat: Add metadata fields to core entities (API)
novakzaballa Jan 23, 2024
8e4afcb
ADd typing
novakzaballa Feb 13, 2024
e970e93
Add typing
novakzaballa Feb 13, 2024
f851601
Add typing in segment serializers
novakzaballa Feb 13, 2024
9095696
Add typing in tests
novakzaballa Feb 13, 2024
4c3ab51
Rename UpdateFeatureSerializer to UpdateFeatureSerializerWithMetadata
novakzaballa Feb 14, 2024
a9d7ef9
Merge branch 'main' into feat/add-metadata-fields-to-core-entities-api
novakzaballa Feb 14, 2024
b05cab3
Add tests return typing
novakzaballa Feb 15, 2024
e182abc
Solve test error
novakzaballa Feb 15, 2024
a1c0ea1
Solve test error
novakzaballa Feb 15, 2024
11468a5
Add types
novakzaballa Feb 15, 2024
bb3cbcd
Solve correctios un features views test
novakzaballa Feb 28, 2024
cf9c494
Correct segments views test
novakzaballa Feb 28, 2024
2cfe423
Revert changes in the last corrected test
novakzaballa Feb 28, 2024
d8f24fe
Modify the metadata serializer, SUPPORTED_REQUIREMENTS_MAPPING, and t…
novakzaballa Mar 4, 2024
fb04bbc
Rename test, add typing, and change segment view
novakzaballa Mar 6, 2024
de463be
Correct the Segment serializer to validate if the metadata is required
novakzaballa Mar 6, 2024
75719cb
Add test using organisation_content_type
novakzaballa Mar 19, 2024
20477cc
Correct types in environment_metadata fixtures
novakzaballa Mar 21, 2024
7076117
Change get_project in FeatureSerializerWithMetadata
novakzaballa Mar 21, 2024
bab2c9f
Refactor supported_content_types endpoint
novakzaballa Mar 22, 2024
f98f8c3
Remove METADATA_SUPPORTED_MODELS
novakzaballa Mar 25, 2024
50b43bc
Correct SUPPORTED_REQUIREMENTS_MAPPING, and change the test
novakzaballa Mar 26, 2024
c5c1044
Remove Exception
novakzaballa Apr 5, 2024
d7dca4a
Add missing type
novakzaballa Apr 16, 2024
0d24c4c
Merge branch 'main' into feat/add-metadata-fields-to-core-entities-api
novakzaballa Apr 16, 2024
a0cdc13
Property get_org_id deleted
novakzaballa Apr 16, 2024
309e3fe
Generate poetry.lock
novakzaballa Apr 19, 2024
0b9a5df
Fix Create or update metadata into segment
novakzaballa Apr 22, 2024
10bc36b
Merge branch 'main' into feat/add-metadata-fields-to-core-entities-api
novakzaballa May 2, 2024
9bbdfe9
Adding metadata to prefetch_related in the queryset
novakzaballa May 7, 2024
100f8eb
Merge branch 'main' into feat/add-metadata-fields-to-core-entities-api
novakzaballa May 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
159 changes: 146 additions & 13 deletions api/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,23 +481,23 @@ def task_processor_synchronously(settings):


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


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


@pytest.fixture()
def required_a_environment_metadata_field(
organisation,
a_metadata_field,
environment,
project,
project_content_type,
):
organisation: Organisation,
a_metadata_field: MetadataField,
environment: Environment,
project: Project,
project_content_type: ContentType,
) -> MetadataModelField:
environment_type = ContentType.objects.get_for_model(environment)
model_field = MetadataModelField.objects.create(
field=a_metadata_field,
Expand All @@ -511,7 +511,119 @@ def required_a_environment_metadata_field(


@pytest.fixture()
def optional_b_environment_metadata_field(organisation, b_metadata_field, environment):
def required_a_feature_metadata_field(
organisation: Organisation,
a_metadata_field: MetadataField,
feature_content_type: ContentType,
project: Project,
project_content_type: ContentType,
) -> MetadataModelField:
model_field = MetadataModelField.objects.create(
field=a_metadata_field,
content_type=feature_content_type,
)

MetadataModelFieldRequirement.objects.create(
content_type=project_content_type, object_id=project.id, model_field=model_field
)

return model_field


@pytest.fixture()
def required_a_feature_metadata_field_using_organisation_content_type(
organisation: Organisation,
a_metadata_field: MetadataField,
feature_content_type: ContentType,
project: Project,
organisation_content_type: ContentType,
) -> MetadataModelField:
model_field = MetadataModelField.objects.create(
field=a_metadata_field,
content_type=feature_content_type,
)

MetadataModelFieldRequirement.objects.create(
content_type=organisation_content_type,
object_id=organisation.id,
model_field=model_field,
)

return model_field


@pytest.fixture()
def required_a_segment_metadata_field(
organisation: Organisation,
a_metadata_field: MetadataField,
segment_content_type: ContentType,
project: Project,
project_content_type: ContentType,
) -> MetadataModelField:
model_field = MetadataModelField.objects.create(
field=a_metadata_field,
content_type=segment_content_type,
)

MetadataModelFieldRequirement.objects.create(
content_type=project_content_type, object_id=project.id, model_field=model_field
)

return model_field


@pytest.fixture()
def required_a_segment_metadata_field_using_organisation_content_type(
organisation: Organisation,
a_metadata_field: MetadataField,
segment_content_type: ContentType,
project: Project,
organisation_content_type: ContentType,
) -> MetadataModelField:
model_field = MetadataModelField.objects.create(
field=a_metadata_field,
content_type=segment_content_type,
)

MetadataModelFieldRequirement.objects.create(
content_type=organisation_content_type,
object_id=organisation.id,
model_field=model_field,
)

return model_field


@pytest.fixture()
def optional_b_feature_metadata_field(
organisation: Organisation, b_metadata_field: MetadataField, feature: Feature
) -> MetadataModelField:
feature_type = ContentType.objects.get_for_model(feature)

return MetadataModelField.objects.create(
field=b_metadata_field,
content_type=feature_type,
)


@pytest.fixture()
def optional_b_segment_metadata_field(
organisation: Organisation, b_metadata_field: MetadataField, segment: Segment
) -> MetadataModelField:
segment_type = ContentType.objects.get_for_model(segment)

return MetadataModelField.objects.create(
field=b_metadata_field,
content_type=segment_type,
)


@pytest.fixture()
def optional_b_environment_metadata_field(
organisation: Organisation,
b_metadata_field: MetadataField,
environment: Environment,
) -> MetadataModelField:
environment_type = ContentType.objects.get_for_model(environment)

return MetadataModelField.objects.create(
Expand All @@ -521,7 +633,10 @@ def optional_b_environment_metadata_field(organisation, b_metadata_field, enviro


@pytest.fixture()
def environment_metadata_a(environment, required_a_environment_metadata_field):
def environment_metadata_a(
environment: Environment,
required_a_environment_metadata_field: MetadataModelField,
) -> Metadata:
environment_type = ContentType.objects.get_for_model(environment)
return Metadata.objects.create(
object_id=environment.id,
Expand All @@ -532,7 +647,10 @@ def environment_metadata_a(environment, required_a_environment_metadata_field):


@pytest.fixture()
def environment_metadata_b(environment, optional_b_environment_metadata_field):
def environment_metadata_b(
environment: Environment,
optional_b_environment_metadata_field: MetadataModelField,
) -> Metadata:
environment_type = ContentType.objects.get_for_model(environment)
return Metadata.objects.create(
object_id=environment.id,
Expand All @@ -543,15 +661,30 @@ def environment_metadata_b(environment, optional_b_environment_metadata_field):


@pytest.fixture()
def environment_content_type():
def environment_content_type() -> ContentType:
return ContentType.objects.get_for_model(Environment)


@pytest.fixture()
def project_content_type():
def feature_content_type() -> ContentType:
return ContentType.objects.get_for_model(Feature)


@pytest.fixture()
def segment_content_type() -> ContentType:
return ContentType.objects.get_for_model(Segment)


@pytest.fixture()
def project_content_type() -> ContentType:
return ContentType.objects.get_for_model(Project)


@pytest.fixture()
def organisation_content_type() -> ContentType:
return ContentType.objects.get_for_model(Organisation)


@pytest.fixture
def manage_user_group_permission(db):
return OrganisationPermissionModel.objects.get(key=MANAGE_USER_GROUPS)
Expand Down
4 changes: 4 additions & 0 deletions api/features/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
SoftDeleteExportableModel,
abstract_base_auditable_model_factory,
)
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import (
NON_FIELD_ERRORS,
ObjectDoesNotExist,
Expand Down Expand Up @@ -72,6 +73,7 @@
STRING,
)
from features.versioning.models import EnvironmentFeatureVersion
from metadata.models import Metadata
from projects.models import Project
from projects.tags.models import Tag

Expand Down Expand Up @@ -126,6 +128,8 @@ class Feature(

objects = FeatureManager()

metadata = GenericRelation(Metadata)

class Meta:
# Note: uniqueness index is added in explicit SQL in the migrations (See 0005, 0050)
# TODO: after upgrade to Django 4.0 use UniqueConstraint()
Expand Down
25 changes: 22 additions & 3 deletions api/features/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from environments.sdk.serializers_mixins import (
HideSensitiveFieldsSerializerMixin,
)
from metadata.serializers import MetadataSerializer, SerializerWithMetadata
from projects.models import Project
from users.serializers import (
UserIdsSerializer,
Expand Down Expand Up @@ -270,11 +271,29 @@ def get_last_modified_in_current_environment(
return getattr(instance, "last_modified_in_current_environment", None)


class UpdateFeatureSerializer(ListCreateFeatureSerializer):
"""prevent users from changing certain values after creation"""
class FeatureSerializerWithMetadata(
SerializerWithMetadata, ListCreateFeatureSerializer
):
metadata = MetadataSerializer(required=False, many=True)

class Meta(ListCreateFeatureSerializer.Meta):
read_only_fields = ListCreateFeatureSerializer.Meta.read_only_fields + (
fields = ListCreateFeatureSerializer.Meta.fields + ("metadata",)

def get_project(self, validated_data: dict = None) -> Project:
project = self.context.get("project")
if project:
return project
else:
raise serializers.ValidationError(
"Unable to retrieve project for metadata validation."
)


class UpdateFeatureSerializerWithMetadata(FeatureSerializerWithMetadata):
"""prevent users from changing certain values after creation"""

class Meta(FeatureSerializerWithMetadata.Meta):
read_only_fields = FeatureSerializerWithMetadata.Meta.read_only_fields + (
"default_enabled",
"initial_value",
"name",
Expand Down
13 changes: 7 additions & 6 deletions api/features/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
FeatureInfluxDataSerializer,
FeatureOwnerInputSerializer,
FeatureQuerySerializer,
FeatureSerializerWithMetadata,
FeatureStateSerializerBasic,
FeatureStateSerializerCreate,
FeatureStateSerializerFull,
Expand All @@ -66,7 +67,7 @@
ProjectFeatureSerializer,
SDKFeatureStateSerializer,
SDKFeatureStatesQuerySerializer,
UpdateFeatureSerializer,
UpdateFeatureSerializerWithMetadata,
WritableNestedFeatureStateSerializer,
)
from .tasks import trigger_feature_state_change_webhooks
Expand Down Expand Up @@ -103,11 +104,11 @@ class FeatureViewSet(viewsets.ModelViewSet):

def get_serializer_class(self):
return {
"list": ListCreateFeatureSerializer,
"retrieve": ListCreateFeatureSerializer,
"create": ListCreateFeatureSerializer,
"update": UpdateFeatureSerializer,
"partial_update": UpdateFeatureSerializer,
"list": FeatureSerializerWithMetadata,
"retrieve": FeatureSerializerWithMetadata,
"create": FeatureSerializerWithMetadata,
"update": UpdateFeatureSerializerWithMetadata,
"partial_update": UpdateFeatureSerializerWithMetadata,
}.get(self.action, ProjectFeatureSerializer)

def get_queryset(self):
Expand Down
12 changes: 3 additions & 9 deletions api/metadata/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,16 @@
from django.db import models

from organisations.models import Organisation
from projects.models import Project

from .fields import GenericObjectID

FIELD_VALUE_MAX_LENGTH = 2000

METADATA_SUPPORTED_MODELS = ["environment"]

# A map of model name to a function that takes the object id and returns the organisation_id
SUPPORTED_REQUIREMENTS_MAPPING = {
"environment": {
"organisation": lambda org_id: org_id,
"project": lambda project_id: Project.objects.get(
id=project_id
).organisation_id,
}
"environment": ["organisation", "project"],
"feature": ["organisation", "project"],
"segment": ["organisation", "project"],
}


Expand Down
23 changes: 7 additions & 16 deletions api/metadata/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
)

from .models import (
SUPPORTED_REQUIREMENTS_MAPPING,
Metadata,
MetadataField,
MetadataModelField,
Expand Down Expand Up @@ -55,21 +54,13 @@ class Meta:
def validate(self, data):
data = super().validate(data)
for requirement in data.get("is_required_for", []):
try:
get_org_id_func = SUPPORTED_REQUIREMENTS_MAPPING[
data["content_type"].model
][requirement["content_type"].model]
except KeyError:
raise serializers.ValidationError(
"Invalid requirement for model {}".format(
data["content_type"].model
)
)

if (
get_org_id_func(requirement["object_id"])
!= data["field"].organisation_id
):
org_id = (
requirement["content_type"]
.model_class()
.objects.get(id=requirement["object_id"])
.get_org_id
)
if org_id != data["field"].organisation_id:
raise serializers.ValidationError(
"The requirement organisation does not match the field organisation"
)
Expand Down