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: Improve versioned change requests to handle multiple open CRs for single feature #4245

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
8ea980b
Initial version to get change requests working with change sets
matthewelwell Jun 26, 2024
cbd170f
Update TODO
matthewelwell Jun 26, 2024
f41d7f2
Update TODO
matthewelwell Jun 26, 2024
77a3038
Add live_from to VersionChangeSet
matthewelwell Jun 26, 2024
b8a6e1b
Merge branch 'refs/heads/main' into feat(versioning)/handle-multiple-…
matthewelwell Jul 8, 2024
7da4183
Minor updates
matthewelwell Jul 8, 2024
155b9f5
Add test for conflict behaviour
matthewelwell Jul 9, 2024
5b18e1b
Implement conflict parsing
matthewelwell Jul 9, 2024
03077f1
Conflicts returned in retrieve endpoint but don't prevent publish
matthewelwell Jul 10, 2024
b004a8c
Handle scheduled change requests
matthewelwell Jul 10, 2024
e06fd1a
Rename method argument
matthewelwell Jul 11, 2024
e859051
Ensure task is correctly scheduled for the future
matthewelwell Jul 11, 2024
1522a20
Add alert when conflicts exist in scheduled change set
matthewelwell Jul 11, 2024
66f823f
Remove assertion on incomplete work
matthewelwell Jul 11, 2024
27cb19d
Merge branch 'refs/heads/main' into feat(versioning)/handle-multiple-…
matthewelwell Jul 11, 2024
106d1bb
Move Conflict dataclass
matthewelwell Jul 11, 2024
72424e9
Remove TODO
matthewelwell Jul 11, 2024
dacfa1d
Set the environment_feature_version on publish
matthewelwell Jul 11, 2024
068a32c
Add validation that VersionChangeSet has a related object
matthewelwell Jul 11, 2024
43fe38b
Fix typing
matthewelwell Jul 15, 2024
dd6b0c7
Minor refactors
matthewelwell Jul 15, 2024
356eac9
Use workflows branch
matthewelwell Jul 15, 2024
5e7bc8d
Merge branch 'refs/heads/main' into feat(versioning)/handle-multiple-…
matthewelwell Jul 15, 2024
0336ee3
Revert workflows deps change
matthewelwell Jul 15, 2024
3588c7a
Remove test_workflows file
matthewelwell Jul 15, 2024
8549eb0
Use branch for workflows again
matthewelwell Jul 16, 2024
8e2c484
Use flagsmith-common library
matthewelwell Jul 16, 2024
4099ce7
Update flagsmith-common
matthewelwell Jul 16, 2024
69fce94
Update flagsmith-workflows and flagsmith-common
matthewelwell Jul 17, 2024
73889b8
Fix issues created by serializer inheritance
matthewelwell Jul 17, 2024
bbc03d7
Fix issue getting conflicts
matthewelwell Jul 17, 2024
30ebb71
Tidying
matthewelwell Jul 17, 2024
3440753
Return empty conflict list if published
matthewelwell Jul 17, 2024
1ca0b36
Add published_at to conflicts
matthewelwell Jul 17, 2024
db71e94
Update workflows
matthewelwell Jul 17, 2024
a99586a
Handle missing segment override for feature_states_to_update
matthewelwell Jul 17, 2024
9882211
Use generic error message
matthewelwell Jul 17, 2024
58c713c
Add tests
matthewelwell Jul 17, 2024
c47205b
Remove TODO
matthewelwell Jul 18, 2024
1b93663
Ensure that current time is always used when publishing a version fro…
matthewelwell Jul 18, 2024
89d65bc
Minor optimisation
matthewelwell Jul 18, 2024
20942bb
Merge branch 'refs/heads/main' into feat(versioning)/handle-multiple-…
matthewelwell Jul 18, 2024
d21cf00
Typing fixes
matthewelwell Jul 18, 2024
3abb740
Update flagsmith-common to use tag
matthewelwell Jul 18, 2024
82e1783
Update workflows dependency to use tag
matthewelwell Jul 19, 2024
c74be59
Update error message
matthewelwell Jul 23, 2024
49018f9
Update error messages
matthewelwell Jul 23, 2024
6c082d8
Merge branch 'refs/heads/main' into feat(versioning)/handle-multiple-…
matthewelwell Jul 23, 2024
1c6608f
Merge branch 'refs/heads/main' into feat(versioning)/handle-multiple-…
matthewelwell Jul 23, 2024
f17678b
Fix test
matthewelwell Jul 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading