Skip to content

Commit ae79e6c

Browse files
committed
Fix(#132): Cleans up SIG creation
Closes #132.
1 parent e084657 commit ae79e6c

11 files changed

Lines changed: 1019 additions & 9 deletions

File tree

backend/membership/admin.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,14 @@
1414

1515
from inventory.services.qr_code_service import QRCodeService
1616

17-
from .models import Membership, SIGAdmin, UserRegistrationToken
17+
from .models import (
18+
Committee,
19+
CommitteeChair,
20+
Membership,
21+
SIGAdmin,
22+
SIGCommittee,
23+
UserRegistrationToken,
24+
)
1825

1926
User = get_user_model()
2027

@@ -505,3 +512,33 @@ def qr_code_preview(self, obj):
505512
qr_url,
506513
)
507514
return format_html('<span style="color: gray;">Token is not active</span>')
515+
516+
517+
@admin.register(Committee)
518+
class CommitteeAdmin(admin.ModelAdmin):
519+
"""Admin interface for Committee model."""
520+
521+
list_display = ["name", "is_active", "sig_count", "created_at"]
522+
list_filter = ["is_active", "created_at"]
523+
search_fields = ["name", "description"]
524+
readonly_fields = ["created_at", "updated_at"]
525+
526+
527+
@admin.register(CommitteeChair)
528+
class CommitteeChairAdmin(admin.ModelAdmin):
529+
"""Admin interface for CommitteeChair model."""
530+
531+
list_display = ["user", "committee", "is_active", "created_at"]
532+
list_filter = ["is_active", "committee", "created_at"]
533+
search_fields = ["user__username", "user__email", "committee__name"]
534+
readonly_fields = ["created_at", "updated_at"]
535+
536+
537+
@admin.register(SIGCommittee)
538+
class SIGCommitteeAdmin(admin.ModelAdmin):
539+
"""Admin interface for SIGCommittee model."""
540+
541+
list_display = ["group", "committee", "created_at"]
542+
list_filter = ["committee", "created_at"]
543+
search_fields = ["group__name", "committee__name"]
544+
readonly_fields = ["created_at", "updated_at"]
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Generated by Django 4.2.26 on 2026-01-21 23:50
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("auth", "0012_alter_user_first_name_max_length"),
12+
("membership", "0006_add_signature_line_offset"),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name="Committee",
18+
fields=[
19+
(
20+
"id",
21+
models.BigAutoField(
22+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
23+
),
24+
),
25+
(
26+
"name",
27+
models.CharField(
28+
help_text="Name of the committee", max_length=200, unique=True
29+
),
30+
),
31+
(
32+
"description",
33+
models.TextField(
34+
blank=True,
35+
help_text="Description of the committee's purpose and responsibilities",
36+
),
37+
),
38+
(
39+
"is_active",
40+
models.BooleanField(
41+
default=True, help_text="Whether this committee is currently active"
42+
),
43+
),
44+
("created_at", models.DateTimeField(auto_now_add=True)),
45+
("updated_at", models.DateTimeField(auto_now=True)),
46+
],
47+
options={
48+
"verbose_name": "Committee",
49+
"verbose_name_plural": "Committees",
50+
"ordering": ["name"],
51+
},
52+
),
53+
migrations.CreateModel(
54+
name="SIGCommittee",
55+
fields=[
56+
(
57+
"id",
58+
models.BigAutoField(
59+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
60+
),
61+
),
62+
("created_at", models.DateTimeField(auto_now_add=True)),
63+
("updated_at", models.DateTimeField(auto_now=True)),
64+
(
65+
"committee",
66+
models.ForeignKey(
67+
help_text="Committee this SIG belongs to",
68+
on_delete=django.db.models.deletion.CASCADE,
69+
related_name="sigs",
70+
to="membership.committee",
71+
),
72+
),
73+
(
74+
"group",
75+
models.OneToOneField(
76+
help_text="SIG (Group) that belongs to this committee",
77+
on_delete=django.db.models.deletion.CASCADE,
78+
related_name="committee_membership",
79+
to="auth.group",
80+
),
81+
),
82+
],
83+
options={
84+
"verbose_name": "SIG Committee Membership",
85+
"verbose_name_plural": "SIG Committee Memberships",
86+
},
87+
),
88+
migrations.CreateModel(
89+
name="CommitteeChair",
90+
fields=[
91+
(
92+
"id",
93+
models.BigAutoField(
94+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
95+
),
96+
),
97+
(
98+
"is_active",
99+
models.BooleanField(default=True, help_text="Is this chair role active?"),
100+
),
101+
("created_at", models.DateTimeField(auto_now_add=True)),
102+
("updated_at", models.DateTimeField(auto_now=True)),
103+
(
104+
"committee",
105+
models.ForeignKey(
106+
help_text="Committee this user chairs",
107+
on_delete=django.db.models.deletion.CASCADE,
108+
related_name="chairs",
109+
to="membership.committee",
110+
),
111+
),
112+
(
113+
"user",
114+
models.ForeignKey(
115+
help_text="User who is a chair of this committee",
116+
on_delete=django.db.models.deletion.CASCADE,
117+
related_name="committee_chair_roles",
118+
to=settings.AUTH_USER_MODEL,
119+
),
120+
),
121+
],
122+
options={
123+
"verbose_name": "Committee Chair",
124+
"verbose_name_plural": "Committee Chairs",
125+
"ordering": ["committee", "user"],
126+
},
127+
),
128+
migrations.AddIndex(
129+
model_name="committee",
130+
index=models.Index(fields=["is_active", "name"], name="membership__is_acti_4dd1ed_idx"),
131+
),
132+
migrations.AddIndex(
133+
model_name="sigcommittee",
134+
index=models.Index(
135+
fields=["committee", "group"], name="membership__committ_0422c5_idx"
136+
),
137+
),
138+
migrations.AddIndex(
139+
model_name="committeechair",
140+
index=models.Index(fields=["user", "is_active"], name="membership__user_id_fd33ca_idx"),
141+
),
142+
migrations.AddIndex(
143+
model_name="committeechair",
144+
index=models.Index(
145+
fields=["committee", "is_active"], name="membership__committ_29beb3_idx"
146+
),
147+
),
148+
migrations.AlterUniqueTogether(
149+
name="committeechair",
150+
unique_together={("user", "committee")},
151+
),
152+
]

