Compare commits

..

No commits in common. "db9f15856a89a501bfa70e5133eec71216a4cbe0" and "acf14a186396c0b5f2432587f6ce45b7ab93fb2a" have entirely different histories.

7 changed files with 33 additions and 43 deletions

View file

@ -65,7 +65,8 @@ class OrganizationAdmin(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
readonly_fields = list(super().get_readonly_fields(request, obj) or [])
readonly_fields.append("namespace") # Always read-only
if obj: # If this is an edit (not a new organization)
readonly_fields.append("namespace")
return readonly_fields

View file

@ -1,24 +0,0 @@
# Generated by Django 5.2b1 on 2025-04-09 14:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0002_alter_controlplanecrd_options_and_more"),
]
operations = [
migrations.AlterField(
model_name="organization",
name="namespace",
field=models.CharField(
help_text="This namespace will be used for all Kubernetes resources.",
max_length=20,
null=True,
unique=True,
verbose_name="Kubernetes Namespace",
),
),
]

View file

@ -9,19 +9,19 @@ from django_scopes import ScopedManager, scopes_disabled
from servala.core import rules as perms
from servala.core.models.mixins import ServalaModelMixin
from servala.core.validators import kubernetes_name_validator
class Organization(ServalaModelMixin, models.Model):
name = models.CharField(max_length=100, verbose_name=_("Name"))
# The namespace is generated as "org-{id}" in accordance with RFC 1035 Label Names.
# It is nullable as we need to write to the database in order to read the ID, but should
# not be null in practical use.
namespace = models.CharField(
max_length=20,
max_length=63,
verbose_name=_("Kubernetes Namespace"),
null=True,
unique=True,
help_text=_("This namespace will be used for all Kubernetes resources."),
help_text=_(
"This namespace will be used for all Kubernetes resources. Cannot be changed after creation."
),
validators=[kubernetes_name_validator],
)
billing_entity = models.ForeignKey(
@ -90,13 +90,6 @@ class Organization(ServalaModelMixin, models.Model):
def __str__(self):
return self.name
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if not self.namespace:
# Set namespace after initial save to ensure we have an ID
self.namespace = f"org-{self.pk}"
self.save(update_fields=["namespace"])
class BillingEntity(ServalaModelMixin, models.Model):
"""

View file

@ -1,4 +1,6 @@
from django import forms
from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from servala.core.models import Organization
from servala.frontend.forms.mixins import HtmxMixin
@ -7,4 +9,19 @@ from servala.frontend.forms.mixins import HtmxMixin
class OrganizationForm(HtmxMixin, ModelForm):
class Meta:
model = Organization
fields = ("name",)
fields = ("name", "namespace")
widgets = {
"namespace": forms.TextInput(
attrs={
"pattern": "[a-z0-9]([-a-z0-9]*[a-z0-9])?",
"title": _(
'Lowercase alphanumeric characters or "-", must start and end with alphanumeric'
),
}
)
}
help_texts = {
"namespace": _(
"This namespace will be used for all resources and cannot be changed later."
)
}

View file

@ -52,7 +52,7 @@
</th>
<td>
<div>{{ form.instance.namespace }}</div>
<small class="text-muted">{% translate "System-generated namespace for Kubernetes resources." %}</small>
<small class="text-muted">{% translate "The namespace cannot be changed after creation." %}</small>
</td>
</tr>
</tbody>

View file

@ -15,12 +15,16 @@ def origin():
@pytest.fixture
def organization(origin):
return Organization.objects.create(name="Test Org", origin=origin)
return Organization.objects.create(
name="Test Org", namespace="test-org", origin=origin
)
@pytest.fixture
def other_organization(origin):
return Organization.objects.create(name="Test Org Alternate", origin=origin)
return Organization.objects.create(
name="Test Org Alternate", namespace="test-org-alternate", origin=origin
)
@pytest.fixture

View file

@ -23,7 +23,6 @@ def test_root_view_invalid_urls_404(client):
def test_owner_can_access_dashboard(client, org_owner, organization):
client.force_login(org_owner)
response = client.get(organization.urls.base)
assert organization.namespace == f"org-{organization.pk}"
assert response.status_code == 200
assert organization.name in response.content.decode()