Commit 4f136efa authored by Robin Sonnabend's avatar Robin Sonnabend
Browse files

Invitation mails

parent 00ede38d
"""empty message
Revision ID: 515d261a624b
Revises: 77bf71eef07f
Create Date: 2017-02-26 00:33:13.555804
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '515d261a624b'
down_revision = '77bf71eef07f'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('protocoltypes', sa.Column('usual_time', sa.Time(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('protocoltypes', 'usual_time')
# ### end Alembic commands ###
"""empty message
Revision ID: 77bf71eef07f
Revises: f91d760158dc
Create Date: 2017-02-26 00:26:48.499578
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '77bf71eef07f'
down_revision = 'f91d760158dc'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('meetingreminders', sa.Column('additional_text', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('meetingreminders', 'additional_text')
# ### end Alembic commands ###
......@@ -21,6 +21,7 @@ class ProtocolType(db.Model):
name = db.Column(db.String, unique=True)
short_name = db.Column(db.String, unique=True)
organization = db.Column(db.String)
usual_time = db.Column(db.Time)
is_public = db.Column(db.Boolean)
private_group = db.Column(db.String)
public_group = db.Column(db.String)
......@@ -36,12 +37,13 @@ class ProtocolType(db.Model):
reminders = relationship("MeetingReminder", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="MeetingReminder.days_before")
todos = relationship("Todo", backref=backref("protocoltype"), order_by="Todo.id")
def __init__(self, name, short_name, organization,
def __init__(self, name, short_name, organization, usual_time,
is_public, private_group, public_group, private_mail, public_mail,
use_wiki, wiki_category, wiki_only_public, printer):
self.name = name
self.short_name = short_name
self.organization = organization
self.usual_time = usual_time
self.is_public = is_public
self.private_group = private_group
self.public_group = public_group
......@@ -56,11 +58,11 @@ class ProtocolType(db.Model):
return ("<ProtocolType(id={}, short_name={}, name={}, "
"organization={}, is_public={}, private_group={}, "
"public_group={}, use_wiki={}, wiki_category='{}', "
"wiki_only_public={}, printer={})>".format(
"wiki_only_public={}, printer={}, usual_time={})>".format(
self.id, self.short_name, self.name,
self.organization, self.is_public, self.private_group,
self.public_group, self.use_wiki, self.wiki_category,
self.wiki_only_public, self.printer))
self.wiki_only_public, self.printer, self.usual_time))
def get_latest_protocol(self):
candidates = sorted([protocol for protocol in self.protocols if protocol.is_done()], key=lambda p: p.date, reverse=True)
......@@ -385,12 +387,14 @@ class MeetingReminder(db.Model):
days_before = db.Column(db.Integer)
send_public = db.Column(db.Boolean)
send_private = db.Column(db.Boolean)
additional_text = db.Column(db.String)
def __init__(self, protocoltype_id, days_before, send_public, send_private):
def __init__(self, protocoltype_id, days_before, send_public, send_private, additional_text):
self.protocoltype_id = protocoltype_id
self.days_before = days_before
self.send_public = send_public
self.send_private = send_private
self.additional_text = additional_text
def __repr__(self):
return "<MeetingReminder(id={}, protocoltype_id={}, days_before={}, send_public={}, send_private={})>".format(
......
alembic==0.8.10
amqp==2.1.4
appdirs==1.4.0
APScheduler==3.3.1
argh==0.26.2
billiard==3.5.0.2
blessings==1.6
......@@ -37,6 +38,7 @@ regex==2017.2.8
requests==2.13.0
six==1.10.0
SQLAlchemy==1.1.5
tzlocal==1.3
vine==1.1.3
watchdog==0.8.3
wcwidth==0.1.7
......
......@@ -9,6 +9,10 @@ from flask_migrate import Migrate, MigrateCommand
#from flask_socketio import SocketIO
from celery import Celery
from sqlalchemy import or_, and_
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger
import atexit
from io import StringIO, BytesIO
import os
from datetime import datetime
......@@ -39,6 +43,17 @@ celery = make_celery(app, config)
# return socketio
#socketio = make_socketio(app, config)
def make_scheduler(app, config, function):
scheduler = BackgroundScheduler()
scheduler.start()
scheduler.add_job(
func=function,
trigger=CronTrigger(hour='*'),
id="scheduler",
name="Do an action regularly",
replace_existing=True)
atexit.register(scheduler.shutdown)
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
app.jinja_env.filters["datify"] = date_filter
......@@ -87,7 +102,7 @@ def new_type():
flash("Du kannst keinen internen Protokolltypen anlegen, zu dem du selbst keinen Zugang hast.", "alert-error")
else:
protocoltype = ProtocolType(form.name.data, form.short_name.data,
form.organization.data, form.is_public.data,
form.organization.data, form.usual_time.data, form.is_public.data,
form.private_group.data, form.public_group.data,
form.private_mail.data, form.public_mail.data,
form.use_wiki.data, form.wiki_category.data,
......@@ -148,7 +163,7 @@ def new_reminder(type_id):
return redirect(request.args.get("next") or url_for("index"))
form = MeetingReminderForm()
if form.validate_on_submit():
reminder = MeetingReminder(protocoltype.id, form.days_before.data, form.send_public.data, form.send_private.data)
reminder = MeetingReminder(protocoltype.id, form.days_before.data, form.send_public.data, form.send_private.data, form.additional_text.data)
db.session.add(reminder)
db.session.commit()
return redirect(request.args.get("next") or url_for("show_type", type_id=protocoltype.id))
......@@ -365,7 +380,7 @@ def list_protocols():
for text, matched in parts
]))
search_results[protocol] = "<br />\n".join(formatted_lines)
protocols = sorted(protocols, key=lambda protocol: protocol.date, reverse=True)
protocols = sorted(protocols, key=lambda protocol: protocol.date, reverse=True)
page = _get_page()
page_count = int(math.ceil(len(protocols)) / config.PAGE_LENGTH)
if page >= page_count:
......@@ -809,6 +824,20 @@ def logout():
flash("You are not logged in.", "alert-error")
return redirect(url_for(".index"))
def check_and_send_reminders():
with app.app_context():
current_time = datetime.now()
current_day = current_time.date()
for protocol in Protocol.query.filter(Protocol.done == False).all():
day_difference = (protocol.date - current_day).days
usual_time = protocol.protocoltype.usual_time
protocol_time = datetime(1, 1, 1, usual_time.hour, usual_time.minute)
hour_difference = (protocol_time - current_time).seconds // 3600
print(protocol.get_identifier(), day_difference, hour_difference)
for reminder in protocol.protocoltype.reminders:
if day_difference == reminder.days_before and hour_difference == 0:
tasks.send_reminder(reminder, protocol)
if __name__ == "__main__":
make_scheduler(app, config, check_and_send_reminders)
manager.run()
......@@ -5,7 +5,7 @@ import subprocess
import shutil
import tempfile
from models.database import Document, Protocol, Error, Todo, Decision, TOP, DefaultTOP
from models.database import Document, Protocol, Error, Todo, Decision, TOP, DefaultTOP, MeetingReminder
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
......@@ -308,21 +308,33 @@ def print_file_async(filename, protocol_id):
db.session.add(error)
db.session.commit()
def send_mail(mail):
send_mail_async.delay(mail.id)
def send_reminder(reminder, protocol):
send_reminder_async.delay(reminder.id, protocol.id)
@celery.task
def send_mail_async(mail_id):
def send_reminder_async(reminder_id, protocol_id):
with app.app_context():
mail = Mail.query.filter_by(id=mail_id).first()
if mail is None:
return False
mail.ready = False
mail.error = False
db.session.commit()
result = mail_manager.send(mail.to_addr, mail.subject, mail.content)
mail.ready = True
mail.error = not result
db.session.commit()
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)
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):
if to_addr is not None and len(to_addr.strip()) > 0:
send_mail_async.delay(protocol.id, to_addr, subject, content)
@celery.task
def send_mail_async(protocol_id, to_addr, subject, content):
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)
except Exception as exc:
error = protocol.create_error("Sending Mail", "Sending mail failed", str(exc))
db.session.add(error)
db.session.commit()
Die nächste {{protocol.protocoltype.name}} findet am {{protocol.date|datify}} um {{protocol.protocoltype.usual_time|timify}} statt.
Die vorläufige 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 %}
{% if reminder.additional_text is not none %}
{{reminder.additional_text}}
{% endif %}
......@@ -67,7 +67,6 @@ class MailManager:
self.hostname = getattr(config, "MAIL_HOST", "")
self.username = getattr(config, "MAIL_USER", "")
self.password = getattr(config, "MAIL_PASSWORD", "")
self.prefix = getattr(config, "MAIL_PREFIX", "")
def send(self, to_addr, subject, content):
if (not self.active
......@@ -75,21 +74,16 @@ class MailManager:
or not self.username
or not self.password
or not self.from_addr):
return True
try:
msg = MIMEMultipart("alternative")
msg["From"] = self.from_addr
msg["To"] = to_addr
msg["Subject"] = "[{}] {}".format(self.prefix, subject) if self.prefix else subject
msg.attach(MIMEText(content, _charset="utf-8"))
server = smtplib.SMTP_SSL(self.hostname)
server.login(self.username, self.password)
server.sendmail(self.from_addr, to_addr, msg.as_string())
server.quit()
except Exception as e:
print(e)
return False
return True
return
msg = MIMEMultipart("alternative")
msg["From"] = self.from_addr
msg["To"] = to_addr
msg["Subject"] = subject
msg.attach(MIMEText(content, _charset="utf-8"))
server = smtplib.SMTP_SSL(self.hostname)
server.login(self.username, self.password)
server.sendmail(self.from_addr, to_addr, msg.as_string())
server.quit()
mail_manager = MailManager(config)
......
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, DateField, HiddenField, IntegerField, SelectField, FileField, DateTimeField
from wtforms import StringField, PasswordField, BooleanField, DateField, HiddenField, IntegerField, SelectField, FileField, DateTimeField, TextAreaField
from wtforms.validators import InputRequired, Optional
import config
......@@ -12,6 +12,7 @@ class ProtocolTypeForm(FlaskForm):
name = StringField("Name", validators=[InputRequired("Du musst einen Namen angeben.")])
short_name = StringField("Abkürzung", validators=[InputRequired("Du musst eine Abkürzung angebene.")])
organization = StringField("Organisation", validators=[InputRequired("Du musst eine zugehörige Organisation angeben.")])
usual_time = DateTimeField("Üblicher Beginn", validators=[InputRequired("Bitte gib die Zeit an, zu der die Sitzung beginnt.")], format="%H:%M")
is_public = BooleanField("Öffentlich sichtbar")
private_group = StringField("Interne Gruppe")
public_group = StringField("Öffentliche Gruppe")
......@@ -30,6 +31,7 @@ class MeetingReminderForm(FlaskForm):
days_before = IntegerField("Tage vor Sitzung", validators=[InputRequired("Du musst eine Dauer angeben.")])
send_public = BooleanField("Öffentlich einladen")
send_private = BooleanField("Intern einladen")
additional_text = TextAreaField("Zusätzlicher Mailinhalt")
class NewProtocolForm(FlaskForm):
protocoltype = SelectField("Typ", choices=[], coerce=int)
......
......@@ -105,8 +105,8 @@ class ProtocolTypeTable(SingleValueTable):
super().__init__(protocoltype.name, protocoltype, newlink=url_for("edit_type", type_id=protocoltype.id))
def headers(self):
headers = ["Name", "Abkürzung", "Organisation", "Öffentlich",
"Interne Gruppe", "Öffentliche Gruppe",
headers = ["Name", "Abkürzung", "Organisation", "Beginn",
"Öffentlich", "Interne Gruppe", "Öffentliche Gruppe",
"Interner Verteiler", "Öffentlicher Verteiler",
"Drucker", "Wiki"]
if self.value.use_wiki:
......@@ -118,6 +118,7 @@ class ProtocolTypeTable(SingleValueTable):
self.value.name,
self.value.short_name,
self.value.organization,
self.value.usual_time.strftime("%H:%M") if self.value.usual_time is not None else "", # todo: remove if, this field is required
Table.bool(self.value.is_public),
self.value.private_group,
self.value.public_group,
......@@ -156,12 +157,13 @@ class MeetingRemindersTable(Table):
self.protocoltype = protocoltype
def headers(self):
return ["Zeit", "Einladen", ""]
return ["Zeit", "Einladen", "Zusätzlicher Mailinhalt", ""]
def row(self, reminder):
return [
"{} Tage".format(reminder.days_before),
self.get_send_summary(reminder),
reminder.additional_text or "",
Table.concat([
Table.link(url_for("edit_reminder", type_id=self.protocoltype.id, reminder_id=reminder.id), "Ändern"),
Table.link(url_for("delete_reminder", type_id=self.protocoltype.id, reminder_id=reminder.id), "Löschen", confirm="Bist du dir sicher, dass du die Einladungsmail {} Tage vor der Sitzung löschen willst?".format(reminder.days_before))
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment