server.py 66.9 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
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,
34
    get_internal_filename, get_csrf_token)
35
from decorators import (
36
    db_lookup, protect_csrf,
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
    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
app.jinja_env.globals["get_csrf_token"] = get_csrf_token
93

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

99

100
101
import tasks

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

111

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

122

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

130

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

153

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

159

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

178

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

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

238
239
        def _todo_sort_key(todo):
            protocol = todo.get_first_protocol()
240
241
242
            if protocol is not None and protocol.date is not None:
                return protocol.date
            return datetime.now().date()
243
        todos = sorted(todos, key=_todo_sort_key, reverse=True)
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():
253
254
255
256
    return render_template(
        "documentation.html")

@app.route("/documentation/sessionmanagement")
257
@back.anchor
258
259
260
261
262
263
@login_required
def sessionmanagement_documentation():
    return render_template(
        "documentation-sessionmanagement.html")

@app.route("/documentation/sessionmanagement/plan")
264
@back.anchor
265
266
267
268
269
270
@login_required
def plan_sessionmanagement_documentation():
    return render_template(
        "documentation-sessionmanagement-plan.html")

@app.route("/documentation/sessionmanagement/write")
271
@back.anchor
272
273
274
275
276
277
@login_required
def write_sessionmanagement_documentation():
    return render_template(
        "documentation-sessionmanagement-write.html")

@app.route("/documentation/sessionmanagement/tracking")
278
@back.anchor
279
280
281
282
283
284
@login_required
def tracking_sessionmanagement_documentation():
    return render_template(
        "documentation-sessionmanagement-tracking.html")

@app.route("/documentation/syntax")
285
@back.anchor
286
287
288
289
290
291
@login_required
def syntax_documentation():
    return render_template(
        "documentation-syntax.html")

@app.route("/documentation/syntax/meta")
292
@back.anchor
293
294
295
296
297
298
@login_required
def meta_syntax_documentation():
    return render_template(
        "documentation-syntax-meta.html")

@app.route("/documentation/syntax/top")
299
@back.anchor
300
301
302
303
304
305
@login_required
def top_syntax_documentation():
    return render_template(
        "documentation-syntax-top.html")

@app.route("/documentation/syntax/lists")
306
@back.anchor
307
308
309
310
311
@login_required
def lists_syntax_documentation():
    return render_template("documentation-syntax-lists.html")

@app.route("/documentation/syntax/internal")
312
@back.anchor
313
314
315
316
317
318
@login_required
def internal_syntax_documentation():
    return render_template(
        "documentation-syntax-internal.html")

@app.route("/documentation/syntax/tags")
319
@back.anchor
320
321
@login_required
def tags_syntax_documentation():
Robin Sonnabend's avatar
Robin Sonnabend committed
322
323
    todostates = list(TodoState)
    name_to_state = TodoState.get_name_to_state()
324
325
326
327
328
    return render_template(
        "documentation-syntax-tags.html", todostates=todostates,
        name_to_state=name_to_state)

@app.route("/documentation/configuration")
329
@back.anchor
330
331
332
333
334
335
@login_required
def configuration_documentation():
    return render_template(
        "documentation-configuration.html")

@app.route("/documentation/configuration/types")
336
@back.anchor
337
338
339
340
341
342
@login_required
def types_configuration_documentation():
    return render_template(
        "documentation-configuration-types.html")

@app.route("/documentation/configuration/todomails")
343
@back.anchor
344
345
346
347
@login_required
def todomails_configuration_documentation():
    return render_template(
        "documentation-configuration-todomails.html")
Robin Sonnabend's avatar
Robin Sonnabend committed
348

Robin Sonnabend's avatar
Robin Sonnabend committed
349
@app.route("/types/list")
350
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
351
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
352
353
354
355
def list_types():
    user = current_user()
    types = [
        protocoltype for protocoltype in ProtocolType.query.all()
356
        if (protocoltype.has_private_view_right(user)
357
358
            or protocoltype.has_public_view_right(user)
            or protocoltype.is_public)]
359
    types = sorted(types, key=lambda t: t.short_name)
Robin Sonnabend's avatar
Robin Sonnabend committed
360
    types_table = ProtocolTypesTable(types)
361
362
363
    return render_template(
        "types-list.html", types=types, types_table=types_table)

Robin Sonnabend's avatar
Robin Sonnabend committed
364
365
366
367
368
369
370
371

@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:
372
373
            flash("Du kannst keinen internen Protokolltypen anlegen, "
                  "zu dem du selbst keinen Zugang hast.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
374
        else:
375
376
            protocoltype = ProtocolType()
            form.populate_obj(protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
377
378
            db.session.add(protocoltype)
            db.session.commit()
379
380
            flash("Der Protokolltyp {} wurde angelegt.".format(
                protocoltype.name), "alert-success")
381
        return back.redirect("list_types")
Robin Sonnabend's avatar
Robin Sonnabend committed
382
383
    return render_template("type-new.html", form=form)

384

Robin Sonnabend's avatar
Robin Sonnabend committed
385
@app.route("/type/edit/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
386
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
387
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
388
@require_private_view_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
389
def edit_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
390
391
392
393
    user = current_user()
    form = ProtocolTypeForm(obj=protocoltype)
    if form.validate_on_submit():
        if form.private_group.data not in user.groups:
394
395
            flash("Du kannst keinen internen Protokolltypen anlegen, "
                  "zu dem du selbst keinen Zugang hast.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
396
397
398
        else:
            form.populate_obj(protocoltype)
            db.session.commit()
399
            return back.redirect("show_type", protocoltype_id=protocoltype.id)
400
401
402
    return render_template(
        "type-edit.html", form=form, protocoltype=protocoltype)

Robin Sonnabend's avatar
Robin Sonnabend committed
403

Robin Sonnabend's avatar
Robin Sonnabend committed
404
@app.route("/type/show/<int:protocoltype_id>")
405
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
406
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
407
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
408
@require_private_view_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
409
def show_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
410
    protocoltype_table = ProtocolTypeTable(protocoltype)
411
412
413
414
    default_tops_table = DefaultTOPsTable(
        protocoltype.default_tops, protocoltype)
    reminders_table = MeetingRemindersTable(
        protocoltype.reminders, protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
415
    metas_table = DefaultMetasTable(protocoltype.metas, protocoltype)
416
417
418
419
420
421
422
423
424
    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)

425

Robin Sonnabend's avatar
Robin Sonnabend committed
426
@app.route("/type/delete/<int:protocoltype_id>")
427
@login_required
428
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
429
@db_lookup(ProtocolType)
430
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
431
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
432
def delete_type(protocoltype):
433
    name = protocoltype.name
434
    db.session.delete(protocoltype)
435
436
    db.session.commit()
    flash("Der Protokolltype {} wurde gelöscht.".format(name), "alert-success")
437
    return back.redirect("list_types")
438

439
440
441

@app.route("/type/reminders/new/<int:protocoltype_id>",
           methods=["GET", "POST"])
442
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
443
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
444
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
445
def new_reminder(protocoltype):
446
447
    form = MeetingReminderForm()
    if form.validate_on_submit():
448
449
        meetingreminder = MeetingReminder(protocoltype_id=protocoltype.id)
        form.populate_obj(meetingreminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
450
        db.session.add(meetingreminder)
451
        db.session.commit()
452
        return back.redirect("show_type", protocoltype_id=protocoltype.id)
453
454
    return render_template(
        "reminder-new.html", form=form, protocoltype=protocoltype)
455

456
457
458

@app.route("/type/reminder/edit/<int:meetingreminder_id>",
           methods=["GET", "POST"])
459
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
460
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
461
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
462
463
def edit_reminder(meetingreminder):
    form = MeetingReminderForm(obj=meetingreminder)
464
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
465
        form.populate_obj(meetingreminder)
466
        db.session.commit()
467
468
469
470
471
        return back.redirect(
            "show_type", protocoltype_id=meetingreminder.protocoltype.id)
    return render_template(
        "reminder-edit.html", form=form, meetingreminder=meetingreminder)

472

Robin Sonnabend's avatar
Robin Sonnabend committed
473
@app.route("/type/reminder/delete/<int:meetingreminder_id>")
474
@login_required
475
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
476
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
477
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
478
479
480
def delete_reminder(meetingreminder):
    protocoltype = meetingreminder.protocoltype
    db.session.delete(meetingreminder)
481
    db.session.commit()
482
    return back.redirect("show_type", protocoltype_id=protocoltype.id)
483
484


Robin Sonnabend's avatar
Robin Sonnabend committed
485
@app.route("/type/tops/new/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
486
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
487
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
488
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
489
def new_default_top(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
490
491
    form = DefaultTopForm()
    if form.validate_on_submit():
492
493
        defaulttop = DefaultTOP(protocoltype_id=protocoltype.id)
        form.populate_obj(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
494
        db.session.add(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
495
        db.session.commit()
496
497
        for protocol in protocoltype.protocols:
            if not protocol.done:
498
499
                localtop = LocalTOP(
                    protocol_id=protocol.id,
Administrator's avatar
Administrator committed
500
                    defaulttop_id=defaulttop.id, description="")
501
502
                db.session.add(localtop)
        db.session.commit()
503
504
        flash("Der Standard-TOP {} wurde für dem Protokolltyp {} hinzugefügt."
              .format(defaulttop.name, protocoltype.name), "alert-success")
505
        return back.redirect()
506
507
    return render_template(
        "default-top-new.html", form=form, protocoltype=protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
508
509


510
511
@app.route("/type/tops/edit/<int:protocoltype_id>/<int:defaulttop_id>",
           methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
512
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
513
@db_lookup(ProtocolType, DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
514
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
515
516
def edit_default_top(protocoltype, defaulttop):
    form = DefaultTopForm(obj=defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
517
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
518
        form.populate_obj(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
519
        db.session.commit()
520
        return back.redirect("show_type", protocoltype_id=protocoltype.id)
521
522
523
524
    return render_template(
        "default-top-edit.html", form=form,
        protocoltype=protocoltype, defaulttop=defaulttop)

Robin Sonnabend's avatar
Robin Sonnabend committed
525

Robin Sonnabend's avatar
Robin Sonnabend committed
526
@app.route("/type/tops/delete/<int:defaulttop_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
527
@login_required
528
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
529
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
530
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
531
532
def delete_default_top(defaulttop):
    db.session.delete(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
533
    db.session.commit()
534
535
536
    return back.redirect(
        "show_type", protocoltype_id=defaulttop.protocoltype.id)

Robin Sonnabend's avatar
Robin Sonnabend committed
537

Robin Sonnabend's avatar
Robin Sonnabend committed
538
@app.route("/type/tops/move/<int:defaulttop_id>/<diff>/")
Robin Sonnabend's avatar
Robin Sonnabend committed
539
@login_required
540
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
541
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
542
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
543
def move_default_top(defaulttop, diff):
Robin Sonnabend's avatar
Robin Sonnabend committed
544
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
545
        defaulttop.number += int(diff)
Robin Sonnabend's avatar
Robin Sonnabend committed
546
547
548
        db.session.commit()
    except ValueError:
        flash("Die angegebene Differenz ist keine Zahl.", "alert-error")
549
550
551
    return back.redirect(
        "show_type", protocoltype_id=defaulttop.protocoltype.id)

Robin Sonnabend's avatar
Robin Sonnabend committed
552
553

@app.route("/protocols/list")
554
@back.anchor
555
556
def list_protocols():
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
557
558
    protocoltype_id = None
    try:
559
        protocoltype_id = int(request.args.get("protocoltype_id"))
Robin Sonnabend's avatar
Robin Sonnabend committed
560
561
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
562
    state_open = -1
Robin Sonnabend's avatar
Robin Sonnabend committed
563
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
564
        state_open = int(request.args.get("state_open"))
Robin Sonnabend's avatar
Robin Sonnabend committed
565
566
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
567
    search_term = request.args.get("search")
568
569
    protocoltypes = ProtocolType.get_public_protocoltypes(
        user, check_networks=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
570
    search_form = ProtocolSearchForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
571
    if protocoltype_id is not None:
572
        search_form.protocoltype_id.data = protocoltype_id
Robin Sonnabend's avatar
Robin Sonnabend committed
573
574
    if state_open is not None:
        search_form.state_open.data = state_open
Robin Sonnabend's avatar
Robin Sonnabend committed
575
576
577
    if search_term is not None:
        search_form.search.data = search_term
    protocol_query = Protocol.query
Robin Sonnabend's avatar
Robin Sonnabend committed
578
579
580
581
582
583
584
585
586
    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))
            ))
587
    protocols = [
Robin Sonnabend's avatar
Robin Sonnabend committed
588
        protocol for protocol in protocol_query.all()
589
590
        if protocol.protocoltype.has_public_view_right(
            user, check_networks=False)
591
    ]
592

Robin Sonnabend's avatar
Robin Sonnabend committed
593
594
595
596
597
598
    def _matches_search(content):
        content = content.lower()
        for search_term in search_terms:
            if search_term.lower() not in content:
                return False
        return True
599

Robin Sonnabend's avatar
Robin Sonnabend committed
600
601
602
603
604
605
606
    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
607
608
609
610
611
    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
612
613
    if state_open is not None and state_open != -1:
        protocol_done = bool(state_open)
Robin Sonnabend's avatar
Robin Sonnabend committed
614
615
        protocols = [
            protocol for protocol in protocols
616
            if (protocol.is_done() or False) == protocol_done
Robin Sonnabend's avatar
Robin Sonnabend committed
617
        ]
Robin Sonnabend's avatar
Robin Sonnabend committed
618
619
620
621
622
    if shall_search:
        protocols = [
            protocol for protocol in protocols
            if (protocol.protocoltype.has_private_view_right(user)
                and _matches_search(protocol.content_private))
623
            or (protocol.has_public_view_right(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
624
625
626
                and _matches_search(protocol.content_public))
        ]
        for protocol in protocols:
627
            content = protocol.get_visible_content(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
628
629
630
631
632
633
634
635
            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):
636
637
638
639
640
641
                    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
642
643
644
645
                    if len(index_candidates) == 0:
                        parts.append((line[last_index:], False))
                        break
                    else:
646
647
                        new_index, term = min(
                            index_candidates, key=lambda t: t[0])
Robin Sonnabend's avatar
Robin Sonnabend committed
648
649
650
651
652
653
654
655
                        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
                ]))
656
            search_results[protocol] = " …<br />\n".join(formatted_lines)
657
658
    protocols = sorted(
        protocols, key=lambda protocol: protocol.date, reverse=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
659
    page = _get_page()
Robin Sonnabend's avatar
Robin Sonnabend committed
660
661
    page_length = _get_page_length()
    page_count = int(math.ceil(len(protocols) / page_length))
Robin Sonnabend's avatar
Robin Sonnabend committed
662
663
    if page >= page_count:
        page = 0
Robin Sonnabend's avatar
Robin Sonnabend committed
664
665
    begin_index = page * page_length
    end_index = (page + 1) * page_length
666
    max_page_length_exp = get_max_page_length_exp(protocols)
Robin Sonnabend's avatar
Robin Sonnabend committed
667
    protocols = protocols[begin_index:end_index]
Robin Sonnabend's avatar
Robin Sonnabend committed
668
    protocols_table = ProtocolsTable(protocols, search_results=search_results)
669
670
671
672
673
674
675
676
    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
677
678
679
680
681

@app.route("/protocol/new", methods=["GET", "POST"])
@login_required
def new_protocol():
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
682
    protocoltypes = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
683
    form = NewProtocolForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
684
    upload_form = NewProtocolSourceUploadForm(protocoltypes)
685
    file_upload_form = NewProtocolFileUploadForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
686
    if form.validate_on_submit():
687
688
        protocoltype = ProtocolType.query.filter_by(
            id=form.protocoltype_id.data).first()
Robin Sonnabend's avatar
Robin Sonnabend committed
689
690
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
691
            return back.redirect()
692
693
        protocol = Protocol.create_new_protocol(
            protocoltype, form.date.data, form.start_time.data)
694
        return back.redirect("show_protocol", protocol_id=protocol.id)
695
    type_id = request.args.get("protocoltype_id")
Robin Sonnabend's avatar
Robin Sonnabend committed
696
697
    if type_id is not None:
        form.protocoltype.data = type_id
698
        upload_form.protocoltype.data = type_id
699
700
701
702
703
    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
704
705

@app.route("/protocol/show/<int:protocol_id>")
706
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
707
708
@db_lookup(Protocol)
def show_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
709
710
    user = current_user()
    errors_table = ErrorsTable(protocol.errors)
711
712
    if not protocol.protocoltype.has_public_view_right(
            user, check_networks=False):
713
714
715
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
        if check_login():
            return redirect(url_for("index"))
716
        return redirect(url_for("login"))
Robin Sonnabend's avatar
Robin Sonnabend committed
717
718
    visible_documents = [
        document for document in protocol.documents
719
720
721
722
        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
723
    ]
724
    documents_table = DocumentsTable(visible_documents, protocol)
725
    document_upload_form = DocumentUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
726
    source_upload_form = KnownProtocolSourceUploadForm()
727
728
    time_diff = protocol.date - datetime.now().date()
    large_time_diff = not protocol.is_done() and time_diff.days > 0
729
730
    content_html = (
        protocol.content_html_private
731
732
        if protocol.has_private_view_right(user)
        else protocol.content_html_public)
733
734
    if content_html is not None:
        content_html = Markup(content_html)
735
736
737
738
739
740
741
    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
742

Robin Sonnabend's avatar
Robin Sonnabend committed
743
744
@app.route("/protocol/delete/<int:protocol_id>")
@login_required
745
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
746
@db_lookup(Protocol)
747
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
748
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
749
def delete_protocol(protocol):
750
    name = protocol.get_short_identifier()
Robin Sonnabend's avatar
Robin Sonnabend committed
751
    protocol.delete_orphan_todos()
Robin Sonnabend's avatar
Robin Sonnabend committed
752
753
754
    db.session.delete(protocol)
    db.session.commit()
    flash("Protokoll {} ist gelöscht.".format(name), "alert-success")
755
    return back.redirect("list_protocols")
Robin Sonnabend's avatar
Robin Sonnabend committed
756
757


Robin Sonnabend's avatar
Robin Sonnabend committed
758
@app.route("/protocol/etherpull/<int:protocol_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
759
@login_required
760
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
761
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
762
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
763
def etherpull_protocol(protocol):
764
765
    if not config.ETHERPAD_ACTIVE:
        flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
766
        return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
767
    protocol.source = get_etherpad_text(protocol.get_identifier())
Robin Sonnabend's avatar
Robin Sonnabend committed
768
769
770
    db.session.commit()
    tasks.parse_protocol(protocol)
    flash("Das Protokoll wird kompiliert.", "alert-success")
771
    return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
772
773


Robin Sonnabend's avatar
Robin Sonnabend committed
774
775
@app.route("/protocol/upload/known/<int:protocol_id>", methods=["POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
776
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
777
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
778
def upload_source_to_known_protocol(protocol):
779
    form = KnownProtocolSourceUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
780
781
782
783
784
785
786
787
788
789
790
791
792
793
    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")
794
    return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
795
796
797
798
799
800


@app.route("/protocol/upload/new/", methods=["POST"])
@login_required
def upload_new_protocol():
    user = current_user()
801
    available_types = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
802
803
804
805
    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")
806
807
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
808
809
810
        file = form.source.data
        if file.filename == "":
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
811
812
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
813
        source = file.stream.read().decode("utf-8")
814
815
        protocoltype = ProtocolType.query.filter_by(
            id=form.protocoltype_id.data).first()
816
817
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Invalider Protokolltyp oder keine Rechte.", "alert-error")
818
819
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
820
        protocol = Protocol(protocoltype_id=protocoltype.id, source=source)
821
822
        db.session.add(protocol)
        db.session.commit()
823
        for local_top in protocol.create_localtops():
824
825
            db.session.add(local_top)
        db.session.commit()
826
        tasks.parse_protocol(protocol)
827
        return back.redirect("show_protocol", protocol_id=protocol.id)
828
829
    return redirect(request.args.get("fail") or url_for("new_protocol"))

830

831
832
833
834
835
836
837
838
839
@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")
840
841
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
842
843
844
        file = form.file.data
        if file.filename == "":
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
845
846
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
847
        filename = secure_filename(file.filename)
848
849
        protocoltype = ProtocolType.query.filter_by(
            id=form.protocoltype_id.data).first()
850
851
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Invalider Protokolltyp oder keine Rechte.", "alert-error")
852
853
854
855
856
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
        protocol = Protocol(
            protocoltype_id=protocoltype.id,
            date=datetime.now().date(), done=True)
857
858
        db.session.add(protocol)
        db.session.commit()
859
        for local_top in protocol.create_localtops():
860
861
            db.session.add(local_top)
        db.session.commit()
862
863
        document = Document(
            protocol_id=protocol.id, name=filename,
864
865
            filename="", is_compiled=False)
        form.populate_obj(document)
866
867
        db.session.add(document)
        db.session.commit()
868
        internal_filename = get_internal_filename(
869
            protocol, document.id, filename)
870
871
872
        document.filename = internal_filename
        file.save(os.path.join(config.DOCUMENTS_PATH, internal_filename))
        db.session.commit()
873
        return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
874
875
    return redirect(request.args.get("fail") or url_for("new_protocol"))

876

Administrator's avatar
Administrator committed
877
878
@app.route("/protocol/recompile/<int:protocol_id>")
@login_required
879
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
880
@db_lookup(Protocol)
881
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
882
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
883
def recompile_protocol(protocol):
Administrator's avatar
Administrator committed
884
    tasks.parse_protocol(protocol)
885
    return back.redirect("show_protocol", protocol_id=protocol.id)
886
887


Robin Sonnabend's avatar
Robin Sonnabend committed
888
889
@app.route("/protocol/source/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
890