diff --git a/foerderbarometer/constants.py b/foerderbarometer/constants.py new file mode 100644 index 0000000..0eef96f --- /dev/null +++ b/foerderbarometer/constants.py @@ -0,0 +1,33 @@ +TYPE_ALL = 'ALL' + +TYPE_BIB = 'BIB' # Bibliotheksstipendium +TYPE_ELIT = 'ELIT' # eLiteraturstipendium +TYPE_SOFT = 'SOFT' # Softwarestipendium +TYPE_MAIL = 'MAIL' # E-Mail-Adresse +TYPE_IFG = 'IFG' # Kostenübernahme IFG-Anfrage +TYPE_LIT = 'LIT' # Literaturstipendium +TYPE_LIST = 'LIST' # Mailingliste +TYPE_TRAV = 'TRAV' # Reisekosten +TYPE_VIS = 'VIS' # Visitenkarten +TYPE_PROJ = 'PROJ' # Projektförderung + +TYPES = [ + TYPE_BIB, + TYPE_ELIT, + TYPE_SOFT, + TYPE_MAIL, + TYPE_IFG, + TYPE_LIT, + TYPE_LIST, + TYPE_TRAV, + TYPE_VIS, + TYPE_PROJ, +] + +RECIPIENT_APPLICANT = 'applicant' +RECIPIENT_STAFF = 'staff' + +RECIPIENTS = [ + RECIPIENT_APPLICANT, + RECIPIENT_STAFF, +] diff --git a/foerderbarometer/settings.py b/foerderbarometer/settings.py index 0141963..c51fc4a 100644 --- a/foerderbarometer/settings.py +++ b/foerderbarometer/settings.py @@ -6,6 +6,8 @@ from dotenv import load_dotenv from input.utils.settings import env, password_validators +from .constants import * + BASE_DIR = Path(__file__).parents[1] load_dotenv(BASE_DIR / '.env') @@ -165,3 +167,26 @@ NUTZUNGSBEDINGUNGEN_MAILINGLISTEN = 'static/input/nutzungsbedingungen-mailinglis NUTZUNGSBEDINGUNGEN_LITERATURSTIPENDIUM = 'static/input/nutzungsbedingungen-literaturstipendium.pdf' NUTZUNGSBEDINGUNGEN_OTRS = 'static/input/2025_Nutzungsvereinbarung_OTRS.docx.pdf' NUTZUNGSBEDINGUNGEN_VISITENKARTEN = 'static/input/nutzungsbedingungen-visitenkarten.pdf' + +MAIL_ATTACHMENT_CACHE_DIR = env('MAIL_ATTACHMENT_CACHE_DIR', BASE_DIR / 'var' / 'mail-attachments') +MAIL_ATTACHMENT_TTL_SECONDS = env('MAIL_ATTACHMENT_TTL_SECONDS', 24 * 60 * 60) +MAIL_ATTACHMENT_URLS = { + RECIPIENT_APPLICANT: { + TYPE_ALL: [], + TYPE_VIS: [ + 'https://foerderung.wikimedia.de/static/input/nutzungsbedingungen-visitenkarten.pdf', + ], + TYPE_MAIL: [ + 'https://foerderung.wikimedia.de/static/input/nutzungsbedingungen-mail.pdf', + ], + TYPE_LIST: [ + 'https://foerderung.wikimedia.de/static/input/nutzungsbedingungen-mailinglisten.pdf', + ], + TYPE_LIT: [ + 'https://foerderung.wikimedia.de/static/input/nutzungsbedingungen-literaturstipendium.pdf', + ], + }, + RECIPIENT_STAFF: { + TYPE_ALL: [], + }, +} diff --git a/input/admin.py b/input/admin.py index aa9e9ea..39bf13b 100755 --- a/input/admin.py +++ b/input/admin.py @@ -1,9 +1,11 @@ import csv from django.contrib import admin -from django.db import models +from django.db import models, transaction from django.http import HttpResponse +from input.utils.mail import send_decision_mails + from .forms import BaseProjectForm from .models import ( Account, @@ -22,6 +24,7 @@ from .models import ( BusinessCard, List, Literature, + TYPE_PROJ, ) @@ -83,7 +86,7 @@ class ProjectAdminForm(BaseProjectForm): super().__init__(*args, **kwargs) for field, model in self.categories.items(): - if self.initial[f'{field}_other']: + if self.initial.get(f'{field}_other'): self.initial[field] = [*self.initial[field], model.other] def clean(self): @@ -100,8 +103,7 @@ class ProjectAdminForm(BaseProjectForm): return cleaned_data -@admin.register(Project) -class ProjectAdmin(admin.ModelAdmin): +class BaseProjectAdmin(admin.ModelAdmin): save_as = True form = ProjectAdminForm search_fields = ('name', 'pid','finance_id', 'realname', 'start', 'end', 'participants_estimated', 'participants_real', 'cost', 'status', 'end_quartal') @@ -158,21 +160,42 @@ class ProjectAdmin(admin.ModelAdmin): class Media: js = ('dropdown/js/otrs_link.js',) - granted = True + granted: bool def get_queryset(self, request): return super().get_queryset(request).filter(granted=self.granted) +@admin.register(Project) +class ProjectAdmin(BaseProjectAdmin): + granted = True + + @admin.register(ProjectRequest) -class ProjectRequestAdmin(ProjectAdmin): +class ProjectRequestAdmin(BaseProjectAdmin): granted = None + def save_model(self, request, obj: ProjectRequest, form: ProjectAdminForm, change: bool): + super().save_model(request, obj, form, change) + + if obj.granted is None: + return None + + transaction.on_commit(lambda: send_decision_mails(obj)) + + return obj.granted + @admin.register(ProjectDeclined) -class ProjectDeclinedAdmin(ProjectAdmin): +class ProjectDeclinedAdmin(BaseProjectAdmin): granted = False + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False + @admin.register(BusinessCard) class BusinessCardAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin): diff --git a/input/management/commands/sendmails.py b/input/management/commands/sendmails.py index 15cac47..654d46a 100755 --- a/input/management/commands/sendmails.py +++ b/input/management/commands/sendmails.py @@ -2,13 +2,13 @@ from datetime import date, timedelta from django.core.management import CommandError from django.core.management.base import BaseCommand -from django.template.loader import get_template from django.core.mail import BadHeaderError -from django.core.mail import EmailMultiAlternatives from django.conf import settings from input.models import Project, Library, HonoraryCertificate, Travel, Email,\ BusinessCard, List, IFG, Literature +from input.utils.mail import send_email + class Command(BaseCommand): ''' mails will be sent here: @@ -34,15 +34,11 @@ class Command(BaseCommand): 'name': name, 'pid': pid, 'SURVEY_PREFIX': settings.SURVEY_PREFIX, } - txt_mail_template = get_template('input/survey_mail.txt') - html_mail_template = get_template('input/survey_mail.html') + + subject = 'Dein Feedback zur Förderung durch Wikimedia Deutschland' + try: - subject, from_email, to = 'Dein Feedback zur Förderung durch Wikimedia Deutschland', settings.IF_EMAIL, email - text_content = txt_mail_template.render(context) - html_content = html_mail_template.render(context) - msg = EmailMultiAlternatives(subject, text_content, from_email, [to], bcc=[settings.SURVEY_EMAIL]) - msg.attach_alternative(html_content, "text/html") - msg.send() + send_email('survey_mail', context, subject, email, bcc=[settings.SURVEY_EMAIL]) #print('survey mail would have been send') #survey_mail = EmailMessage('Dein Feedback zur Förderung durch Wikimedia Deutschland', @@ -70,21 +66,14 @@ class Command(BaseCommand): .exclude(end_mail_send = True)\ .filter(mail_state = 'NONE') - txt_mail_template = get_template('input/if_end_of_project.txt') - html_mail_template = get_template('input/if_end_of_project.html') - + subject = 'Projektende erreicht' + recipient = settings.IF_EMAIL for project in old: - context = {'project': project} - context['URL_PREFIX'] = settings.EMAIL_URL_PREFIX + context = {'project': project, 'URL_PREFIX': settings.EMAIL_URL_PREFIX} try: - subject, from_email, to = 'Projektende erreicht', settings.IF_EMAIL, settings.IF_EMAIL - text_content = txt_mail_template.render(context) - html_content = html_mail_template.render(context) - msg = EmailMultiAlternatives(subject, text_content, from_email, [to]) - msg.attach_alternative(html_content, "text/html") - msg.send() + send_email('if_end_of_project', context, subject, recipient) #print('end of project mail would have been sent') #send_mail('Projektende erreicht', @@ -110,33 +99,19 @@ class Command(BaseCommand): approved_end = Project.objects.filter(status = 'END')\ .exclude(end_mail_send = True)\ .filter(mail_state = 'INF') - txt_mail_template = get_template('input/if_end_of_project_approved.txt') - html_mail_template = get_template('input/if_end_of_project_approved.html') - txt_informMail_template = get_template('input/if_end_of_project_orginformed.txt') - html_informMail_template = get_template('input/if_end_of_project_orginformed.html') # send the mail to project.email, which would be the mail of the volunteer filling out the form for project in approved_end: - context = {'project': project} - context['URL_PREFIX'] = settings.EMAIL_URL_PREFIX - + context = {'project': project, 'URL_PREFIX': settings.EMAIL_URL_PREFIX} try: - subject, from_email, to = 'Projektende erreicht', settings.IF_EMAIL, project.email - text_content = txt_mail_template.render(context) - html_content = html_mail_template.render(context) - msg = EmailMultiAlternatives(subject, text_content, from_email, [to]) - msg.attach_alternative(html_content, "text/html") - msg.send() + send_email('if_end_of_project_approved', context, 'Projektende erreicht', project.email) + #print('if and of project approved mail would have been sent') - inform_subject, inform_from_email, inform_to = 'Projektorganisator*in wurde informiert', settings.IF_EMAIL, settings.IF_EMAIL - inform_text_content = txt_informMail_template.render(context) - inform_html_content = html_informMail_template.render(context) - inform_msg = EmailMultiAlternatives(inform_subject, inform_text_content, inform_from_email, [inform_to]) - inform_msg.attach_alternative(html_content, "text/html") - inform_msg.send() + send_email('if_end_of_project_orginformed', context, 'Projektorganisator*in wurde informiert', settings.IF_EMAIL) + #print('if end of project orginformed mail would have been sent') #send_mail('Projektende erreicht', @@ -168,25 +143,15 @@ class Command(BaseCommand): .exclude(end_mail_send = True)\ .filter(mail_state = 'INF') - html_mail_template = get_template('input/if_not_of_project_approved.html') - txt_mail_template = get_template('input/if_not_of_project_approved.txt') - - txt_informMail_template = get_template('input/if_end_of_project_orginformed.txt') - html_informMail_template = get_template('input/if_end_of_project_orginformed.html') # send the mail to project.email, which would be the mail of the volunteer that filled out the form for project in approved_notHappened: - context = {'project': project} - context['URL_PREFIX'] = settings.EMAIL_URL_PREFIX - try: - subject, from_email, to = 'Projektende erreicht', settings.IF_EMAIL, project.email - text_content = txt_mail_template.render(context) - html_content = html_mail_template.render(context) - msg = EmailMultiAlternatives(subject, text_content, from_email, [to]) - msg.attach_alternative(html_content, "text/html") - msg.send() - #print('if not of project approved end mail would have been sent') + context = {'project': project, 'URL_PREFIX': settings.EMAIL_URL_PREFIX} + try: + send_email('if_not_of_project_approved', context, 'Projektende erreicht', project.email) + + #print('if not of project approved end mail would have been sent') #send_mail('Projektende erreicht', # mail_template.render(context), @@ -194,12 +159,8 @@ class Command(BaseCommand): # [project.email], # fail_silently=False) - inform_subject, inform_from_email, inform_to = 'Projektorganisator*in wurde informiert', settings.IF_EMAIL, settings.IF_EMAIL - inform_text_content = txt_informMail_template.render(context) - inform_html_content = html_informMail_template.render(context) - inform_msg = EmailMultiAlternatives(inform_subject, inform_text_content, inform_from_email, [inform_to]) - inform_msg.attach_alternative(html_content, "text/html") - inform_msg.send() + send_email('if_end_of_project_orginformed', context, 'Projektorganisator*in wurde informiert', settings.IF_EMAIL) + #print('if not of project approved end mail orginformed would have been sent') #send_mail('Projektorganisator*in wurde informiert', diff --git a/input/models.py b/input/models.py index 567e675..45f38e9 100755 --- a/input/models.py +++ b/input/models.py @@ -11,6 +11,9 @@ from django.utils.functional import cached_property, classproperty from django.utils.html import format_html from django.utils.safestring import mark_safe +from foerderbarometer.constants import * + + EMAIL_STATES = { 'NONE': 'noch keine Mail versendet', 'INF': 'die Benachrichtigung zur Projektabschlussmail wurde versendet', @@ -403,17 +406,6 @@ def type_link(path, label): ) -TYPE_BIB = 'BIB' -TYPE_ELIT = 'ELIT' -TYPE_MAIL = 'MAIL' -TYPE_IFG = 'IFG' -TYPE_LIT = 'LIT' -TYPE_LIST = 'LIST' -TYPE_TRAV = 'TRAV' -TYPE_SOFT = 'SOFT' -TYPE_VIS = 'VIS' -TYPE_PROJ = 'PROJ' - TYPE_CHOICES = { TYPE_BIB: type_link('Zugang_zu_Fachliteratur#Bibliotheksstipendium', 'Bibliotheksstipendium'), TYPE_ELIT: type_link('Zugang_zu_Fachliteratur#eLiteraturstipendium', 'eLiteraturstipendium'), diff --git a/input/templates/input/ifg_volunteer_mail.html b/input/templates/input/ifg_volunteer_mail.html deleted file mode 100755 index bd7f989..0000000 --- a/input/templates/input/ifg_volunteer_mail.html +++ /dev/null @@ -1,34 +0,0 @@ - - -Hallo {{data.realname}}, -

