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

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

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

83

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

100
101

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

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

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

126

127
128
import tasks

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

138

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

149

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

157

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

180

181
182
@manager.command
def runserver():
183
    app.run()
184
185
    make_scheduler()

186

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

205

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

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

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

276

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

Robin Sonnabend's avatar
Robin Sonnabend committed
287

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

Robin Sonnabend's avatar
Robin Sonnabend committed
303
304
305
306
307
308
309
310

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

323

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

Robin Sonnabend's avatar
Robin Sonnabend committed
342

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

364

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

378
379
380

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

395
396
397

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

411

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

423

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

Robin Sonnabend's avatar
Robin Sonnabend committed
448

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

Robin Sonnabend's avatar
Robin Sonnabend committed
464

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

Robin Sonnabend's avatar
Robin Sonnabend committed
476

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

Robin Sonnabend's avatar
Robin Sonnabend committed
491
492

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

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

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

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

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

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

696

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

712

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

735

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

769

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

815

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

826

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

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

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