server.py 64 KB
Newer Older
1
2
3
4
#!/usr/bin/env python3
import locale
locale.setlocale(locale.LC_TIME, "de_DE.utf8")

Robin Sonnabend's avatar
Robin Sonnabend committed
5
from flask import Flask, g, current_app, request, session, flash, redirect, url_for, abort, render_template, Response, send_file as flask_send_file, Markup
6
from werkzeug.utils import secure_filename
7
8
9
10
from flask_script import Manager, prompt
from flask_migrate import Migrate, MigrateCommand
#from flask_socketio import SocketIO
from celery import Celery
Robin Sonnabend's avatar
Robin Sonnabend committed
11
from sqlalchemy import or_, and_
12
13
14
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger
Robin Sonnabend's avatar
Robin Sonnabend committed
15
import atexit
16
import feedgen.feed
17
import icalendar
Robin Sonnabend's avatar
Robin Sonnabend committed
18
from io import StringIO, BytesIO
19
import os
20
from datetime import datetime, time, timedelta
Robin Sonnabend's avatar
Robin Sonnabend committed
21
import math
22
import mimetypes
23
import subprocess
24
from dateutil import tz
25
26

import config
Robin Sonnabend's avatar
Robin Sonnabend committed
27
from shared import db, date_filter, datetime_filter, date_filter_long, date_filter_short, time_filter, time_filter_short, user_manager, security_manager, current_user, check_login, login_required, group_required, class_filter, needs_date_test, todostate_name_filter, code_filter, indent_tab_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
28
from utils import is_past, mail_manager, get_first_unused_int, set_etherpad_text, get_etherpad_text, split_terms, optional_int_arg, fancy_join, footnote_hash
Robin Sonnabend's avatar
Robin Sonnabend committed
29
from decorators import db_lookup, require_public_view_right, require_private_view_right, require_modify_right, require_publish_right, require_admin_right
Robin Sonnabend's avatar
Robin Sonnabend committed
30
from models.database import ProtocolType, Protocol, DefaultTOP, TOP, LocalTOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail, DecisionDocument, TodoState, Meta, DefaultMeta, DecisionCategory, Like
Robin Sonnabend's avatar
Robin Sonnabend committed
31
from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, generate_protocol_form, TopForm, LocalTopForm, SearchForm, DecisionSearchForm, ProtocolSearchForm, TodoSearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm, DefaultMetaForm, MetaForm, MergeTodosForm, DecisionCategoryForm, DocumentEditForm
32
from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable, TodoMailsTable, DefaultMetasTable, DecisionCategoriesTable
33
from legacy import import_old_todos, import_old_protocols, import_old_todomails
34
import back
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

app = Flask(__name__)
app.config.from_object(config)
db.init_app(app)
migrate = Migrate(app, db)
manager = Manager(app)
manager.add_command("db", MigrateCommand)

def make_celery(app, config):
    celery = Celery(app.import_name, broker=config.CELERY_BROKER_URL)
    celery.conf.update(app.config)
    return celery
celery = make_celery(app, config)

#def make_socketio(app, config):
#    socketio = SocketIO(app)
#    return socketio
#socketio = make_socketio(app, config)

app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
app.jinja_env.filters["datify"] = date_filter
app.jinja_env.filters["datetimify"] = datetime_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
58
app.jinja_env.filters["timify"] = time_filter
59
app.jinja_env.filters["timify_short"] = time_filter_short
Robin Sonnabend's avatar
Robin Sonnabend committed
60
app.jinja_env.filters["datify_short"] = date_filter_short
Robin Sonnabend's avatar
Robin Sonnabend committed
61
app.jinja_env.filters["datify_long"] = date_filter_long
Robin Sonnabend's avatar
Robin Sonnabend committed
62
#app.jinja_env.filters["url_complete"] = url_manager.complete
63
app.jinja_env.filters["class"] = class_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
64
65
app.jinja_env.filters["todo_get_name"] = todostate_name_filter
app.jinja_env.filters["code"] = code_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
66
app.jinja_env.filters["indent_tab"] = indent_tab_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
67
app.jinja_env.filters["fancy_join"] = fancy_join
Robin Sonnabend's avatar
Robin Sonnabend committed
68
app.jinja_env.filters["footnote_hash"] = footnote_hash
69
app.jinja_env.tests["auth_valid"] = security_manager.check_user
Robin Sonnabend's avatar
Robin Sonnabend committed
70
app.jinja_env.tests["needs_date"] = needs_date_test
71

72
73
74
75
76
77
additional_templates = getattr(config, "LATEX_LOCAL_TEMPLATES", None)
if additional_templates is not None and os.path.isdir(additional_templates):
    if additional_templates not in app.jinja_loader.searchpath:
        app.jinja_loader.searchpath.append(additional_templates)
    

78
79
import tasks

80
81
app.jinja_env.globals.update(check_login=check_login)
app.jinja_env.globals.update(current_user=current_user)
Robin Sonnabend's avatar
Robin Sonnabend committed
82
app.jinja_env.globals.update(zip=zip)
Robin Sonnabend's avatar
Robin Sonnabend committed
83
84
85
app.jinja_env.globals.update(min=min)
app.jinja_env.globals.update(max=max)
app.jinja_env.globals.update(dir=dir)
Robin Sonnabend's avatar
Robin Sonnabend committed
86
app.jinja_env.globals.update(now=datetime.now)
87

