From 0f229e14c453997b795bd17c2a9473f1b5563951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Mon, 27 Nov 2023 00:45:42 +0000 Subject: [PATCH] feat: add Club/Association management tools --- club/__init__.py | 0 club/admin.py | 29 +++++ club/apps.py | 6 + club/forms.py | 49 ++++++++ club/migrations/0001_initial.py | 58 +++++++++ club/migrations/__init__.py | 0 club/models.py | 96 +++++++++++++++ club/templates/club/club.html | 33 +++++ club/templates/club/member_detail.html | 115 ++++++++++++++++++ club/templates/club/member_form.html | 20 +++ club/templates/club/member_list.html | 29 +++++ club/templates/club/member_table.html | 69 +++++++++++ .../club/membershipfeeperiod_form.html | 20 +++ .../club/membershipfeeperiod_list.html | 18 +++ .../club/membershipfeeperiod_table.html | 37 ++++++ club/templatetags/__init__.py | 0 club/templatetags/breadcrumbs.py | 83 +++++++++++++ club/templatetags/kcc3.py | 26 ++++ club/tests.py | 1 + club/urls.py | 30 +++++ club/views.py | 82 +++++++++++++ kcc3/settings/base.py | 16 +++ kcc3/urls.py | 8 +- poetry.lock | 18 ++- pyproject.toml | 15 +-- templates/common/base.html | 21 ++-- templates/common/bool.html | 1 + templates/common/breadcrumbs.html | 13 ++ templates/common/nav_pill.html | 3 + 29 files changed, 869 insertions(+), 27 deletions(-) create mode 100644 club/__init__.py create mode 100644 club/admin.py create mode 100644 club/apps.py create mode 100644 club/forms.py create mode 100644 club/migrations/0001_initial.py create mode 100644 club/migrations/__init__.py create mode 100644 club/models.py create mode 100644 club/templates/club/club.html create mode 100644 club/templates/club/member_detail.html create mode 100644 club/templates/club/member_form.html create mode 100644 club/templates/club/member_list.html create mode 100644 club/templates/club/member_table.html create mode 100644 club/templates/club/membershipfeeperiod_form.html create mode 100644 club/templates/club/membershipfeeperiod_list.html create mode 100644 club/templates/club/membershipfeeperiod_table.html create mode 100644 club/templatetags/__init__.py create mode 100644 club/templatetags/breadcrumbs.py create mode 100644 club/templatetags/kcc3.py create mode 100644 club/tests.py create mode 100644 club/urls.py create mode 100644 club/views.py create mode 100644 templates/common/bool.html create mode 100644 templates/common/breadcrumbs.html create mode 100644 templates/common/nav_pill.html 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 %} + + {% endfor %} + + + + + {% for fee in member.relevant_fees %} + + {% endfor %} + + +
{{ fee.name }}
+ {% if fee in member.paid_fees %} + {% include "common/bool.html" with value=True %} + {% else %} + {% include "common/bool.html" with value=False %} + {% endif %} +
+ +

Roles

+ + + + + + + + + + {% for role in member.memberrole_set.all %} + + + + + + {% endfor %} + + + +
Role nameActive sinceActive until
{{ role.name }}{{ role.active_since|dateformat }}{{ role.active_until|dateformat }}
+
+{% 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

+ +
+ {% csrf_token %} + {% bootstrap_form form %} + + {% bootstrap_button "Create" button_type="submit" button_class="btn-primary" %} +
+
+{% 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 '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 %} +
+ + + + + + + + + + + + + + + + {% for member in members %} + + + + + + + + + + + + {% endfor %} + +
First nameLast nameNicknameWorkspace E-mailPersonal E-mailFee paidRoleMember sinceMember until
+ + {{ member.first_name|placeholder }} + + + + {{ member.last_name|placeholder }} + + + + {{ member.nickname|placeholder }} + + + {% if member.workspace_email %} + + {{ member.workspace_email|placeholder }} + + {% else %} + {% placeholder %} + {% endif %} + + {% if member.workspace_email %} + + {{ member.personal_email|placeholder }} + + {% else %} + {% placeholder %} + {% endif %} + + {% include "common/bool.html" with value=member.current_fee_paid %} + + {{ member.current_role }} + + {{ member.active_since|dateformat }} + + {{ member.active_until|dateformat }} +
+
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

+ +
+ {% csrf_token %} + {% bootstrap_form form %} + + {% bootstrap_button "Create" button_type="submit" button_class="btn-primary" %} +
+
+{% 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 %} +
+ + + + + + + + + + + + {% for fee in fees %} + + + + + + + + {% endfor %} + +
NameFeeActive sinceActive untilMembers paid
+ + {{ fee.name }} + + + {{ fee.fee|floatformat:2 }} + + {{ fee.active_since|dateformat }} + + {{ fee.active_until|dateformat }} + + {{ fee.members.count }} +
+
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! - +{% bootstrap_messages %} + {% block body %}{% endblock %} - - - +{% bootstrap_javascript %} diff --git a/templates/common/bool.html b/templates/common/bool.html new file mode 100644 index 0000000..6027838 --- /dev/null +++ b/templates/common/bool.html @@ -0,0 +1 @@ +{% if value %}{% else %}{% endif %} diff --git a/templates/common/breadcrumbs.html b/templates/common/breadcrumbs.html new file mode 100644 index 0000000..e06dd4d --- /dev/null +++ b/templates/common/breadcrumbs.html @@ -0,0 +1,13 @@ + diff --git a/templates/common/nav_pill.html b/templates/common/nav_pill.html new file mode 100644 index 0000000..93f6cf8 --- /dev/null +++ b/templates/common/nav_pill.html @@ -0,0 +1,3 @@ +