Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion backend/membership/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@

from inventory.services.qr_code_service import QRCodeService

from .models import Membership, SIGAdmin, UserRegistrationToken
from .models import (
Committee,
CommitteeChair,
Membership,
SIGAdmin,
SIGCommittee,
UserRegistrationToken,
)

User = get_user_model()

Expand Down Expand Up @@ -505,3 +512,33 @@ def qr_code_preview(self, obj):
qr_url,
)
return format_html('<span style="color: gray;">Token is not active</span>')


@admin.register(Committee)
class CommitteeAdmin(admin.ModelAdmin):
"""Admin interface for Committee model."""

list_display = ["name", "is_active", "sig_count", "created_at"]
list_filter = ["is_active", "created_at"]
search_fields = ["name", "description"]
readonly_fields = ["created_at", "updated_at"]


@admin.register(CommitteeChair)
class CommitteeChairAdmin(admin.ModelAdmin):
"""Admin interface for CommitteeChair model."""

list_display = ["user", "committee", "is_active", "created_at"]
list_filter = ["is_active", "committee", "created_at"]
search_fields = ["user__username", "user__email", "committee__name"]
readonly_fields = ["created_at", "updated_at"]


@admin.register(SIGCommittee)
class SIGCommitteeAdmin(admin.ModelAdmin):
"""Admin interface for SIGCommittee model."""

list_display = ["group", "committee", "created_at"]
list_filter = ["committee", "created_at"]
search_fields = ["group__name", "committee__name"]
readonly_fields = ["created_at", "updated_at"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Generated by Django 4.2.26 on 2026-01-21 23:50

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("membership", "0006_add_signature_line_offset"),
]

operations = [
migrations.CreateModel(
name="Committee",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"name",
models.CharField(
help_text="Name of the committee", max_length=200, unique=True
),
),
(
"description",
models.TextField(
blank=True,
help_text="Description of the committee's purpose and responsibilities",
),
),
(
"is_active",
models.BooleanField(
default=True, help_text="Whether this committee is currently active"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"verbose_name": "Committee",
"verbose_name_plural": "Committees",
"ordering": ["name"],
},
),
migrations.CreateModel(
name="SIGCommittee",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"committee",
models.ForeignKey(
help_text="Committee this SIG belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="sigs",
to="membership.committee",
),
),
(
"group",
models.OneToOneField(
help_text="SIG (Group) that belongs to this committee",
on_delete=django.db.models.deletion.CASCADE,
related_name="committee_membership",
to="auth.group",
),
),
],
options={
"verbose_name": "SIG Committee Membership",
"verbose_name_plural": "SIG Committee Memberships",
},
),
migrations.CreateModel(
name="CommitteeChair",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"is_active",
models.BooleanField(default=True, help_text="Is this chair role active?"),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"committee",
models.ForeignKey(
help_text="Committee this user chairs",
on_delete=django.db.models.deletion.CASCADE,
related_name="chairs",
to="membership.committee",
),
),
(
"user",
models.ForeignKey(
help_text="User who is a chair of this committee",
on_delete=django.db.models.deletion.CASCADE,
related_name="committee_chair_roles",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Committee Chair",
"verbose_name_plural": "Committee Chairs",
"ordering": ["committee", "user"],
},
),
migrations.AddIndex(
model_name="committee",
index=models.Index(fields=["is_active", "name"], name="membership__is_acti_4dd1ed_idx"),
),
migrations.AddIndex(
model_name="sigcommittee",
index=models.Index(
fields=["committee", "group"], name="membership__committ_0422c5_idx"
),
),
migrations.AddIndex(
model_name="committeechair",
index=models.Index(fields=["user", "is_active"], name="membership__user_id_fd33ca_idx"),
),
migrations.AddIndex(
model_name="committeechair",
index=models.Index(
fields=["committee", "is_active"], name="membership__committ_29beb3_idx"
),
),
migrations.AlterUniqueTogether(
name="committeechair",
unique_together={("user", "committee")},
),
]
144 changes: 144 additions & 0 deletions backend/membership/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,150 @@ def get_sig_admins(cls, group):
).distinct()


class Committee(models.Model):
"""
Committee model for makerspace committees.

Committees are organizational structures that contain Special Interest Groups (SIGs).
Each committee can have one or more chairs who can create and manage SIGs within that committee.
"""

name = models.CharField(
max_length=200,
unique=True,
help_text="Name of the committee",
)
description = models.TextField(
blank=True,
help_text="Description of the committee's purpose and responsibilities",
)
is_active = models.BooleanField(
default=True,
help_text="Whether this committee is currently active",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
ordering = ["name"]
verbose_name = "Committee"
verbose_name_plural = "Committees"
indexes = [
models.Index(fields=["is_active", "name"]),
]

def __str__(self) -> str:
return self.name

@property
def sig_count(self):
"""Get the number of SIGs in this committee."""
return self.sigs.count()


class CommitteeChair(models.Model):
"""
Tracks which users are chairs of which committees.

Committee Chairs can create and manage Special Interest Groups (SIGs)
within their committee.
"""

user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="committee_chair_roles",
help_text="User who is a chair of this committee",
)
committee = models.ForeignKey(
Committee,
on_delete=models.CASCADE,
related_name="chairs",
help_text="Committee this user chairs",
)
is_active = models.BooleanField(
default=True,
help_text="Is this chair role active?",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
unique_together = [["user", "committee"]]
ordering = ["committee", "user"]
indexes = [
models.Index(fields=["user", "is_active"]),
models.Index(fields=["committee", "is_active"]),
]
verbose_name = "Committee Chair"
verbose_name_plural = "Committee Chairs"

def __str__(self) -> str:
return f"{self.user.username} - {self.committee.name}"

@classmethod
def is_committee_chair(cls, user, committee):
"""Check if a user is a chair of a specific committee."""
if not user or not user.is_authenticated or not committee:
return False
return cls.objects.filter(user=user, committee=committee, is_active=True).exists()

@classmethod
def get_user_committees(cls, user):
"""Get all committees that a user chairs."""
if not user or not user.is_authenticated:
return Committee.objects.none()
return Committee.objects.filter(
chairs__user=user, chairs__is_active=True
).distinct()

@classmethod
def get_committee_chairs(cls, committee):
"""Get all chair users for a specific committee."""
from django.contrib.auth import get_user_model

User = get_user_model()
if not committee:
return User.objects.none()
return User.objects.filter(
committee_chair_roles__committee=committee, committee_chair_roles__is_active=True
).distinct()


class SIGCommittee(models.Model):
"""
Links a SIG (Group) to a Committee.

This model creates the relationship between Special Interest Groups (represented as Django Groups)
and Committees. Each SIG belongs to one committee.
"""

group = models.OneToOneField(
Group,
on_delete=models.CASCADE,
related_name="committee_membership",
help_text="SIG (Group) that belongs to this committee",
)
committee = models.ForeignKey(
Committee,
on_delete=models.CASCADE,
related_name="sigs",
help_text="Committee this SIG belongs to",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
verbose_name = "SIG Committee Membership"
verbose_name_plural = "SIG Committee Memberships"
indexes = [
models.Index(fields=["committee", "group"]),
]

def __str__(self) -> str:
return f"{self.group.name} - {self.committee.name}"


class UserRegistrationToken(models.Model):
"""
One-time use token for user registration via QR code.
Expand Down
Loading
Loading