diff --git a/models/database.py b/models/database.py index 2e709c69753b730586a725221327d1a7f4a0fa64..561b14c4e61b843ec878c8e1aacd068203c0c03e 100644 --- a/models/database.py +++ b/models/database.py @@ -2,6 +2,7 @@ from flask import render_template, send_file, url_for, redirect, flash, request from datetime import datetime, time, date, timedelta import math +from io import StringIO, BytesIO from shared import db from utils import random_string, url_manager, get_etherpad_url @@ -104,8 +105,6 @@ class ProtocolType(db.Model): if protocoltype.has_private_view_right(user) ] - - class Protocol(db.Model): __tablename__ = "protocols" id = db.Column(db.Integer, primary_key=True) @@ -283,6 +282,10 @@ class Document(db.Model): def get_filename(self): return os.path.join(config.DOCUMENTS_PATH, self.filename) + def as_file_like(self): + with open(self.get_filename(), "rb") as file: + return BytesIO(file.read()) + @event.listens_for(Document, "before_delete") def on_document_delete(mapper, connection, document): if document.filename is not None: diff --git a/server.py b/server.py index 203d9dd160f586a7e750c2764174f0c20e5c367e..0379c89f97180785e9de09399d063c28f38c510d 100755 --- a/server.py +++ b/server.py @@ -565,6 +565,19 @@ def update_protocol(protocol_id): return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) return render_template("protocol-update.html", upload_form=upload_form, edit_form=edit_form, protocol=protocol) +@app.route("/prococol/send/<int:protocol_id>") +@login_required +def send_protocol(protocol_id): + user = current_user() + protocol = Protocol.query.filter_by(id=protocol_id).first() + if protocol is None or not protocol.protocoltype.has_modify_right(user): + flash("Invalides Protokoll oder keine Berechtigung.", "alert-error") + return redirect(request.args.get("next") or url_for("index")) + tasks.send_protocol(protocol) + flash("Das Protokoll wurde versandt.", "alert-success") + return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) + + @app.route("/protocol/tops/new/<int:protocol_id>", methods=["GET", "POST"]) @login_required def new_top(protocol_id): @@ -738,9 +751,7 @@ def download_document(document_id): and not document.protocol.protocoltype.has_public_view_right(user))): flash("Keine Berechtigung.", "alert-error") return redirect(request.args.get("next") or url_for("index")) - with open(document.get_filename(), "rb") as file: - file_like = BytesIO(file.read()) - return send_file(file_like, cache_timeout=1, as_attachment=True, attachment_filename=document.name) + return send_file(document.as_file_like(), cache_timeout=1, as_attachment=True, attachment_filename=document.name) @app.route("/document/upload/<int:protocol_id>", methods=["POST"]) @login_required diff --git a/tasks.py b/tasks.py index e82b2d4ab5b6a00b8b35d8acbda2923bafe4704d..a60fb7064c772424963ebba60f4de5acf2878f11 100644 --- a/tasks.py +++ b/tasks.py @@ -199,7 +199,6 @@ def parse_protocol_async(protocol_id, encoded_kwargs): content_private = render_template("protocol.txt", render_type=RenderType.plaintext, show_private=True, **render_kwargs) content_public = render_template("protocol.txt", render_type=RenderType.plaintext, show_private=False, **render_kwargs) if content_private != content_public: - print("different") privacy_states.append(True) protocol.content_private = content_private protocol.content_public = content_public @@ -316,23 +315,40 @@ def send_reminder_async(reminder_id, protocol_id): with app.app_context(): reminder = MeetingReminder.query.filter_by(id=reminder_id).first() protocol = Protocol.query.filter_by(id=protocol_id).first() - reminder_text = render_template("reminder.txt", reminder=reminder, protocol=protocol) + reminder_text = render_template("reminder-mail.txt", reminder=reminder, protocol=protocol) if reminder.send_public: send_mail(protocol, protocol.protocoltype.public_mail, "Tagesordnung der {}".format(protocol.protocoltype.name), reminder_text) if reminder.send_private: send_mail(protocol, protocol.protocoltype.private_mail, "Tagesordnung der {}".format(protocol.protocoltype.name), reminder_text) -def send_mail(protocol, to_addr, subject, content): +def send_protocol(protocol): + send_protocol_async.delay(protocol.id, show_private=True) + send_protocol_async.delay(protocol.id, show_private=False) + +@celery.task +def send_protocol_async(protocol_id, show_private): + with app.app_context(): + protocol = Protocol.query.filter_by(id=protocol_id).first() + to_addr = protocol.protocoltype.private_mail if show_private else protocol.protocoltype.public_mail + subject = "{}{}-Protokoll vom {}".format("Internes " if show_private else "", protocol.protocoltype.short_name, date_filter(protocol.date)) + mail_content = render_template("protocol-mail.txt", protocol=protocol) + appendix = [(document.name, document.as_file_like()) + for document in protocol.documents + if show_private or not document.is_private + ] + send_mail(protocol, to_addr, subject, mail_content, appendix) + + +def send_mail(protocol, to_addr, subject, content, appendix=None): if to_addr is not None and len(to_addr.strip()) > 0: - send_mail_async.delay(protocol.id, to_addr, subject, content) + send_mail_async.delay(protocol.id, to_addr, subject, content, appendix) @celery.task -def send_mail_async(protocol_id, to_addr, subject, content): +def send_mail_async(protocol_id, to_addr, subject, content, appendix): with app.app_context(): protocol = Protocol.query.filter_by(id=protocol_id).first() try: - print("sending {} to {}".format(subject, to_addr)) - mail_manager.send(to_addr, subject, content) + mail_manager.send(to_addr, subject, content, appendix) except Exception as exc: error = protocol.create_error("Sending Mail", "Sending mail failed", str(exc)) db.session.add(error) diff --git a/templates/protocol-mail.txt b/templates/protocol-mail.txt new file mode 100644 index 0000000000000000000000000000000000000000..9c228ea83116cdaf40ef55cf7141bf83b46432d3 --- /dev/null +++ b/templates/protocol-mail.txt @@ -0,0 +1,34 @@ +Protocol der {{protocol.name}} vom {{protocol.date|datify}} + +Datum: {{protocol.date|datify_long}} +Zeit: von {{protocol.start_time|timify}} bis {{protocol.end_time|timify}} +Protokollant: {{protocol.author}} +Anwesende: {{protocol.participants}} + +Die Tagesordnung ist: +{% if not protocol.has_nonplanned_tops() %} + {% for default_top in protocol.protocoltype.default_tops %} + {% if not default_top.is_at_end() %} +* {{default_top.name}} + {% endif %} + {% endfor %} +{% endif %} +{% for top in protocol.tops %} +* {{top.name }} +{% endfor %} +{% if not protocol.has_nonplanned_tops() %} + {% for default_top in protocol.protocoltype.default_tops %} + {% if default_top.is_at_end() %} +* {{default_top.name}} + {% endif %} + {% endfor %} +{% endif %} + +Beschlüsse: +{% if protocol.decisions|length > 0 %} + {% for decision in protocol.decisions %} +* {{decision.content}} + {% endfor %} +{% else %} +* Keine Beschlüsse +{% endif %} diff --git a/templates/protocol-show.html b/templates/protocol-show.html index bc3a703dd970381d4af2cfccb69c10dfa5c3472e..af65a7b6bd238982e1c74e63571c0ef4ee06d49d 100644 --- a/templates/protocol-show.html +++ b/templates/protocol-show.html @@ -20,6 +20,8 @@ {% if not protocol.is_done() %} <a class="btn btn-default" href="{{url_for("get_protocol_template", protocol_id=protocol.id)}}">Vorlage</a> <a class="btn btn-primary" href="{{url_for("etherpush_protocol", protocol_id=protocol.id)}}">In Etherpad</a> + {% else %} + <a class="btn btn-default" href="{{url_for("send_protocol", protocol_id=protocol.id)}}">Per Mail versenden</a> {% endif %} <a class="btn btn-default" href="{{protocol.get_etherpad_link()}}" target="_blank">Etherpad</a> <a class="btn btn-default" href="{{url_for("show_type", type_id=protocol.protocoltype.id)}}">Typ</a> diff --git a/templates/reminder.txt b/templates/reminder-mail.txt similarity index 100% rename from templates/reminder.txt rename to templates/reminder-mail.txt diff --git a/utils.py b/utils.py index cc020e5dcba17ae0cd7e80fa0c68d0351af08550..7cd92045111a0662fbe34e0ee18afcc73f686732 100644 --- a/utils.py +++ b/utils.py @@ -6,6 +6,7 @@ import regex import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText +from email.mime.application import MIMEApplication from datetime import datetime, date, timedelta import requests from io import BytesIO @@ -68,18 +69,23 @@ class MailManager: self.username = getattr(config, "MAIL_USER", "") self.password = getattr(config, "MAIL_PASSWORD", "") - def send(self, to_addr, subject, content): + def send(self, to_addr, subject, content, appendix=None): if (not self.active or not self.hostname or not self.username or not self.password or not self.from_addr): return - msg = MIMEMultipart("alternative") + msg = MIMEMultipart("mixed") # todo: test if clients accept attachment-free mails set to multipart/mixed msg["From"] = self.from_addr msg["To"] = to_addr msg["Subject"] = subject msg.attach(MIMEText(content, _charset="utf-8")) + if appendix is not None: + for name, file_like in appendix: + part = MIMEApplication(file_like.read(), "octet-stream") + part["Content-Disposition"] = 'attachment; filename="{}"'.format(name) + msg.attach(part) server = smtplib.SMTP_SSL(self.hostname) server.login(self.username, self.password) server.sendmail(self.from_addr, to_addr, msg.as_string())