Compare commits

...

21 Commits

Author SHA1 Message Date
marius.klein a8d6503b60 Merge branch 'main' into MK/meeting_checklist 4 months ago
mariusrklein eb3e45f016 fix missing time info and variable barcode length 4 months ago
Christian Merten 1f4cb41256
fix import 4 months ago
Christian Merten 7337cec025
fix migration 4 months ago
mariusrklein 218df6aaab Merge remote-tracking branch 'origin/main' into MK/meeting_checklist 4 months ago
mariusrklein 5978e6ab5b fix(members): explain ticket_no field and move it+badge_no to others tab 5 months ago
mariusrklein eb2cd6c68c fix(members): move util function, remove blank space 5 months ago
mariusrklein b0fb7bcfce fix: escape backslash in default text 6 months ago
mariusrklein 01b52feafe feat: abstracted parameters for group checklist into settings.toml 6 months ago
mariusrklein ef7dac6d75 feat (members): change list layout to accomodate large ticket_no barcodes 6 months ago
mariusrklein 8192297c51 feat (members): add entry ticket field in members model 6 months ago
mariusrklein 370b212597 reformat barcode 8 months ago
mariusrklein 757408cfd9 Merge branch 'main' into MK/meeting_checklist 8 months ago
mariusrklein c3c207f64d test 8 months ago
mariusrklein 6388ccd15b added badge no barcodes 8 months ago
mariusrklein 83ffd83830 changed layout to always 25 rows 8 months ago
mariusrklein e16765e7af fixed margins, table layout 8 months ago
mariusrklein eb0769b121 fix page number 8 months ago
mariusrklein f1398584cd fix: sort members 8 months ago
mariusrklein 649e35e26c fix: filter for published groups 8 months ago
mariusrklein 546f2a2dd4 feat: add group checklist 8 months ago

