diff --git a/migrations/versions/188f389b2286_.py b/migrations/versions/188f389b2286_.py new file mode 100644 index 0000000000000000000000000000000000000000..6e7f9ecd6af55e3649db8e41541ee1073a375e2c --- /dev/null +++ b/migrations/versions/188f389b2286_.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: 188f389b2286 +Revises: 515d261a624b +Create Date: 2017-02-26 12:55:43.761405 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '188f389b2286' +down_revision = '515d261a624b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('todomails', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('mail', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('todomails') + # ### end Alembic commands ### diff --git a/models/database.py b/models/database.py index f2664d3075e28ca1938a743128c3b23206aa1c23..e91c3f4531e35a05909654f60e9ff4f34d794d21 100644 --- a/models/database.py +++ b/models/database.py @@ -5,7 +5,7 @@ import math from io import StringIO, BytesIO from shared import db -from utils import random_string, url_manager, get_etherpad_url +from utils import random_string, url_manager, get_etherpad_url, split_terms from models.errors import DateNotMatchingException import os @@ -326,6 +326,12 @@ class Todo(db.Model): return None return candidates[0] + def get_users(self): + return [ + user.lower().strip() + for user in split_terms(self.who, separators=" ,\t") + ] + def get_state(self): return "[Erledigt]" if self.done else "[Offen]" def get_state_plain(self): @@ -428,3 +434,20 @@ class Error(db.Model): if len(lines) <= 4: return "\n".join(lines) return "\n".join(lines[:2], "…", lines[-2:]) + +class TodoMail(db.Model): + __tablename__ = "todomails" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String, unique=True) + mail = db.Column(db.String) + + def __init__(self, name, mail): + self.name = name + self.mail = mail + + def __repr__(self): + return "<TodoMail(name='{}', mail='{}')>".format( + self.name, self.mail) + + def get_formatted_mail(self): + return "{} <{}>".format(self.name, self.mail) diff --git a/server.py b/server.py index b778c3da577edb07922db662586eaf9d75cb2ae0..798fc9e80a18fda8117e16fea87a825d3b2b7d48 100755 --- a/server.py +++ b/server.py @@ -21,9 +21,9 @@ import math import config from shared import db, date_filter, datetime_filter, date_filter_long, time_filter, ldap_manager, security_manager, current_user, check_login, login_required, group_required, class_filter from utils import is_past, mail_manager, url_manager, get_first_unused_int, set_etherpad_text, get_etherpad_text, split_terms, optional_int_arg -from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error -from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, ProtocolForm, TopForm, SearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm -from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable +from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail +from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, ProtocolForm, TopForm, SearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm +from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable, TodoMailsTable app = Flask(__name__) app.config.from_object(config) @@ -977,6 +977,53 @@ def delete_error(error_id): db.session.commit() flash("Fehler {} gelöscht.".format(name), "alert-success") return redirect(request.args.get("next") or url_for("list_errors")) + +@app.route("/todomails/list") +@login_required +def list_todomails(): + todomails = TodoMail.query.all() + todomails_table = TodoMailsTable(todomails) + return render_template("todomails-list.html", todomails=todomails, todomails_table=todomails_table) + +@app.route("/todomail/new", methods=["GET", "POST"]) +@login_required +def new_todomail(): + form = TodoMailForm() + if form.validate_on_submit(): + todomail = TodoMail(form.name.data, form.mail.data) + db.session.add(todomail) + db.session.commit() + flash("Die Todomailzuordnung für {} wurde angelegt.".format(todomail.name), "alert-success") + return redirect(request.args.get("next") or url_for("list_todomails")) + return render_template("todomail-new.html", form=form) + +@app.route("/todomail/edit/<int:todomail_id>", methods=["GET", "POST"]) +@login_required +def edit_todomail(todomail_id): + todomail = TodoMail.query.filter_by(id=todomail_id).first() + if todomail is None: + flash("Invalide Todo-Mail-Zuordnung.", "alert-error") + return redirect(request.args.get("next") or url_for("list_todomails")) + form = TodoMailForm(obj=todomail) + if form.validate_on_submit(): + form.populate_obj(todomail) + db.session.commit() + flash("Die Todo-Mail-Zuordnung wurde geändert.", "alert-success") + return redirect(request.args.get("next") or url_for("list_todomails")) + return render_template("todomail-edit.html", todomail=todomail, form=form) + +@app.route("/todomail/delete/<int:todomail_id>") +@login_required +def delete_todomail(todomail_id): + todomail = TodoMail.query.filter_by(id=todomail_id).first() + if todomail is None: + flash("Invalide Todomailzuordnung.", "alert-error") + return redirect(request.args.get("next") or url_for("list_todomails")) + name = todomail.name + db.session.delete(todomail) + db.session.commit() + flash("Die Todo-Mail-Zuordnung für {} wurde gelöscht.".format(name), "alert-success") + return redirect(request.args.get("next") or url_for("list_todomails")) @app.route("/login", methods=["GET", "POST"]) diff --git a/tasks.py b/tasks.py index a60fb7064c772424963ebba60f4de5acf2878f11..5ea7a53cb7adf66e9a084e857b6e727c87431f30 100644 --- a/tasks.py +++ b/tasks.py @@ -5,7 +5,7 @@ import subprocess import shutil import tempfile -from models.database import Document, Protocol, Error, Todo, Decision, TOP, DefaultTOP, MeetingReminder +from models.database import Document, Protocol, Error, Todo, Decision, TOP, DefaultTOP, MeetingReminder, TodoMail from models.errors import DateNotMatchingException from server import celery, app from shared import db, escape_tex, unhyphen, date_filter, datetime_filter, date_filter_long, date_filter_short, time_filter, class_filter @@ -324,6 +324,7 @@ def send_reminder_async(reminder_id, protocol_id): def send_protocol(protocol): send_protocol_async.delay(protocol.id, show_private=True) send_protocol_async.delay(protocol.id, show_private=False) + send_todomails_async.delay(protocol.id) @celery.task def send_protocol_async(protocol_id, show_private): @@ -338,6 +339,27 @@ def send_protocol_async(protocol_id, show_private): ] send_mail(protocol, to_addr, subject, mail_content, appendix) +@celery.task +def send_todomails_async(protocol_id): + with app.app_context(): + protocol = Protocol.query.filter_by(id=protocol_id).first() + all_todos = Todo.query.filter(Todo.done == False).all() + users = {user for todo in all_todos for user in todo.get_users()} + grouped_todos = { + user: [todo for todo in all_todos if user in todo.get_users()] + for user in users + } + subject = "Du hast noch was zu tun!" + for user in users: + todomail = TodoMail.query.filter(TodoMail.name.ilike(user)).first() + if todomail is None: + error = protocol.create_error("Sending Todomail", "Sending Todomail failed.", "User {} has no Todo-Mail-Assignment.".format(user)) + db.session.add(error) + db.session.commit() + continue + to_addr = todomail.get_formatted_mail() + mail_content = render_template("todo-mail.txt", protocol=protocol, todomail=todomail, todos=grouped_todos[user]) + send_mail(protocol, to_addr, subject, mail_content) def send_mail(protocol, to_addr, subject, content, appendix=None): if to_addr is not None and len(to_addr.strip()) > 0: diff --git a/templates/layout.html b/templates/layout.html index e141b04743902e49572b8cff975b732249c4a338..c8b88d049f7db0837159b8bad6474e589a56dd36 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -27,7 +27,7 @@ <div id="navbar" class="navbar-collapse collapse"> <ul class="nav navbar-nav"> {% if check_login() %} - <li><a href="{{url_for("new_protocol")}}">Neues Protokoll</a></li> + <li><a href="{{url_for("new_protocol")}}">Neu</a></li> {% endif %} <li><a href="{{url_for("list_protocols")}}">Protokolle</a></li> {% if check_login() %} @@ -37,6 +37,7 @@ {% if check_login() %} <li><a href="{{url_for("list_types")}}">Typen</a></li> <li><a href="{{url_for("list_errors")}}">Fehler</a></li> + <li><a href="{{url_for("list_todomails")}}">Todo Mails</a></li> {% endif %} {# todo: add more links #} </ul> diff --git a/templates/todo-mail.txt b/templates/todo-mail.txt new file mode 100644 index 0000000000000000000000000000000000000000..a377bf60f971c9056776c06768a94f47f8fb013b --- /dev/null +++ b/templates/todo-mail.txt @@ -0,0 +1,13 @@ +Hallo {{todomail.name}}, + +Du hast für "{{protocol.protocoltype.name}}" noch offene Todos: + +{% for todo in todos %} +{{todo.who}}: + {{todo.description}} +{% endfor %} + +Fühle die hiermit daran erinnert! + +Viele Grüße, +Dein Protokollsystem diff --git a/templates/todomail-edit.html b/templates/todomail-edit.html new file mode 100644 index 0000000000000000000000000000000000000000..6845b49f4c657f24fcb81012f2c348a175a5ef6a --- /dev/null +++ b/templates/todomail-edit.html @@ -0,0 +1,9 @@ +{% extends "layout.html" %} +{% from "macros.html" import render_form %} +{% block title %}Todomail ändern{% endblock %} + +{% block content %} +<div class="container"> + {{render_form(form, action_url=url_for("edit_todomail", todomail_id=todomail.id), action_text="Ändern")}} +</div> +{% endblock %} diff --git a/templates/todomail-new.html b/templates/todomail-new.html new file mode 100644 index 0000000000000000000000000000000000000000..a34e1f7dcefd8bd79ccb4affa756591113851ce2 --- /dev/null +++ b/templates/todomail-new.html @@ -0,0 +1,9 @@ +{% extends "layout.html" %} +{% from "macros.html" import render_form %} +{% block title %}Neue Todomail{% endblock %} + +{% block content %} +<div class="container"> + {{render_form(form, action_url=url_for("new_todomail"), action_text="Ändern")}} +</div> +{% endblock %} diff --git a/templates/todomails-list.html b/templates/todomails-list.html new file mode 100644 index 0000000000000000000000000000000000000000..727615caa8a139ecca0b5a43ff7c2c6e5e54bcb8 --- /dev/null +++ b/templates/todomails-list.html @@ -0,0 +1,9 @@ +{% extends "layout.html" %} +{% from "macros.html" import render_table %} +{% block title %}Todomails{% endblock %} + +{% block content %} +<div class="container"> + {{render_table(todomails_table)}} +</div> +{% endblock %} diff --git a/utils.py b/utils.py index d98df8186029fcac15db567db3e92862d0498762..efe13167bdf6078c8a3adb2eef31c5a3e9b1118c 100644 --- a/utils.py +++ b/utils.py @@ -75,6 +75,7 @@ class MailManager: or not self.username or not self.password or not self.from_addr): + print("Not sending mail {} to {}".format(subject, to_addr)) return msg = MIMEMultipart("mixed") # todo: test if clients accept attachment-free mails set to multipart/mixed msg["From"] = self.from_addr diff --git a/views/forms.py b/views/forms.py index 9a5bb77b311c4f4d22043aebf9721e9e0a837f09..6e91d33d24760646ee744e435dec6081cf860373 100644 --- a/views/forms.py +++ b/views/forms.py @@ -108,3 +108,7 @@ class TodoForm(FlaskForm): description = StringField("Aufgabe", validators=[InputRequired("Bitte gib an, was erledigt werden soll.")]) tags = StringField("Weitere Tags") done = BooleanField("Erledigt") + +class TodoMailForm(FlaskForm): + name = StringField("Name", validators=[InputRequired("Du musst den Namen angeben, der zugeordnet werden soll.")]) + mail = StringField("Mail", validators=[InputRequired("Du musst die Mailadresse angeben, die zugeordnet werden soll.")]) diff --git a/views/tables.py b/views/tables.py index 2c09ec9ca3494f51ee7675be564fbe3e61c35bfb..f7ab1f9ae83e572e04f471b4511cb38bb5875bda 100644 --- a/views/tables.py +++ b/views/tables.py @@ -314,3 +314,21 @@ class DocumentsTable(Table): if document.protocol.protocoltype.has_modify_right(user) else "" ] + +class TodoMailsTable(Table): + def __init__(self, todomails): + super().__init__("Todo-Mail-Zuordnungen", todomails, url_for("new_todomail")) + + def headers(self): + return ["Name", "Mail", ""] + + def row(self, todomail): + return [ + todomail.name, + todomail.mail, + Table.concat([ + Table.link(url_for("edit_todomail", todomail_id=todomail.id), "Ändern"), + Table.link(url_for("delete_todomail", todomail_id=todomail.id), "Löschen", confirm="Bist du dir sicher, dass du die Todomailzuordnung {} zu {} löschen willst?".format(todomail.name, todomail.mail)) + ]) + ] +