From 262895e2f19dfbf29e898b6b5dfd274cc339d035 Mon Sep 17 00:00:00 2001 From: mariusrklein <47218379+mariusrklein@users.noreply.github.com> Date: Thu, 30 Jan 2025 12:23:47 +0100 Subject: [PATCH 1/6] added group overview export --- jdav_web/members/admin.py | 90 +++++++++++++++++++ .../admin/members/group/change_list.html | 15 ++++ 2 files changed, 105 insertions(+) create mode 100644 jdav_web/templates/admin/members/group/change_list.html diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index c08a05a..e979b05 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -7,6 +7,7 @@ import time import unicodedata import random import string +import xlsxwriter from functools import partial, update_wrapper from django.forms.models import BaseInlineFormSet @@ -28,6 +29,7 @@ from django.forms import Textarea, RadioSelect, TypedChoiceField, CheckboxInput from django.shortcuts import render from django.core.exceptions import PermissionDenied, ValidationError from .pdf import render_tex, fill_pdf_form, merge_pdfs, serve_pdf +from .models import WEEKDAYS from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin @@ -44,6 +46,7 @@ from mailer.mailutils import send as send_mail, get_echo_link from django.conf import settings from utils import get_member, RestrictedFileField from schwifty import IBAN +from .pdf import media_path #from easy_select2 import apply_select2 @@ -791,6 +794,7 @@ class GroupAdminForm(forms.ModelForm): self.fields['leiters'].queryset = Member.objects.filter(group__name='Jugendleiter') + class GroupAdmin(CommonAdminMixin, admin.ModelAdmin): fields = ['name', 'description', 'year_from', 'year_to', 'leiters', 'contact_email', 'show_website', 'weekday', ('start_time', 'end_time')] @@ -798,6 +802,92 @@ class GroupAdmin(CommonAdminMixin, admin.ModelAdmin): list_display = ('name', 'year_from', 'year_to') inlines = [RegistrationPasswordInline, PermissionOnGroupInline] search_fields = ('name',) + + def get_urls(self): + urls = super().get_urls() + + def wrap(view): + def wrapper(*args, **kwargs): + return self.admin_site.admin_view(view)(*args, **kwargs) + + wrapper.model_admin = self + return update_wrapper(wrapper, view) + + custom_urls = [ + path('action/', self.action_view, name='members_group_action'), + ] + return custom_urls + urls + + def action_view(self, request): + if "group_overview" in request.POST: + return self.group_overview(request) + + def group_overview(self, request): + + if not request.user.has_perm('members.view_group'): + messages.error(request, + _("You are not allowed to create a group overview.")) + return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) + + today = f"{datetime.today():%d.%m.%Y}" + filename = f"gruppenuebersicht_jdav_{settings.SEKTION}_{today}.xlsx" + workbook = xlsxwriter.Workbook(media_path(filename)) + default = workbook.add_format({'text_wrap' : True, 'border': 1}) + bold = workbook.add_format({'bold': True, 'border': 1}) + title = workbook.add_format({'bold': True, 'font_size': 16, 'align': 'center'}) + right = workbook.add_format({'bold': True, 'align': 'right'}) + worksheet = workbook.add_worksheet() + + worksheet.merge_range(0, 0, 0, 6, _(f"group overview JDAV {settings.SEKTION}"), title) + row = 1 + worksheet.write(row, 0, "Gruppe", bold) + worksheet.write(row, 1, "Wochentag", bold) + worksheet.write(row, 2, "Uhrzeit", bold) + worksheet.write(row, 3, "Altersgruppe", bold) + worksheet.write(row, 4, "TN", bold) + worksheet.write(row, 5, "JL", bold) + worksheet.write(row, 6, "Jugendleiter*innen", bold) + + for group in self.model.objects.all(): + # only official youth groups are on the website + if not group.show_website: + continue + + row = row + 1 + wd = f"{WEEKDAYS[group.weekday][1]}" if group.weekday else 'kein Wochentag' + times = f"{group.start_time:%H:%M} - {group.end_time:%H:%M}" if group.start_time and group.end_time else 'keine Zeiten' + yl_count = len([member for member in group.member_set.all() if member in group.leiters.all()]) + tn_count = group.member_set.count() - yl_count + members = f"JG {group.year_from} - {group.year_to}" + leaders = f"{', '.join([yl.name for yl in group.leiters.all()])}" + + worksheet.write(row, 0, group.name, default) + worksheet.write(row, 1, wd, default) + worksheet.write(row, 2, times, default) + worksheet.write(row, 3, members, default) + worksheet.write(row, 4, tn_count, default) + worksheet.write(row, 5, yl_count, default) + worksheet.write(row, 6, leaders, default) + + worksheet.write(row+2, 6, f"Stand: {today}", right) + # set column width + worksheet.set_column_pixels(0, 0, 100) + worksheet.set_column_pixels(1, 1, 80) + worksheet.set_column_pixels(2, 2, 90) + worksheet.set_column_pixels(3, 3, 120) + worksheet.set_column_pixels(4, 4, 20) + worksheet.set_column_pixels(5, 5, 20) + worksheet.set_column_pixels(6, 6, 140) + workbook.close() + + with open(media_path(filename), 'rb') as xls: + response = HttpResponse(FileWrapper(xls)) + response['Content-Type'] = 'application/xlsx' + response['Content-Disposition'] = 'attachment; filename='+filename + + return response + + class ActivityCategoryAdmin(admin.ModelAdmin): diff --git a/jdav_web/templates/admin/members/group/change_list.html b/jdav_web/templates/admin/members/group/change_list.html new file mode 100644 index 0000000..e688e94 --- /dev/null +++ b/jdav_web/templates/admin/members/group/change_list.html @@ -0,0 +1,15 @@ +{% extends "admin/change_list.html" %} +{% load i18n admin_urls %} + +{% block object-tools-items %} + +
  • +
    + {% csrf_token %} + +
    +
  • +{{block.super}} + +{% endblock %} + -- 2.38.4 From 9d9e3cee79a3c339ec18f1472d9c25c183acc04d Mon Sep 17 00:00:00 2001 From: mariusrklein <47218379+mariusrklein@users.noreply.github.com> Date: Sat, 1 Feb 2025 11:49:57 +0100 Subject: [PATCH 2/6] fixed error due to translation --- jdav_web/members/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index e979b05..e622a33 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -838,7 +838,7 @@ class GroupAdmin(CommonAdminMixin, admin.ModelAdmin): right = workbook.add_format({'bold': True, 'align': 'right'}) worksheet = workbook.add_worksheet() - worksheet.merge_range(0, 0, 0, 6, _(f"group overview JDAV {settings.SEKTION}"), title) + worksheet.merge_range(0, 0, 0, 6, f"Gruppenübersicht JDAV {settings.SEKTION}", title) row = 1 worksheet.write(row, 0, "Gruppe", bold) worksheet.write(row, 1, "Wochentag", bold) -- 2.38.4 From 1a0a465a8b5f6c78c13ab7d35214fe768c1d28e3 Mon Sep 17 00:00:00 2001 From: mariusrklein <47218379+mariusrklein@users.noreply.github.com> Date: Sat, 1 Feb 2025 14:55:22 +0100 Subject: [PATCH 3/6] added translation --- jdav_web/locale/de/LC_MESSAGES/django.po | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/jdav_web/locale/de/LC_MESSAGES/django.po b/jdav_web/locale/de/LC_MESSAGES/django.po index 7dd4ed7..cafea14 100644 --- a/jdav_web/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-01 21:48+0100\n" +"POT-Creation-Date: 2025-02-01 14:54+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -268,6 +268,10 @@ msgstr "Hinweise für Jugendleiter*innen erstellen" msgid "Finance overview" msgstr "Kostenübersicht" +#: templates/admin/members/group/change_list.html +msgid "Generate group overview" +msgstr "Gruppenübersicht erstellen" + #: templates/admin/members/member/change_form_object_tools.html msgid "Invite as user" msgstr "Als Kompassbenutzer*in einladen" -- 2.38.4 From fc940793653339fc1e4683a05c93714825254586 Mon Sep 17 00:00:00 2001 From: mariusrklein <47218379+mariusrklein@users.noreply.github.com> Date: Sat, 1 Feb 2025 18:19:50 +0100 Subject: [PATCH 4/6] members/groups: create media dir if not exists --- jdav_web/members/admin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index 5029f3d..15d8940 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -46,7 +46,7 @@ from mailer.mailutils import send as send_mail, get_echo_link from django.conf import settings from utils import get_member, RestrictedFileField from schwifty import IBAN -from .pdf import media_path +from .pdf import media_path, media_dir #from easy_select2 import apply_select2 @@ -829,6 +829,9 @@ class GroupAdmin(CommonAdminMixin, admin.ModelAdmin): _("You are not allowed to create a group overview.")) return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) + if not os.path.exists(media_dir()): + os.makedirs(media_dir()) + today = f"{datetime.today():%d.%m.%Y}" filename = f"gruppenuebersicht_jdav_{settings.SEKTION}_{today}.xlsx" workbook = xlsxwriter.Workbook(media_path(filename)) -- 2.38.4 From c3524575877091ad747c021a42a0973a6dafeb32 Mon Sep 17 00:00:00 2001 From: mariusrklein <47218379+mariusrklein@users.noreply.github.com> Date: Sat, 1 Feb 2025 18:55:42 +0100 Subject: [PATCH 5/6] members/groups: factored out xlsxwriter recipe --- jdav_web/members/admin.py | 65 +++--------------------------------- jdav_web/members/excel.py | 69 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 60 deletions(-) create mode 100644 jdav_web/members/excel.py diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index 15d8940..b417ffc 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -7,7 +7,6 @@ import time import unicodedata import random import string -import xlsxwriter from functools import partial, update_wrapper from django.forms.models import BaseInlineFormSet @@ -21,6 +20,7 @@ from django import forms from django.contrib import admin, messages from django.contrib.admin import DateFieldListFilter from django.contrib.contenttypes.admin import GenericTabularInline +from contrib.media import serve_media, ensure_media_dir from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from django.db.models import TextField, ManyToManyField, ForeignKey, Count,\ @@ -29,7 +29,7 @@ from django.forms import Textarea, RadioSelect, TypedChoiceField, CheckboxInput from django.shortcuts import render from django.core.exceptions import PermissionDenied, ValidationError from .pdf import render_tex, fill_pdf_form, merge_pdfs, serve_pdf -from .models import WEEKDAYS +from .excel import generate_group_overview from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin @@ -829,64 +829,9 @@ class GroupAdmin(CommonAdminMixin, admin.ModelAdmin): _("You are not allowed to create a group overview.")) return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) - if not os.path.exists(media_dir()): - os.makedirs(media_dir()) - - today = f"{datetime.today():%d.%m.%Y}" - filename = f"gruppenuebersicht_jdav_{settings.SEKTION}_{today}.xlsx" - workbook = xlsxwriter.Workbook(media_path(filename)) - default = workbook.add_format({'text_wrap' : True, 'border': 1}) - bold = workbook.add_format({'bold': True, 'border': 1}) - title = workbook.add_format({'bold': True, 'font_size': 16, 'align': 'center'}) - right = workbook.add_format({'bold': True, 'align': 'right'}) - worksheet = workbook.add_worksheet() - - worksheet.merge_range(0, 0, 0, 6, f"Gruppenübersicht JDAV {settings.SEKTION}", title) - row = 1 - worksheet.write(row, 0, "Gruppe", bold) - worksheet.write(row, 1, "Wochentag", bold) - worksheet.write(row, 2, "Uhrzeit", bold) - worksheet.write(row, 3, "Altersgruppe", bold) - worksheet.write(row, 4, "TN", bold) - worksheet.write(row, 5, "JL", bold) - worksheet.write(row, 6, "Jugendleiter*innen", bold) - - for group in self.model.objects.all(): - # only official youth groups are on the website - if not group.show_website: - continue - - row = row + 1 - wd = f"{WEEKDAYS[group.weekday][1]}" if group.weekday else 'kein Wochentag' - times = f"{group.start_time:%H:%M} - {group.end_time:%H:%M}" if group.start_time and group.end_time else 'keine Zeiten' - yl_count = len([member for member in group.member_set.all() if member in group.leiters.all()]) - tn_count = group.member_set.count() - yl_count - members = f"JG {group.year_from} - {group.year_to}" - leaders = f"{', '.join([yl.name for yl in group.leiters.all()])}" - - worksheet.write(row, 0, group.name, default) - worksheet.write(row, 1, wd, default) - worksheet.write(row, 2, times, default) - worksheet.write(row, 3, members, default) - worksheet.write(row, 4, tn_count, default) - worksheet.write(row, 5, yl_count, default) - worksheet.write(row, 6, leaders, default) - - worksheet.write(row+2, 6, f"Stand: {today}", right) - # set column width - worksheet.set_column_pixels(0, 0, 100) - worksheet.set_column_pixels(1, 1, 80) - worksheet.set_column_pixels(2, 2, 90) - worksheet.set_column_pixels(3, 3, 120) - worksheet.set_column_pixels(4, 4, 20) - worksheet.set_column_pixels(5, 5, 20) - worksheet.set_column_pixels(6, 6, 140) - workbook.close() - - with open(media_path(filename), 'rb') as xls: - response = HttpResponse(FileWrapper(xls)) - response['Content-Type'] = 'application/xlsx' - response['Content-Disposition'] = 'attachment; filename='+filename + ensure_media_dir() + filename = generate_group_overview(all_groups=self.model.objects.all()) + response = serve_media(filename=filename, content_type='application/xlsx') return response diff --git a/jdav_web/members/excel.py b/jdav_web/members/excel.py new file mode 100644 index 0000000..22d4b3f --- /dev/null +++ b/jdav_web/members/excel.py @@ -0,0 +1,69 @@ +from datetime import datetime +import os +import xlsxwriter +from django.conf import settings +from contrib.media import media_path +from .models import WEEKDAYS + +def generate_group_overview(all_groups, limit_to_public = True): + """ + Creates an Excel Sheet with an overview of all the groups, their dates, times, age range and + number of members, etc. + + arguments: + limit_to_public (optional, default is True): If False, all groups are returned in the overview, + including technical ones. If True, only groups with the flag "show_on_website" are returned. + + """ + today = f"{datetime.today():%d.%m.%Y}" + filename = f"gruppenuebersicht_jdav_{settings.SEKTION}_{today}.xlsx" + workbook = xlsxwriter.Workbook(media_path(filename)) + default = workbook.add_format({'text_wrap' : True, 'border': 1}) + bold = workbook.add_format({'bold': True, 'border': 1}) + title = workbook.add_format({'bold': True, 'font_size': 16, 'align': 'center'}) + right = workbook.add_format({'bold': True, 'align': 'right'}) + worksheet = workbook.add_worksheet() + + worksheet.merge_range(0, 0, 0, 6, f"Gruppenübersicht JDAV {settings.SEKTION}", title) + row = 1 + worksheet.write(row, 0, "Gruppe", bold) + worksheet.write(row, 1, "Wochentag", bold) + worksheet.write(row, 2, "Uhrzeit", bold) + worksheet.write(row, 3, "Altersgruppe", bold) + worksheet.write(row, 4, "TN", bold) + worksheet.write(row, 5, "JL", bold) + worksheet.write(row, 6, "Jugendleiter*innen", bold) + + for group in all_groups: + # choose if only official youth groups on the website are shown + if limit_to_public and not group.show_website: + continue + + row = row + 1 + wd = f"{WEEKDAYS[group.weekday][1]}" if group.weekday else 'kein Wochentag' + times = f"{group.start_time:%H:%M} - {group.end_time:%H:%M}" if group.start_time and group.end_time else 'keine Zeiten' + yl_count = len([member for member in group.member_set.all() if member in group.leiters.all()]) + tn_count = group.member_set.count() - yl_count + members = f"JG {group.year_from} - {group.year_to}" + leaders = f"{', '.join([yl.name for yl in group.leiters.all()])}" + + worksheet.write(row, 0, group.name, default) + worksheet.write(row, 1, wd, default) + worksheet.write(row, 2, times, default) + worksheet.write(row, 3, members, default) + worksheet.write(row, 4, tn_count, default) + worksheet.write(row, 5, yl_count, default) + worksheet.write(row, 6, leaders, default) + + worksheet.write(row+2, 6, f"Stand: {today}", right) + # set column width + worksheet.set_column_pixels(0, 0, 100) + worksheet.set_column_pixels(1, 1, 80) + worksheet.set_column_pixels(2, 2, 90) + worksheet.set_column_pixels(3, 3, 120) + worksheet.set_column_pixels(4, 4, 20) + worksheet.set_column_pixels(5, 5, 20) + worksheet.set_column_pixels(6, 6, 140) + workbook.close() + + return filename -- 2.38.4 From 092e0e97191485cfa0439b4ed8e09898789f6c9b Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Sat, 1 Feb 2025 19:10:11 +0100 Subject: [PATCH 6/6] fix minor whitespace issues --- jdav_web/members/admin.py | 14 ++++++-------- jdav_web/members/excel.py | 12 ++++++------ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index b417ffc..2bfbe9d 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -802,40 +802,38 @@ class GroupAdmin(CommonAdminMixin, admin.ModelAdmin): list_display = ('name', 'year_from', 'year_to') inlines = [RegistrationPasswordInline, PermissionOnGroupInline] search_fields = ('name',) - + def get_urls(self): urls = super().get_urls() - + def wrap(view): def wrapper(*args, **kwargs): return self.admin_site.admin_view(view)(*args, **kwargs) wrapper.model_admin = self return update_wrapper(wrapper, view) - + custom_urls = [ path('action/', self.action_view, name='members_group_action'), ] return custom_urls + urls - + def action_view(self, request): if "group_overview" in request.POST: return self.group_overview(request) def group_overview(self, request): - + if not request.user.has_perm('members.view_group'): messages.error(request, _("You are not allowed to create a group overview.")) return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) - + ensure_media_dir() filename = generate_group_overview(all_groups=self.model.objects.all()) response = serve_media(filename=filename, content_type='application/xlsx') return response - - class ActivityCategoryAdmin(admin.ModelAdmin): diff --git a/jdav_web/members/excel.py b/jdav_web/members/excel.py index 22d4b3f..6fa8ff7 100644 --- a/jdav_web/members/excel.py +++ b/jdav_web/members/excel.py @@ -7,11 +7,11 @@ from .models import WEEKDAYS def generate_group_overview(all_groups, limit_to_public = True): """ - Creates an Excel Sheet with an overview of all the groups, their dates, times, age range and + Creates an Excel Sheet with an overview of all the groups, their dates, times, age range and number of members, etc. - + arguments: - limit_to_public (optional, default is True): If False, all groups are returned in the overview, + limit_to_public (optional, default is True): If False, all groups are returned in the overview, including technical ones. If True, only groups with the flag "show_on_website" are returned. """ @@ -23,7 +23,7 @@ def generate_group_overview(all_groups, limit_to_public = True): title = workbook.add_format({'bold': True, 'font_size': 16, 'align': 'center'}) right = workbook.add_format({'bold': True, 'align': 'right'}) worksheet = workbook.add_worksheet() - + worksheet.merge_range(0, 0, 0, 6, f"Gruppenübersicht JDAV {settings.SEKTION}", title) row = 1 worksheet.write(row, 0, "Gruppe", bold) @@ -46,7 +46,7 @@ def generate_group_overview(all_groups, limit_to_public = True): tn_count = group.member_set.count() - yl_count members = f"JG {group.year_from} - {group.year_to}" leaders = f"{', '.join([yl.name for yl in group.leiters.all()])}" - + worksheet.write(row, 0, group.name, default) worksheet.write(row, 1, wd, default) worksheet.write(row, 2, times, default) @@ -54,7 +54,7 @@ def generate_group_overview(all_groups, limit_to_public = True): worksheet.write(row, 4, tn_count, default) worksheet.write(row, 5, yl_count, default) worksheet.write(row, 6, leaders, default) - + worksheet.write(row+2, 6, f"Stand: {today}", right) # set column width worksheet.set_column_pixels(0, 0, 100) -- 2.38.4