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
def kind_plural(self):
if hasattr(self.resource_definition, 'status') and \
hasattr(self.resource_definition.status, 'accepted_names') and \
self.resource_definition.status.accepted_names:
if (
hasattr(self.resource_definition, "status")
and hasattr(self.resource_definition.status, "accepted_names")
and self.resource_definition.status.accepted_names
):
return self.resource_definition.status.accepted_names.plural
if self.kind.endswith("s"):
return self.kind.lower()
@ -648,7 +650,9 @@ class ServiceInstance(ServalaModelMixin, models.Model):
self.is_deleted = True
self.deleted_at = timezone.now()
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()

View file

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

View file

@ -1,11 +1,18 @@
{% 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 %}
<div class="modal-header">
<h5 class="modal-title" id="deleteInstanceModalLabel">
{% blocktranslate with instance_name=instance.name %}Confirm Deletion of {{ instance_name }}{% endblocktranslate %}
</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 class="modal-body hide-form-errors">
<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" %}
{% load i18n static %}
{% load i18n static pprint_filters %}
{% block html_title %}
{% block page_title %}
{{ instance.name }}
@ -11,13 +11,12 @@
<a href="{{ instance.urls.update }}" class="btn btn-primary me-1 mb-1">{% translate "Edit" %}</a>
{% endif %}
{% 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-target="#deleteInstanceModalContent"
data-bs-toggle="modal"
data-bs-target="#deleteInstanceModal">
{% translate "Delete" %}
</button>
data-bs-target="#deleteInstanceModal">{% translate "Delete" %}</button>
{% endif %}
</div>
{% endblock page_title_extra %}
@ -45,7 +44,7 @@
</dd>
<dt class="col-sm-4">{% translate "Created By" %}</dt>
<dd class="col-sm-8">
{{ instance.created_by }}
{{ instance.created_by|default:"-" }}
</dd>
<dt class="col-sm-4">{% translate "Created At" %}</dt>
<dd class="col-sm-8">
@ -106,9 +105,9 @@
<span class="badge text-bg-secondary">{{ condition.status }}</span>
{% endif %}
</td>
<td>{{ condition.lastTransitionTime }}</td>
<td>{{ condition.reason }}</td>
<td>{{ condition.message }}</td>
<td>{{ condition.lastTransitionTime|date:"SHORT_DATETIME_FORMAT" }}</td>
<td>{{ condition.reason|default:"-" }}</td>
<td>{{ condition.message|truncatewords:20|default:"-" }}</td>
</tr>
{% endfor %}
</tbody>
@ -137,6 +136,8 @@
data-bs-toggle="tab"
role="tab">{{ fieldset.title }}</a>
</li>
{% empty %}
<li class="nav-item ms-2">{% translate "No specification details available." %}</li>
{% endfor %}
</ul>
<!-- Tab Content -->
@ -153,7 +154,7 @@
{% if field.value|default:""|stringformat:"s"|slice:":1" == "{" or field.value|default:""|stringformat:"s"|slice:":1" == "[" %}
<pre>{{ field.value|pprint }}</pre>
{% else %}
{{ field.value }}
{{ field.value|default:"-" }}
{% endif %}
</dd>
{% endfor %}
@ -168,13 +169,15 @@
{% if field.value|default:""|stringformat:"s"|slice:":1" == "{" or field.value|default:""|stringformat:"s"|slice:":1" == "[" %}
<pre>{{ field.value|pprint }}</pre>
{% else %}
{{ field.value }}
{{ field.value|default:"-" }}
{% endif %}
</dd>
{% endfor %}
</dl>
{% endfor %}
</div>
{% empty %}
<p>{% translate "No specification details to display." %}</p>
{% endfor %}
</div>
</div>
@ -218,15 +221,21 @@
{% endif %}
</div>
</section>
<!-- 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-content" id="deleteInstanceModalContent">
{# Content will be loaded here by HTMX #}
<div class="modal-header">
<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 class="modal-body">
<div class="d-flex justify-content-center">

View file

@ -1,9 +1,10 @@
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.http import HttpResponse
from django.shortcuts import redirect
from django.utils.functional import cached_property
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.models import (
@ -35,10 +36,14 @@ class ServiceListView(OrganizationViewMixin, ListView):
def get_queryset(self):
"""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():
services = self.filter_form.filter_queryset(services)
return services
return services.distinct()
@cached_property
def filter_form(self):
@ -93,7 +98,7 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
@cached_property
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"]
field = self.select_form.fields["control_plane"]
return field.initial or field.queryset.first()
@ -111,7 +116,7 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
).first()
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 self.context_object.model_form_class(
data=self.request.POST if self.request.method == "POST" else None,
@ -208,7 +213,11 @@ class ServiceInstanceDetailView(
def get_context_data(self, **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["has_change_permission"] = self.request.user.has_perm(
ServiceInstance.get_perm("change"), self.object
@ -383,11 +392,15 @@ class ServiceInstanceListView(OrganizationViewMixin, ListView):
)
if self.filter_form.is_valid():
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":
queryset = queryset.filter(is_deleted=False)
queryset = queryset.filter(is_deleted=False)
elif status_filter == "deleted":
queryset = queryset.filter(is_deleted=True)
queryset = queryset.filter(is_deleted=True)
return queryset.order_by("-created_at")
def get_context_data(self, **kwargs):
@ -419,9 +432,9 @@ class ServiceInstanceDeleteView(
except Exception as e:
messages.error(
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["HX-Redirect"] = str(self.object.urls.base)