From fa7a1708718b09ed76f0e13db9c20dca43b080e3 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 6 Nov 2025 16:38:13 +0100 Subject: [PATCH] Skip offering selection if there is only one closes #258 --- src/servala/frontend/views/service.py | 27 ++++++- src/tests/test_views.py | 101 ++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 4 deletions(-) diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index 689f381..5f2d914 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -66,14 +66,33 @@ class ServiceDetailView(OrganizationViewMixin, DetailView): def get_queryset(self): return self.request.organization.get_visible_services() - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - offerings = context["service"].offerings.all() + @cached_property + def visible_offerings(self): + offerings = self.object.offerings.all() if self.request.organization.limit_cloudproviders.exists(): offerings = offerings.filter( provider__in=self.request.organization.limit_cloudproviders.all() ) - context["visible_offerings"] = offerings.select_related("provider") + return offerings + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + + # If there's exactly one offering, skip provider selection and go directly to it + if self.visible_offerings.count() == 1: + offering = self.visible_offerings.first() + return redirect( + "frontend:organization.offering", + organization=self.request.organization.slug, + slug=self.object.slug, + pk=offering.pk, + ) + + return super().get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["visible_offerings"] = self.visible_offerings.select_related("provider") return context diff --git a/src/tests/test_views.py b/src/tests/test_views.py index 5ec429e..a97ecd1 100644 --- a/src/tests/test_views.py +++ b/src/tests/test_views.py @@ -1,5 +1,6 @@ import pytest +from servala.core.models.service import CloudProvider, ServiceOffering @pytest.mark.parametrize( "url,redirect", @@ -45,3 +46,103 @@ def test_organization_linked_in_sidebar( assert response.status_code == 200 assert organization.name in response.content.decode() assert other_organization.name not in response.content.decode() + + +@pytest.mark.django_db +def test_service_detail_redirects_with_single_offering( + client, org_owner, organization, test_service, test_service_offering +): + client.force_login(org_owner) + url = f"/org/{organization.slug}/services/{test_service.slug}/" + response = client.get(url) + + assert response.status_code == 302 + expected_url = f"/org/{organization.slug}/services/{test_service.slug}/offering/{test_service_offering.pk}/" + assert response.url == expected_url + + +@pytest.mark.django_db +def test_service_detail_shows_multiple_offerings( + client, org_owner, organization, test_service, test_service_offering +): + second_provider = CloudProvider.objects.create( + name="AWS", description="Amazon Web Services" + ) + second_offering = ServiceOffering.objects.create( + service=test_service, + provider=second_provider, + description="Redis on AWS", + osb_plan_id="test-plan-456", + ) + + client.force_login(org_owner) + url = f"/org/{organization.slug}/services/{test_service.slug}/" + response = client.get(url) + + assert response.status_code == 200 + content = response.content.decode() + + assert test_service_offering.provider.name in content + assert second_offering.provider.name in content + assert "Create Instance" in content + + +@pytest.mark.django_db +def test_service_detail_respects_cloud_provider_restrictions( + client, org_owner, organization, test_service, test_service_offering +): + second_provider = CloudProvider.objects.create( + name="AWS", description="Amazon Web Services" + ) + ServiceOffering.objects.create( + service=test_service, + provider=second_provider, + description="Redis on AWS", + osb_plan_id="test-plan-456", + ) + organization.origin.limit_cloudproviders.add(test_service_offering.provider) + + client.force_login(org_owner) + url = f"/org/{organization.slug}/services/{test_service.slug}/" + response = client.get(url) + + assert response.status_code == 302 + expected_url = f"/org/{organization.slug}/services/{test_service.slug}/offering/{test_service_offering.pk}/" + assert response.url == expected_url + + +@pytest.mark.django_db +def test_service_detail_no_redirect_with_restricted_multiple_offerings( + client, org_owner, organization, test_service, test_service_offering +): + second_provider = CloudProvider.objects.create( + name="AWS", description="Amazon Web Services" + ) + second_offering = ServiceOffering.objects.create( + service=test_service, + provider=second_provider, + description="Redis on AWS", + osb_plan_id="test-plan-456", + ) + third_provider = CloudProvider.objects.create( + name="Azure", description="Microsoft Azure" + ) + third_offering = ServiceOffering.objects.create( + service=test_service, + provider=third_provider, + description="Redis on Azure", + osb_plan_id="test-plan-789", + ) + organization.origin.limit_cloudproviders.add( + test_service_offering.provider, second_provider + ) + + client.force_login(org_owner) + url = f"/org/{organization.slug}/services/{test_service.slug}/" + response = client.get(url) + + assert response.status_code == 200 + content = response.content.decode() + assert test_service_offering.provider.name in content + assert second_offering.provider.name in content + assert third_offering.provider.name not in content