Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Club/Association management tools #33

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
Empty file added club/__init__.py
Empty file.
29 changes: 29 additions & 0 deletions club/admin.py
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)
6 changes: 6 additions & 0 deletions club/apps.py
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"
49 changes: 49 additions & 0 deletions club/forms.py
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)
58 changes: 58 additions & 0 deletions club/migrations/0001_initial.py
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 added club/migrations/__init__.py
Empty file.
96 changes: 96 additions & 0 deletions club/models.py
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()
33 changes: 33 additions & 0 deletions club/templates/club/club.html
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 %}
115 changes: 115 additions & 0 deletions club/templates/club/member_detail.html
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 %}
Loading