server.py 65.6 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_current_ip)
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from decorators import (
    require_private_view_right, require_modify_right, require_publish_right,
    require_admin_right)
from models.database import (
    ProtocolType, Protocol, DefaultTOP, TOP, LocalTOP,
    Document, Todo, Decision, MeetingReminder, Error, TodoMail,
    DecisionDocument, TodoState, DefaultMeta, DecisionCategory, Like)
from views.forms import (
    LoginForm, ProtocolTypeForm, DefaultTopForm,
    MeetingReminderForm, NewProtocolForm, DocumentUploadForm,
    KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm,
    generate_protocol_form, TopForm, LocalTopForm,
    DecisionSearchForm, ProtocolSearchForm, TodoSearchForm,
    NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm,
    DefaultMetaForm, MergeTodosForm, DecisionCategoryForm,
    DocumentEditForm)
from views.tables import (
    ProtocolsTable, ProtocolTypesTable,
    ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable,
    TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable,
    TodoMailsTable, DefaultMetasTable, DecisionCategoriesTable)
56
from legacy import import_old_todos, import_old_protocols, import_old_todomails
Robin Sonnabend's avatar
Robin Sonnabend committed
57
from common import back
58
59
60
from common.csrf import protect_csrf, get_csrf_token
from common.database import db_lookup

61
62
63
64
65
66
67
68

app = Flask(__name__)
app.config.from_object(config)
db.init_app(app)
migrate = Migrate(app, db)
manager = Manager(app)
manager.add_command("db", MigrateCommand)

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

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

85

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

102
103

celery = make_celery(app, config)
104
105
106
107
108

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

123
124
125
126
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)
127

128

129
130
import tasks

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

140

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

151

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

159

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

182

183
184
@manager.command
def runserver():
185
    app.run()
186
187
    make_scheduler()

188

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

207

208
@app.route("/")
209
@back.anchor
210
def index():
Robin Sonnabend's avatar
Robin Sonnabend committed
211
212
213
    user = current_user()
    protocols = [
        protocol for protocol in Protocol.query.all()
214
215
        if protocol.protocoltype.has_public_view_right(
            user, check_networks=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
216
    ]
217

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

267
268
        def _todo_sort_key(todo):
            protocol = todo.get_first_protocol()
269
270
271
            if protocol is not None and protocol.date is not None:
                return protocol.date
            return datetime.now().date()
272
        todos = sorted(todos, key=_todo_sort_key, reverse=True)
273
274
275
276
277
    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)

278

Robin Sonnabend's avatar
Robin Sonnabend committed
279
@app.route("/documentation")
280
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
281
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
282
def documentation():
Robin Sonnabend's avatar
Robin Sonnabend committed
283
284
    todostates = list(TodoState)
    name_to_state = TodoState.get_name_to_state()
285
286
287
288
    return render_template(
        "documentation.html", todostates=todostates,
        name_to_state=name_to_state)

Robin Sonnabend's avatar
Robin Sonnabend committed
289

Robin Sonnabend's avatar
Robin Sonnabend committed
290
@app.route("/types/list")
291
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
292
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
293
294
295
296
def list_types():
    user = current_user()
    types = [
        protocoltype for protocoltype in ProtocolType.query.all()
297
        if (protocoltype.has_private_view_right(user)
298
299
            or protocoltype.has_public_view_right(user)
            or protocoltype.is_public)]
300
    types = sorted(types, key=lambda t: t.short_name)
Robin Sonnabend's avatar
Robin Sonnabend committed
301
    types_table = ProtocolTypesTable(types)
302
303
304
    return render_template(
        "types-list.html", types=types, types_table=types_table)

Robin Sonnabend's avatar
Robin Sonnabend committed
305
306
307
308
309
310
311
312

