diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index c26194d..b9f7d56 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -14,6 +14,7 @@ from servala.core.models import ( ServiceOffering, ) from servala.frontend.forms.service import ( + ComputePlanSelectionForm, ControlPlaneSelectForm, ServiceFilterForm, ServiceInstanceDeleteForm, @@ -152,6 +153,13 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView control_plane=self.selected_plane, service_offering=self.object ).first() + @cached_property + def plan_form(self): + data = self.request.POST if self.request.method == "POST" else None + return ComputePlanSelectionForm( + data=data, control_plane_crd=self.context_object, prefix="plans" + ) + def get_instance_form_kwargs(self, ignore_data=False): return { "initial": { @@ -205,6 +213,7 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView context["select_form"] = self.select_form context["has_control_planes"] = self.planes.exists() context["selected_plane"] = self.selected_plane + context["context_object"] = self.context_object context["hide_expert_mode"] = self.hide_expert_mode if self.request.method == "POST": if self.is_custom_form: @@ -222,6 +231,17 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView if self.selected_plane and self.selected_plane.wildcard_dns: context["wildcard_dns"] = self.selected_plane.wildcard_dns context["organization_namespace"] = self.request.organization.namespace + + if self.context_object: + context["plan_form"] = self.plan_form + context["has_available_plans"] = self.plan_form.fields[ + "compute_plan_assignment" + ].queryset.exists() + if self.context_object.control_plane.storage_plan_price_per_gib: + context["storage_plan"] = { + "price_per_gib": self.context_object.control_plane.storage_plan_price_per_gib, + } + return context def post(self, request, *args, **kwargs): @@ -232,6 +252,9 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView context["form_error"] = True return self.render_to_response(context) + if not self.plan_form.is_valid(): + return self.render_to_response(context) + if self.is_custom_form: form = self.get_custom_instance_form() else: @@ -245,7 +268,11 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView ) return self.render_to_response(context) - if form.is_valid(): + if form.is_valid() and self.plan_form.is_valid(): + compute_plan_assignment = self.plan_form.cleaned_data[ + "compute_plan_assignment" + ] + try: service_instance = ServiceInstance.create_instance( organization=self.request.organization, @@ -253,16 +280,22 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView context=self.context_object, created_by=request.user, spec_data=form.get_nested_data().get("spec"), + compute_plan_assignment=compute_plan_assignment, ) return redirect(service_instance.urls.base) except ValidationError as e: form.add_error(None, e.message or str(e)) except Exception as e: error_message = self.organization.add_support_message( - _(f"Error creating instance: {str(e)}.") + _("Error creating instance: {error}.").format(error=str(e)) ) form.add_error(None, error_message) + if self.is_custom_form: + context["custom_service_form"] = form + else: + context["service_form"] = form + return self.render_to_response(context) @@ -332,6 +365,18 @@ class ServiceInstanceDetailView( context["has_delete_permission"] = self.request.user.has_perm( ServiceInstance.get_perm("delete"), self.object ) + + if self.object.compute_plan_assignment: + context["compute_plan_assignment"] = self.object.compute_plan_assignment + + if ( + self.object.context + and self.object.context.control_plane.storage_plan_price_per_gib + ): + context["storage_plan"] = { + "price_per_gib": self.object.context.control_plane.storage_plan_price_per_gib, + } + return context def get_nested_spec(self): @@ -475,6 +520,17 @@ class ServiceInstanceUpdateView( kwargs.pop("data", None) return cls(**kwargs) + @cached_property + def plan_form(self): + data = self.request.POST if self.request.method == "POST" else None + initial = self.object.compute_plan_assignment if self.object else None + return ComputePlanSelectionForm( + data=data, + control_plane_crd=self.object.context if self.object else None, + prefix="plans", + initial={"compute_plan_assignment": initial} if initial else None, + ) + @property def is_custom_form(self): # Note: "custom form" = user-friendly, subset of fields @@ -489,7 +545,7 @@ class ServiceInstanceUpdateView( else: form = self.get_form() - if form.is_valid(): + if form.is_valid() and self.plan_form.is_valid(): return self.form_valid(form) return self.form_invalid(form) @@ -506,14 +562,29 @@ class ServiceInstanceUpdateView( def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["hide_expert_mode"] = self.hide_expert_mode + + # Check if a form was passed (e.g., from form_invalid) + form_from_kwargs = kwargs.get("form") + if self.request.method == "POST": if self.is_custom_form: - context["custom_form"] = self.get_custom_form() + # Use the form with errors if passed, otherwise create new + context["custom_form"] = form_from_kwargs or self.get_custom_form() context["form"] = self.get_form(ignore_data=True) else: + # Use the form with errors if passed, otherwise create new + context["form"] = form_from_kwargs or self.get_form() context["custom_form"] = self.get_custom_form(ignore_data=True) else: context["custom_form"] = self.get_custom_form() + + if self.object and self.object.context: + context["plan_form"] = self.plan_form + if self.object.context.control_plane.storage_plan_price_per_gib: + context["storage_plan"] = { + "price_per_gib": self.object.context.control_plane.storage_plan_price_per_gib, + } + return context def _deep_merge(self, base, update): @@ -533,7 +604,17 @@ class ServiceInstanceUpdateView( current_spec = dict(self.object.spec) if self.object.spec else {} spec_data = self._deep_merge(current_spec, spec_data) - self.object.update_spec(spec_data=spec_data, updated_by=self.request.user) + compute_plan_assignment = None + if self.plan_form.is_valid(): + compute_plan_assignment = self.plan_form.cleaned_data.get( + "compute_plan_assignment" + ) + + self.object.update_spec( + spec_data=spec_data, + updated_by=self.request.user, + compute_plan_assignment=compute_plan_assignment, + ) messages.success( self.request, _("Service instance '{name}' updated successfully.").format( @@ -546,7 +627,7 @@ class ServiceInstanceUpdateView( return self.form_invalid(form) except Exception as e: error_message = self.organization.add_support_message( - _(f"Error updating instance: {str(e)}.") + _("Error updating instance: {error}.").format(error=str(e)) ) form.add_error(None, error_message) return self.form_invalid(form)