Instance form and template improvements

This commit is contained in:
Tobias Kunze 2025-05-25 22:55:37 +02:00
parent 52aa6acfb6
commit 3e466fb011
5 changed files with 82 additions and 44 deletions

View file

@ -374,9 +374,11 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model):
@cached_property @cached_property
def kind_plural(self): def kind_plural(self):
if hasattr(self.resource_definition, 'status') and \ if (
hasattr(self.resource_definition.status, 'accepted_names') and \ hasattr(self.resource_definition, "status")
self.resource_definition.status.accepted_names: and hasattr(self.resource_definition.status, "accepted_names")
and self.resource_definition.status.accepted_names
):
return self.resource_definition.status.accepted_names.plural return self.resource_definition.status.accepted_names.plural
if self.kind.endswith("s"): if self.kind.endswith("s"):
return self.kind.lower() return self.kind.lower()
@ -648,7 +650,9 @@ class ServiceInstance(ServalaModelMixin, models.Model):
self.is_deleted = True self.is_deleted = True
self.deleted_at = timezone.now() self.deleted_at = timezone.now()
self.deleted_by = user self.deleted_by = user
self.save(update_fields=["is_deleted", "deleted_at", "deleted_by", "updated_at"]) self.save(
update_fields=["is_deleted", "deleted_at", "deleted_by", "updated_at"]
)
self._clear_kubernetes_caches() self._clear_kubernetes_caches()

View file

