feat(members): add group meeting checklist generation (#154)

Add an action to generate checklists for group meetings. These checklists can be used for documentation and for simplifying the check-in procedure in climbing gyms.

Co-authored-by: mariusrklein <47218379+mariusrklein@users.noreply.github.com>
Reviewed-on: #154
Co-authored-by: marius.klein <marius.klein@alpenverein-heidelberg.de>
Co-committed-by: marius.klein <marius.klein@alpenverein-heidelberg.de>
pull/174/head
marius.klein 4 months ago committed by Christian Merten
parent f58a7dc4b6
commit a75208b41c

@ -51,6 +51,14 @@ SEND_FROM_ASSOCIATION_EMAIL = get_var('misc', 'send_from_association_email', def
# domain for association email and generated urls
DOMAIN = get_var('misc', 'domain', default='example.org')
GROUP_CHECKLIST_N_WEEKS = get_var('misc', 'group_checklist_n_weeks', default=18)
GROUP_CHECKLIST_N_MEMBERS = get_var('misc', 'group_checklist_n_members', default=20)
GROUP_CHECKLIST_TEXT = get_var('misc', 'group_checklist_text',
default="""Anwesende Jugendleitende und Teilnehmende werden mit einem
Kreuz ($\\times$) markiert und die ausgefüllte Liste zum Anfang der Gruppenstunde an der Kasse
abgegeben. Zum Ende wird sie wieder abgeholt. Wenn die Punkte auf einer Karte fast aufgebraucht
sind, notiert die Kasse die verbliebenen Eintritte (3, 2, 1) unter dem Kreuz.""")
# finance
ALLOWANCE_PER_DAY = get_var('finance', 'allowance_per_day', default=22)

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-06 19:10+0200\n"
"POT-Creation-Date: 2025-04-15 23:05+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -276,6 +276,10 @@ msgstr "Kostenübersicht"
msgid "Generate group overview"
msgstr "Gruppenübersicht erstellen"
#: templates/admin/members/group/change_list.html
msgid "Generate group checklist"
msgstr "Gruppencheckliste erstellen"
#: templates/admin/members/member/change_form_object_tools.html
msgid "Invite as user"
msgstr "Als Kompassbenutzer*in einladen"

@ -30,6 +30,7 @@ from django.shortcuts import render
from django.core.exceptions import PermissionDenied, ValidationError
from .pdf import render_tex, fill_pdf_form, merge_pdfs, serve_pdf, render_docx
from .excel import generate_group_overview, generate_ljp_vbk
from .models import WEEKDAYS
from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin
@ -44,7 +45,7 @@ from .models import (Member, Group, Freizeit, MemberNoteList, NewMemberOnList, K
from finance.models import Statement, BillOnExcursionProxy
from mailer.mailutils import send as send_mail, get_echo_link
from django.conf import settings
from utils import get_member, RestrictedFileField
from utils import get_member, RestrictedFileField, mondays_until_nth
from schwifty import IBAN
from .pdf import media_path, media_dir
@ -195,7 +196,6 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
('join_date', 'leave_date'),
'comments',
'legal_guardians',
'dav_badge_no',
'active', 'echoed',
'user',
]
@ -213,8 +213,8 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
),
(_("Others"),
{
'fields': ['allergies', 'tetanus_vaccination', 'medication', 'photos_may_be_taken',
'may_cancel_appointment_independently']
'fields': ['dav_badge_no', 'ticket_no', 'allergies', 'tetanus_vaccination',
'medication', 'photos_may_be_taken','may_cancel_appointment_independently']
}
),
(_("Organizational"),
@ -854,6 +854,8 @@ class GroupAdmin(CommonAdminMixin, admin.ModelAdmin):
def action_view(self, request):
if "group_overview" in request.POST:
return self.group_overview(request)
elif "group_checklist" in request.POST:
return self.group_checklist(request)
def group_overview(self, request):
@ -868,6 +870,28 @@ class GroupAdmin(CommonAdminMixin, admin.ModelAdmin):
return response
def group_checklist(self, request):
if not request.user.has_perm('members.view_group'):
messages.error(request,
_("You are not allowed to create a group checklist."))
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
ensure_media_dir()
n_weeks = settings.GROUP_CHECKLIST_N_WEEKS
n_members = settings.GROUP_CHECKLIST_N_MEMBERS
context = {
'groups': self.model.objects.filter(show_website=True),
'settings': settings,
'week_range': range(n_weeks),
'member_range': range(n_members),
'dates': mondays_until_nth(n_weeks),
'weekdays': [long for i, long in WEEKDAYS],
'header_text': settings.GROUP_CHECKLIST_TEXT,
}
return render_tex(f"Gruppen-Checkliste", 'members/group_checklist.tex', context)
class ActivityCategoryAdmin(admin.ModelAdmin):
fields = ['name', 'ljp_category', 'description']

@ -301,6 +301,11 @@ msgid "You are not allowed to create a group overview."
msgstr ""
"Du hast nicht die notwendigen Rechte um eine Gruppenübersicht zu erstellen."
#: members/admin.py
msgid "You are not allowed to create a group checklist."
msgstr ""
"Du hast nicht die notwendigen Rechte um eine Gruppencheckliste zu erstellen."
#: members/admin.py
msgid "Difficulty"
msgstr "Schwierigkeit"
@ -688,6 +693,10 @@ msgstr "Hat Freikarte für Kletterhalle"
msgid "DAV badge number"
msgstr "DAV Mitgliedsnummer"
#: members/models.py
msgid "entrance ticket number"
msgstr "Eintrittskarten Nummer"
#: members/models.py
msgid "Knows how to swim"
msgstr "Kann schwimmen"

@ -0,0 +1,18 @@
# Generated by Django 4.2.20 on 2025-06-22 11:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('members', '0041_freizeit_crisis_intervention_list_sent_and_more'),
]
operations = [
migrations.AddField(
model_name='member',
name='ticket_no',
field=models.CharField(blank=True, default='', max_length=20, verbose_name='entrance ticket number'),
),
]

@ -105,6 +105,11 @@ class Group(models.Model):
verbose_name = _('group')
verbose_name_plural = _('groups')
@property
def sorted_members(self):
"""Returns the members of this group sorted by their last name."""
return self.member_set.all().order_by('lastname')
def has_time_info(self):
# return if the group has all relevant time slot information filled
return self.weekday and self.start_time and self.end_time
@ -318,6 +323,9 @@ class Member(Person):
has_key = models.BooleanField(_('Has key'), default=False)
has_free_ticket_gym = models.BooleanField(_('Has a free ticket for the climbing gym'), default=False)
dav_badge_no = models.CharField(max_length=20, verbose_name=_('DAV badge number'), default='', blank=True)
# use this to store a climbing gym customer or membership id, used to print on meeting checklists
ticket_no = models.CharField(max_length=20, verbose_name=_('entrance ticket number'), default='', blank=True)
swimming_badge = models.BooleanField(verbose_name=_('Knows how to swim'), default=False)
climbing_badge = models.CharField(max_length=100, verbose_name=_('Climbing badge'), default='', blank=True)
alpine_experience = models.TextField(verbose_name=_('Alpine experience'), default='', blank=True)
@ -380,6 +388,11 @@ class Member(Person):
"""Returning the whole place (plz + town)"""
return "{0} {1}".format(self.plz, self.town)
@property
def ticket_tag(self):
"""Returning the ticket number stripped of strings and spaces"""
return "{" + ''.join(re.findall(r'\d', self.ticket_no)) + "}"
@property
def iban_valid(self):
return IBAN(self.iban, allow_invalid=True).is_valid

@ -0,0 +1,65 @@
{% extends "members/tex_base.tex" %}
{% load static common tex_extras %}
{% block headline %}{% endblock %}
{% block contact %}{% endblock %}
{% block extra-preamble %}
\usepackage{rotating}
\usepackage[code=Code39,X=.48mm,ratio=3.5,H=0.5cm]{makebarcode}
\geometry{reset,margin=1cm, bottom=1.5cm}
\renewcommand{\arraystretch}{1}
{% endblock %}
{% block content %}
{% settings_value 'DEFAULT_STATIC_PATH' as static_root %}
{% for group in groups %}
\picpos{2.5cm}{16cm}{-0.4cm}{%
{{ static_root }}/general/img/dav_logo_sektion.png%
}
% HEADLINE
{\noindent\Large{Gruppenliste {{ group.name }} }}\\[1mm]
{% if group.has_time_info %} \noindent {{ weekdays|index:group.weekday|esc_all }}, {{ group.start_time }} - {{ group.end_time }} Uhr\\ {% endif %}
\noindent {{ header_text }}
\begin{table}[H]
\centering
%\begin{tabularx}{\textwidth}{lYY|l|l|l|l|l|l|l|l|l|l|l|l|l|l|l|l|l}
\begin{tabularx}{\textwidth}{X{% for i in week_range %}|l{% endfor%}}
\toprule
\textbf{Name} {% for i in week_range %}
& \begin{sideways} {{ dates|index:i|add:group.weekday|date_vs }} \end{sideways}
{% endfor %} \\
{% for j in member_range %}
{% with m=group.sorted_members|index:j %}
{% with codelength=m.ticket_tag|length %}
\midrule
\begin{tabular}{@{}l}
{% if codelength > 2 %}
\barcode[
X=\dimexpr 3.5mm / \numexpr {{ codelength }} \relax \relax
]{{ m.ticket_tag }}
{% else %}
\rule{0pt}{5mm}
{% endif %}
\vspace{-0.8ex} \\
{\small {{ j|plus:1 }} {% if m in group.leiters.all %}\textbf{JL}{% endif %}
{{ m.name|esc_all }} {% if codelength > 2 %} - {{ m.ticket_tag }}{% endif %}
\vspace{-3ex} }
\end{tabular}
{% for i in week_range %} & {% endfor %}\\
{% endwith %}
{% endwith %}
{% endfor %}
\bottomrule
\end{tabularx}
\end{table}
\clearpage
{% endfor %}
{% endblock content %}

@ -1,5 +1,6 @@
from django import template
from django.utils.safestring import mark_safe
from datetime import timedelta
register = template.Library()
@ -14,6 +15,12 @@ def checked_if_true(name, value):
def esc_all(val):
return mark_safe(str(val).replace('_', '\\_').replace('&', '\\&').replace('%', '\\%'))
@register.filter
def index(sequence, position):
try:
return sequence[position]
except (IndexError, TypeError):
return ''
@register.filter
def datetime_short(date):
@ -24,7 +31,22 @@ def datetime_short(date):
def date_short(date):
return date.strftime('%d.%m.%y')
@register.filter
def date_vs(date):
return date.strftime('%d.%m.')
@register.filter
def time_short(date):
return date.strftime('%H:%M')
@register.filter
def add(date, days):
if days:
return date + timedelta(days=days)
return date
@register.filter
def plus(num1, num2):
if num2:
return num1 + num2
return num1

@ -7,6 +7,7 @@
<form method="post" action="{% url 'admin:members_group_action' %}">
{% csrf_token %}
<input type="submit" name="group_overview" value="{% trans 'Generate group overview' %}">
<input type="submit" name="group_checklist" value="{% trans 'Generate group checklist' %}">
</form>
</li>
{{block.super}}

@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timedelta
from django.db import models
from django.utils import timezone
from django.core.exceptions import ValidationError
@ -88,3 +88,11 @@ def coming_midnight():
return timezone.datetime(year=base.year, month=base.month, day=base.day,
hour=0, minute=0, second=0, microsecond=0,
tzinfo=base.tzinfo)
def mondays_until_nth(n):
""" Returns a list of dates for the next n Mondays, starting from the next Monday.
This functions aids in the generation of weekly schedules or reports."""
today = datetime.today()
next_monday = today + timedelta(days=(7 - today.weekday()) % 7 or 7)
return [(next_monday + timedelta(weeks=i)).date() for i in range(n + 1)]

Loading…
Cancel
Save