-wir haben Deine Anfrage ({{data.type_label|striptags}}) erhalten.
-{% if data.choice in data.grant %}
-Vorraussichtliche Kosten: {{data.cost}}
-Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}
-Domain: {{data.domain}}
-Adressenbestandteil: {{data.address}} {% endif %} {% if data.choice == 'BIB' %}
-Bibliothek: {{data.library}}
-Dauer: {{data.duration}} {% elif data.choice == 'ELIT' %}
-Datenbank: {{data.library}}
-Dauer: {{data.duration}} {% elif data.choice == 'SOFT' %}
-Software: {{data.library}}
-Dauer: {{data.duration}} {% elif data.choice == 'IFG'%}
-Anfrage-URL: {{data.url}} {% elif data.choice == 'LIT'%}
-Info zum Werk: {{data.info}}
-Bezugsquelle: {{data.source}} {% elif data.choice == 'MAIL'%}
-Adressenbestandteil frei gewählt: {{data.other}} {% elif data.choice == 'VIS'%}
-Wikimedia-Projekt: {{data.project}}
-Persönliche Daten: {{data.data}}
-Variante: {{data.variant}}
-Sendungsadrese: {{data.send_to}} {% endif %}
-

-Das Team Comunitys und Engagement wird sich um die Bearbeitung deiner Anfrage kümmern
-und sich in den nächsten Tagen bei dir melden. Solltest du Rückfragen haben,
-wende dich gern an community@wikimedia.de.
-

