Commit b4e667c7 authored by Robin Sonnabend's avatar Robin Sonnabend

Internal protocol

parent 1e997477
......@@ -5,6 +5,7 @@ import math
from shared import db
from utils import random_string, url_manager
from models.errors import DateNotMatchingException
import os
......@@ -45,7 +46,7 @@ class ProtocolType(db.Model):
self.id, self.short_name, self.name, self.organization, self.is_public, self.private_group, self.public_group)
def get_latest_protocol(self):
candidates = sorted([protocol for protocol in self.protocols if protocol.is_done()], key=lambda p: p.data, reverse=True)
candidates = sorted([protocol for protocol in self.protocols if protocol.is_done()], key=lambda p: p.date, reverse=True)
if len(candidates) == 0:
return None
return candidates[0]
......@@ -57,7 +58,7 @@ class ProtocolType(db.Model):
or (self.private_group != "" and self.private_group in user.groups))))
def has_private_view_right(self, user):
return (self.private_group != "" and self.private_group in user.groups)
return (user is not None and self.private_group != "" and self.private_group in user.groups)
def has_modify_right(self, user):
return self.has_private_view_right(user)
......@@ -101,7 +102,9 @@ class Protocol(db.Model):
return Error(self.id, action, name, now, description)
def fill_from_remarks(self, remarks):
self.date = datetime.strptime(remarks["Datum"].value, "%d.%m.%Y")
new_date = datetime.strptime(remarks["Datum"].value, "%d.%m.%Y").date()
if new_date != self.date:
raise DateNotMatchingException(original_date=self.date, protocol_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
......@@ -128,6 +131,20 @@ class Protocol(db.Model):
def get_originating_todos(self):
return [todo for todo in self.todos if self == todo.get_first_protocol()]
def delete_orphan_todos(self):
orphan_todos = [
todo for todo in self.todos
if len(todo.protocols) == 1
]
for todo in orphan_todos:
self.todos.remove(todo)
db.session.delete(todo)
@event.listens_for(Protocol, "before_delete")
def on_protocol_delete(mapper, connection, protocol):
protocol.delete_orphan_todos()
class DefaultTOP(db.Model):
__tablename__ = "defaulttops"
id = db.Column(db.Integer, primary_key=True)
......@@ -189,7 +206,7 @@ class Document(db.Model):
return os.path.join(config.DOCUMENTS_PATH, self.filename)
@event.listens_for(Document, "before_delete")
def on_delete(mapper, connection, document):
def on_document_delete(mapper, connection, document):
if document.filename is not None:
document_path = os.path.join(config.DOCUMENTS_PATH, document.filename)
if os.path.isfile(document_path):
......
class DateNotMatchingException(Exception):
def __init__(self, original_date, protocol_date):
self.original_date = original_date
self.protocol_date = protocol_date
......@@ -4,6 +4,8 @@ from collections import OrderedDict
from shared import escape_tex
import config
class ParserException(Exception):
name = "Parser Exception"
has_explanation = False
......@@ -27,7 +29,7 @@ class Element:
Generic (abstract) base element. Should never really exist.
Template for what an element class should contain.
"""
def render(self):
def render(self, show_private):
"""
Renders the element to TeX.
Returns:
......@@ -92,8 +94,8 @@ class Content(Element):
self.children = children
self.linenumber = linenumber
def render(self):
return "".join(map(lambda e: e.render(), self.children))
def render(self, show_private):
return "".join(map(lambda e: e.render(show_private), self.children))
def dump(self, level=None):
if level is None:
......@@ -144,7 +146,7 @@ class Text:
self.text = text
self.linenumber = linenumber
def render(self):
def render(self, show_private):
return escape_tex(self.text)
def dump(self, level=None):
......@@ -170,7 +172,7 @@ class Tag:
self.values = values
self.linenumber = linenumber
def render(self):
def render(self, show_private):
if self.name == "url":
return r"\url{{{}}}".format(self.values[0])
#return r"\textbf{{{}:}} {}".format(escape_tex(self.name.capitalize()), "; ".join(map(escape_tex, self.values)));
......@@ -197,7 +199,7 @@ class Empty(Element):
def __init__(self, linenumber):
linenumber = linenumber
def render(self):
def render(self, show_private):
return ""
def dump(self, level=None):
......@@ -218,7 +220,7 @@ class Remark(Element):
self.value = value
self.linenumber = linenumber
def render(self):
def render(self, show_private):
return r"\textbf{{{}}}: {}".format(self.name, self.value)
def dump(self, level=None):
......@@ -260,11 +262,32 @@ class Fork(Element):
for child in self.children:
child.dump(level + 1)
def render(self, toplevel=False):
return ((self.name if self.name is not None and len(self.name) > 0 and not toplevel else "")
+ r"\begin{itemize}" + "\n"
+ "\n".join(map(lambda e: r"\item {}".format(e.render()), self.children)) + "\n"
+ r"\end{itemize}" + "\n")
def test_private(self, name):
stripped_name = name.replace(":", "").strip()
return stripped_name in config.PRIVATE_KEYS
def render(self, show_private, toplevel=False):
name_line = self.name if self.name is not None and len(self.name) > 0 else ""
begin_line = r"\begin{itemize}"
end_line = r"\end{itemize}"
content_parts = []
for child in self.children:
part = child.render(show_private)
if len(part.strip()) == 0:
continue
if not part.startswith(r"\item"):
part = r"\item {}".format(part)
content_parts.append(part)
content_lines = "\n".join(content_parts)
if toplevel:
return "\n".join([begin_line, content_lines, end_line])
elif self.test_private(self.name):
if show_private:
return content_lines
else:
return ""
else:
return "\n".join([name_line, begin_line, content_lines, end_line])
def get_tags(self, tags=None):
if tags is None:
......
......@@ -8,13 +8,13 @@ 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 os
from datetime import datetime
import config
from shared import db, date_filter, datetime_filter, date_filter_long, time_filter, ldap_manager, security_manager
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
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, NewProtocolForm, DocumentUploadForm
......@@ -49,34 +49,6 @@ app.jinja_env.tests["auth_valid"] = security_manager.check_user
import tasks
from auth import User
def check_login():
return "auth" in session and security_manager.check_user(session["auth"])
def current_user():
if not check_login():
return None
return User.from_hashstring(session["auth"])
def login_required(function):
@wraps(function)
def decorated_function(*args, **kwargs):
if check_login():
return function(*args, **kwargs)
else:
return redirect(url_for("login", next=request.url))
return decorated_function
def group_required(function, group):
@wraps(function)
def decorated_function(*args, **kwargs):
if group in current_user.groups:
return function(*args, **kwargs)
else:
flash("You do not have the necessary permissions to view this page.")
return redirect(request.args.get("next") or url_for("index"))
return decorated_function
app.jinja_env.globals.update(check_login=check_login)
app.jinja_env.globals.update(current_user=current_user)
app.jinja_env.globals.update(zip=zip)
......@@ -316,7 +288,10 @@ def list_protocols():
@login_required
def new_protocol():
user = current_user()
protocoltypes = ProtocolType.query.all()
protocoltypes = [
protocoltype for protocoltype in ProtocolType.query.all()
if protocoltype.has_modify_right(user)
]
form = NewProtocolForm(protocoltypes)
if form.validate_on_submit():
protocoltype = ProtocolType.query.filter_by(id=form.protocoltype.data).first()
......@@ -327,6 +302,9 @@ def new_protocol():
db.session.add(protocol)
db.session.commit()
return redirect(request.args.get("next") or url_for("list_protocols"))
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, protocoltypes=protocoltypes)
@app.route("/protocol/show/<int:protocol_id>")
......@@ -337,10 +315,29 @@ def show_protocol(protocol_id):
flash("Invalides Protokoll.", "alert-error")
return redirect(request.args.get("next") or url_for("index"))
errors_table = ErrorsTable(protocol.errors)
documents_table = DocumentsTable(protocol.documents)
visible_documents = [
document for document in protocol.documents
if (not document.is_private and document.protocol.protocoltype.has_public_view_right(user))
or (document.is_private and document.protocol.protocoltype.has_private_view_right(user))
]
documents_table = DocumentsTable(visible_documents)
document_upload_form = DocumentUploadForm()
return render_template("protocol-show.html", protocol=protocol, errors_table=errors_table, documents_table=documents_table, document_upload_form=document_upload_form)
@app.route("/protocol/delete/<int:protocol_id>")
@login_required
def delete_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"))
name = protocol.get_identifier()
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"))
@app.route("/protocol/etherpull/<int:protocol_id>")
def etherpull_protocol(protocol_id):
user = current_user()
......@@ -382,8 +379,8 @@ def update_protocol(protocol_id):
def list_todos():
is_logged_in = check_login()
user = current_user()
todos = Todos.query.all()
# TODO: paginate
todos = Todo.query.all()
# TODO: paginate and search
todos_table = TodosTable(todos)
return render_template("todos-list.html", todos=todos, todos_table=todos_table)
......@@ -410,25 +407,43 @@ def upload_document(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("Insufficient permissions.", "alert-error")
flash("Unzureichende Berechtigung.", "alert-error")
return redirect(request.args.get("next") or url_for("index"))
form = DocumentUploadForm()
print(form, form.document.data, form.private.data)
print(request.files)
if form.document.data is None:
flash("No file has been selected.", "alert-error")
flash("Es wurde keine Datei ausgewählt.", "alert-error")
return redirect(request.args.get("next") or url_for("index"))
file = form.document.data
if file.filename == "":
flash("No file has been selected.", "alert-error")
flash("Es wurde keine Datei ausgewählt.", "alert-error")
return redirect(request.args.get("next") or url_for("index"))
# todo: Dateitypen einschränken?
if file:
filename = secure_filename(file.filename)
internal_filename = "{}-{}".format(protocol.id, filename)
file.save(os.path.join(config.DOCUMENTS_PATH, internal_filename))
document = Document(protocol.id, filename, internal_filename, False, form.private.data)
document = Document(protocol.id, filename, "", False, form.private.data)
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))
if datetime.now().date() >= protocol.date:
protocol.done = True
db.session.commit()
return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
@app.route("/document/delete/<int:document_id>")
@login_required
def delete_document(document_id):
user = current_user()
document = Document.query.filter_by(id=document_id).first()
if document is None or not document.protocol.protocoltype.has_modify_right(user):
flash("Unzureichende Berechtigung.", "alert-error")
return redirect(request.args.get("next") or url_for("index"))
name = document.name
protocol = document.protocol
db.session.delete(document)
db.session.commit()
flash("Das Dokument {} wurde gelöscht.".format(name), "alert-success")
return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
......
from flask_sqlalchemy import SQLAlchemy
from flask import session, redirect, url_for, request
import re
from functools import wraps
import config
......@@ -77,6 +79,35 @@ def time_filter(time):
def class_filter(obj):
return obj.__class__.__name__
from auth import LdapManager, SecurityManager
from auth import LdapManager, SecurityManager, User
ldap_manager = LdapManager(config.LDAP_PROVIDER_URL, config.LDAP_BASE)
security_manager = SecurityManager(config.SECURITY_KEY)
from auth import User
def check_login():
return "auth" in session and security_manager.check_user(session["auth"])
def current_user():
if not check_login():
return None
return User.from_hashstring(session["auth"])
def login_required(function):
@wraps(function)
def decorated_function(*args, **kwargs):
if check_login():
return function(*args, **kwargs)
else:
return redirect(url_for("login", next=request.url))
return decorated_function
def group_required(function, group):
@wraps(function)
def decorated_function(*args, **kwargs):
if group in current_user.groups:
return function(*args, **kwargs)
else:
flash("You do not have the necessary permissions to view this page.")
return redirect(request.args.get("next") or url_for("index"))
return decorated_function
......@@ -27,3 +27,7 @@ form {
max-width: 350px;
margin: 0 auto;
}
input[type="file"] {
padding: 0;
}
......@@ -6,6 +6,7 @@ import shutil
import tempfile
from models.database import Document, Protocol, Error, Todo, Decision, TOP, DefaultTOP
from models.errors import DateNotMatchingException
from server import celery, app
from shared import db, escape_tex, unhyphen, date_filter, datetime_filter, date_filter_long, date_filter_short, time_filter, class_filter
from utils import mail_manager, url_manager, encode_kwargs, decode_kwargs
......@@ -94,6 +95,14 @@ def parse_protocol_async(protocol_id, encoded_kwargs):
db.session.add(error)
db.session.commit()
return
except DateNotMatchingException as exc:
error = protocol.create_error("Parsing", "Date not matching",
"This protocol's date should be {}, but the protocol source says {}.".format(date_filter(exc.original_date), date_filter(exc.protocol_date)))
db.session.add(error)
db.session.commit()
return
protocol.delete_orphan_todos()
db.session.commit()
old_todos = list(protocol.todos)
for todo in old_todos:
protocol.todos.remove(todo)
......@@ -163,17 +172,18 @@ def parse_protocol_async(protocol_id, encoded_kwargs):
db.session.add(top)
db.session.commit()
latex_source = texenv.get_template("protocol.tex").render(protocol=protocol, tree=tree)
compile(latex_source, protocol)
for show_private in [True, False]:
latex_source = texenv.get_template("protocol.tex").render(protocol=protocol, tree=tree, show_private=show_private)
compile(latex_source, protocol, show_private=show_private)
protocol.done = True
db.session.commit()
def compile(content, protocol):
compile_async.delay(content, protocol.id)
def compile(content, protocol, show_private):
compile_async.delay(content, protocol.id, show_private)
@celery.task
def compile_async(content, protocol_id):
def compile_async(content, protocol_id, show_private):
with tempfile.TemporaryDirectory() as compile_dir, app.app_context():
protocol = Protocol.query.filter_by(id=protocol_id).first()
try:
......@@ -196,17 +206,17 @@ def compile_async(content, protocol_id):
subprocess.check_call(command, universal_newlines=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.check_call(command, universal_newlines=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
os.chdir(current)
for old_document in [document for document in protocol.documents if document.is_compiled]:
for old_document in [document for document in protocol.documents if document.is_compiled and document.is_private == show_private]:
protocol.documents.remove(old_document)
db.session.commit()
document = Document(protocol.id, name="protokoll_{}_{}.pdf".format(protocol.protocoltype.short_name, date_filter_short(protocol.date)), filename="", is_compiled=True, is_private=False)
document = Document(protocol.id, name="protokoll{}_{}_{}.pdf".format("_intern" if show_private else "", protocol.protocoltype.short_name, date_filter_short(protocol.date)), filename="", is_compiled=True, is_private=show_private)
db.session.add(document)
db.session.commit()
target_filename = "compiled-{}.pdf".format(document.id)
target_filename = "compiled-{}-{}.pdf".format(document.id, "internal" if show_private else "public")
document.filename = target_filename
shutil.copy(os.path.join(compile_dir, protocol_target_filename), os.path.join(config.DOCUMENTS_PATH, target_filename))
db.session.commit()
shutil.copy(os.path.join(compile_dir, log_filename), "/tmp")
#shutil.copy(os.path.join(compile_dir, log_filename), "/tmp")
except subprocess.SubprocessError:
log = ""
total_log_filename = os.path.join(compile_dir, log_filename)
......
......@@ -28,6 +28,7 @@
<ul class="nav navbar-nav">
<li><a href="{{url_for("index")}}">Zuhause</a></li>
<li><a href="{{url_for("list_protocols")}}">Protokolle</a></li>
<li><a href="{{url_for("list_todos")}}">Todos</a></li>
{% if check_login() %}
<li><a href="{{url_for("list_types")}}">Typen</a></li>
{% endif %}
......
......@@ -62,9 +62,13 @@
{% 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 %}
{% if protocol.get_originating_todos()|length > 0 %}
{% for todo in protocol.get_originating_todos() %}
<li>{{todo.render_html()|safe}}</li>
{% endfor %}
{% else %}
<li>Keine Todos</li>
{% endif %}
</ul>
{% endif %}
{% if protocol.errors|length > 0 %}
......
......@@ -19,7 +19,7 @@
\begin{document}
%\thispagestyle{plain} %ggf kommentarzeichen entfernen
\Titel{
\large Protokoll: \VAR{protocol.protocoltype.name|escape_tex}
\large \ENV{if show_private}Internes \ENV{endif} Protokoll: \VAR{protocol.protocoltype.name|escape_tex}
\\\normalsize \VAR{protocol.protocoltype.organization|escape_tex}
}{}
\begin{tabular}{rp{14cm}}
......@@ -55,7 +55,7 @@ Beginn der Sitzung: \VAR{protocol.start_time|timify}
\end{itemize}
\ENV{endif}
\ENV{else}
\VAR{top.render(toplevel=True)}
\VAR{top.render(toplevel=True, show_private=show_private)}
\ENV{endif}
\ENV{endif}
\ENV{endfor}
......
{% extends "layout.html" %}
{% from "macros.html" import render_table %}
{% block title %}Todos{% endblock %}
{% block content %}
<div class="container">
{{render_table(todos_table)}}
</div>
{% endblock %}
# 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, datetime_filter, date_filter_short
from shared import date_filter, datetime_filter, date_filter_short, current_user, check_login
class Table:
def __init__(self, title, values, newlink=None, newtext=None):
......@@ -52,15 +52,24 @@ class ProtocolsTable(Table):
super().__init__("Protokolle", protocols, newlink=url_for("new_protocol"))
def headers(self):
return ["ID", "Sitzung", "Status", "Datum"]
result = ["ID", "Sitzung", "Datum", "Status"]
optional_part = ["Typ", "Löschen"]
if check_login():
result += optional_part
return result
def row(self, protocol):
return [
user = current_user()
result = [
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)
Table.link(url_for("show_protocol", protocol_id=protocol.id), protocol.protocoltype.name),
date_filter(protocol.date),
"Fertig" if protocol.is_done() else "Geplant"
]
if user is not None and protocol.protocoltype.has_private_view_right(user):
result.append(Table.link(url_for("show_type", type_id=protocol.protocoltype.id), protocol.protocoltype.short_name))
result.append(Table.link(url_for("delete_protocol", protocol_id=protocol.id), "Löschen", confirm="Bist du dir sicher, dass du das Protokoll {} löschen möchtest?".format(protocol.get_identifier())))
return result
class ProtocolTypesTable(Table):
def __init__(self, types):
......@@ -70,11 +79,15 @@ class ProtocolTypesTable(Table):
return ["Typ", "Name", "Neuestes Protokoll", ""]
def row(self, protocoltype):
protocol = protocoltype.get_latest_protocol()
user = current_user()
has_modify_right = protocoltype.has_modify_right(user)
return [
Table.link(url_for("show_type", type_id=protocoltype.id), protocoltype.short_name),
Table.link(url_for("show_type", type_id=protocoltype.id), protocoltype.short_name) if has_modify_right else protocoltype.short_name,
protocoltype.name,
protocoltype.get_latest_protocol() or "Noch kein Protokoll",
"" # TODO: add links for new, modify, delete
Table.link(url_for("show_protocol", protocol_id=protocol.id), protocol.get_identifier()) if protocol is not None else "Noch kein Protokoll",
Table.link(url_for("new_protocol", type_id=protocoltype.id), "Neues Protokoll") if has_modify_right else ""
"" # TODO: add link for modify, delete
]
class ProtocolTypeTable(SingleValueTable):
......@@ -170,7 +183,7 @@ class TodosTable(Table):
protocol = todo.get_first_protocol()
return [
todo.get_state(),
Table.link(url_for("show_protocol", protocol_id=protocol.id), protocol.get_identifier()),
Table.link(url_for("show_protocol", protocol_id=protocol.id), protocol.get_identifier()) if protocol is not None else "",
todo.who,
todo.description
]
......@@ -183,8 +196,11 @@ class DocumentsTable(Table):
return ["ID", "Name", ""]
def row(self, document):
user = current_user()
return [
document.id,
Table.link(url_for("download_document", document_id=document.id), document.name),
""
(Table.link(url_for("delete_document", document_id=document.id), "Löschen", confirm="Bist du dir sicher, dass du das Dokument {} löschen willst?".format(document.name))
if document.protocol.protocoltype.has_modify_right(user)
else "")
]
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