Skip to content
Merged
24 changes: 24 additions & 0 deletions api/features/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ class FeatureQuerySerializer(serializers.Serializer): # type: ignore[type-arg]
required=False,
help_text="Integer ID of the environment to view features in the context of.",
)
segment = serializers.IntegerField(
required=False,
help_text="Integer ID of the segment to retrieve segment overrides for.",
)
is_enabled = serializers.BooleanField(
allow_null=True,
required=False,
Expand Down Expand Up @@ -141,6 +145,7 @@ class CreateFeatureSerializer(DeleteBeforeUpdateWritableNestedModelSerializer):
group_owners = UserPermissionGroupSummarySerializer(many=True, read_only=True)

environment_feature_state = serializers.SerializerMethodField()
segment_feature_state = serializers.SerializerMethodField()

num_segment_overrides = serializers.SerializerMethodField(
help_text="Number of segment overrides that exist for the given feature "
Expand Down Expand Up @@ -188,6 +193,7 @@ class Meta:
"uuid",
"project",
"environment_feature_state",
"segment_feature_state",
"num_segment_overrides",
"num_identity_overrides",
"is_num_identity_overrides_complete",
Expand Down Expand Up @@ -296,6 +302,17 @@ def get_environment_feature_state( # type: ignore[return]
):
return FeatureStateSerializerSmall(instance=feature_state).data

@swagger_serializer_method( # type: ignore[misc]
serializer_or_field=FeatureStateSerializerSmall(allow_null=True)
)
def get_segment_feature_state( # type: ignore[return]
self, instance: Feature
) -> dict[str, Any] | None:
if (segment_feature_states := self.context.get("segment_feature_states")) and (
segment_feature_state := segment_feature_states.get(instance.id)
):
return FeatureStateSerializerSmall(instance=segment_feature_state).data

def get_num_segment_overrides(self, instance: Feature) -> int:
try:
return self.context["overrides_data"][instance.id].num_segment_overrides # type: ignore[no-any-return]
Expand Down Expand Up @@ -645,6 +662,13 @@ class SDKFeatureStatesQuerySerializer(serializers.Serializer): # type: ignore[t
)


class EnvironmentFeatureStatesQuerySerializer(serializers.Serializer): # type: ignore[type-arg]
segment = serializers.IntegerField(
required=False,
help_text="ID of the segment to filter segment overrides by.",
)


class CustomCreateSegmentOverrideFeatureStateSerializer(
CreateSegmentOverrideFeatureStateSerializer
):
Expand Down
26 changes: 23 additions & 3 deletions api/features/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,18 +182,33 @@ def get_queryset(self): # type: ignore[no-untyped-def]
page = self.paginate_queryset(queryset)
self.environment = Environment.objects.get(id=environment_id)
self.feature_ids = [feature.id for feature in page]
q = Q(
feature_states_query = Q(
feature_id__in=self.feature_ids,
identity__isnull=True,
feature_segment__isnull=True,
)
feature_states = get_environment_flags_list(
environment=self.environment,
additional_filters=q,
additional_filters=feature_states_query,
additional_select_related_args=["feature_state_value", "feature"],
)
self._feature_states = {fs.feature_id: fs for fs in feature_states}

if segment_id := query_data.get("segment"):
segment_query = Q(
feature_id__in=self.feature_ids,
identity__isnull=True,
feature_segment__segment_id=segment_id,
)
segment_feature_states = get_environment_flags_list(
environment=self.environment,
additional_filters=segment_query,
additional_select_related_args=["feature_state_value", "feature"],
)
self._segment_feature_states = {
fs.feature_id: fs for fs in segment_feature_states
}

return queryset

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

feature_states = getattr(self, "_feature_states", {})
segment_feature_states = getattr(self, "_segment_feature_states", {})
project = get_object_or_404(Project.objects.all(), pk=self.kwargs["project_pk"])
context.update(
project=project, user=self.request.user, feature_states=feature_states
project=project,
user=self.request.user,
feature_states=feature_states,
segment_feature_states=segment_feature_states,
)

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

def get_queryset(self): # type: ignore[no-untyped-def]
queryset = super().get_queryset().filter(feature_segment=None) # type: ignore[no-untyped-call]

if "anyIdentity" in self.request.query_params:
# TODO: deprecate anyIdentity query parameter
return queryset.exclude(identity=None)
Expand Down
62 changes: 60 additions & 2 deletions api/tests/unit/features/test_unit_features_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1595,8 +1595,6 @@ def test_environment_feature_states_does_not_return_null_versions(
assert len(response_json["results"]) == 1
assert response_json["results"][0]["id"] == feature_state.id

# Feature tests


def test_create_feature_default_is_archived_is_false(
admin_client_new: APIClient, project: Project
Expand Down Expand Up @@ -4077,6 +4075,66 @@ def test_get_multivariate_options_feature_not_found_responds_404(
assert response.status_code == status.HTTP_404_NOT_FOUND


@pytest.mark.parametrize(
"get_segment_id,expected_feature_state_value",
[
(lambda segment: segment.id, "segment_value"),
(lambda segment: segment.id + 9999, None),
],
)
def test_list_features_segment_query_param(
admin_client_new: APIClient,
project: Project,
feature: Feature,
environment: Environment,
segment: Segment,
get_segment_id: typing.Callable[[Segment], int],
expected_feature_state_value: str | None,
) -> None:
# Given
feature_segment = FeatureSegment.objects.create(
feature=feature,
segment=segment,
environment=environment,
)
segment_override = FeatureState.objects.create(
feature=feature,
feature_segment=feature_segment,
environment=environment,
enabled=True,
)
segment_override.feature_state_value.string_value = "segment_value"
segment_override.feature_state_value.save()

base_url = reverse("api-v1:projects:project-features-list", args=[project.id])
target_segment_id = get_segment_id(segment)
url = f"{base_url}?environment={environment.id}&segment={target_segment_id}"

# When
response = admin_client_new.get(url)

# Then
assert response.status_code == status.HTTP_200_OK
response_json = response.json()
feature_data = next(
filter(lambda r: r["id"] == feature.id, response_json["results"])
)

assert "environment_feature_state" in feature_data
assert "segment_feature_state" in feature_data

if expected_feature_state_value is None:
assert feature_data["segment_feature_state"] is None
else:
assert feature_data["segment_feature_state"] is not None
assert feature_data["segment_feature_state"]["id"] == segment_override.id
assert (
feature_data["segment_feature_state"]["feature_state_value"]
== expected_feature_state_value
)
assert feature_data["segment_feature_state"]["enabled"] is True


def test_create_multiple_features_with_metadata_keeps_metadata_isolated(
admin_client_new: APIClient,
project: Project,
Expand Down
5 changes: 3 additions & 2 deletions frontend/web/components/modals/CreateFlag.js
Original file line number Diff line number Diff line change
Expand Up @@ -1757,9 +1757,10 @@ const CreateFlag = class extends Component {
e.stopPropagation()
removeUserOverride(
{
cb: () =>
cb: () =>
this.userOverridesPage(
1, true
1,
true,
),
environmentId:
this.props
Expand Down
2 changes: 1 addition & 1 deletion frontend/web/components/tags/Tag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const Tag: FC<TagType> = ({
selected,
tag,
}) => {
const shouldLighten = (color: Color) => getDarkMode() && color.isDark();
const shouldLighten = (color: Color) => getDarkMode() && color.isDark()
const tagColor = Utils.colour(getTagColor(tag, selected))
if (isDot) {
return (
Expand Down
Loading