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

5
6
7
from flask import (
    Flask, request, session, flash, redirect,
    url_for, abort, render_template, Response, Markup)
8
from werkzeug.utils import secure_filename
9
10
11
from flask_script import Manager, prompt
from flask_migrate import Migrate, MigrateCommand
from celery import Celery
12
from sqlalchemy import or_
13
14
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
Robin Sonnabend's avatar
Robin Sonnabend committed
15
import atexit
16
import feedgen.feed
17
import icalendar
18
from io import BytesIO
19
import os
20
from datetime import datetime, timedelta
Robin Sonnabend's avatar
Robin Sonnabend committed
21
import math
22
import mimetypes
23
24

import config
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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,
    class_filter, needs_date_test, todostate_name_filter,
    code_filter, indent_tab_filter)
from utils import (
    get_first_unused_int, get_etherpad_text, split_terms, optional_int_arg,
    fancy_join, footnote_hash, get_git_revision, get_max_page_length_exp,
    get_internal_filename)
from decorators import (
    db_lookup,
    require_private_view_right, require_modify_right, require_publish_right,
    require_admin_right)
from models.database import (
    ProtocolType, Protocol, DefaultTOP, TOP, LocalTOP,
    Document, Todo, Decision, MeetingReminder, Error, TodoMail,
    DecisionDocument, TodoState, DefaultMeta, DecisionCategory, Like)
from views.forms import (
    LoginForm, ProtocolTypeForm, DefaultTopForm,
    MeetingReminderForm, NewProtocolForm, DocumentUploadForm,
    KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm,
    generate_protocol_form, TopForm, LocalTopForm,
    DecisionSearchForm, ProtocolSearchForm, TodoSearchForm,
    NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm,
    DefaultMetaForm, MergeTodosForm, DecisionCategoryForm,
    DocumentEditForm)
from views.tables import (
    ProtocolsTable, ProtocolTypesTable,
    ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable,
    TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable,
    TodoMailsTable, DefaultMetasTable, DecisionCategoriesTable)
57
from legacy import import_old_todos, import_old_protocols, import_old_todomails
58
import back
59
60
61
62
63
64
65
66

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

67

68
69
70
71
72
def make_celery(app, config):
    celery = Celery(app.import_name, broker=config.CELERY_BROKER_URL)
    celery.conf.update(app.config)
    return celery

73
74

celery = make_celery(app, config)
75
76
77
78
79

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
80
app.jinja_env.filters["timify"] = time_filter
81
app.jinja_env.filters["timify_short"] = time_filter_short
Robin Sonnabend's avatar
Robin Sonnabend committed
82
app.jinja_env.filters["datify_short"] = date_filter_short
Robin Sonnabend's avatar
Robin Sonnabend committed
83
app.jinja_env.filters["datify_long"] = date_filter_long
84
app.jinja_env.filters["class"] = class_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
85
86
app.jinja_env.filters["todo_get_name"] = todostate_name_filter
app.jinja_env.filters["code"] = code_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
87
app.jinja_env.filters["indent_tab"] = indent_tab_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
88
app.jinja_env.filters["fancy_join"] = fancy_join
Robin Sonnabend's avatar
Robin Sonnabend committed
89
app.jinja_env.filters["footnote_hash"] = footnote_hash
90
app.jinja_env.tests["auth_valid"] = security_manager.check_user
Robin Sonnabend's avatar
Robin Sonnabend committed
91
app.jinja_env.tests["needs_date"] = needs_date_test
92

93
94
95
96
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)
97

98

99
100
import tasks

101
102
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
103
app.jinja_env.globals.update(zip=zip)
Robin Sonnabend's avatar
Robin Sonnabend committed
104
105
106
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
107
app.jinja_env.globals.update(now=datetime.now)
108
app.jinja_env.globals["git_revision"] = get_git_revision()
109

110

