server.py 69.2 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
import click
9
from werkzeug.utils import secure_filename
10
from flask_migrate import Migrate
11
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
from shared import (
25
    config, db, date_filter, datetime_filter, date_filter_long,
26
27
28
    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,
29
    code_filter, code_key_filter, indent_tab_filter)
30
31
32
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,
33
    get_internal_filename, get_current_ip)
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from decorators import (
    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)
55
from legacy import import_old_todos, import_old_protocols, import_old_todomails
Robin Sonnabend's avatar
Robin Sonnabend committed
56
from common import back
57
58
59
from common.csrf import protect_csrf, get_csrf_token
from common.database import db_lookup

60
61
62
63
64
65

app = Flask(__name__)
app.config.from_object(config)
db.init_app(app)
migrate = Migrate(app, db)

66
67
68
69
70
71
72
73
74
75
76
try:
    from raven.contrib.flask import Sentry
    sentry = Sentry(app, dsn=config.SENTRY_DSN)

    def get_user_info(request):
        return {
            "is_authenticated": check_login(),
            "ip_address": get_current_ip(),
            "release": get_git_revision(),
        }
    sentry.get_user_info = get_user_info
Lars Beckers's avatar
Lars Beckers committed
77
except ImportError:
78
79
80
81
    print("Raven not installed. Not sending issues to Sentry.")
except AttributeError:
    print("DSN not configured. Not sending issues to Sentry.")

82

83
84
85
def make_celery(app, config):
    celery = Celery(app.import_name, broker=config.CELERY_BROKER_URL)
    celery.conf.update(app.config)
86
87
88
89
90
91
92
    try:
        from raven import Client as RavenClient
        from raven.contrib.celery import (
            register_signal, register_logger_signal)
        raven_client = RavenClient(config.SENTRY_DSN)
        register_logger_signal(raven_client)
        register_signal(raven_client)
Lars Beckers's avatar
Lars Beckers committed
93
    except ImportError:
94
95
96
        print("Raven not installed. Not sending celery issues to Sentry.")
    except AttributeError:
        print("DSN not configured. Not sending celery issues to Sentry.")
97
98
    return celery

99
100

celery = make_celery(app, config)
101
102
103
104
105

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
106
app.jinja_env.filters["timify"] = time_filter
107
app.jinja_env.filters["timify_short"] = time_filter_short
Robin Sonnabend's avatar
Robin Sonnabend committed
108
app.jinja_env.filters["datify_short"] = date_filter_short
Robin Sonnabend's avatar
Robin Sonnabend committed
109
app.jinja_env.filters["datify_long"] = date_filter_long
110
app.jinja_env.filters["class"] = class_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
111
112
app.jinja_env.filters["todo_get_name"] = todostate_name_filter
app.jinja_env.filters["code"] = code_filter
113
app.jinja_env.filters["code_key"] = code_key_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
114
app.jinja_env.filters["indent_tab"] = indent_tab_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
115
app.jinja_env.filters["fancy_join"] = fancy_join
Robin Sonnabend's avatar
Robin Sonnabend committed
116
app.jinja_env.filters["footnote_hash"] = footnote_hash
117
app.jinja_env.tests["auth_valid"] = security_manager.check_user
Robin Sonnabend's avatar
Robin Sonnabend committed
118
app.jinja_env.tests["needs_date"] = needs_date_test
119
app.jinja_env.globals["get_csrf_token"] = get_csrf_token
120

121
122
123
124
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)
125

126

127
128
import tasks

129
130
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
131
app.jinja_env.globals.update(zip=zip)
Robin Sonnabend's avatar
Robin Sonnabend committed
132
133
134
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
135
app.jinja_env.globals.update(now=datetime.now)
136
app.jinja_env.globals["git_revision"] = get_git_revision()
137

138

139
@app.cli.command()
140
def import_legacy():
141
    """Import the old todos and protocols from an sql dump"""
142
    filename = prompt("SQL-file")