@ -6,6 +6,7 @@ from servala.core.models import (
ControlPlane, ControlPlane,
Service, Service,
ServiceCategory, ServiceCategory,
ServiceInstance,
ServiceOffering, ServiceOffering,
) )
@ -37,6 +38,8 @@ class ControlPlaneSelectForm(forms.Form):
def __init__(self, *args, planes=None, **kwargs): def __init__(self, *args, planes=None, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields["control_plane"].queryset = planes self.fields["control_plane"].queryset = planes
if planes and planes.count() == 1:
self.fields["control_plane"].initial = planes.first()
class ServiceInstanceFilterForm(forms.Form): class ServiceInstanceFilterForm(forms.Form):
@ -68,23 +71,23 @@ class ServiceInstanceFilterForm(forms.Form):
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
if self.is_valid(): if self.is_valid():
data = self.cleaned_data data = self.cleaned_data
if data["name"]: if data.get("name"):
queryset = queryset.filter(name__icontains=data["name"]) queryset = queryset.filter(name__icontains=data["name"])
if data["service"]: if data.get("service"):
queryset = queryset.filter( queryset = queryset.filter(
context__service_definition__service=data["service"] context__service_definition__service=data["service"]
) )
if data["provider"]: if data.get("provider"):
queryset = queryset.filter( queryset = queryset.filter(
context__service_offering__provider=data["provider"] context__service_offering__provider=data["provider"]
) )
if data["control_plane"]: if data.get("control_plane"):
queryset = queryset.filter(context__control_plane=data["control_plane"]) queryset = queryset.filter(context__control_plane=data["control_plane"])
if data["status"]: status = data.get("status")
if data["status"] == "active": if status == "active":
queryset = queryset.filter(is_deleted=False) queryset = queryset.filter(is_deleted=False)
else: elif status == "deleted":
queryset = queryset.filter(is_deleted=True) queryset = queryset.filter(is_deleted=True)
return queryset return queryset
@ -105,11 +108,13 @@ class ServiceInstanceDeleteForm(forms.ModelForm):
def clean_name(self): def clean_name(self):
entered_name = self.cleaned_data.get("name") entered_name = self.cleaned_data.get("name")
if entered_name != self.instance.name: if entered_name != self.instance.name:
raise ValidationError( raise forms.ValidationError(
_("The entered name does not match the instance name. Deletion not confirmed.") _(
"The entered name does not match the instance name. Deletion not confirmed."
)
) )
return entered_name return entered_name
class Meta: class Meta:
model = ServiceInstance model = ServiceInstance
fields = ("name", ) fields = ("name",)

View file

@ -1,11 +1,18 @@
{% load i18n %} {% load i18n %}
<form method="post" action="{{ view.request.path }}" hx-post="{{ view.request.path }}" hx-target="this" hx-swap="outerHTML"> <form method="post"
action="{{ view.request.path }}"
hx-post="{{ view.request.path }}"
hx-target="this"
hx-swap="outerHTML">
{% csrf_token %} {% csrf_token %}
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="deleteInstanceModalLabel"> <h5 class="modal-title" id="deleteInstanceModalLabel">
{% blocktranslate with instance_name=instance.name %}Confirm Deletion of {{ instance_name }}{% endblocktranslate %} {% blocktranslate with instance_name=instance.name %}Confirm Deletion of {{ instance_name }}{% endblocktranslate %}
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div> </div>
<div class="modal-body hide-form-errors"> <div class="modal-body hide-form-errors">
<p>{% translate "Do you really want to delete this service instance? This action cannot be undone." %}</p> <p>{% translate "Do you really want to delete this service instance? This action cannot be undone." %}</p>

View file

@ -1,5 +1,5 @@
{% extends "frontend/base.html" %} {% extends "frontend/base.html" %}
{% load i18n static %} {% load i18n static pprint_filters %}
{% block html_title %} {% block html_title %}
{% block page_title %} {% block page_title %}
{{ instance.name }} {{ instance.name }}
@ -11,13 +11,12 @@
<a href="{{ instance.urls.update }}" class="btn btn-primary me-1 mb-1">{% translate "Edit" %}</a> <a href="{{ instance.urls.update }}" class="btn btn-primary me-1 mb-1">{% translate "Edit" %}</a>
{% endif %} {% endif %}
{% if has_delete_permission and not instance.is_deleted %} {% if has_delete_permission and not instance.is_deleted %}
<button type="button" class="btn btn-danger me-1 mb-1" <button type="button"
class="btn btn-danger me-1 mb-1"
hx-get="{{ instance.urls.delete }}" hx-get="{{ instance.urls.delete }}"
hx-target="#deleteInstanceModalContent" hx-target="#deleteInstanceModalContent"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#deleteInstanceModal"> data-bs-target="#deleteInstanceModal">{% translate "Delete" %}</button>
{% translate "Delete" %}
</button>
{% endif %} {% endif %}
</div> </div>
{% endblock page_title_extra %} {% endblock page_title_extra %}
@ -45,7 +44,7 @@
</dd> </dd>
<dt class="col-sm-4">{% translate "Created By" %}</dt> <dt class="col-sm-4">{% translate "Created By" %}</dt>
<dd class="col-sm-8"> <dd class="col-sm-8">
{{ instance.created_by }} {{ instance.created_by|default:"-" }}
</dd> </dd>
<dt class="col-sm-4">{% translate "Created At" %}</dt> <dt class="col-sm-4">{% translate "Created At" %}</dt>
<dd class="col-sm-8"> <dd class="col-sm-8">
@ -106,9 +105,9 @@
<span class="badge text-bg-secondary">{{ condition.status }}</span> <span class="badge text-bg-secondary">{{ condition.status }}</span>
{% endif %} {% endif %}
</td> </td>
<td>{{ condition.lastTransitionTime }}</td> <td>{{ condition.lastTransitionTime|date:"SHORT_DATETIME_FORMAT" }}</td>
<td>{{ condition.reason }}</td> <td>{{ condition.reason|default:"-" }}</td>
<td>{{ condition.message }}</td> <td>{{ condition.message|truncatewords:20|default:"-" }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -137,6 +136,8 @@
data-bs-toggle="tab" data-bs-toggle="tab"
role="tab">{{ fieldset.title }}</a> role="tab">{{ fieldset.title }}</a>
</li> </li>
{% empty %}
<li class="nav-item ms-2">{% translate "No specification details available." %}</li>
{% endfor %} {% endfor %}
</ul> </ul>
<!-- Tab Content --> <!-- Tab Content -->
@ -153,7 +154,7 @@
{% if field.value|default:""|stringformat:"s"|slice:":1" == "{" or field.value|default:""|stringformat:"s"|slice:":1" == "[" %} {% if field.value|default:""|stringformat:"s"|slice:":1" == "{" or field.value|default:""|stringformat:"s"|slice:":1" == "[" %}
<pre>{{ field.value|pprint }}</pre> <pre>{{ field.value|pprint }}</pre>
{% else %} {% else %}
{{ field.value }} {{ field.value|default:"-" }}
{% endif %} {% endif %}
</dd> </dd>
{% endfor %} {% endfor %}
@ -168,13 +169,15 @@
{% if field.value|default:""|stringformat:"s"|slice:":1" == "{" or field.value|default:""|stringformat:"s"|slice:":1" == "[" %} {% if field.value|default:""|stringformat:"s"|slice:":1" == "{" or field.value|default:""|stringformat:"s"|slice:":1" == "[" %}
<pre>{{ field.value|pprint }}</pre> <pre>{{ field.value|pprint }}</pre>
{% else %} {% else %}
{{ field.value }} {{ field.value|default:"-" }}
{% endif %} {% endif %}
</dd> </dd>
{% endfor %} {% endfor %}
</dl> </dl>
{% endfor %} {% endfor %}
</div> </div>
{% empty %}
<p>{% translate "No specification details to display." %}</p>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -218,15 +221,21 @@
{% endif %} {% endif %}
</div> </div>
</section> </section>
<!-- Delete Confirmation Modal --> <!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteInstanceModal" tabindex="-1" aria-labelledby="deleteInstanceModalLabel" aria-hidden="true"> <div class="modal fade"
id="deleteInstanceModal"
tabindex="-1"
aria-labelledby="deleteInstanceModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content" id="deleteInstanceModalContent"> <div class="modal-content" id="deleteInstanceModalContent">
{# Content will be loaded here by HTMX #} {# Content will be loaded here by HTMX #}
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="deleteInstanceModalLabel">{% translate "Confirm Deletion" %}</h5> <h5 class="modal-title" id="deleteInstanceModalLabel">{% translate "Confirm Deletion" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">

View file

@ -1,9 +1,10 @@
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.http import HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, UpdateView, ListView from django.views.generic import DetailView, ListView, UpdateView
from servala.core.crd import deslugify from servala.core.crd import deslugify
from servala.core.models import ( from servala.core.models import (
@ -35,10 +36,14 @@ class ServiceListView(OrganizationViewMixin, ListView):
def get_queryset(self): def get_queryset(self):
"""Return all services.""" """Return all services."""
services = Service.objects.all().select_related("category") services = (
Service.objects.all()
.select_related("category")
.prefetch_related("offerings__provider")
)
if self.filter_form.is_valid(): if self.filter_form.is_valid():
services = self.filter_form.filter_queryset(services) services = self.filter_form.filter_queryset(services)
return services return services.distinct()
@cached_property @cached_property
def filter_form(self): def filter_form(self):
@ -93,7 +98,7 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
@cached_property @cached_property
def selected_plane(self): def selected_plane(self):
if self.select_form.data and self.select_form.is_valid(): if self.select_form.is_valid() and self.select_form.cleaned_data:
return self.select_form.cleaned_data["control_plane"] return self.select_form.cleaned_data["control_plane"]
field = self.select_form.fields["control_plane"] field = self.select_form.fields["control_plane"]
return field.initial or field.queryset.first() return field.initial or field.queryset.first()
@ -111,7 +116,7 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
).first() ).first()
def get_instance_form(self): def get_instance_form(self):
if not self.context_object: if not self.context_object or not self.context_object.model_form_class:
return None return None
return self.context_object.model_form_class( return self.context_object.model_form_class(
data=self.request.POST if self.request.method == "POST" else None, data=self.request.POST if self.request.method == "POST" else None,
@ -208,7 +213,11 @@ class ServiceInstanceDetailView(
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
if not self.object.is_deleted and self.object.kubernetes_object and self.object.spec: if (
not self.object.is_deleted
and self.object.kubernetes_object
and self.object.spec
):
context["spec_fieldsets"] = self.get_nested_spec() context["spec_fieldsets"] = self.get_nested_spec()
context["has_change_permission"] = self.request.user.has_perm( context["has_change_permission"] = self.request.user.has_perm(
ServiceInstance.get_perm("change"), self.object ServiceInstance.get_perm("change"), self.object
@ -383,11 +392,15 @@ class ServiceInstanceListView(OrganizationViewMixin, ListView):
) )
if self.filter_form.is_valid(): if self.filter_form.is_valid():
queryset = self.filter_form.filter_queryset(queryset) queryset = self.filter_form.filter_queryset(queryset)
status_filter = self.filter_form.cleaned_data.get("status") if self.filter_form.is_valid() else "active" status_filter = (
self.filter_form.cleaned_data.get("status")
if self.filter_form.is_valid()
else "active"
)
if status_filter == "active": if status_filter == "active":
queryset = queryset.filter(is_deleted=False) queryset = queryset.filter(is_deleted=False)
elif status_filter == "deleted": elif status_filter == "deleted":
queryset = queryset.filter(is_deleted=True) queryset = queryset.filter(is_deleted=True)
return queryset.order_by("-created_at") return queryset.order_by("-created_at")
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -419,9 +432,9 @@ class ServiceInstanceDeleteView(
except Exception as e: except Exception as e:
messages.error( messages.error(
self.request, self.request,
_("An error occurred while trying to delete instance '{name}': {error}").format( _(
name=self.object.name, error=str(e) "An error occurred while trying to delete instance '{name}': {error}"
), ).format(name=self.object.name, error=str(e)),
) )
response = HttpResponse() response = HttpResponse()
response["HX-Redirect"] = str(self.object.urls.base) response["HX-Redirect"] = str(self.object.urls.base)