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

5
from flask import Flask, g, current_app, request, session, flash, redirect, url_for, abort, render_template, Response, send_file as flask_send_file
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
Robin Sonnabend's avatar
Robin Sonnabend committed
16
from io import StringIO, BytesIO
17
import os
Robin Sonnabend's avatar
Robin Sonnabend committed
18
from datetime import datetime
Robin Sonnabend's avatar
Robin Sonnabend committed
19
import math
20
import mimetypes
21
22

import config
Robin Sonnabend's avatar
Robin Sonnabend committed
23
from shared import db, date_filter, datetime_filter, date_filter_long, date_filter_short, time_filter, ldap_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
24
from utils import is_past, mail_manager, url_manager, get_first_unused_int, set_etherpad_text, get_etherpad_text, split_terms, optional_int_arg
Robin Sonnabend's avatar
Robin Sonnabend committed
25
from decorators import db_lookup
Robin Sonnabend's avatar
Robin Sonnabend committed
26
27
28
from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail, DecisionDocument, TodoState, Meta, DefaultMeta
from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, ProtocolForm, TopForm, SearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm, DefaultMetaForm, MetaForm
from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable, TodoMailsTable, DefaultMetasTable
29
from legacy import import_old_todos, import_old_protocols, import_old_todomails
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

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

import tasks

66
67
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
68
app.jinja_env.globals.update(zip=zip)
Robin Sonnabend's avatar
Robin Sonnabend committed
69
70
71
app.jinja_env.globals.update(min=min)
app.jinja_env.globals.update(max=max)
app.jinja_env.globals.update(dir=dir)
72

73
74
# blueprints here

75
76
@manager.command
def import_legacy():
77
    """Import the old todos and protocols from an sql dump"""
78
    filename = prompt("SQL-file")
79
    #filename = "legacy.sql"
80
81
    with open(filename, "rb") as sqlfile:
        content = sqlfile.read().decode("utf-8")
82
83
        import_old_todos(content)
        import_old_protocols(content)
84
        import_old_todomails(content)
85

86
87
88
89
# 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)
90
91
92
93
94
95
96
97
    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"
98
    return response
99

100
101
@app.route("/")
def index():
Robin Sonnabend's avatar
Robin Sonnabend committed
102
103
104
105
106
    user = current_user()
    protocols = [
        protocol for protocol in Protocol.query.all()
        if protocol.protocoltype.has_public_view_right(user)
    ]
