diff --git a/pyproject.toml b/pyproject.toml index b89d892..d881deb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,6 @@ dependencies = [ "cryptography>=45.0.7", "django==5.2.6", "django-allauth>=65.10.0", - "django-auditlog>=3.2.1", "django-fernet-encrypted-fields>=0.3.0", "django-jsonform>=2.23.2", "django-scopes>=2.0.0", diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index 149635c..ed07574 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -272,8 +272,8 @@ class ControlPlaneCRDAdmin(admin.ModelAdmin): @admin.register(ServiceInstance) class ServiceInstanceAdmin(admin.ModelAdmin): - list_display = ("name", "organization", "context", "created_by") - list_filter = ("organization", "context") + list_display = ("name", "organization", "context", "created_by", "is_deleted") + list_filter = ("organization", "context", "is_deleted") search_fields = ( "name", "organization__name", @@ -296,6 +296,9 @@ class ServiceInstanceAdmin(admin.ModelAdmin): "organization", "context", "created_by", + "is_deleted", + "deleted_at", + "deleted_by", ) }, ), diff --git a/src/servala/core/migrations/0006_remove_service_instance_soft_delete.py b/src/servala/core/migrations/0006_remove_service_instance_soft_delete.py deleted file mode 100644 index 0fcc02a..0000000 --- a/src/servala/core/migrations/0006_remove_service_instance_soft_delete.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 5.2.4 on 2025-09-03 23:08 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0005_organization_sale_order_fields"), - ] - - operations = [ - migrations.RemoveField( - model_name="serviceinstance", - name="deleted_at", - ), - migrations.RemoveField( - model_name="serviceinstance", - name="deleted_by", - ), - migrations.RemoveField( - model_name="serviceinstance", - name="is_deleted", - ), - ] diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 6944750..a06db4e 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -6,11 +6,11 @@ import re import kubernetes import rules import urlman -from auditlog.registry import auditlog from django.conf import settings from django.core.cache import cache from django.core.exceptions import ValidationError from django.db import IntegrityError, models, transaction +from django.utils import timezone from django.utils.functional import cached_property from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ @@ -571,6 +571,16 @@ class ServiceInstance(ServalaModelMixin, models.Model): on_delete=models.PROTECT, ) + is_deleted = models.BooleanField(default=False) + deleted_at = models.DateTimeField(null=True, blank=True) + deleted_by = models.ForeignKey( + to="core.User", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="+", + ) + class Meta: verbose_name = _("Service instance") verbose_name_plural = _("Service instances") @@ -784,10 +794,14 @@ class ServiceInstance(ServalaModelMixin, models.Model): raise ValidationError(self.organization.add_support_message(message)) @transaction.atomic - def delete(self, using=None, keep_parents=False, user=None): + def delete_instance(self, user): """ - Deletes the Django instance and the corresponding Kubernetes custom resource. + Soft deletes the instance in Django and initiates deletion of the + corresponding Kubernetes custom resource. """ + if self.is_deleted: + return + if ( self.spec.get("parameters", {}) .get("security", {}) @@ -811,13 +825,19 @@ class ServiceInstance(ServalaModelMixin, models.Model): if e.status != 404: # 404 is fine, the object was deleted already. raise - + 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._clear_kubernetes_caches() - return super().delete(using=using, keep_parents=keep_parents) @cached_property def kubernetes_object(self): """Fetch the Kubernetes custom resource object""" + if self.is_deleted: + return try: api_instance = client.CustomObjectsApi( self.context.control_plane.get_kubernetes_client() @@ -922,6 +942,3 @@ class ServiceInstance(ServalaModelMixin, models.Model): return {"error": str(e)} except Exception as e: return {"error": str(e)} - - -auditlog.register(ServiceInstance, exclude_fields=["updated_at"], serialize_data=True) diff --git a/src/servala/frontend/forms/service.py b/src/servala/frontend/forms/service.py index 5dd78a7..ca84ab3 100644 --- a/src/servala/frontend/forms/service.py +++ b/src/servala/frontend/forms/service.py @@ -64,6 +64,14 @@ class ServiceInstanceFilterForm(forms.Form): required=False, label=_("Service Provider Zone"), ) + status = forms.ChoiceField( + choices=( + ("active", _("Active")), + ("deleted", _("Deleted")), + ), + required=False, + label=_("Status"), + ) def filter_queryset(self, queryset): if self.is_valid(): @@ -80,6 +88,11 @@ class ServiceInstanceFilterForm(forms.Form): ) if data.get("control_plane"): queryset = queryset.filter(context__control_plane=data["control_plane"]) + status = data.get("status") + if status == "active": + queryset = queryset.filter(is_deleted=False) + elif status == "deleted": + queryset = queryset.filter(is_deleted=True) return queryset diff --git a/src/servala/frontend/templates/frontend/organizations/dashboard.html b/src/servala/frontend/templates/frontend/organizations/dashboard.html index 01faa69..b0dd273 100644 --- a/src/servala/frontend/templates/frontend/organizations/dashboard.html +++ b/src/servala/frontend/templates/frontend/organizations/dashboard.html @@ -112,6 +112,13 @@ {{ instance.context.service_offering.service.name }} + + {% if instance.is_deleted %} + {% translate "Deleted" %} + {% else %} + {% translate "Active" %} + {% endif %} + {{ instance.created_at|date:"M d, Y" }} diff --git a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html index a12efcf..4c8ebf1 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html @@ -7,10 +7,10 @@ {% endblock html_title %} {% block page_title_extra %}
- {% if has_change_permission %} + {% if has_change_permission and not instance.is_deleted %} {% translate "Edit" %} {% endif %} - {% if has_delete_permission %} + {% if has_delete_permission and not instance.is_deleted %}
- {% if instance.status_conditions %} + {% if not instance.is_deleted and instance.status_conditions %}
@@ -103,7 +118,7 @@
{% endif %} - {% if instance.spec and spec_fieldsets %} + {% if not instance.is_deleted and instance.spec and spec_fieldsets %}
diff --git a/src/servala/frontend/templates/frontend/organizations/service_instances.html b/src/servala/frontend/templates/frontend/organizations/service_instances.html index c443e6e..e5c7c0e 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instances.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instances.html @@ -27,6 +27,7 @@ {% translate "Service Provider" %} {% translate "Service Provider Zone" %} {% translate "Created At" %} + {% translate "Status" %} @@ -39,10 +40,17 @@ {{ instance.context.service_offering.provider.name }} {{ instance.context.control_plane.name }} {{ instance.created_at|date:"SHORT_DATETIME_FORMAT" }} + + {% if instance.is_deleted %} + {% translate "Deleted" %} + {% else %} + {% translate "Active" %} + {% endif %} + {% empty %} - {% translate "No service instances found." %} + {% translate "No service instances found." %} {% endfor %} diff --git a/src/servala/frontend/views/generic.py b/src/servala/frontend/views/generic.py index 170b159..16b1ec2 100644 --- a/src/servala/frontend/views/generic.py +++ b/src/servala/frontend/views/generic.py @@ -1,6 +1,6 @@ from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin -from django.db.models import Count +from django.db.models import Count, Q from django.shortcuts import redirect, render from django.urls import reverse_lazy from django.utils.functional import cached_property @@ -32,7 +32,9 @@ class OrganizationSelectionView(LoginRequiredMixin, TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["user_organizations"] = self.user_organizations.annotate( - instance_count=Count("service_instances") + instance_count=Count( + "service_instances", filter=Q(service_instances__is_deleted=False) + ) ) return context diff --git a/src/servala/frontend/views/organization.py b/src/servala/frontend/views/organization.py index 2f35f76..6545d39 100644 --- a/src/servala/frontend/views/organization.py +++ b/src/servala/frontend/views/organization.py @@ -70,7 +70,9 @@ class OrganizationDashboardView( context = super().get_context_data(**kwargs) organization = self.get_object() - service_instances = ServiceInstance.objects.filter(organization=organization) + service_instances = ServiceInstance.objects.filter( + organization=organization, is_deleted=False + ) recent_instances = service_instances.order_by("-created_at")[:5] for instance in recent_instances: diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index 201a510..f9ce50d 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -198,7 +198,11 @@ class ServiceInstanceMixin: def get_object(self, **kwargs): instance = super().get_object(**kwargs) - if not instance.kubernetes_object and not self._has_warned: + if ( + not instance.is_deleted + and not instance.kubernetes_object + and not self._has_warned + ): messages.warning( self.request, _( @@ -217,7 +221,11 @@ class ServiceInstanceDetailView( def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - if 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 @@ -398,6 +406,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" + ) + if status_filter == "active": + queryset = queryset.filter(is_deleted=False) + elif status_filter == "deleted": + queryset = queryset.filter(is_deleted=True) return queryset.order_by("-created_at") def get_context_data(self, **kwargs): @@ -416,7 +433,7 @@ class ServiceInstanceDeleteView( def form_valid(self, form): try: - self.object.delete(user=self.request.user) + self.object.delete_instance(user=self.request.user) messages.success( self.request, _("Service instance '{name}' has been scheduled for deletion.").format( diff --git a/src/servala/settings.py b/src/servala/settings.py index 81cdc9c..63f7895 100644 --- a/src/servala/settings.py +++ b/src/servala/settings.py @@ -157,7 +157,6 @@ INSTALLED_APPS = [ "allauth.account", "allauth.socialaccount", "allauth.socialaccount.providers.openid_connect", - "auditlog", "servala.core", ] @@ -171,7 +170,6 @@ MIDDLEWARE = [ "django.middleware.clickjacking.XFrameOptionsMiddleware", "allauth.account.middleware.AccountMiddleware", "django.contrib.auth.middleware.LoginRequiredMiddleware", - "auditlog.middleware.AuditlogMiddleware", "servala.core.middleware.OrganizationMiddleware", ] LOGIN_URL = "account_login" diff --git a/uv.lock b/uv.lock index 86fd355..675f389 100644 --- a/uv.lock +++ b/uv.lock @@ -376,19 +376,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/ac/82/e6f607b0bad524d227f6e5aaffdb5e2b286f6ab1b4b3151134ae2303c2d6/django_allauth-65.11.1.tar.gz", hash = "sha256:e95d5234cccaf92273d315e1393cc4626cb88a19d66a1bf0e81f89f7958cfa06", size = 1915592, upload-time = "2025-08-27T18:05:05.581Z" } -[[package]] -name = "django-auditlog" -version = "3.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "django" }, - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/46/9da1d94493832fa18d2f6324a76d387fa232001593866987a96047709f4e/django_auditlog-3.2.1.tar.gz", hash = "sha256:63a4c9f7793e94eed804bc31a04d9b0b58244b1d280e2ed273c8b406bff1f779", size = 72926, upload-time = "2025-07-03T20:08:17.734Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/06/67296d050a72dcd76f57f220df621cb27e5b9282ba7ad0f5f74870dce241/django_auditlog-3.2.1-py3-none-any.whl", hash = "sha256:99603ca9d015f7e9b062b1c34f3e0826a3ce6ae6e5950c81bb7e663f7802a899", size = 38330, upload-time = "2025-07-03T20:07:51.735Z" }, -] - [[package]] name = "django-fernet-encrypted-fields" version = "0.3.0" @@ -1142,7 +1129,6 @@ dependencies = [ { name = "cryptography" }, { name = "django" }, { name = "django-allauth" }, - { name = "django-auditlog" }, { name = "django-fernet-encrypted-fields" }, { name = "django-jsonform" }, { name = "django-scopes" }, @@ -1180,7 +1166,6 @@ requires-dist = [ { name = "cryptography", specifier = ">=45.0.7" }, { name = "django", specifier = "==5.2.6" }, { name = "django-allauth", specifier = ">=65.10.0" }, - { name = "django-auditlog", specifier = ">=3.2.1" }, { name = "django-fernet-encrypted-fields", specifier = ">=0.3.0" }, { name = "django-jsonform", specifier = ">=2.23.2" }, { name = "django-scopes", specifier = ">=2.0.0" },