Skip to content

Commit

Permalink
feat: Improve versioned change requests to handle multiple open CRs f…
Browse files Browse the repository at this point in the history
…or single feature (#4245)
  • Loading branch information
matthewelwell authored Jul 23, 2024
1 parent 0390075 commit f1cc8d8
Show file tree
Hide file tree
Showing 19 changed files with 1,155 additions and 140 deletions.
18 changes: 18 additions & 0 deletions api/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,11 @@ def segment(project: Project):
return _segment


@pytest.fixture()
def another_segment(project: Project) -> Segment:
return Segment.objects.create(name="another_segment", project=project)


@pytest.fixture()
def segment_rule(segment):
return SegmentRule.objects.create(segment=segment, type=SegmentRule.ALL_RULE)
Expand Down Expand Up @@ -587,6 +592,19 @@ def segment_featurestate(feature_segment, feature, environment):
)


@pytest.fixture()
def another_segment_featurestate(
feature: Feature, environment: Environment, another_segment: Segment
) -> FeatureState:
return FeatureState.objects.create(
feature_segment=FeatureSegment.objects.create(
feature=feature, segment=another_segment, environment=environment
),
feature=feature,
environment=environment,
)


@pytest.fixture()
def feature_with_value_segment(
feature_with_value: Feature, segment: Segment, environment: Environment
Expand Down
11 changes: 6 additions & 5 deletions api/features/feature_segments/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import typing

from common.features.serializers import (
CreateSegmentOverrideFeatureSegmentSerializer,
)
from rest_framework import serializers
from rest_framework.exceptions import PermissionDenied

Expand Down Expand Up @@ -38,16 +41,14 @@ def validate(self, data):
return data


class CreateSegmentOverrideFeatureSegmentSerializer(serializers.ModelSerializer):
class CustomCreateSegmentOverrideFeatureSegmentSerializer(
CreateSegmentOverrideFeatureSegmentSerializer
):
# Since the `priority` field on the FeatureSegment model is set to editable=False
# (to adhere to the django-ordered-model functionality), we redefine the priority
# field here, and use it manually in the save method.
priority = serializers.IntegerField(min_value=0, required=False)

class Meta:
model = FeatureSegment
fields = ("id", "segment", "priority", "uuid")

def save(self, **kwargs: typing.Any) -> FeatureSegment:
priority: int | None = self.initial_data.pop("priority", None)

Expand Down
15 changes: 1 addition & 14 deletions api/features/multivariate/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
from rest_framework import serializers

from features.models import Feature
from features.multivariate.models import (
MultivariateFeatureOption,
MultivariateFeatureStateValue,
)
from features.multivariate.models import MultivariateFeatureOption


class NestedMultivariateFeatureOptionSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -54,13 +51,3 @@ def _get_siblings(self, feature: Feature):
siblings = siblings.exclude(id=self.instance.id)

return siblings


class MultivariateFeatureStateValueSerializer(serializers.ModelSerializer):
class Meta:
model = MultivariateFeatureStateValue
fields = (
"id",
"multivariate_feature_option",
"percentage_allocation",
)
64 changes: 14 additions & 50 deletions api/features/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
from datetime import datetime

import django.core.exceptions
from common.features.multivariate.serializers import (
MultivariateFeatureStateValueSerializer,
)
from common.features.serializers import (
CreateSegmentOverrideFeatureStateSerializer,
FeatureStateValueSerializer,
)
from drf_writable_nested import WritableNestedModelSerializer
from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers
Expand All @@ -27,13 +34,10 @@

from .constants import INTERSECTION, UNION
from .feature_segments.serializers import (
CreateSegmentOverrideFeatureSegmentSerializer,
)
from .models import Feature, FeatureState, FeatureStateValue
from .multivariate.serializers import (
MultivariateFeatureStateValueSerializer,
NestedMultivariateFeatureOptionSerializer,
CustomCreateSegmentOverrideFeatureSegmentSerializer,
)
from .models import Feature, FeatureState
from .multivariate.serializers import NestedMultivariateFeatureOptionSerializer


class FeatureStateSerializerSmall(serializers.ModelSerializer):
Expand Down Expand Up @@ -551,12 +555,6 @@ class Meta:
fields = ("feature", "enabled")


class FeatureStateValueSerializer(serializers.ModelSerializer):
class Meta:
model = FeatureStateValue
fields = ("type", "string_value", "integer_value", "boolean_value")


class FeatureInfluxDataSerializer(serializers.Serializer):
events_list = serializers.ListSerializer(child=serializers.DictField())

Expand Down Expand Up @@ -598,46 +596,12 @@ class SDKFeatureStatesQuerySerializer(serializers.Serializer):
)


