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

Robin Sonnabend's avatar
Robin Sonnabend committed
5
from flask import Flask, g, current_app, request, session, flash, redirect, url_for, abort, render_template, Response, send_file as flask_send_file, Markup
6
from werkzeug.utils import secure_filename
7
8
9
10
from flask_script import Manager, prompt
from flask_migrate import Migrate, MigrateCommand
#from flask_socketio import SocketIO
from celery import Celery
Robin Sonnabend's avatar
Robin Sonnabend committed
11
from sqlalchemy import or_, and_
12
13
14
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger
Robin Sonnabend's avatar
Robin Sonnabend committed
15
import atexit
16
import feedgen.feed
17
import icalendar
Robin Sonnabend's avatar
Robin Sonnabend committed
18
from io import StringIO, BytesIO
19
import os
20
from datetime import datetime, time, timedelta
Robin Sonnabend's avatar
Robin Sonnabend committed
21
import math
22
import mimetypes
23
import subprocess
24
from dateutil import tz
25
26

import config
Robin Sonnabend's avatar
Robin Sonnabend committed
27
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, group_required, class_filter, needs_date_test, todostate_name_filter, code_filter, indent_tab_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
28
from utils import is_past, mail_manager, get_first_unused_int, set_etherpad_text, get_etherpad_text, split_terms, optional_int_arg, fancy_join, footnote_hash
Robin Sonnabend's avatar
Robin Sonnabend committed
29
from decorators import db_lookup, require_public_view_right, require_private_view_right, require_modify_right, require_publish_right, require_admin_right
Robin Sonnabend's avatar
Robin Sonnabend committed
30
from models.database import ProtocolType, Protocol, DefaultTOP, TOP, LocalTOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail, DecisionDocument, TodoState, Meta, DefaultMeta, DecisionCategory, Like
Robin Sonnabend's avatar
Robin Sonnabend committed
31
from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, generate_protocol_form, TopForm, LocalTopForm, SearchForm, DecisionSearchForm, ProtocolSearchForm, TodoSearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm, DefaultMetaForm, MetaForm, MergeTodosForm, DecisionCategoryForm, DocumentEditForm
32
from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable, TodoMailsTable, DefaultMetasTable, DecisionCategoriesTable
33
from legacy import import_old_todos, import_old_protocols, import_old_todomails
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

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

def make_celery(app, config):
    celery = Celery(app.import_name, broker=config.CELERY_BROKER_URL)
    celery.conf.update(app.config)
    return celery
celery = make_celery(app, config)

#def make_socketio(app, config):
#    socketio = SocketIO(app)
#    return socketio
#socketio = make_socketio(app, config)

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
57
app.jinja_env.filters["timify"] = time_filter
58
app.jinja_env.filters["timify_short"] = time_filter_short
Robin Sonnabend's avatar
Robin Sonnabend committed
59
app.jinja_env.filters["datify_short"] = date_filter_short
Robin Sonnabend's avatar
Robin Sonnabend committed
60
app.jinja_env.filters["datify_long"] = date_filter_long
Robin Sonnabend's avatar
Robin Sonnabend committed
61
#app.jinja_env.filters["url_complete"] = url_manager.complete
62
app.jinja_env.filters["class"] = class_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
63
64
app.jinja_env.filters["todo_get_name"] = todostate_name_filter
app.jinja_env.filters["code"] = code_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
65
app.jinja_env.filters["indent_tab"] = indent_tab_filter
Robin Sonnabend's avatar
Robin Sonnabend committed
66
app.jinja_env.filters["fancy_join"] = fancy_join
Robin Sonnabend's avatar
Robin Sonnabend committed
67
app.jinja_env.filters["footnote_hash"] = footnote_hash
68
app.jinja_env.tests["auth_valid"] = security_manager.check_user
Robin Sonnabend's avatar
Robin Sonnabend committed
69
app.jinja_env.tests["needs_date"] = needs_date_test
70

71
72
73
74
75
76
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)
    

77
78
import tasks

79
80
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
81
app.jinja_env.globals.update(zip=zip)
Robin Sonnabend's avatar
Robin Sonnabend committed
82
83
84
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
85
app.jinja_env.globals.update(now=datetime.now)
86

87
88
89
90
91
92
93
94
95
96
97
98
def get_git_revision():
    gitlab_url = "https://git.fsmpi.rwth-aachen.de/protokollsystem/proto3"
    commit_hash = subprocess.check_output(["git", "log", "-g", "-1", "--pretty=%H"]).decode("UTF-8").strip()
    timestamp = int(subprocess.check_output(["git", "log", "-g", "-1", "--pretty=%at"]).strip())
    commit_date = datetime.fromtimestamp(timestamp)
    return {"url": gitlab_url, "hash": commit_hash, "date": commit_date}

