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

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

67

68
69
70
71
72
def make_celery(app, config):
    celery = Celery(app.import_name, broker=config.CELERY_BROKER_URL)
    celery.conf.update(app.config)
    return celery

73
74

celery = make_celery(app, config)
75
76
77
78
79

app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
app.jinja_env.filters["datify"] = date_filter
app.jinja_env.filters["datetimify"] = datetime_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
80
app.jinja_env.filters["timify"] = time_filter
81
app.jinja_env.filters["timify_short"] = time_filter_short
Robin Sonnabend's avatar
Robin Sonnabend committed
82
app.jinja_env.filters["datify_short"] = date_filter_short
Robin Sonnabend's avatar
Robin Sonnabend committed
83
app.jinja_env.filters["datify_long"] = date_filter_long
84
app.jinja_env.filters["class"] = class_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
85
86
app.jinja_env.filters["todo_get_name"] = todostate_name_filter
app.jinja_env.filters["code"] = code_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
87
app.jinja_env.filters["indent_tab"] = indent_tab_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
88
app.jinja_env.filters["fancy_join"] = fancy_join
Robin Sonnabend's avatar
Robin Sonnabend committed
89
app.jinja_env.filters["footnote_hash"] = footnote_hash
90
app.jinja_env.tests["auth_valid"] = security_manager.check_user
Robin Sonnabend's avatar
Robin Sonnabend committed
91
app.jinja_env.tests["needs_date"] = needs_date_test
92
app.jinja_env.globals["get_csrf_token"] = get_csrf_token
93

94
95
96
97
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)
98

99

100
101
import tasks

102
103
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
104
app.jinja_env.globals.update(zip=zip)
Robin Sonnabend's avatar
Robin Sonnabend committed
105
106
107
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
108
app.jinja_env.globals.update(now=datetime.now)
109
app.jinja_env.globals["git_revision"] = get_git_revision()
110

111

112
113
@manager.command
def import_legacy():
114
    """Import the old todos and protocols from an sql dump"""
115
    filename = prompt("SQL-file")
116
117
    with open(filename, "rb") as sqlfile:
        content = sqlfile.read().decode("utf-8")
118
119
        import_old_todos(content)
        import_old_protocols(content)
120
        import_old_todomails(content)
121

122

123
124
125
126
@manager.command
def recompile_all():
    for protocol in sorted(Protocol.query.all(), key=lambda p: p.date):
        if protocol.is_done():
127
            print(protocol.get_short_identifier())
128
129
            tasks.parse_protocol(protocol)

130

131
@manager.command
132
def merge_duplicate_todos():
133
134
135
136
137
138
139
140
    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)
141
            if todo2.state.value > todo1.state.value:
142
143
144
145
146
147
148
149
150
151
152
                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

153

154
155
@manager.command
def runserver():
156
    app.run()
157
158
    make_scheduler()

159

160
def send_file(file_like, cache_timeout, as_attachment, attachment_filename):
161
162
163
    """
    Replaces flask.send_file since that uses an uwsgi function that is buggy.
    """
164
165
    mimetype, _ = mimetypes.guess_type(attachment_filename)
    response = Response(file_like.read(), mimetype)
166
    if as_attachment:
167
168
        response.headers["Content-Disposition"] = (
            'attachment; filename="{}"'.format(attachment_filename))
169
170
171
172
    content_type = mimetype
    if mimetype.startswith("text/"):
        content_type = "{}; charset=utf-8".format(content_type)
    response.headers["Content-Type"] = content_type
173
174
    response.headers["Cache-Control"] = (
        "public, max-age={}".format(cache_timeout))
175
    response.headers["Connection"] = "close"
176
    return response
177

178