-Viele Grüße, dein freundliches aber komplett unmenschliches automatisches -Formularbeantwortungssystem. - - - diff --git a/input/templates/mails/approval_denied_applicant.html b/input/templates/mails/approval_denied_applicant.html new file mode 100644 index 0000000..6343d74 --- /dev/null +++ b/input/templates/mails/approval_denied_applicant.html @@ -0,0 +1,9 @@ + + +

Hallo {{ data.realname }},

+ +

Deine Förderanfrage {{project.name}} wurde leider abgelehnt.

+ +

Fragen? community@wikimedia.de

+ + diff --git a/input/templates/mails/approval_denied_applicant.txt b/input/templates/mails/approval_denied_applicant.txt new file mode 100644 index 0000000..55a8df6 --- /dev/null +++ b/input/templates/mails/approval_denied_applicant.txt @@ -0,0 +1,5 @@ +Hallo {{ data.realname }}, + +deine Förderanfrage {{project.name}} wurde leider abgelehnt. + +Fragen? community@wikimedia.de diff --git a/input/templates/mails/approval_denied_staff.html b/input/templates/mails/approval_denied_staff.html new file mode 100644 index 0000000..3281b63 --- /dev/null +++ b/input/templates/mails/approval_denied_staff.html @@ -0,0 +1,7 @@ + + +