try:
    app.jinja_env.globals["git_revision"] = get_git_revision()
except:
    pass

99
100
# blueprints here

101
102
@manager.command
def import_legacy():
103
    """Import the old todos and protocols from an sql dump"""
104
    filename = prompt("SQL-file")
105
    #filename = "legacy.sql"
106
107
    with open(filename, "rb") as sqlfile:
        content = sqlfile.read().decode("utf-8")
108
109
        import_old_todos(content)
        import_old_protocols(content)
110
        import_old_todomails(content)
111

112
113
114
115
@manager.command
def recompile_all():
    for protocol in sorted(Protocol.query.all(), key=lambda p: p.date):
        if protocol.is_done():
116
            print(protocol.get_short_identifier())
117
118
119
120
121
122
123
124
125
126
127
128
            tasks.parse_protocol(protocol)

@manager.command
def merge_todos():
    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)
129
            if todo2.state.value > todo1.state.value:
130
131
132
133
134
135
136
137
138
139
140
141
142
                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

@manager.command
def runserver():
143
    app.run()
144
145
    make_scheduler()

146
147
148
149
# cause uwsgi currently has a bug
def send_file(file_like, cache_timeout, as_attachment, attachment_filename):
    mimetype, _ = mimetypes.guess_type(attachment_filename)
    response = Response(file_like.read(), mimetype)
150
151
152
153
154
155
156
157
    if as_attachment:
        response.headers["Content-Disposition"] = 'attachment; filename="{}"'.format(attachment_filename)
    content_type = mimetype
    if mimetype.startswith("text/"):
        content_type = "{}; charset=utf-8".format(content_type)
    response.headers["Content-Type"] = content_type
    response.headers["Cache-Control"] = "public, max-age={}".format(cache_timeout)
    response.headers["Connection"] = "close"
158
    return response
159