111
112
@manager.command
def import_legacy():
113
    """Import the old todos and protocols from an sql dump"""
114
    filename = prompt("SQL-file")
115
116
    with open(filename, "rb") as sqlfile:
        content = sqlfile.read().decode("utf-8")
117
118
        import_old_todos(content)
        import_old_protocols(content)
119
        import_old_todomails(content)
120

121

122
123
124
125
@manager.command
def recompile_all():
    for protocol in sorted(Protocol.query.all(), key=lambda p: p.date):
        if protocol.is_done():
126
            print(protocol.get_short_identifier())
127
128
            tasks.parse_protocol(protocol)

129

130
@manager.command
131
def merge_duplicate_todos():
132
133
134
135
136
137
138
139
    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)
140
            if todo2.state.value > todo1.state.value:
141
142
143
144
145
146
147
148
149
150
151
                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

152

153
154
@manager.command
def runserver():
155
    app.run()
156
157
    make_scheduler()

158

159
def send_file(file_like, cache_timeout, as_attachment, attachment_filename):
160
161
162
    """
    Replaces flask.send_file since that uses an uwsgi function that is buggy.
    """
163
164
    mimetype, _ = mimetypes.guess_type(attachment_filename)
    response = Response(file_like.read(), mimetype)
165
    if as_attachment:
166
167
        response.headers["Content-Disposition"] = (
            'attachment; filename="{}"'.format(attachment_filename))
168
169
170
171
    content_type = mimetype
    if mimetype.startswith("text/"):
        content_type = "{}; charset=utf-8".format(content_type)
    response.headers["Content-Type"] = content_type
172
173
    response.headers["Cache-Control"] = (
        "public, max-age={}".format(cache_timeout))
174
    response.headers["Connection"] = "close"
175
    return response
176

177

178
@app.route("/")
179
@back.anchor
180
def index():
Robin Sonnabend's avatar
Robin Sonnabend committed
181
182
183
    user = current_user()
    protocols = [
        protocol for protocol in Protocol.query.all()
184
185
        if protocol.protocoltype.has_public_view_right(
            user, check_networks=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
186
    ]
187

188
    def _protocol_sort_key(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
189
190
191
        if protocol.date is not None:
            return protocol.date
        return datetime.now().date()
Robin Sonnabend's avatar
Robin Sonnabend committed
192
    current_day = datetime.now().date()
193
    open_protocols = sorted(
Robin Sonnabend's avatar
Robin Sonnabend committed
194
195
196
197
        [
            protocol for protocol in protocols
            if not protocol.done
            and (protocol.date - current_day).days < config.MAX_INDEX_DAYS
198
            and (current_day - protocol.date).days < config.MAX_PAST_INDEX_DAYS
Robin Sonnabend's avatar
Robin Sonnabend committed
199
        ],
200
        key=_protocol_sort_key
Robin Sonnabend's avatar
Robin Sonnabend committed
201
202
    )
    finished_protocols = sorted(
203
204
        [
            protocol for protocol in protocols
205
            if protocol.done and protocol.public
206
207
208
209
            and (
                protocol.has_private_view_right(user)
                or protocol.protocoltype.has_public_view_right(
                    user, check_networks=False))
210
        ],
211
212
        key=_protocol_sort_key,
        reverse=True
Robin Sonnabend's avatar
Robin Sonnabend committed
213
    )
214
215
216
217
218
219
    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)
220
221
        has_public_view_right = (
            protocol.protocoltype.has_public_view_right(user))
Robin Sonnabend's avatar
Robin Sonnabend committed
222
223
224
    todos = None
    if check_login():
        todos = [
225
            todo for todo in Todo.query.all()
226
            if todo.protocoltype.has_private_view_right(user)
227
            and not todo.is_done()
Robin Sonnabend's avatar
Robin Sonnabend committed
228
        ]
229
230
        user_todos = [
            todo for todo in todos
231
232
            if user.username.lower()
            in list(map(str.strip, todo.who.lower().split(",")))
233
234
235
        ]
        if len(user_todos) > 0:
            todos = user_todos
236

237
238
        def _todo_sort_key(todo):
            protocol = todo.get_first_protocol()
239
240
241
            if protocol is not None and protocol.date is not None:
                return protocol.date
            return datetime.now().date()
242
        todos = sorted(todos, key=_todo_sort_key, reverse=True)
243
244
245
246
247
    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)

