Skip to content

Commit 9ad82d5

Browse files
feat: added-query-param-to-get-segment-feature-states (#6156)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent e9c4668 commit 9ad82d5

File tree

5 files changed

+121
-8
lines changed

5 files changed

+121
-8
lines changed

api/features/serializers.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ class FeatureQuerySerializer(serializers.Serializer): # type: ignore[type-arg]
9090
required=False,
9191
help_text="Integer ID of the environment to view features in the context of.",
9292
)
93+
segment = serializers.IntegerField(
94+
required=False,
95+
help_text="Integer ID of the segment to retrieve segment overrides for.",
96+
)
9397
is_enabled = serializers.BooleanField(
9498
allow_null=True,
9599
required=False,
@@ -141,6 +145,7 @@ class CreateFeatureSerializer(DeleteBeforeUpdateWritableNestedModelSerializer):
141145
group_owners = UserPermissionGroupSummarySerializer(many=True, read_only=True)
142146

143147
environment_feature_state = serializers.SerializerMethodField()
148+
segment_feature_state = serializers.SerializerMethodField()
144149

145150
num_segment_overrides = serializers.SerializerMethodField(
146151
help_text="Number of segment overrides that exist for the given feature "
@@ -188,6 +193,7 @@ class Meta:
188193
"uuid",
189194
"project",
190195
"environment_feature_state",
196+
"segment_feature_state",
191197
"num_segment_overrides",
192198
"num_identity_overrides",
193199
"is_num_identity_overrides_complete",
@@ -296,6 +302,17 @@ def get_environment_feature_state( # type: ignore[return]
296302
):
297303
return FeatureStateSerializerSmall(instance=feature_state).data
298304

305+
@swagger_serializer_method( # type: ignore[misc]
306+
serializer_or_field=FeatureStateSerializerSmall(allow_null=True)
307+
)
308+
def get_segment_feature_state( # type: ignore[return]
309+
self, instance: Feature
310+
) -> dict[str, Any] | None:
311+
if (segment_feature_states := self.context.get("segment_feature_states")) and (
312+
segment_feature_state := segment_feature_states.get(instance.id)
313+
):
314+
return FeatureStateSerializerSmall(instance=segment_feature_state).data
315+
299316
def get_num_segment_overrides(self, instance: Feature) -> int:
300317
try:
301318
return self.context["overrides_data"][instance.id].num_segment_overrides # type: ignore[no-any-return]
@@ -645,6 +662,13 @@ class SDKFeatureStatesQuerySerializer(serializers.Serializer): # type: ignore[t
645662
)
646663

647664

665+
class EnvironmentFeatureStatesQuerySerializer(serializers.Serializer): # type: ignore[type-arg]
666+
segment = serializers.IntegerField(
667+
required=False,
668+
help_text="ID of the segment to filter segment overrides by.",
669+
)
670+
671+
648672
class CustomCreateSegmentOverrideFeatureStateSerializer(
649673
CreateSegmentOverrideFeatureStateSerializer
650674
):

api/features/views.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,18 +182,33 @@ def get_queryset(self): # type: ignore[no-untyped-def]
182182
page = self.paginate_queryset(queryset)
183183
self.environment = Environment.objects.get(id=environment_id)
184184
self.feature_ids = [feature.id for feature in page]
185-
q = Q(
185+
feature_states_query = Q(
186186
feature_id__in=self.feature_ids,
187187
identity__isnull=True,
188188
feature_segment__isnull=True,
189189
)
190190
feature_states = get_environment_flags_list(
191191
environment=self.environment,
192-
additional_filters=q,
192+
additional_filters=feature_states_query,
193193
additional_select_related_args=["feature_state_value", "feature"],
194194
)
195195
self._feature_states = {fs.feature_id: fs for fs in feature_states}
196196

197+
if segment_id := query_data.get("segment"):
198+
segment_query = Q(
199+
feature_id__in=self.feature_ids,
200+
identity__isnull=True,
201+
feature_segment__segment_id=segment_id,
202+
)
203+
segment_feature_states = get_environment_flags_list(
204+
environment=self.environment,
205+
additional_filters=segment_query,
206+
additional_select_related_args=["feature_state_value", "feature"],
207+
)
208+
self._segment_feature_states = {
209+
fs.feature_id: fs for fs in segment_feature_states
210+
}
211+
197212
return queryset
198213

199214
def paginate_queryset(self, queryset: QuerySet[Feature]) -> list[Feature]: # type: ignore[override]
@@ -225,9 +240,13 @@ def get_serializer_context(self): # type: ignore[no-untyped-def]
225240
return context
226241

227242
feature_states = getattr(self, "_feature_states", {})
243+
segment_feature_states = getattr(self, "_segment_feature_states", {})
228244
project = get_object_or_404(Project.objects.all(), pk=self.kwargs["project_pk"])
229245
context.update(
230-
project=project, user=self.request.user, feature_states=feature_states
246+
project=project,
247+
user=self.request.user,
248+
feature_states=feature_states,
249+
segment_feature_states=segment_feature_states,
231250
)
232251

233252
if self.action == "list" and "environment" in self.request.query_params:
@@ -664,6 +683,7 @@ class EnvironmentFeatureStateViewSet(BaseFeatureStateViewSet):
664683

665684
def get_queryset(self): # type: ignore[no-untyped-def]
666685
queryset = super().get_queryset().filter(feature_segment=None) # type: ignore[no-untyped-call]
686+
667687
if "anyIdentity" in self.request.query_params:
668688
# TODO: deprecate anyIdentity query parameter
669689
return queryset.exclude(identity=None)

api/tests/unit/features/test_unit_features_views.py

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,8 +1595,6 @@ def test_environment_feature_states_does_not_return_null_versions(
15951595
assert len(response_json["results"]) == 1
15961596
assert response_json["results"][0]["id"] == feature_state.id
15971597

1598-
# Feature tests
1599-
16001598

16011599
def test_create_feature_default_is_archived_is_false(
16021600
admin_client_new: APIClient, project: Project
@@ -4077,6 +4075,76 @@ def test_get_multivariate_options_feature_not_found_responds_404(
40774075
assert response.status_code == status.HTTP_404_NOT_FOUND
40784076

40794077

4078+
def test_list_features_segment_query_param_with_valid_segment(
4079+
admin_client_new: APIClient,
4080+
project: Project,
4081+
feature: Feature,
4082+
environment: Environment,
4083+
segment: Segment,
4084+
) -> None:
4085+
# Given
4086+
feature_segment = FeatureSegment.objects.create(
4087+
feature=feature,
4088+
segment=segment,
4089+
environment=environment,
4090+
)
4091+
segment_override = FeatureState.objects.create(
4092+
feature=feature,
4093+
feature_segment=feature_segment,
4094+
environment=environment,
4095+
enabled=True,
4096+
)
4097+
segment_override.feature_state_value.string_value = "segment_value"
4098+
segment_override.feature_state_value.save()
4099+
4100+
base_url = reverse("api-v1:projects:project-features-list", args=[project.id])
4101+
url = f"{base_url}?environment={environment.id}&segment={segment.id}"
4102+
4103+
# When
4104+
response = admin_client_new.get(url)
4105+
4106+
# Then
4107+
assert response.status_code == status.HTTP_200_OK
4108+
response_json = response.json()
4109+
feature_data = next(
4110+
filter(lambda r: r["id"] == feature.id, response_json["results"])
4111+
)
4112+
4113+
assert "environment_feature_state" in feature_data
4114+
segment_state = feature_data["segment_feature_state"]
4115+
4116+
assert segment_state is not None
4117+
assert segment_state["id"] == segment_override.id
4118+
assert segment_state["feature_state_value"] == "segment_value"
4119+
assert segment_state["enabled"] is True
4120+
4121+
4122+
def test_list_features_segment_query_param_with_invalid_segment(
4123+
admin_client_new: APIClient,
4124+
project: Project,
4125+
feature: Feature,
4126+
environment: Environment,
4127+
segment: Segment,
4128+
) -> None:
4129+
# Given
4130+
base_url = reverse("api-v1:projects:project-features-list", args=[project.id])
4131+
invalid_segment_id = segment.id + 9999
4132+
url = f"{base_url}?environment={environment.id}&segment={invalid_segment_id}"
4133+
4134+
# When
4135+
response = admin_client_new.get(url)
4136+
4137+
# Then
4138+
assert response.status_code == status.HTTP_200_OK
4139+
response_json = response.json()
4140+
feature_data = next(
4141+
filter(lambda r: r["id"] == feature.id, response_json["results"])
4142+
)
4143+
4144+
assert "segment_feature_state" in feature_data
4145+
assert feature_data["segment_feature_state"] is None
4146+
4147+
40804148
def test_create_multiple_features_with_metadata_keeps_metadata_isolated(
40814149
admin_client_new: APIClient,
40824150
project: Project,

frontend/web/components/modals/CreateFlag.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1757,9 +1757,10 @@ const CreateFlag = class extends Component {
17571757
e.stopPropagation()
17581758
removeUserOverride(
17591759
{
1760-
cb: () =>
1760+
cb: () =>
17611761
this.userOverridesPage(
1762-
1, true
1762+
1,
1763+
true,
17631764
),
17641765
environmentId:
17651766
this.props

frontend/web/components/tags/Tag.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const Tag: FC<TagType> = ({
6666
selected,
6767
tag,
6868
}) => {
69-
const shouldLighten = (color: Color) => getDarkMode() && color.isDark();
69+
const shouldLighten = (color: Color) => getDarkMode() && color.isDark()
7070
const tagColor = Utils.colour(getTagColor(tag, selected))
7171
if (isDot) {
7272
return (

0 commit comments

Comments
 (0)