107
    def _protocol_sort_key(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
108
109
110
        if protocol.date is not None:
            return protocol.date
        return datetime.now().date()
Robin Sonnabend's avatar
Robin Sonnabend committed
111
    current_day = datetime.now().date()
Robin Sonnabend's avatar
Robin Sonnabend committed
112
    open_protocols = sorted(
Robin Sonnabend's avatar
Robin Sonnabend committed
113
114
115
116
117
        [
            protocol for protocol in protocols
            if not protocol.done
            and (protocol.date - current_day).days < config.MAX_INDEX_DAYS
        ],
118
        key=_protocol_sort_key
Robin Sonnabend's avatar
Robin Sonnabend committed
119
120
    )
    finished_protocols = sorted(
121
122
123
124
125
126
        [
            protocol for protocol in protocols
            if protocol.done
            and (protocol.has_public_view_right(user)
                or protocol.has_private_view_right(user))
        ],
127
128
        key=_protocol_sort_key,
        reverse=True
Robin Sonnabend's avatar
Robin Sonnabend committed
129
130
131
132
133
    )
    protocol = finished_protocols[0] if len(finished_protocols) > 0 else None
    todos = None
    if check_login():
        todos = [
134
            todo for todo in Todo.query.all()
135
            if todo.protocoltype.has_private_view_right(user)
136
            and not todo.is_done()
Robin Sonnabend's avatar
Robin Sonnabend committed
137
        ]
138
139
140
141
        def _todo_sort_key(todo):
            protocol = todo.get_first_protocol()
            return protocol.date if protocol.date is not None else datetime.now().date()
        todos = sorted(todos, key=_todo_sort_key, reverse=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
142
143
    todos_table = TodosTable(todos) if todos is not None else None
    return render_template("index.html", open_protocols=open_protocols, protocol=protocol, todos=todos, todos_table=todos_table)
144

Robin Sonnabend's avatar
Robin Sonnabend committed
145
@app.route("/documentation")
Robin Sonnabend's avatar
Robin Sonnabend committed
146
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
147
def documentation():
Robin Sonnabend's avatar
Robin Sonnabend committed
148
149
150
    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
151

Robin Sonnabend's avatar
Robin Sonnabend committed
152
@app.route("/types/list")
Robin Sonnabend's avatar
Robin Sonnabend committed
153
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
154
155
156
157
158
159
160
161
def list_types():
    is_logged_in = check_login()
    user = current_user()
    types = [
        protocoltype for protocoltype in ProtocolType.query.all()
        if (protocoltype.public_group in user.groups
        or protocoltype.private_group in user.groups
        or protocoltype.is_public)]
162
    types = sorted(types, key=lambda t: t.short_name)
Robin Sonnabend's avatar
Robin Sonnabend committed
163
164
165
166
167
168
169
170
171
172
173
174
175
    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:
            protocoltype = ProtocolType(form.name.data, form.short_name.data,
Robin Sonnabend's avatar
Robin Sonnabend committed
176
                form.organization.data, form.usual_time.data, form.is_public.data,
177
                form.modify_group.data, form.private_group.data, form.public_group.data,
Robin Sonnabend's avatar
Robin Sonnabend committed
178
179
                form.private_mail.data, form.public_mail.data,
                form.use_wiki.data, form.wiki_category.data,
180
                form.wiki_only_public.data, form.printer.data,
181
182
                form.calendar.data, form.restrict_networks.data,
                form.allowed_networks.data)
Robin Sonnabend's avatar
Robin Sonnabend committed
183
184
185
186
187
188
            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
189
@app.route("/type/edit/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
190
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
191
192
@db_lookup(ProtocolType)
def edit_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
193
194
195
196
197
198
199
200
201
202
203
    user = current_user()
    if not protocoltype.has_private_view_right(user):
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
        return redirect(request.args.get("next") or url_for("index"))
    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
204
            return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id))
Robin Sonnabend's avatar
Robin Sonnabend committed
205
206
    return render_template("type-edit.html", form=form, protocoltype=protocoltype)

Robin Sonnabend's avatar
Robin Sonnabend committed
207
@app.route("/type/show/<int:protocoltype_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
208
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
209
210
@db_lookup(ProtocolType)
def show_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
211
212
213
214
215
216
    user = current_user()
    if not protocoltype.has_private_view_right(user):
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
        return redirect(request.args.get("next") or url_for("index"))
    protocoltype_table = ProtocolTypeTable(protocoltype)
    default_tops_table = DefaultTOPsTable(protocoltype.default_tops, protocoltype)
217
    reminders_table = MeetingRemindersTable(protocoltype.reminders, protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
218
219
    metas_table = DefaultMetasTable(protocoltype.metas, 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)
220

Robin Sonnabend's avatar
Robin Sonnabend committed
221
@app.route("/type/delete/<int:protocoltype_id>")
222
@login_required
223
@group_required(config.ADMIN_GROUP)
Robin Sonnabend's avatar
Robin Sonnabend committed
224
225
@db_lookup(ProtocolType)
def delete_type(protocoltype):
226
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
227
228
229
    if not protocoltype.has_modify_right(user):
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
        return redirect(reqeust.args.get("next") or url_for("index"))
230
231
232
233
234
235
    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
236
@app.route("/type/reminders/new/<int:protocoltype_id>", methods=["GET", "POST"])
237
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
238
239
@db_lookup(ProtocolType)
def new_reminder(protocoltype):
240
241
242
243
244
245
    user = current_user()
    if 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"))
    form = MeetingReminderForm()
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
246
247
        meetingreminder = MeetingReminder(protocoltype.id, form.days_before.data, form.send_public.data, form.send_private.data, form.additional_text.data)
        db.session.add(meetingreminder)
248
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
249
        return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id))
250
251
    return render_template("reminder-new.html", form=form, protocoltype=protocoltype)

Robin Sonnabend's avatar
Robin Sonnabend committed
252
@app.route("/type/reminder/edit/<int:meetingreminder_id>", methods=["GET", "POST"])
253
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
254
255
@db_lookup(MeetingReminder)
def edit_reminder(meetingreminder):
256
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
257
    if not meetingreminder.protocoltype.has_modify_right(user):
258
259
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
        return redirect(request.args.get("next") or url_for("index"))
Robin Sonnabend's avatar
Robin Sonnabend committed
260
    form = MeetingReminderForm(obj=meetingreminder)
261
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
262
        form.populate_obj(meetingreminder)
263
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
264
265
        return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id))
    return render_template("reminder-edit.html", form=form, protocoltype=protocoltype, meetingreminder=meetingreminder)
266

Robin Sonnabend's avatar
Robin Sonnabend committed
267
@app.route("/type/reminder/delete/<int:meetingreminder_id>")
268
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
269
270
@db_lookup(MeetingReminder)
def delete_reminder(meetingreminder):
271
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
272
    if not meetingreminder.protocoltype.has_modify_right(user):
273
274
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
        return redirect(request.args.get("next") or url_for("index"))
