server.py 65.7 KB
Newer Older
1 2 3 4
#!/usr/bin/env python3
import locale
locale.setlocale(locale.LC_TIME, "de_DE.utf8")

5 6 7
from flask import (
    Flask, request, session, flash, redirect,
    url_for, abort, render_template, Response, Markup)
8
import click
9
from werkzeug.utils import secure_filename
10
from flask_migrate import Migrate
11
from celery import Celery
12
from sqlalchemy import or_
13 14
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
Robin Sonnabend's avatar
Robin Sonnabend committed
15
import atexit
16
import feedgen.feed
17
import icalendar
18
from io import BytesIO
19
import os
20
from datetime import datetime, timedelta
Robin Sonnabend's avatar
Robin Sonnabend committed
21
import math
22
import mimetypes
23

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

60 61 62 63 64 65

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

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

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

82

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

99 100

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

app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
app.jinja_env.filters["datify"] = date_filter
app.jinja_env.filters["datetimify"] = datetime_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
106
app.jinja_env.filters["timify"] = time_filter
107
app.jinja_env.filters["timify_short"] = time_filter_short
Robin Sonnabend's avatar
Robin Sonnabend committed
108
app.jinja_env.filters["datify_short"] = date_filter_short
Robin Sonnabend's avatar
Robin Sonnabend committed
109
app.jinja_env.filters["datify_long"] = date_filter_long
110
app.jinja_env.filters["class"] = class_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
111 112
app.jinja_env.filters["todo_get_name"] = todostate_name_filter
app.jinja_env.filters["code"] = code_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
113
app.jinja_env.filters["indent_tab"] = indent_tab_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
114
app.jinja_env.filters["fancy_join"] = fancy_join
Robin Sonnabend's avatar
Robin Sonnabend committed
115
app.jinja_env.filters["footnote_hash"] = footnote_hash
116
app.jinja_env.tests["auth_valid"] = security_manager.check_user
Robin Sonnabend's avatar
Robin Sonnabend committed
117
app.jinja_env.tests["needs_date"] = needs_date_test
118
app.jinja_env.globals["get_csrf_token"] = get_csrf_token
119

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

125

126 127
import tasks

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

137

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

148

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

156

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

179

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


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


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

212

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

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

272 273
        def _todo_sort_key(todo):
            protocol = todo.get_first_protocol()
274 275 276
            if protocol is not None and protocol.date is not None:
                return protocol.date
            return datetime.now().date()
277
        todos = sorted(todos, key=_todo_sort_key, reverse=True)
278 279 280 281 282
    return render_template(
        "index.html", open_protocols=open_protocols,
        protocol=protocol, todos=todos, show_private=show_private,
        has_public_view_right=has_public_view_right)

283

Robin Sonnabend's avatar
Robin Sonnabend committed
284
@app.route("/documentation")
285
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
286
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
287
def documentation():
Robin Sonnabend's avatar
Robin Sonnabend committed
288 289
    todostates = list(TodoState)
    name_to_state = TodoState.get_name_to_state()
290 291 292 293
    return render_template(
        "documentation.html", todostates=todostates,
        name_to_state=name_to_state)

Robin Sonnabend's avatar
Robin Sonnabend committed
294

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

Robin Sonnabend's avatar
Robin Sonnabend committed
310 311 312 313 314 315 316 317

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

330

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

Robin Sonnabend's avatar
Robin Sonnabend committed
349

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

371

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

385 386 387

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

402 403 404

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

418

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

430

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

Robin Sonnabend's avatar
Robin Sonnabend committed
455

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

Robin Sonnabend's avatar
Robin Sonnabend committed
471

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

Robin Sonnabend's avatar
Robin Sonnabend committed
483

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

Robin Sonnabend's avatar
Robin Sonnabend committed
498 499

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

Robin Sonnabend's avatar
Robin Sonnabend committed
539 540 541 542 543 544
    def _matches_search(content):
        content = content.lower()
        for search_term in search_terms:
            if search_term.lower() not in content:
                return False
        return True
545

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

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

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

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

703

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

719

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

742

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

776

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

822

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

833