Skip to content

Commit ad1d568

Browse files
authored
fix(segment-update): handle rules/conditons without id correctly (#5953)
1 parent 4da2999 commit ad1d568

File tree

2 files changed

+205
-9
lines changed

2 files changed

+205
-9
lines changed

api/segments/serializers.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,21 @@ class SegmentSerializer(MetadataSerializerMixin, WritableNestedModelSerializer):
8282
rules = SegmentRuleSerializer(many=True, required=True, allow_empty=False)
8383
metadata = MetadataSerializer(required=False, many=True)
8484

85+
def __init__(self, *args: Any, **kwargs: Any) -> None:
86+
"""
87+
Because WritableNestedModelSerializer uses `initial_data` instead of `data`
88+
we need to override the `__init__` method to remove rules and conditions
89+
that are marked for deletion.
90+
"""
91+
data = kwargs.get("data")
92+
if data and "rules" in data:
93+
data["rules"] = self._get_rules_and_conditions_without_deleted(
94+
data["rules"]
95+
)
96+
kwargs["data"] = data
97+
98+
super().__init__(*args, **kwargs)
99+
85100
class Meta:
86101
model = Segment
87102
fields = [
@@ -98,15 +113,11 @@ class Meta:
98113
"metadata",
99114
]
100115

101-
def get_initial(self) -> dict[str, Any]:
102-
attrs: dict[str, Any] = super().get_initial()
103-
attrs["rules"] = self._get_rules_and_conditions_without_deleted(attrs["rules"])
104-
return attrs
105-
106116
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
107117
attrs = super().validate(attrs)
108118
project = self.instance.project if self.instance else attrs["project"] # type: ignore[union-attr]
109119
organisation = project.organisation
120+
110121
self._validate_required_metadata(organisation, attrs.get("metadata", []))
111122
self._validate_segment_rules_conditions_limit(attrs["rules"])
112123
self._validate_project_segment_limit(project)

api/tests/unit/segments/test_unit_segments_views.py

Lines changed: 189 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,120 @@ def test_create_segments_with_description_condition(project, client): # type: i
714714
assert segment_condition_description_value == "test-description"
715715

716716

717+
def test_update_segment_add_new_root_rule(
718+
project: Project, admin_client_new: APIClient, segment: Segment
719+
) -> None:
720+
# Given
721+
url = reverse(
722+
"api-v1:projects:project-segments-detail", args=[project.id, segment.id]
723+
)
724+
data = {
725+
"name": segment.name,
726+
"project": project.id,
727+
"rules": [
728+
{
729+
"type": "ANY",
730+
"rules": [
731+
{
732+
"type": "ALL",
733+
"rules": [],
734+
"conditions": [
735+
{"property": "foo", "operator": "EQUAL", "value": "bar"}
736+
],
737+
}
738+
],
739+
}
740+
],
741+
}
742+
743+
# When
744+
response = admin_client_new.put(
745+
url, data=json.dumps(data), content_type="application/json"
746+
)
747+
# Then
748+
assert response.status_code == status.HTTP_200_OK
749+
assert response.json()["rules"][0]["type"] == "ANY"
750+
assert response.json()["rules"][0]["rules"][0]["type"] == "ALL"
751+
assert response.json()["rules"][0]["rules"][0]["conditions"][0]["property"] == "foo"
752+
assert (
753+
response.json()["rules"][0]["rules"][0]["conditions"][0]["operator"] == "EQUAL"
754+
)
755+
assert response.json()["rules"][0]["rules"][0]["conditions"][0]["value"] == "bar"
756+
757+
758+
def test_update_segment_add_new_rule(
759+
project: Project,
760+
admin_client_new: APIClient,
761+
segment: Segment,
762+
segment_rule: SegmentRule,
763+
settings: SettingsWrapper,
764+
) -> None:
765+
# Given
766+
settings.SEGMENT_RULES_CONDITIONS_EXPLICIT_ORDERING_ENABLED = True
767+
768+
url = reverse(
769+
"api-v1:projects:project-segments-detail", args=[project.id, segment.id]
770+
)
771+
nested_rule = SegmentRule.objects.create(
772+
rule=segment_rule, type=SegmentRule.ANY_RULE
773+
)
774+
existing_condition = Condition.objects.create(
775+
rule=nested_rule, property="foo", operator=EQUAL, value="bar"
776+
)
777+
778+
data = {
779+
"name": segment.name,
780+
"project": project.id,
781+
"rules": [
782+
{
783+
"id": segment_rule.id,
784+
"type": segment_rule.type,
785+
"rules": [
786+
{
787+
"id": nested_rule.id,
788+
"type": nested_rule.type,
789+
"rules": [],
790+
"conditions": [
791+
{
792+
"id": existing_condition.id,
793+
"property": existing_condition.property,
794+
"operator": existing_condition.operator,
795+
"value": existing_condition.value,
796+
},
797+
],
798+
},
799+
{ # New rule
800+
"type": SegmentRule.ANY_RULE,
801+
"rules": [],
802+
"conditions": [
803+
{
804+
"property": "foo",
805+
"operator": EQUAL,
806+
"value": "bar",
807+
},
808+
],
809+
},
810+
],
811+
"conditions": [],
812+
}
813+
],
814+
}
815+
816+
# When
817+
response = admin_client_new.put(
818+
url, data=json.dumps(data), content_type="application/json"
819+
)
820+
821+
# Then
822+
assert response.status_code == status.HTTP_200_OK
823+
assert response.json()["rules"][0]["rules"][1]["type"] == SegmentRule.ANY_RULE
824+
assert response.json()["rules"][0]["rules"][1]["conditions"][0]["property"] == "foo"
825+
assert response.json()["rules"][0]["rules"][1]["conditions"][0]["operator"] == EQUAL
826+
assert response.json()["rules"][0]["rules"][1]["conditions"][0]["value"] == "bar"
827+
828+
assert segment_rule.rules.count() == 2
829+
830+
717831
def test_update_segment_add_new_condition(
718832
project: Project,
719833
admin_client_new: APIClient,
@@ -784,6 +898,79 @@ def test_update_segment_add_new_condition(
784898
assert expected_new_condition.value == new_condition_value
785899

786900

901+
def test_update_segment_delete_and_update_existing_condition(
902+
project: Project,
903+
admin_client_new: APIClient,
904+
segment: Segment,
905+
segment_rule: SegmentRule,
906+
settings: SettingsWrapper,
907+
) -> None:
908+
# Given
909+
settings.SEGMENT_RULES_CONDITIONS_EXPLICIT_ORDERING_ENABLED = True
910+
911+
url = reverse(
912+
"api-v1:projects:project-segments-detail", args=[project.id, segment.id]
913+
)
914+
nested_rule = SegmentRule.objects.create(
915+
rule=segment_rule, type=SegmentRule.ANY_RULE
916+
)
917+
existing_condition = Condition.objects.create(
918+
rule=nested_rule, property="foo", operator=EQUAL, value="bar"
919+
)
920+
new_condition = Condition.objects.create(
921+
rule=nested_rule, property="foo2", operator=EQUAL, value="bar2"
922+
)
923+
924+
new_condition_updated_property = "foo3"
925+
new_condition_updated_value = "bar3"
926+
data = {
927+
"name": segment.name,
928+
"project": project.id,
929+
"rules": [
930+
{
931+
"id": segment_rule.id,
932+
"type": segment_rule.type,
933+
"rules": [
934+
{
935+
"id": nested_rule.id,
936+
"type": nested_rule.type,
937+
"rules": [],
938+
"conditions": [
939+
{
940+
"id": existing_condition.id,
941+
"delete": True,
942+
"property": existing_condition.property,
943+
"operator": existing_condition.operator,
944+
"value": existing_condition.value,
945+
},
946+
{
947+
"id": new_condition.id,
948+
"property": new_condition_updated_property,
949+
"operator": new_condition.operator,
950+
"value": new_condition_updated_value,
951+
},
952+
],
953+
}
954+
],
955+
"conditions": [],
956+
}
957+
],
958+
}
959+
960+
# When
961+
response = admin_client_new.put(
962+
url, data=json.dumps(data), content_type="application/json"
963+
)
964+
965+
# Then
966+
assert response.status_code == status.HTTP_200_OK
967+
968+
assert nested_rule.conditions.count() == 1
969+
assert (expected_new_condition := nested_rule.conditions.first())
970+
assert expected_new_condition.property == new_condition_updated_property
971+
assert expected_new_condition.value == new_condition_updated_value
972+
973+
787974
def test_can_not_update_system_segment(
788975
project: Project,
789976
admin_client_new: APIClient,
@@ -1011,7 +1198,6 @@ def test_update_segment_delete_existing_condition( # type: ignore[no-untyped-de
10111198

10121199
# Then
10131200
assert response.status_code == status.HTTP_200_OK
1014-
10151201
assert nested_rule.conditions.count() == 0
10161202

10171203

@@ -1041,21 +1227,20 @@ def test_update_segment_delete_existing_rule(project, client, segment, segment_r
10411227
"type": nested_rule.type,
10421228
"rules": [],
10431229
"conditions": [],
1230+
"delete": True,
10441231
}
10451232
],
10461233
"conditions": [],
1047-
"delete": True,
10481234
}
10491235
],
10501236
}
10511237

10521238
# When
10531239
response = client.put(url, data=json.dumps(data), content_type="application/json")
1054-
10551240
# Then
10561241
assert response.status_code == status.HTTP_200_OK
10571242

1058-
assert segment_rule.conditions.count() == 0
1243+
assert nested_rule.conditions.count() == 0
10591244

10601245

10611246
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)