backend/membership/models.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,150 @@ def get_sig_admins(cls, group):
242242
).distinct()
243243

244244

245+
class Committee(models.Model):
246+
"""
247+
Committee model for makerspace committees.
248+
249+
Committees are organizational structures that contain Special Interest Groups (SIGs).
250+
Each committee can have one or more chairs who can create and manage SIGs within that committee.
251+
"""
252+
253+
name = models.CharField(
254+
max_length=200,
255+
unique=True,
256+
help_text="Name of the committee",
257+
)
258+
description = models.TextField(
259+
blank=True,
260+
help_text="Description of the committee's purpose and responsibilities",
261+
)
262+
is_active = models.BooleanField(
263+
default=True,
264+
help_text="Whether this committee is currently active",
265+
)
266+
created_at = models.DateTimeField(auto_now_add=True)
267+
updated_at = models.DateTimeField(auto_now=True)
268+
269+
class Meta:
270+
ordering = ["name"]
271+
verbose_name = "Committee"
272+
verbose_name_plural = "Committees"
273+
indexes = [
274+
models.Index(fields=["is_active", "name"]),
275+
]
276+
277+
def __str__(self) -> str:
278+
return self.name
279+
280+
@property
281+
def sig_count(self):
282+
"""Get the number of SIGs in this committee."""
283+
return self.sigs.count()
284+
285+
286+
class CommitteeChair(models.Model):
287+
"""
288+
Tracks which users are chairs of which committees.
289+
290+
Committee Chairs can create and manage Special Interest Groups (SIGs)
291+
within their committee.
292+
"""
293+
294+
user = models.ForeignKey(
295+
settings.AUTH_USER_MODEL,
296+
on_delete=models.CASCADE,
297+
related_name="committee_chair_roles",
298+
help_text="User who is a chair of this committee",
299+
)
300+
committee = models.ForeignKey(
301+
Committee,
302+
on_delete=models.CASCADE,
303+
related_name="chairs",
304+
help_text="Committee this user chairs",
305+
)
306+
is_active = models.BooleanField(
307+
default=True,
308+
help_text="Is this chair role active?",
309+
)
310+
created_at = models.DateTimeField(auto_now_add=True)
311+
updated_at = models.DateTimeField(auto_now=True)
312+
313+
class Meta:
314+
unique_together = [["user", "committee"]]
315+
ordering = ["committee", "user"]
316+
indexes = [
317+
models.Index(fields=["user", "is_active"]),
318+
models.Index(fields=["committee", "is_active"]),
319+
]
320+
verbose_name = "Committee Chair"
321+
verbose_name_plural = "Committee Chairs"
322+
323+
def __str__(self) -> str:
324+
return f"{self.user.username} - {self.committee.name}"
325+
326+
@classmethod
327+
def is_committee_chair(cls, user, committee):
328+
"""Check if a user is a chair of a specific committee."""
329+
if not user or not user.is_authenticated or not committee:
330+
return False
331+
return cls.objects.filter(user=user, committee=committee, is_active=True).exists()
332+
333+
@classmethod
334+
def get_user_committees(cls, user):
335+
"""Get all committees that a user chairs."""
336+
if not user or not user.is_authenticated:
337+
return Committee.objects.none()
338+
return Committee.objects.filter(
339+
chairs__user=user, chairs__is_active=True
340+
).distinct()
341+
342+
@classmethod
343+
def get_committee_chairs(cls, committee):
344+
"""Get all chair users for a specific committee."""
345+
from django.contrib.auth import get_user_model
346+
347+
User = get_user_model()
348+
if not committee:
349+
return User.objects.none()
350+
return User.objects.filter(
351+
committee_chair_roles__committee=committee, committee_chair_roles__is_active=True
352+
).distinct()
353+
354+
355+
class SIGCommittee(models.Model):
356+
"""
357+
Links a SIG (Group) to a Committee.
358+
359+
This model creates the relationship between Special Interest Groups (represented as Django Groups)
360+
and Committees. Each SIG belongs to one committee.
361+
"""
362+
363+
group = models.OneToOneField(
364+
Group,
365+
on_delete=models.CASCADE,
366+
related_name="committee_membership",
367+
help_text="SIG (Group) that belongs to this committee",
368+
)
369+
committee = models.ForeignKey(
370+
Committee,
371+
on_delete=models.CASCADE,
372+
related_name="sigs",
373+
help_text="Committee this SIG belongs to",
374+
)
375+
created_at = models.DateTimeField(auto_now_add=True)
376+
updated_at = models.DateTimeField(auto_now=True)
377+
378+
class Meta:
379+
verbose_name = "SIG Committee Membership"
380+
verbose_name_plural = "SIG Committee Memberships"
381+
indexes = [
382+
models.Index(fields=["committee", "group"]),
383+
]
384+
385+
def __str__(self) -> str:
386+
return f"{self.group.name} - {self.committee.name}"
387+
388+
245389
class UserRegistrationToken(models.Model):
246390
"""
247391
One-time use token for user registration via QR code.

0 commit comments

Comments
 (0)