Hallo Team Community-Konferenzen & Förderung,

+ +

Die Förderanfrage {{project.name}} von {{ data.realname }} wurde abgelehnt.

+ + \ No newline at end of file diff --git a/input/templates/mails/approval_denied_staff.txt b/input/templates/mails/approval_denied_staff.txt new file mode 100644 index 0000000..2a96ae9 --- /dev/null +++ b/input/templates/mails/approval_denied_staff.txt @@ -0,0 +1,3 @@ +Hallo Team Community-Konferenzen & Förderung, + +die Förderanfrage {{project.name}} von {{ data.realname }} wurde abgelehnt. diff --git a/input/templates/mails/approval_granted_applicant.html b/input/templates/mails/approval_granted_applicant.html new file mode 100644 index 0000000..c768a73 --- /dev/null +++ b/input/templates/mails/approval_granted_applicant.html @@ -0,0 +1,10 @@ + + +

Hallo {{ data.realname }},

+ +

Deine Förderanfrage {{project.name}} wurde bewilligt.

+ +

Das Team Community-Konferenzen & Förderung meldet sich bald bei dir.
+ Fragen? community@wikimedia.de

+ + diff --git a/input/templates/mails/approval_granted_applicant.txt b/input/templates/mails/approval_granted_applicant.txt new file mode 100644 index 0000000..c1f3c24 --- /dev/null +++ b/input/templates/mails/approval_granted_applicant.txt @@ -0,0 +1,7 @@ +Hallo {{ data.realname }}, + +deine Förderanfrage {{project.name}} wurde bewilligt. + +Das Team Community-Konferenzen & Förderung meldet sich bald bei dir. +Fragen? community@wikimedia.de + diff --git a/input/templates/mails/approval_granted_staff.html b/input/templates/mails/approval_granted_staff.html new file mode 100644 index 0000000..d315c00 --- /dev/null +++ b/input/templates/mails/approval_granted_staff.html @@ -0,0 +1,7 @@ + + +