143
144
    with open(filename, "rb") as sqlfile:
        content = sqlfile.read().decode("utf-8")
145
146
        import_old_todos(content)
        import_old_protocols(content)
147
        import_old_todomails(content)
148

149

150
@app.cli.command()
151
152
153
def recompile_all():
    for protocol in sorted(Protocol.query.all(), key=lambda p: p.date):
        if protocol.is_done():
154
            print(protocol.get_short_identifier())
155
156
            tasks.parse_protocol(protocol)

157

158
@app.cli.command()
159
def merge_duplicate_todos():
160
161
162
163
164
165
166
167
    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)
168
            if todo2.state.value > todo1.state.value:
169
170
171
172
173
174
175
176
177
178
179
                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

180

181
@app.cli.command()
182
183
184
185
186
187
def check_config():
    #  TODO: check how to set return codes
    import configproxy
    return configproxy.check_config()


188
189
@app.cli.command()
@click.argument("filename")
190
191
192
def create_example_config(filename):
    import configproxy
    return configproxy.write_example_config(filename=filename)
193

194

195
def send_file(file_like, cache_timeout, as_attachment, attachment_filename):
196
197
198
    """
    Replaces flask.send_file since that uses an uwsgi function that is buggy.
    """
199
200
    mimetype, _ = mimetypes.guess_type(attachment_filename)
    response = Response(file_like.read(), mimetype)
201
    if as_attachment:
202
203
        response.headers["Content-Disposition"] = (
            'attachment; filename="{}"'.format(attachment_filename))
204
205
206
207
    content_type = mimetype
    if mimetype.startswith("text/"):
        content_type = "{}; charset=utf-8".format(content_type)
    response.headers["Content-Type"] = content_type
208
209
    response.headers["Cache-Control"] = (
        "public, max-age={}".format(cache_timeout))
210
    response.headers["Connection"] = "close"
211
    return response
212

213

214
@app.route("/")
215
@back.anchor
216
def index():
Robin Sonnabend's avatar
Robin Sonnabend committed
217
218
219
    user = current_user()
    protocols = [
        protocol for protocol in Protocol.query.all()
220
221
        if protocol.protocoltype.has_public_view_right(
            user, check_networks=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
222
    ]
223

224
    def _protocol_sort_key(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
225
226
227
        if protocol.date is not None:
            return protocol.date
        return datetime.now().date()
Robin Sonnabend's avatar
Robin Sonnabend committed
228
    current_day = datetime.now().date()
229
    open_protocols = sorted(
Robin Sonnabend's avatar
Robin Sonnabend committed
230
231
232
233
        [
            protocol for protocol in protocols
            if not protocol.done
            and (protocol.date - current_day).days < config.MAX_INDEX_DAYS
234
            and (current_day - protocol.date).days < config.MAX_PAST_INDEX_DAYS
Robin Sonnabend's avatar
Robin Sonnabend committed
235
        ],
236
        key=_protocol_sort_key
Robin Sonnabend's avatar
Robin Sonnabend committed
237
238
    )
    finished_protocols = sorted(
239
240
        [
            protocol for protocol in protocols
241
            if protocol.done and protocol.public
242
243
244
245
            and (
                protocol.has_private_view_right(user)
                or protocol.protocoltype.has_public_view_right(
                    user, check_networks=False))
246
        ],
247
248
        key=_protocol_sort_key,
        reverse=True
Robin Sonnabend's avatar
Robin Sonnabend committed
249
    )
250
251
252
253
254
255
    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)
256
257
        has_public_view_right = (
            protocol.protocoltype.has_public_view_right(user))
Robin Sonnabend's avatar
Robin Sonnabend committed
258
259
260
    todos = None
    if check_login():
        todos = [
261
            todo for todo in Todo.query.all()
262
            if todo.protocoltype.has_private_view_right(user)
263
            and not todo.is_done()
Robin Sonnabend's avatar
Robin Sonnabend committed
264
        ]
265
266
        user_todos = [
            todo for todo in todos
267
268
            if user.username.lower()
            in list(map(str.strip, todo.who.lower().split(",")))
269
270
271
        ]
        if len(user_todos) > 0:
            todos = user_todos
272

273
274
        def _todo_sort_key(todo):
            protocol = todo.get_first_protocol()
275
276
277
            if protocol is not None and protocol.date is not None:
                return protocol.date
            return datetime.now().date()
278
        todos = sorted(todos, key=_todo_sort_key, reverse=True)
279
280
281
282
    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)