160
161
@app.route("/")
def index():
Robin Sonnabend's avatar
Robin Sonnabend committed
162
163
164
    user = current_user()
    protocols = [
        protocol for protocol in Protocol.query.all()
165
166
        if protocol.protocoltype.has_public_view_right(user,
            check_networks=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
167
    ]
168
    def _protocol_sort_key(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
169
170
171
        if protocol.date is not None:
            return protocol.date
        return datetime.now().date()
Robin Sonnabend's avatar
Robin Sonnabend committed
172
    current_day = datetime.now().date()
173
    open_protocols = sorted(
Robin Sonnabend's avatar
Robin Sonnabend committed
174
175
176
177
        [
            protocol for protocol in protocols
            if not protocol.done
            and (protocol.date - current_day).days < config.MAX_INDEX_DAYS
178
            and (current_day - protocol.date).days < config.MAX_PAST_INDEX_DAYS
Robin Sonnabend's avatar
Robin Sonnabend committed
179
        ],
180
        key=_protocol_sort_key
Robin Sonnabend's avatar
Robin Sonnabend committed
181
182
    )
    finished_protocols = sorted(
183
184
        [
            protocol for protocol in protocols
185
186
187
            if protocol.done and protocol.public
            and (protocol.has_private_view_right(user)
                or protocol.protocoltype.has_public_view_right(user, check_networks=False))
188
        ],
189
190
        key=_protocol_sort_key,
        reverse=True
Robin Sonnabend's avatar
Robin Sonnabend committed
191
    )
192
193
194
195
196
197
198
    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)
        has_public_view_right = protocol.protocoltype.has_public_view_right(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
199
200
201
    todos = None
    if check_login():
        todos = [
202
            todo for todo in Todo.query.all()
203
            if todo.protocoltype.has_private_view_right(user)
204
            and not todo.is_done()
Robin Sonnabend's avatar
Robin Sonnabend committed
205
        ]
206
207
208
209
210
211
        user_todos = [
            todo for todo in todos
            if user.username.lower() in list(map(str.strip, todo.who.lower().split(",")))
        ]
        if len(user_todos) > 0:
            todos = user_todos
212
213
        def _todo_sort_key(todo):
            protocol = todo.get_first_protocol()
214
            return protocol.date if protocol is not None and protocol.date is not None else datetime.now().date()
215
        todos = sorted(todos, key=_todo_sort_key, reverse=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
216
    todos_table = TodosTable(todos) if todos is not None else None
217
    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)
218

Robin Sonnabend's avatar
Robin Sonnabend committed
219
@app.route("/documentation")
Robin Sonnabend's avatar
Robin Sonnabend committed
220
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
221
def documentation():
222
223
224
225
    return render_template(
        "documentation.html")

@app.route("/documentation/sessionmanagement")
226
# @back.anchor
227
228
229
230
231
232
@login_required
def sessionmanagement_documentation():
    return render_template(
        "documentation-sessionmanagement.html")

@app.route("/documentation/sessionmanagement/plan")
233
# @back.anchor
234
235
236
237
238
239
@login_required
def plan_sessionmanagement_documentation():
    return render_template(
        "documentation-sessionmanagement-plan.html")

@app.route("/documentation/sessionmanagement/write")
240
# @back.anchor
241
242
243
244
245
246
@login_required
def write_sessionmanagement_documentation():
    return render_template(
        "documentation-sessionmanagement-write.html")

@app.route("/documentation/sessionmanagement/tracking")
247
# @back.anchor
248
249
250
251
252
253
@login_required
def tracking_sessionmanagement_documentation():
    return render_template(
        "documentation-sessionmanagement-tracking.html")

@app.route("/documentation/syntax")
254
# @back.anchor
255
256
257
258
259
260
@login_required
def syntax_documentation():
    return render_template(
        "documentation-syntax.html")

@app.route("/documentation/syntax/meta")
261
# @back.anchor
262
263
264
265
266
267
@login_required
def meta_syntax_documentation():
    return render_template(
        "documentation-syntax-meta.html")

@app.route("/documentation/syntax/top")
268
# @back.anchor
269
270
271
272
273
274
@login_required
def top_syntax_documentation():
    return render_template(
        "documentation-syntax-top.html")

@app.route("/documentation/syntax/lists")
275
# @back.anchor
276
277
278
279
280
@login_required
def lists_syntax_documentation():
    return render_template("documentation-syntax-lists.html")

@app.route("/documentation/syntax/internal")
281
# @back.anchor
282
283
284
285
286
287
@login_required
def internal_syntax_documentation():
    return render_template(
        "documentation-syntax-internal.html")

@app.route("/documentation/syntax/tags")
288
# @back.anchor
289
290
@login_required
def tags_syntax_documentation():
Robin Sonnabend's avatar
Robin Sonnabend committed
291
292
    todostates = list(TodoState)
    name_to_state = TodoState.get_name_to_state()
293
294
295
296
297
    return render_template(
        "documentation-syntax-tags.html", todostates=todostates,
        name_to_state=name_to_state)

@app.route("/documentation/configuration")
298
# @back.anchor
299
300
301
302
303
304
@login_required
def configuration_documentation():
    return render_template(
        "documentation-configuration.html")

@app.route("/documentation/configuration/types")
305
# @back.anchor
306
307
308
309
310
311
@login_required
def types_configuration_documentation():
    return render_template(
        "documentation-configuration-types.html")

@app.route("/documentation/configuration/todomails")
312
# @back.anchor
313
314
315
316
@login_required
def todomails_configuration_documentation():
    return render_template(
        "documentation-configuration-todomails.html")
Robin Sonnabend's avatar
Robin Sonnabend committed
317

Robin Sonnabend's avatar
Robin Sonnabend committed
318
@app.route("/types/list")
Robin Sonnabend's avatar
Robin Sonnabend committed
319
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
320
321
322
323
324
def list_types():
    is_logged_in = check_login()
    user = current_user()
    types = [
        protocoltype for protocoltype in ProtocolType.query.all()
325
326
        if (protocoltype.has_private_view_right(user)
        or protocoltype.has_public_view_right(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
327
        or protocoltype.is_public)]
328
    types = sorted(types, key=lambda t: t.short_name)
Robin Sonnabend's avatar
Robin Sonnabend committed
329
330
331
332
333
334
335
336
337
338
339
340
    types_table = ProtocolTypesTable(types)
    return render_template("types-list.html", types=types, types_table=types_table)

@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:
            flash("Du kannst keinen internen Protokolltypen anlegen, zu dem du selbst keinen Zugang hast.", "alert-error")
        else:
341
342
            protocoltype = ProtocolType()
            form.populate_obj(protocoltype)
Robin Sonnabend's avatar
Robin Sonnabend committed
343
344
345
346
347
348
            db.session.add(protocoltype)
            db.session.commit()
            flash("Der Protokolltyp {} wurde angelegt.".format(protocoltype.name), "alert-success")
        return redirect(request.args.get("next") or url_for("list_types"))
    return render_template("type-new.html", form=form)

Robin Sonnabend's avatar
Robin Sonnabend committed
349
@app.route("/type/edit/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
350
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
351
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
352
@require_private_view_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
353
def edit_type(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
354
355
356
357
358
359
360
361
    user = current_user()
    form = ProtocolTypeForm(obj=protocoltype)
    if form.validate_on_submit():
        if form.private_group.data not in user.groups:
            flash("Du kannst keinen internen Protokolltypen anlegen, zu dem du selbst keinen Zugang hast.", "alert-error")
        else:
            form.populate_obj(protocoltype)
            db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
362
            return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id))
Robin Sonnabend's avatar
Robin Sonnabend committed
363
364
    return render_template("type-edit.html", form=form, protocoltype=protocoltype)

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

Robin Sonnabend's avatar
Robin Sonnabend committed
377
@app.route("/type/delete/<int:protocoltype_id>")
378
@login_required
379
@group_required(config.ADMIN_GROUP)
Robin Sonnabend's avatar
Robin Sonnabend committed
380
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
381
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
382
def delete_type(protocoltype):
383
384
385
386
387
388
    name = protocoltype.name
    db.session.delete(protocoltype) 
    db.session.commit()
    flash("Der Protokolltype {} wurde gelöscht.".format(name), "alert-success")
    return redirect(request.args.get("next") or url_for("list_types"))

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

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

Robin Sonnabend's avatar
Robin Sonnabend committed
415
@app.route("/type/reminder/delete/<int:meetingreminder_id>")
416
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
417
@db_lookup(MeetingReminder)
Robin Sonnabend's avatar
Robin Sonnabend committed
418
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
419
420
421
def delete_reminder(meetingreminder):
    protocoltype = meetingreminder.protocoltype
    db.session.delete(meetingreminder)
422
    db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
423
    return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id))
424

Robin Sonnabend's avatar
Robin Sonnabend committed
425
@app.route("/type/tops/new/<int:protocoltype_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
426
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
427
@db_lookup(ProtocolType)
Robin Sonnabend's avatar
Robin Sonnabend committed
428
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
429
def new_default_top(protocoltype):
Robin Sonnabend's avatar
Robin Sonnabend committed
430
431
    form = DefaultTopForm()
    if form.validate_on_submit():
432
433
        defaulttop = DefaultTOP(protocoltype_id=protocoltype.id)
        form.populate_obj(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
434
        db.session.add(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
435
        db.session.commit()
436
437
438
        for protocol in protocoltype.protocols:
            if not protocol.done:
                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()
Robin Sonnabend's avatar
Robin Sonnabend committed
442
        flash("Der Standard-TOP {} wurde für dem Protokolltyp {} hinzugefügt.".format(defaulttop.name, protocoltype.name), "alert-success")
Robin Sonnabend's avatar
Robin Sonnabend committed
443
444
445
        return redirect(request.args.get("next") or url_for("index"))
    return render_template("default-top-new.html", form=form, protocoltype=protocoltype)

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

Robin Sonnabend's avatar
Robin Sonnabend committed
458
@app.route("/type/tops/delete/<int:defaulttop_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
459
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
460
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
461
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
462
463
def delete_default_top(defaulttop):
    db.session.delete(defaulttop)
Robin Sonnabend's avatar
Robin Sonnabend committed
464
    db.session.commit()
465
    return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=defaulttop.protocoltype.id))
Robin Sonnabend's avatar
Robin Sonnabend committed
466

Robin Sonnabend's avatar
Robin Sonnabend committed
467
@app.route("/type/tops/move/<int:defaulttop_id>/<diff>/")
Robin Sonnabend's avatar
Robin Sonnabend committed
468
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
469
@db_lookup(DefaultTOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
470
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
471
def move_default_top(defaulttop, diff):
Robin Sonnabend's avatar
Robin Sonnabend committed
472
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
473
        defaulttop.number += int(diff)
Robin Sonnabend's avatar
Robin Sonnabend committed
474
475
476
        db.session.commit()
    except ValueError:
        flash("Die angegebene Differenz ist keine Zahl.", "alert-error")
Robin Sonnabend's avatar
Robin Sonnabend committed
477
    return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=defaulttop.protocoltype.id))
Robin Sonnabend's avatar
Robin Sonnabend committed
478
479

@app.route("/protocols/list")
480
481
482
def list_protocols():
    is_logged_in = check_login()
    user = current_user()
Robin Sonnabend's avatar
Robin Sonnabend committed
483
484
485
    protocoltype = None
    protocoltype_id = None
    try:
486
        protocoltype_id = int(request.args.get("protocoltype_id"))
Robin Sonnabend's avatar
Robin Sonnabend committed
487
488
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
489
    state_open = -1
Robin Sonnabend's avatar
Robin Sonnabend committed
490
    try:
Robin Sonnabend's avatar
Robin Sonnabend committed
491
        state_open = int(request.args.get("state_open"))
Robin Sonnabend's avatar
Robin Sonnabend committed
492
493
    except (ValueError, TypeError):
        pass
Robin Sonnabend's avatar
Robin Sonnabend committed
494
    search_term = request.args.get("search")
495
    protocoltypes = ProtocolType.get_public_protocoltypes(user, check_networks=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
496
    search_form = ProtocolSearchForm(protocoltypes)
Robin Sonnabend's avatar
Robin Sonnabend committed
497
    if protocoltype_id is not None:
498
        search_form.protocoltype_id.data = protocoltype_id
Robin Sonnabend's avatar
Robin Sonnabend committed
499
        protocoltype = ProtocolType.query.filter_by(id=protocoltype_id).first()
Robin Sonnabend's avatar
Robin Sonnabend committed
500
501
    if state_open is not None:
        search_form.state_open.data = state_open
Robin Sonnabend's avatar
Robin Sonnabend committed
502
503
504
    if search_term is not None:
        search_form.search.data = search_term
    protocol_query = Protocol.query
Robin Sonnabend's avatar
Robin Sonnabend committed
505
506
507
508
509
510
511
512
513
    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))
            ))
