Commit b4e667c7 authored by Robin Sonnabend's avatar Robin Sonnabend
Browse files

Internal protocol

parent 1e997477
...@@ -5,6 +5,7 @@ import math ...@@ -5,6 +5,7 @@ import math
from shared import db from shared import db
from utils import random_string, url_manager from utils import random_string, url_manager
from models.errors import DateNotMatchingException
import os import os
...@@ -45,7 +46,7 @@ class ProtocolType(db.Model): ...@@ -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) self.id, self.short_name, self.name, self.organization, self.is_public, self.private_group, self.public_group)
def get_latest_protocol(self): 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: if len(candidates) == 0:
return None return None
return candidates[0] return candidates[0]
...@@ -57,7 +58,7 @@ class ProtocolType(db.Model): ...@@ -57,7 +58,7 @@ class ProtocolType(db.Model):
or (self.private_group != "" and self.private_group in user.groups)))) or (self.private_group != "" and self.private_group in user.groups))))
def has_private_view_right(self, user): 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): def has_modify_right(self, user):
return self.has_private_view_right(user) return self.has_private_view_right(user)
...@@ -101,7 +102,9 @@ class Protocol(db.Model): ...@@ -101,7 +102,9 @@ class Protocol(db.Model):
return Error(self.id, action, name, now, description) return Error(self.id, action, name, now, description)
def fill_from_remarks(self, remarks): 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.start_time = datetime.strptime(remarks["Beginn"].value, "%H:%M").time()
self.end_time = datetime.strptime(remarks["Ende"].value, "%H:%M").time() self.end_time = datetime.strptime(remarks["Ende"].value, "%H:%M").time()
self.author = remarks["Autor"].value self.author = remarks["Autor"].value
...@@ -128,6 +131,20 @@ class Protocol(db.Model): ...@@ -128,6 +131,20 @@ class Protocol(db.Model):
def get_originating_todos(self): def get_originating_todos(self):
return [todo for todo in self.todos if self == todo.get_first_protocol()] 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): class DefaultTOP(db.Model):
__tablename__ = "defaulttops" __tablename__ = "defaulttops"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
...@@ -189,7 +206,7 @@ class Document(db.Model): ...@@ -189,7 +206,7 @@ class Document(db.Model):
return os.path.join(config.DOCUMENTS_PATH, self.filename) return os.path.join(config.DOCUMENTS_PATH, self.filename)
@event.listens_for(Document, "before_delete") @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: if document.filename is not None:
document_path = os.path.join(config.DOCUMENTS_PATH, document.filename) document_path = os.path.join(config.DOCUMENTS_PATH, document.filename)
if os.path.isfile(document_path): 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 ...@@ -4,6 +4,8 @@ from collections import OrderedDict
from shared import escape_tex from shared import escape_tex
import config
class ParserException(Exception): class ParserException(Exception):
name = "Parser Exception" name = "Parser Exception"
has_explanation = False has_explanation = False
...@@ -27,7 +29,7 @@ class Element: ...@@ -27,7 +29,7 @@ class Element:
Generic (abstract) base element. Should never really exist. Generic (abstract) base element. Should never really exist.
Template for what an element class should contain. Template for what an element class should contain.
""" """
def render(self): def render(self, show_private):
""" """
Renders the element to TeX. Renders the element to TeX.
Returns: Returns:
...@@ -92,8 +94,8 @@ class Content(Element): ...@@ -92,8 +94,8 @@ class Content(Element):
self.children = children self.children = children
self.linenumber = linenumber self.linenumber = linenumber
def render(self): def render(self, show_private):
return "".join(map(lambda e: e.render(), self.children)) return "".join(map(lambda e: e.render(show_private), self.children))
def dump(self, level=None): def dump(self, level=None):
if level is None: if level is None:
...@@ -144,7 +146,7 @@ class Text: ...@@ -144,7 +146,7 @@ class Text:
self.text = text self.text = text
self.linenumber = linenumber self.linenumber = linenumber
def render(self): def render(self, show_private):
return escape_tex(self.text) return escape_tex(self.text)
def dump(self, level=None): def dump(self, level=None):
...@@ -170,7 +172,7 @@ class Tag: ...@@ -170,7 +172,7 @@ class Tag:
self.values = values self.values = values
self.linenumber = linenumber self.linenumber = linenumber
def render(self): def render(self, show_private):
if self.name == "url": if self.name == "url":
return r"\url{{{}}}".format(self.values[0]) return r"\url{{{}}}".format(self.values[0])
#return r"\textbf{{{}:}} {}".format(escape_tex(self.name.capitalize()), "; ".join(map(escape_tex, self.values))); #return r"\textbf{{{}:}} {}".format(escape_tex(self.name.capitalize()), "; ".join(map(escape_tex, self.values)));
...@@ -197,7 +199,7 @@ class Empty(Element): ...@@ -197,7 +199,7 @@ class Empty(Element):
def __init__(self, linenumber): def __init__(self, linenumber):
linenumber = linenumber linenumber = linenumber
def render(self): def render(self, show_private):
return "" return ""
def dump(self, level=None): def dump(self, level=None):
...@@ -218,7 +220,7 @@ class Remark(Element): ...@@ -218,7 +220,7 @@ class Remark(Element):
self.value = value self.value = value
self.linenumber = linenumber self.linenumber = linenumber
def render(self): def render(self, show_private):
return r"\textbf{{{}}}: {}".format(self.name, self.value) return r"\textbf{{{}}}: {}".format(self.name, self.value)
def dump(self, level=None): def dump(self, level=None):
...@@ -260,11 +262,32 @@ class Fork(Element): ...@@ -260,11 +262,32 @@ class Fork(Element):
for child in self.children: for child in self.children:
child.dump(level + 1) child.dump(level + 1)
def render(self, toplevel=False): def test_private(self, name):
return ((self.name if self.name is not None and len(self.name) > 0 and not toplevel else "") stripped_name = name.replace(":", "").strip()
+ r"\begin{itemize}" + "\n" return stripped_name in config.PRIVATE_KEYS
+ "\n".join(map(lambda e: r"\item {}".format(e.render()), self.children)) + "\n"
+ r"\end{itemize}" + "\n") 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): def get_tags(self, tags=None):
if tags is None: if tags is None:
......
...@@ -8,13 +8,13 @@ from flask_script import Manager, prompt ...@@ -8,13 +8,13 @@ from flask_script import Manager, prompt
from flask_migrate import Migrate, MigrateCommand from flask_migrate import Migrate, MigrateCommand
#from flask_socketio import SocketIO #from flask_socketio import SocketIO
from celery import Celery from celery import Celery
from functools import wraps
import requests import requests
from io import StringIO, BytesIO from io import StringIO, BytesIO
import os import os
from datetime import datetime
import config 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 utils import is_past, mail_manager, url_manager
from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error
from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm
...@@ -49,34 +49,6 @@ app.jinja_env.tests["auth_valid"] = security_manager.check_user ...@@ -49,34 +49,6 @@ app.jinja_env.tests["auth_valid"] = security_manager.check_user
import tasks 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(check_login=check_login)
app.jinja_env.globals.update(current_user=current_user) app.jinja_env.globals.update(current_user=current_user)
app.jinja_env.globals.update(zip=zip) app.jinja_env.globals.update(zip=zip)
...@@ -316,7 +288,10 @@ def list_protocols(): ...@@ -316,7 +288,10 @@ def list_protocols():
@login_required @login_required
def new_protocol(): def new_protocol():
user = current_user() user = current_user()
protocoltypes = ProtocolType.query.all() protocoltypes = [
protocoltype for protocoltype in ProtocolType.query.all()
if protocoltype.has_modify_right(user)
]
form = NewProtocolForm(protocoltypes) form = NewProtocolForm(protocoltypes)
if form.validate_on_submit(): if form.validate_on_submit():
protocoltype = ProtocolType.query.filter_by(id=form.protocoltype.data).first() protocoltype = ProtocolType.query.filter_by(id=form.protocoltype.data).first()
...@@ -327,6 +302,9 @@ def new_protocol(): ...@@ -327,6 +302,9 @@ def new_protocol():
db.session.add(protocol) db.session.add(protocol)
db.session.commit() db.session.commit()
return redirect(request.args.get("next") or url_for("list_protocols")) 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) return render_template("protocol-new.html", form=form, protocoltypes=protocoltypes)
@app.route("/protocol/show/<int:protocol_id>") @app.route("/protocol/show/<int:protocol_id>")
...@@ -337,10 +315,29 @@ def show_protocol(protocol_id): ...@@ -337,10 +315,29 @@ def show_protocol(protocol_id):
flash("Invalides Protokoll.", "alert-error") flash("Invalides Protokoll.", "alert-error")
return redirect(request.args.get("next") or url_for("index")) return redirect(request.args.get("next") or url_for("index"))
errors_table = ErrorsTable(protocol.errors) 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() 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) 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>") @app.route("/protocol/etherpull/<int:protocol_id>")
def etherpull_protocol(protocol_id): def etherpull_protocol(protocol_id):
user = current_user() user = current_user()
...@@ -382,8 +379,8 @@ def update_protocol(protocol_id): ...@@ -382,8 +379,8 @@ def update_protocol(protocol_id):
def list_todos(): def list_todos():
is_logged_in = check_login() is_logged_in = check_login()
user = current_user() user = current_user()
todos = Todos.query.all() todos = Todo.query.all()
# TODO: paginate # TODO: paginate and search
todos_table = TodosTable(todos) todos_table = TodosTable(todos)
return render_template("todos-list.html", todos=todos, todos_table=todos_table) return render_template("todos-list.html", todos=todos, todos_table=todos_table)
...@@ -410,25 +407,43 @@ def upload_document(protocol_id): ...@@ -410,25 +407,43 @@ def upload_document(protocol_id):
user = current_user() user = current_user()
protocol = Protocol.query.filter_by(id=protocol_id).first() protocol = Protocol.query.filter_by(id=protocol_id).first()
if protocol is None or not protocol.protocoltype.has_modify_right(user): 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")) return redirect(request.args.get("next") or url_for("index"))
form = DocumentUploadForm() form = DocumentUploadForm()
print(form, form.document.data, form.private.data)
print(request.files)
if form.document.data is None: 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")) return redirect(request.args.get("next") or url_for("index"))
file = form.document.data file = form.document.data
if file.filename == "": 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")) return redirect(request.args.get("next") or url_for("index"))
# todo: Dateitypen einschränken?
if file: if file:
filename = secure_filename(file.filename) filename = secure_filename(file.filename)
internal_filename = "{}-{}".format(protocol.id, filename) document = Document(protocol.id, filename, "", False, form.private.data)
file.save(os.path.join(config.DOCUMENTS_PATH, internal_filename))
document = Document(protocol.id, filename, internal_filename, False, form.private.data)
db.session.add(document) db.session.add(document)
db.session.commit() 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)) return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
......
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask import session, redirect, url_for, request
import re import re
from functools import wraps
import config import config
...@@ -77,6 +79,35 @@ def time_filter(time): ...@@ -77,6 +79,35 @@ def time_filter(time):
def class_filter(obj): def class_filter(obj):
return obj.__class__.__name__ 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) ldap_manager = LdapManager(config.LDAP_PROVIDER_URL, config.LDAP_BASE)
security_manager = SecurityManager(config.SECURITY_KEY) 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 { ...@@ -27,3 +27,7 @@ form {
max-width: 350px; max-width: 350px;
margin: 0 auto; margin: 0 auto;
} }
input[type="file"] {
padding: 0;
}
...@@ -6,6 +6,7 @@ import shutil ...@@ -6,6 +6,7 @@ import shutil
import tempfile import tempfile
from models.database import Document, Protocol, Error, Todo, Decision, TOP, DefaultTOP from models.database import Document, Protocol, Error, Todo, Decision, TOP, DefaultTOP
from models.errors import DateNotMatchingException
from server import celery, app 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 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 from utils import mail_manager, url_manager, encode_kwargs, decode_kwargs
...@@ -94,6 +95,14 @@ def parse_protocol_async(protocol_id, encoded_kwargs): ...@@ -94,6 +95,14 @@ def parse_protocol_async(protocol_id, encoded_kwargs):
db.session.add(error) db.session.add(error)
db.session.commit() db.session.commit()
return 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) old_todos = list(protocol.todos)
for todo in old_todos: for todo in old_todos:
protocol.todos.remove(todo) protocol.todos.remove(todo)
...@@ -163,17 +172,18 @@ def parse_protocol_async(protocol_id, encoded_kwargs): ...@@ -163,17 +172,18 @@ def parse_protocol_async(protocol_id, encoded_kwargs):
db.session.add(top) db.session.add(top)
db.session.commit() db.session.commit()
latex_source = texenv.get_template("protocol.tex").render(protocol=protocol, tree=tree) for show_private in [True, False]:
compile(latex_source, protocol) 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 protocol.done = True
db.session.commit() db.session.commit()
def compile(content, protocol): def compile(content, protocol, show_private):
compile_async.delay(content, protocol.id) compile_async.delay(content, protocol.id, show_private)
@celery.task @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(): with tempfile.TemporaryDirectory() as compile_dir, app.app_context():
protocol = Protocol.query.filter_by(id=protocol_id).first() protocol = Protocol.query.filter_by(id=protocol_id).first()
try: try:
...@@ -196,17 +206,17 @@ def compile_async(content, protocol_id): ...@@ -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)
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) 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) protocol.documents.remove(old_document)
db.session.commit() 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.add(document)
db.session.commit() 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 document.filename = target_filename
shutil.<