-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add Club/Association management tools
- Loading branch information
Showing
23 changed files
with
686 additions
and
14 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
from collections.abc import Iterable | ||
from typing import ClassVar | ||
|
||
from django.contrib import admin | ||
|
||
from club.models import Member, MemberRole, MembershipFeePeriod | ||
|
||
|
||
class MemberRoleInline(admin.TabularInline): | ||
model = MemberRole | ||
|
||
|
||
class MemberAdmin(admin.ModelAdmin): | ||
prepopulated_fields: ClassVar[dict[str, Iterable[str]]] = {"id": ("first_name", "last_name")} | ||
|
||
search_fields = ("id", "first_name", "last_name", "nickname") | ||
list_display = ("id", "first_name", "last_name", "nickname", "active_since", "active_until") | ||
list_display_links = ("id", "first_name", "last_name", "nickname") | ||
ordering = ("last_name", "first_name") | ||
|
||
inlines = (MemberRoleInline,) | ||
|
||
|
||
class MembershipFeePeriodAdmin(admin.ModelAdmin): | ||
pass | ||
|
||
|
||
admin.site.register(Member, MemberAdmin) | ||
admin.site.register(MembershipFeePeriod, MembershipFeePeriodAdmin) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class ClubConfig(AppConfig): | ||
default_auto_field = "django.db.models.BigAutoField" | ||
name = "club" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from django.forms import ModelForm | ||
|
||
from club.models import Member | ||
|
||
|
||
class MemberForm(ModelForm): | ||
class Meta: | ||
model = Member | ||
fields = ( | ||
"first_name", | ||
"last_name", | ||
"nickname", | ||
"workspace_email", | ||
"personal_email", | ||
"email_communication_consent", | ||
"address", | ||
"discord_nickname", | ||
"discord_id", | ||
"active_since", | ||
"active_until", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# Generated by Django 4.2.7 on 2023-11-26 22:43 | ||
|
||
from django.conf import settings | ||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
initial = True | ||
|
||
dependencies = [ | ||
('players', '0004_auto_20190922_1346'), | ||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='Member', | ||
fields=[ | ||
('id', models.SlugField(primary_key=True, serialize=False, verbose_name='ID')), | ||
('first_name', models.CharField(max_length=30)), | ||
('last_name', models.CharField(max_length=150)), | ||
('nickname', models.CharField(blank=True, max_length=50)), | ||
('workspace_email', models.EmailField(max_length=254)), | ||
('email_communication_consent', models.BooleanField()), | ||
('personal_email', models.EmailField(blank=True, max_length=254)), | ||
('address', models.TextField()), | ||
('discord_nickname', models.CharField(blank=True, max_length=30, verbose_name='Discord nickname')), | ||
('discord_id', models.CharField(blank=True, max_length=30, verbose_name='Discord ID')), | ||
('active_since', models.DateField()), | ||
('active_until', models.DateField(blank=True, null=True)), | ||
('player', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='players.player')), | ||
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), | ||
], | ||
), | ||
migrations.CreateModel( | ||
name='MembershipFeePeriod', | ||
fields=[ | ||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('name', models.CharField(max_length=30)), | ||
('active_since', models.DateField()), | ||
('active_until', models.DateField()), | ||
('fee', models.DecimalField(decimal_places=6, max_digits=15)), | ||
('members', models.ManyToManyField(blank=True, to='club.member')), | ||
], | ||
), | ||
migrations.CreateModel( | ||
name='MemberRole', | ||
fields=[ | ||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('role', models.CharField(choices=[('BOARD', 'Board'), ('AUDIT_COMMITTEE', 'Audit committee')], max_length=20)), | ||
('active_since', models.DateField()), | ||
('active_until', models.DateField(blank=True, null=True)), | ||
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='club.member')), | ||
], | ||
), | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
from django.contrib.auth.models import User | ||
from django.db import models | ||
from django.utils import timezone | ||
from django_hosts import reverse | ||
|
||
from players.models import Player | ||
|
||
|
||
class Member(models.Model): | ||
id = models.SlugField(primary_key=True, verbose_name="ID") | ||
|
||
user = models.OneToOneField(User, on_delete=models.SET_NULL, null=True, blank=True) | ||
player = models.OneToOneField(Player, on_delete=models.SET_NULL, null=True, blank=True) | ||
|
||
first_name = models.CharField(max_length=30) | ||
last_name = models.CharField(max_length=150) | ||
nickname = models.CharField(max_length=50, blank=True) | ||
|
||
workspace_email = models.EmailField() | ||
email_communication_consent = models.BooleanField() | ||
personal_email = models.EmailField(blank=True) | ||
address = models.TextField() | ||
discord_nickname = models.CharField(max_length=30, blank=True, verbose_name="Discord nickname") | ||
discord_id = models.CharField(max_length=30, blank=True, verbose_name="Discord ID") | ||
|
||
active_since = models.DateField() | ||
active_until = models.DateField(null=True, blank=True) | ||
|
||
def get_absolute_url(self): | ||
return reverse("member-detail", kwargs={"pk": self.pk}, host="root") | ||
|
||
def relevant_fees(self): | ||
return MembershipFeePeriod.objects.filter(active_until__gte=self.active_since) | ||
|
||
def paid_fees(self): | ||
return self.membershipfeeperiod_set.all() | ||
|
||
def current_fee_paid(self): | ||
last_period = MembershipFeePeriod.objects.last_period() | ||
if last_period is None: | ||
return True | ||
return self in last_period.members.all() | ||
|
||
def current_role(self): | ||
today = timezone.now().date() | ||
last_role = self.memberrole_set.order_by("-active_until").first() | ||
if last_role is not None and (last_role.active_until is None or last_role.active_until > today): | ||
return last_role.name() | ||
else: | ||
return "Member" | ||
|
||
def __str__(self): | ||
s = "" | ||
|
||
if self.first_name and self.last_name: | ||
s = f"{self.first_name} {self.last_name}" | ||
|
||
if self.nickname: | ||
if s: | ||
s += f" ({self.nickname})" | ||
else: | ||
s = self.nickname | ||
|
||
return s | ||
|
||
|
||
class MemberRole(models.Model): | ||
class MemberRoleChoice(models.TextChoices): | ||
BOARD = ("BOARD", "Board") | ||
AUDIT_COMMITTEE = ("AUDIT_COMMITTEE", "Audit committee") | ||
|
||
member = models.ForeignKey(Member, on_delete=models.CASCADE) | ||
role = models.CharField( | ||
max_length=20, | ||
choices=MemberRoleChoice.choices, | ||
) | ||
active_since = models.DateField() | ||
active_until = models.DateField(null=True, blank=True) | ||
|
||
def name(self): | ||
return self.get_role_display() | ||
|
||
|
||
class MembershipFeePeriodManager(models.Manager): | ||
def last_period(self): | ||
return self.order_by("-active_until").first() | ||
|
||
|
||
class MembershipFeePeriod(models.Model): | ||
name = models.CharField(max_length=30) | ||
active_since = models.DateField() | ||
active_until = models.DateField() | ||
fee = models.DecimalField(max_digits=15, decimal_places=6) | ||
members = models.ManyToManyField(Member, blank=True) | ||
|
||
objects = MembershipFeePeriodManager() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
{% extends 'common/base.html' %} | ||
{% load breadcrumbs %} | ||
|
||
{% block title %}{{ object }}{% endblock %} | ||
|
||
{% block body %} | ||
<div class="container pt-3"> | ||
{% breadcrumbs %} | ||
|
||
<h1>{{ object }}</h1> | ||
|
||
<dl class="row"> | ||
<dt class="col-sm-3">First name</dt> | ||
<dd class="col-sm-9">{{ object.first_name }}</dd> | ||
|
||
<dt class="col-sm-3">Last name</dt> | ||
<dd class="col-sm-9">{{ object.last_name }}</dd> | ||
|
||
<dt class="col-sm-3">Nickname</dt> | ||
<dd class="col-sm-9">{{ object.nickname|default:"–" }}</dd> | ||
|
||
<dt class="col-sm-3">Workspace email</dt> | ||
<dd class="col-sm-9"> | ||
{% if member.workspace_email %} | ||
<a href="mailto:{{ member.workspace_email }}"> | ||
{{ member.workspace_email|default:"–" }} | ||
</a> | ||
{% else %} | ||
- | ||
{% endif %} | ||
</dd> | ||
|
||
<dt class="col-sm-3">Personal email</dt> | ||
<dd class="col-sm-9"> | ||
{% if member.workspace_email %} | ||
<a href="mailto:{{ member.personal_email }}"> | ||
{{ member.personal_email|default:"–" }} | ||
</a> | ||
{% else %} | ||
- | ||
{% endif %} | ||
</dd> | ||
|
||
<dt class="col-sm-3">Address</dt> | ||
<dd class="col-sm-9">{{ object.address|linebreaksbr }}</dd> | ||
|
||
<dt class="col-sm-3">Discord nickname</dt> | ||
<dd class="col-sm-9">{{ object.discord_nickname|default:"–" }}</dd> | ||
|
||
<dt class="col-sm-3">Discord ID</dt> | ||
<dd class="col-sm-9">{{ object.discord_id|default:"–" }}</dd> | ||
|
||
<dt class="col-sm-3">Active since</dt> | ||
<dd class="col-sm-9">{{ object.active_since|default:"–" }}</dd> | ||
|
||
<dt class="col-sm-3">Active until</dt> | ||
<dd class="col-sm-9">{{ object.active_until|default:"–" }}</dd> | ||
|
||
<dt class="col-sm-3">Current fee paid</dt> | ||
<dd class="col-sm-9"> | ||
{% if object.current_fee_paid %} | ||
✅ | ||
{% else %} | ||
❌ | ||
{% endif %} | ||
</dd> | ||
|
||
<dt class="col-sm-3">Current role</dt> | ||
<dd class="col-sm-9">{{ member.current_role }}</dd> | ||
</dl> | ||
|
||
<h2>Fees</h2> | ||
<table class="table table-striped table-sm"> | ||
<thead> | ||
<tr> | ||
{% for fee in member.relevant_fees %} | ||
<th scope="col">{{ fee.name }}</th> | ||
{% endfor %} | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<tr> | ||
{% for fee in member.relevant_fees %} | ||
<td class="d-none d-sm-table-cell"> | ||
{% if fee in member.paid_fees %} | ||
✅ | ||
{% else %} | ||
❌ | ||
{% endif %} | ||
</td> | ||
{% endfor %} | ||
</tr> | ||
</tbody> | ||
</table> | ||
|
||
<h2>Roles</h2> | ||
<table class="table table-striped table-sm"> | ||
<thead> | ||
<tr> | ||
<th scope="col">Role name</th> | ||
<th scope="col">Active since</th> | ||
<th scope="col">Active until</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{% for role in member.memberrole_set.all %} | ||
<tr> | ||
<td>{{ role.name }}</td> | ||
<td>{{ role.active_since }}</td> | ||
<td>{{ role.active_until|default:"–" }}</td> | ||
</tr> | ||
{% endfor %} | ||
<tr> | ||
</tr> | ||
</tbody> | ||
</table> | ||
</div> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{% extends 'common/base.html' %} | ||
{% load django_bootstrap5 %} | ||
{% load breadcrumbs %} | ||
|
||
{% block title %}Members - Add{% endblock %} | ||
|
||
{% block body %} | ||
<div class="container pt-3"> | ||
{% breadcrumbs %} | ||
|
||
<h2>Create member</h2> | ||
|
||
<form action="" method="post"> | ||
{% csrf_token %} | ||
{% bootstrap_form form %} | ||
|
||
{% bootstrap_button "Create" button_type="submit" button_class="btn-primary" %} | ||
</form> | ||
</div> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
{% extends 'common/base.html' %} | ||
{% load breadcrumbs %} | ||
|
||
{% block title %}Members{% endblock %} | ||
|
||
{% block body %} | ||
<div class="container pt-3"> | ||
{% breadcrumbs %} | ||
|
||
<h2>Members</h2> | ||
|
||
<ul class="nav nav-pills nav-fill mb-3"> | ||
{% include "common/nav_pill.html" with url_name="member-list" link_text="All" %} | ||
{% include "common/nav_pill.html" with url_name="member-active-list" link_text="Active" %} | ||
{% include "common/nav_pill.html" with url_name="member-inactive-list" link_text="Inactive" %} | ||
{% include "common/nav_pill.html" with url_name="member-fee-overdue-list" link_text="Fee overdue" %} | ||
{% include "common/nav_pill.html" with url_name="member-board-list" link_text="Board" %} | ||
{% include "common/nav_pill.html" with url_name="member-audit-committee-list" link_text="Audit committee" %} | ||
</ul> | ||
|
||
<div class="mb-1"> | ||
<a class="btn btn-primary btn-sm" href="{% url 'member-create' %}" role="button"><i class="bi bi-person-fill-add"></i> Create member</a> | ||
<a class="btn btn-secondary btn-sm" href="#" role="button"><i class="bi bi-file-earmark-arrow-down"></i> Download spreadsheet</a> | ||
<a class="btn btn-secondary btn-sm" href="#" role="button"><i class="bi bi-envelope-fill"></i> Copy personal emails</a> | ||
</div> | ||
|
||
{% include 'club/member_table.html' with members=object_list %} | ||
</div> | ||
{% endblock %} |
Oops, something went wrong.