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

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

68
69
70
71
72
73
74
75
76
77
78
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
79
except ImportError:
80
81
82
83
    print("Raven not installed. Not sending issues to Sentry.")
except AttributeError:
    print("DSN not configured. Not sending issues to Sentry.")

84

85
86
87
def make_celery(app, config):
    celery = Celery(app.import_name, broker=config.CELERY_BROKER_URL)
    celery.conf.update(app.config)
88
89
90
91
92
93
94
    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
95
    except ImportError:
96
97
98
        print("Raven not installed. Not sending celery issues to Sentry.")
    except AttributeError:
        print("DSN not configured. Not sending celery issues to Sentry.")
99
100
    return celery

101
102

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

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

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

127

128
129
import tasks

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

139

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

150

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

158

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

181

182
183
184
185
186
187
188
189
190
191
192
193
194
@manager.command
def check_config():
    #  TODO: check how to set return codes
    import configproxy
    return configproxy.check_config()


@manager.command
def create_example_config(filename):
    import configproxy
    return configproxy.write_example_config(filename=filename)


195
196
@manager.command
def runserver():
197
    app.run()
198
199
    make_scheduler()

200

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

219

220
@app.route("/")
221
@back.anchor
222
def index():
Robin Sonnabend's avatar
Robin Sonnabend committed
223
224
225
    user = current_user()
    protocols = [
        protocol for protocol in Protocol.query.all()
226
227
        if protocol.protocoltype.has_public_view_right(
            user, check_networks=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
228
    ]
229

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

279
280
        def _todo_sort_key(todo):
            protocol = todo.get_first_protocol()
281
282
283
            if protocol is not None and protocol.date is not None:
                return protocol.date
            return datetime.now().date()
284
        todos = sorted(todos, key=_todo_sort_key, reverse=True)
285
286
287
288
289
    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)

290

Robin Sonnabend's avatar
Robin Sonnabend committed
291
@app.route("/documentation")
292
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
293
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
294
def documentation():
Robin Sonnabend's avatar
Robin Sonnabend committed
295
296
    todostates = list(TodoState)
    name_to_state = TodoState.get_name_to_state()
297
298
299
300
    return render_template(
        "documentation.html", todostates=todostates,
        name_to_state=name_to_state)

Robin Sonnabend's avatar
Robin Sonnabend committed
301

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

Robin Sonnabend's avatar
Robin Sonnabend committed
317
318
319
320
321
322
323
324

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

337

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

Robin Sonnabend's avatar
Robin Sonnabend committed
356

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

378

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

392
393
394

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

409
410
411

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

425

Robin Sonnabend's avatar
Robin Sonnabend committed
426
@app.route("/type/reminder/delete/<int:meetingreminder_id>")
427
@login_required
428
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
429
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
430
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
431
432
433
def delete_reminder(meetingreminder):
    protocoltype = meetingreminder.protocoltype
    db.session.delete(meetingreminder)
434
    db.session.commit()
435
    return back.redirect("show_type", protocoltype_id=protocoltype.id)
436

437

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

Robin Sonnabend's avatar
Robin Sonnabend committed
462

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

Robin Sonnabend's avatar
Robin Sonnabend committed
478

Robin Sonnabend's avatar
Robin Sonnabend committed
479
@app.route("/type/tops/delete/<int:defaulttop_id>")
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
485
def delete_default_top(defaulttop):
    db.session.delete(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
486
    db.session.commit()
487
488
489
    return back.redirect(
        "show_type", protocoltype_id=defaulttop.protocoltype.id)

Robin Sonnabend's avatar
Robin Sonnabend committed
490

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

Robin Sonnabend's avatar
Robin Sonnabend committed
505
506

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

Robin Sonnabend's avatar
Robin Sonnabend committed
546
547
548
549
550
551
    def _matches_search(content):
        content = content.lower()
        for search_term in search_terms:
            if search_term.lower() not in content:
                return False
        return True
552

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

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

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

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

710

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

726

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

749

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

783

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

829

Administrator's avatar
Administrator committed
830
831
@app.route("/protocol/recompile/<int:protocol_id>")
@login_required
832
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
833
@db_lookup(Protocol)
834
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
835
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
836
def recompile_protocol(protocol):
Administrator's avatar
Administrator committed
837
    tasks.parse_protocol(protocol)
838
    return back.redirect("show_protocol", protocol_id=protocol.id)
839

840

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

Robin Sonnabend's avatar
Robin Sonnabend committed
852
853
@app.route("/protocol/template/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
854
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
855
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
856
def get_protocol_template(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
857
    file_like = BytesIO(protocol.get_template().encode("utf-8"))
858
859
860
861
862
    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
863

Robin Sonnabend's avatar
Robin Sonnabend committed
864
865
@app.route("/protocol/etherpush/<int:protocol_id>")
@login_required
866
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
867
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
868
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
869
def etherpush_protocol(protocol):
870
871
    if not config.ETHERPAD_ACTIVE:
        flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
872
        return back.redirect("show_protocol", protocol_id=protocol.id)
873
874
    if not protocol.is_done():
        tasks.set_etherpad_content(protocol)
875
    return redirect(protocol.get_etherpad_link())
Robin Sonnabend's avatar
Robin Sonnabend committed
876

877

Robin Sonnabend's avatar
Robin Sonnabend committed
878
@app.route("/protocol/update/<int:protocol_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
879
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
880
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
881
@require_publish_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
882
def update_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
883
    upload_form = KnownProtocolSourceUploadForm()
884
    edit_form = generate_protocol_form(protocol)(obj=protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
885
886
    if edit_form.validate_on_submit():
        edit_form.populate_obj(protocol)
887
888
        for meta in protocol.metas:
            meta.value = getattr(edit_form.metas, meta.name).data
Robin Sonnabend's avatar
Robin Sonnabend committed
889
        db.session.commit()
Robin Sonnabend's avatar