From e78a63c67f0ddfda3833633eecaa6f552055d692 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 7 Jul 2025 13:35:54 +0200 Subject: [PATCH 01/20] 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/20] 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/k8s_error.html b/src/servala/frontend/templates/includes/k8s_error.html index f97474d..5707dcc 100644 --- a/src/servala/frontend/templates/includes/k8s_error.html +++ b/src/servala/frontend/templates/includes/k8s_error.html @@ -1,14 +1,12 @@ {% if show_error %} -
- {% if has_list %} - {% if message %}{{ message }}{% endif %} -
    - {% for error in errors %} -
  • {{ error }}
  • - {% endfor %} -
- {% else %} - {{ message }} - {% endif %} -
+
+ {% if has_list %} + {% if message %}{{ message }}{% endif %} +
    + {% for error in errors %}
  • {{ error }}
  • {% endfor %} +
+ {% else %} + {{ message }} + {% endif %} +
{% endif %} diff --git a/src/servala/frontend/templates/includes/message.html b/src/servala/frontend/templates/includes/message.html index d5debc9..278c009 100644 --- a/src/servala/frontend/templates/includes/message.html +++ b/src/servala/frontend/templates/includes/message.html @@ -1,28 +1,29 @@ -
+
{{ message }}
- \ No newline at end of file + document.addEventListener('DOMContentLoaded', function() { + const alert = document.getElementById('auto-dismiss-alert-{{ forloop.counter0|default:' + 0 ' }}'); + if (alert && !alert.classList.contains('alert-danger')) { + setTimeout(function() { + let opacity = 1; + const fadeOutInterval = setInterval(function() { + if (opacity > 0.05) { + opacity -= 0.05; + alert.style.opacity = opacity; + } else { + clearInterval(fadeOutInterval); + const bsAlert = new bootstrap.Alert(alert); + bsAlert.close(); + } + }, 25); + }, 5000); + } + }); + diff --git a/src/servala/frontend/templatetags/error_filters.py b/src/servala/frontend/templatetags/error_filters.py index 18c4c4d..410d49a 100644 --- a/src/servala/frontend/templatetags/error_filters.py +++ b/src/servala/frontend/templatetags/error_filters.py @@ -3,6 +3,7 @@ Template filters for safe error formatting. """ import html + from django import template from django.utils.safestring import mark_safe diff --git a/src/servala/frontend/views/support.py b/src/servala/frontend/views/support.py index 9181c1e..6f4c4aa 100644 --- a/src/servala/frontend/views/support.py +++ b/src/servala/frontend/views/support.py @@ -52,7 +52,7 @@ class SupportView(OrganizationViewMixin, FormView): mark_safe( _( "There was an error submitting your support request. " - "Please try again or contact us directly at servala-support@vshn.ch." + 'Please try again or contact us directly at servala-support@vshn.ch.' ) ), )