Compare commits

..

No commits in common. "090827bbbf2ed21c611ae26cc9c20be17e27fb6e" and "714cd9be5452adf635b4e602c19c83b07e63074b" have entirely different histories.

11 changed files with 152 additions and 142 deletions

View file

@ -63,7 +63,6 @@ class OrganizationAdmin(admin.ModelAdmin):
search_fields = ("name", "namespace")
autocomplete_fields = ("billing_entity", "origin")
inlines = (OrganizationMembershipInline,)
filter_horizontal = ("limit_osb_services",)
def get_readonly_fields(self, request, obj=None):
readonly_fields = list(super().get_readonly_fields(request, obj) or [])
@ -83,10 +82,9 @@ class BillingEntityAdmin(admin.ModelAdmin):
@admin.register(OrganizationOrigin)
class OrganizationOriginAdmin(admin.ModelAdmin):
list_display = ("name", "billing_entity", "default_odoo_sale_order_id")
list_display = ("name", "billing_entity")
search_fields = ("name",)
autocomplete_fields = ("billing_entity",)
filter_horizontal = ("limit_cloudproviders",)
@admin.register(OrganizationMembership)

View file

@ -0,0 +1,33 @@
# Generated by Django 5.2.7 on 2025-10-16 22:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0008_organization_osb_guid_service_osb_service_id_and_more"),
]
operations = [
migrations.AddField(
model_name="organization",
name="limit_cloudproviders",
field=models.ManyToManyField(
blank=True,
related_name="+",
to="core.cloudprovider",
verbose_name="Limit to these Cloud providers",
),
),
migrations.AddField(
model_name="organization",
name="limit_osb_services",
field=models.ManyToManyField(
blank=True,
related_name="+",
to="core.service",
verbose_name="Services activated from OSB",
),
),
]

View file

@ -0,0 +1,26 @@
# Generated by Django 5.2.7 on 2025-10-17 00:22
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0009_organization_limit_cloudproviders_and_more"),
]
operations = [
migrations.AddField(
model_name="organizationorigin",
name="billing_entity",
field=models.ForeignKey(
help_text="If set, this billing entity will be used on new organizations with this origin.",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="origins",
to="core.billingentity",
verbose_name="Billing entity",
),
),
]

View file

