From c1f0094b0ed0d1bfb2046cc8d62cf8d136cdb0c0 Mon Sep 17 00:00:00 2001 From: Srijan Tripathi Date: Thu, 23 Jul 2026 07:20:33 +0000 Subject: [PATCH 1/4] Fix KeyError on partial mv-options PATCH updates (Resolves #8073) --- api/features/multivariate/serializers.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/api/features/multivariate/serializers.py b/api/features/multivariate/serializers.py index 5b7ade60c7a8..7f49f9a1bda5 100644 --- a/api/features/multivariate/serializers.py +++ b/api/features/multivariate/serializers.py @@ -77,16 +77,23 @@ def validate_key(self, value: str | None) -> str | None: raise serializers.ValidationError(RESERVED_VARIANT_KEY_MESSAGE) return value - def validate(self, attrs): # type: ignore[no-untyped-def] +def validate(self, attrs): # type: ignore[no-untyped-def] attrs = super().validate(attrs) + + feature = attrs.get("feature") or getattr(self.instance, "feature", None) + default_percentage_allocation = attrs.get( + "default_percentage_allocation", + getattr(self.instance, "default_percentage_allocation", 0) if self.instance else 0 + ) + total_sibling_percentage_allocation = ( - self._get_siblings(attrs["feature"]).aggregate( + self._get_siblings(feature).aggregate( total_percentage_allocation=Sum("default_percentage_allocation") )["total_percentage_allocation"] or 0 ) total_percentage_allocation = ( - total_sibling_percentage_allocation + attrs["default_percentage_allocation"] + total_sibling_percentage_allocation + default_percentage_allocation ) if total_percentage_allocation > 100: @@ -102,13 +109,15 @@ def _validate_key_is_unique(self, attrs: dict[str, typing.Any]) -> None: key = attrs.get("key") if key is None: return - if self._get_siblings(attrs["feature"]).filter(key=key).exists(): + + feature = attrs.get("feature") or getattr(self.instance, "feature", None) + + if self._get_siblings(feature).filter(key=key).exists(): raise ValidationError( { "key": "Multivariate option with this key already exists for the feature." } ) - def _get_siblings(self, feature: Feature): # type: ignore[no-untyped-def] siblings = feature.multivariate_options.all() if self.instance: From 7066fd7a37893e164707800eca94ae50b2cea2fc Mon Sep 17 00:00:00 2001 From: Srijan Tripathi Date: Thu, 23 Jul 2026 07:27:23 +0000 Subject: [PATCH 2/4] Fix model default bypass on creates and add regression test --- api/features/multivariate/serializers.py | 2 +- .../test_unit_multivariate_views.py | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/api/features/multivariate/serializers.py b/api/features/multivariate/serializers.py index 7f49f9a1bda5..96559c4e56a1 100644 --- a/api/features/multivariate/serializers.py +++ b/api/features/multivariate/serializers.py @@ -83,7 +83,7 @@ def validate(self, attrs): # type: ignore[no-untyped-def] feature = attrs.get("feature") or getattr(self.instance, "feature", None) default_percentage_allocation = attrs.get( "default_percentage_allocation", - getattr(self.instance, "default_percentage_allocation", 0) if self.instance else 0 + getattr(self.instance, "default_percentage_allocation", 0) if self.instance else 100 ) total_sibling_percentage_allocation = ( diff --git a/api/tests/unit/features/multivariate/test_unit_multivariate_views.py b/api/tests/unit/features/multivariate/test_unit_multivariate_views.py index 3fb923d257b3..9b913177abbf 100644 --- a/api/tests/unit/features/multivariate/test_unit_multivariate_views.py +++ b/api/tests/unit/features/multivariate/test_unit_multivariate_views.py @@ -105,3 +105,25 @@ def test_list_mv_options__feature_in_other_project__returns_404( # Then assert response.status_code == status.HTTP_404_NOT_FOUND + + +def test_multivariate_feature_option_create_omitted_allocation_fails_if_siblings_exist( + feature, +): + # Given an existing option taking up 50% + MultivariateFeatureOption.objects.create( + feature=feature, + default_percentage_allocation=50, + type="unicode", + string_value="control", + ) + + # When we try to create a new one without specifying an allocation + # (Serializer defaults to 100, so 50 + 100 = 150 > 100) + serializer = MultivariateFeatureOptionSerializer( + data={"feature": feature.id, "type": "unicode", "string_value": "variant"} + ) + + # Then it should fail validation + assert serializer.is_valid() is False + assert "default_percentage_allocation" in serializer.errors \ No newline at end of file From 61a3a7b93da6dc9d050a7b5b1ed32a41c0dc0d1b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 07:34:35 +0000 Subject: [PATCH 3/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../unit/features/multivariate/test_unit_multivariate_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/tests/unit/features/multivariate/test_unit_multivariate_views.py b/api/tests/unit/features/multivariate/test_unit_multivariate_views.py index 9b913177abbf..42609d657421 100644 --- a/api/tests/unit/features/multivariate/test_unit_multivariate_views.py +++ b/api/tests/unit/features/multivariate/test_unit_multivariate_views.py @@ -126,4 +126,4 @@ def test_multivariate_feature_option_create_omitted_allocation_fails_if_siblings # Then it should fail validation assert serializer.is_valid() is False - assert "default_percentage_allocation" in serializer.errors \ No newline at end of file + assert "default_percentage_allocation" in serializer.errors From f45e3ec10bad2d64854162f294a411126f6ee0cf Mon Sep 17 00:00:00 2001 From: Srijan Tripathi Date: Thu, 23 Jul 2026 14:24:24 +0000 Subject: [PATCH 4/4] Resolve key from instance in uniqueness validation --- api/features/multivariate/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/features/multivariate/serializers.py b/api/features/multivariate/serializers.py index 96559c4e56a1..7142aa8f83e8 100644 --- a/api/features/multivariate/serializers.py +++ b/api/features/multivariate/serializers.py @@ -106,6 +106,7 @@ def validate(self, attrs): # type: ignore[no-untyped-def] return attrs def _validate_key_is_unique(self, attrs: dict[str, typing.Any]) -> None: + key = attrs.get("key") or getattr(self.instance, "key", None) key = attrs.get("key") if key is None: return