Compare commits

...
Sign in to create a new pull request.

24 commits

Author SHA1 Message Date
Renovate Bot
83a18837ec Update https://github.com/renovatebot/github-action action to v43.0.5 2025-07-28 03:01:03 +00:00
b403370b9d Merge pull request 'Update dependency sentry-sdk to >=2.33.0' (#155) from renovate/sentry-sdk-2.x into main
All checks were successful
Build and Deploy Staging / build (push) Successful in 1m24s
Tests / test (push) Successful in 25s
Build and Deploy Staging / deploy (push) Successful in 9s
Reviewed-on: #155
2025-07-17 12:39:33 +00:00
Renovate Bot
934b47aadf Update dependency sentry-sdk to >=2.33.0
All checks were successful
Tests / test (push) Successful in 27s
2025-07-16 03:01:48 +00:00
56c258ee34 Merge pull request 'Update https://github.com/renovatebot/github-action action to v43.0.3' (#153) from renovate/https-github.com-renovatebot-github-action-43.x into main
Reviewed-on: #153
2025-07-14 08:27:02 +00:00
Renovate Bot
05fadd851b Update https://github.com/renovatebot/github-action action to v43.0.3
All checks were successful
Tests / test (push) Successful in 24s
2025-07-14 07:55:06 +00:00
3956a34ba0
automerge updates to workflows 2025-07-14 09:53:10 +02:00
8992ec51ff Merge pull request 'Lock file maintenance' (#154) from renovate/lock-file-maintenance into main
All checks were successful
Build and Deploy Staging / build (push) Successful in 1m6s
Tests / test (push) Successful in 26s
Build and Deploy Staging / deploy (push) Successful in 8s
Reviewed-on: #154
2025-07-14 06:58:53 +00:00
Renovate Bot
6691b775f2 Lock file maintenance
All checks were successful
Tests / test (push) Successful in 25s
2025-07-14 03:01:18 +00:00
683977a001 Merge pull request 'Try to fix permissions issue' (#151) from 148-instance-edit-permissions into main
All checks were successful
Build and Deploy Staging / build (push) Successful in 1m6s
Tests / test (push) Successful in 24s
Build and Deploy Staging / deploy (push) Successful in 22s
Reviewed-on: #151
2025-07-11 14:55:47 +00:00
da2a1f6c64 Merge pull request 'Include link to support in error message' (#149) from error-with-help into main
Some checks failed
Build and Deploy Staging / build (push) Successful in 1m4s
Tests / test (push) Successful in 39s
Build and Deploy Staging / deploy (push) Has been cancelled
Reviewed-on: #149
2025-07-11 14:54:01 +00:00
0bd895c486 Make rules compatible with instance checks
All checks were successful
Tests / test (push) Successful in 26s
2025-07-11 16:37:45 +02:00
b5d691e407
remove unused def
All checks were successful
Tests / test (push) Successful in 24s
2025-07-11 14:45:27 +02:00
8a45a22759
sanitize kubernetes messages
All checks were successful
Tests / test (push) Successful in 28s
2025-07-11 14:43:26 +02:00
5feabda513 Make sure admin is visible to staff users
All checks were successful
Tests / test (push) Successful in 25s
2025-07-11 12:25:20 +02:00
3f8901aa93 Try to fix permissions issue 2025-07-11 12:24:26 +02:00
a54b1b1108 Merge pull request 'Update dependency django-allauth to >=65.10.0' (#150) from renovate/django-allauth-65.x into main
All checks were successful
Build and Deploy Staging / build (push) Successful in 1m25s
Tests / test (push) Successful in 25s
Build and Deploy Staging / deploy (push) Successful in 8s
Reviewed-on: #150
2025-07-11 09:50:44 +00:00
Renovate Bot
fdb07b89b9 Update dependency django-allauth to >=65.10.0
All checks were successful
Tests / test (push) Successful in 27s
2025-07-11 03:01:10 +00:00
9c3ce54bb3
format and retext control plane errors
All checks were successful
Tests / test (push) Successful in 28s
2025-07-10 16:32:41 +02:00
9317547630
restore translated strings 2025-07-10 16:18:40 +02:00
3c270d9c12
refactor into a shared function
All checks were successful
Tests / test (push) Successful in 27s
2025-07-10 16:11:30 +02:00
24cb249e9e Merge pull request 'refactor odoo company type handling' (#143) from odoo-company-check into main
All checks were successful
Build and Deploy Staging / build (push) Successful in 1m1s
Build and Deploy Antora Docs / build (push) Successful in 59s
Tests / test (push) Successful in 24s
Build and Deploy Staging / deploy (push) Successful in 10s
Build and Deploy Antora Docs / deploy (push) Successful in 6s
Reviewed-on: #143
Reviewed-by: Tobias Kunze <r@rixx.de>
2025-07-07 12:11:29 +00:00
5fa3d32c57
do not dismiss message when error message
All checks were successful
Tests / test (push) Successful in 25s
2025-07-07 13:38:09 +02:00
e78a63c67f
add link to support form on error message 2025-07-07 13:35:54 +02:00
76bc37e3f0
refactor odoo company type handling
All checks were successful
Tests / test (push) Successful in 27s
2025-07-03 16:43:19 +02:00
14 changed files with 242 additions and 74 deletions

View file

@ -19,7 +19,7 @@ jobs:
node-version: "22" node-version: "22"
- name: Renovate - name: Renovate
uses: https://github.com/renovatebot/github-action@v43.0.2 uses: https://github.com/renovatebot/github-action@v43.0.5
with: with:
token: ${{ secrets.RENOVATE_TOKEN }} token: ${{ secrets.RENOVATE_TOKEN }}
env: env:

View file

@ -17,9 +17,9 @@ Search is done this way:
When choosing to add a new billing address, two new records are created in the Odoo `res.partner` model: When choosing to add a new billing address, two new records are created in the Odoo `res.partner` model:
* A record with the field `company_type = company` * A record with the field `is_company = False`
* A record with the following field configuration: * A record with the following field configuration:
** `company_type = person` ** `is_company = False`
** `type = invoice` ** `type = invoice`
** `parent_id = company_id` ** `parent_id = company_id`

View file

@ -8,7 +8,7 @@ dependencies = [
"argon2-cffi>=25.1.0", "argon2-cffi>=25.1.0",
"cryptography>=45.0.5", "cryptography>=45.0.5",
"django==5.2.4", "django==5.2.4",
"django-allauth>=65.9.0", "django-allauth>=65.10.0",
"django-fernet-encrypted-fields>=0.3.0", "django-fernet-encrypted-fields>=0.3.0",
"django-scopes>=2.0.0", "django-scopes>=2.0.0",
"django-storages[s3]>=1.14.6", "django-storages[s3]>=1.14.6",
@ -20,7 +20,7 @@ dependencies = [
"pyjwt>=2.10.1", "pyjwt>=2.10.1",
"requests>=2.32.4", "requests>=2.32.4",
"rules>=3.5", "rules>=3.5",
"sentry-sdk[django]>=2.32.0", "sentry-sdk[django]>=2.33.0",
"urlman>=2.0.2", "urlman>=2.0.2",
] ]

View file

@ -11,7 +11,8 @@
"matchFileNames": [ "matchFileNames": [
".forgejo/workflows/*.yml", ".forgejo/workflows/*.yml",
".forgejo/workflows/*.yaml" ".forgejo/workflows/*.yaml"
] ],
"automerge": true
}, },
{ {
"matchManagers": [ "matchManagers": [

View file

@ -4,6 +4,7 @@ from django.conf import settings
from django.db import models, transaction from django.db import models, transaction
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled from django_scopes import ScopedManager, scopes_disabled
@ -74,6 +75,14 @@ class Organization(ServalaModelMixin, models.Model):
user=user, organization=self, role=OrganizationRole.OWNER user=user, organization=self, role=OrganizationRole.OWNER
) )
def add_support_message(self, message):
support_message = _(
"Need help? We're happy to help via the <a href='{support_url}'>support form</a>."
).format(support_url=self.urls.support)
return mark_safe(
f'{message} <i class="bi bi-person-raised-hand"></i> {support_message}'
)
@classmethod @classmethod
@transaction.atomic @transaction.atomic
def create_organization(cls, instance, owner): def create_organization(cls, instance, owner):
@ -147,8 +156,8 @@ class BillingEntity(ServalaModelMixin, models.Model):
) )
# Odoo IDs are nullable for creation, should never be null in practice # Odoo IDs are nullable for creation, should never be null in practice
# The company ID points at a record of type res.partner with company_type=company # The company ID points at a record of type res.partner with is_company=True
# The invoice ID points at a record of type res.partner with company_type=person, # The invoice ID points at a record of type res.partner with is_company=False,
# type=invoice, parent_id=company_id (the invoice address). # type=invoice, parent_id=company_id (the invoice address).
odoo_company_id = models.IntegerField(null=True) odoo_company_id = models.IntegerField(null=True)
odoo_invoice_id = models.IntegerField(null=True) odoo_invoice_id = models.IntegerField(null=True)
@ -166,8 +175,8 @@ class BillingEntity(ServalaModelMixin, models.Model):
""" """
Creates a BillingEntity and corresponding Odoo records. Creates a BillingEntity and corresponding Odoo records.
This method creates a `res.partner` record in Odoo with `company_type='company'` This method creates a `res.partner` record in Odoo with `is_company=True`
for the main company, and another `res.partner` record with `company_type='person'` for the main company, and another `res.partner` record with `is_company=False`
and `type='invoice'` (linked via `parent_id` to the first record) for the and `type='invoice'` (linked via `parent_id` to the first record) for the
invoice address. The IDs of these Odoo records are stored in the BillingEntity. invoice address. The IDs of these Odoo records are stored in the BillingEntity.
@ -187,14 +196,14 @@ class BillingEntity(ServalaModelMixin, models.Model):
instance = cls.objects.create(name=name) instance = cls.objects.create(name=name)
company_payload = { company_payload = {
"name": odoo_data.get("company_name", name), "name": odoo_data.get("company_name", name),
"company_type": "company", "is_company": True,
} }
company_id = CLIENT.execute("res.partner", "create", [company_payload]) company_id = CLIENT.execute("res.partner", "create", [company_payload])
instance.odoo_company_id = company_id instance.odoo_company_id = company_id
invoice_address_payload = { invoice_address_payload = {
"name": name, "name": name,
"company_type": "person", "is_company": False,
"type": "invoice", "type": "invoice",
"parent_id": company_id, "parent_id": company_id,
} }
@ -249,10 +258,10 @@ class BillingEntity(ServalaModelMixin, models.Model):
"invoice_address": None, "invoice_address": None,
} }
company_fields = ["name", "company_type"] company_fields = ["name", "is_company"]
invoice_address_fields = [ invoice_address_fields = [
"name", "name",
"company_type", "is_company",
"type", "type",
"parent_id", "parent_id",
"street", "street",

View file

@ -1,5 +1,7 @@
import copy import copy
import html
import json import json
import re
import kubernetes import kubernetes
import rules import rules
@ -10,6 +12,7 @@ from django.core.exceptions import ValidationError
from django.db import IntegrityError, models, transaction from django.db import IntegrityError, models, transaction
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from encrypted_fields.fields import EncryptedJSONField from encrypted_fields.fields import EncryptedJSONField
from kubernetes import client, config from kubernetes import client, config
@ -571,7 +574,7 @@ class ServiceInstance(ServalaModelMixin, models.Model):
unique_together = [("name", "organization", "context")] unique_together = [("name", "organization", "context")]
rules_permissions = { rules_permissions = {
"view": rules.is_staff | perms.is_organization_member, "view": rules.is_staff | perms.is_organization_member,
"change": rules.is_staff | perms.is_organization_member, "change": rules.is_staff | perms.is_organization_admin,
"delete": rules.is_staff | perms.is_organization_admin, "delete": rules.is_staff | perms.is_organization_admin,
"add": rules.is_authenticated, "add": rules.is_authenticated,
} }
@ -603,6 +606,58 @@ class ServiceInstance(ServalaModelMixin, models.Model):
spec_data = prune_empty_data(spec_data) spec_data = prune_empty_data(spec_data)
return spec_data return spec_data
@classmethod
def _format_kubernetes_error(cls, error_message):
if not error_message:
return {"message": "", "errors": None, "has_list": False}
error_message = str(error_message).strip()
# Pattern to match validation errors in brackets like [error1, error2, error3]
pattern = r"\[([^\]]+)\]"
match = re.search(pattern, error_message)
if not match:
return {"message": error_message, "errors": None, "has_list": False}
errors_text = match.group(1).strip()
if "," not in errors_text:
return {"message": error_message, "errors": None, "has_list": False}
errors = [error.strip().strip("\"'") for error in errors_text.split(",")]
errors = [error for error in errors if error]
if len(errors) <= 1:
return {"message": error_message, "errors": None, "has_list": False}
base_message = re.sub(pattern, "", error_message).strip()
base_message = base_message.rstrip(":").strip()
return {"message": base_message, "errors": errors, "has_list": True}
@classmethod
def _safe_format_error(cls, error_data):
if not isinstance(error_data, dict):
return html.escape(str(error_data))
if not error_data.get("has_list", False):
return html.escape(error_data.get("message", ""))
message = html.escape(error_data.get("message", ""))
errors = error_data.get("errors", [])
if not errors:
return message
escaped_errors = [html.escape(str(error)) for error in errors]
error_items = "".join(f"<li>{error}</li>" for error in escaped_errors)
if message:
return mark_safe(f"{message}<ul>{error_items}</ul>")
else:
return mark_safe(f"<ul>{error_items}</ul>")
@classmethod @classmethod
def create_instance(cls, name, organization, context, created_by, spec_data): def create_instance(cls, name, organization, context, created_by, spec_data):
# Ensure the namespace exists # Ensure the namespace exists
@ -615,11 +670,10 @@ class ServiceInstance(ServalaModelMixin, models.Model):
context=context, context=context,
) )
except IntegrityError: except IntegrityError:
raise ValidationError( message = _(
_( "An instance with this name already exists in this organization. Please choose a different name."
"An instance with this name already exists in this organization. Please choose a different name."
)
) )
raise ValidationError(organization.add_support_message(message))
try: try:
spec_data = cls._prepare_spec_data(spec_data) spec_data = cls._prepare_spec_data(spec_data)
@ -657,10 +711,25 @@ class ServiceInstance(ServalaModelMixin, models.Model):
try: try:
error_body = json.loads(e.body) error_body = json.loads(e.body)
reason = error_body.get("message", str(e)) reason = error_body.get("message", str(e))
raise ValidationError(_("Kubernetes API error: {}").format(reason)) error_data = cls._format_kubernetes_error(reason)
formatted_reason = cls._safe_format_error(error_data)
message = _("Error reported by control plane: {reason}").format(
reason=formatted_reason
)
raise ValidationError(organization.add_support_message(message))
except (ValueError, TypeError): except (ValueError, TypeError):
raise ValidationError(_("Kubernetes API error: {}").format(str(e))) error_data = cls._format_kubernetes_error(str(e))
raise ValidationError(_("Error creating instance: {}").format(str(e))) formatted_error = cls._safe_format_error(error_data)
message = _("Error reported by control plane: {error}").format(
error=formatted_error
)
raise ValidationError(organization.add_support_message(message))
error_data = cls._format_kubernetes_error(str(e))
formatted_error = cls._safe_format_error(error_data)
message = _("Error creating instance: {error}").format(
error=formatted_error
)
raise ValidationError(organization.add_support_message(message))
return instance return instance
def update_spec(self, spec_data, updated_by): def update_spec(self, spec_data, updated_by):
@ -681,29 +750,33 @@ class ServiceInstance(ServalaModelMixin, models.Model):
self.save() # Updates updated_at timestamp self.save() # Updates updated_at timestamp
except ApiException as e: except ApiException as e:
if e.status == 404: if e.status == 404:
raise ValidationError( message = _(
_( "Service instance not found in control plane. It may have been deleted externally."
"Service instance not found in Kubernetes. It may have been deleted externally."
)
) )
raise ValidationError(self.organization.add_support_message(message))
try: try:
error_body = json.loads(e.body) error_body = json.loads(e.body)
reason = error_body.get("message", str(e)) reason = error_body.get("message", str(e))
raise ValidationError( error_data = self._format_kubernetes_error(reason)
_("Kubernetes API error updating instance: {error}").format( formatted_reason = self._safe_format_error(error_data)
error=reason message = _(
) "Error reported by control plane while updating instance: {reason}"
) ).format(reason=formatted_reason)
raise ValidationError(self.organization.add_support_message(message))
except (ValueError, TypeError): except (ValueError, TypeError):
raise ValidationError( error_data = self._format_kubernetes_error(str(e))
_("Kubernetes API error updating instance: {error}").format( formatted_error = self._safe_format_error(error_data)
error=str(e) message = _(
) "Error reported by control plane while updating instance: {error}"
) ).format(error=formatted_error)
raise ValidationError(self.organization.add_support_message(message))
except Exception as e: except Exception as e:
raise ValidationError( error_data = self._format_kubernetes_error(str(e))
_("Error updating instance: {error}").format(error=str(e)) formatted_error = self._safe_format_error(error_data)
message = _("Error updating instance: {error}").format(
error=formatted_error
) )
raise ValidationError(self.organization.add_support_message(message))
@transaction.atomic @transaction.atomic
def delete_instance(self, user): def delete_instance(self, user):

View file

@ -84,7 +84,7 @@ class User(ServalaModelMixin, PermissionsMixin, AbstractBaseUser):
result = odoo.CLIENT.search_read( result = odoo.CLIENT.search_read(
model="res.partner", model="res.partner",
domain=[ domain=[
("company_type", "=", "person"), ("is_company", "=", False),
("type", "=", "contact"), ("type", "=", "contact"),
("email", "ilike", self.email), ("email", "ilike", self.email),
("parent_id", "=", organization.billing_entity.odoo_company_id), ("parent_id", "=", organization.billing_entity.odoo_company_id),
@ -107,7 +107,7 @@ class User(ServalaModelMixin, PermissionsMixin, AbstractBaseUser):
partner_data = { partner_data = {
"name": f"{self.first_name} {self.last_name}".strip() or self.email, "name": f"{self.first_name} {self.last_name}".strip() or self.email,
"email": self.email, "email": self.email,
"company_type": "person", "is_company": False,
"type": "contact", "type": "contact",
"parent_id": organization.billing_entity.odoo_company_id, "parent_id": organization.billing_entity.odoo_company_id,
} }

View file

@ -15,7 +15,7 @@ ADDRESS_FIELDS = [
"email", "email",
"phone", "phone",
"vat", "vat",
"company_type", "is_company",
"type", "type",
"parent_id", "parent_id",
] ]
@ -118,8 +118,8 @@ def get_odoo_countries():
def get_odoo_access_conditions(user): def get_odoo_access_conditions(user):
# Were building our conditions in order: # We're building our conditions in order:
# - in exceptions, users may be using a billing accounts email # - in exceptions, users may be using a billing account's email
# - if the user is an admin or owner of a Servala organization # - if the user is an admin or owner of a Servala organization
# - if the user is associated with an odoo user, return all billing # - if the user is associated with an odoo user, return all billing
# addresses / organizations created by the user # addresses / organizations created by the user
@ -153,7 +153,7 @@ def get_odoo_access_conditions(user):
odoo_contacts = CLIENT.search_read( odoo_contacts = CLIENT.search_read(
model="res.partner", model="res.partner",
domain=[ domain=[
("company_type", "=", "person"), ("is_company", "=", False),
("type", "=", "contact"), ("type", "=", "contact"),
("email", "ilike", email), ("email", "ilike", email),
], ],
@ -186,7 +186,7 @@ def get_invoice_addresses(user):
or_conditions = get_odoo_access_conditions(user) or_conditions = get_odoo_access_conditions(user)
domain = [ domain = [
("company_type", "=", "person"), ("is_company", "=", False),
("type", "=", "invoice"), ("type", "=", "invoice"),
] + or_conditions ] + or_conditions

View file

@ -13,15 +13,30 @@ def has_organization_role(user, org, roles):
@rules.predicate @rules.predicate
def is_organization_owner(user, org): def is_organization_owner(user, obj):
if hasattr(obj, "organization"):
org = obj.organization
else:
org = obj
return has_organization_role(user, org, ["owner"]) return has_organization_role(user, org, ["owner"])
@rules.predicate @rules.predicate
def is_organization_admin(user, org): def is_organization_admin(user, obj):
if hasattr(obj, "organization"):
org = obj.organization
else:
org = obj
return has_organization_role(user, org, ["owner", "admin"]) return has_organization_role(user, org, ["owner", "admin"])
@rules.predicate @rules.predicate
def is_organization_member(user, org): def is_organization_member(user, obj):
if hasattr(obj, "organization"):
org = obj.organization
else:
org = obj
return has_organization_role(user, org, None) return has_organization_role(user, org, None)
rules.add_perm("core", rules.is_staff)

View file

@ -0,0 +1,14 @@
{% if show_error %}
<div class="{{ css_class }}">
{% if has_list %}
{% if message %}{{ message }}{% endif %}
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% else %}
{{ message }}
{% endif %}
</div>
{% endif %}

View file

@ -9,7 +9,7 @@
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const alert = document.getElementById('auto-dismiss-alert-{{ forloop.counter0|default:'0' }}'); const alert = document.getElementById('auto-dismiss-alert-{{ forloop.counter0|default:'0' }}');
if (alert) { if (alert && !alert.classList.contains('alert-danger')) {
setTimeout(function() { setTimeout(function() {
let opacity = 1; let opacity = 1;
const fadeOutInterval = setInterval(function() { const fadeOutInterval = setInterval(function() {

View file

@ -0,0 +1,43 @@
"""
Template filters for safe error formatting.
"""
import html
from django import template
from django.utils.safestring import mark_safe
register = template.Library()
@register.filter
def format_k8s_error(error_data):
"""
Template filter to safely format Kubernetes error data.
Usage: {{ error_data|format_k8s_error }}
Args:
error_data: Dictionary with structure from _format_kubernetes_error method
or a simple string
Returns:
Safely formatted HTML string
"""
if not error_data:
return ""
if not error_data.get("has_list", False):
return html.escape(error_data.get("message", ""))
message = html.escape(error_data.get("message", ""))
errors = error_data.get("errors", [])
if not errors:
return message
escaped_errors = [html.escape(str(error)) for error in errors]
error_items = "".join(f"<li>{error}</li>" for error in escaped_errors)
if message:
return mark_safe(f"{message}<ul>{error_items}</ul>")
else:
return mark_safe(f"<ul>{error_items}</ul>")

View file

@ -144,7 +144,12 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
form = self.get_instance_form() form = self.get_instance_form()
if not form: # Should not happen if context_object is valid, but as a safeguard 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.organization.add_support_message(
_("Could not initialize service form.")
),
)
return self.render_to_response(context) return self.render_to_response(context)
if form.is_valid(): if form.is_valid():
@ -161,7 +166,10 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
messages.error(self.request, e.message or str(e)) messages.error(self.request, e.message or str(e))
except Exception as e: except Exception as e:
messages.error( messages.error(
self.request, _("Error creating instance: {}").format(str(e)) self.request,
self.organization.add_support_message(
_(f"Error creating instance: {str(e)}.")
),
) )
# If the form is not valid or if the service creation failed, we render it again # If the form is not valid or if the service creation failed, we render it again
@ -366,7 +374,10 @@ class ServiceInstanceUpdateView(
return self.form_invalid(form) return self.form_invalid(form)
except Exception as e: except Exception as e:
messages.error( messages.error(
self.request, _("Error updating instance: {error}").format(error=str(e)) self.request,
self.organization.add_support_message(
_(f"Error updating instance: {str(e)}.")
),
) )
return self.form_invalid(form) return self.form_invalid(form)
@ -435,9 +446,11 @@ class ServiceInstanceDeleteView(
except Exception as e: except Exception as e:
messages.error( messages.error(
self.request, self.request,
_( self.organization.add_support_message(
"An error occurred while trying to delete instance '{name}': {error}" _(
).format(name=self.object.name, error=str(e)), f"An error occurred while trying to delete instance '{self.object.name}': {str(e)}."
)
),
) )
response = HttpResponse() response = HttpResponse()
response["HX-Redirect"] = str(self.object.urls.base) response["HX-Redirect"] = str(self.object.urls.base)

38
uv.lock generated
View file

@ -37,11 +37,11 @@ wheels = [
[[package]] [[package]]
name = "asgiref" name = "asgiref"
version = "3.9.0" version = "3.9.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6a/68/fb4fb78c9eac59d5e819108a57664737f855c5a8e9b76aec1738bb137f9e/asgiref-3.9.0.tar.gz", hash = "sha256:3dd2556d0f08c4fab8a010d9ab05ef8c34565f6bf32381d17505f7ca5b273767", size = 36772, upload-time = "2025-07-03T13:25:01.491Z" } sdist = { url = "https://files.pythonhosted.org/packages/90/61/0aa957eec22ff70b830b22ff91f825e70e1ef732c06666a805730f28b36b/asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", size = 36870, upload-time = "2025-07-08T09:07:43.344Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/f9/76c9f4d4985b5a642926162e2d41fe6019b1fa929cfa58abb7d2dc9041e5/asgiref-3.9.0-py3-none-any.whl", hash = "sha256:06a41250a0114d2b6f6a2cb3ab962147d355b53d1de15eebc34a9d04a7b79981", size = 23788, upload-time = "2025-07-03T13:24:59.115Z" }, { url = "https://files.pythonhosted.org/packages/7c/3c/0464dcada90d5da0e71018c04a140ad6349558afb30b3051b4264cc5b965/asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c", size = 23790, upload-time = "2025-07-08T09:07:41.548Z" },
] ]
[[package]] [[package]]
@ -75,30 +75,30 @@ wheels = [
[[package]] [[package]]
name = "boto3" name = "boto3"
version = "1.39.3" version = "1.39.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "botocore" }, { name = "botocore" },
{ name = "jmespath" }, { name = "jmespath" },
{ name = "s3transfer" }, { name = "s3transfer" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/02/42/712a74bb86d06538c55067a35b8a82c57aa303eba95b2b1ee91c829288f4/boto3-1.39.3.tar.gz", hash = "sha256:0a367106497649ae3d8a7b571b8c3be01b7b935a0fe303d4cc2574ed03aecbb4", size = 111838, upload-time = "2025-07-03T19:26:00.988Z" } sdist = { url = "https://files.pythonhosted.org/packages/6a/1f/b7510dcd26eb14735d6f4b2904e219b825660425a0cf0b6f35b84c7249b0/boto3-1.39.4.tar.gz", hash = "sha256:6c955729a1d70181bc8368e02a7d3f350884290def63815ebca8408ee6d47571", size = 111829, upload-time = "2025-07-09T19:23:01.512Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/15/70/723d2ab259aeaed6c96e5c1857ebe7d474ed9aa8f487dea352c60f33798f/boto3-1.39.3-py3-none-any.whl", hash = "sha256:056cfa2440fe1a157a7c2be897c749c83e1a322144aa4dad889f2fca66571019", size = 139906, upload-time = "2025-07-03T19:25:58.803Z" }, { url = "https://files.pythonhosted.org/packages/12/5c/93292e4d8c809950c13950b3168e0eabdac828629c21047959251ad3f28c/boto3-1.39.4-py3-none-any.whl", hash = "sha256:f8e9534b429121aa5c5b7c685c6a94dd33edf14f87926e9a182d5b50220ba284", size = 139908, upload-time = "2025-07-09T19:22:59.808Z" },
] ]
[[package]] [[package]]
name = "botocore" name = "botocore"
version = "1.39.3" version = "1.39.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "jmespath" }, { name = "jmespath" },
{ name = "python-dateutil" }, { name = "python-dateutil" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/60/66/96e89cc261d75f0b8125436272c335c74d2a39df84504a0c3956adcd1301/botocore-1.39.3.tar.gz", hash = "sha256:da8f477e119f9f8a3aaa8b3c99d9c6856ed0a243680aa3a3fbbfc15a8d4093fb", size = 14132316, upload-time = "2025-07-03T19:25:49.502Z" } sdist = { url = "https://files.pythonhosted.org/packages/e6/9f/21c823ea2fae3fa5a6c9e8caaa1f858acd55018e6d317505a4f14c5bb999/botocore-1.39.4.tar.gz", hash = "sha256:e662ac35c681f7942a93f2ec7b4cde8f8b56dd399da47a79fa3e370338521a56", size = 14136116, upload-time = "2025-07-09T19:22:49.811Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/53/e4/3698dbb037a44d82a501577c6e3824c19f4289f4afbcadb06793866250d8/botocore-1.39.3-py3-none-any.whl", hash = "sha256:66a81cfac18ad5e9f47696c73fdf44cdbd8f8ca51ab3fca1effca0aabf61f02f", size = 13791724, upload-time = "2025-07-03T19:25:44.026Z" }, { url = "https://files.pythonhosted.org/packages/58/44/f120319e0a9afface645e99f300175b9b308e4724cb400b32e1bd6eb3060/botocore-1.39.4-py3-none-any.whl", hash = "sha256:c41e167ce01cfd1973c3fa9856ef5244a51ddf9c82cb131120d8617913b6812a", size = 13795516, upload-time = "2025-07-09T19:22:44.446Z" },
] ]
[[package]] [[package]]
@ -127,11 +127,11 @@ wheels = [
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2025.6.15" version = "2025.7.9"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } sdist = { url = "https://files.pythonhosted.org/packages/de/8a/c729b6b60c66a38f590c4e774decc4b2ec7b0576be8f1aa984a53ffa812a/certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079", size = 160386, upload-time = "2025-07-09T02:13:58.874Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, { url = "https://files.pythonhosted.org/packages/66/f3/80a3f974c8b535d394ff960a11ac20368e06b736da395b551a49ce950cce/certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39", size = 159230, upload-time = "2025-07-09T02:13:57.007Z" },
] ]
[[package]] [[package]]
@ -295,13 +295,13 @@ wheels = [
[[package]] [[package]]
name = "django-allauth" name = "django-allauth"
version = "65.9.0" version = "65.10.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "asgiref" }, { name = "asgiref" },
{ name = "django" }, { name = "django" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/9d/a3/00aa9d5bb5df4f7464495675074dc11107c08b3eea3462fb3edc059d71e1/django_allauth-65.9.0.tar.gz", hash = "sha256:a06bca9974df44321e94c33bcf770bb6f924d1a44b57defbce4d7ec54a55483e", size = 1710514, upload-time = "2025-06-01T19:21:07.771Z" } sdist = { url = "https://files.pythonhosted.org/packages/e1/9e/271e3b8ea27c089ddf3431140cf4aa86df86556ec102e360da5af62c3a99/django_allauth-65.10.0.tar.gz", hash = "sha256:47daa3b0e11a1d75724ea32995de37bd2b8963e9e4cce2b3a7fd64eb6d3b3c48", size = 1897777, upload-time = "2025-07-10T11:32:44.098Z" }
[[package]] [[package]]
name = "django-fernet-encrypted-fields" name = "django-fernet-encrypted-fields"
@ -1002,15 +1002,15 @@ wheels = [
[[package]] [[package]]
name = "sentry-sdk" name = "sentry-sdk"
version = "2.32.0" version = "2.33.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/10/59/eb90c45cb836cf8bec973bba10230ddad1c55e2b2e9ffa9d7d7368948358/sentry_sdk-2.32.0.tar.gz", hash = "sha256:9016c75d9316b0f6921ac14c8cd4fb938f26002430ac5be9945ab280f78bec6b", size = 334932, upload-time = "2025-06-27T08:10:02.89Z" } sdist = { url = "https://files.pythonhosted.org/packages/09/0b/6139f589436c278b33359845ed77019cd093c41371f898283bbc14d26c02/sentry_sdk-2.33.0.tar.gz", hash = "sha256:cdceed05e186846fdf80ceea261fe0a11ebc93aab2f228ed73d076a07804152e", size = 335233, upload-time = "2025-07-15T12:07:42.413Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/01/a1/fc4856bd02d2097324fb7ce05b3021fb850f864b83ca765f6e37e92ff8ca/sentry_sdk-2.32.0-py2.py3-none-any.whl", hash = "sha256:6cf51521b099562d7ce3606da928c473643abe99b00ce4cb5626ea735f4ec345", size = 356122, upload-time = "2025-06-27T08:10:01.424Z" }, { url = "https://files.pythonhosted.org/packages/93/e5/f24e9f81c9822a24a2627cfcb44c10a3971382e67e5015c6e068421f5787/sentry_sdk-2.33.0-py2.py3-none-any.whl", hash = "sha256:a762d3f19a1c240e16c98796f2a5023f6e58872997d5ae2147ac3ed378b23ec2", size = 356397, upload-time = "2025-07-15T12:07:40.729Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@ -1062,7 +1062,7 @@ requires-dist = [
{ name = "argon2-cffi", specifier = ">=25.1.0" }, { name = "argon2-cffi", specifier = ">=25.1.0" },
{ name = "cryptography", specifier = ">=45.0.5" }, { name = "cryptography", specifier = ">=45.0.5" },
{ name = "django", specifier = "==5.2.4" }, { name = "django", specifier = "==5.2.4" },
{ name = "django-allauth", specifier = ">=65.9.0" }, { name = "django-allauth", specifier = ">=65.10.0" },
{ name = "django-fernet-encrypted-fields", specifier = ">=0.3.0" }, { name = "django-fernet-encrypted-fields", specifier = ">=0.3.0" },
{ name = "django-scopes", specifier = ">=2.0.0" }, { name = "django-scopes", specifier = ">=2.0.0" },
{ name = "django-storages", extras = ["s3"], specifier = ">=1.14.6" }, { name = "django-storages", extras = ["s3"], specifier = ">=1.14.6" },
@ -1074,7 +1074,7 @@ requires-dist = [
{ name = "pyjwt", specifier = ">=2.10.1" }, { name = "pyjwt", specifier = ">=2.10.1" },
{ name = "requests", specifier = ">=2.32.4" }, { name = "requests", specifier = ">=2.32.4" },
{ name = "rules", specifier = ">=3.5" }, { name = "rules", specifier = ">=3.5" },
{ name = "sentry-sdk", extras = ["django"], specifier = ">=2.32.0" }, { name = "sentry-sdk", extras = ["django"], specifier = ">=2.33.0" },
{ name = "urlman", specifier = ">=2.0.2" }, { name = "urlman", specifier = ">=2.0.2" },
] ]