283

Robin Sonnabend's avatar
Robin Sonnabend committed
284
@app.route("/documentation")
285
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
286
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
287
def documentation():
288
289
290
291
    return render_template(
        "documentation.html")

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

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

@app.route("/documentation/sessionmanagement/write")
306
@back.anchor
307
308
309
310
311
312
@login_required
def write_sessionmanagement_documentation():
    return render_template(
        "documentation-sessionmanagement-write.html")

@app.route("/documentation/sessionmanagement/tracking")
313
@back.anchor
314
315
316
317
318
319
@login_required
def tracking_sessionmanagement_documentation():
    return render_template(
        "documentation-sessionmanagement-tracking.html")

@app.route("/documentation/syntax")
320
@back.anchor
321
322
323
324
325
326
@login_required
def syntax_documentation():
    return render_template(
        "documentation-syntax.html")

@app.route("/documentation/syntax/meta")
327
@back.anchor
328
329
330
331
332
333
@login_required
def meta_syntax_documentation():
    return render_template(
        "documentation-syntax-meta.html")

@app.route("/documentation/syntax/top")
334
@back.anchor
335
336
337
338
339
340
@login_required
def top_syntax_documentation():
    return render_template(
        "documentation-syntax-top.html")

@app.route("/documentation/syntax/lists")
341
@back.anchor
342
343
344
345
346
@login_required
def lists_syntax_documentation():
    return render_template("documentation-syntax-lists.html")

@app.route("/documentation/syntax/internal")
347
@back.anchor
348
349
350
351
352
353
@login_required
def internal_syntax_documentation():
    return render_template(
        "documentation-syntax-internal.html")

@app.route("/documentation/syntax/tags")
354
@back.anchor
355
356
@login_required
def tags_syntax_documentation():
357
358
359
360
361
362
    states = {state:[] for state in list(TodoState)}
    name_to_state = TodoState.get_name_to_state()
    for state_name in name_to_state:
        states[name_to_state[state_name]].append(state_name)
    return render_template(
        "documentation-syntax-tags.html", states=states)
363
364

@app.route("/documentation/configuration")
365
@back.anchor
366
367
368
369
370
371
@login_required
def configuration_documentation():
    return render_template(
        "documentation-configuration.html")

@app.route("/documentation/configuration/types")
372
@back.anchor
373
374
375
376
377
378
@login_required
def types_configuration_documentation():
    return render_template(
        "documentation-configuration-types.html")

@app.route("/documentation/configuration/todomails")
379
@back.anchor
380
381
382
383
@login_required
def todomails_configuration_documentation():
    return render_template(
        "documentation-configuration-todomails.html")
Robin Sonnabend's avatar
Robin Sonnabend committed
384

385
386
387
388
@app.route("/documentation/configuration/settings")
@back.anchor
@login_required
def settings_configuration_documentation():
389
390
    user = current_user()
    return render_template(
391
        "documentation-configuration-settings.html",
392
        system_administrator=(user is not None and config.ADMIN_GROUP in user.groups))
393

Robin Sonnabend's avatar
Robin Sonnabend committed
394
@app.route("/types/list")
395
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
396
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
397
398
399
400
def list_types():
    user = current_user()
    types = [
        protocoltype for protocoltype in ProtocolType.query.all()
401
        if (protocoltype.has_private_view_right(user)
402
403
            or protocoltype.has_public_view_right(user)
            or protocoltype.is_public)]
404
    types = sorted(types, key=lambda t: t.short_name)
