diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index 9310bd4..d718fe0 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin, messages from django.utils.translation import gettext_lazy as _ -from servala.core.forms import ControlPlaneAdminForm, ServiceDefinitionAdminForm +from servala.core.forms import ControlPlaneAdminForm from servala.core.models import ( BillingEntity, CloudProvider, @@ -12,9 +12,7 @@ from servala.core.models import ( Plan, Service, ServiceCategory, - ServiceDefinition, ServiceOffering, - ServiceOfferingControlPlane, User, ) @@ -103,7 +101,6 @@ class ServiceAdmin(admin.ModelAdmin): list_filter = ("category",) search_fields = ("name", "description") autocomplete_fields = ("category",) - prepopulated_fields = {"slug": ["name"]} @admin.register(CloudProvider) @@ -124,7 +121,7 @@ class ControlPlaneAdmin(admin.ModelAdmin): fieldsets = ( ( None, - {"fields": ("name", "description", "cloud_provider", "service_definition")}, + {"fields": ("name", "description", "cloud_provider")}, ), ( _("API Credentials"), @@ -173,54 +170,11 @@ class PlanAdmin(admin.ModelAdmin): autocomplete_fields = ("service_offering",) -@admin.register(ServiceDefinition) -class ServiceDefinitionAdmin(admin.ModelAdmin): - form = ServiceDefinitionAdminForm - list_display = ("name", "service") - list_filter = ("service",) - search_fields = ("name", "description") - autocomplete_fields = ("service",) - - fieldsets = ( - ( - None, - {"fields": ("name", "description", "service")}, - ), - ( - _("API Definition"), - { - "fields": ("api_group", "api_version", "api_kind"), - "description": _("API definition for the Kubernetes Custom Resource"), - }, - ), - ) - - def get_exclude(self, request, obj=None): - # Exclude the original api_definition field as we're using our custom fields - return ["api_definition"] - - -class ServiceOfferingControlPlaneInline(admin.TabularInline): - model = ServiceOfferingControlPlane - extra = 1 - autocomplete_fields = ("control_plane", "service_definition") - - -@admin.register(ServiceOfferingControlPlane) -class ServiceOfferingControlPlaneAdmin(admin.ModelAdmin): - list_display = ("service_offering", "control_plane", "service_definition") - list_filter = ("service_offering", "control_plane", "service_definition") - search_fields = ("service_offering__service__name", "control_plane__name") - autocomplete_fields = ("service_offering", "control_plane", "service_definition") - - @admin.register(ServiceOffering) class ServiceOfferingAdmin(admin.ModelAdmin): list_display = ("id", "service", "provider") - list_filter = ("service", "provider") + list_filter = ("service", "provider", "control_plane") search_fields = ("description",) autocomplete_fields = ("service", "provider") - inlines = ( - ServiceOfferingControlPlaneInline, - PlanInline, - ) + filter_horizontal = ("control_plane",) + inlines = (PlanInline,) diff --git a/src/servala/core/forms.py b/src/servala/core/forms.py index 1fece41..d7740ae 100644 --- a/src/servala/core/forms.py +++ b/src/servala/core/forms.py @@ -1,7 +1,7 @@ from django import forms from django.utils.translation import gettext_lazy as _ -from servala.core.models import ControlPlane, ServiceDefinition +from servala.core.models import ControlPlane class ControlPlaneAdminForm(forms.ModelForm): @@ -70,65 +70,3 @@ class ControlPlaneAdminForm(forms.ModelForm): def save(self, *args, **kwargs): self.instance.api_credentials = self.cleaned_data["api_credentials"] return super().save(*args, **kwargs) - - -class ServiceDefinitionAdminForm(forms.ModelForm): - api_group = forms.CharField( - required=False, - help_text=_("API Group"), - ) - api_version = forms.CharField( - required=False, - help_text=_("API Version"), - ) - api_kind = forms.CharField( - required=False, - help_text=_("API Kind"), - ) - - class Meta: - model = ServiceDefinition - fields = "__all__" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # If we have existing api_definition, populate the individual fields - if self.instance.pk and self.instance.api_definition: - api_def = self.instance.api_definition - self.fields["api_group"].initial = api_def.get("group", "") - self.fields["api_version"].initial = api_def.get("version", "") - self.fields["api_kind"].initial = api_def.get("kind", "") - - def clean(self): - cleaned_data = super().clean() - - api_group = cleaned_data.get("api_group") - api_version = cleaned_data.get("api_version") - api_kind = cleaned_data.get("api_kind") - - if api_group and api_version and api_kind: - cleaned_data["api_definition"] = { - "group": api_group, - "version": api_version, - "kind": api_kind, - } - else: - if not (api_group or api_version or api_kind): - cleaned_data["api_definition"] = {} - else: - # Some fields are filled but not all – validation will fail at model level. - api_def = {} - if api_group: - api_def["group"] = api_group - if api_version: - api_def["version"] = api_version - if api_kind: - api_def["kind"] = api_kind - cleaned_data["api_definition"] = api_def - - return cleaned_data - - def save(self, *args, **kwargs): - self.instance.api_definition = self.cleaned_data["api_definition"] - return super().save(*args, **kwargs) diff --git a/src/servala/core/migrations/0006_service_slug.py b/src/servala/core/migrations/0006_service_slug.py deleted file mode 100644 index f15e796..0000000 --- a/src/servala/core/migrations/0006_service_slug.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.2b1 on 2025-03-24 14:29 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0005_remove_controlplane_k8s_api_endpoint"), - ] - - operations = [ - migrations.AddField( - model_name="service", - name="slug", - field=models.SlugField( - default="slug", max_length=100, unique=True, verbose_name="URL slug" - ), - preserve_default=False, - ), - ] diff --git a/src/servala/core/migrations/0007_service_definition.py b/src/servala/core/migrations/0007_service_definition.py deleted file mode 100644 index be0fac6..0000000 --- a/src/servala/core/migrations/0007_service_definition.py +++ /dev/null @@ -1,115 +0,0 @@ -# Generated by Django 5.2b1 on 2025-03-25 11:02 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0006_service_slug"), - ] - - operations = [ - migrations.RemoveField( - model_name="serviceoffering", - name="control_plane", - ), - migrations.CreateModel( - name="ServiceDefinition", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=100, verbose_name="Name")), - ( - "description", - models.TextField(blank=True, verbose_name="Description"), - ), - ( - "api_definition", - models.JSONField( - blank=True, - help_text="Contains group, version, and kind information", - null=True, - verbose_name="API Definition", - ), - ), - ( - "service", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="service_definitions", - to="core.service", - verbose_name="Service", - ), - ), - ], - options={ - "verbose_name": "Service definition", - "verbose_name_plural": "Service definitions", - }, - ), - migrations.CreateModel( - name="ServiceOfferingControlPlane", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "control_plane", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="offering_connections", - to="core.controlplane", - verbose_name="Control plane", - ), - ), - ( - "service_definition", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="offering_control_planes", - to="core.servicedefinition", - verbose_name="Service definition", - ), - ), - ( - "service_offering", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="control_plane_connections", - to="core.serviceoffering", - verbose_name="Service offering", - ), - ), - ], - options={ - "verbose_name": "Service offering control plane connection", - "verbose_name_plural": "Service offering control planes connections", - "unique_together": {("service_offering", "control_plane")}, - }, - ), - migrations.AddField( - model_name="serviceoffering", - name="control_planes", - field=models.ManyToManyField( - related_name="offerings", - through="core.ServiceOfferingControlPlane", - to="core.controlplane", - verbose_name="Control planes", - ), - ), - ] diff --git a/src/servala/core/models/__init__.py b/src/servala/core/models/__init__.py index 6d5b24d..f637b57 100644 --- a/src/servala/core/models/__init__.py +++ b/src/servala/core/models/__init__.py @@ -11,9 +11,7 @@ from .service import ( Plan, Service, ServiceCategory, - ServiceDefinition, ServiceOffering, - ServiceOfferingControlPlane, ) from .user import User @@ -28,8 +26,6 @@ __all__ = [ "Plan", "Service", "ServiceCategory", - "ServiceDefinition", "ServiceOffering", - "ServiceOfferingControlPlane", "User", ] diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index e842c36..be6737e 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -38,7 +38,6 @@ class Organization(ServalaModelMixin, models.Model): class urls(urlman.Urls): base = "/org/{self.slug}/" details = "{base}details/" - services = "{base}services/" @cached_property def slug(self): diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index f1dc20b..78815da 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -44,7 +44,6 @@ class Service(models.Model): """ name = models.CharField(max_length=100, verbose_name=_("Name")) - slug = models.SlugField(max_length=100, verbose_name=_("URL slug"), unique=True) category = models.ForeignKey( to="ServiceCategory", on_delete=models.PROTECT, @@ -68,13 +67,18 @@ class Service(models.Model): return self.name -def validate_dict(data, required_fields=None, allow_empty=True): - if not data: - if allow_empty: - return - raise ValidationError(_("Data may not be empty!")) +def validate_api_credentials(value): + """ + Validates that api_credentials either contains all required fields or is empty. + """ + # If empty dict, that's valid + if not value: + return + + # Check for required fields + required_fields = ("certificate-authority-data", "server", "token") + missing_fields = required_fields - set(value) - missing_fields = required_fields - set(data) if missing_fields: raise ValidationError( _("Missing required fields in API credentials: %(fields)s"), @@ -82,11 +86,6 @@ def validate_dict(data, required_fields=None, allow_empty=True): ) -def validate_api_credentials(value): - required_fields = ("certificate-authority-data", "server", "token") - return validate_dict(value, required_fields) - - class ControlPlane(models.Model): name = models.CharField(max_length=100, verbose_name=_("Name")) description = models.TextField(blank=True, verbose_name=_("Description")) @@ -229,88 +228,6 @@ class Plan(models.Model): return self.name -def validate_api_definition(value): - required_fields = ("group", "version", "kind") - return validate_dict(value, required_fields) - - -class ServiceDefinition(models.Model): - """ - Configuration/service implementation: contains information on which - CompositeResourceDefinition (aka XRD) implements a service on a ControlPlane. - - Is required in order to query the OpenAPI spec for dynamic form generation. - """ - - name = models.CharField(max_length=100, verbose_name=_("Name")) - description = models.TextField(blank=True, verbose_name=_("Description")) - api_definition = models.JSONField( - verbose_name=_("API Definition"), - help_text=_("Contains group, version, and kind information"), - null=True, - blank=True, - ) - service = models.ForeignKey( - to="Service", - on_delete=models.CASCADE, - related_name="service_definitions", - verbose_name=_("Service"), - ) - - @property - def control_planes(self): - return ControlPlane.objects.filter( - offering_connections__service_definition=self - ).distinct() - - @property - def service_offerings(self): - return ServiceOffering.objects.filter( - control_plane_connections__service_definition=self - ).distinct() - - class Meta: - verbose_name = _("Service definition") - verbose_name_plural = _("Service definitions") - - def __str__(self): - return self.name - - -class ServiceOfferingControlPlane(models.Model): - """ - Each combination of ServiceOffering and ControlPlane can have a different - ServiceDefinition, which is here modeled as the "through" model. - """ - - service_offering = models.ForeignKey( - to="ServiceOffering", - on_delete=models.CASCADE, - related_name="control_plane_connections", - verbose_name=_("Service offering"), - ) - control_plane = models.ForeignKey( - to="ControlPlane", - on_delete=models.CASCADE, - related_name="offering_connections", - verbose_name=_("Control plane"), - ) - service_definition = models.ForeignKey( - to="ServiceDefinition", - on_delete=models.PROTECT, - related_name="offering_control_planes", - verbose_name=_("Service definition"), - ) - - class Meta: - verbose_name = _("Service offering control plane connection") - verbose_name_plural = _("Service offering control planes connections") - unique_together = [("service_offering", "control_plane")] - - def __str__(self): - return f"{self.service_offering} on {self.control_plane} with {self.service_definition}" - - class ServiceOffering(models.Model): """ A service offering, e.g. "PostgreSQL on AWS", "MinIO on GCP". @@ -328,9 +245,8 @@ class ServiceOffering(models.Model): related_name="offerings", verbose_name=_("Provider"), ) - control_planes = models.ManyToManyField( + control_plane = models.ManyToManyField( to="ControlPlane", - through="ServiceOfferingControlPlane", related_name="offerings", verbose_name=_("Control planes"), ) @@ -339,8 +255,3 @@ class ServiceOffering(models.Model): class Meta: verbose_name = _("Service offering") verbose_name_plural = _("Service offerings") - - def __str__(self): - return _("{service_name} at {provider_name}").format( - service_name=self.service.name, provider_name=self.provider.name - ) diff --git a/src/servala/frontend/forms/service.py b/src/servala/frontend/forms/service.py deleted file mode 100644 index d7ea60f..0000000 --- a/src/servala/frontend/forms/service.py +++ /dev/null @@ -1,22 +0,0 @@ -from django import forms - -from servala.core.models import CloudProvider, ServiceCategory - - -class ServiceFilterForm(forms.Form): - category = forms.ModelChoiceField( - queryset=ServiceCategory.objects.all(), required=False - ) - cloud_provider = forms.ModelChoiceField( - queryset=CloudProvider.objects.all(), required=False - ) - q = forms.CharField(required=False) - - def filter_queryset(self, queryset): - if category := self.cleaned_data.get("category"): - queryset = queryset.filter(category=category) - if cloud_provider := self.cleaned_data.get("cloud_provider"): - queryset = queryset.filter( - offerings__control_planes__cloud_provider=cloud_provider - ) - return queryset diff --git a/src/servala/frontend/templates/frontend/organizations/service_detail.html b/src/servala/frontend/templates/frontend/organizations/service_detail.html deleted file mode 100644 index c3962eb..0000000 --- a/src/servala/frontend/templates/frontend/organizations/service_detail.html +++ /dev/null @@ -1,65 +0,0 @@ -{% extends "frontend/base.html" %} -{% load i18n %} -{% load static %} -{% block html_title %} - {% block page_title %} - {{ service.name }} - {% endblock page_title %} -{% endblock html_title %} -{% block content %} -
-
-
- {% if service.logo %} - {{ service.name }} - {% endif %} -
-

{{ service.name }}

- {{ service.category }} -
-
-
-
-

{{ service.description|default:"No description available." }}

-
-
-
- {% for offering in service.offerings.all %} -
-
- {% if offering.provider.logo %} - {{ offering.provider.name }} - {% endif %} -
-

{{ offering.provider.name }}

-
-
-
- {% if offering.description %} -

{{ offering.description }}

- {% elif offering.provider.description %} -

{{ offering.provider.description }}

- {% endif %} -
- -
- {% empty %} -
-
-

{% translate "No offerings found." %}

-
-
- {% endfor %} -
-{% endblock content %} diff --git a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html b/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html deleted file mode 100644 index 1ed9a12..0000000 --- a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html +++ /dev/null @@ -1,40 +0,0 @@ -{% extends "frontend/base.html" %} -{% load i18n %} -{% load static %} -{% block html_title %} - {% block page_title %} - {{ offering }} - {% endblock page_title %} -{% endblock html_title %} -{% block content %} -
-
-
- {% if service.logo %} - {{ service.name }} - {% endif %} -
-

{{ offering }}

- {{ offering.service.category }} -
-
-
-
- {% 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 %} -
-
-
-
-{% endblock content %} diff --git a/src/servala/frontend/templates/frontend/organizations/services.html b/src/servala/frontend/templates/frontend/organizations/services.html deleted file mode 100644 index 3286952..0000000 --- a/src/servala/frontend/templates/frontend/organizations/services.html +++ /dev/null @@ -1,51 +0,0 @@ -{% extends "frontend/base.html" %} -{% load i18n static %} -{% block html_title %} - {% block page_title %} - {% translate "Services" %} - {% endblock page_title %} -{% endblock html_title %} -{% block content %} -
-
-
-
-
- {{ filter_form }} -
-
-
-
- {% for service in services %} -
-
- {% if service.logo %} - {{ service.name }} - {% endif %} -
-

{{ service.name }}

- {{ service.category }} -
-
-
- {% if service.description %}

{{ service.description }}

{% endif %} -
- -
- {% empty %} -
-
-

{% translate "No services found." %}

-
-
- {% endfor %} -
- -{% endblock content %} diff --git a/src/servala/frontend/templates/frontend/profile.html b/src/servala/frontend/templates/frontend/profile.html index db6eda4..2d6433a 100644 --- a/src/servala/frontend/templates/frontend/profile.html +++ b/src/servala/frontend/templates/frontend/profile.html @@ -73,7 +73,7 @@
-

{% translate "Profile" %}

+

{% translate "Profile" %}

@@ -106,7 +106,7 @@
-

{% translate "Account" %}

+

{% translate "Account" %}

diff --git a/src/servala/frontend/templates/includes/sidebar.html b/src/servala/frontend/templates/includes/sidebar.html index e2a98a2..13d9d5c 100644 --- a/src/servala/frontend/templates/includes/sidebar.html +++ b/src/servala/frontend/templates/includes/sidebar.html @@ -116,12 +116,6 @@ {% translate 'Details' %} - {% endif %}