-
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
29 changed files
with
869 additions
and
27 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,49 @@ | ||
import datetime | ||
import itertools | ||
|
||
from django import forms | ||
from django.forms import Form, ModelForm | ||
|
||
from club.models import Member, MembershipFeePeriod | ||
|
||
|
||
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", | ||
) | ||
|
||
|
||
def get_fee_choices() -> list[tuple[str, str]]: | ||
fees = [create_half_year_fee_period(year, half) for year, half in itertools.product(range(2020, 2030), (1, 2))] | ||
return [(str(idx), fee.name) for idx, fee in enumerate(fees)] | ||
|
||
|
||
def create_half_year_fee_period(year: int, half: int) -> MembershipFeePeriod: | ||
name = f"{year}H{half}" | ||
if half == 1: | ||
active_since = datetime.date(year, 1, 1) | ||
active_until = datetime.date(year, 7, 1) | ||
elif half == 2: | ||
active_since = datetime.date(year, 7, 1) | ||
active_until = datetime.date(year + 1, 1, 1) | ||
else: | ||
raise Exception(f"Half {half} not valid") | ||
|
||
return MembershipFeePeriod(name=name, active_since=active_since, active_until=active_until) | ||
|
||
|
||
class MembershipFeePeriodForm(Form): | ||
period = forms.ChoiceField(choices=get_fee_choices()) | ||
fee = forms.DecimalField(max_digits=10, decimal_places=2) |
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,33 @@ | ||
{% extends 'common/base.html' %} | ||
{% load breadcrumbs %} | ||
|
||
{% block title %}Club{% endblock %} | ||
|
||
{% block body %} | ||
<div class="container pt-3"> | ||
{% breadcrumbs %} | ||
|
||
<h1>Club</h1> | ||
|
||
<div class="row"> | ||
<div class="col-sm-6 mb-3 mb-sm-0"> | ||
<div class="card h-100"> | ||
<div class="card-body"> | ||
<h5 class="card-title">Members</h5> | ||
<p class="card-text">Manage club members. Create new members, see who hasn't paid their membership fee, or check out their e-mail addresses.</p> | ||
<a href="{% url "member-list" %}" class="btn btn-primary">Manage members</a> | ||
</div> | ||
</div> | ||
</div> | ||
<div class="col-sm-6"> | ||
<div class="card h-100"> | ||
<div class="card-body"> | ||
<h5 class="card-title">Fees</h5> | ||
<p class="card-text">View, add, or edit membership fees.</p> | ||
<a href="{% url "fee-list" %}" class="btn btn-primary">Manage fees</a> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</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,115 @@ | ||
{% extends 'common/base.html' %} | ||
{% load breadcrumbs %} | ||
{% load kcc3 %} | ||
|
||
{% 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|placeholder }}</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 }} | ||
</a> | ||
{% else %} | ||
{% placeholder %} | ||
{% endif %} | ||
</dd> | ||
|
||
<dt class="col-sm-3">Personal email</dt> | ||
<dd class="col-sm-9"> | ||
{% if member.personal_email %} | ||
<a href="mailto:{{ member.personal_email }}"> | ||
{{ member.personal_email }} | ||
</a> | ||
{% else %} | ||
{% placeholder %} | ||
{% 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|placeholder }}</dd> | ||
|
||
<dt class="col-sm-3">Discord ID</dt> | ||
<dd class="col-sm-9">{{ object.discord_id|placeholder }}</dd> | ||
|
||
<dt class="col-sm-3">Active since</dt> | ||
<dd class="col-sm-9">{{ object.active_since|dateformat }}</dd> | ||
|
||
<dt class="col-sm-3">Active until</dt> | ||
<dd class="col-sm-9">{{ object.active_until|dateformat }}</dd> | ||
|
||
<dt class="col-sm-3">Current fee paid</dt> | ||
<dd class="col-sm-9"> | ||
{% include "common/bool.html" with value=object.current_fee_paid %} | ||
</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 %} | ||
{% include "common/bool.html" with value=True %} | ||
{% else %} | ||
{% include "common/bool.html" with value=False %} | ||
{% 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|dateformat }}</td> | ||
<td>{{ role.active_until|dateformat }}</td> | ||
</tr> | ||
{% endfor %} | ||
<tr> | ||
</tr> | ||
</tbody> | ||
</table> | ||
</div> | ||
{% endblock %} |
Oops, something went wrong.