Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.1
rev: v0.14.2
hooks:
# Run the linter.
- id: ruff
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

# [Flagsmith](https://flagsmith.com/) is an Open-Source Feature Flagging Tool to Ship Faster & Control Releases

Change the way your team releases software. Roll out, segment, and optimise—with granular control. Stay secure with on-premise and private cloud hosting.
Change the way your team releases software. Roll out, segment, and optimise—with granular control. Stay secure with on-premise and private cloud hosting.

* Feature flags: Release features behind the safety of a feature flag
* Make changes remotely: Easily toggle individual features on and off, and make changes without deploying new code
Expand Down
2 changes: 1 addition & 1 deletion api/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ integrate-private-tests:

.PHONY: generate-docs
generate-docs:
poetry run flagsmith docgen metrics > ../docs/docs/system-administration/metrics.md
poetry run flagsmith docgen metrics > ../docs/docs/administration-and-security/platform-configuration/metrics.md

.PHONY: add-known-sdk-version
add-known-sdk-version:
Expand Down
5 changes: 4 additions & 1 deletion api/app_analytics/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
"flagsmith-kotlin-android-sdk": ["unknown"],
"flagsmith-nodejs-sdk": ["unknown"],
"flagsmith-php-sdk": ["unknown"],
"flagsmith-python-sdk": ["unknown"],
"flagsmith-python-sdk": [
"unknown",
"5.0.0",
],
"flagsmith-ruby-sdk": ["unknown"],
"flagsmith-rust-sdk": ["unknown"],
"flagsmith-swift-ios-sdk": ["unknown"],
Expand Down
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
31 changes: 26 additions & 5 deletions api/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ pygithub = "2.1.1"
hubspot-api-client = "^8.2.1"
djangorestframework-dataclasses = "^1.3.1"
pyotp = "^2.9.0"
flagsmith-common = "^2.2.0"
flagsmith-common = "^2.2.2"
django-stubs = "^5.1.3"
tzdata = "^2024.1"
djangorestframework-simplejwt = "^5.5.1"
Expand Down
72 changes: 70 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,76 @@ def test_get_multivariate_options_feature_not_found_responds_404(
assert response.status_code == status.HTTP_404_NOT_FOUND


def test_list_features_segment_query_param_with_valid_segment(
admin_client_new: APIClient,
project: Project,
feature: Feature,
environment: Environment,
segment: Segment,
) -> 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])
url = f"{base_url}?environment={environment.id}&segment={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
segment_state = feature_data["segment_feature_state"]

assert segment_state is not None
assert segment_state["id"] == segment_override.id
assert segment_state["feature_state_value"] == "segment_value"
assert segment_state["enabled"] is True


def test_list_features_segment_query_param_with_invalid_segment(
admin_client_new: APIClient,
project: Project,
feature: Feature,
environment: Environment,
segment: Segment,
) -> None:
# Given
base_url = reverse("api-v1:projects:project-features-list", args=[project.id])
invalid_segment_id = segment.id + 9999
url = f"{base_url}?environment={environment.id}&segment={invalid_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 "segment_feature_state" in feature_data
assert feature_data["segment_feature_state"] is None


def test_create_multiple_features_with_metadata_keeps_metadata_isolated(
admin_client_new: APIClient,
project: Project,
Expand Down
5 changes: 5 additions & 0 deletions docs/docs/administration-and-security/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"label": "Administration and Security",
"position": 4,
"collapsed": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"label": "Access Control",
"position": 2,
"collapsed": true
}
Loading