server.py 67.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
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

349
350
351
352
353
354
355
@app.route("/documentation/configuration/settings")
@back.anchor
@login_required
def settings_configuration_documentation():
    return render_template(
        "documentation-configuration-settings.html")

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

Robin Sonnabend's avatar
Robin Sonnabend committed
371
372
373
374
375
376
377
378

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

391

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

Robin Sonnabend's avatar
Robin Sonnabend committed
410

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

432

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

446
447
448

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

463
464
465

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

479

Robin Sonnabend's avatar
Robin Sonnabend committed
480
@app.route("/type/reminder/delete/<int:meetingreminder_id>")
481
@login_required
482
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
483
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
484
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
485
486
487
def delete_reminder(meetingreminder):
    protocoltype = meetingreminder.protocoltype
    db.session.delete(meetingreminder)
488
    db.session.commit()
489
    return back.redirect("show_type", protocoltype_id=protocoltype.id)
490
491


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


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

Robin Sonnabend's avatar
Robin Sonnabend committed
532

Robin Sonnabend's avatar
Robin Sonnabend committed
533
@app.route("/type/tops/delete/<int:defaulttop_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
534
@login_required
535
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
536
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
537
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
538
539
def delete_default_top(defaulttop):
    db.session.delete(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
540
    db.session.commit()
541
542
543
    return back.redirect(
        "show_type", protocoltype_id=defaulttop.protocoltype.id)

Robin Sonnabend's avatar
Robin Sonnabend committed
544

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

Robin Sonnabend's avatar
Robin Sonnabend committed
559
560

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

Robin Sonnabend's avatar
Robin Sonnabend committed
600
601
602
603
604
605
    def _matches_search(content):
        content = content.lower()
        for search_term in search_terms:
            if search_term.lower() not in content:
                return False
        return True
606

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

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

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

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


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


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


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

837

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

883

Administrator's avatar
Administrator committed
884
885
@app.route("/protocol/recompile/<int:protocol_id>")
@login_required
886
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
887
@db_lookup(Protocol)
888
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
889
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
890
def recompile_protocol(protocol):
Administrator's avatar
Administrator committed
891
    tasks.parse_protocol(protocol)
892
    return back.redirect("show_protocol", protocol_id=protocol.id)
893
894


Robin Sonnabend's avatar
Robin Sonnabend committed
895
896
@app.route("/protocol/source/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
897
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
898
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
899
def get_protocol_source(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
900
    file_like = BytesIO(protocol.source.encode("utf-8"))
901
902
903
904
    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
905

Robin Sonnabend's avatar
Robin Sonnabend committed
906
907
@app.route("/protocol/template/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
908
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
909
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
910
def get_protocol_template(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
911
    file_like = BytesIO(protocol.get_template().encode("utf-8"))
912
913
914
915
916
    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
917

Robin Sonnabend's avatar
Robin Sonnabend committed
918
919
@app.route("/protocol/etherpush/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar