Skip to content

Commit

Permalink
feat: Filter features by owners and group owners (#3579)
Browse files Browse the repository at this point in the history
  • Loading branch information
zachaysan authored Mar 14, 2024
1 parent 165088b commit 79ad523
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 2 deletions.
26 changes: 25 additions & 1 deletion api/features/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,31 @@ class FeatureQuerySerializer(serializers.Serializer):
help_text="Integer ID of the environment to view features in the context of.",
)

def validate_tags(self, tags):
owners = serializers.CharField(
required=False,
help_text="Comma separated list of owner ids to filter on",
)
group_owners = serializers.CharField(
required=False,
help_text="Comma separated list of group owner ids to filter on",
)

def validate_owners(self, owners: str) -> list[int]:
try:
return [int(owner_id.strip()) for owner_id in owners.split(",")]
except ValueError:
raise serializers.ValidationError("Owner IDs must be integers.")

def validate_group_owners(self, group_owners: str) -> list[int]:
try:
return [
int(group_owner_id.strip())
for group_owner_id in group_owners.split(",")
]
except ValueError:
raise serializers.ValidationError("Group owner IDs must be integers.")

def validate_tags(self, tags: str) -> list[int]:
try:
return [int(tag_id.strip()) for tag_id in tags.split(",")]
except ValueError:
Expand Down
23 changes: 22 additions & 1 deletion api/features/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,26 @@ def _trigger_feature_state_change_webhooks(
feature_state, WebhookEventType.FLAG_DELETED
)

def _filter_queryset(self, queryset: QuerySet) -> QuerySet:
def filter_owners_and_group_owners(
self,
queryset: QuerySet[Feature],
query_data: dict[str, typing.Any],
) -> QuerySet[Feature]:
owners_q = Q()
if query_data.get("owners"):
owners_q = owners_q | Q(
owners__id__in=query_data["owners"],
)

group_owners_q = Q()
if query_data.get("group_owners"):
group_owners_q = group_owners_q | Q(
group_owners__id__in=query_data["group_owners"],
)

return queryset.filter(owners_q | group_owners_q)

def _filter_queryset(self, queryset: QuerySet[Feature]) -> QuerySet[Feature]:
query_serializer = FeatureQuerySerializer(data=self.request.query_params)
query_serializer.is_valid(raise_exception=True)
query_data = query_serializer.validated_data
Expand All @@ -324,6 +343,8 @@ def _filter_queryset(self, queryset: QuerySet) -> QuerySet:
if "is_archived" in query_serializer.initial_data:
queryset = queryset.filter(is_archived=query_data["is_archived"])

queryset = self.filter_owners_and_group_owners(queryset, query_data)

return queryset


Expand Down
133 changes: 133 additions & 0 deletions api/tests/unit/features/test_unit_features_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2622,3 +2622,136 @@ def test_feature_list_last_modified_values(
feature_data["last_modified_in_current_environment"]
== two_hours_ago.isoformat()
)


def test_filter_features_with_owners(
staff_client: APIClient,
staff_user: FFAdminUser,
admin_user: FFAdminUser,
project: Project,
feature: Feature,
with_project_permissions: WithProjectPermissionsCallable,
environment: Environment,
) -> None:
# Given
with_project_permissions([VIEW_PROJECT])

feature2 = Feature.objects.create(
name="included_feature", project=project, initial_value="initial_value"
)
Feature.objects.create(
name="not_included_feature", project=project, initial_value="gone"
)

# Include admin only in the first feature.
feature.owners.add(admin_user)

# Include staff only in the second feature.
feature2.owners.add(staff_user)

base_url = reverse("api-v1:projects:project-features-list", args=[project.id])

# Search for both users in the owners query param.
url = (
f"{base_url}?environment={environment.id}&"
f"owners={admin_user.id},{staff_user.id}"
)
# When
response = staff_client.get(url)

# Then
assert response.status_code == status.HTTP_200_OK

assert len(response.data["results"]) == 2
assert response.data["results"][0]["id"] == feature.id
assert response.data["results"][1]["id"] == feature2.id


def test_filter_features_with_group_owners(
staff_client: APIClient,
project: Project,
organisation: Organisation,
feature: Feature,
with_project_permissions: WithProjectPermissionsCallable,
environment: Environment,
) -> None:
# Given
with_project_permissions([VIEW_PROJECT])

feature2 = Feature.objects.create(
name="included_feature", project=project, initial_value="initial_value"
)
Feature.objects.create(
name="not_included_feature", project=project, initial_value="gone"
)

group_1 = UserPermissionGroup.objects.create(
name="Test Group", organisation=organisation
)
group_2 = UserPermissionGroup.objects.create(
name="Second Group", organisation=organisation
)

feature.group_owners.add(group_1)
feature2.group_owners.add(group_2)

base_url = reverse("api-v1:projects:project-features-list", args=[project.id])

# Search for both users in the owners query param.
url = (
f"{base_url}?environment={environment.id}&"
f"group_owners={group_1.id},{group_2.id}"
)
# When
response = staff_client.get(url)

# Then
assert response.status_code == status.HTTP_200_OK

assert len(response.data["results"]) == 2
assert response.data["results"][0]["id"] == feature.id
assert response.data["results"][1]["id"] == feature2.id


def test_filter_features_with_owners_and_group_owners_together(
staff_client: APIClient,
staff_user: FFAdminUser,
project: Project,
organisation: Organisation,
feature: Feature,
with_project_permissions: WithProjectPermissionsCallable,
environment: Environment,
) -> None:
# Given
with_project_permissions([VIEW_PROJECT])

feature2 = Feature.objects.create(
name="included_feature", project=project, initial_value="initial_value"
)
Feature.objects.create(
name="not_included_feature", project=project, initial_value="gone"
)

group_1 = UserPermissionGroup.objects.create(
name="Test Group", organisation=organisation
)

feature.group_owners.add(group_1)
feature2.owners.add(staff_user)

base_url = reverse("api-v1:projects:project-features-list", args=[project.id])

# Search for both users in the owners query param.
url = (
f"{base_url}?environment={environment.id}&"
f"group_owners={group_1.id}&owners={staff_user.id}"
)
# When
response = staff_client.get(url)

# Then
assert response.status_code == status.HTTP_200_OK

assert len(response.data["results"]) == 2
assert response.data["results"][0]["id"] == feature.id
assert response.data["results"][1]["id"] == feature2.id

0 comments on commit 79ad523

Please sign in to comment.