514
    protocols = [
Robin Sonnabend's avatar
Robin Sonnabend committed
515
        protocol for protocol in protocol_query.all()
516
        if protocol.protocoltype.has_public_view_right(user, check_networks=False)
517
    ]
Robin Sonnabend's avatar
Robin Sonnabend committed
518
519
520
521
522
523
524
525
526
527
528
529
530
    def _matches_search(content):
        content = content.lower()
        for search_term in search_terms:
            if search_term.lower() not in content:
                return False
        return True
    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
531
532
533
534
535
    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
536
537
    if state_open is not None and state_open != -1:
        protocol_done = bool(state_open)
Robin Sonnabend's avatar
Robin Sonnabend committed
538
539
        protocols = [
            protocol for protocol in protocols
540
            if (protocol.is_done() or False) == protocol_done
Robin Sonnabend's avatar
Robin Sonnabend committed
541
        ]
Robin Sonnabend's avatar
Robin Sonnabend committed
542
543
544
545
546
    if shall_search:
        protocols = [
            protocol for protocol in protocols
            if (protocol.protocoltype.has_private_view_right(user)
                and _matches_search(protocol.content_private))
547
            or (protocol.has_public_view_right(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
                and _matches_search(protocol.content_public))
        ]
        for protocol in protocols:
            content = protocol.content_private if protocol.protocoltype.has_private_view_right(user) else protocol.content_public
            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):
                    index_candidates = list(filter(lambda t: t[0] != -1, 
                        [(lower_line.find(term, last_index), term) for term in search_terms]))
                    if len(index_candidates) == 0:
                        parts.append((line[last_index:], False))
                        break
                    else:
                        new_index, term = min(index_candidates, key=lambda t: t[0])
                        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
                ]))
