From 218505324ec26510d9b6c8839d79acde054c85ee Mon Sep 17 00:00:00 2001 From: Gagan Trivedi Date: Fri, 29 Nov 2024 13:52:43 +0530 Subject: [PATCH] feat(environment/views): Add get-by-uuid action --- .../migrations/0037_add_uuid_field.py | 29 +++++++++++++++ api/environments/models.py | 6 ++- api/environments/serializers.py | 1 + api/environments/views.py | 22 ++++++++++- .../test_unit_environments_views.py | 37 +++++++++++++++++++ 5 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 api/environments/migrations/0037_add_uuid_field.py diff --git a/api/environments/migrations/0037_add_uuid_field.py b/api/environments/migrations/0037_add_uuid_field.py new file mode 100644 index 000000000000..eaf32b1c642b --- /dev/null +++ b/api/environments/migrations/0037_add_uuid_field.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.23 on 2024-11-27 08:46 + +from django.db import migrations, models +import uuid + +from core.migration_helpers import AddDefaultUUIDs + + +class Migration(migrations.Migration): + dependencies = [ + ("environments", "0036_add_is_creating_field"), + ] + + operations = [ + migrations.AddField( + model_name="environment", + name="uuid", + field=models.UUIDField(editable=False, null=True), + ), + migrations.RunPython( + AddDefaultUUIDs("environments", "environment"), + reverse_code=migrations.RunPython.noop, + ), + migrations.AlterField( + model_name="environment", + name="uuid", + field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True), + ), + ] diff --git a/api/environments/models.py b/api/environments/models.py index f1cf5adc5132..b01b15c89453 100644 --- a/api/environments/models.py +++ b/api/environments/models.py @@ -1,5 +1,6 @@ import logging import typing +import uuid from copy import deepcopy from core.models import abstract_base_auditable_model_factory @@ -63,7 +64,8 @@ class Environment( LifecycleModel, abstract_base_auditable_model_factory( - change_details_excluded_fields=["updated_at"] + change_details_excluded_fields=["updated_at"], + historical_records_excluded_fields=["uuid"], ), SoftDeleteObject, ): @@ -71,6 +73,7 @@ class Environment( related_object_type = RelatedObjectType.ENVIRONMENT name = models.CharField(max_length=2000) + uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False) created_date = models.DateTimeField("DateCreated", auto_now_add=True) description = models.TextField(null=True, blank=True, max_length=20000) project = models.ForeignKey( @@ -178,6 +181,7 @@ def clone( """ clone = deepcopy(self) clone.id = None + clone.uuid = uuid.uuid4() clone.name = name clone.api_key = api_key if api_key else create_hash() clone.is_creating = True diff --git a/api/environments/serializers.py b/api/environments/serializers.py index 4d2836732859..70ac6f9be8cc 100644 --- a/api/environments/serializers.py +++ b/api/environments/serializers.py @@ -44,6 +44,7 @@ class Meta: model = Environment fields = ( "id", + "uuid", "name", "api_key", "description", diff --git a/api/environments/views.py b/api/environments/views.py index b60b8a1ab929..c035e8bc6218 100644 --- a/api/environments/views.py +++ b/api/environments/views.py @@ -1,13 +1,17 @@ import logging -from common.environments.permissions import TAG_SUPPORTED_PERMISSIONS +from common.environments.permissions import ( + TAG_SUPPORTED_PERMISSIONS, + VIEW_ENVIRONMENT, +) from django.db.models import Count, Q from django.utils.decorators import method_decorator from drf_yasg import openapi from drf_yasg.utils import no_body, swagger_auto_schema from rest_framework import mixins, status, viewsets from rest_framework.decorators import action -from rest_framework.exceptions import ValidationError +from rest_framework.exceptions import PermissionDenied, ValidationError +from rest_framework.generics import get_object_or_404 from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response @@ -142,6 +146,20 @@ def perform_create(self, serializer): user=self.request.user, environment=environment, admin=True ) + @action( + detail=False, + url_path=r"get-by-uuid/(?P[0-9a-f-]+)", + methods=["get"], + ) + def get_by_uuid(self, request, uuid): + qs = self.get_queryset() + environment = get_object_or_404(qs, uuid=uuid) + if not request.user.has_environment_permission(VIEW_ENVIRONMENT, environment): + raise PermissionDenied() + + serializer = self.get_serializer(environment) + return Response(serializer.data) + @action(detail=True, methods=["GET"], url_path="trait-keys") def trait_keys(self, request, *args, **kwargs): keys = [ diff --git a/api/tests/unit/environments/test_unit_environments_views.py b/api/tests/unit/environments/test_unit_environments_views.py index bbe4102330f7..41c39911b4e6 100644 --- a/api/tests/unit/environments/test_unit_environments_views.py +++ b/api/tests/unit/environments/test_unit_environments_views.py @@ -76,6 +76,43 @@ def test_retrieve_environment( ) +def test_get_by_uuid_returns_environment( + staff_client: APIClient, + environment: Environment, + with_environment_permissions: WithEnvironmentPermissionsCallable, +) -> None: + # Given + with_environment_permissions([VIEW_ENVIRONMENT]) + + url = reverse( + "api-v1:environments:environment-get-by-uuid", + args=[environment.uuid], + ) + + # When + response = staff_client.get(url) + + # Then + assert response.status_code == status.HTTP_200_OK + assert response.json()["uuid"] == str(environment.uuid) + + +def test_get_by_uuid_returns_403_for_user_without_permission( + staff_client: APIClient, environment: Environment +) -> None: + # Given + url = reverse( + "api-v1:environments:environment-get-by-uuid", + args=[environment.uuid], + ) + + # When + response = staff_client.get(url) + + # Then + assert response.status_code == status.HTTP_403_FORBIDDEN + + def test_user_with_view_environment_permission_can_retrieve_environment( staff_client: APIClient, environment: Environment,