@ -51,6 +51,14 @@ SEND_FROM_ASSOCIATION_EMAIL = get_var('misc', 'send_from_association_email', def
# domain for association email and generated urls # domain for association email and generated urls
DOMAIN = get_var('misc', 'domain', default='example.org') 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 # finance
ALLOWANCE_PER_DAY = get_var('finance', 'allowance_per_day', default=22) ALLOWANCE_PER_DAY = get_var('finance', 'allowance_per_day', default=22)

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

@ -30,6 +30,7 @@ from django.shortcuts import render
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied, ValidationError
from .pdf import render_tex, fill_pdf_form, merge_pdfs, serve_pdf, render_docx from .pdf import render_tex, fill_pdf_form, merge_pdfs, serve_pdf, render_docx
from .excel import generate_group_overview, generate_ljp_vbk from .excel import generate_group_overview, generate_ljp_vbk
from .models import WEEKDAYS
from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin 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 finance.models import Statement, BillOnExcursionProxy
from mailer.mailutils import send as send_mail, get_echo_link from mailer.mailutils import send as send_mail, get_echo_link
from django.conf import settings 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 schwifty import IBAN
from .pdf import media_path, media_dir from .pdf import media_path, media_dir
@ -195,7 +196,6 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
('join_date', 'leave_date'), ('join_date', 'leave_date'),
'comments', 'comments',
'legal_guardians', 'legal_guardians',
'dav_badge_no',
'active', 'echoed', 'active', 'echoed',
'user', 'user',
] ]
@ -213,8 +213,8 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
), ),
(_("Others"), (_("Others"),
{ {
'fields': ['allergies', 'tetanus_vaccination', 'medication', 'photos_may_be_taken', 'fields': ['dav_badge_no', 'ticket_no', 'allergies', 'tetanus_vaccination',
'may_cancel_appointment_independently'] 'medication', 'photos_may_be_taken','may_cancel_appointment_independently']
} }
), ),
(_("Organizational"), (_("Organizational"),
@ -854,6 +854,8 @@ class GroupAdmin(CommonAdminMixin, admin.ModelAdmin):
def action_view(self, request): def action_view(self, request):
if "group_overview" in request.POST: if "group_overview" in request.POST:
return self.group_overview(request) return self.group_overview(request)
elif "group_checklist" in request.POST:
return self.group_checklist(request)
def group_overview(self, request): def group_overview(self, request):
@ -868,6 +870,28 @@ class GroupAdmin(CommonAdminMixin, admin.ModelAdmin):
return response 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): class ActivityCategoryAdmin(admin.ModelAdmin):
fields = ['name', 'ljp_category', 'description'] fields = ['name', 'ljp_category', 'description']

@ -301,6 +301,11 @@ msgid "You are not allowed to create a group overview."
msgstr "" msgstr ""
"Du hast nicht die notwendigen Rechte um eine Gruppenübersicht zu erstellen." "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 #: members/admin.py
msgid "Difficulty" msgid "Difficulty"
msgstr "Schwierigkeit" msgstr "Schwierigkeit"
@ -688,6 +693,10 @@ msgstr "Hat Freikarte für Kletterhalle"
msgid "DAV badge number" msgid "DAV badge number"
msgstr "DAV Mitgliedsnummer" msgstr "DAV Mitgliedsnummer"
#: members/models.py
msgid "entrance ticket number"
msgstr "Eintrittskarten Nummer"
#: members/models.py #: members/models.py
msgid "Knows how to swim" msgid "Knows how to swim"
msgstr "Kann schwimmen" 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 = _('group')
verbose_name_plural = _('groups') 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): def has_time_info(self):
# return if the group has all relevant time slot information filled # return if the group has all relevant time slot information filled
return self.weekday and self.start_time and self.end_time 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_key = models.BooleanField(_('Has key'), default=False)
has_free_ticket_gym = models.BooleanField(_('Has a free ticket for the climbing gym'), 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) 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) 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) 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) alpine_experience = models.TextField(verbose_name=_('Alpine experience'), default='', blank=True)
@ -380,6 +388,11 @@ class Member(Person):
"""Returning the whole place (plz + town)""" """Returning the whole place (plz + town)"""
return "{0} {1}".format(self.plz, self.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 @property
def iban_valid(self): def iban_valid(self):
return IBAN(self.iban, allow_invalid=True).is_valid 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 import template
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from datetime import timedelta
register = template.Library() register = template.Library()
@ -14,6 +15,12 @@ def checked_if_true(name, value):
def esc_all(val): def esc_all(val):
return mark_safe(str(val).replace('_', '\\_').replace('&', '\\&').replace('%', '\\%')) return mark_safe(str(val).replace('_', '\\_').replace('&', '\\&').replace('%', '\\%'))
@register.filter
def index(sequence, position):
try:
return sequence[position]
except (IndexError, TypeError):
return ''
@register.filter @register.filter
def datetime_short(date): def datetime_short(date):
@ -24,7 +31,22 @@ def datetime_short(date):
def date_short(date): def date_short(date):
return date.strftime('%d.%m.%y') return date.strftime('%d.%m.%y')
@register.filter
def date_vs(date):
return date.strftime('%d.%m.')
@register.filter @register.filter
def time_short(date): def time_short(date):
return date.strftime('%H:%M') 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' %}"> <form method="post" action="{% url 'admin:members_group_action' %}">
{% csrf_token %} {% csrf_token %}
<input type="submit" name="group_overview" value="{% trans 'Generate group overview' %}"> <input type="submit" name="group_overview" value="{% trans 'Generate group overview' %}">
<input type="submit" name="group_checklist" value="{% trans 'Generate group checklist' %}">
</form> </form>
</li> </li>
{{block.super}} {{block.super}}

@ -1,4 +1,4 @@
from datetime import datetime from datetime import datetime, timedelta
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.core.exceptions import ValidationError 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, return timezone.datetime(year=base.year, month=base.month, day=base.day,
hour=0, minute=0, second=0, microsecond=0, hour=0, minute=0, second=0, microsecond=0,
tzinfo=base.tzinfo) 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