server.py 67.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
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
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,
30
    code_filter, code_key_filter, indent_tab_filter)
31
32
33
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
87
app.jinja_env.filters["code_key"] = code_key_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
88
app.jinja_env.filters["indent_tab"] = indent_tab_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
89
app.jinja_env.filters["fancy_join"] = fancy_join
Robin Sonnabend's avatar
Robin Sonnabend committed
90
app.jinja_env.filters["footnote_hash"] = footnote_hash
91
app.jinja_env.tests["auth_valid"] = security_manager.check_user
Robin Sonnabend's avatar
Robin Sonnabend committed
92
app.jinja_env.tests["needs_date"] = needs_date_test
93
app.jinja_env.globals["get_csrf_token"] = get_csrf_token
94

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

100

101
102
import tasks

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

112

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

123

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

131

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

154

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

160

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

179

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

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

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

Robin Sonnabend's avatar
Robin Sonnabend committed
250
@app.route("/documentation")
251
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
252
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
253
def documentation():
254
255
256
257
    return render_template(
        "documentation.html")

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

392

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

Robin Sonnabend's avatar
Robin Sonnabend committed
411

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

433

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

447
448
449

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

464
465
466

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

480

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


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


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

Robin Sonnabend's avatar
Robin Sonnabend committed
533

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

Robin Sonnabend's avatar
Robin Sonnabend committed
545

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

Robin Sonnabend's avatar
Robin Sonnabend committed
560
561

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

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

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

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

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

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


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


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


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

838

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

884

Administrator's avatar
Administrator committed
885
886
@app.route("/protocol/recompile/<int:protocol_id>")
@login_required
887
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
888
@db_lookup(Protocol)
889
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
890
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
891
def recompile_protocol(protocol):