From 46320d6dce3e92a04b23eebdfe928ec741dfadc5 Mon Sep 17 00:00:00 2001 From: erichhasl Date: Mon, 13 Mar 2017 18:59:38 +0100 Subject: [PATCH 1/6] evaluate skills based on activities --- jdav_web/members/admin.py | 18 ++++++++- jdav_web/members/models.py | 37 ++++++++++++++++--- .../templates/members/change_member.html | 20 ++++++++++ 3 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 jdav_web/members/templates/members/change_member.html diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index a9dd7a9..d600d6b 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -16,7 +16,7 @@ from django.forms import Textarea from django.shortcuts import render from .models import (Member, Group, MemberList, MemberOnList, Klettertreff, - KlettertreffAttendee) + KlettertreffAttendee, ActivityCategory) # Register your models here. @@ -28,18 +28,31 @@ class MemberAdmin(admin.ModelAdmin): formfield_overrides = { ManyToManyField: {'widget': forms.CheckboxSelectMultiple} } + change_form_template = "members/change_member.html" + + def change_view(self, request, object_id, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context['qualities'] =\ + Member.objects.get(pk=object_id).get_skills() + return super(MemberAdmin, self).change_view(request, object_id, + form_url=form_url, + extra_context=extra_context) class GroupAdmin(admin.ModelAdmin): fields = ['name', 'min_age'] list_display = ('name', 'min_age') + +class ActivityCategoryAdmin(admin.ModelAdmin): + fields = ['name', 'description'] + + class MemberListAdminForm(forms.ModelForm): class Meta: model = MemberList exclude = ['add_member'] - def __init__(self, *args, **kwargs): super(MemberListAdminForm, self).__init__(*args, **kwargs) self.fields['jugendleiter'].queryset = Member.objects.filter(group__name='Jugendleiter') @@ -217,3 +230,4 @@ admin.site.register(Member, MemberAdmin) admin.site.register(Group, GroupAdmin) admin.site.register(MemberList, MemberListAdmin) admin.site.register(Klettertreff, KlettertreffAdmin) +admin.site.register(ActivityCategory, ActivityCategoryAdmin) diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py index b103e70..5e3e921 100644 --- a/jdav_web/members/models.py +++ b/jdav_web/members/models.py @@ -7,6 +7,22 @@ from django.utils import timezone from multiselectfield import MultiSelectField + +class ActivityCategory(models.Model): + """ + Describes one kind of activity + """ + name = models.CharField(max_length=20, verbose_name=_('Name')) + description = models.TextField(_('Description')) + + def __str__(self): + return self.name + + class Meta: + verbose_name = _('Activity') + verbose_name_plural = _('Activities') + + class Group(models.Model): """ Represents one group of the association @@ -85,22 +101,32 @@ class Member(models.Model): verbose_name = _('member') verbose_name_plural = _('members') + def get_skills(self): + # get skills by summing up all the activities taken part in + skills = {} + for kind in ActivityCategory.objects.all(): + lists = MemberList.objects.filter(activity=kind, + memberonlist__member=self) + skills[kind.name] = len(lists) + return skills + class MemberList(models.Model): """Lets the user create a list of members in pdf format. """ name = models.CharField(verbose_name='Activity', default='', max_length=50) - place = models.CharField(verbose_name=_('Place'), default='', max_length=50) - destination = models.CharField(verbose_name=_('Destination (optional)'), default='', max_length=50, blank=True) + place = models.CharField(verbose_name=_('Place'), default='', max_length=50) + destination = models.CharField(verbose_name=_('Destination (optional)'), default='', max_length=50, blank=True) date = models.DateField(default=datetime.today) end = models.DateField(verbose_name=_('End (optional)'), blank=True, default=datetime.today) #comment = models.TextField(_('Comments'), default='', blank=True) groups = models.ManyToManyField(Group) jugendleiter = models.ManyToManyField(Member) - tour_type_choices = (('Gemeinschaftstour','Gemeinschaftstour'), ('Führungstour', 'Führungstour'), + tour_type_choices = (('Gemeinschaftstour','Gemeinschaftstour'), ('Führungstour', 'Führungstour'), ('Ausbildung', 'Ausbildung')) - tour_type = MultiSelectField(choices=tour_type_choices, default='', max_choices=1) + tour_type = MultiSelectField(choices=tour_type_choices, default='', max_choices=1) + activity = models.ManyToManyField(ActivityCategory, default=None) def __str__(self): @@ -136,7 +162,7 @@ class Klettertreff(models.Model): topic = models.CharField(_('Topic'), default='', max_length=60) jugendleiter = models.ManyToManyField(Member) group = models.ForeignKey(Group, default='') - + def __str__(self): return self.location + ' ' + self.date.strftime('%d.%m.%Y') @@ -157,7 +183,6 @@ class Klettertreff(models.Model): return True return False - get_jugendleiter.short_description = _('Jugendleiter') class Meta: diff --git a/jdav_web/members/templates/members/change_member.html b/jdav_web/members/templates/members/change_member.html new file mode 100644 index 0000000..c622fd1 --- /dev/null +++ b/jdav_web/members/templates/members/change_member.html @@ -0,0 +1,20 @@ +{% extends "admin/change_form.html" %} +{% load i18n %} +{% load static %} +{% block after_field_sets %} + +