class CreateSegmentOverrideFeatureStateSerializer(WritableNestedModelSerializer):
feature_state_value = FeatureStateValueSerializer()
feature_segment = CreateSegmentOverrideFeatureSegmentSerializer(
class CustomCreateSegmentOverrideFeatureStateSerializer(
CreateSegmentOverrideFeatureStateSerializer
):
feature_segment = CustomCreateSegmentOverrideFeatureSegmentSerializer(
required=False, allow_null=True
)
multivariate_feature_state_values = MultivariateFeatureStateValueSerializer(
many=True, required=False
)

class Meta:
model = FeatureState
fields = (
"id",
"feature",
"enabled",
"feature_state_value",
"feature_segment",
"deleted_at",
"uuid",
"created_at",
"updated_at",
"live_from",
"environment",
"identity",
"change_request",
"multivariate_feature_state_values",
)

read_only_fields = (
"id",
"deleted_at",
"uuid",
"created_at",
"updated_at",
"live_from",
"environment",
"identity",
"change_request",
"feature",
)

def _get_save_kwargs(self, field_name):
kwargs = super()._get_save_kwargs(field_name)
Expand Down
14 changes: 14 additions & 0 deletions api/features/versioning/dataclasses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from datetime import datetime

from pydantic import BaseModel, computed_field


class Conflict(BaseModel):
segment_id: int | None = None
original_cr_id: int | None = None
published_at: datetime | None = None

@computed_field
@property
def is_environment_default(self) -> bool:
return self.segment_id is None
44 changes: 44 additions & 0 deletions api/features/versioning/migrations/0004_add_version_change_set.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated by Django 3.2.25 on 2024-07-10 11:18

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django_lifecycle.mixins


class Migration(migrations.Migration):

dependencies = [
('environments', '0034_alter_environment_project'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('workflows_core', '0009_prevent_cascade_delete_from_user_delete'),
('features', '0064_fix_feature_help_text_typo'),
('feature_versioning', '0003_cascade_delete_versions_on_cr_delete'),
]

operations = [
migrations.CreateModel(
name='VersionChangeSet',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('deleted_at', models.DateTimeField(blank=True, db_index=True, default=None, editable=False, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('published_at', models.DateTimeField(blank=True, null=True)),
('live_from', models.DateTimeField(null=True)),
('feature_states_to_create', models.TextField(help_text='JSON blob describing the feature states that should be created when the change request is published', null=True)),
('feature_states_to_update', models.TextField(help_text='JSON blob describing the feature states that should be updated when the change request is published', null=True)),
('segment_ids_to_delete_overrides', models.TextField(help_text='JSON blob describing the segment overrides for whichthe segment overrides should be deleted when the change request is published', null=True)),
('change_request', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='change_sets', to='workflows_core.changerequest')),
('environment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='environments.environment')),
('environment_feature_version', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='feature_versioning.environmentfeatureversion')),
('feature', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='features.feature')),
('published_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'permissions': (('can_undelete', 'Can undelete this object'),),
'abstract': False,
},
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model),
),
]
Loading

0 comments on commit f1cc8d8

Please sign in to comment.