From af0cc9c3105759e95a35283bab2776aeab5ce65c Mon Sep 17 00:00:00 2001 From: Matthew Elwell Date: Wed, 3 Apr 2024 17:55:28 +0100 Subject: [PATCH] feat: add is_live filter to versions endpoint (#3688) --- api/features/versioning/serializers.py | 4 ++ api/features/versioning/views.py | 29 ++++++++- .../versioning/test_unit_versioning_views.py | 62 +++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/api/features/versioning/serializers.py b/api/features/versioning/serializers.py index bcd67bdeea83..ce90f6b4f34f 100644 --- a/api/features/versioning/serializers.py +++ b/api/features/versioning/serializers.py @@ -48,3 +48,7 @@ def save(self, **kwargs): live_from=live_from, published_by=self.context["request"].user ) return self.instance + + +class EnvironmentFeatureVersionQuerySerializer(serializers.Serializer): + is_live = serializers.BooleanField(allow_null=True, required=False, default=None) diff --git a/api/features/versioning/views.py b/api/features/versioning/views.py index 72772fd831a9..3275db292843 100644 --- a/api/features/versioning/views.py +++ b/api/features/versioning/views.py @@ -1,4 +1,8 @@ +from django.db.models import BooleanField, ExpressionWrapper, Q from django.shortcuts import get_object_or_404 +from django.utils import timezone +from django.utils.decorators import method_decorator +from drf_yasg.utils import swagger_auto_schema from rest_framework.decorators import action from rest_framework.mixins import ( CreateModelMixin, @@ -25,12 +29,19 @@ from features.versioning.serializers import ( EnvironmentFeatureVersionFeatureStateSerializer, EnvironmentFeatureVersionPublishSerializer, + EnvironmentFeatureVersionQuerySerializer, EnvironmentFeatureVersionSerializer, ) from projects.permissions import VIEW_PROJECT from users.models import FFAdminUser +@method_decorator( + name="list", + decorator=swagger_auto_schema( + query_serializer=EnvironmentFeatureVersionQuerySerializer() + ), +) class EnvironmentFeatureVersionViewSet( GenericViewSet, ListModelMixin, @@ -77,10 +88,26 @@ def get_queryset(self): if getattr(self, "swagger_fake_view", False): return EnvironmentFeatureVersion.objects.none() - return EnvironmentFeatureVersion.objects.filter( + queryset = EnvironmentFeatureVersion.objects.filter( environment=self.environment, feature_id=self.feature ) + query_serializer = EnvironmentFeatureVersionQuerySerializer( + data=self.request.query_params + ) + query_serializer.is_valid(raise_exception=True) + + if (is_live := query_serializer.validated_data.get("is_live")) is not None: + queryset = queryset.annotate( + _is_live=ExpressionWrapper( + Q(published_at__isnull=False, live_from__lte=timezone.now()), + output_field=BooleanField(), + ) + ) + queryset = queryset.filter(_is_live=is_live) + + return queryset + def perform_create(self, serializer: Serializer) -> None: created_by = None if isinstance(self.request.user, FFAdminUser): diff --git a/api/tests/unit/features/versioning/test_unit_versioning_views.py b/api/tests/unit/features/versioning/test_unit_versioning_views.py index 876afbe7aee2..4f0b2267f180 100644 --- a/api/tests/unit/features/versioning/test_unit_versioning_views.py +++ b/api/tests/unit/features/versioning/test_unit_versioning_views.py @@ -9,8 +9,14 @@ from rest_framework import status from environments.models import Environment +from environments.permissions.constants import VIEW_ENVIRONMENT from features.models import FeatureSegment, FeatureState from features.versioning.models import EnvironmentFeatureVersion +from projects.permissions import VIEW_PROJECT +from tests.types import ( + WithEnvironmentPermissionsCallable, + WithProjectPermissionsCallable, +) if typing.TYPE_CHECKING: from rest_framework.test import APIClient @@ -520,3 +526,59 @@ def test_cannot_delete_environment_default_feature_state_for_unpublished_environ segment_override.refresh_from_db() assert segment_override.deleted is False + + +def test_filter_versions_by_is_live( + environment_v2_versioning: Environment, + feature: "Feature", + staff_user: "FFAdminUser", + staff_client: "APIClient", + with_environment_permissions: WithEnvironmentPermissionsCallable, + with_project_permissions: WithProjectPermissionsCallable, +) -> None: + # Given + # we give the user the correct permissions + with_environment_permissions([VIEW_ENVIRONMENT], environment_v2_versioning.id) + with_project_permissions([VIEW_PROJECT]) + + # an unpublished environment feature version + unpublished_environment_feature_version = EnvironmentFeatureVersion.objects.create( + environment=environment_v2_versioning, feature=feature + ) + + # and a published version + published_environment_feature_version = EnvironmentFeatureVersion.objects.create( + environment=environment_v2_versioning, feature=feature + ) + published_environment_feature_version.publish(staff_user) + + _base_url = reverse( + "api-v1:versioning:environment-feature-versions-list", + args=[environment_v2_versioning.id, feature.id], + ) + live_versions_url = "%s?is_live=true" % _base_url + not_live_versions_url = "%s?is_live=false" % _base_url + + # When + live_versions_response = staff_client.get(live_versions_url) + not_live_versions_response = staff_client.get(not_live_versions_url) + + # Then + # only the live versions are returned (the initial version) and the one we + # published above when we request the live versions + assert live_versions_response.status_code == status.HTTP_200_OK + + live_versions_response_json = live_versions_response.json() + assert live_versions_response_json["count"] == 2 + assert unpublished_environment_feature_version.uuid not in [ + result["uuid"] for result in live_versions_response_json["results"] + ] + + # and only the unpublished version is returned when we request the 'not live' versions + assert not_live_versions_response.status_code == status.HTTP_200_OK + + not_live_versions_response_json = not_live_versions_response.json() + assert not_live_versions_response_json["count"] == 1 + assert not_live_versions_response_json["results"][0]["uuid"] == str( + unpublished_environment_feature_version.uuid + )