diff --git a/club/__init__.py b/club/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/club/admin.py b/club/admin.py
new file mode 100644
index 0000000..51633d5
--- /dev/null
+++ b/club/admin.py
@@ -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)
diff --git a/club/apps.py b/club/apps.py
new file mode 100644
index 0000000..fffa95b
--- /dev/null
+++ b/club/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ClubConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "club"
diff --git a/club/forms.py b/club/forms.py
new file mode 100644
index 0000000..68fa6da
--- /dev/null
+++ b/club/forms.py
@@ -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)
diff --git a/club/migrations/0001_initial.py b/club/migrations/0001_initial.py
new file mode 100644
index 0000000..7fb8229
--- /dev/null
+++ b/club/migrations/0001_initial.py
@@ -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')),
+ ],
+ ),
+ ]
diff --git a/club/migrations/__init__.py b/club/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/club/models.py b/club/models.py
new file mode 100644
index 0000000..9d38d16
--- /dev/null
+++ b/club/models.py
@@ -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()
diff --git a/club/templates/club/club.html b/club/templates/club/club.html
new file mode 100644
index 0000000..b310491
--- /dev/null
+++ b/club/templates/club/club.html
@@ -0,0 +1,33 @@
+{% extends 'common/base.html' %}
+{% load breadcrumbs %}
+
+{% block title %}Club{% endblock %}
+
+{% block body %}
+
+ {% breadcrumbs %}
+
+
Club
+
+
+
+
+
+
Members
+
Manage club members. Create new members, see who hasn't paid their membership fee, or check out their e-mail addresses.
+
Manage members
+
+
+
+
+
+
+
Fees
+
View, add, or edit membership fees.
+
Manage fees
+
+
+
+
+
+{% endblock %}
diff --git a/club/templates/club/member_detail.html b/club/templates/club/member_detail.html
new file mode 100644
index 0000000..e0cd550
--- /dev/null
+++ b/club/templates/club/member_detail.html
@@ -0,0 +1,115 @@
+{% extends 'common/base.html' %}
+{% load breadcrumbs %}
+{% load kcc3 %}
+
+{% block title %}{{ object }}{% endblock %}
+
+{% block body %}
+
+ {% breadcrumbs %}
+
+
{{ object }}
+
+
+ First name
+ {{ object.first_name }}
+
+ Last name
+ {{ object.last_name }}
+
+ Nickname
+ {{ object.nickname|placeholder }}
+
+ Workspace email
+
+ {% if member.workspace_email %}
+
+ {{ member.workspace_email }}
+
+ {% else %}
+ {% placeholder %}
+ {% endif %}
+
+
+ Personal email
+
+ {% if member.personal_email %}
+
+ {{ member.personal_email }}
+
+ {% else %}
+ {% placeholder %}
+ {% endif %}
+
+
+ Address
+ {{ object.address|linebreaksbr }}
+
+ Discord nickname
+ {{ object.discord_nickname|placeholder }}
+
+ Discord ID
+ {{ object.discord_id|placeholder }}
+
+ Active since
+ {{ object.active_since|dateformat }}
+
+ Active until
+ {{ object.active_until|dateformat }}
+
+ Current fee paid
+
+ {% include "common/bool.html" with value=object.current_fee_paid %}
+
+
+ Current role
+ {{ member.current_role }}
+
+
+
Fees
+
+
+
+ {% for fee in member.relevant_fees %}
+ {{ fee.name }}
+ {% endfor %}
+
+
+
+
+ {% for fee in member.relevant_fees %}
+
+ {% if fee in member.paid_fees %}
+ {% include "common/bool.html" with value=True %}
+ {% else %}
+ {% include "common/bool.html" with value=False %}
+ {% endif %}
+
+ {% endfor %}
+
+
+
+
+
Roles
+
+
+
+ Role name
+ Active since
+ Active until
+
+
+
+ {% for role in member.memberrole_set.all %}
+
+ {{ role.name }}
+ {{ role.active_since|dateformat }}
+ {{ role.active_until|dateformat }}
+
+ {% endfor %}
+
+
+
+
+
+{% endblock %}
diff --git a/club/templates/club/member_form.html b/club/templates/club/member_form.html
new file mode 100644
index 0000000..cc1c4e2
--- /dev/null
+++ b/club/templates/club/member_form.html
@@ -0,0 +1,20 @@
+{% extends 'common/base.html' %}
+{% load django_bootstrap5 %}
+{% load breadcrumbs %}
+
+{% block title %}Members - Create{% endblock %}
+
+{% block body %}
+
+ {% breadcrumbs %}
+
+
Create member
+
+
+
+{% endblock %}
diff --git a/club/templates/club/member_list.html b/club/templates/club/member_list.html
new file mode 100644
index 0000000..4d0e3d4
--- /dev/null
+++ b/club/templates/club/member_list.html
@@ -0,0 +1,29 @@
+{% extends 'common/base.html' %}
+{% load breadcrumbs %}
+
+{% block title %}Members{% endblock %}
+
+{% block body %}
+
+ {% breadcrumbs %}
+
+
Members
+
+
+ {% 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" %}
+
+
+
+
+ {% include 'club/member_table.html' with members=object_list %}
+
+{% endblock %}
diff --git a/club/templates/club/member_table.html b/club/templates/club/member_table.html
new file mode 100644
index 0000000..820e1d9
--- /dev/null
+++ b/club/templates/club/member_table.html
@@ -0,0 +1,69 @@
+{% load kcc3 %}
+
diff --git a/club/templates/club/membershipfeeperiod_form.html b/club/templates/club/membershipfeeperiod_form.html
new file mode 100644
index 0000000..b93e5e9
--- /dev/null
+++ b/club/templates/club/membershipfeeperiod_form.html
@@ -0,0 +1,20 @@
+{% extends 'common/base.html' %}
+{% load django_bootstrap5 %}
+{% load breadcrumbs %}
+
+{% block title %}Fees - Create{% endblock %}
+
+{% block body %}
+
+ {% breadcrumbs %}
+
+
Create fee
+
+
+
+{% endblock %}
diff --git a/club/templates/club/membershipfeeperiod_list.html b/club/templates/club/membershipfeeperiod_list.html
new file mode 100644
index 0000000..e92ca73
--- /dev/null
+++ b/club/templates/club/membershipfeeperiod_list.html
@@ -0,0 +1,18 @@
+{% extends 'common/base.html' %}
+{% load breadcrumbs %}
+
+{% block title %}Members{% endblock %}
+
+{% block body %}
+
+ {% breadcrumbs %}
+
+
Fees
+
+
+
+ {% include 'club/membershipfeeperiod_table.html' with fees=object_list %}
+
+{% endblock %}
diff --git a/club/templates/club/membershipfeeperiod_table.html b/club/templates/club/membershipfeeperiod_table.html
new file mode 100644
index 0000000..ae79de5
--- /dev/null
+++ b/club/templates/club/membershipfeeperiod_table.html
@@ -0,0 +1,37 @@
+{% load kcc3 %}
+
+
+
+
+ Name
+ Fee
+ Active since
+ Active until
+ Members paid
+
+
+
+ {% for fee in fees %}
+
+
+
+ {{ fee.name }}
+
+
+
+ {{ fee.fee|floatformat:2 }}
+
+
+ {{ fee.active_since|dateformat }}
+
+
+ {{ fee.active_until|dateformat }}
+
+
+ {{ fee.members.count }}
+
+
+ {% endfor %}
+
+
+
diff --git a/club/templatetags/__init__.py b/club/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/club/templatetags/breadcrumbs.py b/club/templatetags/breadcrumbs.py
new file mode 100644
index 0000000..511f541
--- /dev/null
+++ b/club/templatetags/breadcrumbs.py
@@ -0,0 +1,83 @@
+from django import template
+from django_hosts import reverse
+
+from club.models import Member
+
+register = template.Library()
+
+
+class Breadcrumb:
+ def __init__(self, parent_url_name: str | None, url_name: str, link_text: str | None = None):
+ self.parent_url_name = parent_url_name
+ self.view_name = url_name
+ self.link_text = link_text
+
+ def get_link_text(self, request):
+ return self.link_text
+
+ def get_url(self, request):
+ return reverse(self.view_name, kwargs=self.get_url_kwargs(request), host="root")
+
+ def get_url_kwargs(self, request):
+ return {}
+
+
+class MemberBreadcrumb(Breadcrumb):
+ def get_link_text(self, request):
+ return str(Member.objects.get(pk=self.get_pk(request)))
+
+ def get_url_kwargs(self, request):
+ return {"pk": self.get_pk(request)}
+
+ @staticmethod
+ def get_pk(request):
+ return request.resolver_match.kwargs["pk"]
+
+
+BREADCRUMBS: dict[str, Breadcrumb] = {}
+
+
+def add_breadcrumb(breadcrumb: Breadcrumb):
+ BREADCRUMBS[breadcrumb.view_name] = breadcrumb
+
+
+def add_breadcrumb_copy(url_name: str, alternate_url_name: str):
+ BREADCRUMBS[alternate_url_name] = BREADCRUMBS[url_name]
+
+
+add_breadcrumb(Breadcrumb(None, "club", "Club"))
+add_breadcrumb(Breadcrumb("club", "member-list", "Members"))
+add_breadcrumb_copy("member-list", "member-active-list")
+add_breadcrumb_copy("member-list", "member-inactive-list")
+add_breadcrumb_copy("member-list", "member-fee-overdue-list")
+add_breadcrumb_copy("member-list", "member-board-list")
+add_breadcrumb_copy("member-list", "member-audit-committee-list")
+add_breadcrumb(MemberBreadcrumb("member-list", "member-detail"))
+add_breadcrumb(Breadcrumb("member-list", "member-create", "Create"))
+add_breadcrumb(Breadcrumb("club", "fee-list", "Fees"))
+add_breadcrumb(Breadcrumb("fee-list", "fee-create", "Create"))
+
+
+@register.inclusion_tag("common/breadcrumbs.html", takes_context=True)
+def breadcrumbs(context):
+ request = context["request"]
+ result = []
+ url_name = request.resolver_match.url_name
+ if url_name not in BREADCRUMBS:
+ raise Exception(f"URL name {url_name} unknown, cannot create breadcrumbs")
+ current_breadcrumb = BREADCRUMBS[url_name]
+
+ while current_breadcrumb is not None:
+ result.insert(
+ 0,
+ {
+ "url": current_breadcrumb.get_url(request),
+ "link_text": current_breadcrumb.get_link_text(request),
+ },
+ )
+
+ current_breadcrumb = BREADCRUMBS.get(current_breadcrumb.parent_url_name)
+
+ return {
+ "breadcrumbs": result,
+ }
diff --git a/club/templatetags/kcc3.py b/club/templatetags/kcc3.py
new file mode 100644
index 0000000..2cd4a24
--- /dev/null
+++ b/club/templatetags/kcc3.py
@@ -0,0 +1,26 @@
+import datetime
+
+from django import template
+
+register = template.Library()
+
+
+PLACEHOLDER = "\u2014"
+DEFAULT_DATE_FORMAT = "%Y-%m-%d"
+
+
+@register.simple_tag(name="placeholder")
+def placeholder_tag() -> str:
+ return PLACEHOLDER
+
+
+@register.filter(name="placeholder")
+def placeholder_filter(data: str | None) -> str:
+ return data if data else PLACEHOLDER
+
+
+@register.filter
+def dateformat(date: datetime.date | None) -> str:
+ if date is None:
+ return PLACEHOLDER
+ return date.strftime(DEFAULT_DATE_FORMAT)
diff --git a/club/tests.py b/club/tests.py
new file mode 100644
index 0000000..a39b155
--- /dev/null
+++ b/club/tests.py
@@ -0,0 +1 @@
+# Create your tests here.
diff --git a/club/urls.py b/club/urls.py
new file mode 100644
index 0000000..185cb2b
--- /dev/null
+++ b/club/urls.py
@@ -0,0 +1,30 @@
+from django.urls import path
+
+from .views import (
+ ClubView,
+ FeeCreateView,
+ FeeListView,
+ MemberActiveListView,
+ MemberAuditCommitteeListView,
+ MemberBoardListView,
+ MemberCreateView,
+ MemberDetailView,
+ MemberFeeOverdueListView,
+ MemberInactiveListView,
+ MemberListView,
+)
+
+urlpatterns = [
+ path("", ClubView.as_view(), name="club"),
+ path("members/", MemberListView.as_view(), name="member-list"),
+ path("members/active/", MemberActiveListView.as_view(), name="member-active-list"),
+ path("members/inactive/", MemberInactiveListView.as_view(), name="member-inactive-list"),
+ path("members/fee-overdue/", MemberFeeOverdueListView.as_view(), name="member-fee-overdue-list"),
+ path("members/board/", MemberBoardListView.as_view(), name="member-board-list"),
+ path("members/audit-committee/", MemberAuditCommitteeListView.as_view(), name="member-audit-committee-list"),
+ path("members/create/", MemberCreateView.as_view(), name="member-create"),
+ path("members/get//", MemberDetailView.as_view(), name="member-detail"),
+ # path("members//edit/", MemberDetailView.as_view(), name="member-edit"),
+ path("fees/", FeeListView.as_view(), name="fee-list"),
+ path("fees/create/", FeeCreateView.as_view(), name="fee-create"),
+]
diff --git a/club/views.py b/club/views.py
new file mode 100644
index 0000000..a28cc04
--- /dev/null
+++ b/club/views.py
@@ -0,0 +1,82 @@
+from django.db.models import Q
+from django.utils import timezone
+from django.views.generic import CreateView, FormView, ListView, TemplateView
+from django.views.generic.detail import DetailView
+
+from club.forms import MemberForm, MembershipFeePeriodForm
+from club.models import Member, MemberRole, MembershipFeePeriod
+
+
+class ClubView(TemplateView):
+ template_name = "club/club.html"
+
+
+class MemberListView(ListView):
+ model = Member
+
+
+class MemberActiveListView(MemberListView):
+ def get_queryset(self):
+ today = timezone.now().date()
+ return Member.objects.filter(Q(active_until__isnull=True) | Q(active_until__gt=today))
+
+
+class MemberInactiveListView(MemberListView):
+ def get_queryset(self):
+ print(self.request.resolver_match.url_name)
+ print(self.request.resolver_match.view_name)
+ today = timezone.now().date()
+ return Member.objects.filter(active_until__lte=today)
+
+
+class MemberFeeOverdueListView(MemberListView):
+ def get_queryset(self):
+ last_period = MembershipFeePeriod.objects.last_period()
+ if last_period is None:
+ return Member.objects.none()
+ return Member.objects.exclude(id__in=last_period.members.all())
+
+
+class MemberBoardListView(MemberListView):
+ def get_queryset(self):
+ today = timezone.now().date()
+ member_ids = MemberRole.objects.filter(
+ Q(role=MemberRole.MemberRoleChoice.BOARD) & (Q(active_until__isnull=True) | Q(active_until__gt=today))
+ ).values_list("member")
+ return Member.objects.filter(id__in=member_ids)
+
+
+class MemberAuditCommitteeListView(MemberListView):
+ def get_queryset(self):
+ today = timezone.now().date()
+ member_ids = MemberRole.objects.filter(
+ Q(role=MemberRole.MemberRoleChoice.AUDIT_COMMITTEE)
+ & (Q(active_until__isnull=True) | Q(active_until__gt=today))
+ ).values_list("member")
+ return Member.objects.filter(id__in=member_ids)
+
+
+class MemberDetailView(DetailView):
+ model = Member
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+
+ print(context["object"].membershipfeeperiod_set.all())
+
+ return context
+
+
+class MemberCreateView(CreateView):
+ model = Member
+ form_class = MemberForm
+
+
+class FeeListView(ListView):
+ model = MembershipFeePeriod
+
+
+class FeeCreateView(FormView):
+ template_name = "club/membershipfeeperiod_form.html"
+ # model = MembershipFeePeriod
+ form_class = MembershipFeePeriodForm
diff --git a/kcc3/settings/base.py b/kcc3/settings/base.py
index 7a06834..d142b64 100644
--- a/kcc3/settings/base.py
+++ b/kcc3/settings/base.py
@@ -31,6 +31,7 @@
"django_hosts",
"rest_framework",
"rest_framework.authtoken",
+ "django_bootstrap5",
]
PROJECT_APPS = [
@@ -39,6 +40,7 @@
"players",
"chombos",
"yakumans",
+ "club",
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS
@@ -139,6 +141,20 @@
}
+# Bootstrap
+BOOTSTRAP5 = {
+ "css_url": {
+ "url": "https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css",
+ "integrity": "sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN",
+ "crossorigin": "anonymous",
+ },
+ "javascript_url": {
+ "url": "https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js",
+ "integrity": "sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL",
+ "crossorigin": "anonymous",
+ },
+}
+
# Custom settings
BADGE_IMAGE_MIN_RES = 96
BADGE_IMAGE_MAX_RES = 512
diff --git a/kcc3/urls.py b/kcc3/urls.py
index 6d543ab..4432729 100644
--- a/kcc3/urls.py
+++ b/kcc3/urls.py
@@ -21,6 +21,7 @@
import badges.urls
import chombos.urls
+import club.urls
import players.urls
from badges.viewsets import BadgeViewSet
from chombos.viewsets import ChomboViewSet
@@ -42,12 +43,13 @@
]
urlpatterns = [
- path("api/", include(router.urls)),
- path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
+ path("", include(badges.urls.global_urlpatterns)),
path("admin/", include(admin_urlpatterns)),
+ path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
+ path("api/", include(router.urls)),
path("badge-clients/", include(badgeclients_urlpatterns)),
- path("", include(badges.urls.global_urlpatterns)),
path("badges/", include(badges.urls.urlpatterns)),
+ path("club/", include(club.urls.urlpatterns)),
path("players/", include(players.urls.urlpatterns)),
*static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
]
diff --git a/poetry.lock b/poetry.lock
index 9e088c7..cd81613 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
[[package]]
name = "amqp"
@@ -311,6 +311,20 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"]
+[[package]]
+name = "django-bootstrap5"
+version = "23.3"
+description = "Bootstrap 5 for Django"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "django_bootstrap5-23.3-py3-none-any.whl", hash = "sha256:ca1bb2f40175ed1c1725f52f249a574bf629b33b5a0af3bec0eab1de6d72836c"},
+ {file = "django_bootstrap5-23.3.tar.gz", hash = "sha256:21e1956a8a819370decc5d365ce4f4207761cb065afec5a7df8c4c8c49507f2e"},
+]
+
+[package.dependencies]
+django = ">=3.2"
+
[[package]]
name = "django-celery-beat"
version = "2.5.0"
@@ -749,4 +763,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
-content-hash = "5ec0887b8e7534afc56038141ad90291504ea68a903243bde149c1b9cd5e21fd"
+content-hash = "a3cd9dd0ea2ca417875414492918b4055d159c69399868ebceab4713cea53741"
diff --git a/pyproject.toml b/pyproject.toml
index af47094..593ea87 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -19,6 +19,7 @@ psycopg2-binary = "2.9.9"
pytz = "2023.3.post1"
requests = "2.31.0"
sqlparse = "0.4.4"
+django-bootstrap5 = "^23.3"
[tool.ruff]
line-length = 120
@@ -28,19 +29,7 @@ exclude = [
]
[tool.ruff.lint]
-select = [
- "B",
- "C4",
- "DTZ",
- "E",
- "F",
- "G",
- "I",
- "PIE",
- "RUF",
- "UP",
- "W",
-]
+select = ["ALL"]
[build-system]
requires = ["poetry-core"]
diff --git a/templates/common/base.html b/templates/common/base.html
index d491b8a..e28a807 100644
--- a/templates/common/base.html
+++ b/templates/common/base.html
@@ -1,4 +1,5 @@
{% load static %}
+{% load django_bootstrap5 %}
@@ -7,7 +8,9 @@
{% block title %}{% endblock %} — Kraków Chombo Club
-
+ {% bootstrap_css %}
+
+
{% block head %}{% endblock %}
@@ -19,11 +22,8 @@
class="d-inline-block align-top" alt="">
Chombo!
-
-
+
+
@@ -34,15 +34,18 @@
Yakuman
+
+ Club
+
+{% bootstrap_messages %}
+
{% block body %}{% endblock %}
-
-
-
+{% bootstrap_javascript %}