Hallo Team Community-Konferenzen & Förderung,

+ +

Die Förderanfrage {{project.name}} von {{ data.realname }} wurde bewilligt.

+ + \ No newline at end of file diff --git a/input/templates/mails/approval_granted_staff.txt b/input/templates/mails/approval_granted_staff.txt new file mode 100644 index 0000000..5fd989b --- /dev/null +++ b/input/templates/mails/approval_granted_staff.txt @@ -0,0 +1,3 @@ +Hallo Team Community-Konferenzen & Förderung, + +die Förderanfrage {{project.name}} von {{ data.realname }} wurde bewilligt. diff --git a/input/templates/input/if_end_of_project.html b/input/templates/mails/if_end_of_project.html similarity index 100% rename from input/templates/input/if_end_of_project.html rename to input/templates/mails/if_end_of_project.html diff --git a/input/templates/input/if_end_of_project.txt b/input/templates/mails/if_end_of_project.txt similarity index 100% rename from input/templates/input/if_end_of_project.txt rename to input/templates/mails/if_end_of_project.txt diff --git a/input/templates/input/if_end_of_project_approved.html b/input/templates/mails/if_end_of_project_approved.html similarity index 100% rename from input/templates/input/if_end_of_project_approved.html rename to input/templates/mails/if_end_of_project_approved.html diff --git a/input/templates/input/if_end_of_project_approved.txt b/input/templates/mails/if_end_of_project_approved.txt similarity index 100% rename from input/templates/input/if_end_of_project_approved.txt rename to input/templates/mails/if_end_of_project_approved.txt diff --git a/input/templates/input/if_end_of_project_orginformed.html b/input/templates/mails/if_end_of_project_orginformed.html similarity index 100% rename from input/templates/input/if_end_of_project_orginformed.html rename to input/templates/mails/if_end_of_project_orginformed.html diff --git a/input/templates/input/if_end_of_project_orginformed.txt b/input/templates/mails/if_end_of_project_orginformed.txt similarity index 100% rename from input/templates/input/if_end_of_project_orginformed.txt rename to input/templates/mails/if_end_of_project_orginformed.txt diff --git a/input/templates/input/if_mail.html b/input/templates/mails/if_mail.html similarity index 79% rename from input/templates/input/if_mail.html rename to input/templates/mails/if_mail.html index 92107f9..31237ba 100755 --- a/input/templates/input/if_mail.html +++ b/input/templates/mails/if_mail.html @@ -1,10 +1,11 @@ -Hallo Team Communitys und Engagement, -

-es gab einen neuen Antrag von {{data.realname}}. -

-Der Nutzer mit dem Username {{data.username}} ({{data.email}}) fragt ein_e {{data.type_label|striptags}} an.
+

Hallo Team Community-Konferenzen & Förderung,

+ +

es gibt eine neue Anfrage von {{ data.realname }}.

+ +

{{ data.username|default:data.realname }} ({{ data.email }}) fragt an: {{ data.type_label|striptags }}

+ {% if data.choice in data.grant %}
Vorraussichtliche Kosten: {{data.cost}}
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}
@@ -24,9 +25,8 @@ Wikimedia-Projekt: {{data.project}}
Persönliche Daten: {{data.data}}
Variante: {{data.variant}}
Sendungsadrese: {{data.send_to}}
{% endif %} -

-Zum Eintrag in der Förderdatenbank: +

Zum Eintrag in der Förderdatenbank: {% if data.choice == 'BIB' %} {{data.url_prefix}}/admin/input/library/{{data.pk}}/change {% elif data.choice == 'ELIT'%} @@ -46,13 +46,15 @@ Zum Eintrag in der Förderdatenbank: {% elif data.choice == 'VIS'%} {{data.url_prefix}}/admin/input/businesscard/{{data.pk}}/change {% endif %} -

+

-Zum Genehmigen hier klicken: {{data.url_prefix}}{% url 'authorize' data.choice data.pk %} -

-Zu Ablehnen hier klicken: {{data.url_prefix}}{% url 'deny' data.choice data.pk %} -

-Stets zu Diensten, Deine Förderdatenbank +

Zum Genehmigen hier klicken: + {{data.url_prefix}}{% url 'authorize' data.choice data.pk %} +

