1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-12-06 20:07:43 +01:00

add email feature

This commit is contained in:
quark
2025-12-05 11:41:05 +01:00
parent 7500c33f0f
commit c908bee872
8 changed files with 128 additions and 10 deletions

View File

@@ -24,3 +24,7 @@ WIKI_PASSWORD=
# OIDC
OIDC_RSA_PRIVATE_KEY=CHANGE_ME
# Activity configuration
TRUSTED_ACTIVITY_MAIL=
ACTIVITY_EMAIL_MANAGER=

View File

@@ -120,3 +120,12 @@ class GuestForm(forms.ModelForm):
},
),
}
class EmailForm(forms.Form):
"""
Form to export guest list by email
"""
emails = forms.CharField()
emails.label = _("Emails")
emails.widget.attrs['placeholder'] = _("Emails, separated by a comma")

View File

@@ -2,7 +2,7 @@
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n perms %}
{% load i18n perms crispy_forms_tags %}
{% load render_table from django_tables2 %}
{% load static django_tables2 i18n %}
@@ -43,6 +43,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
<button class="btn btn-block btn-danger"><i class="fa fa-file-pdf-o"></i> {% trans "Export to PDF" %}</button>
</a>
</div>
<div class="card-body">
<form action="{% url 'activity:guest_pdf' activity_pk=activity.pk %}" method="post">
{% csrf_token %}
{{ email_form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Share" %}</button>
</form>
</div>
{% endif %}
</div>
{% endif %}
@@ -120,5 +127,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
errMsg(xhr.responseJSON);
});
});
{% if mail %}
var mails = {{ mail|safe }};
for (const mail of mails) {
addMsg(gettext("An email has been sent to") + " " + mail, "success");
}
{% endif %}
</script>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% load i18n %}
{% now "Y-m-d" as today %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>[Note Kfet] Liste des invité·e·s à l'activité {{ activity.name }}</title>
</head>
<body>
Bonjour,
Vous trouverez en pièce-jointe la liste des invité·e·s à l'activité : {{ activity.name }}
Cette liste vous est partagée par {{ user_identity }} (en copie de ce mail).
Bonne journée
--
<p>
Le BDE<br>
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
</p>
</body>
</html>

View File

@@ -0,0 +1,13 @@
{% load i18n %}
Bonjour,
Vous trouverez en pièce-jointe la liste des invité·e·s à l'activité : {{ activity.name }}
Cette liste vous est partagée par {{ user_identity }} (en copie de ce mail).
Bonne journée
--
Le BDE
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}

View File

@@ -17,11 +17,11 @@ jusqu'au {{ activity.date_end.astimezone.date }} à {{ activity.date_end.astimez
\begin{center}
\normalsize
\begin{longtable}{c||c|c|c}
& \textbf{Nom} & \textbf{Prénom} & \textbf{École} \\
\begin{longtable}{c||c|c|c|c|}
& \textbf{Nom} & \textbf{Prénom} & \textbf{École} & \textbf{Entrée} \\
\hline\hline
{% for guest in guests %}
{{ forloop.counter }} & {{ guest.last_name|safe }} & {{ guest.first_name|safe }} & {{ guest.school|safe }} \\
{{ forloop.counter }} & {{ guest.last_name|safe }} & {{ guest.first_name|safe }} & {{ guest.school|safe }} & \\
\hline
{% endfor %}
\end{longtable}

View File