575
            search_results[protocol] = " …<br />\n".join(formatted_lines)
Robin Sonnabend's avatar
Robin Sonnabend committed
576
    protocols = sorted(protocols, key=lambda protocol: protocol.date, reverse=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
577
    page = _get_page()
Robin Sonnabend's avatar
Robin Sonnabend committed
578
579
    page_length = _get_page_length()
    page_count = int(math.ceil(len(protocols) / page_length))
Robin Sonnabend's avatar
Robin Sonnabend committed
580
581
    if page >= page_count:
        page = 0
Robin Sonnabend's avatar
Robin Sonnabend committed
582
583
    begin_index = page * page_length
    end_index = (page + 1) * page_length
584
    max_page_length_exp = math.ceil(math.log10(len(protocols))) if len(protocols) > 0 else 1
Robin Sonnabend's avatar
Robin Sonnabend committed
585
    protocols = protocols[begin_index:end_index]
Robin Sonnabend's avatar
Robin Sonnabend committed
586
    protocols_table = ProtocolsTable(protocols, search_results=search_results)
Robin Sonnabend's avatar
Robin Sonnabend committed
587
    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
588
589
590
591
592

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

@app.route("/protocol/show/<int:protocol_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
612
613
@db_lookup(Protocol)
def show_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
614
615
    user = current_user()
    errors_table = ErrorsTable(protocol.errors)
616
    if not protocol.protocoltype.has_public_view_right(user, check_networks=False):
617
618
619
        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
        if check_login():
            return redirect(url_for("index"))
620
        return redirect(request.args.get("next") or url_for("login", next=request.url))
Robin Sonnabend's avatar
Robin Sonnabend committed
621
622
    visible_documents = [
        document for document in protocol.documents
623
        if (not document.is_private and document.protocol.has_public_view_right(user))
Robin Sonnabend's avatar
Robin Sonnabend committed
624
625
        or (document.is_private and document.protocol.protocoltype.has_private_view_right(user))
    ]
626
    documents_table = DocumentsTable(visible_documents, protocol)
627
    document_upload_form = DocumentUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
628
    source_upload_form = KnownProtocolSourceUploadForm()
629
630
    time_diff = protocol.date - datetime.now().date()
    large_time_diff = not protocol.is_done() and time_diff.days > 0
631
632
633
    content_html = (protocol.content_html_private
        if protocol.has_private_view_right(user)
        else protocol.content_html_public)
634
635
636
    if content_html is not None:
        content_html = Markup(content_html)
    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
637

Robin Sonnabend's avatar
Robin Sonnabend committed
638
639
@app.route("/protocol/delete/<int:protocol_id>")
@login_required
640
@group_required(config.ADMIN_GROUP)
Robin Sonnabend's avatar
Robin Sonnabend committed
641
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
642
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
643
def delete_protocol(protocol):
644
    name = protocol.get_short_identifier()
Robin Sonnabend's avatar
Robin Sonnabend committed
645
    protocol.delete_orphan_todos()
Robin Sonnabend's avatar
Robin Sonnabend committed
646
647
648
649
650
    db.session.delete(protocol)
    db.session.commit()
    flash("Protokoll {} ist gelöscht.".format(name), "alert-success")
    return redirect(request.args.get("next") or url_for("list_protocols"))

Robin Sonnabend's avatar
Robin Sonnabend committed
651
@app.route("/protocol/etherpull/<int:protocol_id>")
Robin Sonnabend's avatar
Robin Sonnabend committed
652
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
653
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
654
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
655
def etherpull_protocol(protocol):
656
657
658
    if not config.ETHERPAD_ACTIVE:
        flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id))