@ -1,4 +1,4 @@
# Generated by Django 5.2.7 on 2025-10-22 09:38
# Generated by Django 5.2.7 on 2025-10-17 00:58
import django.db.models.deletion
import rules.contrib.models
@ -9,88 +9,10 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0008_organization_osb_guid_service_osb_service_id_and_more"),
("core", "0010_organizationorigin_billing_entity"),
]
operations = [
migrations.AddField(
model_name="controlplane",
name="wildcard_dns",
field=models.CharField(
blank=True,
help_text="Wildcard DNS domain for auto-generating FQDNs (e.g., apps.exoscale-ch-gva-2-prod2.services.servala.com)",
max_length=255,
null=True,
verbose_name="Wildcard DNS",
),
),
migrations.AddField(
model_name="organization",
name="limit_osb_services",
field=models.ManyToManyField(
blank=True,
related_name="+",
to="core.service",
verbose_name="Services activated from OSB",
),
),
migrations.AddField(
model_name="organizationorigin",
name="billing_entity",
field=models.ForeignKey(
help_text="If set, this billing entity will be used on new organizations with this origin.",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="origins",
to="core.billingentity",
verbose_name="Billing entity",
),
),
migrations.AddField(
model_name="organizationorigin",
name="default_odoo_sale_order_id",
field=models.IntegerField(
blank=True,
help_text="If set, this sale order will be used for new organizations with this origin.",
null=True,
verbose_name="Default Odoo Sale Order ID",
),
),
migrations.AddField(
model_name="organizationorigin",
name="limit_cloudproviders",
field=models.ManyToManyField(
blank=True,
help_text="If set, all organizations with this origin will be limited to these cloud providers.",
related_name="+",
to="core.cloudprovider",
verbose_name="Limit to these Cloud providers",
),
),
migrations.AddField(
model_name="servicedefinition",
name="advanced_fields",
field=models.JSONField(
blank=True,
default=list,
help_text=(
"Array of field names that should be hidden behind an 'Advanced' toggle."
"Use dot notation (e.g., ['spec.parameters.monitoring.enabled', 'spec.parameters.backup.schedule'])"
),
null=True,
verbose_name="Advanced fields",
),
),
migrations.AddField(
model_name="serviceoffering",
name="external_links",
field=models.JSONField(
blank=True,
help_text='JSON array of link objects: {"url": "", "title": ""}. ',
null=True,
verbose_name="External links",
),
),
migrations.CreateModel(
name="OrganizationInvitation",
fields=[

View file

@ -0,0 +1,23 @@
# Generated by Django 5.2.7 on 2025-10-17 02:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0011_organizationinvitation"),
]
operations = [
migrations.AddField(
model_name="serviceoffering",
name="external_links",
field=models.JSONField(
blank=True,
help_text='JSON array of link objects: {"url": "", "title": ""}. ',
null=True,
verbose_name="External links",
),
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 5.2.7 on 2025-10-17 02:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0012_serviceoffering_external_links"),
]
operations = [
migrations.AddField(
model_name="controlplane",
name="wildcard_dns",
field=models.CharField(
blank=True,
help_text="Wildcard DNS domain for auto-generating FQDNs (e.g., apps.exoscale-ch-gva-2-prod2.services.servala.com)",
max_length=255,
null=True,
verbose_name="Wildcard DNS",
),
),
]

View file

@ -0,0 +1,27 @@
# Generated by Django 5.2.7 on 2025-10-17 03:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0013_controlplane_wildcard_dns"),
]
operations = [
migrations.AddField(
model_name="servicedefinition",
name="advanced_fields",
field=models.JSONField(
blank=True,
default=list,
help_text=(
"Array of field names that should be hidden behind an 'Advanced' toggle. "
"Use dot notation (e.g., ['spec.parameters.monitoring.enabled', 'spec.parameters.backup.schedule'])"
),
null=True,
verbose_name="Advanced fields",
),
),
]

View file

@ -52,6 +52,12 @@ class Organization(ServalaModelMixin, models.Model):
related_name="organizations",
verbose_name=_("Members"),
)
limit_cloudproviders = models.ManyToManyField(
to="CloudProvider",
related_name="+",
verbose_name=_("Limit to these Cloud providers"),
blank=True,
)
limit_osb_services = models.ManyToManyField(
to="Service",
related_name="+",
@ -93,14 +99,6 @@ class Organization(ServalaModelMixin, models.Model):
def has_inherited_billing_entity(self):
return self.origin and self.billing_entity == self.origin.billing_entity
@property
def limit_cloudproviders(self):
if self.origin:
return self.origin.limit_cloudproviders.all()
from servala.core.models import CloudProvider
return CloudProvider.objects.none()
def set_owner(self, user):
with scopes_disabled():
OrganizationMembership.objects.filter(user=user, organization=self).delete()
@ -129,20 +127,7 @@ class Organization(ServalaModelMixin, models.Model):
if owner:
instance.set_owner(owner)
if instance.origin and instance.origin.default_odoo_sale_order_id:
sale_order_id = instance.origin.default_odoo_sale_order_id
sale_order_data = CLIENT.search_read(
model="sale.order",
domain=[["id", "=", sale_order_id]],
fields=["name"],
limit=1,
)
instance.odoo_sale_order_id = sale_order_id
if sale_order_data:
instance.odoo_sale_order_name = sale_order_data[0]["name"]
instance.save(update_fields=["odoo_sale_order_id", "odoo_sale_order_name"])
elif (
if (
instance.billing_entity.odoo_company_id
and instance.billing_entity.odoo_invoice_id
):
@ -176,8 +161,9 @@ class Organization(ServalaModelMixin, models.Model):
if self.limit_osb_services.exists():
queryset = self.limit_osb_services.all()
if self.limit_cloudproviders.exists():
allowed_providers = self.limit_cloudproviders.all()
queryset = queryset.filter(
offerings__provider__in=self.limit_cloudproviders
offerings__provider__in=allowed_providers
).distinct()
return queryset.prefetch_related(
"offerings", "offerings__provider"
@ -191,8 +177,9 @@ class Organization(ServalaModelMixin, models.Model):
queryset = Service.objects.select_related("category")
if self.limit_cloudproviders.exists():
allowed_providers = self.limit_cloudproviders.all()
queryset = queryset.filter(
offerings__provider__in=self.limit_cloudproviders
offerings__provider__in=allowed_providers
).distinct()
queryset = queryset.exclude(id__in=self.limit_osb_services.all())
return queryset.prefetch_related("offerings", "offerings__provider")
@ -389,23 +376,6 @@ class OrganizationOrigin(ServalaModelMixin, models.Model):
),
null=True,
)
limit_cloudproviders = models.ManyToManyField(
to="CloudProvider",
related_name="+",
verbose_name=_("Limit to these Cloud providers"),
blank=True,
help_text=_(
"If set, all organizations with this origin will be limited to these cloud providers."
),
)
default_odoo_sale_order_id = models.IntegerField(
null=True,
blank=True,
verbose_name=_("Default Odoo Sale Order ID"),
help_text=_(
"If set, this sale order will be used for new organizations with this origin."
),
)
class Meta:
verbose_name = _("Organization origin")

View file

@ -21,15 +21,6 @@ class ServiceFilterForm(forms.Form):
)
q = forms.CharField(label=_("Search"), required=False)
def __init__(self, *args, organization=None, **kwargs):
super().__init__(*args, **kwargs)
if organization and organization.limit_cloudproviders.exists():
allowed_providers = organization.limit_cloudproviders
if allowed_providers.count() <= 1:
self.fields.pop("cloud_provider", None)
else:
self.fields["cloud_provider"].queryset = allowed_providers
def filter_queryset(self, queryset):
if category := self.cleaned_data.get("category"):
queryset = queryset.filter(category=category)

View file

@ -20,7 +20,6 @@
{% for service in services %}
<div class="col-12 col-md-6 col-lg-3">{% include "includes/service_card.html" %}</div>
{% empty %}
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="card-content">
@ -28,7 +27,6 @@
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% if deactivated_services %}

View file

@ -44,9 +44,7 @@ class ServiceListView(OrganizationViewMixin, ListView):
@cached_property
def filter_form(self):
return ServiceFilterForm(
data=self.request.GET or None, organization=self.request.organization
)
return ServiceFilterForm(data=self.request.GET or None)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)