+ +

Zum Ablehnen hier klicken: + {{data.url_prefix}}{% url 'deny' data.choice data.pk %} +

diff --git a/input/templates/input/if_mail.txt b/input/templates/mails/if_mail.txt similarity index 100% rename from input/templates/input/if_mail.txt rename to input/templates/mails/if_mail.txt diff --git a/input/templates/input/if_not_of_project_approved.html b/input/templates/mails/if_not_of_project_approved.html similarity index 100% rename from input/templates/input/if_not_of_project_approved.html rename to input/templates/mails/if_not_of_project_approved.html diff --git a/input/templates/input/if_not_of_project_approved.txt b/input/templates/mails/if_not_of_project_approved.txt similarity index 100% rename from input/templates/input/if_not_of_project_approved.txt rename to input/templates/mails/if_not_of_project_approved.txt diff --git a/input/templates/mails/ifg_volunteer_mail.html b/input/templates/mails/ifg_volunteer_mail.html new file mode 100755 index 0000000..5652db1 --- /dev/null +++ b/input/templates/mails/ifg_volunteer_mail.html @@ -0,0 +1,47 @@ + + +

Hallo {{ data.username|default:data.realname }},

+ +

vielen Dank für deine Anfrage ({{ data.type_label|striptags }}), die bei uns eingegangen ist.

+ +Dies ist eine automatisch generierte E-Mail. Im Folgenden findest du deine Formulareingaben nochmals zu deiner Übersicht:
+{% if data.choice in data.grant %}
+Vorraussichtliche Kosten: {{data.cost}}
+Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}
+Domain: {{data.domain}}
+Adressenbestandteil: {{data.address}} {% endif %} {% if data.choice == 'BIB' %}
+Bibliothek: {{data.library}}
+Dauer: {{data.duration}} {% elif data.choice == 'ELIT' %}
+Datenbank: {{data.library}}
+Dauer: {{data.duration}} {% elif data.choice == 'SOFT' %}
+Software: {{data.library}}
+Dauer: {{data.duration}} {% elif data.choice == 'IFG'%}
+Anfrage-URL: {{data.url}} {% elif data.choice == 'LIT'%}
+Info zum Werk: {{data.info}}
+Bezugsquelle: {{data.source}} {% elif data.choice == 'MAIL'%}
+Adressenbestandteil frei gewählt: {{data.other}} {% elif data.choice == 'VIS'%}
+Wikimedia-Projekt: {{data.project}}
+Persönliche Daten: {{data.data}}
+Variante: {{data.variant}}
+Sendungsadrese: {{data.send_to}} {% endif %}
+ +

Das Team Community-Konferenzen & Förderung wird sich um deine Anfrage kümmern und sich in den nächsten Tagen bei dir melden. Wenn du Fragen hast, wende dich gern jederzeit an community@wikimedia.de.

+ +

+ --
+ Wikimedia Deutschland e. V. | Tempelhofer Ufer 23–24 | 10963 Berlin
+ Zentrale: +49 30 5771162-0
+ wikimedia.de +

+ +

+ Unsere Vision ist eine Welt, in der alle Menschen am Wissen der Menschheit teilhaben, es nutzen und mehren können. Helfen Sie uns dabei!
+ spenden.wikimedia.de +

+ +

Wikimedia Deutschland – Gesellschaft zur Förderung Freien Wissens e. V. Eingetragen im Vereinsregister des Amtsgerichts Charlottenburg, VR 23855 B. Als gemeinnützig anerkannt durch das Finanzamt für Körperschaften I Berlin, Steuernummer 27/029/42207. Geschäftsführende Vorständin: Franziska Heine.

+ +

Datenschutzerklärung:
+ Soweit Sie uns personenbezogene Daten mitteilen, verarbeiten wir diese Daten gemäß unserer Datenschutzerklärung .