88
89
90
91
92
93
94
95
96
97
98
99
def get_git_revision():
    gitlab_url = "https://git.fsmpi.rwth-aachen.de/protokollsystem/proto3"
    commit_hash = subprocess.check_output(["git", "log", "-g", "-1", "--pretty=%H"]).decode("UTF-8").strip()
    timestamp = int(subprocess.check_output(["git", "log", "-g", "-1", "--pretty=%at"]).strip())
    commit_date = datetime.fromtimestamp(timestamp)
    return {"url": gitlab_url, "hash": commit_hash, "date": commit_date}

try:
    app.jinja_env.globals["git_revision"] = get_git_revision()
except:
    pass

100
101
# blueprints here

102
103
@manager.command
def import_legacy():
104
    """Import the old todos and protocols from an sql dump"""
105
    filename = prompt("SQL-file")
106
    #filename = "legacy.sql"
107
108
    with open(filename, "rb") as sqlfile:
        content = sqlfile.read().decode("utf-8")
109
110
        import_old_todos(content)
        import_old_protocols(content)
111
        import_old_todomails(content)
112

113
114
115
116
@manager.command
def recompile_all():
    for protocol in sorted(Protocol.query.all(), key=lambda p: p.date):
        if protocol.is_done():
117
            print(protocol.get_short_identifier())
118
119
120
121
122
123
124
125
126
127
128
129
            tasks.parse_protocol(protocol)

@manager.command
def merge_todos():
    todo_by_id = {}
    todos = Todo.query.all()
    for todo in todos:
        todo_id = todo.get_id()
        if todo_id in todo_by_id:
            todo1, todo2 = todo, todo_by_id[todo_id]
            print(todo1)
            print(todo2)
130
            if todo2.state.value > todo1.state.value:
131
132
133
134
135
136
137
138
139
140
141
142
143
                todo2, todo1 = todo1, todo2
            for protocol in todo2.protocols:
                if protocol not in todo1.protocols:
                    todo1.protocols.append(protocol)
                todo2.protocols.remove(protocol)
            db.session.delete(todo2)
            db.session.commit()
            todo_by_id[todo_id] = todo1
        else:
            todo_by_id[todo_id] = todo

@manager.command
def runserver():
144
    app.run()
145
146
    make_scheduler()

147
148
149
150
# cause uwsgi currently has a bug
def send_file(file_like, cache_timeout, as_attachment, attachment_filename):
    mimetype, _ = mimetypes.guess_type(attachment_filename)
    response = Response(file_like.read(), mimetype)
151
152
153
154
155
156
157
158
    if as_attachment:
        response.headers["Content-Disposition"] = 'attachment; filename="{}"'.format(attachment_filename)
    content_type = mimetype
    if mimetype.startswith("text/"):
        content_type = "{}; charset=utf-8".format(content_type)
    response.headers["Content-Type"] = content_type
    response.headers["Cache-Control"] = "public, max-age={}".format(cache_timeout)
    response.headers["Connection"] = "close"
159
    return response
160