{% trans "Qualities:" %}

+ + + + + +{% for key, value in qualities.items %} + + + + +{% endfor %} +
{% trans "Activity" %}{% trans "Skill level" %}
{{ key }}
+ +{% endblock %} From 3568b07369437fa84e6fe97e102d5136a371964e Mon Sep 17 00:00:00 2001 From: erichhasl Date: Mon, 13 Mar 2017 19:15:11 +0100 Subject: [PATCH 2/6] add difficulty field to memberlist --- jdav_web/members/admin.py | 6 +++++- jdav_web/members/models.py | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index d600d6b..5848b61 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -12,7 +12,7 @@ from django.contrib import admin from django.contrib.admin import DateFieldListFilter from django.utils.translation import ugettext_lazy as translate from django.db.models import TextField, ManyToManyField -from django.forms import Textarea +from django.forms import Textarea, RadioSelect, TypedChoiceField from django.shortcuts import render from .models import (Member, Group, MemberList, MemberOnList, Klettertreff, @@ -49,6 +49,10 @@ class ActivityCategoryAdmin(admin.ModelAdmin): class MemberListAdminForm(forms.ModelForm): + difficulty = TypedChoiceField(MemberList.difficulty_choices, + widget=RadioSelect, + coerce=int) + class Meta: model = MemberList exclude = ['add_member'] diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py index 5e3e921..dfbd5d5 100644 --- a/jdav_web/members/models.py +++ b/jdav_web/members/models.py @@ -127,6 +127,9 @@ class MemberList(models.Model): ('Ausbildung', 'Ausbildung')) tour_type = MultiSelectField(choices=tour_type_choices, default='', max_choices=1) activity = models.ManyToManyField(ActivityCategory, default=None) + difficulty_choices = [(1, _('easy')), (2, _('medium')), (3, _('hard'))] + difficulty = models.IntegerField(verbose_name=_('Difficulty'), + choices=difficulty_choices) def __str__(self): From 7bbfde8474ad5ec791750d362f8298bcff663ced Mon Sep 17 00:00:00 2001 From: erichhasl Date: Mon, 13 Mar 2017 19:18:44 +0100 Subject: [PATCH 3/6] consider difficulty of activity to calculate skill --- jdav_web/members/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py index dfbd5d5..94081cb 100644 --- a/jdav_web/members/models.py +++ b/jdav_web/members/models.py @@ -107,7 +107,7 @@ class Member(models.Model): for kind in ActivityCategory.objects.all(): lists = MemberList.objects.filter(activity=kind, memberonlist__member=self) - skills[kind.name] = len(lists) + skills[kind.name] = sum([l.difficulty * 3 for l in lists]) return skills From 2eebf2b463a59863dfa80b2561512a827895ec59 Mon Sep 17 00:00:00 2001 From: erichhasl Date: Mon, 13 Mar 2017 23:21:45 +0100 Subject: [PATCH 4/6] generate note of memberlist for jugendleiter --- .../media/memberlists/membernote_template.tex | 58 ++++++++++ jdav_web/members/admin.py | 108 +++++++++++++++++- 2 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 jdav_web/media/memberlists/membernote_template.tex diff --git a/jdav_web/media/memberlists/membernote_template.tex b/jdav_web/media/memberlists/membernote_template.tex new file mode 100644 index 0000000..7eef1f6 --- /dev/null +++ b/jdav_web/media/memberlists/membernote_template.tex @@ -0,0 +1,58 @@ +\documentclass{article} + +\usepackage[utf8]{inputenc} +\usepackage{booktabs} +\usepackage{tabularx} +\usepackage{ragged2e} +\usepackage{amssymb} +\usepackage{cmbright} +\usepackage{graphicx} +\usepackage{textpos} +\usepackage[colorlinks]{hyperref} +\usepackage{float} +\usepackage[margin=1cm]{geometry} + +\renewcommand{\arraystretch}{1.5} + +\newcolumntype{Y}{>{\RaggedRight\arraybackslash}X} +\begin{document} + +% HEADLINE +{\noindent\LARGE\textsc{Teilnehmerliste \\Sektionsveranstaltung}}\\ +\textit{Erstellt: MEMBERLIST-DATE}\\ + +% DESCRIPTION TABLE +\begin{table}[H] + \begin{tabular}{ll} + \large Aktivität: & ACTIVITY \\ + \large Gruppe: & GROUP \\ + \large Ziel: & DESTINATION \\ + \large Stützpunkt: & PLACE \\ + \large Zeitraum: & TIME-PERIOD \\ + \end{tabular} +\end{table} + +\begin{table}[H] + \begin{tabularx}{\textwidth}{@{} l l Y @{}} + \toprule + \textbf{Name} & \textbf{Fähigkeiten (max. 100)} & \textbf{Kommentare} \\ + \midrule + TABLE + \bottomrule + \end{tabularx} +\end{table} + +\noindent\large Fähigkeiten der Gruppe\\ +\begin{table}[H] + \begin{tabular*}{1\linewidth}{@{\extracolsep{\fill}}llll} + \toprule + \textbf{Name} & \textbf{Durchschnitt} & \textbf{Minimum} & \textbf{Maximum} \\ + \midrule + TABLE-QUALITIES + \bottomrule + \end{tabular*} +\end{table} + +\vspace{1cm} + +\end{document} diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index 5848b61..7b57255 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -61,7 +61,7 @@ class MemberListAdminForm(forms.ModelForm): super(MemberListAdminForm, self).__init__(*args, **kwargs) self.fields['jugendleiter'].queryset = Member.objects.filter(group__name='Jugendleiter') #self.fields['add_member'].queryset = Member.objects.filter(prename__startswith='F') - + class MemberOnListInline(admin.StackedInline): model = MemberOnList @@ -72,11 +72,12 @@ class MemberOnListInline(admin.StackedInline): 'cols': 40})}, } + class MemberListAdmin(admin.ModelAdmin): inlines = [MemberOnListInline] form = MemberListAdminForm list_display = ['__str__', 'date'] - actions = ['convert_to_pdf'] + actions = ['convert_to_pdf', 'generate_notes'] formfield_overrides = { ManyToManyField: {'widget': forms.CheckboxSelectMultiple} } @@ -86,7 +87,6 @@ class MemberListAdmin(admin.ModelAdmin): def convert_to_pdf(self, request, queryset): """Converts a member list to pdf. - """ for memberlist in queryset: # create a unique filename @@ -103,8 +103,8 @@ class MemberListAdmin(admin.ModelAdmin): line = '{0} {1} & {2}, {3} & {4} & {5} \\\\ \n'.format(memberonlist.member.prename, memberonlist.member.lastname, memberonlist.member.street, memberonlist.member.town, memberonlist.member.phone_number, memberonlist.member.email) - f.write(line) - + f.write(line) + # copy and adapt latex memberlist template shutil.copy('media/memberlists/memberlist_template.tex', 'media/memberlists/'+filename_tex) @@ -112,7 +112,7 @@ class MemberListAdmin(admin.ModelAdmin): # read in template with open('media/memberlists/'+filename_tex, 'r') as f: template_content = f.read() - + # adapt template template_content = template_content.replace('ACTIVITY', memberlist.name) groups = ', '.join(g.name for g in memberlist.groups.all()) @@ -172,6 +172,102 @@ class MemberListAdmin(admin.ModelAdmin): return response + def generate_notes(self, request, queryset): + """Generates a short note for the jugendleiter""" + for memberlist in queryset: + # unique filename + filename = memberlist.name + "_note_" +\ + datetime.today().strftime("%d_%m_%Y") + filename = filename.replace(' ', '_') + filename_tex = filename + '.tex' + filename_pdf = filename + '.pdf' + + # generate table + table = "" + activities = [a.name for a in memberlist.activity.all()] + skills = {a: [] for a in activities} + for memberonlist in memberlist.memberonlist_set.all(): + m = memberonlist.member + qualities = [] + for activity, value in m.get_skills().items(): + if activity not in activities: + continue + skills[activity].append(value) + qualities.append("\\textit{%s:} %s" % (activity, value)) + comment = ". ".join(c for c + in (m.comments, + memberonlist.comments) if + c).replace("..", ".") + line = '{0} {1} & {2} & {3} \\\\'.format( + m.prename, m.lastname, + ", ".join(qualities), comment or "---", + ) + table += line + + table_qualities = "" + for activity in activities: + line = '{0} & {1} & {2} & {3} \\\\ \n'.format( + activity, + sum(skills[activity]) / len(skills[activity]), + min(skills[activity]), + max(skills[activity]) + ) + table_qualities += line + + # copy template + shutil.copy('media/memberlists/membernote_template.tex', + 'media/memberlists/' + filename_tex) + + # read in template + with open('media/memberlists/' + filename_tex, 'r') as f: + template_content = f.read() + + # adapt template + template_content = template_content.replace('ACTIVITY', memberlist.name) + groups = ', '.join(g.name for g in memberlist.groups.all()) + template_content = template_content.replace('GROUP', groups) + template_content = template_content.replace('DESTINATION', memberlist.destination) + template_content = template_content.replace('PLACE', memberlist.place) + template_content = template_content.replace('MEMBERLIST-DATE', + datetime.today().strftime('%d.%m.%Y')) + time_period = memberlist.date.strftime('%d.%m.%Y') + if memberlist.end != memberlist.date: + time_period += " - " + memberlist.end.strftime('%d.%m.%Y') + template_content = template_content.replace('TIME-PERIOD', time_period) + jugendleiter = ', '.join(j.name for j in memberlist.jugendleiter.all()) + template_content = template_content.replace('JUGENDLEITER', jugendleiter) + + template_content = template_content.replace('TABLE-QUALITIES', + table_qualities) + template_content = template_content.replace('TABLE', table) + + # write adapted template to file + with open('media/memberlists/' + filename_tex, 'w') as f: + f.write(template_content) + + # compile using pdflatex + oldwd = os.getcwd() + os.chdir('media/memberlists') + subprocess.call(['pdflatex', filename_tex]) + time.sleep(1) + + # do some cleanup + for f in glob.glob('*.log'): + os.remove(f) + for f in glob.glob('*.aux'): + os.remove(f) + os.remove(filename_tex) + + os.chdir(oldwd) + + # provide the user with the resulting pdf file + with open('media/memberlists/'+filename_pdf, 'rb') as pdf: + response = HttpResponse(FileWrapper(pdf)) + response['Content-Type'] = 'application/pdf' + response['Content-Disposition'] = 'attachment; filename=' + filename_pdf + + return response + class KlettertreffAdminForm(forms.ModelForm): class Meta: From 171c747492d7539bbdd0f9d44336cf2c409f1f10 Mon Sep 17 00:00:00 2001 From: erichhasl Date: Mon, 13 Mar 2017 23:34:05 +0100 Subject: [PATCH 5/6] only consider past memberlists to calculate skill --- jdav_web/members/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py index 94081cb..19eee21 100644 --- a/jdav_web/members/models.py +++ b/jdav_web/members/models.py @@ -107,7 +107,8 @@ class Member(models.Model): for kind in ActivityCategory.objects.all(): lists = MemberList.objects.filter(activity=kind, memberonlist__member=self) - skills[kind.name] = sum([l.difficulty * 3 for l in lists]) + skills[kind.name] = sum([l.difficulty * 3 for l in lists + if l.date < datetime.now().date()]) return skills From 21994c984ca255f5b1c9f57a1ba4bb7556701db9 Mon Sep 17 00:00:00 2001 From: erichhasl Date: Tue, 14 Mar 2017 20:07:15 +0100 Subject: [PATCH 6/6] use only one connection to send multiple mails --- jdav_web/mailer/mailutils.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/jdav_web/mailer/mailutils.py b/jdav_web/mailer/mailutils.py index cab4fa6..fe9df14 100644 --- a/jdav_web/mailer/mailutils.py +++ b/jdav_web/mailer/mailutils.py @@ -1,3 +1,4 @@ +from django.core import mail from django.core.mail import EmailMessage @@ -13,18 +14,20 @@ def send(subject, content, sender, recipients, reply_to=None, kwargs = {"reply_to": [reply_to]} else: kwargs = {} - for recipient in recipients: - email = EmailMessage(subject, content, sender, [recipient], **kwargs) - if attachments is not None: - for attach in attachments: - email.attach_file(attach) - try: - email.send() - except Exception as e: - print("Error when sending mail:", e) - failed = True - else: - succeeded = True + with mail.get_connection() as connection: + for recipient in recipients: + email = EmailMessage(subject, content, sender, [recipient], + connection=connection, **kwargs) + if attachments is not None: + for attach in attachments: + email.attach_file(attach) + try: + email.send() + except Exception as e: + print("Error when sending mail:", e) + failed = True + else: + succeeded = True return NOT_SENT if failed and not succeeded else SENT if not failed\ and succeeded else PARTLY_SENT