From e78a63c67f0ddfda3833633eecaa6f552055d692 Mon Sep 17 00:00:00 2001
From: Tobias Brunner
Date: Mon, 7 Jul 2025 13:35:54 +0200
Subject: [PATCH 01/25] add link to support form on error message
---
src/servala/core/models/service.py | 72 +++++++++++++++++++--------
src/servala/frontend/views/mixins.py | 24 +++++++++
src/servala/frontend/views/service.py | 23 ++++++---
3 files changed, 92 insertions(+), 27 deletions(-)
diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py
index 362661b..ebc2878 100644
--- a/src/servala/core/models/service.py
+++ b/src/servala/core/models/service.py
@@ -10,6 +10,7 @@ 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 _
from encrypted_fields.fields import EncryptedJSONField
from kubernetes import client, config
@@ -19,6 +20,10 @@ from servala.core import rules as perms
from servala.core.models.mixins import ServalaModelMixin
from servala.core.validators import kubernetes_name_validator
+SUPPORT_MESSAGE_TEMPLATE = _(
+ "Need help? We're happy to help via the support form."
+)
+
class ServiceCategory(ServalaModelMixin, models.Model):
"""
@@ -615,10 +620,9 @@ class ServiceInstance(ServalaModelMixin, models.Model):
context=context,
)
except IntegrityError:
- raise ValidationError(
- _(
- "An instance with this name already exists in this organization. Please choose a different name."
- )
+ raise cls._format_error_with_support(
+ "An instance with this name already exists in this organization. Please choose a different name.",
+ organization,
)
try:
@@ -657,10 +661,16 @@ class ServiceInstance(ServalaModelMixin, models.Model):
try:
error_body = json.loads(e.body)
reason = error_body.get("message", str(e))
- raise ValidationError(_("Kubernetes API error: {}").format(reason))
+ raise cls._format_error_with_support(
+ "Kubernetes API error: {error}.", organization, error=reason
+ )
except (ValueError, TypeError):
- raise ValidationError(_("Kubernetes API error: {}").format(str(e)))
- raise ValidationError(_("Error creating instance: {}").format(str(e)))
+ raise cls._format_error_with_support(
+ "Kubernetes API error: {error}.", organization, error=str(e)
+ )
+ raise cls._format_error_with_support(
+ "Error creating instance: {error}.", organization, error=str(e)
+ )
return instance
def update_spec(self, spec_data, updated_by):
@@ -681,28 +691,27 @@ class ServiceInstance(ServalaModelMixin, models.Model):
self.save() # Updates updated_at timestamp
except ApiException as e:
if e.status == 404:
- raise ValidationError(
- _(
- "Service instance not found in Kubernetes. It may have been deleted externally."
- )
+ raise self._format_error_with_support(
+ "Service instance not found in Kubernetes. It may have been deleted externally.",
+ self.organization,
)
try:
error_body = json.loads(e.body)
reason = error_body.get("message", str(e))
- raise ValidationError(
- _("Kubernetes API error updating instance: {error}").format(
- error=reason
- )
+ raise self._format_error_with_support(
+ "Kubernetes API error updating instance: {error}.",
+ self.organization,
+ error=reason,
)
except (ValueError, TypeError):
- raise ValidationError(
- _("Kubernetes API error updating instance: {error}").format(
- error=str(e)
- )
+ raise self._format_error_with_support(
+ "Kubernetes API error updating instance: {error}.",
+ self.organization,
+ error=str(e),
)
except Exception as e:
- raise ValidationError(
- _("Error updating instance: {error}").format(error=str(e))
+ raise self._format_error_with_support(
+ "Error updating instance: {error}.", self.organization, error=str(e)
)
@transaction.atomic
@@ -854,3 +863,24 @@ class ServiceInstance(ServalaModelMixin, models.Model):
return {"error": str(e)}
except Exception as e:
return {"error": str(e)}
+
+ @classmethod
+ def _format_error_with_support(cls, message, organization, **kwargs):
+ """
+ Helper method to format error messages with support links.
+
+ Args:
+ message: The error message template (without support text)
+ organization: The organization object to get the support URL
+ **kwargs: Additional format parameters for the message
+
+ Returns:
+ A ValidationError with the formatted message including support link
+ """
+ support_url = organization.urls.support
+ # Combine the main message with the support message template
+ full_message = _("{message} {support_message}").format(
+ message=_(message).format(**kwargs),
+ support_message=SUPPORT_MESSAGE_TEMPLATE.format(support_url=support_url),
+ )
+ return ValidationError(mark_safe(full_message))
diff --git a/src/servala/frontend/views/mixins.py b/src/servala/frontend/views/mixins.py
index fd5a494..7e7b453 100644
--- a/src/servala/frontend/views/mixins.py
+++ b/src/servala/frontend/views/mixins.py
@@ -1,9 +1,14 @@
from django.utils.functional import cached_property
+from django.utils.safestring import mark_safe
+from django.utils.translation import gettext_lazy as _
from django.views.generic import UpdateView
from rules.contrib.views import AutoPermissionRequiredMixin, PermissionRequiredMixin
from servala.core.models import Organization
+# Import the support message template from the service model
+from servala.core.models.service import SUPPORT_MESSAGE_TEMPLATE
+
class HtmxViewMixin:
fragments = []
@@ -90,3 +95,22 @@ class OrganizationViewMixin(PermissionRequiredMixin):
def has_permission(self):
return self.has_organization_permission() and super().has_permission()
+
+ def format_error_with_support(self, message, **kwargs):
+ """
+ Helper method to format error messages with support links for frontend views.
+
+ Args:
+ message: The error message template (without support text)
+ **kwargs: Additional format parameters for the message
+
+ Returns:
+ A formatted message with support link
+ """
+ support_url = self.organization.urls.support
+ # Combine the main message with the support message template
+ full_message = _("{message} {support_message}").format(
+ message=_(message).format(**kwargs),
+ support_message=SUPPORT_MESSAGE_TEMPLATE.format(support_url=support_url),
+ )
+ return mark_safe(full_message)
diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py
index 2d74842..49ac683 100644
--- a/src/servala/frontend/views/service.py
+++ b/src/servala/frontend/views/service.py
@@ -144,7 +144,10 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
form = self.get_instance_form()
if not form: # Should not happen if context_object is valid, but as a safeguard
- messages.error(self.request, _("Could not initialize service form."))
+ messages.error(
+ self.request,
+ self.format_error_with_support("Could not initialize service form."),
+ )
return self.render_to_response(context)
if form.is_valid():
@@ -161,7 +164,10 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
messages.error(self.request, e.message or str(e))
except Exception as e:
messages.error(
- self.request, _("Error creating instance: {}").format(str(e))
+ self.request,
+ self.format_error_with_support(
+ "Error creating instance: {error}.", error=str(e)
+ ),
)
# If the form is not valid or if the service creation failed, we render it again
@@ -366,7 +372,10 @@ class ServiceInstanceUpdateView(
return self.form_invalid(form)
except Exception as e:
messages.error(
- self.request, _("Error updating instance: {error}").format(error=str(e))
+ self.request,
+ self.format_error_with_support(
+ "Error updating instance: {error}.", error=str(e)
+ ),
)
return self.form_invalid(form)
@@ -435,9 +444,11 @@ 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)),
+ self.format_error_with_support(
+ "An error occurred while trying to delete instance '{name}': {error}.",
+ name=self.object.name,
+ error=str(e),
+ ),
)
response = HttpResponse()
response["HX-Redirect"] = str(self.object.urls.base)
From 5fa3d32c57549b11c48bc285a09616c59150a0ce Mon Sep 17 00:00:00 2001
From: Tobias Brunner
Date: Mon, 7 Jul 2025 13:38:09 +0200
Subject: [PATCH 02/25] do not dismiss message when error message
---
src/servala/frontend/templates/includes/message.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/servala/frontend/templates/includes/message.html b/src/servala/frontend/templates/includes/message.html
index 59260d2..d5debc9 100644
--- a/src/servala/frontend/templates/includes/message.html
+++ b/src/servala/frontend/templates/includes/message.html
@@ -9,7 +9,7 @@
- {% block html_title %}Dashboard{% endblock html_title %} – Servala
+
+ {% block html_title %}
+ Dashboard
+ {% endblock html_title %}
+ – Servala
diff --git a/src/servala/frontend/templates/frontend/profile.html b/src/servala/frontend/templates/frontend/profile.html
index dc12100..b9c1799 100644
--- a/src/servala/frontend/templates/frontend/profile.html
+++ b/src/servala/frontend/templates/frontend/profile.html
@@ -116,7 +116,8 @@
{% endblocktranslate %}
-
{% translate "VSHN Account Console" %}
diff --git a/src/servala/frontend/templates/includes/message.html b/src/servala/frontend/templates/includes/message.html
index 59260d2..3125da1 100644
--- a/src/servala/frontend/templates/includes/message.html
+++ b/src/servala/frontend/templates/includes/message.html
@@ -1,28 +1,29 @@
-
-
+
{% for service in services %}
-
+
-
+
{% if service.description %}
{{ service.description|urlize }}
{% endif %}
From 5b876de18ab03090141ed4335972609902bd4fa8 Mon Sep 17 00:00:00 2001
From: Tobias Kunze
Date: Fri, 11 Jul 2025 13:20:25 +0200
Subject: [PATCH 25/25] Code style
---
src/servala/core/models/organization.py | 2 +-
.../frontend/templates/account/login.html | 29 +++++++------
.../frontend/templates/frontend/base.html | 9 +++-
.../frontend/templates/frontend/profile.html | 3 +-
.../templates/includes/k8s_error.html | 22 +++++-----
.../frontend/templates/includes/message.html | 43 ++++++++++---------
.../frontend/templatetags/error_filters.py | 1 +
src/servala/frontend/views/support.py | 2 +-
8 files changed, 59 insertions(+), 52 deletions(-)
diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py
index 57cd660..855e4f0 100644
--- a/src/servala/core/models/organization.py
+++ b/src/servala/core/models/organization.py
@@ -3,8 +3,8 @@ import urlman
from django.conf import settings
from django.db import models, transaction
from django.utils.functional import cached_property
-from django.utils.text import slugify
from django.utils.safestring import mark_safe
+from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
diff --git a/src/servala/frontend/templates/account/login.html b/src/servala/frontend/templates/account/login.html
index 233118b..58cd49b 100644
--- a/src/servala/frontend/templates/account/login.html
+++ b/src/servala/frontend/templates/account/login.html
@@ -1,21 +1,22 @@
{% extends "frontend/base.html" %}
{% load static i18n %}
{% load allauth account socialaccount %}
-
{% block html_title %}
{% translate "Sign in" %}
{% endblock html_title %}
-
{% block page_title %}
{% translate "Welcome to Servala" %}
{% endblock page_title %}
-
{% block card_header %}
-
-
@@ -96,7 +98,6 @@
-