161
@app.route("/")
162
@back.anchor
163
def index():
Robin Sonnabend's avatar
Robin Sonnabend committed
164
165
166
    user = current_user()
    protocols = [
        protocol for protocol in Protocol.query.all()
167
168
        if protocol.protocoltype.has_public_view_right(user,
            check_networks=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
169
    ]
170
    def _protocol_sort_key(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
171
172
173
        if protocol.date is not None:
            return protocol.date
        return datetime.now().date()
Robin Sonnabend's avatar
Robin Sonnabend committed
174
    current_day = datetime.now().date()
175
    open_protocols = sorted(
Robin Sonnabend's avatar
Robin Sonnabend committed
176
177
178
179
        [
            protocol for protocol in protocols
            if not protocol.done
            and (protocol.date - current_day).days < config.MAX_INDEX_DAYS
180
            and (current_day - protocol.date).days < config.MAX_PAST_INDEX_DAYS
Robin Sonnabend's avatar
Robin Sonnabend committed
181
        ],
182
        key=_protocol_sort_key
Robin Sonnabend's avatar
Robin Sonnabend committed
183
184
    )
    finished_protocols = sorted(
185
186
        [
            protocol for protocol in protocols
187
188
189
            if protocol.done and protocol.public
            and (protocol.has_private_view_right(user)
                or protocol.protocoltype.has_public_view_right(user, check_networks=False))
190
        ],
191
192
        key=_protocol_sort_key,
        reverse=True
Robin Sonnabend's avatar
Robin Sonnabend committed
193
    )
194
195
196
197
198
199
200
    protocol = None
    show_private = False
    has_public_view_right = False
    if len(finished_protocols) > 0:
        protocol = finished_protocols[0]
        show_private = protocol.has_private_view_right(user)
        has_public_view_right = protocol.protocoltype.has_public_view_right(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
201
202
203
    todos = None
    if check_login():
        todos = [
204
            todo for todo in Todo.query.all()
205
            if todo.protocoltype.has_private_view_right(user)
206
            and not todo.is_done()
Robin Sonnabend's avatar
Robin Sonnabend committed
207
        ]
208
209
210
211
212
213
        user_todos = [
            todo for todo in todos
            if user.username.lower() in list(map(str.strip, todo.who.lower().split(",")))
        ]
        if len(user_todos) > 0:
            todos = user_todos
214
215
        def _todo_sort_key(todo):
            protocol = todo.get_first_protocol()
216
            return protocol.date if protocol is not None and protocol.date is not None else datetime.now().date()
217
        todos = sorted(todos, key=_todo_sort_key, reverse=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
218
    todos_table = TodosTable(todos) if todos is not None else None
219
    return render_template("index.html", open_protocols=open_protocols, protocol=protocol, todos=todos, show_private=show_private, has_public_view_right=has_public_view_right)
220

Robin Sonnabend's avatar
Robin Sonnabend committed
221
@app.route("/documentation")
222
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
223
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
224
def documentation():
Robin Sonnabend's avatar
Robin Sonnabend committed
225
226
227
    todostates = list(TodoState)
    name_to_state = TodoState.get_name_to_state()
    return render_template("documentation.html", todostates=todostates, name_to_state=name_to_state)
Robin Sonnabend's avatar
Robin Sonnabend committed
228

Robin Sonnabend's avatar
Robin Sonnabend committed
229
@app.route("/types/list")
230
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
231
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
232
233
234
235
236
def list_types():
    is_logged_in = check_login()
    user = current_user()
    types = [
        protocoltype for protocoltype in ProtocolType.query.all()
237
238
        if (protocoltype.has_private_view_right(user)
        or protocoltype.has_public_view_right(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
239
        or protocoltype.is_public)]
240
    types = sorted(types, key=lambda t: t.short_name)
Robin Sonnabend's avatar
Robin Sonnabend committed
241
242
243
244
245
246
247
248
249
250
251
252
    types_table = ProtocolTypesTable(types)
    return render_template("types-list.html", types=types, types_table=types_table)

@app.route("/type/new", methods=["GET", "POST"])
@login_required
def new_type():
    form = ProtocolTypeForm()
    if form.validate_on_submit():
        user = current_user()
        if form.private_group.data not in user.groups:
            flash("Du kannst keinen internen Protokolltypen anlegen, zu dem du selbst keinen Zugang hast.", "alert-error")
        else:
253
254
            protocoltype = ProtocolType()
            form.populate_obj(protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
255
256
257
            db.session.add(protocoltype)
            db.session.commit()
            flash("Der Protokolltyp {} wurde angelegt.".format(protocoltype.name), "alert-success")
258
        return back.redirect("list_types")
Robin Sonnabend's avatar
Robin Sonnabend committed
259
260
    return render_template("type-new.html", form=form)

Robin Sonnabend's avatar
Robin Sonnabend committed
261
@app.route("/type/edit/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
262
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
263
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
264
@require_private_view_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
265
def edit_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
266
267
268
269
270
271
272
273
    user = current_user()
    form = ProtocolTypeForm(obj=protocoltype)
    if form.validate_on_submit():
        if form.private_group.data not in user.groups:
            flash("Du kannst keinen internen Protokolltypen anlegen, zu dem du selbst keinen Zugang hast.", "alert-error")
        else:
            form.populate_obj(protocoltype)
            db.session.commit()
274
            return back.redirect("show_type", protocoltype_id=protocoltype.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
275
276
    return render_template("type-edit.html", form=form, protocoltype=protocoltype)

Robin Sonnabend's avatar
Robin Sonnabend committed
277
@app.route("/type/show/<int:protocoltype_id>")
278
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
279
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
280
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
281
@require_private_view_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
282
def show_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
283
284
    protocoltype_table = ProtocolTypeTable(protocoltype)
    default_tops_table = DefaultTOPsTable(protocoltype.default_tops, protocoltype)
285
    reminders_table = MeetingRemindersTable(protocoltype.reminders, protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
286
    metas_table = DefaultMetasTable(protocoltype.metas, protocoltype)
287
288
    categories_table = DecisionCategoriesTable(protocoltype.decisioncategories, protocoltype)
    return render_template("type-show.html", protocoltype=protocoltype, protocoltype_table=protocoltype_table, default_tops_table=default_tops_table, metas_table=metas_table, reminders_table=reminders_table, mail_active=config.MAIL_ACTIVE, categories_table=categories_table)
289

Robin Sonnabend's avatar
Robin Sonnabend committed
290
@app.route("/type/delete/<int:protocoltype_id>")
291
@login_required
292
@group_required(config.ADMIN_GROUP)
Robin Sonnabend's avatar
Robin Sonnabend committed
293
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
294
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
295
def delete_type(protocoltype):
296
297
298
299
    name = protocoltype.name
    db.session.delete(protocoltype) 
    db.session.commit()
    flash("Der Protokolltype {} wurde gelöscht.".format(name), "alert-success")
300
    return back.redirect("list_types")
301

Robin Sonnabend's avatar
Robin Sonnabend committed
302
@app.route("/type/reminders/new/<int:protocoltype_id>", methods=["GET", "POST"])
303
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
304
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
305
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
306
def new_reminder(protocoltype):
307
308
    form = MeetingReminderForm()
    if form.validate_on_submit():
309
310
        meetingreminder = MeetingReminder(protocoltype_id=protocoltype.id)
        form.populate_obj(meetingreminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
311
        db.session.add(meetingreminder)
312
        db.session.commit()
313
        return back.redirect("show_type", protocoltype_id=protocoltype.id)
314
315
    return render_template("reminder-new.html", form=form, protocoltype=protocoltype)

Robin Sonnabend's avatar
Robin Sonnabend committed
316
@app.route("/type/reminder/edit/<int:meetingreminder_id>", methods=["GET", "POST"])
317
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
318
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
319
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
320
321
def edit_reminder(meetingreminder):
    form = MeetingReminderForm(obj=meetingreminder)
322
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
323
        form.populate_obj(meetingreminder)
324
        db.session.commit()
325
        return back.redirect("show_type", protocoltype_id=protocoltype.id)
326
    return render_template("reminder-edit.html", form=form, meetingreminder=meetingreminder)
327

Robin Sonnabend's avatar
Robin Sonnabend committed
328
@app.route("/type/reminder/delete/<int:meetingreminder_id>")
329
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
330
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
331
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
332
333
334
def delete_reminder(meetingreminder):
    protocoltype = meetingreminder.protocoltype
    db.session.delete(meetingreminder)
335
    db.session.commit()
336
    return back.redirect("show_type", protocoltype_id=protocoltype.id)
337

Robin Sonnabend's avatar
Robin Sonnabend committed
338
@app.route("/type/tops/new/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
339
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
340
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
341
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
342
def new_default_top(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
343
344
    form = DefaultTopForm()
    if form.validate_on_submit():
345
346
        defaulttop = DefaultTOP(protocoltype_id=protocoltype.id)
        form.populate_obj(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
347
        db.session.add(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
348
        db.session.commit()
349
350
351
        for protocol in protocoltype.protocols:
            if not protocol.done:
                localtop = LocalTOP(protocol_id=protocol.id,
Administrator's avatar
Administrator committed
352
                    defaulttop_id=defaulttop.id, description="")
353
354
                db.session.add(localtop)
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
355
        flash("Der Standard-TOP {} wurde für dem Protokolltyp {} hinzugefügt.".format(defaulttop.name, protocoltype.name), "alert-success")
356
        return back.redirect()
Robin Sonnabend's avatar
Robin Sonnabend committed
357
358
    return render_template("default-top-new.html", form=form, protocoltype=protocoltype)

Robin Sonnabend's avatar
Robin Sonnabend committed
359
@app.route("/type/tops/edit/<int:protocoltype_id>/<int:defaulttop_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
360
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
361
@db_lookup(ProtocolType, DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
362
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
363
364
def edit_default_top(protocoltype, defaulttop):
    form = DefaultTopForm(obj=defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
365
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
366
        form.populate_obj(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
367
        db.session.commit()
368
        return back.redirect("show_type", protocoltype_id=protocoltype.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
369
    return render_template("default-top-edit.html", form=form, protocoltype=protocoltype, defaulttop=defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
370

Robin Sonnabend's avatar
Robin Sonnabend committed
371
@app.route("/type/tops/delete/<int:defaulttop_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
372
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
373
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
374
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
375
376
def delete_default_top(defaulttop):
    db.session.delete(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
377
    db.session.commit()
378
    return back.redirect("show_type", protocoltype_id=defaulttop.protocoltype.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
379

Robin Sonnabend's avatar
Robin Sonnabend committed
380
@app.route("/type/tops/move/<int:defaulttop_id>/<diff>/")
Robin Sonnabend's avatar
Robin Sonnabend committed
381
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
382
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
383
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
384
def move_default_top(defaulttop, diff):
Robin Sonnabend's avatar
Robin Sonnabend committed
385
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
386
        defaulttop.number += int(diff)
Robin Sonnabend's avatar
Robin Sonnabend committed
387
388
389
        db.session.commit()
    except ValueError:
        flash("Die angegebene Differenz ist keine Zahl.", "alert-error")
390
    return back.redirect("show_type", protocoltype_id=defaulttop.protocoltype.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
391
392

@app.route("/protocols/list")
393
@back.anchor
394
395
396
def list_protocols():
    is_logged_in = check_login()
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
397
398
399
    protocoltype = None
    protocoltype_id = None
    try:
400
        protocoltype_id = int(request.args.get("protocoltype_id"))
Robin Sonnabend's avatar
Robin Sonnabend committed
401
402
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
403
    state_open = -1
Robin Sonnabend's avatar
Robin Sonnabend committed
404
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
405
        state_open = int(request.args.get("state_open"))
Robin Sonnabend's avatar
Robin Sonnabend committed
406
407
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
408
    search_term = request.args.get("search")
409
    protocoltypes = ProtocolType.get_public_protocoltypes(user, check_networks=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
410
    search_form = ProtocolSearchForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
411
    if protocoltype_id is not None:
412
        search_form.protocoltype_id.data = protocoltype_id
Robin Sonnabend's avatar
Robin Sonnabend committed
413
        protocoltype = ProtocolType.query.filter_by(id=protocoltype_id).first()
Robin Sonnabend's avatar
Robin Sonnabend committed
414
415
    if state_open is not None:
        search_form.state_open.data = state_open
Robin Sonnabend's avatar
Robin Sonnabend committed
416
417
418
    if search_term is not None:
        search_form.search.data = search_term
    protocol_query = Protocol.query
Robin Sonnabend's avatar
Robin Sonnabend committed
419
420
421
422
423
424
425
426
427
    shall_search = search_term is not None and len(search_term.strip()) > 0
    search_terms = []
    if shall_search:
        search_terms = list(map(str.lower, split_terms(search_term)))
        for term in search_terms:
            protocol_query = protocol_query.filter(or_(
                Protocol.content_public.ilike("%{}%".format(term)),
                Protocol.content_private.ilike("%{}%".format(term))
            ))
428
    protocols = [
Robin Sonnabend's avatar
Robin Sonnabend committed
429
        protocol for protocol in protocol_query.all()
430
        if protocol.protocoltype.has_public_view_right(user, check_networks=False)
431
    ]
Robin Sonnabend's avatar
Robin Sonnabend committed
432
433
434
435
436
437
438
439
440
441
442
443
444
    def _matches_search(content):
        content = content.lower()
        for search_term in search_terms:
            if search_term.lower() not in content:
                return False
        return True
    def _matches_search_lazy(content):
        content = content.lower()
        for search_term in search_terms:
            if search_term.lower() in content:
                return True
        return False
    search_results = {} if shall_search else None
Robin Sonnabend's avatar
Robin Sonnabend committed
445
446
447
448
449
    if protocoltype_id is not None and protocoltype_id != -1:
        protocols = [
            protocol for protocol in protocols
            if protocol.protocoltype.id == protocoltype_id
        ]
Robin Sonnabend's avatar
Robin Sonnabend committed
450
451
    if state_open is not None and state_open != -1:
        protocol_done = bool(state_open)
Robin Sonnabend's avatar
Robin Sonnabend committed
452
453
        protocols = [
            protocol for protocol in protocols
454
            if (protocol.is_done() or False) == protocol_done
Robin Sonnabend's avatar
Robin Sonnabend committed
455
        ]
Robin Sonnabend's avatar
Robin Sonnabend committed
456
457
458
459
460
    if shall_search:
        protocols = [
            protocol for protocol in protocols
            if (protocol.protocoltype.has_private_view_right(user)
                and _matches_search(protocol.content_private))
461
            or (protocol.has_public_view_right(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
                and _matches_search(protocol.content_public))
        ]
        for protocol in protocols:
            content = protocol.content_private if protocol.protocoltype.has_private_view_right(user) else protocol.content_public
            lines = content.splitlines()
            matches = [line for line in lines if _matches_search_lazy(line)]
            formatted_lines = []
            for line in matches:
                parts = []
                lower_line = line.lower()
                last_index = 0
                while last_index < len(line):
                    index_candidates = list(filter(lambda t: t[0] != -1, 
                        [(lower_line.find(term, last_index), term) for term in search_terms]))
                    if len(index_candidates) == 0:
                        parts.append((line[last_index:], False))
                        break
                    else:
                        new_index, term = min(index_candidates, key=lambda t: t[0])
                        new_end_index = new_index + len(term)
                        parts.append((line[last_index:new_index], False))
                        parts.append((line[new_index:new_end_index], True))
                        last_index = new_end_index
                formatted_lines.append("".join([
                    "<b>{}</b>".format(text) if matched else text
                    for text, matched in parts
                ]))
489
            search_results[protocol] = " …<br />\n".join(formatted_lines)
Robin Sonnabend's avatar
Robin Sonnabend committed
490
    protocols = sorted(protocols, key=lambda protocol: protocol.date, reverse=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
491
    page = _get_page()
Robin Sonnabend's avatar
Robin Sonnabend committed
492
493
    page_length = _get_page_length()
    page_count = int(math.ceil(len(protocols) / page_length))
Robin Sonnabend's avatar
Robin Sonnabend committed
494
495
    if page >= page_count:
        page = 0
Robin Sonnabend's avatar
Robin Sonnabend committed
496
497
    begin_index = page * page_length
    end_index = (page + 1) * page_length
498
    max_page_length_exp = math.ceil(math.log10(len(protocols))) if len(protocols) > 0 else 1
Robin Sonnabend's avatar
Robin Sonnabend committed
499
    protocols = protocols[begin_index:end_index]
Robin Sonnabend's avatar
Robin Sonnabend committed
500
    protocols_table = ProtocolsTable(protocols, search_results=search_results)
Robin Sonnabend's avatar
Robin Sonnabend committed
501
    return render_template("protocols-list.html", protocols=protocols, protocols_table=protocols_table, search_form=search_form, page=page, page_count=page_count, page_diff=config.PAGE_DIFF, protocoltype_id=protocoltype_id, search_term=search_term, state_open=state_open, page_length=page_length, max_page_length_exp=max_page_length_exp)
Robin Sonnabend's avatar
Robin Sonnabend committed
502
503
504
505
506

@app.route("/protocol/new", methods=["GET", "POST"])
@login_required
def new_protocol():
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
507
    protocoltypes = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
508
    form = NewProtocolForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
509
    upload_form = NewProtocolSourceUploadForm(protocoltypes)
510
    file_upload_form = NewProtocolFileUploadForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
511
    if form.validate_on_submit():
512
        protocoltype = ProtocolType.query.filter_by(id=form.protocoltype_id.data).first()
Robin Sonnabend's avatar
Robin Sonnabend committed
513
514
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
515
            return back.redirect()
516
517
        protocol = Protocol.create_new_protocol(protocoltype,
            form.date.data, form.start_time.data)
518
        return back.redirect("show_protocol", protocol_id=protocol.id)
519
    type_id = request.args.get("protocoltype_id")
Robin Sonnabend's avatar
Robin Sonnabend committed
520
521
    if type_id is not None:
        form.protocoltype.data = type_id
522
523
        upload_form.protocoltype.data = type_id
    return render_template("protocol-new.html", form=form, upload_form=upload_form, file_upload_form=file_upload_form, protocoltypes=protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
524
525

@app.route("/protocol/show/<int:protocol_id>")
526
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
527
528
@db_lookup(Protocol)
def show_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
529
530
    user = current_user()
    errors_table = ErrorsTable(protocol.errors)
531
    if not protocol.protocoltype.has_public_view_right(user, check_networks=False):
532
533
534
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
        if check_login():
            return redirect(url_for("index"))
535
        return redirect(url_for("login"))
Robin Sonnabend's avatar
Robin Sonnabend committed
536
537
    visible_documents = [
        document for document in protocol.documents
538
        if (not document.is_private and document.protocol.has_public_view_right(user))
Robin Sonnabend's avatar
Robin Sonnabend committed
539
540
        or (document.is_private and document.protocol.protocoltype.has_private_view_right(user))
    ]
541
    documents_table = DocumentsTable(visible_documents, protocol)
542
    document_upload_form = DocumentUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
543
    source_upload_form = KnownProtocolSourceUploadForm()
544
545
    time_diff = protocol.date - datetime.now().date()
    large_time_diff = not protocol.is_done() and time_diff.days > 0
546
547
548
    content_html = (protocol.content_html_private
        if protocol.has_private_view_right(user)
        else protocol.content_html_public)
549
550
551
    if content_html is not None:
        content_html = Markup(content_html)
    return render_template("protocol-show.html", protocol=protocol, errors_table=errors_table, documents_table=documents_table, document_upload_form=document_upload_form, source_upload_form=source_upload_form, time_diff=time_diff, large_time_diff=large_time_diff, content_html=content_html)
Robin Sonnabend's avatar
Robin Sonnabend committed
552

Robin Sonnabend's avatar
Robin Sonnabend committed
553
554
@app.route("/protocol/delete/<int:protocol_id>")
@login_required
555
@group_required(config.ADMIN_GROUP)
Robin Sonnabend's avatar
Robin Sonnabend committed
556
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
557
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
558
def delete_protocol(protocol):
559
    name = protocol.get_short_identifier()
Robin Sonnabend's avatar
Robin Sonnabend committed
560
    protocol.delete_orphan_todos()
Robin Sonnabend's avatar
Robin Sonnabend committed
561
562
563
    db.session.delete(protocol)
    db.session.commit()
    flash("Protokoll {} ist gelöscht.".format(name), "alert-success")
564
    return back.redirect("list_protocols")
Robin Sonnabend's avatar
Robin Sonnabend committed
565

Robin Sonnabend's avatar
Robin Sonnabend committed
566
@app.route("/protocol/etherpull/<int:protocol_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
567
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
568
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
569
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
570
def etherpull_protocol(protocol):
571
572
    if not config.ETHERPAD_ACTIVE:
        flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
573
        return back.redirect("show_protocol", protocol_id=protocol_id)
Robin Sonnabend's avatar
Robin Sonnabend committed
574
    protocol.source = get_etherpad_text(protocol.get_identifier())
Robin Sonnabend's avatar
Robin Sonnabend committed
575
576
577
    db.session.commit()
    tasks.parse_protocol(protocol)
    flash("Das Protokoll wird kompiliert.", "alert-success")
578
    return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
579

Robin Sonnabend's avatar
Robin Sonnabend committed
580
581
@app.route("/protocol/upload/known/<int:protocol_id>", methods=["POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
582
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
583
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
584
def upload_source_to_known_protocol(protocol):
585
    form = KnownProtocolSourceUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
586
587
588
589
590
591
592
593
594
595
596
597
598
599
    if form.validate_on_submit():
        if form.source.data is None:
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
        else:
            file = form.source.data
            if file.filename == "":
                flash("Es wurde keine Datei ausgewählt.", "alert-error")
            else:
                # todo: Prüfen, ob es Text ist?
                source = file.stream.read().decode("utf-8")
                protocol.source = source
                db.session.commit()
                tasks.parse_protocol(protocol)
                flash("Das Protokoll wird kompiliert.", "alert-success")
600
    return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
601
602
603
604
605

@app.route("/protocol/upload/new/", methods=["POST"])
@login_required
def upload_new_protocol():
    user = current_user()
606
    available_types = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
607
608
609
610
    form = NewProtocolSourceUploadForm(protocoltypes=available_types)
    if form.validate_on_submit():
        if form.source.data is None:
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
611
612
613
614
615
616
            return redirect(request.args.get("fail") or url_for("new_protocol"))
        file = form.source.data
        if file.filename == "":
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
            return redirect(request.args.get("fail") or url_for("new_protocol"))
        source = file.stream.read().decode("utf-8")
617
        protocoltype = ProtocolType.query.filter_by(id=form.protocoltype_id.data).first()
618
619
620
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Invalider Protokolltyp oder keine Rechte.", "alert-error")
            return redirect(request.args.get("fail") or url_for("new_protocol"))
621
        protocol = Protocol(protocoltype_id=protocoltype.id, source=source)
622
623
        db.session.add(protocol)
        db.session.commit()
624
625
626
        for local_top in protocol.create_localtops:
            db.session.add(local_top)
        db.session.commit()
627
        tasks.parse_protocol(protocol)
628
        return back.redirect("show_protocol", protocol_id=protocol.id)
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
    return redirect(request.args.get("fail") or url_for("new_protocol"))

@app.route("/protocol/upload/new/file/", methods=["POST"])
@login_required
def upload_new_protocol_by_file():
    user = current_user()
    available_types = ProtocolType.get_modifiable_protocoltypes(user)
    form = NewProtocolFileUploadForm(protocoltypes=available_types)
    if form.validate_on_submit():
        if form.file.data is None:
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
            return redirect(request.args.get("fail") or url_for("new_protocol"))
        file = form.file.data
        if file.filename == "":
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
            return redirect(request.args.get("fail") or url_for("new_protocol"))
        filename = secure_filename(file.filename)
646
        protocoltype = ProtocolType.query.filter_by(id=form.protocoltype_id.data).first()
647
648
649
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Invalider Protokolltyp oder keine Rechte.", "alert-error")
            return redirect(request.args.get("fail") or url_for("new_protocol"))
650
        protocol = Protocol(protocoltype_id=protocoltype.id, date=datetime.now().date(), done=True)
651
652
        db.session.add(protocol)
        db.session.commit()
653
654
655
        for local_top in protocol.create_localtops:
            db.session.add(local_top)
        db.session.commit()
656
657
658
        document = Document(protocol_id=protocol.id, name=filename,
            filename="", is_compiled=False)
        form.populate_obj(document)
659
660
661
662
663
664
        db.session.add(document)
        db.session.commit()
        internal_filename = "{}-{}-{}".format(protocol.id, document.id, filename)
        document.filename = internal_filename
        file.save(os.path.join(config.DOCUMENTS_PATH, internal_filename))
        db.session.commit()
665
        return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
666
667
    return redirect(request.args.get("fail") or url_for("new_protocol"))

Administrator's avatar
Administrator committed
668
669
670
@app.route("/protocol/recompile/<int:protocol_id>")
@login_required
@group_required(config.ADMIN_GROUP)
Robin Sonnabend's avatar
Robin Sonnabend committed
671
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
672
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
673
def recompile_protocol(protocol):
Administrator's avatar
Administrator committed
674
    tasks.parse_protocol(protocol)
675
    return back.redirect("show_protocol", protocol_id=protocol.id)
676

Robin Sonnabend's avatar
Robin Sonnabend committed
677
678
@app.route("/protocol/source/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
679
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
680
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
681
def get_protocol_source(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
682
    file_like = BytesIO(protocol.source.encode("utf-8"))
683
    return send_file(file_like, cache_timeout=1, as_attachment=True, attachment_filename="{}.txt".format(protocol.get_short_identifier()))
Robin Sonnabend's avatar
Robin Sonnabend committed
684

Robin Sonnabend's avatar
Robin Sonnabend committed
685
686
@app.route("/protocol/template/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
687
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
688
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
689
def get_protocol_template(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
690
    file_like = BytesIO(protocol.get_template().encode("utf-8"))
691
    return send_file(file_like, cache_timeout=1, as_attachment=True, attachment_filename="{}-template.txt".format(protocol.get_short_identifier()))
Robin Sonnabend's avatar
Robin Sonnabend committed
692

Robin Sonnabend's avatar
Robin Sonnabend committed
693
694
@app.route("/protocol/etherpush/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
695
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
696
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
697
def etherpush_protocol(protocol):
698
699
    if not config.ETHERPAD_ACTIVE:
        flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
700
        return back.redirect("show_protocol", protocol_id=protocol_id)
701
702
    if not protocol.is_done():
        tasks.set_etherpad_content(protocol)
703
    return redirect(protocol.get_etherpad_link())
Robin Sonnabend's avatar
Robin Sonnabend committed
704

Robin Sonnabend's avatar
Robin Sonnabend committed
705
@app.route("/protocol/update/<int:protocol_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
706
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
707
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
708
@require_publish_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
709
def update_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
710
    upload_form = KnownProtocolSourceUploadForm()
711
    edit_form = generate_protocol_form(protocol)(obj=protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
712
713
    if edit_form.validate_on_submit():
        edit_form.populate_obj(protocol)
714
715
        for meta in protocol.metas:
            meta.value = getattr(edit_form.metas, meta.name).data
Robin Sonnabend's avatar
Robin Sonnabend committed
716
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
717
        tasks.push_tops_to_calendar(protocol)
718
        return back.redirect("show_protocol", protocol_id=protocol.id)
719
720
    for meta in protocol.metas:
        getattr(edit_form.metas, meta.name).data = meta.value
Robin Sonnabend's avatar
Robin Sonnabend committed
721
    return render_template("protocol-update.html", upload_form=upload_form, edit_form=edit_form, protocol=protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
722

723
724
@app.route("/protocol/publish/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
725
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
726
@require_publish_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
727
def publish_protocol(protocol):
728
729
    protocol.public = True
    db.session.commit()
730
    return back.redirect("show_protocol", protocol_id=protocol.id)
731

732
@app.route("/prococol/send/private/<int:protocol_id>")
733
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
734
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
735
@require_modify_right()
736
def send_protocol_private(protocol):
737
738
    if not config.MAIL_ACTIVE:
        flash("Die Mailfunktion ist nicht aktiviert.", "alert-error")
739
        return back.redirect("show_protocol", protocol_id=protocol_id)
740
741
    tasks.send_protocol_private(protocol)
    flash("Das Protokoll wurde versandt.", "alert-success")
742
    return back.redirect("show_protocol", protocol_id=protocol.id)
743
744
745
746

@app.route("/prococol/send/public/<int:protocol_id>")
@login_required
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
747
@require_publish_right()
748
749
750
def send_protocol_public(protocol):
    if not config.MAIL_ACTIVE:
        flash("Die Mailfunktion ist nicht aktiviert.", "alert-error")
751
        return back.redirect("show_protocol", protocol_id=protocol_id)
752
    tasks.send_protocol_public(protocol)
753
    flash("Das Protokoll wurde versandt.", "alert-success")
754
    return back.redirect("show_protocol", protocol_id=protocol.id)
755

756
757
758
759
760
761
762
@app.route("/protocol/reminder/<int:protocol_id>")
@login_required
@db_lookup(Protocol)
@require_modify_right()
def send_protocol_reminder(protocol):
    if not config.MAIL_ACTIVE:
        flash("Die Mailfunktion ist nicht aktiviert.", "alert-error")
763
        return back.redirect("show_protocol", protocol_id=protocol_id)
764
765
766
    meetingreminders = MeetingReminder.query.filter_by(protocoltype_id=protocol.protocoltype.id).all()
    if len(meetingreminders) == 0:
        flash("Für diesen Protokolltyp sind keine Einladungsmails konfiguriert.", "alert-error")
767
        return back.redirect("show_protocol", protocol_id=protocol_id)
768
769
770
771
772
773
774
775
776
777
778
779
    day_difference = (protocol.date - datetime.now().date()).days
    past_reminders = [
        meetingreminder for meetingreminder in meetingreminders
        if meetingreminder.days_before > day_difference
    ]
    if len(past_reminders) == 0:
        flash("Bisher hätte keine Einladungsmail verschickt werden sollen, schicke letzte.", "alert-info")
        past_reminders = meetingreminders
    past_reminders = sorted(past_reminders, key=lambda r: r.days_before)
    choosen_reminder = past_reminders[0]
    tasks.send_reminder(choosen_reminder, protocol)
    flash("Einladungsmail ist versandt.", "alert-success")
780
    return back.redirect("show_protocol", protocol_id=protocol.id)
781
782
        

Robin Sonnabend's avatar
Robin Sonnabend committed
783
784
@app.route("/protocol/tops/new/<int:protocol_id>", methods=["GET", "POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
785
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
786
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
787
def new_top(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
788
789
    form = TopForm()
    if form.validate_on_submit():
790
791
        top = TOP(protocol_id=protocol.id, planned=True)
        form.populate_obj(top)
Robin Sonnabend's avatar
Robin Sonnabend committed
792
793
        db.session.add(top)
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
794
        tasks.push_tops_to_calendar(top.protocol)
795
        return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
796
797
798
799
800
801
802
803
    else:
        current_numbers = list(map(lambda t: t.number, protocol.tops))
        suggested_number = get_first_unused_int(current_numbers)
        form.number.data = suggested_number
    return render_template("top-new.html", form=form, protocol=protocol)

@app.route("/protocol/top/edit/<int:top_id>", methods=["GET", "POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
804
@db_lookup(TOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
805
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
806
def edit_top(top):
Robin Sonnabend's avatar
Robin Sonnabend committed
807
808
809
810
    form = TopForm(obj=top)
    if form.validate_on_submit():
        form.populate_obj(top)
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
811
        tasks.push_tops_to_calendar(top.protocol)
812
        return back.redirect("show_protocol", protocol_id=top.protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
813
814
815
816
    return render_template("top-edit.html", form=form, top=top)

@app.route("/protocol/top/delete/<int:top_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
817
@db_lookup(TOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
818
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
819
def delete_top(top):
Robin Sonnabend's avatar
Robin Sonnabend committed
820
    name = top.name
Robin Sonnabend's avatar
Robin Sonnabend committed
821
    protocol = top.protocol
Robin Sonnabend's avatar
Robin Sonnabend committed
822
823
    db.session.delete(top)
    db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
824
    tasks.push_tops_to_calendar(protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
825
    flash("Der TOP {} wurde gelöscht.".format(name), "alert-success")
826
    return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar