server.py 62.9 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
Robin Sonnabend's avatar
Robin Sonnabend committed
17
from io import StringIO, BytesIO
18
import os
19
from datetime import datetime, time
Robin Sonnabend's avatar
Robin Sonnabend committed
20
import math
21
import mimetypes
22
import subprocess
23
from dateutil import tz
24
25

import config
Robin Sonnabend's avatar
Robin Sonnabend committed
26
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
27
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
28
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
29
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
30
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
31
from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable, TodoMailsTable, DefaultMetasTable, DecisionCategoriesTable
32
from legacy import import_old_todos, import_old_protocols, import_old_todomails
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

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
56
app.jinja_env.filters["timify"] = time_filter
57
app.jinja_env.filters["timify_short"] = time_filter_short
Robin Sonnabend's avatar
Robin Sonnabend committed
58
app.jinja_env.filters["datify_short"] = date_filter_short
Robin Sonnabend's avatar
Robin Sonnabend committed
59
app.jinja_env.filters["datify_long"] = date_filter_long
Robin Sonnabend's avatar
Robin Sonnabend committed
60
#app.jinja_env.filters["url_complete"] = url_manager.complete
61
app.jinja_env.filters["class"] = class_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
62
63
app.jinja_env.filters["todo_get_name"] = todostate_name_filter
app.jinja_env.filters["code"] = code_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
64
app.jinja_env.filters["indent_tab"] = indent_tab_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
65
app.jinja_env.filters["fancy_join"] = fancy_join
Robin Sonnabend's avatar
Robin Sonnabend committed
66
app.jinja_env.filters["footnote_hash"] = footnote_hash
67
app.jinja_env.tests["auth_valid"] = security_manager.check_user
Robin Sonnabend's avatar
Robin Sonnabend committed
68
app.jinja_env.tests["needs_date"] = needs_date_test
69

70
71
72
73
74
75
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)
    

76
77
import tasks

78
79
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
80
app.jinja_env.globals.update(zip=zip)
Robin Sonnabend's avatar
Robin Sonnabend committed
81
82
83
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
84
app.jinja_env.globals.update(now=datetime.now)
85

86
87
88
89
90
91
92
93
94
95
96
97
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

98
99
# blueprints here

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

111
112
113
114
@manager.command
def recompile_all():
    for protocol in sorted(Protocol.query.all(), key=lambda p: p.date):
        if protocol.is_done():
115
            print(protocol.get_short_identifier())
116
117
118
119
120
121
122
123
124
125
126
127
            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)
128
            if todo2.state.value > todo1.state.value:
129
130
131
132
133
134
135
136
137
138
139
140
141
                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():
142
    app.run()
143
144
    make_scheduler()

145
146
147
148
# 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)
149
150
151
152
153
154
155
156
    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"
157
    return response
158

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

Robin Sonnabend's avatar
Robin Sonnabend committed
218
@app.route("/documentation")
Robin Sonnabend's avatar
Robin Sonnabend committed
219
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
220
def documentation():
Robin Sonnabend's avatar
Robin Sonnabend committed
221
222
223
    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
224