Robin Sonnabend's avatar
Robin Sonnabend committed
405
    types_table = ProtocolTypesTable(types)
406
407
408
    return render_template(
        "types-list.html", types=types, types_table=types_table)

Robin Sonnabend's avatar
Robin Sonnabend committed
409
410
411
412
413
414
415
416

@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:
417
418
            flash("Du kannst keinen internen Protokolltypen anlegen, "
                  "zu dem du selbst keinen Zugang hast.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
419
        else:
420
421
            protocoltype = ProtocolType()
            form.populate_obj(protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
422
423
            db.session.add(protocoltype)
            db.session.commit()
424
425
            flash("Der Protokolltyp {} wurde angelegt.".format(
                protocoltype.name), "alert-success")
426
        return back.redirect("list_types")
Robin Sonnabend's avatar
Robin Sonnabend committed
427
428
    return render_template("type-new.html", form=form)

429

Robin Sonnabend's avatar
Robin Sonnabend committed
430
@app.route("/type/edit/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
431
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
432
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
433
@require_private_view_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
434
def edit_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
435
436
437
438
    user = current_user()
    form = ProtocolTypeForm(obj=protocoltype)
    if form.validate_on_submit():
        if form.private_group.data not in user.groups:
439
440
            flash("Du kannst keinen internen Protokolltypen anlegen, "
                  "zu dem du selbst keinen Zugang hast.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
441
442
443
        else:
            form.populate_obj(protocoltype)
            db.session.commit()
444
            return back.redirect("show_type", protocoltype_id=protocoltype.id)
445
446
447
    return render_template(
        "type-edit.html", form=form, protocoltype=protocoltype)

Robin Sonnabend's avatar
Robin Sonnabend committed
448

Robin Sonnabend's avatar
Robin Sonnabend committed
449
@app.route("/type/show/<int:protocoltype_id>")
450
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
451
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
452
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
453
@require_private_view_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
454
def show_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
455
    protocoltype_table = ProtocolTypeTable(protocoltype)
456
457
458
459
    default_tops_table = DefaultTOPsTable(
        protocoltype.default_tops, protocoltype)
    reminders_table = MeetingRemindersTable(
        protocoltype.reminders, protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
460
    metas_table = DefaultMetasTable(protocoltype.metas, protocoltype)
461
462
463
464
465
466
467
468
469
    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)

470

Robin Sonnabend's avatar
Robin Sonnabend committed
471
@app.route("/type/delete/<int:protocoltype_id>")
472
@login_required
473
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
474
@db_lookup(ProtocolType)
475
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
476
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
477
def delete_type(protocoltype):
478
    name = protocoltype.name
479
    db.session.delete(protocoltype)
480
481
    db.session.commit()
    flash("Der Protokolltype {} wurde gelöscht.".format(name), "alert-success")
482
    return back.redirect("list_types")
483

484
485
486

@app.route("/type/reminders/new/<int:protocoltype_id>",
           methods=["GET", "POST"])
487
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
488
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
489
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
490
def new_reminder(protocoltype):
491
492
    form = MeetingReminderForm()
    if form.validate_on_submit():
493
494
        meetingreminder = MeetingReminder(protocoltype_id=protocoltype.id)
        form.populate_obj(meetingreminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
495
        db.session.add(meetingreminder)
496
        db.session.commit()
497
        return back.redirect("show_type", protocoltype_id=protocoltype.id)
498
499
    return render_template(
        "reminder-new.html", form=form, protocoltype=protocoltype)
500

501
502
503

@app.route("/type/reminder/edit/<int:meetingreminder_id>",
           methods=["GET", "POST"])
504
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
505
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
506
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
507
508
def edit_reminder(meetingreminder):
    form = MeetingReminderForm(obj=meetingreminder)
509
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
510
        form.populate_obj(meetingreminder)
511
        db.session.commit()
512
513
514
515
516
        return back.redirect(
            "show_type", protocoltype_id=meetingreminder.protocoltype.id)
    return render_template(
        "reminder-edit.html", form=form, meetingreminder=meetingreminder)

517

Robin Sonnabend's avatar
Robin Sonnabend committed
518
@app.route("/type/reminder/delete/<int:meetingreminder_id>")
519
@login_required
520
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
521
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
522
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
523
524
525
def delete_reminder(meetingreminder):
    protocoltype = meetingreminder.protocoltype
    db.session.delete(meetingreminder)
526
    db.session.commit()
527
    return back.redirect("show_type", protocoltype_id=protocoltype.id)
528
529


Robin Sonnabend's avatar
Robin Sonnabend committed
530
@app.route("/type/tops/new/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
531
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
532
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
533
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
534
def new_default_top(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
535
536
    form = DefaultTopForm()
    if form.validate_on_submit():
537
538
        defaulttop = DefaultTOP(protocoltype_id=protocoltype.id)
        form.populate_obj(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
539
        db.session.add(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
540
        db.session.commit()
541
542
        for protocol in protocoltype.protocols:
            if not protocol.done:
543
544
                localtop = LocalTOP(
                    protocol_id=protocol.id,
Administrator's avatar
Administrator committed
545
                    defaulttop_id=defaulttop.id, description="")
546
547
                db.session.add(localtop)
        db.session.commit()
548
549
        flash("Der Standard-TOP {} wurde für dem Protokolltyp {} hinzugefügt."
              .format(defaulttop.name, protocoltype.name), "alert-success")
550
        return back.redirect()
551
552
    return render_template(
        "default-top-new.html", form=form, protocoltype=protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
553
554


555
556
@app.route("/type/tops/edit/<int:protocoltype_id>/<int:defaulttop_id>",
           methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
557
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
558
@db_lookup(ProtocolType, DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
559
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
560
561
def edit_default_top(protocoltype, defaulttop):
    form = DefaultTopForm(obj=defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
562
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
563
        form.populate_obj(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
564
        db.session.commit()
565
        return back.redirect("show_type", protocoltype_id=protocoltype.id)
566
567
568
569
    return render_template(
        "default-top-edit.html", form=form,
        protocoltype=protocoltype, defaulttop=defaulttop)

Robin Sonnabend's avatar
Robin Sonnabend committed
570

Robin Sonnabend's avatar
Robin Sonnabend committed
571
@app.route("/type/tops/delete/<int:defaulttop_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
572
@login_required
573
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
574
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
575
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
576
577
def delete_default_top(defaulttop):
    db.session.delete(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
578
    db.session.commit()
579
580
581
    return back.redirect(
        "show_type", protocoltype_id=defaulttop.protocoltype.id)

Robin Sonnabend's avatar
Robin Sonnabend committed
582

Robin Sonnabend's avatar
Robin Sonnabend committed
583
@app.route("/type/tops/move/<int:defaulttop_id>/<diff>/")
Robin Sonnabend's avatar
Robin Sonnabend committed
584
@login_required
585
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
586
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
587
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
588
def move_default_top(defaulttop, diff):
Robin Sonnabend's avatar
Robin Sonnabend committed
589
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
590
        defaulttop.number += int(diff)
Robin Sonnabend's avatar
Robin Sonnabend committed
591
592
593
        db.session.commit()
    except ValueError:
        flash("Die angegebene Differenz ist keine Zahl.", "alert-error")
594
595
596
    return back.redirect(
        "show_type", protocoltype_id=defaulttop.protocoltype.id)

Robin Sonnabend's avatar
Robin Sonnabend committed
597
598

@app.route("/protocols/list")
599
@back.anchor
600
601
def list_protocols():
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
602
603
    protocoltype_id = None
    try:
604
        protocoltype_id = int(request.args.get("protocoltype_id"))
Robin Sonnabend's avatar
Robin Sonnabend committed
605
606
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
607
    state_open = -1
Robin Sonnabend's avatar
Robin Sonnabend committed
608
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
609
        state_open = int(request.args.get("state_open"))
Robin Sonnabend's avatar
Robin Sonnabend committed
610
611
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
612
    search_term = request.args.get("search")
613
614
    protocoltypes = ProtocolType.get_public_protocoltypes(
        user, check_networks=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
615
    search_form = ProtocolSearchForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
616
    if protocoltype_id is not None:
617
        search_form.protocoltype_id.data = protocoltype_id
Robin Sonnabend's avatar
Robin Sonnabend committed
618
619
    if state_open is not None:
        search_form.state_open.data = state_open
Robin Sonnabend's avatar
Robin Sonnabend committed
620
621
622
    if search_term is not None:
        search_form.search.data = search_term
    protocol_query = Protocol.query
Robin Sonnabend's avatar
Robin Sonnabend committed
623
624
625
626
627
628
629
630
631
    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))
            ))
632
    protocols = [
Robin Sonnabend's avatar
Robin Sonnabend committed
633
        protocol for protocol in protocol_query.all()
634
635
        if protocol.protocoltype.has_public_view_right(
            user, check_networks=False)
636
    ]
637

Robin Sonnabend's avatar
Robin Sonnabend committed
638
639
640
641
642
643
    def _matches_search(content):
        content = content.lower()
        for search_term in search_terms:
            if search_term.lower() not in content:
                return False
        return True
644

Robin Sonnabend's avatar
Robin Sonnabend committed
645
646
647
648
649
650
651
    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
652
653
654
655
656
    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
657
658
    if state_open is not None and state_open != -1:
        protocol_done = bool(state_open)
Robin Sonnabend's avatar
Robin Sonnabend committed
659
660
        protocols = [
            protocol for protocol in protocols
661
            if (protocol.is_done() or False) == protocol_done
Robin Sonnabend's avatar
Robin Sonnabend committed
662
        ]
Robin Sonnabend's avatar
Robin Sonnabend committed
663
664
665
666
667
    if shall_search:
        protocols = [
            protocol for protocol in protocols
            if (protocol.protocoltype.has_private_view_right(user)
                and _matches_search(protocol.content_private))
668
            or (protocol.has_public_view_right(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
669
670
671
                and _matches_search(protocol.content_public))
        ]
        for protocol in protocols:
672
            content = protocol.get_visible_content(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
673
674
675
676
677
678
679
680
            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):
681
682
683
684
685
686
                    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
687
688
689
690
                    if len(index_candidates) == 0:
                        parts.append((line[last_index:], False))
                        break
                    else:
691
692
                        new_index, term = min(
                            index_candidates, key=lambda t: t[0])
Robin Sonnabend's avatar
Robin Sonnabend committed
693
694
695
696
697
698
699
700
                        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
                ]))
701
            search_results[protocol] = " …<br />\n".join(formatted_lines)
702
703
    protocols = sorted(
        protocols, key=lambda protocol: protocol.date, reverse=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
704
    page = _get_page()
Robin Sonnabend's avatar
Robin Sonnabend committed
705
706
    page_length = _get_page_length()
    page_count = int(math.ceil(len(protocols) / page_length))
Robin Sonnabend's avatar
Robin Sonnabend committed
707
708
    if page >= page_count:
        page = 0
Robin Sonnabend's avatar
Robin Sonnabend committed
709
710
    begin_index = page * page_length
    end_index = (page + 1) * page_length
711
    max_page_length_exp = get_max_page_length_exp(protocols)
Robin Sonnabend's avatar
Robin Sonnabend committed
712
    protocols = protocols[begin_index:end_index]
Robin Sonnabend's avatar
Robin Sonnabend committed
713
    protocols_table = ProtocolsTable(protocols, search_results=search_results)
714
715
716
717
718
719
720
721
    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
722
723
724
725
726

@app.route("/protocol/new", methods=["GET", "POST"])
@login_required
def new_protocol():
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
727
    protocoltypes = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
728
    form = NewProtocolForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
729
    upload_form = NewProtocolSourceUploadForm(protocoltypes)
730
    file_upload_form = NewProtocolFileUploadForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
731
    if form.validate_on_submit():
732
733
        protocoltype = ProtocolType.query.filter_by(
            id=form.protocoltype_id.data).first()
Robin Sonnabend's avatar
Robin Sonnabend committed
734
735
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
736
            return back.redirect()
737
738
        protocol = Protocol.create_new_protocol(
            protocoltype, form.date.data, form.start_time.data)
739
        return back.redirect("show_protocol", protocol_id=protocol.id)
740
    type_id = request.args.get("protocoltype_id")
Robin Sonnabend's avatar
Robin Sonnabend committed
741
742
    if type_id is not None:
        form.protocoltype.data = type_id
743
        upload_form.protocoltype.data = type_id
744
745
746
747
748
    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
749
750

@app.route("/protocol/show/<int:protocol_id>")
751
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
752
753
@db_lookup(Protocol)
def show_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
754
755
    user = current_user()
    errors_table = ErrorsTable(protocol.errors)
756
757
    if not protocol.protocoltype.has_public_view_right(
            user, check_networks=False):
758
759
760
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
        if check_login():
            return redirect(url_for("index"))
761
        return redirect(url_for("login"))
Robin Sonnabend's avatar
Robin Sonnabend committed
762
763
    visible_documents = [
        document for document in protocol.documents
764
765
766
767
        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
768
    ]
769
    documents_table = DocumentsTable(visible_documents, protocol)
770
    document_upload_form = DocumentUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
771
    source_upload_form = KnownProtocolSourceUploadForm()
772
773
    time_diff = protocol.date - datetime.now().date()
    large_time_diff = not protocol.is_done() and time_diff.days > 0
774
775
    content_html = (
        protocol.content_html_private
776
777
        if protocol.has_private_view_right(user)
        else protocol.content_html_public)
778
779
    if content_html is not None:
        content_html = Markup(content_html)
780
781
782
783
784
785
786
    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
787

Robin Sonnabend's avatar
Robin Sonnabend committed
788
789
@app.route("/protocol/delete/<int:protocol_id>")
@login_required
790
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
791
@db_lookup(Protocol)
792
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
793
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
794
def delete_protocol(protocol):
795
    name = protocol.get_short_identifier()
Robin Sonnabend's avatar
Robin Sonnabend committed
796
    protocol.delete_orphan_todos()
Robin Sonnabend's avatar
Robin Sonnabend committed
797
798
799
    db.session.delete(protocol)
    db.session.commit()
    flash("Protokoll {} ist gelöscht.".format(name), "alert-success")
800
    return back.redirect("list_protocols")
Robin Sonnabend's avatar
Robin Sonnabend committed
801
802


Robin Sonnabend's avatar
Robin Sonnabend committed
803
@app.route("/protocol/etherpull/<int:protocol_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
804
@login_required
805
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
806
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
807
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
808
def etherpull_protocol(protocol):
809
810
    if not config.ETHERPAD_ACTIVE:
        flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
811
        return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
812
    protocol.source = get_etherpad_text(protocol.get_identifier())
Robin Sonnabend's avatar
Robin Sonnabend committed
813
814
815
    db.session.commit()
    tasks.parse_protocol(protocol)
    flash("Das Protokoll wird kompiliert.", "alert-success")
816
    return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
817
818


Robin Sonnabend's avatar
Robin Sonnabend committed
819
820
@app.route("/protocol/upload/known/<int:protocol_id>", methods=["POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
821
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
822
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
823
def upload_source_to_known_protocol(protocol):
824
    form = KnownProtocolSourceUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
825
826
827
828
829
830
831
832
833
834
835
836
837
838
    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")
839
    return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
840
841
842
843
844
845


@app.route("/protocol/upload/new/", methods=["POST"])
@login_required
def upload_new_protocol():
    user = current_user()
846
    available_types = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
847
848
849
850
    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")
851
852
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
853
854
855
        file = form.source.data
        if file.filename == "":
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
856
857
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
858
        source = file.stream.read().decode("utf-8")