Robin Sonnabend's avatar
Robin Sonnabend committed
659
    protocol.source = get_etherpad_text(protocol.get_identifier())
Robin Sonnabend's avatar
Robin Sonnabend committed
660
661
662
663
664
    db.session.commit()
    tasks.parse_protocol(protocol)
    flash("Das Protokoll wird kompiliert.", "alert-success")
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))

Robin Sonnabend's avatar
Robin Sonnabend committed
665
666
@app.route("/protocol/upload/known/<int:protocol_id>", methods=["POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
667
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
668
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
669
def upload_source_to_known_protocol(protocol):
670
    form = KnownProtocolSourceUploadForm()
Robin Sonnabend's avatar
Robin Sonnabend committed
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
    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")
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))

@app.route("/protocol/upload/new/", methods=["POST"])
@login_required
def upload_new_protocol():
    user = current_user()
691
    available_types = ProtocolType.get_modifiable_protocoltypes(user)
Robin Sonnabend's avatar
Robin Sonnabend committed
692
693
694
695
    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")
696
697
698
699
700
701
            return redirect(request.args.get("fail") or url_for("new_protocol"))
        file = form.source.data
        if file.filename == "":
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
            return redirect(request.args.get("fail") or url_for("new_protocol"))
        source = file.stream.read().decode("utf-8")
702
        protocoltype = ProtocolType.query.filter_by(id=form.protocoltype_id.data).first()
703
704
705
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Invalider Protokolltyp oder keine Rechte.", "alert-error")
            return redirect(request.args.get("fail") or url_for("new_protocol"))
706
        protocol = Protocol(protocoltype_id=protocoltype.id, source=source)
707
708
        db.session.add(protocol)
        db.session.commit()
709
710
711
        for local_top in protocol.create_localtops:
            db.session.add(local_top)
        db.session.commit()
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
        tasks.parse_protocol(protocol)
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
    return redirect(request.args.get("fail") or url_for("new_protocol"))

@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")
            return redirect(request.args.get("fail") or url_for("new_protocol"))
        file = form.file.data
        if file.filename == "":
            flash("Es wurde keine Datei ausgewählt.", "alert-error")
            return redirect(request.args.get("fail") or url_for("new_protocol"))
        filename = secure_filename(file.filename)
731
        protocoltype = ProtocolType.query.filter_by(id=form.protocoltype_id.data).first()
732
733
734
        if protocoltype is None or not protocoltype.has_modify_right(user):
            flash("Invalider Protokolltyp oder keine Rechte.", "alert-error")
            return redirect(request.args.get("fail") or url_for("new_protocol"))