Robin Sonnabend's avatar
Robin Sonnabend committed
225
@app.route("/types/list")
Robin Sonnabend's avatar
Robin Sonnabend committed
226
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
227
228
229
230
231
def list_types():
    is_logged_in = check_login()
    user = current_user()
    types = [
        protocoltype for protocoltype in ProtocolType.query.all()
232
233
        if (protocoltype.has_private_view_right(user)
        or protocoltype.has_public_view_right(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
234
        or protocoltype.is_public)]
235
    types = sorted(types, key=lambda t: t.short_name)
Robin Sonnabend's avatar
Robin Sonnabend committed
236
237
238
239
240
241
242
243
244
245
246
247
    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:
248
249
            protocoltype = ProtocolType()
            form.populate_obj(protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
250
251
252
253
254
255
            db.session.add(protocoltype)
            db.session.commit()
            flash("Der Protokolltyp {} wurde angelegt.".format(protocoltype.name), "alert-success")
        return redirect(request.args.get("next") or url_for("list_types"))
    return render_template("type-new.html", form=form)

Robin Sonnabend's avatar
Robin Sonnabend committed
256
@app.route("/type/edit/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
257
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
258
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
259
@require_private_view_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
260
def edit_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
261
262
263
264
265
266
267
268
    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()
Robin Sonnabend's avatar
Robin Sonnabend committed
269
            return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id))
Robin Sonnabend's avatar
Robin Sonnabend committed
270
271
    return render_template("type-edit.html", form=form, protocoltype=protocoltype)

Robin Sonnabend's avatar
Robin Sonnabend committed
272
@app.route("/type/show/<int:protocoltype_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
273
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
274
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
275
@require_private_view_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
276
def show_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
277
278
    protocoltype_table = ProtocolTypeTable(protocoltype)
    default_tops_table = DefaultTOPsTable(protocoltype.default_tops, protocoltype)
279
    reminders_table = MeetingRemindersTable(protocoltype.reminders, protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
280
    metas_table = DefaultMetasTable(protocoltype.metas, protocoltype)
281
282
    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)
283

Robin Sonnabend's avatar
Robin Sonnabend committed
284
@app.route("/type/delete/<int:protocoltype_id>")
285
@login_required
286
@group_required(config.ADMIN_GROUP)
Robin Sonnabend's avatar
Robin Sonnabend committed
287
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
288
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
289
def delete_type(protocoltype):
290
291
292
293
294
295
    name = protocoltype.name
    db.session.delete(protocoltype) 
    db.session.commit()
    flash("Der Protokolltype {} wurde gelöscht.".format(name), "alert-success")
    return redirect(request.args.get("next") or url_for("list_types"))

Robin Sonnabend's avatar
Robin Sonnabend committed
296
@app.route("/type/reminders/new/<int:protocoltype_id>", methods=["GET", "POST"])
297
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
298
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
299
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
300
def new_reminder(protocoltype):
301
302
    form = MeetingReminderForm()
    if form.validate_on_submit():
303
304
        meetingreminder = MeetingReminder(protocoltype_id=protocoltype.id)
        form.populate_obj(meetingreminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
305
        db.session.add(meetingreminder)
306
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
307
        return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id))
308
309
    return render_template("reminder-new.html", form=form, protocoltype=protocoltype)

Robin Sonnabend's avatar
Robin Sonnabend committed
310
@app.route("/type/reminder/edit/<int:meetingreminder_id>", methods=["GET", "POST"])
311
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
312
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
313
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
314
315
def edit_reminder(meetingreminder):
    form = MeetingReminderForm(obj=meetingreminder)
316
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
317
        form.populate_obj(meetingreminder)
318
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
319
        return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id))
320
    return render_template("reminder-edit.html", form=form, meetingreminder=meetingreminder)
321

Robin Sonnabend's avatar
Robin Sonnabend committed
322
@app.route("/type/reminder/delete/<int:meetingreminder_id>")
323
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
324
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
325
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
326
327
328
def delete_reminder(meetingreminder):
    protocoltype = meetingreminder.protocoltype
    db.session.delete(meetingreminder)
329
    db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
330
    return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id))
331

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

