diff --git a/server.py b/server.py index 029eedacf61ae3623f7aab88d81022db52aa7569..6584081df16bd055715b4f1c4eaf334b75a5cba1 100755 --- a/server.py +++ b/server.py @@ -20,10 +20,10 @@ import math import config from shared import db, date_filter, datetime_filter, date_filter_long, time_filter, ldap_manager, security_manager, current_user, check_login, login_required, group_required, class_filter -from utils import is_past, mail_manager, url_manager, get_first_unused_int, set_etherpad_text, get_etherpad_text, split_terms +from utils import is_past, mail_manager, url_manager, get_first_unused_int, set_etherpad_text, get_etherpad_text, split_terms, optional_int_arg from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error -from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, ProtocolForm, TopForm, SearchForm, NewProtocolFileUploadForm -from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable +from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, ProtocolForm, TopForm, SearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm +from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable app = Flask(__name__) app.config.from_object(config) @@ -718,6 +718,11 @@ def list_todos(): todo for todo in todos if search_term.lower() in todo.description.lower() ] + def _sort_key(todo): + first_protocol = todo.get_first_protocol() + result = (not todo.done, first_protocol.date if first_protocol is not None else datetime.now().date()) + return result + todos = sorted(todos, key=_sort_key, reverse=True) page = _get_page() page_count = int(math.ceil(len(todos) / config.PAGE_LENGTH)) if page >= page_count: @@ -728,6 +733,88 @@ def list_todos(): todos_table = TodosTable(todos) return render_template("todos-list.html", todos=todos, todos_table=todos_table, search_form=search_form, page=page, page_count=page_count, page_diff=config.PAGE_DIFF, protocoltype_id=protocoltype_id, search_term=search_term) +@app.route("/todo/new", methods=["GET", "POST"]) +@login_required +def new_todo(): + user = current_user() + protocoltype_id = optional_int_arg("type_id") + protocol_id = optional_int_arg("protocol_id") + protocoltype = ProtocolType.query.filter_by(id=protocoltype_id).first() + protocol = Protocol.query.filter_by(id=protocol_id).first() + if protocoltype is not None and protocol is not None: + if protocol.protocoltype != protocoltype: + flash("Ungültige Protokoll-Typ-Kombination", "alert-error") + return redirect(request.args.get("next") or url_for("index")) + if protocoltype is None and protocol is not None: + protocoltype = protocol.protocoltype + protocoltypes = ProtocolType.get_modifiable_protocoltypes(user) + form = NewTodoForm(protocoltypes) + if form.validate_on_submit(): + added_protocoltype = ProtocolType.query.filter_by(id=form.protocoltype_id.data).first() + if added_protocoltype is None or not added_protocoltype.has_modify_right(user): + flash("Invalider Protokolltyp.") + return redirect(request.args.get("next") or url_for("index")) + todo = Todo(type_id=form.protocoltype_id.data, who=form.who.data, + description=form.description.data, tags=form.tags.data, + done=form.done.data) + if protocol is not None: + todo.protocols.append(protocol) + db.session.add(todo) + db.session.commit() + todo.number = todo.id + db.session.commit() + flash("Todo wurde angelegt.", "alert-success") + if protocol is not None: + return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) + else: + return redirect(request.args.get("next") or url_for("list_todos", protocoltype_id=protocoltype_id)) + else: + if protocoltype is not None: + form.protocoltype_id.data = protocoltype.id + return render_template("todo-new.html", form=form, protocol=protocol, protocoltype=protocoltype) + +@app.route("/todo/edit/<int:todo_id>", methods=["GET", "POST"]) +@login_required +def edit_todo(todo_id): + user = current_user() + todo = Todo.query.filter_by(id=todo_id).first() + if todo is None or not todo.protocoltype.has_modify_right(user): + flash("Invalides Todo oder unzureichende Berechtigung.", "alert-error") + return redirect(request.args.get("next") or url_for("index")) + form = TodoForm(obj=todo) + if form.validate_on_submit(): + form.populate_obj(todo) + db.session.commit() + return redirect(request.args.get("next") or url_for("list_todos", protocoltype=todo.protocoltype.id)) + return render_template("todo-edit.html", form=form, todo=todo) + +@app.route("/todo/show/<int:todo_id>") +@login_required +def show_todo(todo_id): + user = current_user() + todo = Todo.query.filter_by(id=todo_id).first() + if todo is None or not todo.protocoltype.has_private_view_right(user): + flash("Invalides Todo oder unzureichende Berechtigung.", "alert-error") + return redirect(request.args.get("next") or url_for("index")) + todo_table = TodoTable(todo) + return render_template("todo-show.html", todo=todo, todo_table=todo_table) + +@app.route("/todo/delete/<int:todo_id>") +@login_required +def delete_todo(todo_id): + user = current_user() + todo = Todo.query.filter_by(id=todo_id).first() + if todo is None or not todo.protocoltype.has_private_view_right(user): + flash("Invalides Todo oder unzureichende Berechtigung.", "alert-error") + return redirect(request.args.get("next") or url_for("index")) + type_id = todo.protocoltype.id + db.session.delete(todo) + db.session.commit() + flash("Todo gelöscht.", "alert-success") + return redirect(request.args.get("next") or url_for("list_todos", protocoltype=type_id)) + + + @app.route("/decisions/list") def list_decisions(): is_logged_In = check_login() diff --git a/templates/protocol-show.html b/templates/protocol-show.html index d9f75865ece24925ae32624a490f8c8e1cc9bc96..f2aef05326560bdc24bb56ca7c21d1873cc11d54 100644 --- a/templates/protocol-show.html +++ b/templates/protocol-show.html @@ -2,7 +2,7 @@ {% from "macros.html" import render_table, render_form %} {% block title %}Protokoll{% endblock %} -{% set loggedin = check_login() %} +{% set logged_in = check_login() %} {% set user = current_user() %} {% set has_public_view_right = protocol.protocoltype.has_public_view_right(user) %} {% set has_private_view_right = protocol.protocoltype.has_private_view_right(user) %} @@ -69,7 +69,7 @@ </div> <div id="right-column" class="col-lg-6"> {% if protocol.is_done() and has_public_view_right and logged_in %} - <h3>Todos dieser Sitzung <a href="{{url_for("list_todos")}}">Aktuelle Todos</a></h3> + <h3>Todos dieser Sitzung <a href="{{url_for("list_todos")}}">Aktuelle Todos</a> <a href="{{url_for("new_todo", protocol_id=protocol.id)}}">Neu</a></h3> <ul> {% if protocol.get_originating_todos()|length > 0 %} {% for todo in protocol.get_originating_todos() %} diff --git a/templates/protocol-template.txt b/templates/protocol-template.txt index 5818a73b3a1d4b5b2f770a788858d7dc75127e43..4f1516f532d83cb3dab051e8933c94fd51888d26 100644 --- a/templates/protocol-template.txt +++ b/templates/protocol-template.txt @@ -10,7 +10,7 @@ {% if top.name == "Todos" %} {% if protocol.todos|length > 0 %} {% for todo in protocol.todos %} - [todo;{{todo.who}};{{todo.description}};id {{todo.get_number()}}]; + [todo;{{todo.who}};{{todo.description}};id {{todo.get_id()}}]; {% endfor %} {% else %} diff --git a/templates/todo-edit.html b/templates/todo-edit.html new file mode 100644 index 0000000000000000000000000000000000000000..13a2ba231cd3f9a6c1bb9ddf2965b068a6454c31 --- /dev/null +++ b/templates/todo-edit.html @@ -0,0 +1,9 @@ +{% extends "layout.html" %} +{% from "macros.html" import render_form %} +{% block title %}Todo ändern{% endblock %} + +{% block content %} +<div class="container"> + {{render_form(form, action_url=url_for("edit_todo", todo_id=todo.id), action_text="Ändern")}} +</div> +{% endblock %} diff --git a/templates/todo-new.html b/templates/todo-new.html new file mode 100644 index 0000000000000000000000000000000000000000..348f4a495568fc8237ac416956d2e6802b4e9d85 --- /dev/null +++ b/templates/todo-new.html @@ -0,0 +1,9 @@ +{% extends "layout.html" %} +{% from "macros.html" import render_form %} +{% block title %}Todo anlegen{% endblock %} + +{% block content %} +<div class="container"> + {{render_form(form, action_url=url_for("new_todo", protocol_id=protocol.id, protocoltype_id=protocoltype.id), action_text="Anlegen")}} +</div> +{% endblock %} diff --git a/templates/todo-show.html b/templates/todo-show.html new file mode 100644 index 0000000000000000000000000000000000000000..40ea947ccd79b41eacfde82179b2016447f0f579 --- /dev/null +++ b/templates/todo-show.html @@ -0,0 +1,9 @@ +{% extends "layout.html" %} +{% from "macros.html" import render_single_table %} +{% block title %}Todo{% endblock %} + +{% block content %} +<div class="container"> + {{render_single_table(todo_table)}} +</div> +{% endblock %} diff --git a/utils.py b/utils.py index 7cd92045111a0662fbe34e0ee18afcc73f686732..d98df8186029fcac15db567db3e92862d0498762 100644 --- a/utils.py +++ b/utils.py @@ -1,4 +1,4 @@ -from flask import render_template +from flask import render_template, request import random import string @@ -152,3 +152,9 @@ def split_terms(text, quote_chars="\"'", separators=" \t\n"): if len(current_term) > 0: terms.append(current_term) return terms + +def optional_int_arg(name): + try: + return int(request.args.get(name)) + except (ValueError, TypeError): + return None diff --git a/views/forms.py b/views/forms.py index 1c579e8edb375ae8e668f874a89b0717980b43f4..9a5bb77b311c4f4d22043aebf9721e9e0a837f09 100644 --- a/views/forms.py +++ b/views/forms.py @@ -4,6 +4,12 @@ from wtforms.validators import InputRequired, Optional import config +def get_protocoltype_choices(protocoltypes, add_all=True): + choices = [(protocoltype.id, protocoltype.short_name) for protocoltype in protocoltypes] + if add_all: + choices.insert(0, (-1, "Alle")) + return choices + class LoginForm(FlaskForm): username = StringField("Benutzer", validators=[InputRequired("Bitte gib deinen Benutzernamen ein.")]) password = PasswordField("Passwort", validators=[InputRequired("Bitte gib dein Passwort ein.")]) @@ -39,7 +45,7 @@ class NewProtocolForm(FlaskForm): def __init__(self, protocoltypes, **kwargs): super().__init__(**kwargs) - self.protocoltype.choices = [(protocoltype.id, protocoltype.short_name) for protocoltype in protocoltypes] + self.protocoltype.choices = get_protocoltype_choices(protocoltypes, add_all=False) class DocumentUploadForm(FlaskForm): document = FileField("Datei") @@ -54,7 +60,7 @@ class NewProtocolSourceUploadForm(FlaskForm): def __init__(self, protocoltypes, **kwargs): super().__init__(**kwargs) - self.protocoltype.choices = [(protocoltype.id, protocoltype.short_name) for protocoltype in protocoltypes] + self.protocoltype.choices = get_protocoltype_choices(protocoltypes, add_all=False) class NewProtocolFileUploadForm(FlaskForm): file = FileField("Datei") @@ -63,7 +69,7 @@ class NewProtocolFileUploadForm(FlaskForm): def __init__(self, protocoltypes, **kwargs): super().__init__(**kwargs) - self.protocoltype.choices = [(protocoltype.id, protocoltype.short_name) for protocoltype in protocoltypes] + self.protocoltype.choices = get_protocoltype_choices(protocoltypes, add_all=False) class ProtocolForm(FlaskForm): date = DateField("Datum", validators=[InputRequired("Bitte gib das Datum des Protkolls an.")], format="%d.%m.%Y") @@ -84,6 +90,21 @@ class SearchForm(FlaskForm): def __init__(self, protocoltypes, **kwargs): super().__init__(**kwargs) - choices = [(protocoltype.id, protocoltype.short_name) for protocoltype in protocoltypes] - choices.insert(0, (-1, "Alle")) - self.protocoltype.choices = choices + self.protocoltype.choices = get_protocoltype_choices(protocoltypes) + +class NewTodoForm(FlaskForm): + protocoltype_id = SelectField("Typ", choices=[], coerce=int) + who = StringField("Person", validators=[InputRequired("Bitte gib an, wer das Todo erledigen soll.")]) + description = StringField("Aufgabe", validators=[InputRequired("Bitte gib an, was erledigt werden soll.")]) + tags = StringField("Weitere Tags") + done = BooleanField("Erledigt") + + def __init__(self, protocoltypes, **kwargs): + super().__init__(**kwargs) + self.protocoltype_id.choices = get_protocoltype_choices(protocoltypes, add_all=False) + +class TodoForm(FlaskForm): + who = StringField("Person", validators=[InputRequired("Bitte gib an, wer das Todo erledigen soll.")]) + description = StringField("Aufgabe", validators=[InputRequired("Bitte gib an, was erledigt werden soll.")]) + tags = StringField("Weitere Tags") + done = BooleanField("Erledigt") diff --git a/views/tables.py b/views/tables.py index ce3389f5e2375b570527bfd0ba533f385c5ff33d..0cc7c3069ceeb9ff504ba664740e751d599eeb07 100644 --- a/views/tables.py +++ b/views/tables.py @@ -195,19 +195,57 @@ class ErrorsTable(Table): class TodosTable(Table): def __init__(self, todos): - super().__init__("Todos", todos) + super().__init__("Todos", todos, newlink=url_for("new_todo")) def headers(self): - return ["Status", "Sitzung", "Name", "Aufgabe"] + return ["ID", "Status", "Sitzung", "Name", "Aufgabe", ""] def row(self, todo): + user = current_user() protocol = todo.get_first_protocol() - return [ + row = [ + Table.link(url_for("show_todo", todo_id=todo.id), todo.get_id()), todo.get_state(), - Table.link(url_for("show_protocol", protocol_id=protocol.id), protocol.get_identifier()) if protocol is not None else "", + Table.link(url_for("show_protocol", protocol_id=protocol.id), protocol.get_identifier()) + if protocol is not None + else Table.link(url_for("list_protocols", protocoltype=todo.protocoltype.id), todo.protocoltype.short_name), todo.who, - todo.description + todo.description, ] + if todo.protocoltype.has_modify_right(user): + row.append(Table.link(url_for("edit_todo", todo_id=todo.id), "Ändern")) + else: + row.append("") + return row + +class TodoTable(SingleValueTable): + def __init__(self, todo): + super().__init__("Todo", todo) + + def headers(self): + return ["ID", "Status", "Sitzung", "Name", "Aufgabe", "Tags", ""] + + def row(self): + user = current_user() + protocol = self.value.get_first_protocol() + row = [ + self.value.get_id(), + self.value.get_state_plain(), + Table.link(url_for("show_protocol", protocol_id=protocol.id), protocol.get_identifier()) + if protocol is not None + else Table.link(url_for("list_protocols", protocolttype=self.value.protocoltype.id), self.value.protocoltype.short_name), + self.value.who, + self.value.description, + self.value.tags + ] + if self.value.protocoltype.has_modify_right(user): + row.append(Table.concat([ + Table.link(url_for("edit_todo", todo_id=self.value.id), "Ändern"), + Table.link(url_for("delete_todo", todo_id=self.value.id), "Löschen", confirm="Bist du dir sicher, dass du das Todo löschen willst?") + ])) + else: + row.append("") + return row class DecisionsTable(Table): def __init__(self, decisions):