Skip to content
2 changes: 2 additions & 0 deletions squarelet/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
InvoiceFactory,
MembershipFactory,
OrganizationFactory,
OrganizationInvitationFactory,
OrganizationPlanFactory,
PlanFactory,
ProfessionalPlanFactory,
Expand All @@ -28,6 +29,7 @@
register(InvoiceFactory)
register(MembershipFactory)
register(OrganizationFactory)
register(OrganizationInvitationFactory)
register(OrganizationPlanFactory)
register(ProfessionalPlanFactory)
register(SubscriptionFactory)
Expand Down
62 changes: 62 additions & 0 deletions squarelet/organizations/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
Organization,
OrganizationChangeLog,
OrganizationEmailDomain,
OrganizationInvitation,
OrganizationSubtype,
OrganizationType,
OrganizationUrl,
Expand Down Expand Up @@ -208,6 +209,38 @@ def queryset(self, request, queryset):
return queryset


class OutgoingOrganizationInvitationInline(admin.TabularInline):
model = OrganizationInvitation
fk_name = "from_organization"
fields = (
"to_organization",
"relationship_type",
"request",
"accepted_at",
"rejected_at",
)
readonly_fields = ("to_organization", "accepted_at", "rejected_at")
extra = 0
verbose_name = "Outgoing Organization Invitation"
verbose_name_plural = "Outgoing Organization Invitations"


class IncomingOrganizationInvitationInline(admin.TabularInline):
model = OrganizationInvitation
fk_name = "to_organization"
fields = (
"from_organization",
"relationship_type",
"request",
"accepted_at",
"rejected_at",
)
readonly_fields = ("from_organization", "accepted_at", "rejected_at")
extra = 0
verbose_name = "Incoming Organization Invitation"
verbose_name_plural = "Incoming Organization Invitations"


@admin.register(Organization)
class OrganizationAdmin(VersionAdmin):
def export_organizations_as_csv(self, request, queryset):
Expand Down Expand Up @@ -285,6 +318,8 @@ def export_organizations_as_csv(self, request, queryset):
"city",
"state",
"country",
"collective_enabled",
"share_resources",
"parent",
"members",
"merged",
Expand All @@ -308,6 +343,8 @@ def export_organizations_as_csv(self, request, queryset):
inlines = (
ChildrenInline,
MembershipsInline,
OutgoingOrganizationInvitationInline,
IncomingOrganizationInvitationInline,
OrganizationUrlInline,
OrganizationEmailDomainInline,
SubscriptionInline,
Expand Down Expand Up @@ -712,3 +749,28 @@ def changelist_view(self, request, extra_context=None):
)

return super().changelist_view(request, extra_context)


@admin.register(OrganizationInvitation)
class OrganizationInvitationAdmin(VersionAdmin):
list_display = (
"from_organization",
"to_organization",
"from_user",
"closed_by_user",
"relationship_type",
"request",
"created_at",
"accepted_at",
"rejected_at",
)
list_filter = ("relationship_type", "request")
search_fields = ("from_organization__name", "to_organization__name")
readonly_fields = ("uuid", "created_at", "accepted_at", "rejected_at")
autocomplete_fields = (
"from_organization",
"to_organization",
"from_user",
"closed_by_user",
)
date_hierarchy = "created_at"
5 changes: 5 additions & 0 deletions squarelet/organizations/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ class ChangeLogReason(DjangoChoices):
credit_card = ChoiceItem(3, _("Credit Card"))


class RelationshipType(DjangoChoices):
member = ChoiceItem(0, _("Member"))
child = ChoiceItem(1, _("Child"))


COUNTRY_CHOICES = (
("US", _("United States of America")),
("CA", _("Canada")),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Generated by Django 4.2.18 on 2026-01-07 18:56

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import squarelet.core.fields
import uuid


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("organizations", "0046_plan_benefits"),
]

operations = [
migrations.AddField(
model_name="organization",
name="collective_enabled",
field=models.BooleanField(
default=False,
help_text="Enable this organization to participate in the collective feature as a parent or membership group. Only staff can enable this via admin.",
verbose_name="collective enabled",
),
),
migrations.AddField(
model_name="organization",
name="share_resources",
field=models.BooleanField(
default=True,
help_text="Share resources (subscriptions, credits) with all children and member organizations. Global toggle that applies to all relationships.",
verbose_name="share resources",
),
),
migrations.CreateModel(
name="OrganizationInvitation",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="UUID serves as secret token for this invitation in URLs",
unique=True,
verbose_name="UUID",
),
),
(
"relationship_type",
models.PositiveSmallIntegerField(
choices=[(0, "Member"), (1, "Child")],
help_text="Type of relationship: member or child",
verbose_name="relationship type",
),
),
(
"request",
models.BooleanField(
default=False,
help_text="True if this is a request TO JOIN from to_organization. False if this is an invitation FROM from_organization.",
verbose_name="request",
),
),
(
"created_at",
squarelet.core.fields.AutoCreatedField(
default=django.utils.timezone.now,
editable=False,
help_text="When this invitation was created",
verbose_name="created at",
),
),
(
"accepted_at",
models.DateTimeField(
blank=True,
help_text="When accepted (NULL if pending)",
null=True,
verbose_name="accepted at",
),
),
(
"rejected_at",
models.DateTimeField(
blank=True,
help_text="When rejected (NULL if pending)",
null=True,
verbose_name="rejected at",
),
),
(
"message",
models.TextField(
blank=True,
help_text="Optional message from the inviter",
verbose_name="message",
),
),
(
"closed_by_user",
models.ForeignKey(
blank=True,
help_text="The user who accepted or rejected this invitation",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="+",
to=settings.AUTH_USER_MODEL,
verbose_name="user",
),
),
(
"from_organization",
models.ForeignKey(
help_text="The organization extending the invitation or receiving the request. This is always the organization that is the parent or group.",
on_delete=django.db.models.deletion.CASCADE,
related_name="outgoing_org_invitations",
to="organizations.organization",
verbose_name="from organization",
),
),
(
"from_user",
models.ForeignKey(
help_text="The user who initiated this invitation",
on_delete=django.db.models.deletion.PROTECT,
related_name="+",
to=settings.AUTH_USER_MODEL,
verbose_name="user",
),
),
(
"to_organization",
models.ForeignKey(
help_text="The organization being invited. This is always the child or member",
on_delete=django.db.models.deletion.CASCADE,
related_name="incoming_org_invitations",
to="organizations.organization",
verbose_name="to organization",
),
),
],
options={
"ordering": ("-created_at",),
},
),
]
13 changes: 13 additions & 0 deletions squarelet/organizations/migrations/0049_merge_20260108_1319.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Generated by Django 4.2.18 on 2026-01-08 18:19

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("organizations", "0047_organization_collective_enabled_and_more"),
("organizations", "0048_add_last_overdue_email_sent_to_invoice"),
]

operations = []
13 changes: 13 additions & 0 deletions squarelet/organizations/migrations/0050_merge_20260122_1344.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Generated by Django 4.2.18 on 2026-01-22 18:44

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("organizations", "0049_merge_20260108_1319"),
("organizations", "0049_plan_short_description"),
]

operations = []
Loading