+ + \ No newline at end of file diff --git a/input/templates/input/ifg_volunteer_mail.txt b/input/templates/mails/ifg_volunteer_mail.txt similarity index 100% rename from input/templates/input/ifg_volunteer_mail.txt rename to input/templates/mails/ifg_volunteer_mail.txt diff --git a/input/templates/input/survey_mail.html b/input/templates/mails/survey_mail.html similarity index 100% rename from input/templates/input/survey_mail.html rename to input/templates/mails/survey_mail.html diff --git a/input/templates/input/survey_mail.txt b/input/templates/mails/survey_mail.txt similarity index 100% rename from input/templates/input/survey_mail.txt rename to input/templates/mails/survey_mail.txt diff --git a/input/utils/mail/__init__.py b/input/utils/mail/__init__.py new file mode 100644 index 0000000..ec25b3f --- /dev/null +++ b/input/utils/mail/__init__.py @@ -0,0 +1,84 @@ +from django.conf import settings +from django.core.mail import EmailMultiAlternatives +from django.template.loader import get_template + +from input.models import Project + +from .attachments import collect_attachment_paths, attach_files + +__all__ = [ + 'build_email', + 'send_email', + 'collect_attachment_paths', + 'attach_files', + 'send_applicant_decision_mail', + 'send_staff_decision_mail', + 'send_decision_mails', +] + + +def build_email(template_name: str, context: dict, subject: str, *recipients: str, **kwargs): + body = get_template(f'mails/{template_name}.txt').render(context) + html = get_template(f'mails/{template_name}.html').render(context) + + kwargs.setdefault('from_email', settings.IF_EMAIL) + + kwargs['subject'] = subject + kwargs['body'] = body + kwargs['to'] = recipients + + email = EmailMultiAlternatives(**kwargs) + + email.attach_alternative(html, 'text/html') + + return email + + +def send_email(template_name: str, context: dict, subject: str, *recipients: str, fail_silently=False, **kwargs): + return build_email(template_name, context, subject, *recipients, **kwargs).send(fail_silently) + + +def get_decision_mail_context(obj: Project): + """ + Build a minimal, consistent context for decision mails (applicant & staff). + """ + + return { + 'project': obj, + 'data': { + 'realname': obj.realname or obj.email, + 'name': obj.name, + }, + } + + +def send_base_decision_mail(obj: Project, scope: str, subject: str, recipient: str): + context = get_decision_mail_context(obj) + decision = 'granted' if obj.granted else 'denied' + decision_label = 'bewilligt' if obj.granted else 'abgelehnt' + subject = subject.format(name=obj.name, decision=decision_label) + + return send_email(f'approval_{scope}_{decision}', context, subject, recipient) + + +def send_applicant_decision_mail(obj: Project): + """ + Send a decision email to the applicant after manual approval/denial. + """ + + if recipient := obj.email: + return send_base_decision_mail(obj, 'applicant', 'Deine Förderanfrage „{name}“ – {decision}', recipient) + + return 0 + + +def send_staff_decision_mail(obj: Project): + """ + Send a decision email to the internal team (staff) after approval/denial. + """ + + return send_base_decision_mail(obj, 'staff', 'Entscheidung: {name} ({decision})', settings.IF_EMAIL) + + +def send_decision_mails(obj: Project): + return send_applicant_decision_mail(obj) + send_staff_decision_mail(obj) diff --git a/input/utils/mail/attachments.py b/input/utils/mail/attachments.py new file mode 100644 index 0000000..27d9cf5 --- /dev/null +++ b/input/utils/mail/attachments.py @@ -0,0 +1,94 @@ +import os +import posixpath +import time +import mimetypes + +from os import PathLike +from pathlib import Path +from urllib.parse import urlparse +from urllib.request import urlretrieve + +from django.conf import settings +from django.core.mail import EmailMultiAlternatives + +from foerderbarometer.constants import * + +PathList = list[Path] + + +def ensure_dir(directory: PathLike) -> Path: + """ + Ensure that the given directory exists. + Creates it recursively if it doesn't. + """ + + directory = Path(directory) + + directory.mkdir(parents=True, exist_ok=True) + + return directory + + +def is_fresh(path: Path, ttl_seconds: int) -> bool: + """ + Check if the cached file exists and is still fresh within TTL. + """ + + try: + mtime = path.stat().st_mtime + except FileNotFoundError: + return False + else: + return time.time() - mtime < ttl_seconds + + +def get_attachment(url: str) -> Path: + filepath = urlparse(url).path + filename = posixpath.basename(filepath) + destination = ensure_dir(settings.MAIL_ATTACHMENT_CACHE_DIR) / filename + + if is_fresh(destination, settings.MAIL_ATTACHMENT_TTL_SECONDS): + return destination + + return download_attachment(url, destination) + + +def download_attachment(url: str, destination: Path) -> Path: + filepath = destination.with_suffix('.tmp') + + try: + urlretrieve(url, filepath) + os.replace(filepath, destination) + finally: + filepath.unlink(missing_ok=True) + + return destination + + +def collect_attachment_paths(recipient: str, type_code: str) -> PathList: + assert recipient in RECIPIENTS + assert type_code in TYPES + + config = settings.MAIL_ATTACHMENT_URLS[recipient] + urls = [*config[TYPE_ALL], *config.get(type_code, [])] + + return [get_attachment(url) for url in urls] + + +def get_mime_type(path: Path) -> str: + mime_type, encoding = mimetypes.guess_type(path) + + return mime_type or 'application/octet-stream' + + +def attach_files(message: EmailMultiAlternatives, files: list[Path]): + """ + Attach files to the EmailMultiAlternatives message. + MIME type is guessed from path; falls back to application/octet-stream. + """ + + for path in files: + mime_type = get_mime_type(path) + + with open(path, 'rb') as fp: + message.attach(path.name, fp.read(), mime_type) diff --git a/input/views.py b/input/views.py index 3eb4bc6..d92b2f9 100755 --- a/input/views.py +++ b/input/views.py @@ -5,12 +5,14 @@ from django.shortcuts import render from django.http import HttpResponse, Http404 from django.utils.functional import cached_property from django.utils.safestring import mark_safe -from django.core.mail import BadHeaderError, EmailMultiAlternatives -from django.template.loader import get_template +from django.core.mail import BadHeaderError from django.conf import settings from django.contrib.auth.decorators import login_required from django.views.generic import TemplateView from django.views.generic.edit import FormView +from django.utils.html import strip_tags + +from input.utils.mail import collect_attachment_paths, attach_files, build_email from .forms import ( BaseApplicationForm, @@ -208,15 +210,28 @@ class ApplicationView(FormView): - Return the "done" response. """ + data = self.prepare_data(form) + obj = self.save_obj(form, data) + + if response := self.send_mail(obj, data): + return response + + return done(self.request) + + def prepare_data(self, form): # Collect cleaned data and mark the current type - data = form.cleaned_data.copy() - data['choice'] = self.type_code + + data = {**form.cleaned_data, 'choice': self.type_code} # Special rule for literature applications if self.type_code == TYPE_LIT and data.get('selfbuy') == 'TRUE': data['selfbuy_give_data'] = 'False' + return data + + def save_obj(self, form, data): # Save model instance + modell = form.save(commit=False) # Username from session if present @@ -226,6 +241,7 @@ class ApplicationView(FormView): # Copy common fields if provided by the form if 'realname' in data: modell.realname = data['realname'] + if 'email' in data: modell.email = data['email'] @@ -238,39 +254,51 @@ class ApplicationView(FormView): modell.selfbuy_give_data = data['selfbuy_give_data'] modell.save() + if hasattr(form, 'save_m2m'): form.save_m2m() + return modell + + def send_mail(self, obj, data): # Prepare minimal mail context and send mails - data['pk'] = modell.pk + + type_label_html = self.type_info.label + type_label_plain = strip_tags(type_label_html) + + data['pk'] = obj.pk data['url_prefix'] = settings.EMAIL_URL_PREFIX - data['type_label'] = self.type_info.label + data['type_label'] = type_label_html + context = {'data': data} + applicant_name = self.get_recipient_name(obj, data) + applicant_subject = 'Deine Förderanfrage bei Wikimedia Deutschland' + + staff_subject = f'Anfrage {type_label_plain} von {applicant_name}' + try: - # Mail to applicant - txt1 = get_template('input/ifg_volunteer_mail.txt').render(context) - html1 = get_template('input/ifg_volunteer_mail.html').render(context) - msg1 = EmailMultiAlternatives( - 'Formular ausgefüllt', txt1, settings.IF_EMAIL, [data['email']] - ) - msg1.attach_alternative(html1, 'text/html') - msg1.send() - - # Mail to IF - txt2 = get_template('input/if_mail.txt').render(context) - html2 = get_template('input/if_mail.html').render(context) - msg2 = EmailMultiAlternatives( - 'Formular ausgefüllt', txt2, settings.IF_EMAIL, [settings.IF_EMAIL] - ) - msg2.attach_alternative(html2, 'text/html') - msg2.send() - + self.send_email('applicant', 'ifg_volunteer_mail', applicant_subject, data['email'], context) + self.send_email('staff', 'if_mail', staff_subject, settings.IF_EMAIL, context) except BadHeaderError: - modell.delete() + obj.delete() return HttpResponse('Invalid header found. Data not saved!') except SMTPException: - modell.delete() - return HttpResponse('Error in sending mails (probably wrong adress?). Data not saved!') + obj.delete() + return HttpResponse('Error in sending mails (probably wrong address?). Data not saved!') - return done(self.request) + def send_email(self, kind, template_name, subject, recipient, context, *, fail_silently=False): + email = build_email(template_name, context, subject, recipient) + applicant_files = collect_attachment_paths(kind, self.type_code) + + attach_files(email, applicant_files) + + return email.send(fail_silently) + + @staticmethod + def get_recipient_name(obj, data): + for field in 'username', 'realname', 'email': + if name := getattr(obj, field, None) or data.get(field): + return name + + return 'Unbekannt'