735
        protocol = Protocol(protocoltype_id=protocoltype.id, date=datetime.now().date(), done=True)
736
737
        db.session.add(protocol)
        db.session.commit()
738
739
740
        for local_top in protocol.create_localtops:
            db.session.add(local_top)
        db.session.commit()
741
742
743
        document = Document(protocol_id=protocol.id, name=filename,
            filename="", is_compiled=False)
        form.populate_obj(document)
744
745
746
747
748
749
750
        db.session.add(document)
        db.session.commit()
        internal_filename = "{}-{}-{}".format(protocol.id, document.id, filename)
        document.filename = internal_filename
        file.save(os.path.join(config.DOCUMENTS_PATH, internal_filename))
        db.session.commit()
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
Robin Sonnabend's avatar
Robin Sonnabend committed
751
752
    return redirect(request.args.get("fail") or url_for("new_protocol"))

Administrator's avatar
Administrator committed
753
754
755
@app.route("/protocol/recompile/<int:protocol_id>")
@login_required
@group_required(config.ADMIN_GROUP)
Robin Sonnabend's avatar
Robin Sonnabend committed
756
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
757
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
758
def recompile_protocol(protocol):
Administrator's avatar
Administrator committed
759
760
    tasks.parse_protocol(protocol)
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
761

Robin Sonnabend's avatar
Robin Sonnabend committed
762
763
@app.route("/protocol/source/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
764
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
765
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
766
def get_protocol_source(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
767
    file_like = BytesIO(protocol.source.encode("utf-8"))
768
    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
769

Robin Sonnabend's avatar
Robin Sonnabend committed
770
771
@app.route("/protocol/template/<int:protocol_id>")
@login_required
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 get_protocol_template(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
775
    file_like = BytesIO(protocol.get_template().encode("utf-8"))
776
    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
777

Robin Sonnabend's avatar
Robin Sonnabend committed
778
779
@app.route("/protocol/etherpush/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
780
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
781
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
782
def etherpush_protocol(protocol):
783
784
785
    if not config.ETHERPAD_ACTIVE:
        flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id))
786
787
788
    if not protocol.is_done():
        tasks.set_etherpad_content(protocol)
    return redirect(request.args.get("next") or protocol.get_etherpad_link())
Robin Sonnabend's avatar
Robin Sonnabend committed
789

Robin Sonnabend's avatar
Robin Sonnabend committed
790
@app.route("/protocol/update/<int:protocol_id>", methods=["GET", "POST"])
Robin Sonnabend's avatar
Robin Sonnabend committed
791
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
792
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
793
@require_publish_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
794
def update_protocol(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
795
    upload_form = KnownProtocolSourceUploadForm()
796
    edit_form = generate_protocol_form(protocol)(obj=protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
797
798
    if edit_form.validate_on_submit():
        edit_form.populate_obj(protocol)
799
800
        for meta in protocol.metas:
            meta.value = getattr(edit_form.metas, meta.name).data
Robin Sonnabend's avatar
Robin Sonnabend committed
801
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
802
        tasks.push_tops_to_calendar(protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
803
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
804
805
    for meta in protocol.metas:
        getattr(edit_form.metas, meta.name).data = meta.value
Robin Sonnabend's avatar
Robin Sonnabend committed
806
    return render_template("protocol-update.html", upload_form=upload_form, edit_form=edit_form, protocol=protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
807

808
809
@app.route("/protocol/publish/<int:protocol_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
810
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
811
@require_publish_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
812
def publish_protocol(protocol):
813
814
815
816
    protocol.public = True
    db.session.commit()
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))

817
@app.route("/prococol/send/private/<int:protocol_id>")
818
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
819
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
820
@require_modify_right()
821
def send_protocol_private(protocol):
822
823
824
    if not config.MAIL_ACTIVE:
        flash("Die Mailfunktion ist nicht aktiviert.", "alert-error")
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id))
825
826
827
828
829
830
831
    tasks.send_protocol_private(protocol)
    flash("Das Protokoll wurde versandt.", "alert-success")
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))

@app.route("/prococol/send/public/<int:protocol_id>")
@login_required
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
832
@require_publish_right()
833
834
835
836
837
def send_protocol_public(protocol):
    if not config.MAIL_ACTIVE:
        flash("Die Mailfunktion ist nicht aktiviert.", "alert-error")
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id))
    tasks.send_protocol_public(protocol)
838
839
840
    flash("Das Protokoll wurde versandt.", "alert-success")
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))