@@ -11,11 +11,12 @@ from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.core.mail import EmailMultiAlternatives
from django.db import transaction
from django.db.models import F, Q
from django.db.models.functions.text import Lower
from django.http import HttpResponse, JsonResponse
from django.urls import reverse_lazy
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
@@ -31,7 +32,7 @@ from note.models import Alias, NoteSpecial, NoteUser
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms import ActivityForm, GuestForm
from .forms import ActivityForm, GuestForm, EmailForm
from .models import Activity, Entry, Guest, Opener
from .tables import ActivityTable, EntryTable, GuestTable, OpenerTable
@@ -199,6 +200,9 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMix
guests_view = guests.filter(PermissionBackend.filter_queryset(self.request, Guest, "view"))
if guests.exists() and guests.count() == guests_view.count():
context["export"] = True
context["email_form"] = EmailForm
if 'mail' in self.request.GET:
context["mail"] = self.request.GET['mail'].split(',')
return context
@@ -445,6 +449,29 @@ class GuestListRenderView(LoginRequiredMixin, View):
return qs.distinct()
def get(self, request, **kwargs):
pdf = self.generate_pdf(request)
return self.view_pdf(request, pdf)
def post(self, request, **kwargs):
recipients = []
emails = request.POST['emails'].split(',')
trust_address = os.getenv('TRUSTED_ACTIVITY_MAIL', '').split(',')
for email_address in emails:
if email_address in trust_address:
recipients.append(email_address)
# don't send email if no recipient
if not recipients:
raise PermissionDenied(_("Emails are not trusted!"))
pdf = self.generate_pdf(request)
self.send_pdf(request, recipients, pdf)
url = reverse('activity:activity_detail', kwargs={"pk": self.kwargs["activity_pk"]})
url += '?mail='
for email in recipients:
url += email + ','
url = url[:-1] # delete last comma
return HttpResponseRedirect(url)
def generate_pdf(self, request, **kwargs):
qs = self.get_queryset()
activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
@@ -480,19 +507,43 @@ class GuestListRenderView(LoginRequiredMixin, View):
log = f.read()
raise IOError("An error attempted while generating a Guest list (code=" + str(error) + ")\n\n" + log)
# Display the generated pdf as a HTTP Response
with open("{}/guest-list.pdf".format(tmp_dir), 'rb') as f:
pdf = f.read()
response = HttpResponse(pdf, content_type="application/pdf")
response['Content-Disposition'] = "inline;filename=Liste des invité·e·s.pdf"
return pdf
except IOError as e:
raise e
finally:
# Delete all temporary files
shutil.rmtree(tmp_dir)
def view_pdf(self, request, pdf):
response = HttpResponse(pdf, content_type="application/pdf")
response['Content-Disposition'] = "inline;filename=Liste des invité·e·s.pdf"
return response
def send_pdf(self, request, recipients, pdf):
user_identity = request.user.first_name.capitalize() + ' ' + request.user.last_name.upper()
activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
subject = _(f"Guest list of the activity {activity.name} share by {user_identity}")
# add the user in cc
cc = [request.user.email]
context = {'activity': activity, 'user_identity': user_identity}
message = render_to_string("activity/guest_list.txt", context=context)
html_message = render_to_string("activity/guest_list.html", context=context)
if os.getenv('ACTIVITY_EMAIL_MANAGER', ''):
cc.append(os.getenv('ACTIVITY_EMAIL_MANAGER'))
email = EmailMultiAlternatives(
subject=subject,
to=recipients,
cc=cc,
body=message,
)
email.attach("Liste des invité·e·s.pdf", pdf)
email.attach_alternative(html_message, "text/html")
email.send()
return
# Cache for 1 hour
@method_decorator(cache_page(60 * 60), name='dispatch')

View File

@@ -107,6 +107,10 @@ N'importe qui peut inviter des ami⋅es non adhérent⋅es, tant que les contrai
trois personnes par activité et une personne ne peut pas être invitée plus de 5 fois par an). L'invitation est
facturée à l'entrée.
Les ayant-droit peuvent également générer la liste des invité·e·s au format PDF afin de la transmettre
aux vigiles. Iels peuvent aussi lenvoyer par mail (solution privilégiée), mais uniquement à une liste
dadresses mail bien précise, vérifiée régulièrement.
Entrées aux soirées
~~~~~~~~~~~~~~~~~~~