179
@app.route("/")
180
@back.anchor
181
def index():
Robin Sonnabend's avatar
Robin Sonnabend committed
182
183
184
    user = current_user()
    protocols = [
        protocol for protocol in Protocol.query.all()
185
186
        if protocol.protocoltype.has_public_view_right(
            user, check_networks=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
187
    ]
188

189
    def _protocol_sort_key(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
190
191
192
        if protocol.date is not None:
            return protocol.date
        return datetime.now().date()
Robin Sonnabend's avatar
Robin Sonnabend committed
193
    current_day = datetime.now().date()
194
    open_protocols = sorted(
Robin Sonnabend's avatar
Robin Sonnabend committed
195
196
197
198
        [
            protocol for protocol in protocols
            if not protocol.done
            and (protocol.date - current_day).days < config.MAX_INDEX_DAYS
199
            and (current_day - protocol.date).days < config.MAX_PAST_INDEX_DAYS
Robin Sonnabend's avatar
Robin Sonnabend committed
200
        ],
201
        key=_protocol_sort_key
Robin Sonnabend's avatar
Robin Sonnabend committed
202
203
    )
    finished_protocols = sorted(
204
205
        [
            protocol for protocol in protocols
206
            if protocol.done and protocol.public
207
208
209
210
            and (
                protocol.has_private_view_right(user)
                or protocol.protocoltype.has_public_view_right(
                    user, check_networks=False))
211
        ],
212
213
        key=_protocol_sort_key,
        reverse=True
Robin Sonnabend's avatar
Robin Sonnabend committed
214
    )
215
216
217
218
219
220
    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)
221
222
        has_public_view_right = (
            protocol.protocoltype.has_public_view_right(user))
Robin Sonnabend's avatar
Robin Sonnabend committed
223
224
225
    todos = None
    if check_login():
        todos = [
226
            todo for todo in Todo.query.all()
227
            if todo.protocoltype.has_private_view_right(user)
228
            and not todo.is_done()
Robin Sonnabend's avatar
Robin Sonnabend committed
229
        ]
230
231
        user_todos = [
            todo for todo in todos
232
233
            if user.username.lower()
            in list(map(str.strip, todo.who.lower().split(",")))
234
235
236
        ]
        if len(user_todos) > 0:
            todos = user_todos
237

238
239
        def _todo_sort_key(todo):
            protocol = todo.get_first_protocol()
240
241
242
            if protocol is not None and protocol.date is not None:
                return protocol.date
            return datetime.now().date()
243
        todos = sorted(todos, key=_todo_sort_key, reverse=True)
244
245
246
247
248
    return render_template(
        "index.html", open_protocols=open_protocols,
        protocol=protocol, todos=todos, show_private=show_private,
        has_public_view_right=has_public_view_right)

249

Robin Sonnabend's avatar
Robin Sonnabend committed
250
@app.route("/documentation")
251
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
252
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
253
def documentation():
Robin Sonnabend's avatar
Robin Sonnabend committed
254
255
    todostates = list(TodoState)
    name_to_state = TodoState.get_name_to_state()
256
257
258
259
    return render_template(
        "documentation.html", todostates=todostates,
        name_to_state=name_to_state)

Robin Sonnabend's avatar
Robin Sonnabend committed
260

Robin Sonnabend's avatar
Robin Sonnabend committed
261
@app.route("/types/list")
262
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
263
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
264
265
266
267
def list_types():
    user = current_user()
    types = [
        protocoltype for protocoltype in ProtocolType.query.all()
268
        if (protocoltype.has_private_view_right(user)
269
270
            or protocoltype.has_public_view_right(user)
            or protocoltype.is_public)]
271
    types = sorted(types, key=lambda t: t.short_name)
Robin Sonnabend's avatar
Robin Sonnabend committed
272
    types_table = ProtocolTypesTable(types)
273
274
275
    return render_template(
        "types-list.html", types=types, types_table=types_table)

Robin Sonnabend's avatar
Robin Sonnabend committed
276
277
278
279
280
281
282
283

@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:
284
285
            flash("Du kannst keinen internen Protokolltypen anlegen, "
                  "zu dem du selbst keinen Zugang hast.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
286
        else:
287
288
            protocoltype = ProtocolType()
            form.populate_obj(protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
289
290
            db.session.add(protocoltype)
            db.session.commit()
291
292
            flash("Der Protokolltyp {} wurde angelegt.".format(
                protocoltype.name), "alert-success")
293
        return back.redirect("list_types")
Robin Sonnabend's avatar
Robin Sonnabend committed
294
295
    return render_template("type-new.html", form=form)

296

Robin Sonnabend's avatar
Robin Sonnabend committed
297
@app.route("/type/edit/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
298
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
299
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
300
@require_private_view_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
301
def edit_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
302
303
304
305
    user = current_user()
    form = ProtocolTypeForm(obj=protocoltype)
    if form.validate_on_submit():
        if form.private_group.data not in user.groups:
306
307
            flash("Du kannst keinen internen Protokolltypen anlegen, "
                  "zu dem du selbst keinen Zugang hast.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
308
309
310
        else:
            form.populate_obj(protocoltype)
            db.session.commit()
311
            return back.redirect("show_type", protocoltype_id=protocoltype.id)
312
313
314
    return render_template(
        "type-edit.html", form=form, protocoltype=protocoltype)

Robin Sonnabend's avatar
Robin Sonnabend committed
315

Robin Sonnabend's avatar
Robin Sonnabend committed
316
@app.route("/type/show/<int:protocoltype_id>")
317
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
318
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
319
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
320
@require_private_view_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
321
def show_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
322
    protocoltype_table = ProtocolTypeTable(protocoltype)
323
324
325
326
    default_tops_table = DefaultTOPsTable(
        protocoltype.default_tops, protocoltype)
    reminders_table = MeetingRemindersTable(
        protocoltype.reminders, protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
327
    metas_table = DefaultMetasTable(protocoltype.metas, protocoltype)
328
329
330
331
332
333
334
335
336
    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)

337

Robin Sonnabend's avatar
Robin Sonnabend committed
338
@app.route("/type/delete/<int:protocoltype_id>")
339
@login_required
340
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
341
@db_lookup(ProtocolType)
342
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
343
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
344
def delete_type(protocoltype):
345
    name = protocoltype.name
346
    db.session.delete(protocoltype)
347
348
    db.session.commit()
    flash("Der Protokolltype {} wurde gelöscht.".format(name), "alert-success")
349
    return back.redirect("list_types")
350

351
352
353

@app.route("/type/reminders/new/<int:protocoltype_id>",
           methods=["GET", "POST"])
354
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
355
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
356
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
357
def new_reminder(protocoltype):
358
359
    form = MeetingReminderForm()
    if form.validate_on_submit():
360
361
        meetingreminder = MeetingReminder(protocoltype_id=protocoltype.id)
        form.populate_obj(meetingreminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
362
        db.session.add(meetingreminder)
363
        db.session.commit()
364
        return back.redirect("show_type", protocoltype_id=protocoltype.id)
365
366
    return render_template(
        "reminder-new.html", form=form, protocoltype=protocoltype)
367

368
369
370

@app.route("/type/reminder/edit/<int:meetingreminder_id>",
           methods=["GET", "POST"])
371
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
372
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
373
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
374
375
def edit_reminder(meetingreminder):
    form = MeetingReminderForm(obj=meetingreminder)
376
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
377
        form.populate_obj(meetingreminder)
378
        db.session.commit()
379
380
381
382
383
        return back.redirect(
            "show_type", protocoltype_id=meetingreminder.protocoltype.id)
    return render_template(
        "reminder-edit.html", form=form, meetingreminder=meetingreminder)

384

Robin Sonnabend's avatar
Robin Sonnabend committed
385
@app.route("/type/reminder/delete/<int:meetingreminder_id>")
386
@login_required
387
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
388
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
389
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
390
391
392
def delete_reminder(meetingreminder):
    protocoltype = meetingreminder.protocoltype
    db.session.delete(meetingreminder)
393
    db.session.commit()
394
    return back.redirect("show_type", protocoltype_id=protocoltype.id)
395

396

Robin Sonnabend's avatar
Robin Sonnabend committed
397
@app.route("/type/tops/new/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
398
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
399
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
400
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
401
def new_default_top(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
402
403
    form = DefaultTopForm()
    if form.validate_on_submit():
404
405
        defaulttop = DefaultTOP(protocoltype_id=protocoltype.id)
        form.populate_obj(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
406
        db.session.add(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
407
        db.session.commit()
408
409
        for protocol in protocoltype.protocols:
            if not protocol.done:
410
411
                localtop = LocalTOP(
                    protocol_id=protocol.id,
Administrator's avatar
Administrator committed
412
                    defaulttop_id=defaulttop.id, description="")
413
414
                db.session.add(localtop)
        db.session.commit()
415
416
        flash("Der Standard-TOP {} wurde für dem Protokolltyp {} hinzugefügt."
              .format(defaulttop.name, protocoltype.name), "alert-success")
417
        return back.redirect()
418
419
420
    return render_template(
        "default-top-new.html", form=form, protocoltype=protocoltype)

Robin Sonnabend's avatar
Robin Sonnabend committed
421

422
423
@app.route("/type/tops/edit/<int:protocoltype_id>/<int:defaulttop_id>",
           methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
424
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
425
@db_lookup(ProtocolType, DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
426
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
427
428
def edit_default_top(protocoltype, defaulttop):
    form = DefaultTopForm(obj=defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
429
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
430
        form.populate_obj(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
431
        db.session.commit()
432
        return back.redirect("show_type", protocoltype_id=protocoltype.id)
433
434
435
436
    return render_template(
        "default-top-edit.html", form=form,
        protocoltype=protocoltype, defaulttop=defaulttop)

Robin Sonnabend's avatar
Robin Sonnabend committed
437

Robin Sonnabend's avatar
Robin Sonnabend committed
438
@app.route("/type/tops/delete/<int:defaulttop_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
439
@login_required
440
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
441
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
442
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
443
444
def delete_default_top(defaulttop):
    db.session.delete(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
445
    db.session.commit()
446
447
448
    return back.redirect(
        "show_type", protocoltype_id=defaulttop.protocoltype.id)

Robin Sonnabend's avatar
Robin Sonnabend committed
449

Robin Sonnabend's avatar
Robin Sonnabend committed
450
@app.route("/type/tops/move/<int:defaulttop_id>/<diff>/")
Robin Sonnabend's avatar
Robin Sonnabend committed
451
@login_required
452
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
453
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
454
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
455
def move_default_top(defaulttop, diff):
Robin Sonnabend's avatar
Robin Sonnabend committed
456
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
457
        defaulttop.number += int(diff)
Robin Sonnabend's avatar
Robin Sonnabend committed
458
459
460
        db.session.commit()
    except ValueError:
        flash("Die angegebene Differenz ist keine Zahl.", "alert-error")
461
462
463
    return back.redirect(
        "show_type", protocoltype_id=defaulttop.protocoltype.id)

Robin Sonnabend's avatar
Robin Sonnabend committed
464
465

@app.route("/protocols/list")
466
@back.anchor
467
468
def list_protocols():
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
469
470
    protocoltype_id = None
    try:
471
        protocoltype_id = int(request.args.get("protocoltype_id"))
Robin Sonnabend's avatar
Robin Sonnabend committed
472
473
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
474
    state_open = -1
Robin Sonnabend's avatar
Robin Sonnabend committed
475
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
476
        state_open = int(request.args.get("state_open"))
Robin Sonnabend's avatar
Robin Sonnabend committed
477
478
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
479
    search_term = request.args.get("search")
480
481
    protocoltypes = ProtocolType.get_public_protocoltypes(
        user, check_networks=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
482
    search_form = ProtocolSearchForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
483
    if protocoltype_id is not None:
484
        search_form.protocoltype_id.data = protocoltype_id
Robin Sonnabend's avatar
Robin Sonnabend committed
485
486
    if state_open is not None:
        search_form.state_open.data = state_open
Robin Sonnabend's avatar
Robin Sonnabend committed
487
488
489
    if search_term is not None:
        search_form.search.data = search_term
    protocol_query = Protocol.query
Robin Sonnabend's avatar
Robin Sonnabend committed
490
491
492
493
494
495
496
497
498
    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))
            ))
499
    protocols = [
Robin Sonnabend's avatar
Robin Sonnabend committed
500
        protocol for protocol in protocol_query.all()
501
502
        if protocol.protocoltype.has_public_view_right(
            user, check_networks=False)
503
    ]
504

Robin Sonnabend's avatar
Robin Sonnabend committed
505
506
507
508
509
510
    def _matches_search(content):
        content = content.lower()
        for search_term in search_terms:
            if search_term.lower() not in content:
                return False
        return True
511

Robin Sonnabend's avatar
Robin Sonnabend committed
512
513
514
515
516
517
518
    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
519
520
521
522
523
    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
524
525
    if state_open is not None and state_open != -1:
        protocol_done = bool(state_open)
Robin Sonnabend's avatar
Robin Sonnabend committed
526
527
        protocols = [
            protocol for protocol in protocols
528
            if (protocol.is_done() or False) == protocol_done
Robin Sonnabend's avatar
Robin Sonnabend committed
529
        ]
Robin Sonnabend's avatar
Robin Sonnabend committed
530
531
532
533
534
    if shall_search:
        protocols = [
            protocol for protocol in protocols
            if (protocol.protocoltype.has_private_view_right(user)
                and _matches_search(protocol.content_private))
535
            or (protocol.has_public_view_right(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
536
537
538
                and _matches_search(protocol.content_public))
        ]
        for protocol in protocols:
539
            content = protocol.get_visible_content(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
540
541
542
543
544
545
546
547
            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):
548
549
550
551
552
553
                    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
554
555
556
557
                    if len(index_candidates) == 0:
                        parts.append((line[last_index:], False))
                        break
                    else:
558
559
                        new_index, term = min(
                            index_candidates, key=lambda t: t[0])
Robin Sonnabend's avatar
Robin Sonnabend committed
560
561
562
563
564
565
566
567
                        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
                ]))
568
            search_results[protocol] = " …<br />\n".join(formatted_lines)
569
570
    protocols = sorted(
        protocols, key=lambda protocol: protocol.date, reverse=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
571
    page = _get_page()
Robin Sonnabend's avatar
Robin Sonnabend committed
572
573
    page_length = _get_page_length()
    page_count = int(math.ceil(len(protocols) / page_length))
Robin Sonnabend's avatar
Robin Sonnabend committed
574
575
    if page >= page_count:
        page = 0
Robin Sonnabend's avatar
Robin Sonnabend committed
576
577
    begin_index = page * page_length
    end_index = (page + 1) * page_length
578
    max_page_length_exp = get_max_page_length_exp(protocols)
Robin Sonnabend's avatar
Robin Sonnabend committed
579
    protocols = protocols[begin_index:end_index]
Robin Sonnabend's avatar
Robin Sonnabend committed
580
    protocols_table = ProtocolsTable(protocols, search_results=search_results)
581
582
583
584
585
586
587
588
    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
589
590
591
592
593

@app.route("/protocol/new", methods=["GET", "POST"])
@login_required
def new_protocol():
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
594
    protocoltypes = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
595
    form = NewProtocolForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
596
    upload_form = NewProtocolSourceUploadForm(protocoltypes)
597
    file_upload_form = NewProtocolFileUploadForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
598
    if form.validate_on_submit():
599
600
        protocoltype = ProtocolType.query.filter_by(
            id=form.protocoltype_id.data).first()
Robin Sonnabend's avatar
Robin Sonnabend committed
601
602
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
603
            return back.redirect()
604
605
        protocol = Protocol.create_new_protocol(
            protocoltype, form.date.data, form.start_time.data)
606
        return back.redirect("show_protocol", protocol_id=protocol.id)
607
    type_id = request.args.get("protocoltype_id")
Robin Sonnabend's avatar
Robin Sonnabend committed
608
609
    if type_id is not None:
        form.protocoltype.data = type_id
610
        upload_form.protocoltype.data = type_id
611
612
613
614
615
    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
616
617

@app.route("/protocol/show/<int:protocol_id>")
618
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
619
620
@db_lookup(Protocol)
def show_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
621
622
    user = current_user()
    errors_table = ErrorsTable(protocol.errors)
623
624
    if not protocol.protocoltype.has_public_view_right(
            user, check_networks=False):
625
626
627
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
        if check_login():
            return redirect(url_for("index"))
628
        return redirect(url_for("login"))
Robin Sonnabend's avatar
Robin Sonnabend committed
629
630
    visible_documents = [
        document for document in protocol.documents
631
632
633
634
        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
635
    ]
636
    documents_table = DocumentsTable(visible_documents, protocol)
637
    document_upload_form = DocumentUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
638
    source_upload_form = KnownProtocolSourceUploadForm()
639
640
    time_diff = protocol.date - datetime.now().date()
    large_time_diff = not protocol.is_done() and time_diff.days > 0
641
642
    content_html = (
        protocol.content_html_private
643
644
        if protocol.has_private_view_right(user)
        else protocol.content_html_public)
645
646
    if content_html is not None:
        content_html = Markup(content_html)
647
648
649
650
651
652
653
    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
654

Robin Sonnabend's avatar
Robin Sonnabend committed
655
656
@app.route("/protocol/delete/<int:protocol_id>")
@login_required
657
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
658
@db_lookup(Protocol)
659
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
660
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
661
def delete_protocol(protocol):
662
    name = protocol.get_short_identifier()
Robin Sonnabend's avatar
Robin Sonnabend committed
663
    protocol.delete_orphan_todos()
Robin Sonnabend's avatar
Robin Sonnabend committed
664
665
666
    db.session.delete(protocol)
    db.session.commit()
    flash("Protokoll {} ist gelöscht.".format(name), "alert-success")
667
    return back.redirect("list_protocols")
Robin Sonnabend's avatar
Robin Sonnabend committed
668

669

Robin Sonnabend's avatar
Robin Sonnabend committed
670
@app.route("/protocol/etherpull/<int:protocol_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
671
@login_required
672
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
673
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
674
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
675
def etherpull_protocol(protocol):
676
677
    if not config.ETHERPAD_ACTIVE:
        flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
678
        return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
679
    protocol.source = get_etherpad_text(protocol.get_identifier())
Robin Sonnabend's avatar
Robin Sonnabend committed
680
681
682
    db.session.commit()
    tasks.parse_protocol(protocol)
    flash("Das Protokoll wird kompiliert.", "alert-success")
683
    return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
684

685

Robin Sonnabend's avatar
Robin Sonnabend committed
686
687
@app.route("/protocol/upload/known/<int:protocol_id>", methods=["POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
688
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
689
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
690
def upload_source_to_known_protocol(protocol):
691
    form = KnownProtocolSourceUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
692
693
694
695
696
697
698
699
700
701
702
703
704
705
    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")
706
    return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
707

708

Robin Sonnabend's avatar
Robin Sonnabend committed
709
710
711
712
@app.route("/protocol/upload/new/", methods=["POST"])
@login_required
def upload_new_protocol():
    user = current_user()
713
    available_types = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
714
715
716
717
    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")
718
719
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
720
721
722
        file = form.source.data
        if file.filename == "":
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
723
724
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
725
        source = file.stream.read().decode("utf-8")
726
727
        protocoltype = ProtocolType.query.filter_by(
            id=form.protocoltype_id.data).first()
728
729
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Invalider Protokolltyp oder keine Rechte.", "alert-error")
730
731
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
732
        protocol = Protocol(protocoltype_id=protocoltype.id, source=source)
733
734
        db.session.add(protocol)
        db.session.commit()
735
        for local_top in protocol.create_localtops():
736
737
            db.session.add(local_top)
        db.session.commit()
738
        tasks.parse_protocol(protocol)
739
        return back.redirect("show_protocol", protocol_id=protocol.id)
740
741
    return redirect(request.args.get("fail") or url_for("new_protocol"))

742

743
744
745
746
747
748
749
750
751
@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")
752
753
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
754
755
756
        file = form.file.data
        if file.filename == "":
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
757
758
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
759
        filename = secure_filename(file.filename)
760
761
        protocoltype = ProtocolType.query.filter_by(
            id=form.protocoltype_id.data).first()
762
763
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Invalider Protokolltyp oder keine Rechte.", "alert-error")
764
765
766
767
768
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
        protocol = Protocol(
            protocoltype_id=protocoltype.id,
            date=datetime.now().date(), done=True)
769
770
        db.session.add(protocol)
        db.session.commit()
771
        for local_top in protocol.create_localtops():
772
773
            db.session.add(local_top)
        db.session.commit()
774
775
        document = Document(
            protocol_id=protocol.id, name=filename,
776
777
            filename="", is_compiled=False)
        form.populate_obj(document)
778
779
        db.session.add(document)
        db.session.commit()
780
        internal_filename = get_internal_filename(
781
            protocol, document.id, filename)
782
783
784
        document.filename = internal_filename
        file.save(os.path.join(config.DOCUMENTS_PATH, internal_filename))
        db.session.commit()
785
        return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
786
787
    return redirect(request.args.get("fail") or url_for("new_protocol"))

788

Administrator's avatar
Administrator committed
789
790
@app.route("/protocol/recompile/<int:protocol_id>")
@login_required
791
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
792
@db_lookup(Protocol)
793
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
794
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
795
def recompile_protocol(protocol):
Administrator's avatar
Administrator committed
796
    tasks.parse_protocol(protocol)
797
    return back.redirect("show_protocol", protocol_id=protocol.id)
798

799

Robin Sonnabend's avatar
Robin Sonnabend committed
800
801
@app.route("/protocol/source/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
802
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
803
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
804
def get_protocol_source(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
805
    file_like = BytesIO(protocol.source.encode("utf-8"))
806
807
808
809
    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
810

Robin Sonnabend's avatar
Robin Sonnabend committed
811
812
@app.route("/protocol/template/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
813
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
814
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
815
def get_protocol_template(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
816
    file_like = BytesIO(protocol.get_template().encode("utf-8"))
817
818
819
820
821
    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
822

Robin Sonnabend's avatar
Robin Sonnabend committed
823
824
@app.route("/protocol/etherpush/<int:protocol_id>")
@login_required
825
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
826
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
827
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
828
def etherpush_protocol(protocol):
829
830
    if not config.ETHERPAD_ACTIVE:
        flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
831
        return back.redirect("show_protocol", protocol_id=protocol.id)
832
833
    if not protocol.is_done():
        tasks.set_etherpad_content(protocol)
834
    return redirect(protocol.get_etherpad_link())
Robin Sonnabend's avatar
Robin Sonnabend committed
835

836

Robin Sonnabend's avatar
Robin Sonnabend committed
837
@app.route("/protocol/update/<int:protocol_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
838
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
839
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
840
@require_publish_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
841
def update_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
842
    upload_form = KnownProtocolSourceUploadForm()
843
    edit_form = generate_protocol_form(protocol)(obj=protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
844
845
    if edit_form.validate_on_submit():
        edit_form.populate_obj(protocol)
846
847
        for meta in protocol.metas:
            meta.value = getattr(edit_form.metas, meta.name).data
Robin Sonnabend's avatar
Robin Sonnabend committed
848
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
849
        tasks.push_tops_to_calendar(protocol)
850
        return back.redirect("show_protocol", protocol_id=protocol.id)
851
852
    for meta in protocol.metas:
        getattr(edit_form.metas, meta.name).data = meta.value
853
854
855
856
    return render_template(
        "protocol-update.html", upload_form=upload_form,
        edit_form=edit_form, protocol=protocol)

Robin Sonnabend's avatar
Robin Sonnabend committed
857

858
859
@app.route("/protocol/publish/<int:protocol_id>")
@login_required
860
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
861
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
862
@require_publish_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
863
def publish_protocol(protocol):
864
865
    protocol.public = True
    db.session.commit()
866
    return back.redirect("show_protocol", protocol_id=protocol.id)
867