Commit 2ba18b3f authored by Robin Sonnabend's avatar Robin Sonnabend

Pull from etherpad

parent 3b3ae5eb
"""empty message
Revision ID: 97cf1913e60d
Revises: bbc1782c0999
Create Date: 2017-02-22 23:36:29.467493
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '97cf1913e60d'
down_revision = 'bbc1782c0999'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('tops', sa.Column('number', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('tops', 'number')
# ### end Alembic commands ###
"""empty message
Revision ID: bbc1782c0999
Revises: 162da8aeeb71
Create Date: 2017-02-22 23:36:11.613892
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'bbc1782c0999'
down_revision = '162da8aeeb71'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('tops', 'number')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('tops', sa.Column('number', sa.VARCHAR(), autoincrement=False, nullable=True))
# ### end Alembic commands ###
from flask import render_template, send_file, url_for, redirect, flash, request
from datetime import datetime, date, timedelta
import time
from datetime import datetime, time, date, timedelta
import math
from shared import db
......@@ -11,6 +10,8 @@ from utils import random_string, url_manager
from sqlalchemy.orm import relationship, backref
import config
class ProtocolType(db.Model):
__tablename__ = "protocoltypes"
id = db.Column(db.Integer, primary_key=True)
......@@ -79,7 +80,7 @@ class Protocol(db.Model):
documents = relationship("Document", backref=backref("protocol"), cascade="all, delete-orphan", order_by="Document.is_compiled")
errors = relationship("Error", backref=backref("protocol"), cascade="all, delete-orphan", order_by="Error.id")
def __init__(self, protocoltype_id, date, source=None, start_time=None, end_time=None, author=None, participants=None, location=None):
def __init__(self, protocoltype_id, date, source=None, start_time=None, end_time=None, author=None, participants=None, location=None, done=False):
self.protocoltype_id = protocoltype_id
self.date = date
self.source = source
......@@ -88,6 +89,7 @@ class Protocol(db.Model):
self.author = author
self.participants = participants
self.location = location
self.done = done
def __repr__(self):
return "<Protocol(id={}, protocoltype_id={})>".format(
......@@ -99,8 +101,8 @@ class Protocol(db.Model):
def fill_from_remarks(self, remarks):
self.date = datetime.strptime(remarks["Datum"].value, "%d.%m.%Y")
self.start_time = time.strptime(remarks["Beginn"].value, "%H:%M")
self.end_time = time.strptime(remarks["Ende"].value, "%H:%M")
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
......@@ -108,6 +110,22 @@ class Protocol(db.Model):
def is_done(self):
return self.done
def get_identifier(self):
return "{}-{}".format(
self.protocoltype.short_name.lower(),
self.date.strftime("%y-%m-%d"))
def get_etherpad_link(self):
return config.ETHERPAD_URL + self.get_identifier()
def get_etherpad_source_link(self):
return self.get_etherpad_link() + "/export/txt"
def has_nonplanned_tops(self):
return len([top for top in self.tops if not top.planned]) > 0
def get_originating_todos(self):
return [todo for todo in self.todos if self == todo.get_first_protocol()]
class DefaultTOP(db.Model):
__tablename__ = "defaulttops"
......@@ -133,7 +151,7 @@ class TOP(db.Model):
id = db.Column(db.Integer, primary_key=True)
protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
name = db.Column(db.String)
number = db.Column(db.String)
number = db.Column(db.Integer)
planned = db.Column(db.Boolean)
def __init__(self, protocol_id, name, number, planned):
......@@ -184,6 +202,24 @@ class Todo(db.Model):
return "<Todo(id={}, who={}, description={}, tags={}, done={})>".format(
self.id, self.who, self.description, self.tags, self.done)
def get_first_protocol(self):
candidates = sorted(self.protocols, key=lambda p: p.date)
if len(candidates) == 0:
return None
return candidates[0]
def get_state(self):
return "[Erledigt]" if self.done else "[Offen]"
def render_html(self):
parts = [
self.get_state(),
"<strong>{}:</strong>".format(self.who),
self.description
]
return " ".join(parts)
class TodoProtocolAssociation(db.Model):
__tablename__ = "todoprotocolassociations"
todo_id = db.Column(db.Integer, db.ForeignKey("todos.id"), primary_key=True)
......
......@@ -221,6 +221,9 @@ class Remark(Element):
level = 0
print("{}remark: {}: {}".format(" " * level, self.name, self.value))
def get_tags(self, tags):
return tags
@staticmethod
def parse(match, current, linenumber=None):
linenumber = Element.parse_inner(match, current, linenumber)
......
......@@ -29,6 +29,7 @@ pytz==2016.10
PyYAML==3.12
redis==2.10.5
regex==2017.2.8
requests==2.13.0
six==1.10.0
SQLAlchemy==1.1.5
vine==1.1.3
......
......@@ -2,19 +2,21 @@
import locale
locale.setlocale(locale.LC_TIME, "de_DE.utf8")
from flask import Flask, g, current_app, request, session, flash, redirect, url_for, abort, render_template, Response
from flask import Flask, g, current_app, request, session, flash, redirect, url_for, abort, render_template, Response, send_file
from flask_script import Manager, prompt
from flask_migrate import Migrate, MigrateCommand
#from flask_socketio import SocketIO
from celery import Celery
from functools import wraps
import requests
from io import StringIO, BytesIO
import config
from shared import db, date_filter, datetime_filter, ldap_manager, security_manager
from shared import db, date_filter, datetime_filter, date_filter_long, time_filter, ldap_manager, security_manager
from utils import is_past, mail_manager, url_manager
from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error
from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm
from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable
from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm
from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable
app = Flask(__name__)
app.config.from_object(config)
......@@ -38,6 +40,8 @@ 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
app.jinja_env.filters["timify"] = time_filter
app.jinja_env.filters["datify_long"] = date_filter_long
app.jinja_env.filters["url_complete"] = url_manager.complete
app.jinja_env.tests["auth_valid"] = security_manager.check_user
......@@ -290,9 +294,9 @@ def move_default_top(type_id, top_id, diff):
default_top.number += int(diff)
db.session.commit()
return redirect(request.args.get("next") or url_for("show_type", type_id=protocoltype.id))
@app.route("/protocol/list")
@app.route("/protocols/list")
def list_protocols():
is_logged_in = check_login()
user = current_user()
......@@ -302,9 +306,87 @@ def list_protocols():
or (is_logged_in and (
protocol.protocoltype.public_group in user.groups
or protocol.protocoltype.private_group in user.groups))]
# TODO: sort by date and paginate
protocols_table = ProtocolsTable(protocols)
return render_template("protocols-list.html", protocols=protocols, protocols_table=protocols_table)
@app.route("/protocol/new", methods=["GET", "POST"])
@login_required
def new_protocol():
user = current_user()
protocoltypes = ProtocolType.query.all()
form = NewProtocolForm(protocoltypes)
if form.validate_on_submit():
protocoltype = ProtocolType.query.filter_by(id=form.protocoltype.data).first()
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"))
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 render_template("protocol-new.html", form=form, protocoltypes=protocoltypes)
@app.route("/protocol/show/<int:protocol_id>")
def show_protocol(protocol_id):
user = current_user()
protocol = Protocol.query.filter_by(id=protocol_id).first()
if protocol is None or not protocol.protocoltype.has_public_view_right(user):
flash("Invalides Protokoll.", "alert-error")
return redirect(request.args.get("next") or url_for("index"))
errors_table = ErrorsTable(protocol.errors)
return render_template("protocol-show.html", protocol=protocol, errors_table=errors_table)
@app.route("/protocol/etherpull/<int:protocol_id>")
def etherpull_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 keine Berechtigung.", "alert-error")
return redirect(request.args.get("next") or url_for("index"))
source_req = requests.get(protocol.get_etherpad_source_link())
#source = source_req.content.decode("utf-8")
source = source_req.text
print(source.split("\r"))
#print(source.split("\n"))
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/source/<int:protocol_id>")
@login_required
def get_protocol_source(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.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>")
@login_required
def update_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 keine Berechtigung.", "alert-error")
return redirect(request.args.get("next") or url_for("index"))
# TODO: render form to upload a new version
@app.route("/todos/list")
def list_todos():
is_logged_in = check_login()
user = current_user()
todos = Todos.query.all()
# TODO: paginate
todos_table = TodosTable(todos)
return render_template("todos-list.html", todos=todos, todos_table=todos_table)
@app.route("/login", methods=["GET", "POST"])
def login():
......
......@@ -22,3 +22,8 @@ body {
h3 > a {
font-size: 18px;
}
form {
max-width: 350px;
margin: 0 auto;
}
......@@ -4,7 +4,7 @@ import os
import subprocess
import shutil
from models.database import Document, Protocol, Error, Todo, Decision
from models.database import Document, Protocol, Error, Todo, Decision, TOP, DefaultTOP
from server import celery, app
from shared import db, escape_tex, unhyphen, date_filter, datetime_filter, date_filter_long, time_filter
from utils import mail_manager, url_manager, encode_kwargs, decode_kwargs
......@@ -49,6 +49,9 @@ def parse_protocol_async(protocol_id, encoded_kwargs):
protocol = Protocol.query.filter_by(id=protocol_id).first()
if protocol is None:
raise Exception("No protocol given. Aborting parsing.")
for error in protocol.errors:
protocol.errors.remove(error)
db.session.commit()
if protocol.source is None:
error = protocol.create_error("Parsing", "Protocol source is None", "")
db.session.add(error)
......@@ -68,9 +71,9 @@ def parse_protocol_async(protocol_id, encoded_kwargs):
db.session.add(error)
db.session.commit()
return
remarks = {element.name: element for element in tree.children is isinstance(element, Remark)}
remarks = {element.name: element for element in tree.children if isinstance(element, Remark)}
required_fields = ["Datum", "Anwesende", "Beginn", "Ende", "Autor", "Ort"]
missing_fields = [field for field in required_files if field not in remarks]
missing_fields = [field for field in required_fields if field not in remarks]
if len(missing_fields) > 0:
error = protocol.create_error("Parsing", "Missing fields", ", ".join(missing_fields))
db.session.add(error)
......@@ -147,6 +150,16 @@ def parse_protocol_async(protocol_id, encoded_kwargs):
decision = Decision(protocol_id=protocol.id, content=decision_tag.values[0])
db.session.add(decision)
db.session.commit()
old_tops = list(protocol.tops)
for top in old_tops:
protocol.tops.remove(top)
tops = []
for index, fork in enumerate((child for child in tree.children if isinstance(child, Fork))):
top = TOP(protocol.id, fork.name, index, False)
db.session.add(top)
db.session.commit()
protocol.done = True
db.session.commit()
......
{% extends "layout.html" %}
{% from "macros.html" import render_form %}
{% block title %}Protokoll anlegen{% endblock %}
{% block content %}
<div class="container">
{{render_form(form, action_url=url_for("new_protocol"), action_text="Anlegen")}}
</div>
{% endblock %}
{% extends "layout.html" %}
{% from "macros.html" import render_table %}
{% block title %}Protokoll{% endblock %}
{% block content %}
<div class="container">
<div class="btn-group">
<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>
{% 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 %}
{% if protocol.is_done() %}
<a class="btn btn-success" href="{{url_for("update_protocol", protocol_id=protocol.id)}}">Protokoll editieren</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>
</div>
<div class="row">
<div id="left-column" class="col-lg-6">
<h2>Protokoll: {{protocol.protocoltype.name}} vom {{protocol.date|datify}}</h2>
{% if protocol.is_done() %}
<p><strong>Datum:</strong> {{protocol.date|datify_long}}</p>
<p><strong>Zeit:</strong> von {{protocol.start_time|timify}} bis {{protocol.end_time|timify}}</p>
<p><strong>Ort:</strong> {{protocol.location}}</p>
<p><strong>Protokollant:</strong> {{protocol.author}}</p>
<p><strong>Anwesende:</strong> {{protocol.participants}}</p>
{% else %}
<p><strong>Geplant:</strong> {{protocol.date|datify_long}}</p>
{% endif %}
<h3>Tagesordnung</h3>
<ul>
{% if not protocol.has_nonplanned_tops() %}
{% for default_top in protocol.protocoltype.default_tops %}
{% if not default_top.is_at_end() %}
<li>{{default_top.name}}</li>
{% endif %}
{% endfor %}
{% endif %}
{% for top in protocol.tops %}
<li>{{top.name}}</li>
{% endfor %}
{% if not protocol.has_nonplanned_tops() %}
{% for default_top in protocol.protocoltype.default_tops %}
{% if default_top.is_at_end() %}
<li>{{default_top.name}}</li>
{% endif %}
{% endfor %}
{% endif %}
</ul>
{% if protocol.is_done() %}
<h3>Beschlüsse</h3>
<ul>
{% for decision in protocol.decisions %}
<li>{{decision.content}}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div id="right-column" class="col-lg-6">
{% if protocol.is_done() %}
<h3>Todos dieser Sitzung <a href="{{url_for("list_todos")}}">Aktuelle Todos</a></h3>
<ul>
{% for todo in protocol.get_originating_todos() %}
<li>{{todo.render_html()|safe}}</li>
{% endfor %}
</ul>
{% endif %}
{% if protocol.errors|length > 0 %}
{{render_table(errors_table)}}
{% endif %}
{% if protocol.documents|length > 0 %}
<h3>Anhang</h3>
{# TODO: render documents table here #}
{% endif %}
</div>
</div>
</div>
{% endblock %}
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, DateField, HiddenField, IntegerField
from wtforms import StringField, PasswordField, BooleanField, DateField, HiddenField, IntegerField, SelectField
from wtforms.validators import InputRequired
class LoginForm(FlaskForm):
......@@ -24,3 +24,11 @@ class MeetingReminderForm(FlaskForm):
days_before = IntegerField("Tage vor Sitzung", validators=[InputRequired("Du musst eine Dauer angeben.")])
send_public = BooleanField("Öffentlich einladen")
send_private = BooleanField("Intern einladen")
class NewProtocolForm(FlaskForm):
protocoltype = SelectField("Typ", choices=[], coerce=int)
date = DateField("Datum", validators=[InputRequired("Du musst ein Datum angeben.")], format="%d.%m.%Y")
def __init__(self, protocoltypes, **kwargs):
super().__init__(**kwargs)
self.protocoltype.choices = [(protocoltype.id, protocoltype.short_name) for protocoltype in protocoltypes]
# coding: utf-8
from flask import Markup, url_for, request
from models.database import Protocol, ProtocolType, DefaultTOP, TOP, Todo, Decision
from shared import date_filter
from shared import date_filter, datetime_filter
class Table:
def __init__(self, title, values, newlink=None, newtext=None):
......@@ -49,16 +49,17 @@ class SingleValueTable:
class ProtocolsTable(Table):
def __init__(self, protocols):
super().__init__("Protokolle", protocols, newlink=None)
super().__init__("Protokolle", protocols, newlink=url_for("new_protocol"))
def headers(self):
return ["ID", "Sitzung", "Datum"]
return ["ID", "Sitzung", "Status", "Datum"]
def row(self, protocol):
return [
Table.link(url_for("protocol_view", protocol_id=protocol.id), str(protocol.id)),
protocol.protocoltype.name,
date_filter(protocol.data)
Table.link(url_for("show_protocol", protocol_id=protocol.id), str(protocol.id)),
Table.link(url_for("show_type", type_id=protocol.protocoltype.id), protocol.protocoltype.name),
Table.link(url_for("show_protocol", protocol_id=protocol.id), "Fertig" if protocol.is_done() else "Geplant"),
date_filter(protocol.date)
]
class ProtocolTypesTable(Table):
......@@ -142,3 +143,34 @@ class MeetingRemindersTable(Table):
if reminder.send_private:
parts.append("Intern")
return " und ".join(parts)
class ErrorsTable(Table):
def __init__(self, errors):
super().__init__("Fehler", errors)
def headers(self):
return ["Protokoll", "Fehler", "Zeitpunkt", "Beschreibung"]
def row(self, error):
return [
Table.link(url_for("show_protocol", protocol_id=error.protocol.id), error.protocol.get_identifier()),
error.name,
datetime_filter(error.datetime),
error.description
]
class TodosTable(Table):
def __init__(self, todos):
super().__init__("Todos", todos)
def headers(self):
return ["Status", "Sitzung", "Name", "Aufgabe"]
def row(self, todo):
protocol = todo.get_first_protocol()
return [
todo.get_state(),
Table.link(url_for("show_protocol", protocol_id=protocol.id), protocol.get_identifier()),
todo.who,
todo.description
]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment