Skip to content

Commit

Permalink
feat: add Club/Association management tools
Browse files Browse the repository at this point in the history
  • Loading branch information
m4tx committed Nov 29, 2023
1 parent 136d02f commit c23c0f8
Show file tree
Hide file tree
Showing 23 changed files with 686 additions and 14 deletions.
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"
21 changes: 21 additions & 0 deletions club/forms.py
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",
)
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()
118 changes: 118 additions & 0 deletions club/templates/club/member_detail.html
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 %}
20 changes: 20 additions & 0 deletions club/templates/club/member_form.html
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 %}
29 changes: 29 additions & 0 deletions club/templates/club/member_list.html
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 %}
Loading

0 comments on commit c23c0f8

Please sign in to comment.