diff --git a/migrations/versions/24bd2198a626_.py b/migrations/versions/24bd2198a626_.py new file mode 100644 index 0000000000000000000000000000000000000000..78cbf6c47d5ac36c5915f530e72a5d7134e1ba8b --- /dev/null +++ b/migrations/versions/24bd2198a626_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 24bd2198a626 +Revises: 2e2682dfac21 +Create Date: 2017-02-24 17:20:07.135782 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '24bd2198a626' +down_revision = '2e2682dfac21' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('todos', sa.Column('number', sa.Integer(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('todos', 'number') + # ### end Alembic commands ### diff --git a/migrations/versions/2e2682dfac21_.py b/migrations/versions/2e2682dfac21_.py new file mode 100644 index 0000000000000000000000000000000000000000..60d7a0f5320d4f82c8f1d21a2349a31138cba552 --- /dev/null +++ b/migrations/versions/2e2682dfac21_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 2e2682dfac21 +Revises: aebae2c4523d +Create Date: 2017-02-24 16:31:01.729972 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '2e2682dfac21' +down_revision = 'aebae2c4523d' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('todos', sa.Column('is_id_fixed', sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('todos', 'is_id_fixed') + # ### end Alembic commands ### diff --git a/models/database.py b/models/database.py index 0402687db9337adb973927b65c10eb92a48af497..582c9ece0f1faad29f2f7f443d4586861988e6d1 100644 --- a/models/database.py +++ b/models/database.py @@ -102,17 +102,17 @@ class Protocol(db.Model): return Error(self.id, action, name, now, description) def fill_from_remarks(self, remarks): - new_date = datetime.strptime(remarks["Datum"].value, "%d.%m.%Y").date() + new_date = datetime.strptime(remarks["Datum"].value.strip(), "%d.%m.%Y").date() if self.date is not None: if new_date != self.date: raise DateNotMatchingException(original_date=self.date, protocol_date=new_date) else: self.date = new_date - self.start_time = datetime.strptime(remarks["Beginn"].value, "%H:%M").time() - self.end_time = datetime.strptime(remarks["Ende"].value, "%H:%M").time() - self.author = remarks["Autor"].value - self.participants = remarks["Anwesende"].value - self.location = remarks["Ort"].value + self.start_time = datetime.strptime(remarks["Beginn"].value.strip(), "%H:%M").time() + self.end_time = datetime.strptime(remarks["Ende"].value.strip(), "%H:%M").time() + self.author = remarks["Autor"].value.strip() + self.participants = remarks["Anwesende"].value.strip() + self.location = remarks["Ort"].value.strip() def is_done(self): return self.done @@ -160,6 +160,9 @@ class Protocol(db.Model): return public_candidates[0] return None + def get_template(self): + return render_template("protocol-template.txt", protocol=self) + def delete_orphan_todos(self): orphan_todos = [ todo for todo in self.todos @@ -244,22 +247,28 @@ def on_document_delete(mapper, connection, document): class Todo(db.Model): __tablename__ = "todos" id = db.Column(db.Integer, primary_key=True) + number = db.Column(db.Integer) who = db.Column(db.String) description = db.Column(db.String) tags = db.Column(db.String) done = db.Column(db.Boolean) + is_id_fixed = db.Column(db.Boolean, default=False) protocols = relationship("Protocol", secondary="todoprotocolassociations", backref="todos") - def __init__(self, who, description, tags, done): + def __init__(self, who, description, tags, done, number=None): self.who = who self.description = description self.tags = tags self.done = done + self.number = number def __repr__(self): - return "<Todo(id={}, who={}, description={}, tags={}, done={})>".format( - self.id, self.who, self.description, self.tags, self.done) + return "<Todo(id={}, number={}, who={}, description={}, tags={}, done={})>".format( + self.id, self.number, self.who, self.description, self.tags, self.done) + + def get_id(self): + return self.number if self.number is not None else self.id def get_first_protocol(self): candidates = sorted(self.protocols, key=lambda p: p.date) diff --git a/server.py b/server.py index 05c8d4172793c6a7f7b93e83e986e157462890cc..a9d1962a935e9ff0be8ce1139cb1e8f6af370c94 100755 --- a/server.py +++ b/server.py @@ -304,27 +304,12 @@ def new_protocol(): protocol = Protocol(protocoltype.id, form.date.data) db.session.add(protocol) db.session.commit() - return redirect(request.args.get("next") or url_for("list_protocols")) + return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) type_id = request.args.get("type_id") if type_id is not None: form.protocoltype.data = type_id return render_template("protocol-new.html", form=form, upload_form=upload_form, protocoltypes=protocoltypes) -@app.route("/protocol/edit/<int:protocol_id>", methods=["POST"]) -@login_required -def edit_protocol(protocol_id): - user = current_user() - protocol = Protocol.query.filter_by(id=protocol_id).first() - if protocol is None or not protocol.protocoltype.has_modify_right(user): - flash("Invalides Protokoll oder fehlende Zugriffsrechte.", "alert-error") - return redirect(request.args.get("next") or url_for("list_protocols")) - form = ProtocolForm(obj=protocol) - if form.validate_on_submit(): - form.populate_obj(protocol) - db.session.commit() - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) - return redirect(request.args.get("fail") or url_for("update_protocol", protocol_id=protocol.id)) - @app.route("/protocol/show/<int:protocol_id>") def show_protocol(protocol_id): user = current_user() @@ -439,7 +424,18 @@ def get_protocol_source(protocol_id): file_like = BytesIO(protocol.source.encode("utf-8")) return send_file(file_like, cache_timeout=1, as_attachment=True, attachment_filename="{}.txt".format(protocol.get_identifier())) -@app.route("/protocol/update/<int:protocol_id>") +@app.route("/protocol/template/<int:protocol_id>") +@login_required +def get_protocol_template(protocol_id): + user = current_user() + protocol = Protocol.query.filter_by(id=protocol_id).first() + if protocol is None or not protocol.protocoltype.has_modify_right(user): + flash("Invalides Protokoll oder keine Berechtigung.", "alert-error") + return redirect(request.args.get("next") or url_for("index")) + file_like = BytesIO(protocol.get_template().encode("utf-8")) + return send_file(file_like, cache_timeout=1, as_attachment=True, attachment_filename="{}-template.txt".format(protocol.get_identifier())) + +@app.route("/protocol/update/<int:protocol_id>", methods=["GET", "POST"]) @login_required def update_protocol(protocol_id): user = current_user() @@ -449,6 +445,10 @@ def update_protocol(protocol_id): return redirect(request.args.get("next") or url_for("index")) upload_form = KnownProtocolSourceUploadForm() edit_form = ProtocolForm(obj=protocol) + if edit_form.validate_on_submit(): + edit_form.populate_obj(protocol) + db.session.commit() + return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) return render_template("protocol-update.html", upload_form=upload_form, edit_form=edit_form, protocol=protocol) @app.route("/protocol/tops/new/<int:protocol_id>", methods=["GET", "POST"]) diff --git a/tasks.py b/tasks.py index 8a97791d59721a05bc21417cb514f070e332569c..7da102cba82a55100761c0cfe3ebf01f2887bb38 100644 --- a/tasks.py +++ b/tasks.py @@ -121,9 +121,9 @@ def parse_protocol_async(protocol_id, encoded_kwargs): who = todo_tag.values[0] what = todo_tag.values[1] todo = None + field_id = None for other_field in todo_tag.values[2:]: if other_field.startswith(ID_FIELD_BEGINNING): - field_id = 0 try: field_id = int(other_field[len(ID_FIELD_BEGINNING):]) except ValueError: @@ -133,10 +133,27 @@ def parse_protocol_async(protocol_id, encoded_kwargs): db.session.add(error) db.session.commit() return - todo = Todo.query.filter_by(id=field_id).first() + todo = Todo.query.filter_by(number=field_id).first() + who = who.strip() + what = what.strip() if todo is None: - todo = Todo(who=who, description=what, tags="", done=False) - db.session.add(todo) + if field_id is not None: + candidate = Todo.query.filter_by(who=who, description=what, number=None).first() + if candidate is None: + candidate = Todo.query.filter_by(description=what, number=None).first() + if candidate is not None: + candidate.number = field_id + todo = candidate + else: + todo = Todo(who=who, description=what, tags="", done=False) + todo.number = field_id + else: + candidate = Todo.query.filter_by(who=who, description=what).first() + if candidate is not None: + todo = candidate + else: + todo = Todo(who=who, description=what, tags="", done=False) + db.session.add(todo) todo.protocols.append(protocol) todo_tags_internal = todo.tags.split(";") for other_field in todo_tag.values[2:]: diff --git a/templates/protocol-show.html b/templates/protocol-show.html index 1bb9db94ff79269fbf8784d4d5a7845109d5b50a..20dd1a6983112fb4e53cac72744bfe652cb9364b 100644 --- a/templates/protocol-show.html +++ b/templates/protocol-show.html @@ -12,11 +12,14 @@ <div class="container"> <div class="btn-group"> {% if has_modify_right %} - <a class="btn {% if protocol.source is none %}btn-primary{% else %}btn-default{% endif %}" href="{{url_for("etherpull_protocol", protocol_id=protocol.id)}}">From etherpad</a> + <a class="btn {% if protocol.source is none %}btn-primary{% else %}btn-default{% endif %}" href="{{url_for("etherpull_protocol", protocol_id=protocol.id)}}">Aus Etherpad</a> {% if protocol.source is not none %} <a class="btn btn-primary" href="{{url_for("get_protocol_source", protocol_id=protocol.id)}}">Download Quelltext</a> {% endif %} <a class="btn {% if protocol.is_done() %}btn-success{% else %}btn-default{% endif %}" href="{{url_for("update_protocol", protocol_id=protocol.id)}}">Protokoll editieren</a> + {% if not protocol.is_done() %} + <a class="btn btn-default" href="{{url_for("get_protocol_template", protocol_id=protocol.id)}}">Vorlage herunterladen</a> + {% endif %} <a class="btn btn-default" href="{{protocol.get_etherpad_link()}}" target="_blank">Etherpad</a> <a class="btn btn-default" href="{{url_for("show_type", type_id=protocol.protocoltype.id)}}">Typ</a> <a class="btn btn-danger" href="{{url_for("delete_protocol", protocol_id=protocol.id)}}" onclick="return confirm('Bist du dir sicher, dass du das Protokoll {{protocol.get_identifier()}} löschen möchtest?');">Löschen</a> diff --git a/templates/protocol-template.txt b/templates/protocol-template.txt new file mode 100644 index 0000000000000000000000000000000000000000..5818a73b3a1d4b5b2f770a788858d7dc75127e43 --- /dev/null +++ b/templates/protocol-template.txt @@ -0,0 +1,40 @@ +#Datum;{{protocol.date|datify}} +#Anwesende; +#Beginn; +#Ende; +#Autor; +#Ort; + +{% macro render_top(top) %} +{TOP {{top.name}} + {% if top.name == "Todos" %} + {% if protocol.todos|length > 0 %} + {% for todo in protocol.todos %} + [todo;{{todo.who}};{{todo.description}};id {{todo.get_number()}}]; + {% endfor %} + {% else %} + + {% endif %} + {% else %} + + {% endif %} +} +{% endmacro -%} + +{% if not protocol.has_nonplanned_tops() %} + {% for default_top in protocol.protocoltype.default_tops %} + {% if not default_top.is_at_end() %} + {{-render_top(default_top)}} + {% endif %} + {% endfor %} +{% endif %} +{% for top in protocol.tops %} + {{-render_top(top)}} +{% endfor %} +{% if not protocol.has_nonplanned_tops() %} + {% for default_top in protocol.protocoltype.default_tops %} + {% if default_top.is_at_end() %} + {{-render_top(default_top)}} + {% endif %} + {% endfor %} +{% endif %} diff --git a/templates/protocol-tops-include.html b/templates/protocol-tops-include.html index ca9a0a63c76463789e1f57e0ee5fb7de27911b04..c7302ed1d7e5b88b39568d8c662014c17e266c32 100644 --- a/templates/protocol-tops-include.html +++ b/templates/protocol-tops-include.html @@ -9,10 +9,10 @@ {% for top in protocol.tops %} <li> {{top.name}} - {% if has_private_view_right %} + {% if not protocol.is_done() and has_private_view_right %} ({{top.number}}) {% endif %} - {% if has_modify_right %} + {% if not protocol.is_done() and has_modify_right %} <a href="{{url_for('edit_top', top_id=top.id)}}">Ändern</a> <a href="{{url_for('move_top', top_id=top.id, diff=1)}}">Runter</a> <a href="{{url_for('move_top', top_id=top.id, diff=-1)}}">Hoch</a> diff --git a/templates/protocol-update.html b/templates/protocol-update.html index 28e8c47a5145b953808427c0c03c861699766e56..749be3081de739043b4f380dbe37fb6fda8572e3 100644 --- a/templates/protocol-update.html +++ b/templates/protocol-update.html @@ -6,7 +6,7 @@ <div class="container"> <div class="row"> <div id="left-column" class="col-lg-6"> - {{render_form(edit_form, action_url=url_for("edit_protocol", protocol_id=protocol.id), action_text="Ändern")}} + {{render_form(edit_form, action_url=url_for("update_protocol", protocol_id=protocol.id), action_text="Ändern")}} </div> <div id="right-column" class="col-lg-6"> <h3>Protokoll herunterladen</h3> diff --git a/views/forms.py b/views/forms.py index a8303bf4448c4ac268cc7ee3df7c9b0a93ec9900..3392ba286c08c62f7d1f1899dbc88c33cdd80bd9 100644 --- a/views/forms.py +++ b/views/forms.py @@ -1,6 +1,6 @@ from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, BooleanField, DateField, HiddenField, IntegerField, SelectField, FileField, DateTimeField -from wtforms.validators import InputRequired +from wtforms.validators import InputRequired, Optional class LoginForm(FlaskForm): username = StringField("Benutzer", validators=[InputRequired("Bitte gib deinen Benutzernamen ein.")]) @@ -50,8 +50,8 @@ class NewProtocolSourceUploadForm(FlaskForm): class ProtocolForm(FlaskForm): date = DateField("Datum", validators=[InputRequired("Bitte gib das Datum des Protkolls an.")], format="%d.%m.%Y") - start_time = DateTimeField("Beginn", format="%H:%M") - end_time = DateTimeField("Ende", format="%H:%M") + start_time = DateTimeField("Beginn", format="%H:%M", validators=[Optional()]) + end_time = DateTimeField("Ende", format="%H:%M", validators=[Optional()]) location = StringField("Ort") author = StringField("Protokollant") participants = StringField("Anwesende") diff --git a/views/tables.py b/views/tables.py index 77013423346c7e8a508bdb4b628a235d3fbd5e76..a03970ec8890ebda49d571b3748b1efd5e1d6be7 100644 --- a/views/tables.py +++ b/views/tables.py @@ -177,11 +177,12 @@ class TodosTable(Table): super().__init__("Todos", todos) def headers(self): - return ["Status", "Sitzung", "Name", "Aufgabe"] + return ["ID", "Status", "Sitzung", "Name", "Aufgabe"] def row(self, todo): protocol = todo.get_first_protocol() return [ + 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 "", todo.who,