841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
@app.route("/protocol/reminder/<int:protocol_id>")
@login_required
@db_lookup(Protocol)
@require_modify_right()
def send_protocol_reminder(protocol):
    if not config.MAIL_ACTIVE:
        flash("Die Mailfunktion ist nicht aktiviert.", "alert-error")
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id))
    meetingreminders = MeetingReminder.query.filter_by(protocoltype_id=protocol.protocoltype.id).all()
    if len(meetingreminders) == 0:
        flash("Für diesen Protokolltyp sind keine Einladungsmails konfiguriert.", "alert-error")
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id))
    day_difference = (protocol.date - datetime.now().date()).days
    past_reminders = [
        meetingreminder for meetingreminder in meetingreminders
        if meetingreminder.days_before > day_difference
    ]
    if len(past_reminders) == 0:
        flash("Bisher hätte keine Einladungsmail verschickt werden sollen, schicke letzte.", "alert-info")
        past_reminders = meetingreminders
    past_reminders = sorted(past_reminders, key=lambda r: r.days_before)
    choosen_reminder = past_reminders[0]
    tasks.send_reminder(choosen_reminder, protocol)
    flash("Einladungsmail ist versandt.", "alert-success")
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
        

Robin Sonnabend's avatar
Robin Sonnabend committed
868
869
@app.route("/protocol/tops/new/<int:protocol_id>", methods=["GET", "POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
870
@db_lookup(Protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
871
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
872
def new_top(protocol):
Robin Sonnabend's avatar
Robin Sonnabend committed
873
874
    form = TopForm()
    if form.validate_on_submit():
875
876
        top = TOP(protocol_id=protocol.id, planned=True)
        form.populate_obj(top)
Robin Sonnabend's avatar
Robin Sonnabend committed
877
878
        db.session.add(top)
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
879
        tasks.push_tops_to_calendar(top.protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
880
881
882
883
884
885
886
887
888
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
    else:
        current_numbers = list(map(lambda t: t.number, protocol.tops))
        suggested_number = get_first_unused_int(current_numbers)
        form.number.data = suggested_number
    return render_template("top-new.html", form=form, protocol=protocol)

@app.route("/protocol/top/edit/<int:top_id>", methods=["GET", "POST"])
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
889
@db_lookup(TOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
890
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
891
def edit_top(top):
Robin Sonnabend's avatar
Robin Sonnabend committed
892
893
894
895
    form = TopForm(obj=top)
    if form.validate_on_submit():
        form.populate_obj(top)
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
896
        tasks.push_tops_to_calendar(top.protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
897
898
899
900
901
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=top.protocol.id))
    return render_template("top-edit.html", form=form, top=top)

@app.route("/protocol/top/delete/<int:top_id>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
902
@db_lookup(TOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
903
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
904
def delete_top(top):
Robin Sonnabend's avatar
Robin Sonnabend committed
905
    name = top.name
Robin Sonnabend's avatar
Robin Sonnabend committed
906
    protocol = top.protocol
Robin Sonnabend's avatar
Robin Sonnabend committed
907
908
    db.session.delete(top)
    db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
909
    tasks.push_tops_to_calendar(protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
910
    flash("Der TOP {} wurde gelöscht.".format(name), "alert-success")
Robin Sonnabend's avatar
Robin Sonnabend committed
911
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
Robin Sonnabend's avatar
Robin Sonnabend committed
912
913
914

@app.route("/protocol/top/move/<int:top_id>/<diff>")
@login_required
Robin Sonnabend's avatar
Robin Sonnabend committed
915
@db_lookup(TOP)
Robin Sonnabend's avatar
Robin Sonnabend committed
916
@require_modify_right()
Robin Sonnabend's avatar
Robin Sonnabend committed
917
def move_top(top, diff):
Robin Sonnabend's avatar
Robin Sonnabend committed
918
919
920
    try:
        top.number += int(diff)
        db.session.commit()
Robin Sonnabend's avatar
Robin Sonnabend committed
921
        tasks.push_tops_to_calendar(top.protocol)
Robin Sonnabend's avatar
Robin Sonnabend committed
922
923
924
925
    except ValueError:
        flash("Die angegebene Differenz ist keine Zahl.", "alert-error")
    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=top.protocol.id))

926
927
928
929
930
931
932
933
934
935
936
937
@app.route("/protocol/localtop/edit/<int:localtop_id>", methods=["GET", "POST"])
@login_required
@db_lookup(LocalTOP)
@require_modify_right()
def edit_localtop(localtop):
    form = LocalTopForm(obj=localtop)
    if form.validate_on_submit():
        form.populate_obj(localtop)
        db.session.commit()
        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=localtop.protocol.id))
    return render_template("localtop-edit.html", form=form, localtop=localtop)