Robin Sonnabend's avatar
Robin Sonnabend committed
353
@app.route("/type/tops/edit/<int:protocoltype_id>/<int:defaulttop_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
354
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
355
@db_lookup(ProtocolType, DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
356
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
357
358
def edit_default_top(protocoltype, defaulttop):
    form = DefaultTopForm(obj=defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
359
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
360
        form.populate_obj(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
361
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
362
363
        return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id))
    return render_template("default-top-edit.html", form=form, protocoltype=protocoltype, defaulttop=defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
364

Robin Sonnabend's avatar
Robin Sonnabend committed
365
@app.route("/type/tops/delete/<int:defaulttop_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
366
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
367
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
368
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
369
370
def delete_default_top(defaulttop):
    db.session.delete(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
371
    db.session.commit()
372
    return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=defaulttop.protocoltype.id))
Robin Sonnabend's avatar
Robin Sonnabend committed
373

Robin Sonnabend's avatar
Robin Sonnabend committed
374
@app.route("/type/tops/move/<int:defaulttop_id>/<diff>/")
Robin Sonnabend's avatar
Robin Sonnabend committed
375
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
376
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
377
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
378
def move_default_top(defaulttop, diff):
Robin Sonnabend's avatar
Robin Sonnabend committed
379
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
380
        defaulttop.number += int(diff)
Robin Sonnabend's avatar
Robin Sonnabend committed
381
382
383
        db.session.commit()
    except ValueError:
        flash("Die angegebene Differenz ist keine Zahl.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
384
    return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=defaulttop.protocoltype.id))
Robin Sonnabend's avatar
Robin Sonnabend committed
385
386

@app.route("/protocols/list")
387
388
389
def list_protocols():
    is_logged_in = check_login()
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
390
391
392
    protocoltype = None
    protocoltype_id = None
    try:
393
        protocoltype_id = int(request.args.get("protocoltype_id"))
Robin Sonnabend's avatar
Robin Sonnabend committed
394
395
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
396
    state_open = -1
Robin Sonnabend's avatar
Robin Sonnabend committed
397
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
398
        state_open = int(request.args.get("state_open"))
Robin Sonnabend's avatar
Robin Sonnabend committed
399
400
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
401
    search_term = request.args.get("search")
402
    protocoltypes = ProtocolType.get_public_protocoltypes(user, check_networks=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
403
    search_form = ProtocolSearchForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
404
    if protocoltype_id is not None:
405
        search_form.protocoltype_id.data = protocoltype_id
Robin Sonnabend's avatar
Robin Sonnabend committed
406
        protocoltype = ProtocolType.query.filter_by(id=protocoltype_id).first()
Robin Sonnabend's avatar
Robin Sonnabend committed
407
408
    if state_open is not None:
        search_form.state_open.data = state_open
Robin Sonnabend's avatar
Robin Sonnabend committed
409
410
411
    if search_term is not None:
        search_form.search.data = search_term
    protocol_query = Protocol.query
Robin Sonnabend's avatar
Robin Sonnabend committed
412
413
414
415
416
417
418
419
420
    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))
            ))
421
    protocols = [
Robin Sonnabend's avatar
Robin Sonnabend committed
422
        protocol for protocol in protocol_query.all()
423
        if protocol.protocoltype.has_public_view_right(user, check_networks=False)
424
    ]
Robin Sonnabend's avatar
Robin Sonnabend committed
425
426
427
428
429
430
431
432
433
434
435
436
437
    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
438
439
440
441
442
    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
443
444
    if state_open is not None and state_open != -1:
        protocol_done = bool(state_open)
Robin Sonnabend's avatar
Robin Sonnabend committed
445
446
        protocols = [
            protocol for protocol in protocols
447
            if (protocol.is_done() or False) == protocol_done
Robin Sonnabend's avatar
Robin Sonnabend committed
448
        ]
Robin Sonnabend's avatar
Robin Sonnabend committed
449
450
451
452
453
    if shall_search:
        protocols = [
            protocol for protocol in protocols
            if (protocol.protocoltype.has_private_view_right(user)
                and _matches_search(protocol.content_private))
454
            or (protocol.has_public_view_right(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
                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
                ]))
482
            search_results[protocol] = " …<br />\n".join(formatted_lines)