248

Robin Sonnabend's avatar
Robin Sonnabend committed
249
@app.route("/documentation")
250
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
251
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
252
def documentation():
Robin Sonnabend's avatar
Robin Sonnabend committed
253
254
    todostates = list(TodoState)
    name_to_state = TodoState.get_name_to_state()
255
256
257
258
    return render_template(
        "documentation.html", todostates=todostates,
        name_to_state=name_to_state)

Robin Sonnabend's avatar
Robin Sonnabend committed
259

Robin Sonnabend's avatar
Robin Sonnabend committed
260
@app.route("/types/list")
261
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
262
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
263
264
265
266
def list_types():
    user = current_user()
    types = [
        protocoltype for protocoltype in ProtocolType.query.all()
267
        if (protocoltype.has_private_view_right(user)
268
269
            or protocoltype.has_public_view_right(user)
            or protocoltype.is_public)]
270
    types = sorted(types, key=lambda t: t.short_name)
Robin Sonnabend's avatar
Robin Sonnabend committed
271
    types_table = ProtocolTypesTable(types)
272
273
274
    return render_template(
        "types-list.html", types=types, types_table=types_table)

Robin Sonnabend's avatar
Robin Sonnabend committed
275
276
277
278
279
280
281
282

@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:
283
284
            flash("Du kannst keinen internen Protokolltypen anlegen, "
                  "zu dem du selbst keinen Zugang hast.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
285
        else:
286
287
            protocoltype = ProtocolType()
            form.populate_obj(protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
288
289
            db.session.add(protocoltype)
            db.session.commit()
290
291
            flash("Der Protokolltyp {} wurde angelegt.".format(
                protocoltype.name), "alert-success")
292
        return back.redirect("list_types")
Robin Sonnabend's avatar
Robin Sonnabend committed
293
294
    return render_template("type-new.html", form=form)

295

Robin Sonnabend's avatar
Robin Sonnabend committed
296
@app.route("/type/edit/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
297
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
298
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
299
@require_private_view_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
300
def edit_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
301
302
303
304
    user = current_user()
    form = ProtocolTypeForm(obj=protocoltype)
    if form.validate_on_submit():
        if form.private_group.data not in user.groups:
305
306
            flash("Du kannst keinen internen Protokolltypen anlegen, "
                  "zu dem du selbst keinen Zugang hast.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
307
308
309
        else:
            form.populate_obj(protocoltype)
            db.session.commit()
310
            return back.redirect("show_type", protocoltype_id=protocoltype.id)
311
312
313
    return render_template(
        "type-edit.html", form=form, protocoltype=protocoltype)

Robin Sonnabend's avatar
Robin Sonnabend committed
314

Robin Sonnabend's avatar
Robin Sonnabend committed
315
@app.route("/type/show/<int:protocoltype_id>")
316
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
317
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
318
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
319
@require_private_view_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
320
def show_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
321
    protocoltype_table = ProtocolTypeTable(protocoltype)
322
323
324
325
    default_tops_table = DefaultTOPsTable(
        protocoltype.default_tops, protocoltype)
    reminders_table = MeetingRemindersTable(
        protocoltype.reminders, protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
326
    metas_table = DefaultMetasTable(protocoltype.metas, protocoltype)
327
328
329
330
331
332
333
334
335
    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)

336

Robin Sonnabend's avatar
Robin Sonnabend committed
337
@app.route("/type/delete/<int:protocoltype_id>")
338
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
339
@db_lookup(ProtocolType)
340
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
341
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
342
def delete_type(protocoltype):
343
    name = protocoltype.name
344
    db.session.delete(protocoltype)
345
346
    db.session.commit()
    flash("Der Protokolltype {} wurde gelöscht.".format(name), "alert-success")
347
    return back.redirect("list_types")
348

349
350
351

@app.route("/type/reminders/new/<int:protocoltype_id>",
           methods=["GET", "POST"])
352
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
353
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
354
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
355
def new_reminder(protocoltype):
356
357
    form = MeetingReminderForm()
    if form.validate_on_submit():
358
359
        meetingreminder = MeetingReminder(protocoltype_id=protocoltype.id)
        form.populate_obj(meetingreminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
360
        db.session.add(meetingreminder)
361
        db.session.commit()
362
        return back.redirect("show_type", protocoltype_id=protocoltype.id)
363
364
    return render_template(
        "reminder-new.html", form=form, protocoltype=protocoltype)
365

366
367
368

@app.route("/type/reminder/edit/<int:meetingreminder_id>",
           methods=["GET", "POST"])
369
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
370
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
371
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
372
373
def edit_reminder(meetingreminder):
    form = MeetingReminderForm(obj=meetingreminder)
374
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
375
        form.populate_obj(meetingreminder)
376
        db.session.commit()
377
378
379
380
381
        return back.redirect(
            "show_type", protocoltype_id=meetingreminder.protocoltype.id)
    return render_template(
        "reminder-edit.html", form=form, meetingreminder=meetingreminder)

382

Robin Sonnabend's avatar
Robin Sonnabend committed
383
@app.route("/type/reminder/delete/<int:meetingreminder_id>")
384
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
385
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
386
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
387
388
389
def delete_reminder(meetingreminder):
    protocoltype = meetingreminder.protocoltype
    db.session.delete(meetingreminder)
390
    db.session.commit()
391
    return back.redirect("show_type", protocoltype_id=protocoltype.id)
392

393

Robin Sonnabend's avatar
Robin Sonnabend committed
394
@app.route("/type/tops/new/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
395
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
396
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
397
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
398
def new_default_top(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
399
400
    form = DefaultTopForm()
    if form.validate_on_submit():
401
402
        defaulttop = DefaultTOP(protocoltype_id=protocoltype.id)
        form.populate_obj(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
403
        db.session.add(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
404
        db.session.commit()
405
406
        for protocol in protocoltype.protocols:
            if not protocol.done:
407
408
                localtop = LocalTOP(
                    protocol_id=protocol.id,
Administrator's avatar
Administrator committed
409
                    defaulttop_id=defaulttop.id, description="")
410
411
                db.session.add(localtop)
        db.session.commit()
412
413
        flash("Der Standard-TOP {} wurde für dem Protokolltyp {} hinzugefügt."
              .format(defaulttop.name, protocoltype.name), "alert-success")
414
        return back.redirect()
415
416
417
    return render_template(
        "default-top-new.html", form=form, protocoltype=protocoltype)

Robin Sonnabend's avatar
Robin Sonnabend committed
418

419
420
@app.route("/type/tops/edit/<int:protocoltype_id>/<int:defaulttop_id>",
           methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
421
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
422
@db_lookup(ProtocolType, DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
423
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
424
425
def edit_default_top(protocoltype, defaulttop):
    form = DefaultTopForm(obj=defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
426
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
427
        form.populate_obj(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
428
        db.session.commit()
429
        return back.redirect("show_type", protocoltype_id=protocoltype.id)
430
431
432
433
    return render_template(
        "default-top-edit.html", form=form,
        protocoltype=protocoltype, defaulttop=defaulttop)

Robin Sonnabend's avatar
Robin Sonnabend committed
434

Robin Sonnabend's avatar
Robin Sonnabend committed
435
@app.route("/type/tops/delete/<int:defaulttop_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
436
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
437
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
438
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
439
440
def delete_default_top(defaulttop):
    db.session.delete(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
441
    db.session.commit()
442
443
444
    return back.redirect(
        "show_type", protocoltype_id=defaulttop.protocoltype.id)

Robin Sonnabend's avatar
Robin Sonnabend committed
445

Robin Sonnabend's avatar
Robin Sonnabend committed
446
@app.route("/type/tops/move/<int:defaulttop_id>/<diff>/")
Robin Sonnabend's avatar
Robin Sonnabend committed
447
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
448
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
449
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
450
def move_default_top(defaulttop, diff):
Robin Sonnabend's avatar
Robin Sonnabend committed
451
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
452
        defaulttop.number += int(diff)
Robin Sonnabend's avatar
Robin Sonnabend committed
453
454
455
        db.session.commit()
    except ValueError:
        flash("Die angegebene Differenz ist keine Zahl.", "alert-error")
456
457
458
    return back.redirect(
        "show_type", protocoltype_id=defaulttop.protocoltype.id)

Robin Sonnabend's avatar
Robin Sonnabend committed
459
460

@app.route("/protocols/list")
461
@back.anchor
462
463
def list_protocols():
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
464
465
    protocoltype_id = None
    try:
466
        protocoltype_id = int(request.args.get("protocoltype_id"))
Robin Sonnabend's avatar
Robin Sonnabend committed
467
468
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
469
    state_open = -1
Robin Sonnabend's avatar
Robin Sonnabend committed
470
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
471
        state_open = int(request.args.get("state_open"))
Robin Sonnabend's avatar
Robin Sonnabend committed
472
473
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
474
    search_term = request.args.get("search")
475
476
    protocoltypes = ProtocolType.get_public_protocoltypes(
        user, check_networks=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
477
    search_form = ProtocolSearchForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
478
    if protocoltype_id is not None:
479
        search_form.protocoltype_id.data = protocoltype_id
Robin Sonnabend's avatar
Robin Sonnabend committed
480
481
    if state_open is not None:
        search_form.state_open.data = state_open
Robin Sonnabend's avatar
Robin Sonnabend committed
482
483
484
    if search_term is not None:
        search_form.search.data = search_term
    protocol_query = Protocol.query
Robin Sonnabend's avatar
Robin Sonnabend committed
485
486
487
488
489
490
491
492
493
    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))
            ))
494
    protocols = [
Robin Sonnabend's avatar
Robin Sonnabend committed
495
        protocol for protocol in protocol_query.all()
496
497
        if protocol.protocoltype.has_public_view_right(
            user, check_networks=False)
498
    ]
499

Robin Sonnabend's avatar
Robin Sonnabend committed
500
501
502
503
504
505
    def _matches_search(content):
        content = content.lower()
        for search_term in search_terms:
            if search_term.lower() not in content:
                return False
        return True
506

Robin Sonnabend's avatar
Robin Sonnabend committed
507
508
509
510
511
512
513
    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
514
515
516
517
518
    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
519
520
    if state_open is not None and state_open != -1:
        protocol_done = bool(state_open)
Robin Sonnabend's avatar
Robin Sonnabend committed
521
522
        protocols = [
            protocol for protocol in protocols
523
            if (protocol.is_done() or False) == protocol_done
Robin Sonnabend's avatar
Robin Sonnabend committed
524
        ]
Robin Sonnabend's avatar
Robin Sonnabend committed
525
526
527
528
529
    if shall_search:
        protocols = [
            protocol for protocol in protocols
            if (protocol.protocoltype.has_private_view_right(user)
                and _matches_search(protocol.content_private))
530
            or (protocol.has_public_view_right(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
531
532
533
                and _matches_search(protocol.content_public))
        ]
        for protocol in protocols:
534
            content = protocol.get_visible_content(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
535
536
537
538
539
540
541
542
            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):
543
544
545
546
547
548
                    index_candidates = list(filter(
                        lambda t: t[0] != -1,
                        [
                            (lower_line.find(term, last_index), term)
                            for term in search_terms
                        ]))
Robin Sonnabend's avatar
Robin Sonnabend committed
549
550
551
552
                    if len(index_candidates) == 0:
                        parts.append((line[last_index:], False))
                        break
                    else:
553
554
                        new_index, term = min(
                            index_candidates, key=lambda t: t[0])
Robin Sonnabend's avatar
Robin Sonnabend committed
555
556
557
558
559
560
561
562
                        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
                ]))
563
            search_results[protocol] = " …<br />\n".join(formatted_lines)
564
565
    protocols = sorted(
        protocols, key=lambda protocol: protocol.date, reverse=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
566
    page = _get_page()
Robin Sonnabend's avatar
Robin Sonnabend committed
567
568
    page_length = _get_page_length()
    page_count = int(math.ceil(len(protocols) / page_length))
Robin Sonnabend's avatar
Robin Sonnabend committed
569
570
    if page >= page_count:
        page = 0
Robin Sonnabend's avatar
Robin Sonnabend committed
571
572
    begin_index = page * page_length
    end_index = (page + 1) * page_length
573
    max_page_length_exp = get_max_page_length_exp(protocols)
Robin Sonnabend's avatar
Robin Sonnabend committed
574
    protocols = protocols[begin_index:end_index]
Robin Sonnabend's avatar
Robin Sonnabend committed
575
    protocols_table = ProtocolsTable(protocols, search_results=search_results)
576
577
578
579
580
581
582
583
    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
584
585
586
587
588

@app.route("/protocol/new", methods=["GET", "POST"])
@login_required
def new_protocol():
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
589
    protocoltypes = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
590
    form = NewProtocolForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
591
    upload_form = NewProtocolSourceUploadForm(protocoltypes)
592
    file_upload_form = NewProtocolFileUploadForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
593
    if form.validate_on_submit():
594
595
        protocoltype = ProtocolType.query.filter_by(
            id=form.protocoltype_id.data).first()
Robin Sonnabend's avatar
Robin Sonnabend committed
596
597
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
598
            return back.redirect()
599
600
        protocol = Protocol.create_new_protocol(
            protocoltype, form.date.data, form.start_time.data)
601
        return back.redirect("show_protocol", protocol_id=protocol.id)
602
    type_id = request.args.get("protocoltype_id")
Robin Sonnabend's avatar
Robin Sonnabend committed
603
604
    if type_id is not None:
        form.protocoltype.data = type_id
605
        upload_form.protocoltype.data = type_id
606
607
608
609
610
    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
611
612

@app.route("/protocol/show/<int:protocol_id>")
613
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
614
615
@db_lookup(Protocol)
def show_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
616
617
    user = current_user()
    errors_table = ErrorsTable(protocol.errors)
618
619
    if not protocol.protocoltype.has_public_view_right(
            user, check_networks=False):
620
621
622
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
        if check_login():
            return redirect(url_for("index"))
623
        return redirect(url_for("login"))
Robin Sonnabend's avatar
Robin Sonnabend committed
624
625
    visible_documents = [
        document for document in protocol.documents
626
627
628
629
        if (not document.is_private
            and document.protocol.has_public_view_right(user))
        or (document.is_private
            and document.protocol.protocoltype.has_private_view_right(user))
Robin Sonnabend's avatar
Robin Sonnabend committed
630
    ]
631
    documents_table = DocumentsTable(visible_documents, protocol)
632
    document_upload_form = DocumentUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
633
    source_upload_form = KnownProtocolSourceUploadForm()
634
635
    time_diff = protocol.date - datetime.now().date()
    large_time_diff = not protocol.is_done() and time_diff.days > 0
636
637
    content_html = (
        protocol.content_html_private
638
639
        if protocol.has_private_view_right(user)
        else protocol.content_html_public)
640
641
    if content_html is not None:
        content_html = Markup(content_html)
642
643
644
645
646
647
648
    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
649

Robin Sonnabend's avatar
Robin Sonnabend committed
650
651
@app.route("/protocol/delete/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
652
@db_lookup(Protocol)
653
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
654
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
655
def delete_protocol(protocol):
656
    name = protocol.get_short_identifier()
Robin Sonnabend's avatar
Robin Sonnabend committed
657
    protocol.delete_orphan_todos()
Robin Sonnabend's avatar
Robin Sonnabend committed
658
659
660
    db.session.delete(protocol)
    db.session.commit()
    flash("Protokoll {} ist gelöscht.".format(name), "alert-success")
661
    return back.redirect("list_protocols")
Robin Sonnabend's avatar
Robin Sonnabend committed
662

663

Robin Sonnabend's avatar
Robin Sonnabend committed
664
@app.route("/protocol/etherpull/<int:protocol_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
665
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
666
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
667
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
668
def etherpull_protocol(protocol):
669
670
    if not config.ETHERPAD_ACTIVE:
        flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
671
        return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
672
    protocol.source = get_etherpad_text(protocol.get_identifier())
Robin Sonnabend's avatar
Robin Sonnabend committed
673
674
675
    db.session.commit()
    tasks.parse_protocol(protocol)
    flash("Das Protokoll wird kompiliert.", "alert-success")
676
    return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
677

678

Robin Sonnabend's avatar
Robin Sonnabend committed
679
680
@app.route("/protocol/upload/known/<int:protocol_id>", methods=["POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
681
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
682
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
683
def upload_source_to_known_protocol(protocol):
684
    form = KnownProtocolSourceUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
685
686
687
688
689
690
691
692
693
694
695
696
697
698
    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")
699
    return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
700

701

Robin Sonnabend's avatar
Robin Sonnabend committed
702
703
704
705
@app.route("/protocol/upload/new/", methods=["POST"])
@login_required
def upload_new_protocol():
    user = current_user()
706
    available_types = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
707
708
709
710
    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")
711
712
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
713
714
715
        file = form.source.data
        if file.filename == "":
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
716
717
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
718
        source = file.stream.read().decode("utf-8")
719
720
        protocoltype = ProtocolType.query.filter_by(
            id=form.protocoltype_id.data).first()
721
722
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Invalider Protokolltyp oder keine Rechte.", "alert-error")
723
724
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
725
        protocol = Protocol(protocoltype_id=protocoltype.id, source=source)
726
727
        db.session.add(protocol)
        db.session.commit()
728
729
730
        for local_top in protocol.create_localtops:
            db.session.add(local_top)
        db.session.commit()
731
        tasks.parse_protocol(protocol)
732
        return back.redirect("show_protocol", protocol_id=protocol.id)
733
734
    return redirect(request.args.get("fail") or url_for("new_protocol"))

735

736
737
738
739
740
741
742
743
744
@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")
745
746
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
747
748
749
        file = form.file.data
        if file.filename == "":
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
750
751
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
752
        filename = secure_filename(file.filename)
753
754
        protocoltype = ProtocolType.query.filter_by(
            id=form.protocoltype_id.data).first()
755
756
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Invalider Protokolltyp oder keine Rechte.", "alert-error")
757
758
759
760
761
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
        protocol = Protocol(
            protocoltype_id=protocoltype.id,
            date=datetime.now().date(), done=True)
762
763
        db.session.add(protocol)
        db.session.commit()
764
765
766
        for local_top in protocol.create_localtops:
            db.session.add(local_top)
        db.session.commit()
767
768
        document = Document(
            protocol_id=protocol.id, name=filename,
769
770
            filename="", is_compiled=False)
        form.populate_obj(document)
771
772
        db.session.add(document)
        db.session.commit()
773
774
        internal_filename = get_internal_filename(
            protocol.id, document.id, filename)
775
776
777
        document.filename = internal_filename
        file.save(os.path.join(config.DOCUMENTS_PATH, internal_filename))
        db.session.commit()
778
        return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
779
780
    return redirect(request.args.get("fail") or url_for("new_protocol"))

781

Administrator's avatar
Administrator committed
782
783
@app.route("/protocol/recompile/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
784
@db_lookup(Protocol)
785
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
786
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
787
def recompile_protocol(protocol):
Administrator's avatar
Administrator committed
788
    tasks.parse_protocol(protocol)
789
    return back.redirect("show_protocol", protocol_id=protocol.id)
790

791

Robin Sonnabend's avatar
Robin Sonnabend committed
792
793
@app.route("/protocol/source/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
794
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
795
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
796
def get_protocol_source(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
797
    file_like = BytesIO(protocol.source.encode("utf-8"))
798
799
800
801
    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
802

Robin Sonnabend's avatar
Robin Sonnabend committed
803
804
@app.route("/protocol/template/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
805
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
806
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
807
def get_protocol_template(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
808
    file_like = BytesIO(protocol.get_template().encode("utf-8"))
809
810
811
812
813
    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
814

Robin Sonnabend's avatar
Robin Sonnabend committed
815
816
@app.route("/protocol/etherpush/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
817
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
818
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
819
def etherpush_protocol(protocol):
820
821
    if not config.ETHERPAD_ACTIVE:
        flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
822
        return back.redirect("show_protocol", protocol_id=protocol.id)
823
824
    if not protocol.is_done():
        tasks.set_etherpad_content(protocol)
825
    return redirect(protocol.get_etherpad_link())
Robin Sonnabend's avatar
Robin Sonnabend committed
826

827

Robin Sonnabend's avatar
Robin Sonnabend committed
828
@app.route("/protocol/update/<int:protocol_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
829
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
830
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
831
@require_publish_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
832
def update_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
833
    upload_form = KnownProtocolSourceUploadForm()
834
    edit_form = generate_protocol_form(protocol)(obj=protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
835
836
    if edit_form.validate_on_submit():
        edit_form.populate_obj(protocol)
837
838
        for meta in protocol.metas:
            meta.value = getattr(edit_form.metas, meta.name).data
Robin Sonnabend's avatar
Robin Sonnabend committed
839
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
840
        tasks.push_tops_to_calendar(protocol)
841
        return back.redirect("show_protocol", protocol_id=protocol.id)
842
843
    for meta in protocol.metas:
        getattr(edit_form.metas, meta.name).data = meta.value
844
845
846
847
    return render_template(
        "protocol-update.html", upload_form=upload_form,
        edit_form=edit_form, protocol=protocol)

Robin Sonnabend's avatar
Robin Sonnabend committed
848

849
850
@app.route("/protocol/publish/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
851
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
852
@require_publish_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
853
def publish_protocol(protocol):
854
855
    protocol.public = True
    db.session.commit()
856
    return back.redirect("show_protocol", protocol_id=protocol.id)
857

858

859
@app.route("/prococol/send/private/<int:protocol_id>")
860
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
861
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
862
@require_modify_right()
863
def send_protocol_private(protocol):
864
865
    if not config.MAIL_ACTIVE:
        flash("Die Mailfunktion ist nicht aktiviert.", "alert-error")
866
        return back.redirect("show_protocol", protocol_id=protocol.id)
867
868
    tasks.send_protocol_private(protocol)
    flash("Das Protokoll wurde versandt.", "alert-success")
869
    return back.redirect("show_protocol", protocol_id=protocol.id)
870

871

872
873
874
@app.route("/prococol/send/public/<int:protocol_id>")
@login_required
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
875
@require_publish_right()
876
877
878