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():
323
324
325
326
327
328
	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)
329
330

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

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

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

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

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

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

393

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

Robin Sonnabend's avatar
Robin Sonnabend committed
412

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

434

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

448
449
450

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

465
466
467

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

481

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


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


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

Robin Sonnabend's avatar
Robin Sonnabend committed
534

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

Robin Sonnabend's avatar
Robin Sonnabend committed
546

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

Robin Sonnabend's avatar
Robin Sonnabend committed
561
562

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

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

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

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

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

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


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


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


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

839

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

885

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


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

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