Robin Sonnabend's avatar
Robin Sonnabend committed
275
276
    protocoltype = meetingreminder.protocoltype
    db.session.delete(meetingreminder)
277
    db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
278
    return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id))
279

Robin Sonnabend's avatar
Robin Sonnabend committed
280
@app.route("/type/tops/new/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
281
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
282
283
@db_lookup(ProtocolType)
def new_default_top(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
284
285
286
287
288
289
    user = current_user()
    if 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"))
    form = DefaultTopForm()
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
290
291
        defaulttop = DefaultTOP(protocoltype.id, form.name.data, form.number.data)
        db.session.add(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
292
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
293
        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
294
295
296
        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
297
@app.route("/type/tops/edit/<int:protocoltype_id>/<int:defaulttop_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
298
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
299
300
@db_lookup(ProtocolType, DefaultTOP)
def edit_default_top(protocoltype, defaulttop):
Robin Sonnabend's avatar
Robin Sonnabend committed
301
302
303
304
    user = current_user()
    if 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"))
Robin Sonnabend's avatar
Robin Sonnabend committed
305
    form = DefaultTopForm(obj=defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
306
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
307
        form.populate_obj(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
308
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
309
310
        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
311

Robin Sonnabend's avatar
Robin Sonnabend committed
312
@app.route("/type/tops/delete/<int:defaulttop_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
313
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
314
315
@db_lookup(DefaultTOP)
def delete_default_top(defaulttop):
Robin Sonnabend's avatar
Robin Sonnabend committed
316
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
317
    if not defaulttop.protocoltype.has_modify_right(user):
Robin Sonnabend's avatar
Robin Sonnabend committed
318
319
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
        return redirect(request.args.get("next") or url_for("index"))
Robin Sonnabend's avatar
Robin Sonnabend committed
320
    db.session.delete(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
321
    db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
322
    return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id))
Robin Sonnabend's avatar
Robin Sonnabend committed
323

Robin Sonnabend's avatar
Robin Sonnabend committed
324
@app.route("/type/tops/move/<int:defaulttop_id>/<diff>/")
Robin Sonnabend's avatar
Robin Sonnabend committed
325
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
326
327
@db_lookup(DefaultTOP)
def move_default_top(defaulttop, diff):
Robin Sonnabend's avatar
Robin Sonnabend committed
328
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
329
    if not defaulttop.protocoltype.has_modify_right(user):
Robin Sonnabend's avatar
Robin Sonnabend committed
330
331
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
        return redirect(request.args.get("next") or url_for("index"))
Robin Sonnabend's avatar
Robin Sonnabend committed
332
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
333
        defaulttop.number += int(diff)
Robin Sonnabend's avatar
Robin Sonnabend committed
334
335
336
        db.session.commit()
    except ValueError:
        flash("Die angegebene Differenz ist keine Zahl.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
337
    return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=defaulttop.protocoltype.id))
Robin Sonnabend's avatar
Robin Sonnabend committed
338
339

@app.route("/protocols/list")
340
341
342
def list_protocols():
    is_logged_in = check_login()
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
    protocoltype = None
    protocoltype_id = None
    try:
        protocoltype_id = int(request.args.get("protocoltype"))
    except (ValueError, TypeError):
        pass
    search_term = request.args.get("search")
    protocoltypes = ProtocolType.get_public_protocoltypes(user)
    search_form = SearchForm(protocoltypes)
    if protocoltype_id is not None:
        search_form.protocoltype.data = protocoltype_id
        protocoltype = ProtocolType.query.filter_by(id=protocoltype_id).first()
    if search_term is not None:
        search_form.search.data = search_term
    protocol_query = Protocol.query
Robin Sonnabend's avatar
Robin Sonnabend committed
358
359
360
361
362
363
364
365
366
    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))
            ))
367
    protocols = [
Robin Sonnabend's avatar
Robin Sonnabend committed
368
        protocol for protocol in protocol_query.all()
369
370
371
372
        if (not is_logged_in and protocol.protocoltype.is_public)
        or (is_logged_in and (
            protocol.protocoltype.public_group in user.groups
            or protocol.protocoltype.private_group in user.groups))]
Robin Sonnabend's avatar
Robin Sonnabend committed
373
374
375
376
377
378
379
380
381
382
383
384
385
    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
386
387
388
389
390
    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
391
392
393
394
395
    if shall_search:
        protocols = [
            protocol for protocol in protocols
            if (protocol.protocoltype.has_private_view_right(user)
                and _matches_search(protocol.content_private))
396
            or (protocol.has_public_view_right(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
                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
                ]))
            search_results[protocol] = "<br />\n".join(formatted_lines)
Robin Sonnabend's avatar
Robin Sonnabend committed
425
    protocols = sorted(protocols, key=lambda protocol: protocol.date, reverse=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
426
    page = _get_page()
427
    page_count = int(math.ceil(len(protocols) / config.PAGE_LENGTH))
Robin Sonnabend's avatar
Robin Sonnabend committed
428
429
430
431
432
    if page >= page_count:
        page = 0
    begin_index = page * config.PAGE_LENGTH
    end_index = (page + 1) * config.PAGE_LENGTH
    protocols = protocols[begin_index:end_index]
Robin Sonnabend's avatar
Robin Sonnabend committed
433
    protocols_table = ProtocolsTable(protocols, search_results=search_results)
Robin Sonnabend's avatar
Robin Sonnabend committed
434
    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)
Robin Sonnabend's avatar
Robin Sonnabend committed
435
436
437
438
439

@app.route("/protocol/new", methods=["GET", "POST"])
@login_required
def new_protocol():
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
440
    protocoltypes = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
441
    form = NewProtocolForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
442
    upload_form = NewProtocolSourceUploadForm(protocoltypes)
443
    file_upload_form = NewProtocolFileUploadForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
444
445
446
447
448
449
450
451
    if form.validate_on_submit():
        protocoltype = ProtocolType.query.filter_by(id=form.protocoltype.data).first()
        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"))
        protocol = Protocol(protocoltype.id, form.date.data)
        db.session.add(protocol)
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
452
        tasks.push_tops_to_calendar(protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
453
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
Robin Sonnabend's avatar
Robin Sonnabend committed
454
455
456
    type_id = request.args.get("type_id")
    if type_id is not None:
        form.protocoltype.data = type_id
457
458
        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
459
460

@app.route("/protocol/show/<int:protocol_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
461
462
@db_lookup(Protocol)
def show_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
463
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
464
465
    if not protocol.protocoltype.has_public_view_right(user):
        flash("Die fehlen die nötigen Zugriffsrechte.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
466
467
        return redirect(request.args.get("next") or url_for("index"))
    errors_table = ErrorsTable(protocol.errors)
Robin Sonnabend's avatar
Robin Sonnabend committed
468
469
    visible_documents = [
        document for document in protocol.documents
470
        if (not document.is_private and document.protocol.has_public_view_right(user))
Robin Sonnabend's avatar
Robin Sonnabend committed
471
472
473
        or (document.is_private and document.protocol.protocoltype.has_private_view_right(user))
    ]
    documents_table = DocumentsTable(visible_documents)
474
    document_upload_form = DocumentUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
475
476
    source_upload_form = KnownProtocolSourceUploadForm()
    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)
Robin Sonnabend's avatar
Robin Sonnabend committed
477

Robin Sonnabend's avatar
Robin Sonnabend committed
478
479
@app.route("/protocol/delete/<int:protocol_id>")
@login_required
480
@group_required(config.ADMIN_GROUP)
Robin Sonnabend's avatar
Robin Sonnabend committed
481
482
@db_lookup(Protocol)
def delete_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
483
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
484
485
    if not protocol.protocoltype.has_modify_right(user):
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
486
487
        return redirect(request.args.get("next") or url_for("index"))
    name = protocol.get_identifier()
Robin Sonnabend's avatar
Robin Sonnabend committed
488
    protocol.delete_orphan_todos()
Robin Sonnabend's avatar
Robin Sonnabend committed
489
490
491
492
493
    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
494
@app.route("/protocol/etherpull/<int:protocol_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
495
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
496
497
@db_lookup(Protocol)
def etherpull_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
498
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
499
500
    if not protocol.protocoltype.has_modify_right(user):
        flash("Die fehlen die nötigen Zugriffsrechte.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
501
        return redirect(request.args.get("next") or url_for("index"))
502
503
504
    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
505
    protocol.source = get_etherpad_text(protocol.get_identifier())
Robin Sonnabend's avatar
Robin Sonnabend committed
506
507
508
509
510
    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
511
512
@app.route("/protocol/upload/known/<int:protocol_id>", methods=["POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
513
514
@db_lookup(Protocol)
def upload_source_to_known_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
515
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
516
517
    if not protocol.protocoltype.has_modify_right(user):
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
        return redirect(request.args.get("next") or url_for("index"))
    form = KnownProtocolSourceUploadForm()
    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()
540
    available_types = ProtocolType.get_modifiable_protocoltypes()
Robin Sonnabend's avatar
Robin Sonnabend committed
541
542
543
544
    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")
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
            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")
        protocoltype = ProtocolType.query.filter_by(id=form.protocoltype.data).first()
        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"))
        protocol = Protocol(protocoltype.id, None, source)
        db.session.add(protocol)
        db.session.commit()
        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)
        protocoltype = ProtocolType.query.filter_by(id=form.protocoltype.data).first()
        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"))
        protocol = Protocol(protocoltype.id, datetime.now().date(), done=True)
        db.session.add(protocol)
        db.session.commit()
        document = Document(protocol.id, filename, "", False, form.private.data)
        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
592
593
    return redirect(request.args.get("fail") or url_for("new_protocol"))

Administrator's avatar
Administrator committed
594
595
596
@app.route("/protocol/recompile/<int:protocol_id>")
@login_required
@group_required(config.ADMIN_GROUP)
Robin Sonnabend's avatar
Robin Sonnabend committed
597
598
@db_lookup(Protocol)
def recompile_protocol(protocol):
Administrator's avatar
Administrator committed
599
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
600
601
    if not protocol.protocoltype.has_modify_right(user):
        flash("Die fehlen die nötigen Zugriffsrechte.", "alert-error")
Administrator's avatar
Administrator committed
602
603
604
        return redirect(request.args.get("next") or url_for("index"))
    tasks.parse_protocol(protocol)
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
605

Robin Sonnabend's avatar
Robin Sonnabend committed
606
607
@app.route("/protocol/source/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
608
609
@db_lookup(Protocol)
def get_protocol_source(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
610
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
611
612
    if not protocol.protocoltype.has_modify_right(user):
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
613
614
615
616
        return redirect(request.args.get("next") or url_for("index"))
    file_like = BytesIO(protocol.source.encode("utf-8"))
    return send_file(file_like, cache_timeout=1, as_attachment=True, attachment_filename="{}.txt".format(protocol.get_identifier()))

Robin Sonnabend's avatar
Robin Sonnabend committed
617
618
@app.route("/protocol/template/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
619
620
@db_lookup(Protocol)
def get_protocol_template(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
621
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
622
623
    if not protocol.protocoltype.has_modify_right(user):
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
624
625
626
627
        return redirect(request.args.get("next") or url_for("index"))
    file_like = BytesIO(protocol.get_template().encode("utf-8"))
    return send_file(file_like, cache_timeout=1, as_attachment=True, attachment_filename="{}-template.txt".format(protocol.get_identifier()))

Robin Sonnabend's avatar
Robin Sonnabend committed
628
629
@app.route("/protocol/etherpush/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
630
631
@db_lookup(Protocol)
def etherpush_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
632
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
633
    if not protocol.protocoltype.has_modify_right(user):
Robin Sonnabend's avatar
Robin Sonnabend committed
634
635
        flash("Invalides Protokoll oder keine Berechtigung.", "alert-error")
        return redirect(request.args.get("next") or url_for("index"))
636
637
638
    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))
639
640
641
    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
642

Robin Sonnabend's avatar
Robin Sonnabend committed
643
@app.route("/protocol/update/<int:protocol_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
644
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
645
646
@db_lookup(Protocol)
def update_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
647
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
648
649
    if not protocol.protocoltype.has_modify_right(user):
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
650
        return redirect(request.args.get("next") or url_for("index"))
Robin Sonnabend's avatar
Robin Sonnabend committed
651
    upload_form = KnownProtocolSourceUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
652
    edit_form = ProtocolForm(obj=protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
653
654
655
    if edit_form.validate_on_submit():
        edit_form.populate_obj(protocol)
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
656
        tasks.push_tops_to_calendar(protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
657
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
Robin Sonnabend's avatar
Robin Sonnabend committed
658
    return render_template("protocol-update.html", upload_form=upload_form, edit_form=edit_form, protocol=protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
659

660
661
@app.route("/protocol/publish/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
662
663
@db_lookup(Protocol)
def publish_protocol(protocol):
664
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
665
666
    if not protocol.protocoltype.has_modify_right(user):
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
667
668
669
670
671
        return redirect(request.args.get("next") or url_for("index"))
    protocol.public = True
    db.session.commit()
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))

672
673
@app.route("/prococol/send/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
674
675
@db_lookup(Protocol)
def send_protocol(protocol):
676
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
677
678
    if not protocol.protocoltype.has_modify_right(user):
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
679
        return redirect(request.args.get("next") or url_for("index"))
680
681
682
    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))
683
684
685
686
    tasks.send_protocol(protocol)
    flash("Das Protokoll wurde versandt.", "alert-success")
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))

Robin Sonnabend's avatar
Robin Sonnabend committed
687
688
@app.route("/protocol/tops/new/<int:protocol_id>", methods=["GET", "POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
689
690
@db_lookup(Protocol)
def new_top(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
691
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
692
693
    if not protocol.protocoltype.has_modify_right(user):
        flash("Dir fehlen dir nötigen Zugriffsrechte.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
694
695
696
697
698
699
        return redirect(request.args.get("next") or url_for("index"))
    form = TopForm()
    if form.validate_on_submit():
        top = TOP(protocol_id=protocol.id, name=form.name.data, number=form.number.data, planned=True)
        db.session.add(top)
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
700
        tasks.push_tops_to_calendar(top.protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
701
702
703
704
705
706
707
708
709
        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
710
711
@db_lookup(TOP)
def edit_top(top):
Robin Sonnabend's avatar
Robin Sonnabend committed
712
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
713
714
    if not top.protocol.protocoltype.has_modify_right(user):
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
715
716
717
718
719
        return redirect(request.args.get("next") or url_for("index"))
    form = TopForm(obj=top)
    if form.validate_on_submit():
        form.populate_obj(top)
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
720
        tasks.push_tops_to_calendar(top.protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
721
722
723
724
725
        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
726
727
@db_lookup(TOP)
def delete_top(top):
Robin Sonnabend's avatar
Robin Sonnabend committed
728
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
729
730
    if not top.protocol.protocoltype.has_modify_right(user):
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
731
732
        return redirect(request.args.get("next") or url_for("index"))
    name = top.name
Robin Sonnabend's avatar
Robin Sonnabend committed
733
    protocol = top.protocol
Robin Sonnabend's avatar
Robin Sonnabend committed
734
735
    db.session.delete(top)
    db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
736
    tasks.push_tops_to_calendar(protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
737
    flash("Der TOP {} wurde gelöscht.".format(name), "alert-success")
Robin Sonnabend's avatar
Robin Sonnabend committed
738
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
Robin Sonnabend's avatar
Robin Sonnabend committed
739
740
741

@app.route("/protocol/top/move/<int:top_id>/<diff>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
742
743
@db_lookup(TOP)
def move_top(top, diff):
Robin Sonnabend's avatar
Robin Sonnabend committed
744
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
745
746
    if not top.protocol.protocoltype.has_modify_right(user):
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
747
748
749
750
        return redirect(request.args.get("next") or url_for("index"))
    try:
        top.number += int(diff)
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
751
        tasks.push_tops_to_calendar(top.protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
752
753
754
755
    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))

Robin Sonnabend's avatar
Robin Sonnabend committed
756
757
758
759
760
761
762
763
764
def _get_page():
    try:
        page = request.args.get("page")
        if page is None:
            return 0
        return int(page)
    except ValueError:
        return 0

765
@app.route("/todos/list")
Robin Sonnabend's avatar
Robin Sonnabend committed
766
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
767
768
def list_todos():
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
769
770
771
    protocoltype = None
    protocoltype_id = None
    try:
772
        protocoltype_id = int(request.args.get("protocoltype"))
Robin Sonnabend's avatar
Robin Sonnabend committed
773
774
775
    except (ValueError, TypeError):
        pass
    search_term = request.args.get("search")
Robin Sonnabend's avatar
Robin Sonnabend committed
776
    protocoltypes = ProtocolType.get_public_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
777
778
    search_form = SearchForm(protocoltypes)
    if protocoltype_id is not None:
779
        search_form.protocoltype.data = protocoltype_id
Robin Sonnabend's avatar
Robin Sonnabend committed
780
        protocoltype = ProtocolType.query.filter_by(id=protocoltype_id).first()
781
782
    if search_term is not None:
        search_form.search.data = search_term
Robin Sonnabend's avatar
Robin Sonnabend committed
783
784
785
786
    todos = [
        todo for todo in Todo.query.all()
        if todo.protocoltype.has_public_view_right(user)
    ]
Robin Sonnabend's avatar
Robin Sonnabend committed
787
    if protocoltype_id is not None and protocoltype_id != -1:
Robin Sonnabend's avatar
Robin Sonnabend committed
788
789
790
791
        todos = [
            todo for todo in todos
            if todo.protocoltype.id == protocoltype_id
        ]
Robin Sonnabend's avatar
Robin Sonnabend committed
792
    if search_term is not None and len(search_term.strip()) > 0:
Robin Sonnabend's avatar
Robin Sonnabend committed
793
794
795
796
        todos = [
            todo for todo in todos
            if search_term.lower() in todo.description.lower()
        ]
Robin Sonnabend's avatar
Robin Sonnabend committed
797
    def _sort_key(todo):
Administrator's avatar
Administrator committed
798
        return (not todo.is_done(), todo.get_id())
Robin Sonnabend's avatar
Robin Sonnabend committed
799
    todos = sorted(todos, key=_sort_key, reverse=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
800
    page = _get_page()
Robin Sonnabend's avatar
Robin Sonnabend committed
801
    page_count = int(math.ceil(len(todos) / config.PAGE_LENGTH))
Robin Sonnabend's avatar
Robin Sonnabend committed
802
803
804
805
    if page >= page_count:
        page = 0
    begin_index = page * config.PAGE_LENGTH
    end_index = (page + 1) * config.PAGE_LENGTH
Robin Sonnabend's avatar
Robin Sonnabend committed
806
    todos = todos[begin_index:end_index]
Robin Sonnabend's avatar
Robin Sonnabend committed
807
    todos_table = TodosTable(todos)
Robin Sonnabend's avatar
Robin Sonnabend committed
808
    return render_template("todos-list.html", todos=todos, todos_table=todos_table, search_form=search_form, page=page, page_count=page_count, page_diff=config.PAGE_DIFF, protocoltype_id=protocoltype_id, search_term=search_term)
Robin Sonnabend's avatar
Robin Sonnabend committed
809

Robin Sonnabend's avatar
Robin Sonnabend committed
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
@app.route("/todo/new", methods=["GET", "POST"])
@login_required
def new_todo():
    user = current_user()
    protocoltype_id = optional_int_arg("type_id")
    protocol_id = optional_int_arg("protocol_id")
    protocoltype = ProtocolType.query.filter_by(id=protocoltype_id).first()
    protocol = Protocol.query.filter_by(id=protocol_id).first()
    if protocoltype is not None and protocol is not None:
        if protocol.protocoltype != protocoltype:
            flash("Ungültige Protokoll-Typ-Kombination", "alert-error")
            return redirect(request.args.get("next") or url_for("index"))
    if protocoltype is None and protocol is not None:
        protocoltype = protocol.protocoltype
    protocoltypes = ProtocolType.get_modifiable_protocoltypes(user)
    form = NewTodoForm(protocoltypes)
    if form.validate_on_submit():
        added_protocoltype = ProtocolType.query.filter_by(id=form.protocoltype_id.data).first()
        if added_protocoltype is None or not added_protocoltype.has_modify_right(user):
            flash("Invalider Protokolltyp.")
            return redirect(request.args.get("next") or url_for("index"))
        todo = Todo(type_id=form.protocoltype_id.data, who=form.who.data,
            description=form.description.data, tags=form.tags.data,
            done=form.done.data)
        if protocol is not None:
            todo.protocols.append(protocol)
        db.session.add(todo)
        db.session.commit()
        todo.number = todo.id
        db.session.commit()
        flash("Todo wurde angelegt.", "alert-success")
        if protocol is not None:
            return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
        else:
            return redirect(request.args.get("next") or url_for("list_todos", protocoltype_id=protocoltype_id))
    else:
        if protocoltype is not None:
            form.protocoltype_id.data = protocoltype.id
    return render_template("todo-new.html", form=form, protocol=protocol, protocoltype=protocoltype)

@app.route("/todo/edit/<int:todo_id>", methods=["GET", "POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
852
853
@db_lookup(Todo)
def edit_todo(todo):
Robin Sonnabend's avatar
Robin Sonnabend committed
854
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
855
    if not todo.protocoltype.has_modify_right(user):
Robin Sonnabend's avatar
Robin Sonnabend committed
856
857
858
859
860
861
862
863
864
865
866
        flash("Invalides Todo oder unzureichende Berechtigung.", "alert-error")
        return redirect(request.args.get("next") or url_for("index"))
    form = TodoForm(obj=todo)
    if form.validate_on_submit():
        form.populate_obj(todo)
        db.session.commit()
        return redirect(request.args.get("next") or url_for("list_todos", protocoltype=todo.protocoltype.id))
    return render_template("todo-edit.html", form=form, todo=todo)

@app.route("/todo/show/<int:todo_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
867
868
@db_lookup(Todo)
def show_todo(todo):
Robin Sonnabend's avatar
Robin Sonnabend committed
869
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
870
871
    if not todo.protocoltype.has_private_view_right(user):
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
872
873
874
875
876
877
        return redirect(request.args.get("next") or url_for("index"))
    todo_table = TodoTable(todo)
    return render_template("todo-show.html", todo=todo, todo_table=todo_table)

@app.route("/todo/delete/<int:todo_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
878
879
@db_lookup(Todo)
def delete_todo(todo):
Robin Sonnabend's avatar
Robin Sonnabend committed
880
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
881
882
    if not todo.protocoltype.has_private_view_right(user):
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
883
884
885
886
887
888
889
        return redirect(request.args.get("next") or url_for("index"))
    type_id = todo.protocoltype.id
    db.session.delete(todo)
    db.session.commit()
    flash("Todo gelöscht.", "alert-success")
    return redirect(request.args.get("next") or url_for("list_todos", protocoltype=type_id))

Robin Sonnabend's avatar
Robin Sonnabend committed
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
@app.route("/decisions/list")
def list_decisions():
    is_logged_In = check_login()
    user = current_user()
    protocoltype = None
    protocoltype_id = None
    try:
        protocoltype_id = int(request.args.get("protocoltype"))
    except (ValueError, TypeError):
        pass
    search_term = request.args.get("search")
    protocoltypes = ProtocolType.get_public_protocoltypes(user)
    search_form = SearchForm(protocoltypes)
    if protocoltype_id is not None:
        search_form.protocoltype.data = protocoltype_id
        protocoltype = ProtocolType.query.filter_by(id=protocoltype_id).first()
    if search_term is not None:
        search_form.search.data = search_term
    decisions = [
        decision for decision in Decision.query.all()
910
        if decision.protocol.has_public_view_right(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
911
912
913
914
915
916
917
918
919
920
921
    ]
    if protocoltype_id is not None and protocoltype_id != -1:
        decisions = [
            decision for decision in decisions 
            if decision.protocol.protocoltype.id == protocoltype_id
        ]
    if search_term is not None and len(search_term.strip()) > 0:
        decisions = [
            decision for decision in decisions
            if search_term.lower() in decision.content.lower()
        ]
Robin Sonnabend's avatar
Robin Sonnabend committed
922
923
    decisions = sorted(decisions, key=lambda d: d.protocol.date, reverse=True)
        
Robin Sonnabend's avatar
Robin Sonnabend committed
924
925
926
927
928
929
930
931
932
933
    page = _get_page()
    page_count = int(math.ceil(len(decisions) / config.PAGE_LENGTH))
    if page >= page_count:
        page = 0
    begin_index = page * config.PAGE_LENGTH
    end_index = (page + 1) * config.PAGE_LENGTH
    decisions = decisions[begin_index:end_index]
    decisions_table = DecisionsTable(decisions)
    return render_template("decisions-list.html", decisions=decisions, decisions_table=decisions_table, search_form=search_form, page=page, page_count=page_count, page_diff=config.PAGE_DIFF, protocoltype_id=protocoltype_id, search_term=search_term)

934
@app.route("/document/download/<int:document_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
935
936
@db_lookup(Document)
def download_document(document):
937
938
939
940
    user = current_user()
    if ((document.is_private
            and not document.protocol.protocoltype.has_private_view_right(user))
        or (not document.is_private
941
            and not document.protocol.has_public_view_right(user))):
Robin Sonnabend's avatar
Robin Sonnabend committed
942
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
943
        return redirect(request.args.get("next") or url_for("index"))
944
    return send_file(document.as_file_like(), cache_timeout=1, as_attachment=True, attachment_filename=document.name)
945
946
947

@app.route("/document/upload/<int:protocol_id>", methods=["POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
948
949
@db_lookup(Protocol)
def upload_document(protocol):
950
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
951
952
    if not protocol.protocoltype.has_modify_right(user):
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
953
954
955
        return redirect(request.args.get("next") or url_for("index"))
    form = DocumentUploadForm()
    if form.document.data is None:
Robin Sonnabend's avatar
Robin Sonnabend committed
956
        flash("Es wurde keine Datei ausgewählt.", "alert-error")
957
958
959
        return redirect(request.args.get("next") or url_for("index"))
    file = form.document.data
    if file.filename == "":
Robin Sonnabend's avatar
Robin Sonnabend committed
960
        flash("Es wurde keine Datei ausgewählt.", "alert-error")
961
        return redirect(request.args.get("next") or url_for("index"))
Robin Sonnabend's avatar
Robin Sonnabend committed
962
    # todo: Dateitypen einschränken?
963
964
    if file:
        filename = secure_filename(file.filename)
Robin Sonnabend's avatar
Robin Sonnabend committed
965
        document = Document(protocol.id, filename, "", False, form.private.data)
966
967
        db.session.add(document)
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
968
969
970
971
972
973
974
975
976
977
        internal_filename = "{}-{}-{}".format(protocol.id, document.id, filename)
        document.filename = internal_filename
        file.save(os.path.join(config.DOCUMENTS_PATH, internal_filename))
        if datetime.now().date() >= protocol.date:
            protocol.done = True
        db.session.commit()
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))

@app.route("/document/delete/<int:document_id>")
@login_required
978
@group_required(config.ADMIN_GROUP)
Robin Sonnabend's avatar
Robin Sonnabend committed
979
980
@db_lookup(Document)
def delete_document(document):
Robin Sonnabend's avatar
Robin Sonnabend committed
981
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
982
983
    if not document.protocol.protocoltype.has_modify_right(user):
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
984
985
986
987
988
989
        return redirect(request.args.get("next") or url_for("index"))
    name = document.name
    protocol = document.protocol
    db.session.delete(document)
    db.session.commit()
    flash("Das Dokument {} wurde gelöscht.".format(name), "alert-success")
990
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
Robin Sonnabend's avatar
Robin Sonnabend committed