@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:
313
314
            flash("Du kannst keinen internen Protokolltypen anlegen, "
                  "zu dem du selbst keinen Zugang hast.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
315
        else:
316
317
            protocoltype = ProtocolType()
            form.populate_obj(protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
318
319
            db.session.add(protocoltype)
            db.session.commit()
320
321
            flash("Der Protokolltyp {} wurde angelegt.".format(
                protocoltype.name), "alert-success")
322
        return back.redirect("list_types")
Robin Sonnabend's avatar
Robin Sonnabend committed
323
324
    return render_template("type-new.html", form=form)

325

Robin Sonnabend's avatar
Robin Sonnabend committed
326
@app.route("/type/edit/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
327
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
328
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
329
@require_private_view_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
330
def edit_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
331
332
333
334
    user = current_user()
    form = ProtocolTypeForm(obj=protocoltype)
    if form.validate_on_submit():
        if form.private_group.data not in user.groups:
335
336
            flash("Du kannst keinen internen Protokolltypen anlegen, "
                  "zu dem du selbst keinen Zugang hast.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
337
338
339
        else:
            form.populate_obj(protocoltype)
            db.session.commit()
340
            return back.redirect("show_type", protocoltype_id=protocoltype.id)
341
342
343
    return render_template(
        "type-edit.html", form=form, protocoltype=protocoltype)

Robin Sonnabend's avatar
Robin Sonnabend committed
344

Robin Sonnabend's avatar
Robin Sonnabend committed
345
@app.route("/type/show/<int:protocoltype_id>")
346
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
347
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
348
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
349
@require_private_view_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
350
def show_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
351
    protocoltype_table = ProtocolTypeTable(protocoltype)
352
353
354
355
    default_tops_table = DefaultTOPsTable(
        protocoltype.default_tops, protocoltype)
    reminders_table = MeetingRemindersTable(
        protocoltype.reminders, protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
356
    metas_table = DefaultMetasTable(protocoltype.metas, protocoltype)
357
358
359
360
361
362
363
364
365
    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)

366

Robin Sonnabend's avatar
Robin Sonnabend committed
367
@app.route("/type/delete/<int:protocoltype_id>")
368
@login_required
369
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
370
@db_lookup(ProtocolType)
371
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
372
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
373
def delete_type(protocoltype):
374
    name = protocoltype.name
375
    db.session.delete(protocoltype)
376
377
    db.session.commit()
    flash("Der Protokolltype {} wurde gelöscht.".format(name), "alert-success")
378
    return back.redirect("list_types")
379

380
381
382

@app.route("/type/reminders/new/<int:protocoltype_id>",
           methods=["GET", "POST"])
383
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
384
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
385
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
386
def new_reminder(protocoltype):
387
388
    form = MeetingReminderForm()
    if form.validate_on_submit():
389
390
        meetingreminder = MeetingReminder(protocoltype_id=protocoltype.id)
        form.populate_obj(meetingreminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
391
        db.session.add(meetingreminder)
392
        db.session.commit()
393
        return back.redirect("show_type", protocoltype_id=protocoltype.id)
394
395
    return render_template(
        "reminder-new.html", form=form, protocoltype=protocoltype)
396

397
398
399

@app.route("/type/reminder/edit/<int:meetingreminder_id>",
           methods=["GET", "POST"])
400
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
401
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
402
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
403
404
def edit_reminder(meetingreminder):
    form = MeetingReminderForm(obj=meetingreminder)
405
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
406
        form.populate_obj(meetingreminder)
407
        db.session.commit()
408
409
410
411
412
        return back.redirect(
            "show_type", protocoltype_id=meetingreminder.protocoltype.id)
    return render_template(
        "reminder-edit.html", form=form, meetingreminder=meetingreminder)

413

Robin Sonnabend's avatar
Robin Sonnabend committed
414
@app.route("/type/reminder/delete/<int:meetingreminder_id>")
415
@login_required
416
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
417
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
418
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
419
420
421
def delete_reminder(meetingreminder):
    protocoltype = meetingreminder.protocoltype
    db.session.delete(meetingreminder)
422
    db.session.commit()
423
    return back.redirect("show_type", protocoltype_id=protocoltype.id)
424

425

Robin Sonnabend's avatar
Robin Sonnabend committed
426
@app.route("/type/tops/new/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
427
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
428
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
429
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
430
def new_default_top(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
431
432
    form = DefaultTopForm()
    if form.validate_on_submit():
433
434
        defaulttop = DefaultTOP(protocoltype_id=protocoltype.id)
        form.populate_obj(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
435
        db.session.add(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
436
        db.session.commit()
437
438
        for protocol in protocoltype.protocols:
            if not protocol.done:
439
440
                localtop = LocalTOP(
                    protocol_id=protocol.id,
Administrator's avatar
Administrator committed
441
                    defaulttop_id=defaulttop.id, description="")
442
443
                db.session.add(localtop)
        db.session.commit()
444
445
        flash("Der Standard-TOP {} wurde für dem Protokolltyp {} hinzugefügt."
              .format(defaulttop.name, protocoltype.name), "alert-success")
446
        return back.redirect()
447
448
449
    return render_template(
        "default-top-new.html", form=form, protocoltype=protocoltype)

Robin Sonnabend's avatar
Robin Sonnabend committed
450

451
452
@app.route("/type/tops/edit/<int:protocoltype_id>/<int:defaulttop_id>",
           methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
453
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
454
@db_lookup(ProtocolType, DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
455
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
456
457
def edit_default_top(protocoltype, defaulttop):
    form = DefaultTopForm(obj=defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
458
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
459
        form.populate_obj(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
460
        db.session.commit()
461
        return back.redirect("show_type", protocoltype_id=protocoltype.id)
462
463
464
465
    return render_template(
        "default-top-edit.html", form=form,
        protocoltype=protocoltype, defaulttop=defaulttop)

Robin Sonnabend's avatar
Robin Sonnabend committed
466

Robin Sonnabend's avatar
Robin Sonnabend committed
467
@app.route("/type/tops/delete/<int:defaulttop_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
468
@login_required
469
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
470
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
471
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
472
473
def delete_default_top(defaulttop):
    db.session.delete(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
474
    db.session.commit()
475
476
477
    return back.redirect(
        "show_type", protocoltype_id=defaulttop.protocoltype.id)

Robin Sonnabend's avatar
Robin Sonnabend committed
478

Robin Sonnabend's avatar
Robin Sonnabend committed
479
@app.route("/type/tops/move/<int:defaulttop_id>/<diff>/")
Robin Sonnabend's avatar
Robin Sonnabend committed
480
@login_required
481
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
482
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
483
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
484
def move_default_top(defaulttop, diff):
Robin Sonnabend's avatar
Robin Sonnabend committed
485
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
486
        defaulttop.number += int(diff)
Robin Sonnabend's avatar
Robin Sonnabend committed
487
488
489
        db.session.commit()
    except ValueError:
        flash("Die angegebene Differenz ist keine Zahl.", "alert-error")
490
491
492
    return back.redirect(
        "show_type", protocoltype_id=defaulttop.protocoltype.id)

Robin Sonnabend's avatar
Robin Sonnabend committed
493
494

@app.route("/protocols/list")
495
@back.anchor
496
497
def list_protocols():
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
498
499
    protocoltype_id = None
    try:
500
        protocoltype_id = int(request.args.get("protocoltype_id"))
Robin Sonnabend's avatar
Robin Sonnabend committed
501
502
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
503
    state_open = -1
Robin Sonnabend's avatar
Robin Sonnabend committed
504
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
505
        state_open = int(request.args.get("state_open"))
Robin Sonnabend's avatar
Robin Sonnabend committed
506
507
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
508
    search_term = request.args.get("search")
509
510
    protocoltypes = ProtocolType.get_public_protocoltypes(
        user, check_networks=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
511
    search_form = ProtocolSearchForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
512
    if protocoltype_id is not None:
513
        search_form.protocoltype_id.data = protocoltype_id
Robin Sonnabend's avatar
Robin Sonnabend committed
514
515
    if state_open is not None:
        search_form.state_open.data = state_open
Robin Sonnabend's avatar
Robin Sonnabend committed
516
517
518
    if search_term is not None:
        search_form.search.data = search_term
    protocol_query = Protocol.query
Robin Sonnabend's avatar
Robin Sonnabend committed
519
520
521
522
523
524
525
526
527
    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))
            ))
528
    protocols = [
Robin Sonnabend's avatar
Robin Sonnabend committed
529
        protocol for protocol in protocol_query.all()
530
531
        if protocol.protocoltype.has_public_view_right(
            user, check_networks=False)
532
    ]
533

Robin Sonnabend's avatar
Robin Sonnabend committed
534
535
536
537
538
539
    def _matches_search(content):
        content = content.lower()
        for search_term in search_terms:
            if search_term.lower() not in content:
                return False
        return True
540

Robin Sonnabend's avatar
Robin Sonnabend committed
541
542
543
544
545
546
547
    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
548
549
550
551
552
    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
553
554
    if state_open is not None and state_open != -1:
        protocol_done = bool(state_open)
Robin Sonnabend's avatar
Robin Sonnabend committed
555
556
        protocols = [
            protocol for protocol in protocols
557
            if (protocol.is_done() or False) == protocol_done
Robin Sonnabend's avatar
Robin Sonnabend committed
558
        ]
Robin Sonnabend's avatar
Robin Sonnabend committed
559
560
561
562
563
    if shall_search:
        protocols = [
            protocol for protocol in protocols
            if (protocol.protocoltype.has_private_view_right(user)
                and _matches_search(protocol.content_private))
564
            or (protocol.has_public_view_right(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
565
566
567
                and _matches_search(protocol.content_public))
        ]
        for protocol in protocols:
568
            content = protocol.get_visible_content(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
569
570
571
572
573
574
575
576
            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):
577
578
579
580
581
582
                    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
583
584
585
586
                    if len(index_candidates) == 0:
                        parts.append((line[last_index:], False))
                        break
                    else:
587
588
                        new_index, term = min(
                            index_candidates, key=lambda t: t[0])
Robin Sonnabend's avatar
Robin Sonnabend committed
589
590
591
592
593
594
595
596
                        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
                ]))
597
            search_results[protocol] = " …<br />\n".join(formatted_lines)
598
599
    protocols = sorted(
        protocols, key=lambda protocol: protocol.date, reverse=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
600
    page = _get_page()
Robin Sonnabend's avatar
Robin Sonnabend committed
601
602
    page_length = _get_page_length()
    page_count = int(math.ceil(len(protocols) / page_length))
Robin Sonnabend's avatar
Robin Sonnabend committed
603
604
    if page >= page_count:
        page = 0
Robin Sonnabend's avatar
Robin Sonnabend committed
605
606
    begin_index = page * page_length
    end_index = (page + 1) * page_length
607
    max_page_length_exp = get_max_page_length_exp(protocols)
Robin Sonnabend's avatar
Robin Sonnabend committed
608
    protocols = protocols[begin_index:end_index]
Robin Sonnabend's avatar
Robin Sonnabend committed
609
    protocols_table = ProtocolsTable(protocols, search_results=search_results)
610
611
612
613
614
615
616
617
    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
618
619
620
621
622

@app.route("/protocol/new", methods=["GET", "POST"])
@login_required
def new_protocol():
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
623
    protocoltypes = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
624
    form = NewProtocolForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
625
    upload_form = NewProtocolSourceUploadForm(protocoltypes)
626
    file_upload_form = NewProtocolFileUploadForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
627
    if form.validate_on_submit():
628
629
        protocoltype = ProtocolType.query.filter_by(
            id=form.protocoltype_id.data).first()
Robin Sonnabend's avatar
Robin Sonnabend committed
630
631
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
632
            return back.redirect()
633
634
        protocol = Protocol.create_new_protocol(
            protocoltype, form.date.data, form.start_time.data)
635
        return back.redirect("show_protocol", protocol_id=protocol.id)
636
    type_id = request.args.get("protocoltype_id")
Robin Sonnabend's avatar
Robin Sonnabend committed
637
638
    if type_id is not None:
        form.protocoltype.data = type_id
639
        upload_form.protocoltype.data = type_id
640
641
642
643
644
    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
645
646

@app.route("/protocol/show/<int:protocol_id>")
647
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
648
649
@db_lookup(Protocol)
def show_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
650
651
    user = current_user()
    errors_table = ErrorsTable(protocol.errors)
652
653
    if not protocol.protocoltype.has_public_view_right(
            user, check_networks=False):
654
655
656
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
        if check_login():
            return redirect(url_for("index"))
657
        return redirect(url_for("login"))
Robin Sonnabend's avatar
Robin Sonnabend committed
658
659
    visible_documents = [
        document for document in protocol.documents
660
661
662
663
        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
664
    ]
665
    documents_table = DocumentsTable(visible_documents, protocol)
666
    document_upload_form = DocumentUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
667
    source_upload_form = KnownProtocolSourceUploadForm()
668
669
    time_diff = protocol.date - datetime.now().date()
    large_time_diff = not protocol.is_done() and time_diff.days > 0
670
671
    content_html = (
        protocol.content_html_private
672
673
        if protocol.has_private_view_right(user)
        else protocol.content_html_public)
674
675
    if content_html is not None:
        content_html = Markup(content_html)
676
677
678
679
680
681
682
    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
683

Robin Sonnabend's avatar
Robin Sonnabend committed
684
685
@app.route("/protocol/delete/<int:protocol_id>")
@login_required
686
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
687
@db_lookup(Protocol)
688
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
689
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
690
def delete_protocol(protocol):
691
    name = protocol.get_short_identifier()
Robin Sonnabend's avatar
Robin Sonnabend committed
692
    protocol.delete_orphan_todos()
Robin Sonnabend's avatar
Robin Sonnabend committed
693
694
695
    db.session.delete(protocol)
    db.session.commit()
    flash("Protokoll {} ist gelöscht.".format(name), "alert-success")
696
    return back.redirect("list_protocols")
Robin Sonnabend's avatar
Robin Sonnabend committed
697

698

Robin Sonnabend's avatar
Robin Sonnabend committed
699
@app.route("/protocol/etherpull/<int:protocol_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
700
@login_required
701
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
702
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
703
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
704
def etherpull_protocol(protocol):
705
706
    if not config.ETHERPAD_ACTIVE:
        flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
707
        return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
708
    protocol.source = get_etherpad_text(protocol.get_identifier())
Robin Sonnabend's avatar
Robin Sonnabend committed
709
710
711
    db.session.commit()
    tasks.parse_protocol(protocol)
    flash("Das Protokoll wird kompiliert.", "alert-success")
712
    return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
713

714

Robin Sonnabend's avatar
Robin Sonnabend committed
715
716
@app.route("/protocol/upload/known/<int:protocol_id>", methods=["POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
717
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
718
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
719
def upload_source_to_known_protocol(protocol):
720
    form = KnownProtocolSourceUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
721
722
723
724
725
726
727
728
729
730
731
732
733
734
    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")
735
    return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
736

737

Robin Sonnabend's avatar
Robin Sonnabend committed
738
739
740
741
@app.route("/protocol/upload/new/", methods=["POST"])
@login_required
def upload_new_protocol():
    user = current_user()
742
    available_types = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
743
744
745
746
    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")
747
748
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
749
750
751
        file = form.source.data
        if file.filename == "":
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
752
753
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
754
        source = file.stream.read().decode("utf-8")
755
756
        protocoltype = ProtocolType.query.filter_by(
            id=form.protocoltype_id.data).first()
757
758
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Invalider Protokolltyp oder keine Rechte.", "alert-error")
759
760
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
761
        protocol = Protocol(protocoltype_id=protocoltype.id, source=source)
762
763
        db.session.add(protocol)
        db.session.commit()
764
        for local_top in protocol.create_localtops():
765
766
            db.session.add(local_top)
        db.session.commit()
767
        tasks.parse_protocol(protocol)
768
        return back.redirect("show_protocol", protocol_id=protocol.id)
769
770
    return redirect(request.args.get("fail") or url_for("new_protocol"))

771

772
773
774
775
776
777
778
779
780
@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")
781
782
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
783
784
785
        file = form.file.data
        if file.filename == "":
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
786
787
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
788
        filename = secure_filename(file.filename)
789
790
        protocoltype = ProtocolType.query.filter_by(
            id=form.protocoltype_id.data).first()
791
792
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Invalider Protokolltyp oder keine Rechte.", "alert-error")
793
794
795
796
797
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
        protocol = Protocol(
            protocoltype_id=protocoltype.id,
            date=datetime.now().date(), done=True)
798
799
        db.session.add(protocol)
        db.session.commit()
800
        for local_top in protocol.create_localtops():
801
802
            db.session.add(local_top)
        db.session.commit()
803
804
        document = Document(
            protocol_id=protocol.id, name=filename,
805
806
            filename="", is_compiled=False)
        form.populate_obj(document)
807
808
        db.session.add(document)
        db.session.commit()
809
        internal_filename = get_internal_filename(
810
            protocol, document.id, filename)
811
812
813
        document.filename = internal_filename
        file.save(os.path.join(config.DOCUMENTS_PATH, internal_filename))
        db.session.commit()
814
        return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
815
816
    return redirect(request.args.get("fail") or url_for("new_protocol"))

817

Administrator's avatar
Administrator committed
818
819
@app.route("/protocol/recompile/<int:protocol_id>")
@login_required
820
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
821
@db_lookup(Protocol)
822
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
823
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
824
def recompile_protocol(protocol):
Administrator's avatar
Administrator committed
825
    tasks.parse_protocol(protocol)
826
    return back.redirect("show_protocol", protocol_id=protocol.id)
827

828

Robin Sonnabend's avatar
Robin Sonnabend committed
829
830
@app.route("/protocol/source/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
831
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
832
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
833
def get_protocol_source(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
834
    file_like = BytesIO(protocol.source.encode("utf-8"))
835
836
837
838
    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
839

Robin Sonnabend's avatar
Robin Sonnabend committed
840
841
@app.route("/protocol/template/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
842
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
843
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
844
def get_protocol_template(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
845
    file_like = BytesIO(protocol.get_template().encode("utf-8"))
846
847
848
849
850
    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
851

Robin Sonnabend's avatar
Robin Sonnabend committed
852
853
@app.route("/protocol/etherpush/<int:protocol_id>")
@login_required
854
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
855
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
856
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
857
def etherpush_protocol(protocol):
858
859
    if not config.ETHERPAD_ACTIVE:
        flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
860
        return back.redirect("show_protocol", protocol_id=protocol.id)
861
862
    if not protocol.is_done():
        tasks.set_etherpad_content(protocol)
863