Robin Sonnabend's avatar
Robin Sonnabend committed
483
    protocols = sorted(protocols, key=lambda protocol: protocol.date, reverse=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
484
    page = _get_page()
Robin Sonnabend's avatar
Robin Sonnabend committed
485
486
    page_length = _get_page_length()
    page_count = int(math.ceil(len(protocols) / page_length))
Robin Sonnabend's avatar
Robin Sonnabend committed
487
488
    if page >= page_count:
        page = 0
Robin Sonnabend's avatar
Robin Sonnabend committed
489
490
    begin_index = page * page_length
    end_index = (page + 1) * page_length
491
    max_page_length_exp = math.ceil(math.log10(len(protocols))) if len(protocols) > 0 else 1
Robin Sonnabend's avatar
Robin Sonnabend committed
492
    protocols = protocols[begin_index:end_index]
Robin Sonnabend's avatar
Robin Sonnabend committed
493
    protocols_table = ProtocolsTable(protocols, search_results=search_results)
Robin Sonnabend's avatar
Robin Sonnabend committed
494
    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
495
496
497
498
499

@app.route("/protocol/new", methods=["GET", "POST"])
@login_required
def new_protocol():
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
500
    protocoltypes = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
501
    form = NewProtocolForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
502
    upload_form = NewProtocolSourceUploadForm(protocoltypes)
503
    file_upload_form = NewProtocolFileUploadForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
504
    if form.validate_on_submit():
505
        protocoltype = ProtocolType.query.filter_by(id=form.protocoltype_id.data).first()
Robin Sonnabend's avatar
Robin Sonnabend committed
506
507
508
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
            return redirect(request.args.get("next") or url_for("index"))
509
510
        protocol = Protocol.create_new_protocol(protocoltype,
            form.date.data, form.start_time.data)
Robin Sonnabend's avatar
Robin Sonnabend committed
511
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
512
    type_id = request.args.get("protocoltype_id")
Robin Sonnabend's avatar
Robin Sonnabend committed
513
514
    if type_id is not None:
        form.protocoltype.data = type_id
515
516
        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
517
518

@app.route("/protocol/show/<int:protocol_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
519
520
@db_lookup(Protocol)
def show_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
521
522
    user = current_user()
    errors_table = ErrorsTable(protocol.errors)
523
    if not protocol.protocoltype.has_public_view_right(user, check_networks=False):
524
525
526
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
        if check_login():
            return redirect(url_for("index"))
527
        return redirect(request.args.get("next") or url_for("login", next=request.url))
Robin Sonnabend's avatar
Robin Sonnabend committed
528
529
    visible_documents = [
        document for document in protocol.documents
530
        if (not document.is_private and document.protocol.has_public_view_right(user))
Robin Sonnabend's avatar
Robin Sonnabend committed
531
532
        or (document.is_private and document.protocol.protocoltype.has_private_view_right(user))
    ]
533
    documents_table = DocumentsTable(visible_documents, protocol)
534
    document_upload_form = DocumentUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
535
    source_upload_form = KnownProtocolSourceUploadForm()
536
537
    time_diff = protocol.date - datetime.now().date()
    large_time_diff = not protocol.is_done() and time_diff.days > 0
538
539
540
    content_html = (protocol.content_html_private
        if protocol.has_private_view_right(user)
        else protocol.content_html_public)
541
542
543
    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
544

Robin Sonnabend's avatar
Robin Sonnabend committed
545
546
@app.route("/protocol/delete/<int:protocol_id>")
@login_required
547
@group_required(config.ADMIN_GROUP)
Robin Sonnabend's avatar
Robin Sonnabend committed
548
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
549
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
550
def delete_protocol(protocol):
551
    name = protocol.get_short_identifier()
Robin Sonnabend's avatar
Robin Sonnabend committed
552
    protocol.delete_orphan_todos()
Robin Sonnabend's avatar
Robin Sonnabend committed
553
554
555
556
557
    db.session.delete(protocol)
    db.session.commit()
    flash("Protokoll {} ist gelöscht.".format(name), "alert-success")
    return redirect(request.args.get("next") or url_for("list_protocols"))

Robin Sonnabend's avatar
Robin Sonnabend committed
558
@app.route("/protocol/etherpull/<int:protocol_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
559
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
560
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
561
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
562
def etherpull_protocol(protocol):
563
564
565
    if not config.ETHERPAD_ACTIVE:
        flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id))
Robin Sonnabend's avatar
Robin Sonnabend committed
566
    protocol.source = get_etherpad_text(protocol.get_identifier())
Robin Sonnabend's avatar
Robin Sonnabend committed
567
568
569
570
571
    db.session.commit()
    tasks.parse_protocol(protocol)
    flash("Das Protokoll wird kompiliert.", "alert-success")
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))

Robin Sonnabend's avatar
Robin Sonnabend committed
572
573
@app.route("/protocol/upload/known/<int:protocol_id>", methods=["POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
574
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
575
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
576
def upload_source_to_known_protocol(protocol):
577
    form = KnownProtocolSourceUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
    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")
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))

@app.route("/protocol/upload/new/", methods=["POST"])
@login_required
def upload_new_protocol():
    user = current_user()
598
    available_types = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
599
600
601
602
    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")
603
604
605
606
607
608
            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")
609
        protocoltype = ProtocolType.query.filter_by(id=form.protocoltype_id.data).first()
610
611
612
        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"))
613
        protocol = Protocol(protocoltype_id=protocoltype.id, source=source)
614
615
        db.session.add(protocol)
        db.session.commit()
616
617
618
        for local_top in protocol.create_localtops:
            db.session.add(local_top)
        db.session.commit()
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
        tasks.parse_protocol(protocol)
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
    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)
638
        protocoltype = ProtocolType.query.filter_by(id=form.protocoltype_id.data).first()
639
640
641
        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"))
642
        protocol = Protocol(protocoltype_id=protocoltype.id, date=datetime.now().date(), done=True)
643
644
        db.session.add(protocol)
        db.session.commit()
645
646
647
        for local_top in protocol.create_localtops:
            db.session.add(local_top)
        db.session.commit()
648
649
650
        document = Document(protocol_id=protocol.id, name=filename,
            filename="", is_compiled=False)
        form.populate_obj(document)
651
652
653
654
655
656
657
        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()
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
Robin Sonnabend's avatar
Robin Sonnabend committed
658
659
    return redirect(request.args.get("fail") or url_for("new_protocol"))

Administrator's avatar
Administrator committed
660
661
662
@app.route("/protocol/recompile/<int:protocol_id>")
@login_required
@group_required(config.ADMIN_GROUP)
Robin Sonnabend's avatar
Robin Sonnabend committed
663
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
664
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
665
def recompile_protocol(protocol):
Administrator's avatar
Administrator committed
666
667
    tasks.parse_protocol(protocol)
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
668

Robin Sonnabend's avatar
Robin Sonnabend committed
669
670
@app.route("/protocol/source/<int:protocol_id>")
@login_required
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 get_protocol_source(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
674
    file_like = BytesIO(protocol.source.encode("utf-8"))
675
    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
676

Robin Sonnabend's avatar
Robin Sonnabend committed
677
678
@app.route("/protocol/template/<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_template(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
682
    file_like = BytesIO(protocol.get_template().encode("utf-8"))
683
    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
684

Robin Sonnabend's avatar
Robin Sonnabend committed
685
686
@app.route("/protocol/etherpush/<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 etherpush_protocol(protocol):
690
691
692
    if not config.ETHERPAD_ACTIVE:
        flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id))
693
694
695
    if not protocol.is_done():
        tasks.set_etherpad_content(protocol)
    return redirect(request.args.get("next") or protocol.get_etherpad_link())
Robin Sonnabend's avatar
Robin Sonnabend committed
696

Robin Sonnabend's avatar
Robin Sonnabend committed
697
@app.route("/protocol/update/<int:protocol_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
698
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
699
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
700
@require_publish_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
701
def update_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
702
    upload_form = KnownProtocolSourceUploadForm()
703
    edit_form = generate_protocol_form(protocol)(obj=protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
704
705
    if edit_form.validate_on_submit():
        edit_form.populate_obj(protocol)
706
707
        for meta in protocol.metas:
            meta.value = getattr(edit_form.metas, meta.name).data
Robin Sonnabend's avatar
Robin Sonnabend committed
708
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
709
        tasks.push_tops_to_calendar(protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
710
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
711
712
    for meta in protocol.metas:
        getattr(edit_form.metas, meta.name).data = meta.value
Robin Sonnabend's avatar
Robin Sonnabend committed
713
    return render_template("protocol-update.html", upload_form=upload_form, edit_form=edit_form, protocol=protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
714

715
716
@app.route("/protocol/publish/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
717
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
718
@require_publish_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
719
def publish_protocol(protocol):
720
721
722
723
    protocol.public = True
    db.session.commit()
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))

724
@app.route("/prococol/send/private/<int:protocol_id>")
725
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
726
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
727
@require_modify_right()
728
def send_protocol_private(protocol):
729
730
731
    if not config.MAIL_ACTIVE:
        flash("Die Mailfunktion ist nicht aktiviert.", "alert-error")
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id))
732
733
734
735
736
737
738
    tasks.send_protocol_private(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("/prococol/send/public/<int:protocol_id>")
@login_required
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
739
@require_publish_right()
740
741
742
743
744
def send_protocol_public(protocol):
    if not config.MAIL_ACTIVE:
        flash("Die Mailfunktion ist nicht aktiviert.", "alert-error")
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id))
    tasks.send_protocol_public(protocol)
745
746
747
    flash("Das Protokoll wurde versandt.", "alert-success")
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))

748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
@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")
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id))
    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")
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id))
    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")
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
        

Robin Sonnabend's avatar
Robin Sonnabend committed
775
776
@app.route("/protocol/tops/new/<int:protocol_id>", methods=["GET", "POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
777
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
778
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
779
def new_top(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
780
781
    form = TopForm()
    if form.validate_on_submit():
782
783
        top = TOP(protocol_id=protocol.id, planned=True)
        form.populate_obj(top)
Robin Sonnabend's avatar
Robin Sonnabend committed
784
785
        db.session.add(top)
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
786
        tasks.push_tops_to_calendar(top.protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
787
788
789
790
791
792
793
794
795
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
    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
796
@db_lookup(TOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
797
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
798
def edit_top(top):
Robin Sonnabend's avatar
Robin Sonnabend committed
799
800
801
802
    form = TopForm(obj=top)
    if form.validate_on_submit():
        form.populate_obj(top)
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
803
        tasks.push_tops_to_calendar(top.protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
804
805
806
807
808
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=top.protocol.id))
    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
809
@db_lookup(TOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
810
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
811
def delete_top(top):
Robin Sonnabend's avatar
Robin Sonnabend committed
812
    name = top.name
Robin Sonnabend's avatar
Robin Sonnabend committed
813
    protocol = top.protocol
Robin Sonnabend's avatar
Robin Sonnabend committed
814
815
    db.session.delete(top)
    db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
816
    tasks.push_tops_to_calendar(protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
817
    flash("Der TOP {} wurde gelöscht.".format(name), "alert-success")
Robin Sonnabend's avatar
Robin Sonnabend committed
818
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
Robin Sonnabend's avatar
Robin Sonnabend committed
819
820
821

@app.route("/protocol/top/move/<int:top_id>/<diff>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
822
@db_lookup(TOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
823
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
824
def move_top(top, diff):
Robin Sonnabend's avatar
Robin Sonnabend committed
825
826
827
    try:
        top.number += int(diff)
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
828
        tasks.push_tops_to_calendar(top.protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
829
830
831
832
    except ValueError:
        flash("Die angegebene Differenz ist keine Zahl.", "alert-error")
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=top.protocol.id))

833
834
835
836
837
838
839
840
841
842
843
844
@app.route("/protocol/localtop/edit/<int:localtop_id>", methods=["GET", "POST"])
@login_required
@db_lookup(LocalTOP)
@require_modify_right()
def edit_localtop(localtop):
    form = LocalTopForm(obj=localtop)
    if form.validate_on_submit():
        form.populate_obj(localtop)
        db.session.commit()
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=localtop.protocol.id))
    return render_template("localtop-edit.html", form=form, localtop=localtop)

Robin Sonnabend's avatar
Robin Sonnabend committed
845
846
847
848
849
850
851
852
853
def _get_page():
    try:
        page = request.args.get("page")
        if page is None:
            return 0
        return int(page)
    except ValueError:
        return 0

Robin Sonnabend's avatar
Robin Sonnabend committed
854
855
856
857
858
859
860
861
862
def _get_page_length():
    try:
        page_length = request.args.get("page_length")
        if page_length is None:
            return config.PAGE_LENGTH
        return int(page_length)
    except ValueError:
        return config.PAGE_LENGTH

863
@app.route("/todos/list")
Robin Sonnabend's avatar
Robin Sonnabend committed
864
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
865
866
def list_todos():
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
867
868
869
    protocoltype = None
    protocoltype_id = None
    try:
870
        protocoltype_id = int(request.args.get("protocoltype_id"))
Robin Sonnabend's avatar
Robin Sonnabend committed
871
872
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
873
    state_open = -1
Robin Sonnabend's avatar
Robin Sonnabend committed
874
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
875
        state_open = int(request.args.get("state_open"))
Robin Sonnabend's avatar
Robin Sonnabend committed
876
877
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
878
    search_term = request.args.get("search")
Robin Sonnabend's avatar
Robin Sonnabend committed
879
    protocoltypes = ProtocolType.get_public_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
880
    search_form = TodoSearchForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
881
    if protocoltype_id is not None:
882
        search_form.protocoltype_id.data = protocoltype_id
Robin Sonnabend's avatar
Robin Sonnabend committed
883
        protocoltype = ProtocolType.query.filter_by(id=protocoltype_id).first()