parent
842b66a84d
commit
8b1e0f74bb
4 changed files with 201 additions and 0 deletions
|
|
@ -9,6 +9,7 @@ from servala.core.models import (
|
|||
ControlPlane,
|
||||
ControlPlaneCRD,
|
||||
Organization,
|
||||
OrganizationInvitation,
|
||||
OrganizationMembership,
|
||||
OrganizationOrigin,
|
||||
Service,
|
||||
|
|
@ -105,6 +106,28 @@ class OrganizationMembershipAdmin(admin.ModelAdmin):
|
|||
date_hierarchy = "date_joined"
|
||||
|
||||
|
||||
@admin.register(OrganizationInvitation)
|
||||
class OrganizationInvitationAdmin(admin.ModelAdmin):
|
||||
list_display = ("email", "organization", "role", "is_accepted", "created_at")
|
||||
list_filter = ("role", "created_at", "accepted_at", "organization")
|
||||
search_fields = ("email", "organization__name")
|
||||
autocomplete_fields = ("organization", "accepted_by")
|
||||
readonly_fields = (
|
||||
"secret",
|
||||
"accepted_by",
|
||||
"accepted_at",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
date_hierarchy = "created_at"
|
||||
|
||||
def is_accepted(self, obj):
|
||||
return obj.is_accepted
|
||||
|
||||
is_accepted.boolean = True
|
||||
is_accepted.short_description = _("Accepted")
|
||||
|
||||
|
||||
@admin.register(ServiceCategory)
|
||||
class ServiceCategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "parent")
|
||||
|
|
|
|||
107
src/servala/core/migrations/0011_organizationinvitation.py
Normal file
107
src/servala/core/migrations/0011_organizationinvitation.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# Generated by Django 5.2.7 on 2025-10-17 00:58
|
||||
|
||||
import django.db.models.deletion
|
||||
import rules.contrib.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("core", "0010_organizationorigin_billing_entity"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="OrganizationInvitation",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="Created"),
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
models.DateTimeField(auto_now=True, verbose_name="Last updated"),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(max_length=254, verbose_name="Email address"),
|
||||
),
|
||||
(
|
||||
"role",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("member", "Member"),
|
||||
("admin", "Administrator"),
|
||||
("owner", "Owner"),
|
||||
],
|
||||
default="member",
|
||||
max_length=20,
|
||||
verbose_name="Role",
|
||||
),
|
||||
),
|
||||
(
|
||||
"secret",
|
||||
models.CharField(
|
||||
editable=False,
|
||||
max_length=64,
|
||||
unique=True,
|
||||
verbose_name="Secret token",
|
||||
),
|
||||
),
|
||||
(
|
||||
"accepted_at",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="Accepted at"
|
||||
),
|
||||
),
|
||||
(
|
||||
"accepted_by",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="accepted_invitations",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Accepted by",
|
||||
),
|
||||
),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="created_invitations",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Created by",
|
||||
),
|
||||
),
|
||||
(
|
||||
"organization",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="invitations",
|
||||
to="core.organization",
|
||||
verbose_name="Organization",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Organization invitation",
|
||||
"verbose_name_plural": "Organization invitations",
|
||||
"unique_together": {("organization", "email")},
|
||||
},
|
||||
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
from .organization import (
|
||||
BillingEntity,
|
||||
Organization,
|
||||
OrganizationInvitation,
|
||||
OrganizationMembership,
|
||||
OrganizationOrigin,
|
||||
OrganizationRole,
|
||||
|
|
@ -23,6 +24,7 @@ __all__ = [
|
|||
"ControlPlane",
|
||||
"ControlPlaneCRD",
|
||||
"Organization",
|
||||
"OrganizationInvitation",
|
||||
"OrganizationMembership",
|
||||
"OrganizationOrigin",
|
||||
"OrganizationRole",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import secrets
|
||||
|
||||
import rules
|
||||
import urlman
|
||||
from django.conf import settings
|
||||
|
|
@ -417,3 +419,70 @@ class OrganizationMembership(ServalaModelMixin, models.Model):
|
|||
|
||||
def __str__(self):
|
||||
return f"{self.user} in {self.organization} as {self.role}"
|
||||
|
||||
|
||||
class OrganizationInvitation(ServalaModelMixin, models.Model):
|
||||
organization = models.ForeignKey(
|
||||
to=Organization,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="invitations",
|
||||
verbose_name=_("Organization"),
|
||||
)
|
||||
email = models.EmailField(verbose_name=_("Email address"))
|
||||
role = models.CharField(
|
||||
max_length=20,
|
||||
choices=OrganizationRole.choices,
|
||||
default=OrganizationRole.MEMBER,
|
||||
verbose_name=_("Role"),
|
||||
)
|
||||
secret = models.CharField(
|
||||
max_length=64,
|
||||
unique=True,
|
||||
editable=False,
|
||||
verbose_name=_("Secret token"),
|
||||
)
|
||||
created_by = models.ForeignKey(
|
||||
to="core.User",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="created_invitations",
|
||||
verbose_name=_("Created by"),
|
||||
)
|
||||
accepted_by = models.ForeignKey(
|
||||
to="core.User",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="accepted_invitations",
|
||||
verbose_name=_("Accepted by"),
|
||||
)
|
||||
accepted_at = models.DateTimeField(
|
||||
null=True, blank=True, verbose_name=_("Accepted at")
|
||||
)
|
||||
|
||||
class urls(urlman.Urls):
|
||||
accept = "/invitations/{self.secret}/accept/"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Organization invitation")
|
||||
verbose_name_plural = _("Organization invitations")
|
||||
unique_together = [["organization", "email"]]
|
||||
|
||||
def __str__(self):
|
||||
return f"Invitation for {self.email} to {self.organization}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.secret:
|
||||
self.secret = secrets.token_urlsafe(48)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def is_accepted(self):
|
||||
# We check both accepted_by and accepted_at to avoid a deleted user
|
||||
# freeing up an invitation
|
||||
return self.accepted_by or self.accepted_at
|
||||
|
||||
@property
|
||||
def can_be_accepted(self):
|
||||
return not self.is_accepted
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue