server.py 67.3 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
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,
30
    code_filter, code_key_filter, indent_tab_filter)
31
32
33
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
87
app.jinja_env.filters["code_key"] = code_key_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
88
app.jinja_env.filters["indent_tab"] = indent_tab_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
89
app.jinja_env.filters["fancy_join"] = fancy_join
Robin Sonnabend's avatar
Robin Sonnabend committed
90
app.jinja_env.filters["footnote_hash"] = footnote_hash
91
app.jinja_env.tests["auth_valid"] = security_manager.check_user
Robin Sonnabend's avatar
Robin Sonnabend committed
92
app.jinja_env.tests["needs_date"] = needs_date_test
93
app.jinja_env.globals["get_csrf_token"] = get_csrf_token
94

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

100

101
102
import tasks

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

112

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

123

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

131

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

154

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

160

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

179

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

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

239
240
        def _todo_sort_key(todo):
            protocol = todo.get_first_protocol()
241
242
243
            if protocol is not None and protocol.date is not None:
                return protocol.date
            return datetime.now().date()
244
        todos = sorted(todos, key=_todo_sort_key, reverse=True)
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():
254
255
256
257
    return render_template(
        "documentation.html")

@app.route("/documentation/sessionmanagement")
258
@back.anchor
259
260
261
262
263
264
@login_required
def sessionmanagement_documentation():
    return render_template(
        "documentation-sessionmanagement.html")

@app.route("/documentation/sessionmanagement/plan")
265
@back.anchor
266
267
268
269
270
271
@login_required
def plan_sessionmanagement_documentation():
    return render_template(
        "documentation-sessionmanagement-plan.html")

@app.route("/documentation/sessionmanagement/write")
272
@back.anchor
273
274
275
276
277
278
@login_required
def write_sessionmanagement_documentation():
    return render_template(
        "documentation-sessionmanagement-write.html")

@app.route("/documentation/sessionmanagement/tracking")
279
@back.anchor
280
281
282
283
284
285
@login_required
def tracking_sessionmanagement_documentation():
    return render_template(
        "documentation-sessionmanagement-tracking.html")

@app.route("/documentation/syntax")
286
@back.anchor
287
288
289
290
291
292
@login_required
def syntax_documentation():
    return render_template(
        "documentation-syntax.html")

@app.route("/documentation/syntax/meta")
293
@back.anchor
294
295
296
297
298
299
@login_required
def meta_syntax_documentation():
    return render_template(
        "documentation-syntax-meta.html")

@app.route("/documentation/syntax/top")
300
@back.anchor
301
302
303
304
305
306
@login_required
def top_syntax_documentation():
    return render_template(
        "documentation-syntax-top.html")

@app.route("/documentation/syntax/lists")
307
@back.anchor
308
309
310
311
312
@login_required
def lists_syntax_documentation():
    return render_template("documentation-syntax-lists.html")

@app.route("/documentation/syntax/internal")
313
@back.anchor
314
315
316
317
318
319
@login_required
def internal_syntax_documentation():
    return render_template(
        "documentation-syntax-internal.html")

@app.route("/documentation/syntax/tags")
320
@back.anchor
321
322
@login_required
def tags_syntax_documentation():
323
324
325
326
327
328
	states = {state:[] for state in list(TodoState)}
	name_to_state = TodoState.get_name_to_state()
	for state_name in name_to_state:
		states[name_to_state[state_name]].append(state_name)
	return render_template(
		"documentation-syntax-tags.html", states=states)
329
330

@app.route("/documentation/configuration")
331
@back.anchor
332
333
334
335
336
337
@login_required
def configuration_documentation():
    return render_template(
        "documentation-configuration.html")

@app.route("/documentation/configuration/types")
338
@back.anchor
339
340
341
342
343
344
@login_required
def types_configuration_documentation():
    return render_template(
        "documentation-configuration-types.html")

@app.route("/documentation/configuration/todomails")
345
@back.anchor
346
347
348
349
@login_required
def todomails_configuration_documentation():
    return render_template(
        "documentation-configuration-todomails.html")
Robin Sonnabend's avatar
Robin Sonnabend committed
350

351
352
353
354
@app.route("/documentation/configuration/settings")
@back.anchor
@login_required
def settings_configuration_documentation():
355
356
357
358
	user = current_user()
	return render_template(
        "documentation-configuration-settings.html",
		system_administrator=(user is not None and config.ADMIN_GROUP in user.groups))
359

Robin Sonnabend's avatar
Robin Sonnabend committed
360
@app.route("/types/list")
361
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
362
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
363
364
365
366
def list_types():
    user = current_user()
    types = [
        protocoltype for protocoltype in ProtocolType.query.all()
367
        if (protocoltype.has_private_view_right(user)
368
369
            or protocoltype.has_public_view_right(user)
            or protocoltype.is_public)]
370
    types = sorted(types, key=lambda t: t.short_name)
Robin Sonnabend's avatar
Robin Sonnabend committed
371
    types_table = ProtocolTypesTable(types)
372
373
374
    return render_template(
        "types-list.html", types=types, types_table=types_table)

Robin Sonnabend's avatar
Robin Sonnabend committed
375
376
377
378
379
380
381
382

@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:
383
384
            flash("Du kannst keinen internen Protokolltypen anlegen, "
                  "zu dem du selbst keinen Zugang hast.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
385
        else:
386
387
            protocoltype = ProtocolType()
            form.populate_obj(protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
388
389
            db.session.add(protocoltype)
            db.session.commit()
390
391
            flash("Der Protokolltyp {} wurde angelegt.".format(
                protocoltype.name), "alert-success")
392
        return back.redirect("list_types")
Robin Sonnabend's avatar
Robin Sonnabend committed
393
394
    return render_template("type-new.html", form=form)

395

Robin Sonnabend's avatar
Robin Sonnabend committed
396
@app.route("/type/edit/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
397
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
398
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
399
@require_private_view_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
400
def edit_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
401
402
403
404
    user = current_user()
    form = ProtocolTypeForm(obj=protocoltype)
    if form.validate_on_submit():
        if form.private_group.data not in user.groups:
405
406
            flash("Du kannst keinen internen Protokolltypen anlegen, "
                  "zu dem du selbst keinen Zugang hast.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
407
408
409
        else:
            form.populate_obj(protocoltype)
            db.session.commit()
410
            return back.redirect("show_type", protocoltype_id=protocoltype.id)
411
412
413
    return render_template(
        "type-edit.html", form=form, protocoltype=protocoltype)

Robin Sonnabend's avatar
Robin Sonnabend committed
414

Robin Sonnabend's avatar
Robin Sonnabend committed
415
@app.route("/type/show/<int:protocoltype_id>")
416
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
417
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
418
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
419
@require_private_view_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
420
def show_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
421
    protocoltype_table = ProtocolTypeTable(protocoltype)
422
423
424
425
    default_tops_table = DefaultTOPsTable(
        protocoltype.default_tops, protocoltype)
    reminders_table = MeetingRemindersTable(
        protocoltype.reminders, protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
426
    metas_table = DefaultMetasTable(protocoltype.metas, protocoltype)
427
428
429
430
431
432
433
434
435
    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)

436

Robin Sonnabend's avatar
Robin Sonnabend committed
437
@app.route("/type/delete/<int:protocoltype_id>")
438
@login_required
439
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
440
@db_lookup(ProtocolType)
441
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
442
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
443
def delete_type(protocoltype):
444
    name = protocoltype.name
445
    db.session.delete(protocoltype)
446
447
    db.session.commit()
    flash("Der Protokolltype {} wurde gelöscht.".format(name), "alert-success")
448
    return back.redirect("list_types")
449

450
451
452

@app.route("/type/reminders/new/<int:protocoltype_id>",
           methods=["GET", "POST"])
453
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
454
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
455
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
456
def new_reminder(protocoltype):
457
458
    form = MeetingReminderForm()
    if form.validate_on_submit():
459
460
        meetingreminder = MeetingReminder(protocoltype_id=protocoltype.id)
        form.populate_obj(meetingreminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
461
        db.session.add(meetingreminder)
462
        db.session.commit()
463
        return back.redirect("show_type", protocoltype_id=protocoltype.id)
464
465
    return render_template(
        "reminder-new.html", form=form, protocoltype=protocoltype)
466

467
468
469

@app.route("/type/reminder/edit/<int:meetingreminder_id>",
           methods=["GET", "POST"])
470
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
471
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
472
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
473
474
def edit_reminder(meetingreminder):
    form = MeetingReminderForm(obj=meetingreminder)
475
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
476
        form.populate_obj(meetingreminder)
477
        db.session.commit()
478
479
480
481
482
        return back.redirect(
            "show_type", protocoltype_id=meetingreminder.protocoltype.id)
    return render_template(
        "reminder-edit.html", form=form, meetingreminder=meetingreminder)

483

Robin Sonnabend's avatar
Robin Sonnabend committed
484
@app.route("/type/reminder/delete/<int:meetingreminder_id>")
485
@login_required
486
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
487
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
488
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
489
490
491
def delete_reminder(meetingreminder):
    protocoltype = meetingreminder.protocoltype
    db.session.delete(meetingreminder)
492
    db.session.commit()
493
    return back.redirect("show_type", protocoltype_id=protocoltype.id)
494
495


Robin Sonnabend's avatar
Robin Sonnabend committed
496
@app.route("/type/tops/new/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
497
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
498
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
499
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
500
def new_default_top(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
501
502
    form = DefaultTopForm()
    if form.validate_on_submit():
503
504
        defaulttop = DefaultTOP(protocoltype_id=protocoltype.id)
        form.populate_obj(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
505
        db.session.add(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
506
        db.session.commit()
507
508
        for protocol in protocoltype.protocols:
            if not protocol.done:
509
510
                localtop = LocalTOP(
                    protocol_id=protocol.id,
Administrator's avatar
Administrator committed
511
                    defaulttop_id=defaulttop.id, description="")
512
513
                db.session.add(localtop)
        db.session.commit()
514
515
        flash("Der Standard-TOP {} wurde für dem Protokolltyp {} hinzugefügt."
              .format(defaulttop.name, protocoltype.name), "alert-success")
516
        return back.redirect()
517
518
    return render_template(
        "default-top-new.html", form=form, protocoltype=protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
519
520


521
522
@app.route("/type/tops/edit/<int:protocoltype_id>/<int:defaulttop_id>",
           methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
523
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
524
@db_lookup(ProtocolType, DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
525
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
526
527
def edit_default_top(protocoltype, defaulttop):
    form = DefaultTopForm(obj=defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
528
    if form.validate_on_submit():
Robin Sonnabend's avatar
Robin Sonnabend committed
529
        form.populate_obj(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
530
        db.session.commit()
531
        return back.redirect("show_type", protocoltype_id=protocoltype.id)
532
533
534
535
    return render_template(
        "default-top-edit.html", form=form,
        protocoltype=protocoltype, defaulttop=defaulttop)

Robin Sonnabend's avatar
Robin Sonnabend committed
536

Robin Sonnabend's avatar
Robin Sonnabend committed
537
@app.route("/type/tops/delete/<int:defaulttop_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
538
@login_required
539
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
540
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
541
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
542
543
def delete_default_top(defaulttop):
    db.session.delete(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
544
    db.session.commit()
545
546
547
    return back.redirect(
        "show_type", protocoltype_id=defaulttop.protocoltype.id)

Robin Sonnabend's avatar
Robin Sonnabend committed
548

Robin Sonnabend's avatar
Robin Sonnabend committed
549
@app.route("/type/tops/move/<int:defaulttop_id>/<diff>/")
Robin Sonnabend's avatar
Robin Sonnabend committed
550
@login_required
551
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
552
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
553
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
554
def move_default_top(defaulttop, diff):
Robin Sonnabend's avatar
Robin Sonnabend committed
555
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
556
        defaulttop.number += int(diff)
Robin Sonnabend's avatar
Robin Sonnabend committed
557
558
559
        db.session.commit()
    except ValueError:
        flash("Die angegebene Differenz ist keine Zahl.", "alert-error")
560
561
562
    return back.redirect(
        "show_type", protocoltype_id=defaulttop.protocoltype.id)

Robin Sonnabend's avatar
Robin Sonnabend committed
563
564

@app.route("/protocols/list")
565
@back.anchor
566
567
def list_protocols():
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
568
569
    protocoltype_id = None
    try:
570
        protocoltype_id = int(request.args.get("protocoltype_id"))
Robin Sonnabend's avatar
Robin Sonnabend committed
571
572
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
573
    state_open = -1
Robin Sonnabend's avatar
Robin Sonnabend committed
574
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
575
        state_open = int(request.args.get("state_open"))
Robin Sonnabend's avatar
Robin Sonnabend committed
576
577
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
578
    search_term = request.args.get("search")
579
580
    protocoltypes = ProtocolType.get_public_protocoltypes(
        user, check_networks=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
581
    search_form = ProtocolSearchForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
582
    if protocoltype_id is not None:
583
        search_form.protocoltype_id.data = protocoltype_id
Robin Sonnabend's avatar
Robin Sonnabend committed
584
585
    if state_open is not None:
        search_form.state_open.data = state_open
Robin Sonnabend's avatar
Robin Sonnabend committed
586
587
588
    if search_term is not None:
        search_form.search.data = search_term
    protocol_query = Protocol.query
Robin Sonnabend's avatar
Robin Sonnabend committed
589
590
591
592
593
594
595
596
597
    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))
            ))
598
    protocols = [
Robin Sonnabend's avatar
Robin Sonnabend committed
599
        protocol for protocol in protocol_query.all()
600
601
        if protocol.protocoltype.has_public_view_right(
            user, check_networks=False)
602
    ]
603

Robin Sonnabend's avatar
Robin Sonnabend committed
604
605
606
607
608
609
    def _matches_search(content):
        content = content.lower()
        for search_term in search_terms:
            if search_term.lower() not in content:
                return False
        return True
610

Robin Sonnabend's avatar
Robin Sonnabend committed
611
612
613
614
615
616
617
    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
618
619
620
621
622
    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
623
624
    if state_open is not None and state_open != -1:
        protocol_done = bool(state_open)
Robin Sonnabend's avatar
Robin Sonnabend committed
625
626
        protocols = [
            protocol for protocol in protocols
627
            if (protocol.is_done() or False) == protocol_done
Robin Sonnabend's avatar
Robin Sonnabend committed
628
        ]
Robin Sonnabend's avatar
Robin Sonnabend committed
629
630
631
632
633
    if shall_search:
        protocols = [
            protocol for protocol in protocols
            if (protocol.protocoltype.has_private_view_right(user)
                and _matches_search(protocol.content_private))
634
            or (protocol.has_public_view_right(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
635
636
637
                and _matches_search(protocol.content_public))
        ]
        for protocol in protocols:
638
            content = protocol.get_visible_content(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
639
640
641
642
643
644
645
646
            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):
647
648
649
650
651
652
                    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
653
654
655
656
                    if len(index_candidates) == 0:
                        parts.append((line[last_index:], False))
                        break
                    else:
657
658
                        new_index, term = min(
                            index_candidates, key=lambda t: t[0])
Robin Sonnabend's avatar
Robin Sonnabend committed
659
660
661
662
663
664
665
666
                        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
                ]))
667
            search_results[protocol] = " …<br />\n".join(formatted_lines)
668
669
    protocols = sorted(
        protocols, key=lambda protocol: protocol.date, reverse=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
670
    page = _get_page()
Robin Sonnabend's avatar
Robin Sonnabend committed
671
672
    page_length = _get_page_length()
    page_count = int(math.ceil(len(protocols) / page_length))
Robin Sonnabend's avatar
Robin Sonnabend committed
673
674
    if page >= page_count:
        page = 0
Robin Sonnabend's avatar
Robin Sonnabend committed
675
676
    begin_index = page * page_length
    end_index = (page + 1) * page_length
677
    max_page_length_exp = get_max_page_length_exp(protocols)
Robin Sonnabend's avatar
Robin Sonnabend committed
678
    protocols = protocols[begin_index:end_index]
Robin Sonnabend's avatar
Robin Sonnabend committed
679
    protocols_table = ProtocolsTable(protocols, search_results=search_results)
680
681
682
683
684
685
686
687
    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
688
689
690
691
692

@app.route("/protocol/new", methods=["GET", "POST"])
@login_required
def new_protocol():
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
693
    protocoltypes = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
694
    form = NewProtocolForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
695
    upload_form = NewProtocolSourceUploadForm(protocoltypes)
696
    file_upload_form = NewProtocolFileUploadForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
697
    if form.validate_on_submit():
698
699
        protocoltype = ProtocolType.query.filter_by(
            id=form.protocoltype_id.data).first()
Robin Sonnabend's avatar
Robin Sonnabend committed
700
701
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
702
            return back.redirect()
703
704
        protocol = Protocol.create_new_protocol(
            protocoltype, form.date.data, form.start_time.data)
705
        return back.redirect("show_protocol", protocol_id=protocol.id)
706
    type_id = request.args.get("protocoltype_id")
Robin Sonnabend's avatar
Robin Sonnabend committed
707
708
    if type_id is not None:
        form.protocoltype.data = type_id
709
        upload_form.protocoltype.data = type_id
710
711
712
713
714
    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
715
716

@app.route("/protocol/show/<int:protocol_id>")
717
@back.anchor
Robin Sonnabend's avatar
Robin Sonnabend committed
718
719
@db_lookup(Protocol)
def show_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
720
721
    user = current_user()
    errors_table = ErrorsTable(protocol.errors)
722
723
    if not protocol.protocoltype.has_public_view_right(
            user, check_networks=False):
724
725
726
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
        if check_login():
            return redirect(url_for("index"))
727
        return redirect(url_for("login"))
Robin Sonnabend's avatar
Robin Sonnabend committed
728
729
    visible_documents = [
        document for document in protocol.documents
730
731
732
733
        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
734
    ]
735
    documents_table = DocumentsTable(visible_documents, protocol)
736
    document_upload_form = DocumentUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
737
    source_upload_form = KnownProtocolSourceUploadForm()
738
739
    time_diff = protocol.date - datetime.now().date()
    large_time_diff = not protocol.is_done() and time_diff.days > 0
740
741
    content_html = (
        protocol.content_html_private
742
743
        if protocol.has_private_view_right(user)
        else protocol.content_html_public)
744
745
    if content_html is not None:
        content_html = Markup(content_html)
746
747
748
749
750
751
752
    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
753

Robin Sonnabend's avatar
Robin Sonnabend committed
754
755
@app.route("/protocol/delete/<int:protocol_id>")
@login_required
756
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
757
@db_lookup(Protocol)
758
@require_admin_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
759
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
760
def delete_protocol(protocol):
761
    name = protocol.get_short_identifier()
Robin Sonnabend's avatar
Robin Sonnabend committed
762
    protocol.delete_orphan_todos()
Robin Sonnabend's avatar
Robin Sonnabend committed
763
764
765
    db.session.delete(protocol)
    db.session.commit()
    flash("Protokoll {} ist gelöscht.".format(name), "alert-success")
766
    return back.redirect("list_protocols")
Robin Sonnabend's avatar
Robin Sonnabend committed
767
768


Robin Sonnabend's avatar
Robin Sonnabend committed
769
@app.route("/protocol/etherpull/<int:protocol_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
770
@login_required
771
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
772
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
773
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
774
def etherpull_protocol(protocol):
775
776
    if not config.ETHERPAD_ACTIVE:
        flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
777
        return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
778
    protocol.source = get_etherpad_text(protocol.get_identifier())
Robin Sonnabend's avatar
Robin Sonnabend committed
779
780
781
    db.session.commit()
    tasks.parse_protocol(protocol)
    flash("Das Protokoll wird kompiliert.", "alert-success")
782
    return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
783
784


Robin Sonnabend's avatar
Robin Sonnabend committed
785
786
@app.route("/protocol/upload/known/<int:protocol_id>", methods=["POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
787
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
788
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
789
def upload_source_to_known_protocol(protocol):
790
    form = KnownProtocolSourceUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
791
792
793
794
795
796
797
798
799
800
801
802
803
804
    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")
805
    return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
806
807
808
809
810
811


@app.route("/protocol/upload/new/", methods=["POST"])
@login_required
def upload_new_protocol():
    user = current_user()
812
    available_types = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
813
814
815
816
    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")
817
818
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
819
820
821
        file = form.source.data
        if file.filename == "":
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
822
823
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
824
        source = file.stream.read().decode("utf-8")
825
826
        protocoltype = ProtocolType.query.filter_by(
            id=form.protocoltype_id.data).first()
827
828
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Invalider Protokolltyp oder keine Rechte.", "alert-error")
829
830
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
831
        protocol = Protocol(protocoltype_id=protocoltype.id, source=source)
832
833
        db.session.add(protocol)
        db.session.commit()
834
        for local_top in protocol.create_localtops():
835
836
            db.session.add(local_top)
        db.session.commit()
837
        tasks.parse_protocol(protocol)
838
        return back.redirect("show_protocol", protocol_id=protocol.id)
839
840
    return redirect(request.args.get("fail") or url_for("new_protocol"))

841

842
843
844
845
846
847
848
849
850
@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")
851
852
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
853
854
855
        file = form.file.data
        if file.filename == "":
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
856
857
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
858
        filename = secure_filename(file.filename)
859
860
        protocoltype = ProtocolType.query.filter_by(
            id=form.protocoltype_id.data).first()
861
862
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Invalider Protokolltyp oder keine Rechte.", "alert-error")
863
864
865
866
867
            return redirect(request.args.get("fail")
                            or url_for("new_protocol"))
        protocol = Protocol(
            protocoltype_id=protocoltype.id,
            date=datetime.now().date(), done=True)
868
869
        db.session.add(protocol)
        db.session.commit()
870
        for local_top in protocol.create_localtops():
871
872
            db.session.add(local_top)
        db.session.commit()
873
874
        document = Document(
            protocol_id=protocol.id, name=filename,
875
876
            filename="", is_compiled=False)
        form.populate_obj(document)
877
878
        db.session.add(document)
        db.session.commit()
879
        internal_filename = get_internal_filename(
880
            protocol, document.id, filename)
881
882
883
        document.filename = internal_filename
        file.save(os.path.join(config.DOCUMENTS_PATH, internal_filename))
        db.session.commit()
884
        return back.redirect("show_protocol", protocol_id=protocol.id)
Robin Sonnabend's avatar
Robin Sonnabend committed
885
886
    return redirect(request.args.get("fail") or url_for("new_protocol"))

887

Administrator's avatar
Administrator committed
888
889
@app.route("/protocol/recompile/<int:protocol_id>")
@login_required
890
@protect_csrf
Robin Sonnabend's avatar
Robin Sonnabend committed
891
@db_lookup(Protocol)