diff --git a/src/servala/core/crd.py b/src/servala/core/crd.py new file mode 100644 index 0000000..bf22bcc --- /dev/null +++ b/src/servala/core/crd.py @@ -0,0 +1,125 @@ +import re + +from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator +from django.db import models +from django.forms.models import ModelForm, ModelFormMetaclass +from django.utils.translation import gettext_lazy as _ + + +def generate_django_model(schema, group, version, kind): + """ + Generates a virtual Django model from a Kubernetes CRD's OpenAPI v3 schema. + """ + spec = schema["properties"].get("spec") or {} + # defaults = {"apiVersion": f"{group}/{version}", "kind": kind} + + model_fields = {"__module__": "crd_models"} + model_fields.update(build_object_fields(spec, "spec")) + + meta_class = type("Meta", (), {"app_label": "crd_models"}) + model_fields["Meta"] = meta_class + + # create the model class + model_name = kind + model_class = type(model_name, (models.Model,), model_fields) + return model_class + + +def build_object_fields(schema, name, verbose_name_prefix=None): + required_fields = schema.get("required", []) + properties = schema.get("properties", {}) + fields = {} + + for field_name, field_schema in properties.items(): + is_required = field_name in required_fields + full_name = f"{name}.{field_name}" + result = get_django_field( + field_schema, + is_required, + field_name, + full_name, + verbose_name_prefix=verbose_name_prefix, + ) + if isinstance(result, dict): + fields.update(result) + else: + fields[full_name] = result + return fields + + +def deslugify(title): + if "_" in title: + title.replace("_", " ") + return title.title() + return re.sub(r"(? + {% translate "Oops! Something went wrong with the service form generation. Please try again later." %} + + {% else %} + {% include "includes/form.html" with form=service_form %} + {% endif %} +{% endif %} +{% endpartialdef %} {% block content %}
@@ -23,17 +35,16 @@
-
- {% if offering.control_planes.all.count > 1 %} -

{% translate "Please choose your zone." %}

- {% else %} -

- {% blocktranslate trimmed with zone=offering.control_planes.all.first.name %} - Your zone will be {{ zone }}. - {% endblocktranslate %} -

- {% endif %} -
+ {% if offering.control_planes.all.count == 0 %} +

{% translate "We currently cannot offer this service, sorry!" %}

+ {% else %} +
+ {{ select_form }} +
+
{% partial service-form %}
+ {% endif %}
diff --git a/src/servala/frontend/views/mixins.py b/src/servala/frontend/views/mixins.py index f304e42..fd5a494 100644 --- a/src/servala/frontend/views/mixins.py +++ b/src/servala/frontend/views/mixins.py @@ -5,13 +5,30 @@ from rules.contrib.views import AutoPermissionRequiredMixin, PermissionRequiredM from servala.core.models import Organization -class HtmxUpdateView(AutoPermissionRequiredMixin, UpdateView): +class HtmxViewMixin: fragments = [] @cached_property def is_htmx(self): return self.request.headers.get("HX-Request") + def _get_fragment(self): + if self.request.method == "POST": + fragment = self.request.POST.get("fragment") + else: + fragment = self.request.GET.get("fragment") + if fragment and fragment in self.fragments: + return fragment + + def get_template_names(self): + template_names = super().get_template_names() + if self.is_htmx and (fragment := self._get_fragment()): + return [f"{template_names[0]}#{fragment}"] + return template_names + + +class HtmxUpdateView(AutoPermissionRequiredMixin, HtmxViewMixin, UpdateView): + @property def permission_type(self): if self.request.method == "POST" or getattr( @@ -31,20 +48,6 @@ class HtmxUpdateView(AutoPermissionRequiredMixin, UpdateView): result["has_change_permission"] = self.has_change_permission() return result - def _get_fragment(self): - if self.request.method == "POST": - fragment = self.request.POST.get("fragment") - else: - fragment = self.request.GET.get("fragment") - if fragment and fragment in self.fragments: - return fragment - - def get_template_names(self): - template_names = super().get_template_names() - if self.is_htmx and (fragment := self._get_fragment()): - return [f"{template_names[0]}#{fragment}"] - return template_names - def get_form_kwargs(self): result = super().get_form_kwargs() if self.is_htmx: @@ -82,8 +85,8 @@ class OrganizationViewMixin(PermissionRequiredMixin): def get_permission_object(self): return self.organization + def has_organization_permission(self): + return self.request.user.has_perm("core.view_organization", self.organization) + def has_permission(self): - return ( - self.request.user.has_perm("core.view_organization", self.organization) - and super().has_permission() - ) + return self.has_organization_permission() and super().has_permission() diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index f5c33f0..faead0f 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -1,9 +1,9 @@ from django.utils.functional import cached_property from django.views.generic import DetailView, ListView -from servala.core.models import Service, ServiceOffering -from servala.frontend.forms.service import ServiceFilterForm -from servala.frontend.views.mixins import OrganizationViewMixin +from servala.core.models import Service, ServiceOffering, ServiceOfferingControlPlane +from servala.frontend.forms.service import ControlPlaneSelectForm, ServiceFilterForm +from servala.frontend.views.mixins import HtmxViewMixin, OrganizationViewMixin class ServiceListView(OrganizationViewMixin, ListView): @@ -44,13 +44,43 @@ class ServiceDetailView(OrganizationViewMixin, DetailView): ) -class ServiceOfferingDetailView(OrganizationViewMixin, DetailView): +class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView): template_name = "frontend/organizations/service_offering_detail.html" context_object_name = "offering" model = ServiceOffering permission_type = "view" + fragments = ("service-form",) + + def has_permission(self): + return self.has_organization_permission() def get_queryset(self): return ServiceOffering.objects.all().select_related( "service", "service__category", "provider" ) + + @cached_property + def planes(self): + return self.object.control_planes.all() + + @cached_property + def select_form(self): + data = None + if "control_plane" in self.request.GET: + data = self.request.GET + return ControlPlaneSelectForm(data=data, planes=self.planes) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["select_form"] = self.select_form + if "control_plane" in self.request.GET: + if self.select_form.is_valid(): + so_cp = ServiceOfferingControlPlane.objects.filter( + control_plane=self.select_form.cleaned_data["control_plane"], + service_offering=self.object, + ).first() + if not so_cp: + context["form_error"] = True + else: + context["service_form"] = so_cp.model_form_class() + return context