diff --git a/auth.py b/auth.py index 0323a2fd3d1253fa22e80baeae21ff634cac0eb0..3241a712952fce4fd8bdc24dfa4a5e5d49d13740 100644 --- a/auth.py +++ b/auth.py @@ -1,12 +1,12 @@ -import hmac, hashlib +import hmac +import hashlib import ssl -import ldap3 -from ldap3.utils.dn import parse_dn from datetime import datetime -import grp, pwd, pam + class User: - def __init__(self, username, groups, timestamp=None, obsolete=False, permanent=False): + def __init__(self, username, groups, timestamp=None, obsolete=False, + permanent=False): self.username = username self.groups = groups if timestamp is not None: @@ -17,7 +17,10 @@ class User: self.permanent = permanent def summarize(self): - return "{}:{}:{}:{}:{}".format(self.username, ",".join(self.groups), str(self.timestamp.timestamp()), self.obsolete, self.permanent) + return ":".join(( + self.username, ",".join(self.groups), + str(self.timestamp.timestamp()), str(self.obsolete), + str(self.permanent))) @staticmethod def from_summary(summary): @@ -36,6 +39,7 @@ class User: summary, hash = secure_string.split("=", 1) return User.from_summary(summary) + class UserManager: def __init__(self, backends): self.backends = backends @@ -44,7 +48,9 @@ class UserManager: for backend in self.backends: if backend.authenticate(username, password): groups = sorted(list(set(backend.groups(username, password)))) - return User(username, groups, obsolete=backend.obsolete, permanent=permanent) + return User( + username, groups, obsolete=backend.obsolete, + permanent=permanent) return None def all_groups(self): @@ -52,89 +58,33 @@ class UserManager: yield from backend.all_groups() -class LdapManager: - def __init__(self, host, user_dn, group_dn, port=636, use_ssl=True, obsolete=False): - self.server = ldap3.Server(host, port=port, use_ssl=use_ssl) - self.user_dn = user_dn - self.group_dn = group_dn - self.obsolete = obsolete - - def authenticate(self, username, password): - try: - connection = ldap3.Connection(self.server, self.user_dn.format(username), password) - return connection.bind() - except ldap3.core.exceptions.LDAPSocketOpenError: - return False - - def groups(self, username, password=None): - connection = ldap3.Connection(self.server) - obj_def = ldap3.ObjectDef("posixgroup", connection) - group_reader = ldap3.Reader(connection, obj_def, self.group_dn) - username = username.lower() - for group in group_reader.search(): - members = group.memberUid.value - if members is not None and username in members: - yield group.cn.value +class SecurityManager: + def __init__(self, key, max_duration=300): + self.maccer = hmac.new(key.encode("utf-8"), digestmod=hashlib.sha512) + self.max_duration = max_duration - def all_groups(self): - connection = ldap3.Connection(self.server) - obj_def = ldap3.ObjectDef("posixgroup", connection) - group_reader = ldap3.Reader(connection, obj_def, self.group_dn) - for group in group_reader.search(): - yield group.cn.value - - -class ADManager: - def __init__(self, host, domain, user_dn, group_dn, - port=636, use_ssl=True, ca_cert=None, obsolete=False): - tls_config = ldap3.Tls(validate=ssl.CERT_REQUIRED) - if ca_cert is not None: - tls_config = ldap3.Tls(validate=ssl.CERT_REQUIRED, - ca_certs_file=ca_cert) - self.server = ldap3.Server(host, port=port, use_ssl=use_ssl, - tls=tls_config) - self.domain = domain - self.user_dn = user_dn - self.group_dn = group_dn - self.obsolete = obsolete + def hash_user(self, user): + maccer = self.maccer.copy() + summary = user.summarize() + maccer.update(summary.encode("utf-8")) + return "{}={}".format(summary, maccer.hexdigest()) - def prepare_connection(self, username=None, password=None): - if username is not None and password is not None: - ad_user = "{}\\{}".format(self.domain, username) - return ldap3.Connection(self.server, ad_user, password) - return ldap3.Connection(self.server) - - def authenticate(self, username, password): - try: - return self.prepare_connection(username, password).bind() - except ldap3.core.exceptions.LDAPSocketOpenError: + def check_user(self, string): + parts = string.split("=", 1) + if len(parts) != 2: + # wrong format, expecting summary:hash return False - - def groups(self, username, password): - connection = self.prepare_connection(username, password) - connection.bind() - obj_def = ldap3.ObjectDef("user", connection) - name_filter = "cn:={}".format(username) - user_reader = ldap3.Reader(connection, obj_def, self.user_dn, name_filter) - group_def = ldap3.ObjectDef("group", connection) - def _yield_recursive_groups(group_dn): - group_reader = ldap3.Reader(connection, group_def, group_dn, None) - for entry in group_reader.search(): - yield entry.name.value - for child in entry.memberOf: - yield from _yield_recursive_groups(child) - for result in user_reader.search(): - for group_dn in result.memberOf: - yield from _yield_recursive_groups(group_dn) - - - def all_groups(self): - connection = self.prepare_connection() - connection.bind() - obj_def = ldap3.ObjectDef("group", connection) - group_reader = ldap3.Reader(connection, obj_def, self.group_dn) - for result in reader.search(): - yield result.name.value + summary, hash = map(lambda s: s.encode("utf-8"), parts) + maccer = self.maccer.copy() + maccer.update(summary) + user = User.from_hashstring(string) + if user is None: + return False + session_duration = datetime.now() - user.timestamp + macs_equal = hmac.compare_digest( + maccer.hexdigest().encode("utf-8"), hash) + time_short = int(session_duration.total_seconds()) < self.max_duration + return macs_equal and (time_short or user.permanent) class StaticUserManager: @@ -151,59 +101,132 @@ class StaticUserManager: def authenticate(self, username, password): return (username in self.passwords - and self.passwords[username] == password) + and self.passwords[username] == password) def groups(self, username, password=None): if username in self.group_map: yield from self.group_map[username] def all_groups(self): - yield from list(set(group for group in groups.values())) - - -class PAMManager: - def __init__(self, obsolete=False): - self.pam = pam.pam() - self.obsolete = obsolete - - def authenticate(self, username, password): - return self.pam.authenticate(username, password) + yield from list(set(group for group in self.group_map.values())) + + +try: + import ldap3 + + class LdapManager: + def __init__(self, host, user_dn, group_dn, port=636, use_ssl=True, + obsolete=False): + self.server = ldap3.Server(host, port=port, use_ssl=use_ssl) + self.user_dn = user_dn + self.group_dn = group_dn + self.obsolete = obsolete + + def authenticate(self, username, password): + try: + connection = ldap3.Connection( + self.server, self.user_dn.format(username), password) + return connection.bind() + except ldap3.core.exceptions.LDAPSocketOpenError: + return False + + def groups(self, username, password=None): + connection = ldap3.Connection(self.server) + obj_def = ldap3.ObjectDef("posixgroup", connection) + group_reader = ldap3.Reader(connection, obj_def, self.group_dn) + username = username.lower() + for group in group_reader.search(): + members = group.memberUid.value + if members is not None and username in members: + yield group.cn.value + + def all_groups(self): + connection = ldap3.Connection(self.server) + obj_def = ldap3.ObjectDef("posixgroup", connection) + group_reader = ldap3.Reader(connection, obj_def, self.group_dn) + for group in group_reader.search(): + yield group.cn.value - def groups(self, username, password=None): - print(username) - yield grp.getgrgid(pwd.getpwnam(username).pw_gid).gr_name - for group in grp.getgrall(): - if username in group.gr_mem: + class ADManager: + def __init__(self, host, domain, user_dn, group_dn, + port=636, use_ssl=True, ca_cert=None, obsolete=False): + tls_config = ldap3.Tls(validate=ssl.CERT_REQUIRED) + if ca_cert is not None: + tls_config = ldap3.Tls( + validate=ssl.CERT_REQUIRED, ca_certs_file=ca_cert) + self.server = ldap3.Server( + host, port=port, use_ssl=use_ssl, tls=tls_config) + self.domain = domain + self.user_dn = user_dn + self.group_dn = group_dn + self.obsolete = obsolete + + def prepare_connection(self, username=None, password=None): + if username is not None and password is not None: + ad_user = "{}\\{}".format(self.domain, username) + return ldap3.Connection(self.server, ad_user, password) + return ldap3.Connection(self.server) + + def authenticate(self, username, password): + try: + return self.prepare_connection(username, password).bind() + except ldap3.core.exceptions.LDAPSocketOpenError: + return False + + def groups(self, username, password): + connection = self.prepare_connection(username, password) + connection.bind() + obj_def = ldap3.ObjectDef("user", connection) + name_filter = "cn:={}".format(username) + user_reader = ldap3.Reader( + connection, obj_def, self.user_dn, name_filter) + group_def = ldap3.ObjectDef("group", connection) + + def _yield_recursive_groups(group_dn): + group_reader = ldap3.Reader( + connection, group_def, group_dn, None) + for entry in group_reader.search(): + yield entry.name.value + for child in entry.memberOf: + yield from _yield_recursive_groups(child) + for result in user_reader.search(): + for group_dn in result.memberOf: + yield from _yield_recursive_groups(group_dn) + + def all_groups(self): + connection = self.prepare_connection() + connection.bind() + obj_def = ldap3.ObjectDef("group", connection) + group_reader = ldap3.Reader(connection, obj_def, self.group_dn) + for result in group_reader.search(): + yield result.name.value + +except ModuleNotFoundError: + pass + + +try: + import grp + import pwd + import pam + + class PAMManager: + def __init__(self, obsolete=False): + self.pam = pam.pam() + self.obsolete = obsolete + + def authenticate(self, username, password): + return self.pam.authenticate(username, password) + + def groups(self, username, password=None): + print(username) + yield grp.getgrgid(pwd.getpwnam(username).pw_gid).gr_name + for group in grp.getgrall(): + if username in group.gr_mem: + yield group.gr_name + + def all_groups(self): + for group in grp.getgrall(): yield group.gr_name - - def all_groups(self): - for group in grp.getgrall(): - yield group.gr_name - -class SecurityManager: - def __init__(self, key, max_duration=300): - self.maccer = hmac.new(key.encode("utf-8"), digestmod=hashlib.sha512) - self.max_duration = max_duration - - def hash_user(self, user): - maccer = self.maccer.copy() - summary = user.summarize() - maccer.update(summary.encode("utf-8")) - return "{}={}".format(summary, maccer.hexdigest()) - - def check_user(self, string): - parts = string.split("=", 1) - if len(parts) != 2: - # wrong format, expecting summary:hash - return False - summary, hash = map(lambda s: s.encode("utf-8"), parts) - maccer = self.maccer.copy() - maccer.update(summary) - user = User.from_hashstring(string) - if user is None: - return False - session_duration = datetime.now() - user.timestamp - macs_equal = hmac.compare_digest(maccer.hexdigest().encode("utf-8"), hash) - time_short = int(session_duration.total_seconds()) < self.max_duration - return macs_equal and (time_short or user.permanent) - +except ModuleNotFoundError: + pass diff --git a/back.py b/back.py new file mode 100644 index 0000000000000000000000000000000000000000..57c91f332a845c619caf29d9d3fb41dcc305201c --- /dev/null +++ b/back.py @@ -0,0 +1,27 @@ +# This snippet is in public domain. +# However, please retain this link in your sources: +# http://flask.pocoo.org/snippets/120/ +# Danya Alexeyevsky + +import functools +from flask import session, request, redirect as flask_redirect, url_for +import config + +cookie = getattr(config, "REDIRECT_BACK_COOKIE", "back") +default_view = getattr(config, "REDIRECT_BACK_DEFAULT", "index") + + +def anchor(func, cookie=cookie): + @functools.wraps(func) + def result(*args, **kwargs): + session[cookie] = request.url + return func(*args, **kwargs) + return result + + +def url(default=default_view, cookie=cookie, **url_args): + return session.get(cookie, url_for(default, **url_args)) + + +def redirect(default=default_view, cookie=cookie, **url_args): + return flask_redirect(url(default, cookie, **url_args)) diff --git a/calendarpush.py b/calendarpush.py index 804e1890d83d0ecb363abf370ea9f7af1df71ad2..5f0564a2d2d9ab72c6ff670a24d46fe635add969 100644 --- a/calendarpush.py +++ b/calendarpush.py @@ -2,15 +2,16 @@ from datetime import datetime, timedelta import random import quopri -from caldav import DAVClient, Principal, Calendar, Event -from caldav.lib.error import PropfindError +from caldav import DAVClient from vobject.base import ContentLine import config + class CalendarException(Exception): pass + class Client: def __init__(self, calendar=None, url=None): if not config.CALENDAR_ACTIVE: @@ -23,9 +24,12 @@ class Client: self.principal = self.client.principal() break except Exception as exc: - print("Got exception {} from caldav, retrying".format(str(exc))) + print("Got exception {} from caldav, retrying".format( + str(exc))) if self.principal is None: - raise CalendarException("Got {} CalDAV-error from the CalDAV server.".format(config.CALENDAR_MAX_REQUESTS)) + raise CalendarException( + "Got {} CalDAV-error from the CalDAV server.".format( + config.CALENDAR_MAX_REQUESTS)) if calendar is not None: self.calendar = self.get_calendar(calendar) else: @@ -41,9 +45,11 @@ class Client: for calendar in self.principal.calendars() ] except Exception as exc: - print("Got exception {} from caldav, retrying".format(str(exc))) - raise CalendarException("Got {} CalDAV Errors from the CalDAV server.".format(config.CALENDAR_MAX_REQUESTS)) - + print("Got exception {} from caldav, retrying".format( + str(exc))) + raise CalendarException( + "Got {} CalDAV Errors from the CalDAV server.".format( + config.CALENDAR_MAX_REQUESTS)) def get_calendar(self, calendar_name): candidates = self.principal.calendars() @@ -57,12 +63,14 @@ class Client: return candidates = [ Event.from_raw_event(raw_event) - for raw_event in self.calendar.date_search(begin, begin + timedelta(hours=1)) + for raw_event in self.calendar.date_search( + begin, begin + timedelta(hours=1)) ] candidates = [event for event in candidates if event.name == name] event = None if len(candidates) == 0: - event = Event(None, name, description, begin, + event = Event( + None, name, description, begin, begin + timedelta(hours=config.CALENDAR_DEFAULT_DURATION)) vevent = self.calendar.add_event(event.to_vcal()) event.vevent = vevent @@ -76,11 +84,14 @@ NAME_KEY = "summary" DESCRIPTION_KEY = "description" BEGIN_KEY = "dtstart" END_KEY = "dtend" + + def _get_item(content, key): if key in content: return content[key][0].value return None - + + class Event: def __init__(self, vevent, name, description, begin, end): self.vevent = vevent @@ -97,7 +108,8 @@ class Event: description = _get_item(content, DESCRIPTION_KEY) begin = _get_item(content, BEGIN_KEY) end = _get_item(content, END_KEY) - return Event(vevent=vevent, name=name, description=description, + return Event( + vevent=vevent, name=name, description=description, begin=begin, end=end) def set_description(self, description): @@ -105,7 +117,8 @@ class Event: self.description = description encoded = encode_quopri(description) if DESCRIPTION_KEY not in raw_event.contents: - raw_event.contents[DESCRIPTION_KEY] = [ContentLine(DESCRIPTION_KEY, {"ENCODING": ["QUOTED-PRINTABLE"]}, encoded)] + raw_event.contents[DESCRIPTION_KEY] = [ContentLine( + DESCRIPTION_KEY, {"ENCODING": ["QUOTED-PRINTABLE"]}, encoded)] else: content_line = raw_event.contents[DESCRIPTION_KEY][0] content_line.value = encoded @@ -129,21 +142,28 @@ SUMMARY:{summary} DESCRIPTION;ENCODING=QUOTED-PRINTABLE:{description} END:VEVENT END:VCALENDAR""".format( - uid=create_uid(), now=date_format(datetime.now()-offset), - begin=date_format(self.begin-offset), end=date_format(self.end-offset), + uid=create_uid(), + now=date_format(datetime.now() - offset), + begin=date_format(self.begin - offset), + end=date_format(self.end - offset), summary=self.name, description=encode_quopri(self.description)) + def create_uid(): return str(random.randint(0, 1e10)).rjust(10, "0") + def date_format(dt): return dt.strftime("%Y%m%dT%H%M%SZ") + def get_timezone_offset(): difference = datetime.now() - datetime.utcnow() - return timedelta(hours=round(difference.seconds / 3600 + difference.days * 24)) + return timedelta( + hours=round(difference.seconds / 3600 + difference.days * 24)) -def encode_quopri(text): - return quopri.encodestring(text.encode("utf-8")).replace(b"\n", b"=0A").decode("utf-8") +def encode_quopri(text): + return quopri.encodestring(text.encode("utf-8")).replace( + b"\n", b"=0A").decode("utf-8") diff --git a/check_routes.py b/check_routes.py index 5a577c037fb3c0078cfce2fb422e5abf7558ca0c..c7e8760d1aee5e9023fbec48e293137f36bf38be 100644 --- a/check_routes.py +++ b/check_routes.py @@ -3,14 +3,20 @@ import regex as re import os import sys -ROUTE_PATTERN = r'@(?:[[:alpha:]])+\.route\(\"(?<url>[^"]+)"[^)]*\)\s*(?:@[[:alpha:]_()., ]+\s*)*def\s+(?<name>[[:alpha:]][[:alnum:]_]*)\((?<params>[[:alnum:], ]*)\):' +ROUTE_PATTERN = ( + r'@(?:[[:alpha:]])+\.route\(\"(?<url>[^"]+)"[^)]*\)\s*' + r'(?:@[[:alpha:]_()., ]+\s*)*def\s+(?<name>[[:alpha:]][[:alnum:]_]*)' + r'\((?<params>[[:alnum:], ]*)\):') quote_group = "[\"']" -URL_FOR_PATTERN = r'url_for\({quotes}(?<name>[[:alpha:]][[:alnum:]_]*){quotes}'.format(quotes=quote_group) +URL_FOR_PATTERN = ( + r'url_for\({quotes}(?<name>[[:alpha:]][[:alnum:]_]*)' + '{quotes}'.format(quotes=quote_group)) ROOT_DIR = "." ENDINGS = [".py", ".html", ".txt"] MAX_DEPTH = 2 + def list_dir(dir, level=0): if level >= MAX_DEPTH: return @@ -23,7 +29,8 @@ def list_dir(dir, level=0): if file.endswith(ending): yield path elif os.path.isdir(path): - yield from list_dir(path, level+1) + yield from list_dir(path, level + 1) + class Route: def __init__(self, file, name, parameters): @@ -38,13 +45,15 @@ class Route: def get_parameter_set(self): return {parameter.name for parameter in self.parameters} + class Parameter: def __init__(self, name, type=None): self.name = name self.type = type def __repr__(self): - return "Parameter({name}, {type})".format(name=self.name, type=self.type) + return "Parameter({name}, {type})".format( + name=self.name, type=self.type) @staticmethod def from_string(text): @@ -53,6 +62,7 @@ class Parameter: return Parameter(name, type) return Parameter(text) + def split_url_parameters(url): params = [] current_param = None @@ -68,9 +78,11 @@ def split_url_parameters(url): current_param += char return params + def split_function_parameters(parameters): return list(map(str.strip, parameters.split(","))) + def read_url_for_parameters(content): params = [] bracket_level = 1 @@ -92,6 +104,7 @@ def read_url_for_parameters(content): elif char == ")": bracket_level -= 1 + class UrlFor: def __init__(self, file, name, parameters): self.file = file @@ -99,8 +112,10 @@ class UrlFor: self.parameters = parameters def __repr__(self): - return "UrlFor(file={file}, name={name}, parameters={parameters})".format( - file=self.file, name=self.name, parameters=self.parameters) + return ( + "UrlFor(file={file}, name={name}, parameters={parameters})".format( + file=self.file, name=self.name, parameters=self.parameters)) + routes = {} url_fors = [] @@ -109,24 +124,29 @@ for file in list_dir(ROOT_DIR): content = infile.read() for match in re.finditer(ROUTE_PATTERN, content): name = match.group("name") - function_parameters = split_function_parameters(match.group("params")) + function_parameters = split_function_parameters( + match.group("params")) url_parameters = split_url_parameters(match.group("url")) routes[name] = Route(file, name, url_parameters) for match in re.finditer(URL_FOR_PATTERN, content): name = match.group("name") begin, end = match.span() parameters = read_url_for_parameters(content[end:]) - url_fors.append(UrlFor(file=file, name=name, parameters=parameters)) + url_fors.append(UrlFor( + file=file, name=name, parameters=parameters)) + for url_for in url_fors: if url_for.name not in routes: - print("Missing route '{}' (for url_for in '{}')".format(url_for.name, url_for.file)) + print("Missing route '{}' (for url_for in '{}')".format( + url_for.name, url_for.file)) continue route = routes[url_for.name] route_parameters = route.get_parameter_set() url_parameters = set(url_for.parameters) if len(route_parameters ^ url_parameters) > 0: - print("Parameters not matching for '{}' in '{}:'".format(url_for.name, url_for.file)) + print("Parameters not matching for '{}' in '{}:'".format( + url_for.name, url_for.file)) only_route = route_parameters - url_parameters only_url = url_parameters - route_parameters if len(only_route) > 0: diff --git a/config.py.example b/config.py.example index d23beb8f7ffb7567deb7c8a8b3ae8f782aa46603..581ca2258cc1f74ba69b02a9fdcafc4f7e33565d 100644 --- a/config.py.example +++ b/config.py.example @@ -196,7 +196,7 @@ LATEX_BULLETPOINTS = [ # "logo": "asta-logo.tex", # optional: replaces the general template to include at the top of protocol.tex set by LATEX_LOGO_TEMPLATE # "geometry": "bottom=1.6cm,top=1.6cm,inner=2.5cm,outer=1.0cm,footskip=1.0cm,headsep=0.6cm", # optional: replaces the general protocol page geometry set by LATEX_GEOMETRY # "pagestyle": "fancy", # optional: replaces the general protocol pagestyle set by LATEX_PAGESTYLE -# "additionalpackages": ["[absolute]{textpos}", "{fancyheadings}"], # optional: replaces the general latex packages set by LATEX_ADDITIONAL_PACKAGES +# "additional_packages": ["[absolute]{textpos}", "{fancyheadings}"], # optional: replaces the general latex packages set by LATEX_ADDITIONAL_PACKAGES # "headerfooter": True # optional: replaces the general LATEX_HEADER_FOOTER option # } #} diff --git a/decorators.py b/decorators.py index 30fd142c8eedfdef29b4da748073397176a4ca32..17b73569653f0b514b910aa6ef152884252d2fb8 100644 --- a/decorators.py +++ b/decorators.py @@ -1,9 +1,11 @@ -from flask import redirect, flash, request, url_for +from flask import request, flash, abort from functools import wraps from models.database import ALL_MODELS -from shared import db, current_user +from shared import current_user +from utils import get_csrf_token +import back ID_KEY = "id" KEY_NOT_PRESENT_MESSAGE = "Missing {}_id." @@ -11,11 +13,14 @@ OBJECT_DOES_NOT_EXIST_MESSAGE = "There is no {} with id {}." MISSING_VIEW_RIGHT = "Dir fehlenden die nötigen Zugriffsrechte." + def default_redirect(): - return redirect(request.args.get("next") or url_for("index")) + return back.redirect() + def login_redirect(): - return redirect(request.args.get("next") or url_for("login")) + return back.redirect("login") + def db_lookup(*models, check_exists=True): def _decorator(function): @@ -31,7 +36,8 @@ def db_lookup(*models, check_exists=True): obj = model.query.filter_by(id=obj_id).first() if check_exists and obj is None: model_name = model.__class__.__name__ - flash(OBJECT_DOES_NOT_EXIST_MESSAGE.format(model_name, obj_id), + flash(OBJECT_DOES_NOT_EXIST_MESSAGE.format( + model_name, obj_id), "alert-error") return default_redirect() kwargs[key] = obj @@ -40,8 +46,10 @@ def db_lookup(*models, check_exists=True): return _decorated_function return _decorator + def require_right(right, require_exist): necessary_right_name = "has_{}_right".format(right) + def _decorator(function): @wraps(function) def _decorated_function(*args, **kwargs): @@ -64,17 +72,33 @@ def require_right(right, require_exist): return _decorated_function return _decorator + def require_public_view_right(require_exist=True): return require_right("public_view", require_exist) + def require_private_view_right(require_exist=True): return require_right("private_view", require_exist) + def require_modify_right(require_exist=True): return require_right("modify", require_exist) + def require_publish_right(require_exist=True): return require_right("publish", require_exist) + def require_admin_right(require_exist=True): return require_right("admin", require_exist) + + +def protect_csrf(function): + @wraps(function) + def _decorated_function(*args, **kwargs): + token = request.args.get("csrf_token") + if token != get_csrf_token(): + print(token, get_csrf_token()) + abort(400) + return function(*args, **kwargs) + return _decorated_function diff --git a/legacy.py b/legacy.py index 868458262da8a2b1dbf5fbb949a5e50db4dd6a82..507634ad3bef1afc5bfae78a6c34c8f8e913adff 100644 --- a/legacy.py +++ b/legacy.py @@ -1,16 +1,17 @@ from datetime import datetime -from fuzzywuzzy import fuzz, process -import tempfile +from fuzzywuzzy import process -from models.database import Todo, OldTodo, Protocol, ProtocolType, TodoMail +from models.database import OldTodo, Protocol, ProtocolType, TodoMail from shared import db import config + def lookup_todo_id(old_candidates, new_who, new_description): # Check for perfect matches for candidate in old_candidates: - if candidate.who == new_who and candidate.description == new_description: + if (candidate.who == new_who + and candidate.description == new_description): return candidate.old_id # Accept if who has been changed for candidate in old_candidates: @@ -32,11 +33,13 @@ def lookup_todo_id(old_candidates, new_who, new_description): new_description, best_match, best_match_score)) return None + INSERT_PROTOCOLTYPE = "INSERT INTO `protocolManager_protocoltype`" INSERT_PROTOCOL = "INSERT INTO `protocolManager_protocol`" INSERT_TODO = "INSERT INTO `protocolManager_todo`" INSERT_TODOMAIL = "INSERT INTO `protocolManager_todonamemailassignment`" + def import_old_protocols(sql_text): protocoltype_lines = [] protocol_lines = [] @@ -46,22 +49,27 @@ def import_old_protocols(sql_text): elif line.startswith(INSERT_PROTOCOL): protocol_lines.append(line) if (len(protocoltype_lines) == 0 - or len(protocol_lines) == 0): + or len(protocol_lines) == 0): raise ValueError("Necessary lines not found.") type_id_to_handle = {} for type_line in protocoltype_lines: - for id, handle, name, mail, protocol_id in _split_insert_line(type_line): + for id, handle, name, mail, protocol_id in _split_insert_line( + type_line): type_id_to_handle[int(id)] = handle.lower() protocols = [] for protocol_line in protocol_lines: for (protocol_id, old_type_id, date, source, textsummary, htmlsummary, - deleted, sent, document_id) in _split_insert_line(protocol_line): + deleted, sent, document_id) in _split_insert_line( + protocol_line): date = datetime.strptime(date, "%Y-%m-%d") handle = type_id_to_handle[int(old_type_id)] - protocoltype = ProtocolType.query.filter(ProtocolType.short_name.ilike(handle)).first() + protocoltype = ProtocolType.query.filter( + ProtocolType.short_name.ilike(handle)).first() if protocoltype is None: - raise KeyError("No protocoltype for handle '{}'.".format(handle)) - protocol = Protocol(protocoltype_id=protocoltype.id, date=date, source=source) + raise KeyError( + "No protocoltype for handle '{}'.".format(handle)) + protocol = Protocol( + protocoltype_id=protocoltype.id, date=date, source=source) db.session.add(protocol) db.session.commit() import tasks @@ -70,6 +78,7 @@ def import_old_protocols(sql_text): print(protocol.date) tasks.parse_protocol(protocol) + def import_old_todomails(sql_text): todomail_lines = [] for line in sql_text.splitlines(): @@ -98,28 +107,34 @@ def import_old_todos(sql_text): elif line.startswith(INSERT_TODO): todo_lines.append(line) if (len(protocoltype_lines) == 0 - or len(protocol_lines) == 0 - or len(todo_lines) == 0): + or len(protocol_lines) == 0 + or len(todo_lines) == 0): raise ValueError("Necessary lines not found.") type_id_to_handle = {} for type_line in protocoltype_lines: - for id, handle, name, mail, protocol_id in _split_insert_line(type_line): + for id, handle, name, mail, protocol_id in _split_insert_line( + type_line): type_id_to_handle[int(id)] = handle.lower() protocol_id_to_key = {} for protocol_line in protocol_lines: for (protocol_id, type_id, date, source, textsummary, htmlsummary, - deleted, sent, document_id) in _split_insert_line(protocol_line): + deleted, sent, document_id) in _split_insert_line( + protocol_line): handle = type_id_to_handle[int(type_id)] - date_string = date [2:] - protocol_id_to_key[int(protocol_id)] = "{}-{}".format(handle, date_string) + date_string = date[2:] + protocol_id_to_key[int(protocol_id)] = "{}-{}".format( + handle, date_string) todos = [] for todo_line in todo_lines: - for old_id, protocol_id, who, what, start_time, end_time, done in _split_insert_line(todo_line): + for (old_id, protocol_id, who, what, start_time, end_time, + done) in _split_insert_line(todo_line): protocol_id = int(protocol_id) if protocol_id not in protocol_id_to_key: - print("Missing protocol with ID {} for Todo {}".format(protocol_id, what)) + print("Missing protocol with ID {} for Todo {}".format( + protocol_id, what)) continue - todo = OldTodo(old_id=old_id, who=who, description=what, + todo = OldTodo( + old_id=old_id, who=who, description=what, protocol_key=protocol_id_to_key[protocol_id]) todos.append(todo) OldTodo.query.delete() @@ -127,12 +142,16 @@ def import_old_todos(sql_text): for todo in todos: db.session.add(todo) db.session.commit() - + + def _split_insert_line(line): insert_part, values_part = line.split("VALUES", 1) return _split_base_level(values_part) -def _split_base_level(text, begin="(", end=")", separator=",", string_terminator="'", line_end=";", ignore=" ", escape="\\"): + +def _split_base_level( + text, begin="(", end=")", separator=",", string_terminator="'", + line_end=";", ignore=" ", escape="\\"): raw_parts = [] current_part = None index = 0 @@ -210,5 +229,3 @@ def _split_base_level(text, begin="(", end=")", separator=",", string_terminator fields.append(current_field) parts.append(fields) return parts - - diff --git a/models/database.py b/models/database.py index 5304fbf3c9b72d5e56b094cf42cde3a65f3b6d11..69f2bde49162f95a58e579655af750dee82158e3 100644 --- a/models/database.py +++ b/models/database.py @@ -1,25 +1,26 @@ -from flask import render_template, send_file, url_for, redirect, flash, request +from flask import render_template -from datetime import datetime, time, date, timedelta -import math -from io import StringIO, BytesIO +from datetime import datetime +from io import BytesIO from enum import Enum from uuid import uuid4 -from shared import db, date_filter, date_filter_short, escape_tex, DATE_KEY, START_TIME_KEY, END_TIME_KEY, current_user -from utils import random_string, get_etherpad_url, split_terms, check_ip_in_networks +from shared import ( + db, date_filter_short, escape_tex, DATE_KEY, START_TIME_KEY, END_TIME_KEY, + current_user) +from utils import get_etherpad_url, split_terms, check_ip_in_networks from models.errors import DateNotMatchingException from dateutil import tz import os from sqlalchemy import event -from sqlalchemy.orm import relationship, backref, sessionmaker -from sqlalchemy.ext.hybrid import hybrid_method +from sqlalchemy.orm import relationship, backref import config from todostates import make_states + class DatabaseModel(db.Model): __abstract__ = True @@ -48,6 +49,11 @@ class DatabaseModel(db.Model): columns.append("{}={}".format(column_name, value)) return "{}({})".format(self.__class__.__name__, ", ".join(columns)) + @classmethod + def first_by_id(cls, instance_id): + return cls.query.filter_by(id=instance_id).first() + + class ProtocolType(DatabaseModel): __tablename__ = "protocoltypes" __model_name__ = "protocoltype" @@ -73,46 +79,70 @@ class ProtocolType(DatabaseModel): allowed_networks = db.Column(db.String) latex_template = db.Column(db.String) - protocols = relationship("Protocol", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="Protocol.id") - default_tops = relationship("DefaultTOP", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="DefaultTOP.number") - reminders = relationship("MeetingReminder", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="MeetingReminder.days_before") - todos = relationship("Todo", backref=backref("protocoltype"), order_by="Todo.id") - metas = relationship("DefaultMeta", backref=backref("protocoltype"), cascade="all, delete-orphan") - decisioncategories = relationship("DecisionCategory", backref=backref("protocoltype"), cascade="all, delete-orphan") + protocols = relationship( + "Protocol", backref=backref("protocoltype"), + cascade="all, delete-orphan", order_by="Protocol.id") + default_tops = relationship( + "DefaultTOP", backref=backref("protocoltype"), + cascade="all, delete-orphan", order_by="DefaultTOP.number") + reminders = relationship( + "MeetingReminder", backref=backref("protocoltype"), + cascade="all, delete-orphan", order_by="MeetingReminder.days_before") + todos = relationship( + "Todo", backref=backref("protocoltype"), order_by="Todo.id") + metas = relationship( + "DefaultMeta", backref=backref("protocoltype"), + cascade="all, delete-orphan") + decisioncategories = relationship( + "DecisionCategory", backref=backref("protocoltype"), + cascade="all, delete-orphan") def get_latest_protocol(self): - candidates = sorted([protocol for protocol in self.protocols if protocol.is_done()], key=lambda p: p.date, 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] def has_public_view_right(self, user, check_networks=True): - return (self.has_public_anonymous_view_right(check_networks=check_networks) - or (user is not None and self.has_public_authenticated_view_right(user)) + return ( + self.has_public_anonymous_view_right(check_networks=check_networks) + or (user is not None + and self.has_public_authenticated_view_right(user)) or self.has_admin_right(user)) def has_public_anonymous_view_right(self, check_networks=True): - return (self.is_public + return ( + self.is_public and ((not self.restrict_networks or not check_networks) - or check_ip_in_networks(self.allowed_networks))) + or check_ip_in_networks(self.allowed_networks))) def has_public_authenticated_view_right(self, user): - return ((self.public_group != "" and self.public_group in user.groups) - or (self.private_group != "" and self.private_group in user.groups)) + return ( + (self.public_group != "" and self.public_group in user.groups) + or (self.private_group != "" + and self.private_group in user.groups)) def has_private_view_right(self, user): - return ((user is not None - and (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)) or self.has_admin_right(user)) def has_modify_right(self, user): - return ((user is not None - and (self.modify_group != "" and self.modify_group in user.groups)) + return ( + (user is not None + and (self.modify_group != "" + and self.modify_group in user.groups)) or self.has_admin_right(user)) def has_publish_right(self, user): - return ((user is not None - and (self.publish_group != "" and self.publish_group in user.groups)) + return ( + (user is not None + and (self.publish_group != "" + and self.publish_group in user.groups)) or self.has_admin_right(user)) def has_admin_right(self, user): @@ -129,7 +159,8 @@ class ProtocolType(DatabaseModel): def get_public_protocoltypes(user, check_networks=True): return [ protocoltype for protocoltype in ProtocolType.query.all() - if protocoltype.has_public_view_right(user, check_networks=check_networks) + if protocoltype.has_public_view_right( + user, check_networks=check_networks) ] @staticmethod @@ -163,12 +194,22 @@ class Protocol(DatabaseModel): public = db.Column(db.Boolean) pad_identifier = db.Column(db.String) - tops = relationship("TOP", backref=backref("protocol"), cascade="all, delete-orphan", order_by="TOP.number") - decisions = relationship("Decision", backref=backref("protocol"), cascade="all, delete-orphan", order_by="Decision.id") - 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") - metas = relationship("Meta", backref=backref("protocol"), cascade="all, delete-orphan") - localtops = relationship("LocalTOP", backref=backref("protocol"), cascade="all, delete-orphan") + tops = relationship( + "TOP", backref=backref("protocol"), + cascade="all, delete-orphan", order_by="TOP.number") + decisions = relationship( + "Decision", backref=backref("protocol"), + cascade="all, delete-orphan", order_by="Decision.id") + 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") + metas = relationship( + "Meta", backref=backref("protocol"), cascade="all, delete-orphan") + localtops = relationship( + "LocalTOP", backref=backref("protocol"), cascade="all, delete-orphan") likes = relationship("Like", secondary="likeprotocolassociations") @@ -177,14 +218,16 @@ class Protocol(DatabaseModel): def create_error(self, action, name, description): now = datetime.now() - return Error(protocol_id=self.id, action=action, name=name, + return Error( + protocol_id=self.id, action=action, name=name, datetime=now, description=description) def create_localtops(self): local_tops = [] for default_top in self.protocoltype.default_tops: - local_tops.append(LocalTOP(defaulttop_id=default_top.id, - protocol_id=self.id, description=default_top.description or "")) + local_tops.append(LocalTOP( + defaulttop_id=default_top.id, protocol_id=self.id, + description=default_top.description or "")) return local_tops def fill_from_remarks(self, remarks): @@ -211,7 +254,8 @@ class Protocol(DatabaseModel): new_date = _date_or_lazy(DATE_KEY, get_date=True) if self.date is not None: if new_date != self.date: - raise DateNotMatchingException(original_date=self.date, protocol_date=new_date) + raise DateNotMatchingException( + original_date=self.date, protocol_date=new_date) else: self.date = new_date if START_TIME_KEY in remarks: @@ -225,7 +269,9 @@ class Protocol(DatabaseModel): for default_meta in self.protocoltype.metas: if default_meta.key in remarks: value = remarks[default_meta.key].value.strip() - meta = Meta(protocol_id=self.id, name=default_meta.name, value=value, internal=default_meta.internal) + meta = Meta( + protocol_id=self.id, name=default_meta.name, value=value, + internal=default_meta.internal) db.session.add(meta) db.session.commit() @@ -235,16 +281,21 @@ class Protocol(DatabaseModel): or self.protocoltype.has_private_view_right(user) ) + def get_visible_content(self, user): + if self.has_private_view_right(user): + return self.content_private + return self.content_public + def is_done(self): return self.done def get_state_glyph(self): if self.is_done(): - state = "unchecked" #"Fertig" + state = "unchecked" # Fertig if self.public: - state = "check" #"Veröffentlicht" + state = "check" # Veröffentlicht else: - state = "pencil" #"Geplant" + state = "pencil" # Geplant return state def get_state_name(self): @@ -280,7 +331,9 @@ class Protocol(DatabaseModel): if self.pad_identifier is None: identifier = self.get_identifier() if self.protocoltype.non_reproducible_pad_links: - identifier = "{}-{}".format(identifier, str(uuid4()).replace("-", ""))[:50] + identifier = "{}-{}".format( + identifier, + str(uuid4()).replace("-", ""))[:50] self.pad_identifier = identifier db.session.commit() return get_etherpad_url(self.pad_identifier) @@ -292,13 +345,18 @@ class Protocol(DatabaseModel): def get_datetime(self): time = self.get_time() - return datetime(self.date.year, self.date.month, self.date.day, time.hour, time.minute) + return datetime( + self.date.year, self.date.month, self.date.day, time.hour, + time.minute) 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()] + return [ + todo for todo in self.todos + if self == todo.get_first_protocol() + ] def get_open_todos(self): return [ @@ -310,7 +368,7 @@ class Protocol(DatabaseModel): candidates = [ document for document in self.documents if document.is_compiled - and (private is None or document.is_private == private) + and (private is None or document.is_private == private) ] return len(candidates) > 0 @@ -318,10 +376,16 @@ class Protocol(DatabaseModel): candidates = [ document for document in self.documents if document.is_compiled - and (private is None or document.is_private == private) + and (private is None or document.is_private == private) + ] + private_candidates = [ + document for document in candidates + if document.is_private + ] + public_candidates = [ + document for document in candidates + if not document.is_private ] - private_candidates = [document for document in candidates if document.is_private] - public_candidates = [document for document in candidates if not document.is_private] if len(private_candidates) > 0: return private_candidates[0] elif len(public_candidates) > 0: @@ -359,15 +423,16 @@ class Protocol(DatabaseModel): def create_new_protocol(protocoltype, date, start_time=None): if start_time is None: start_time = protocoltype.usual_time - protocol = Protocol(protocoltype_id=protocoltype.id, - date=date, start_time=start_time) + protocol = Protocol( + protocoltype_id=protocoltype.id, date=date, start_time=start_time) db.session.add(protocol) db.session.commit() for local_top in protocol.create_localtops(): db.session.add(local_top) for default_meta in protocoltype.metas: if default_meta.prior: - meta = Meta(protocol_id=protocol.id, name=default_meta.name, + meta = Meta( + protocol_id=protocol.id, name=default_meta.name, internal=default_meta.internal, value=default_meta.value) db.session.add(meta) db.session.commit() @@ -375,7 +440,6 @@ class Protocol(DatabaseModel): tasks.push_tops_to_calendar(protocol) return protocol - @event.listens_for(Protocol, "before_delete") def on_protocol_delete(mapper, connection, protocol): @@ -391,7 +455,9 @@ class DefaultTOP(DatabaseModel): number = db.Column(db.Integer) description = db.Column(db.String) - localtops = relationship("LocalTOP", backref=backref("defaulttop"), cascade="all, delete-orphan") + localtops = relationship( + "LocalTOP", backref=backref("defaulttop"), + cascade="all, delete-orphan") def get_parent(self): return self.protocoltype @@ -400,15 +466,17 @@ class DefaultTOP(DatabaseModel): return self.number > 0 def get_localtop(self, protocol): - return LocalTOP.query.filter_by(defaulttop_id=self.id, - protocol_id=protocol.id).first() + return LocalTOP.query.filter_by( + defaulttop_id=self.id, protocol_id=protocol.id).first() def get_top(self, protocol): localtop = self.get_localtop(protocol) - top = TOP(protocol_id=protocol.id, name=self.name, + top = TOP( + protocol_id=protocol.id, name=self.name, description=localtop.description) return top + class TOP(DatabaseModel): __tablename__ = "tops" __model_name__ = "top" @@ -424,6 +492,7 @@ class TOP(DatabaseModel): def get_parent(self): return self.protocol + class LocalTOP(DatabaseModel): __tablename__ = "localtops" __model_name__ = "localtop" @@ -437,7 +506,8 @@ class LocalTOP(DatabaseModel): def is_expandable(self): user = current_user() - return (self.has_private_view_right(user) + return ( + self.has_private_view_right(user) and self.description is not None and len(self.description) > 0) @@ -447,6 +517,7 @@ class LocalTOP(DatabaseModel): classes.append("expansion-button") return classes + class Document(DatabaseModel): __tablename__ = "documents" __model_name__ = "document" @@ -467,6 +538,7 @@ class Document(DatabaseModel): with open(self.get_filename(), "rb") as file: return BytesIO(file.read()) + @event.listens_for(Document, "before_delete") def on_document_delete(mapper, connection, document): if document.filename is not None: @@ -474,6 +546,7 @@ def on_document_delete(mapper, connection, document): if os.path.isfile(document_path): os.remove(document_path) + class DecisionDocument(DatabaseModel): __tablename__ = "decisiondocuments" __model_name__ = "decisiondocument" @@ -492,6 +565,7 @@ class DecisionDocument(DatabaseModel): with open(self.get_filename(), "rb") as file: return BytesIO(file.read()) + @event.listens_for(DecisionDocument, "before_delete") def on_decisions_document_delete(mapper, connection, document): if document.filename is not None: @@ -499,6 +573,7 @@ def on_decisions_document_delete(mapper, connection, document): if os.path.isfile(document_path): os.remove(document_path) + class TodoState(Enum): open = 0 waiting = 1 @@ -513,7 +588,7 @@ class TodoState(Enum): def get_name(self): STATE_TO_NAME, NAME_TO_STATE = make_states(TodoState) return STATE_TO_NAME[self] - + @staticmethod def get_name_to_state(): STATE_TO_NAME, NAME_TO_STATE = make_states(TodoState) @@ -550,8 +625,10 @@ class TodoState(Enum): @staticmethod def from_name_with_date(name, protocol=None): name = name.strip().lower() - if not " " in name: - raise ValueError("{} does definitely not contain a state and a date".format(name)) + if " " not in name: + raise ValueError( + "{} does not contain a state and a date".format( + name)) name_part, date_part = name.split(" ", 1) state = TodoState.from_name(name_part) date = None @@ -566,7 +643,8 @@ class TodoState(Enum): year = datetime.now().year if protocol is not None: year = protocol.date.year - date = datetime(year=year, month=date.month, day=date.day).date() + date = datetime( + year=year, month=date.month, day=date.day).date() break except ValueError as exc: last_exc = exc @@ -587,7 +665,8 @@ class Todo(DatabaseModel): state = db.Column(db.Enum(TodoState), nullable=False) date = db.Column(db.Date, nullable=True) - protocols = relationship("Protocol", secondary="todoprotocolassociations", backref="todos") + protocols = relationship( + "Protocol", secondary="todoprotocolassociations", backref="todos") likes = relationship("Like", secondary="liketodoassociations") def get_parent(self): @@ -618,11 +697,13 @@ class Todo(DatabaseModel): def get_state(self): return "[{}]".format(self.get_state_plain()) + def get_state_plain(self): result = self.state.get_name() if self.state.needs_date(): result = "{} {}".format(result, date_filter_short(self.date)) return result + def get_state_tex(self): return self.get_state_plain() @@ -651,7 +732,8 @@ class Todo(DatabaseModel): bold = "'''" if use_dokuwiki: bold = "**" - return "{0}{1}:{0} {2}: {3} - {4}".format(bold, + return "{0}{1}:{0} {2}: {3} - {4}".format( + bold, "Neuer Todo" if self.is_new(current_protocol) else "Todo", self.who, self.description, @@ -665,10 +747,14 @@ class Todo(DatabaseModel): parts.append("id {}".format(self.get_id())) return "[{}]".format(";".join(parts)) + class TodoProtocolAssociation(DatabaseModel): __tablename__ = "todoprotocolassociations" - todo_id = db.Column(db.Integer, db.ForeignKey("todos.id"), primary_key=True) - protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"), primary_key=True) + todo_id = db.Column( + db.Integer, db.ForeignKey("todos.id"), primary_key=True) + protocol_id = db.Column( + db.Integer, db.ForeignKey("protocols.id"), primary_key=True) + class Decision(DatabaseModel): __tablename__ = "decisions" @@ -677,9 +763,12 @@ class Decision(DatabaseModel): protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id")) content = db.Column(db.String) - document = relationship("DecisionDocument", backref=backref("decision"), cascade="all, delete-orphan", uselist=False) + document = relationship( + "DecisionDocument", backref=backref("decision"), + cascade="all, delete-orphan", uselist=False) - categories = relationship("DecisionCategory", secondary="decisioncategoryassociations") + categories = relationship( + "DecisionCategory", secondary="decisioncategoryassociations") likes = relationship("Like", secondary="likedecisionassociations") def get_parent(self): @@ -688,6 +777,7 @@ class Decision(DatabaseModel): def get_categories_str(self): return ", ".join(map(lambda c: c.name, self.categories)) + class DecisionCategory(DatabaseModel): __tablename__ = "decisioncategories" __model_name__ = "decisioncategory" @@ -698,10 +788,14 @@ class DecisionCategory(DatabaseModel): def get_parent(self): return self.protocoltype + class DecisionCategoryAssociation(DatabaseModel): __tablename__ = "decisioncategoryassociations" - decision_id = db.Column(db.Integer, db.ForeignKey("decisions.id"), primary_key=True) - decisioncategory_id = db.Column(db.Integer, db.ForeignKey("decisioncategories.id"), primary_key=True) + decision_id = db.Column( + db.Integer, db.ForeignKey("decisions.id"), primary_key=True) + decisioncategory_id = db.Column( + db.Integer, db.ForeignKey("decisioncategories.id"), primary_key=True) + class MeetingReminder(DatabaseModel): __tablename__ = "meetingreminders" @@ -716,6 +810,7 @@ class MeetingReminder(DatabaseModel): def get_parent(self): return self.protocoltype + class Error(DatabaseModel): __tablename__ = "errors" __model_name__ = "error" @@ -737,6 +832,7 @@ class Error(DatabaseModel): return "\n".join(lines) return "\n".join(["\n".join(lines[:2]), "…", "\n".join(lines[-2:])]) + class TodoMail(DatabaseModel): __tablename__ = "todomails" __model_name__ = "todomail" @@ -747,6 +843,7 @@ class TodoMail(DatabaseModel): def get_formatted_mail(self): return "{} <{}>".format(self.name, self.mail) + class OldTodo(DatabaseModel): __tablename__ = "oldtodos" __model_name__ = "oldtodo" @@ -756,6 +853,7 @@ class OldTodo(DatabaseModel): description = db.Column(db.String) protocol_key = db.Column(db.String) + class DefaultMeta(DatabaseModel): __tablename__ = "defaultmetas" __model_name__ = "defaultmeta" @@ -770,6 +868,7 @@ class DefaultMeta(DatabaseModel): def get_parent(self): return self.protocoltype + class Meta(DatabaseModel): __tablename__ = "metas" __model_name__ = "meta" @@ -782,30 +881,42 @@ class Meta(DatabaseModel): def get_parent(self): return self.protocol + class Like(DatabaseModel): __tablename__ = "likes" __model_name__ = "like" id = db.Column(db.Integer, primary_key=True) who = db.Column(db.String) + class LikeProtocolAssociation(DatabaseModel): __tablename__ = "likeprotocolassociations" - like_id = db.Column(db.Integer, db.ForeignKey("likes.id"), primary_key=True) - protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"), primary_key=True) + like_id = db.Column( + db.Integer, db.ForeignKey("likes.id"), primary_key=True) + protocol_id = db.Column( + db.Integer, db.ForeignKey("protocols.id"), primary_key=True) + class LikeTodoAssociation(DatabaseModel): __tablename__ = "liketodoassociations" - like_id = db.Column(db.Integer, db.ForeignKey("likes.id"), primary_key=True) - todo_id = db.Column(db.Integer, db.ForeignKey("todos.id"), primary_key=True) + like_id = db.Column( + db.Integer, db.ForeignKey("likes.id"), primary_key=True) + todo_id = db.Column( + db.Integer, db.ForeignKey("todos.id"), primary_key=True) + class LikeDecisionAssociation(DatabaseModel): __tablename__ = "likedecisionassociations" - like_id = db.Column(db.Integer, db.ForeignKey("likes.id"), primary_key=True) - decision_id = db.Column(db.Integer, db.ForeignKey("decisions.id"), primary_key=True) + like_id = db.Column( + db.Integer, db.ForeignKey("likes.id"), primary_key=True) + decision_id = db.Column( + db.Integer, db.ForeignKey("decisions.id"), primary_key=True) + class LikeTOPAssociation(DatabaseModel): __tablename__ = "liketopassociations" - like_id = db.Column(db.Integer, db.ForeignKey("likes.id"), primary_key=True) + like_id = db.Column( + db.Integer, db.ForeignKey("likes.id"), primary_key=True) top_id = db.Column(db.Integer, db.ForeignKey("tops.id"), primary_key=True) diff --git a/protoparser.py b/protoparser.py index bee63917caf77d0178d08d90adef684569bcaad7..5bcba3d38c475e6f0f9311fbed5dcec501b4fb56 100644 --- a/protoparser.py +++ b/protoparser.py @@ -10,10 +10,11 @@ import config INDENT_LETTER = "-" + class ParserException(Exception): name = "Parser Exception" has_explanation = False - #explanation = "The source did generally not match the expected protocol syntax." + def __init__(self, message, linenumber=None, tree=None): self.message = message self.linenumber = linenumber @@ -22,13 +23,15 @@ class ParserException(Exception): def __str__(self): result = "" if self.linenumber is not None: - result = "Exception at line {}: {}".format(self.linenumber, self.message) + result = "Exception at line {}: {}".format( + self.linenumber, self.message) else: result = "Exception: {}".format(self.message) if self.has_explanation: result += "\n" + self.explanation return result + class RenderType(Enum): latex = 0 wikitext = 1 @@ -36,8 +39,12 @@ class RenderType(Enum): html = 3 dokuwiki = 4 + def _not_implemented(self, render_type): - return NotImplementedError("The rendertype {} has not been implemented for {}.".format(render_type.name, self.__class__.__name__)) + return NotImplementedError( + "The rendertype {} has not been implemented for {}.".format( + render_type.name, self.__class__.__name__)) + class Element: """ @@ -63,13 +70,15 @@ class Element: Parses a match of this elements pattern. Arguments: - match: the match of this elements pattern - - current: the current element of the document. Should be a fork. May be modified. + - current: the current element of the document. Should be a fork. + May be modified. - linenumber: the current line number, for error messages Returns: - the new current element - the line number after parsing this element """ - raise ParserException("Trying to parse the generic base element!", linenumber) + raise ParserException( + "Trying to parse the generic base element!", linenumber) @staticmethod def parse_inner(match, current, linenumber=None): @@ -104,7 +113,8 @@ class Element: element.fork = current return current - PATTERN = r"x(?<!x)" # yes, a master piece, but it should never be called + PATTERN = r"x(?<!x)" + class Content(Element): def __init__(self, children, linenumber): @@ -112,7 +122,9 @@ class Content(Element): self.linenumber = linenumber def render(self, render_type, show_private, level=None, protocol=None): - return "".join(map(lambda e: e.render(render_type, show_private, level=level, protocol=protocol), self.children)) + return "".join(map(lambda e: e.render( + render_type, show_private, level=level, protocol=protocol), + self.children)) def dump(self, level=None): if level is None: @@ -123,14 +135,18 @@ class Content(Element): return "\n".join(result_lines) def get_tags(self, tags): - tags.extend([child for child in self.children if isinstance(child, Tag)]) + tags.extend([ + child for child in self.children + if isinstance(child, Tag) + ]) return tags @staticmethod def parse(match, current, linenumber=None): linenumber = Element.parse_inner(match, current, linenumber) if match.group("content") is None: - raise ParserException("Content is missing its content!", linenumber) + raise ParserException( + "Content is missing its content!", linenumber) content = match.group("content") element = Content.from_content(content, current, linenumber) if len(content) == 0: @@ -147,23 +163,22 @@ class Content(Element): match = pattern.match(content) if match is not None: matched = True - children.append(TEXT_PATTERNS[pattern](match, current, linenumber)) + children.append(TEXT_PATTERNS[pattern]( + match, current, linenumber)) content = content[len(match.group()):] break if not matched: - raise ParserException("Dies ist kein valider Tag! (mögliche Tags sind: {})", linenumber, ", ".join(Tag.KNOWN_TAGS)) + raise ParserException( + "Dies ist kein valider Tag! " + "(mögliche Tags sind: {})".format( + ", ".join(Tag.KNOWN_TAGS)), + linenumber) return Content(children, linenumber) - # v1: has problems with missing semicolons - #PATTERN = r"\s*(?<content>(?:[^\[\];]+)?(?:\[[^\]]+\][^;\[\]]*)*);" - # v2: does not require the semicolon, but the newline - #PATTERN = r"\s*(?<content>(?:[^\[\];\r\n]+)?(?:\[[^\]\r\n]+\][^;\[\]\r\n]*)*);?" - # v3: does not allow braces in the content - #PATTERN = r"\s*(?<content>(?:[^\[\];\r\n{}]+)?(?:\[[^\]\r\n{}]+\][^;\[\]\r\n{}]*)*);?" - # v4: do not allow empty match (require either the first or the second part to be non-empty) - PATTERN = r"\s*(?<content>(?:(?:[^\[\];\r\n{}]+)|(?:[^\[\];\r\n{}]+)?(?:\[[^\]\r\n{}]+\][^;\[\]\r\n{}]*)+));?" - # v5: do match emptystring if followed by a semi colon - #PATTERN = r"\s*(?<content>(?:[^\[\];\r\n{}]+);?|(?:[^\[\];\r\n{}]+)?(?:\[[^\]\r\n{}]+\][^;\[\]\r\n{}]*)+;?|;)" + PATTERN = ( + r"\s*(?<content>(?:(?:[^\[\];\r\n{}]+)|(?:[^\[\];\r\n{}]+)?" + r"(?:\[[^\]\r\n{}]+\][^;\[\]\r\n{}]*)+));?") + class Text: def __init__(self, text, linenumber, fork): @@ -199,9 +214,6 @@ class Text: raise ParserException("Text is empty!", linenumber) return Text(content, linenumber, current) - # v1: does not allow any [, as that is part of a tag - # PATTERN = r"(?<text>[^\[]+)(?:(?=\[)|$)" - # v2: does allow one [ at the beginning, which is used if it did not match a tag PATTERN = r"(?<text>\[?[^\[{}]+)(?:(?=\[)|$)" @@ -222,12 +234,16 @@ class Tag: return self.todo.render_latex(current_protocol=protocol) elif self.name == "beschluss": if len(self.decision.categories): - return r"\Beschluss[{}]{{{}}}".format(self.decision.get_categories_str(),self.decision.content) + return r"\Beschluss[{}]{{{}}}".format( + escape_tex(self.decision.get_categories_str()), + escape_tex(self.decision.content)) else: return r"\Beschluss{{{}}}".format(self.decision.content) elif self.name == "footnote": return r"\footnote{{{}}}".format(self.values[0]) - return r"\textbf{{{}:}} {}".format(escape_tex(self.name.capitalize()), escape_tex(";".join(self.values))) + return r"\textbf{{{}:}} {}".format( + escape_tex(self.name.capitalize()), + escape_tex(";".join(self.values))) elif render_type == RenderType.plaintext: if self.name == "url": return self.values[0] @@ -237,7 +253,8 @@ class Tag: return self.values[0] elif self.name == "footnote": return "[^]({})".format(self.values[0]) - return "{}: {}".format(self.name.capitalize(), ";".join(self.values)) + return "{}: {}".format( + self.name.capitalize(), ";".join(self.values)) elif render_type == RenderType.wikitext: if self.name == "url": return "[{0} {0}]".format(self.values[0]) @@ -247,7 +264,8 @@ class Tag: return self.todo.render_wikitext(current_protocol=protocol) elif self.name == "footnote": return "<ref>{}</ref>".format(self.values[0]) - return "'''{}:''' {}".format(self.name.capitalize(), ";".join(self.values)) + return "'''{}:''' {}".format( + self.name.capitalize(), ";".join(self.values)) elif render_type == RenderType.html: if self.name == "url": return "<a href=\"{0}\">{0}</a>".format(self.values[0]) @@ -268,8 +286,9 @@ class Tag: else: return "<b>Beschluss:</b> {}".format(self.values[0]) elif self.name == "footnote": - return '<sup id="#fnref{0}"><a href="#fn{0}">Fn</a></sup>'.format( - footnote_hash(self.values[0])) + return ( + '<sup id="#fnref{0}"><a href="#fn{0}">Fn</a></sup>'.format( + footnote_hash(self.values[0]))) return "[{}: {}]".format(self.name, ";".join(self.values)) elif render_type == RenderType.dokuwiki: if self.name == "url": @@ -277,21 +296,24 @@ class Tag: elif self.name == "todo": if not show_private: return "" - return self.todo.render_wikitext(current_protocol=protocol, - use_dokuwiki=True) + return self.todo.render_wikitext( + current_protocol=protocol, use_dokuwiki=True) elif self.name == "beschluss": - return "**{}:** {}".format(self.name.capitalize(), ";".join(self.values)) + return "**{}:** {}".format( + self.name.capitalize(), ";".join(self.values)) elif self.name == "footnote": return "(({}))".format(self.values[0]) else: - return "**{}:** {}".format(self.name.capitalize(), ";".join(self.values)) + return "**{}:** {}".format( + self.name.capitalize(), ";".join(self.values)) else: raise _not_implemented(self, render_type) def dump(self, level=None): if level is None: level = 0 - return "{}tag: {}: {}".format(INDENT_LETTER * level, self.name, "; ".join(self.values)) + return "{}tag: {}: {}".format( + INDENT_LETTER * level, self.name, "; ".join(self.values)) @staticmethod def parse(match, current, linenumber): @@ -302,12 +324,7 @@ class Tag: raise ParserException("Tag is empty!", linenumber) parts = content.split(";") return Tag(parts[0], parts[1:], linenumber, current) - - # v1: matches [text without semicolons] - #PATTERN = r"\[(?<content>(?:[^;\]]*;)*(?:[^;\]]*))\]" - # v2: needs at least two parts separated by a semicolon - #PATTERN = r"\[(?<content>(?:[^;\]]*;)+(?:[^;\]]*))\]" - # v3: also match [] without semicolons inbetween, as there is not other use for that + PATTERN = r"\[(?<content>[^\]]*)\]" KNOWN_TAGS = ["todo", "url", "beschluss", "footnote", "sitzung"] @@ -332,6 +349,7 @@ class Empty(Element): PATTERN = r"(?:\s+|;)" + class Remark(Element): def __init__(self, name, value, linenumber): self.name = name @@ -351,12 +369,12 @@ class Remark(Element): return r"{}: {}\\".format(self.name, self.value) else: raise _not_implemented(self, render_type) - def dump(self, level=None): if level is None: level = 0 - return "{}remark: {}: {}".format(INDENT_LETTER * level, self.name, self.value) + return "{}remark: {}: {}".format( + INDENT_LETTER * level, self.name, self.value) def get_tags(self, tags): return tags @@ -377,10 +395,11 @@ class Remark(Element): PATTERN = r"\s*\#(?<content>[^\n]+)" + class Fork(Element): def __init__(self, is_top, name, parent, linenumber, children=None): self.is_top = is_top - self.name = name.strip() if (name is not None and len(name) > 0) else None + self.name = name.strip() if name else None self.parent = parent self.linenumber = linenumber self.children = [] if children is None else children @@ -388,7 +407,12 @@ class Fork(Element): def dump(self, level=None): if level is None: level = 0 - result_lines = ["{}fork: {}'{}'".format(INDENT_LETTER * level, "TOP " if self.is_top else "", self.name)] + result_lines = [ + "{}fork: {}'{}'".format( + INDENT_LETTER * level, + "TOP " if self.is_top else "", + self.name) + ] for child in self.children: result_lines.append(child.dump(level + 1)) return "\n".join(result_lines) @@ -408,7 +432,9 @@ class Fork(Element): end_line = r"\end{itemize}" content_parts = [] for child in self.children: - part = child.render(render_type, show_private, level=level+1, protocol=protocol) + part = child.render( + render_type, show_private, level=level + 1, + protocol=protocol) if len(part.strip()) == 0: continue if not part.startswith(r"\item"): @@ -421,27 +447,36 @@ class Fork(Element): return "\n".join([begin_line, content_lines, end_line]) elif self.test_private(self.name): if show_private: - return (r"\begin{tcolorbox}[breakable,title=Interner Abschnitt]" + "\n" + return (r"\begin{tcolorbox}[breakable,title=Interner " + r"Abschnitt]" + "\n" + r"\begin{itemize}" + "\n" + content_lines + "\n" + r"\end{itemize}" + "\n" + r"\end{tcolorbox}") else: - return r"\textit{[An dieser Stelle wurde intern protokolliert.]}" + return (r"\textit{[An dieser Stelle wurde intern " + r"protokolliert.]}") else: - return "\n".join([escape_tex(name_line), begin_line, content_lines, end_line]) - elif render_type == RenderType.wikitext or render_type == RenderType.dokuwiki: + return "\n".join([ + escape_tex(name_line), begin_line, + content_lines, end_line + ]) + elif (render_type == RenderType.wikitext + or render_type == RenderType.dokuwiki): equal_signs = level + 2 if render_type == RenderType.dokuwiki: equal_signs = 6 - level title_line = "{0} {1} {0}".format("=" * equal_signs, name_line) content_parts = [] for child in self.children: - part = child.render(render_type, show_private, level=level+1, protocol=protocol) + part = child.render( + render_type, show_private, level=level + 1, + protocol=protocol) if len(part.strip()) == 0: continue content_parts.append(part) - content_lines = "{}\n\n{}\n".format(title_line, "\n\n".join(content_parts)) + content_lines = "{}\n\n{}\n".format( + title_line, "\n\n".join(content_parts)) if self.test_private(self.name) and not show_private: return "" else: @@ -450,11 +485,14 @@ class Fork(Element): title_line = "{} {}".format("#" * (level + 1), name_line) content_parts = [] for child in self.children: - part = child.render(render_type, show_private, level=level+1, protocol=protocol) + part = child.render( + render_type, show_private, level=level + 1, + protocol=protocol) if len(part.strip()) == 0: continue content_parts.append(part) - content_lines = "{}\n{}".format(title_line, "\n".join(content_parts)) + content_lines = "{}\n{}".format( + title_line, "\n".join(content_parts)) if self.test_private(self.name) and not show_private: return "" else: @@ -463,22 +501,29 @@ class Fork(Element): depth = level + 1 + getattr(config, "HTML_LEVEL_OFFSET", 0) content_lines = "" if depth < 5: - title_line = "<h{depth}>{content}</h{depth}>".format(depth=depth, content=name_line) + title_line = "<h{depth}>{content}</h{depth}>".format( + depth=depth, content=name_line) content_parts = [] for child in self.children: - part = child.render(render_type, show_private, level=level+1, protocol=protocol) + part = child.render( + render_type, show_private, level=level + 1, + protocol=protocol) if len(part.strip()) == 0: continue content_parts.append("<p>{}</p>".format(part)) - content_lines = "{}\n\n{}".format(title_line, "\n".join(content_parts)) + content_lines = "{}\n\n{}".format( + title_line, "\n".join(content_parts)) else: content_parts = [] for child in self.children: - part = child.render(render_type, show_private, level=level+1, protocol=protocol) + part = child.render( + render_type, show_private, level=level + 1, + protocol=protocol) if len(part.strip()) == 0: continue content_parts.append("<li>{}</li>".format(part)) - content_lines = "{}\n<ul>\n{}\n</ul>".format(name_line, "\n".join(content_parts)) + content_lines = "{}\n<ul>\n{}\n</ul>".format( + name_line, "\n".join(content_parts)) if self.test_private(self.name) and not show_private: return "" else: @@ -486,7 +531,6 @@ class Fork(Element): else: raise _not_implemented(self, render_type) - def get_tags(self, tags=None): if tags is None: tags = [] @@ -495,7 +539,7 @@ class Fork(Element): return tags def is_anonymous(self): - return self.name == None + return self.name is None def is_root(self): return self.parent is None @@ -509,7 +553,8 @@ class Fork(Element): if self.is_root(): return 1 top = self.get_top() - tops = [child + tops = [ + child for child in top.parent.children if isinstance(child, Fork) ] @@ -559,23 +604,19 @@ class Fork(Element): def parse_end(match, current, linenumber=None): linenumber = Element.parse_inner(match, current, linenumber) if current.is_root(): - raise ParserException("Found end tag for root element!", linenumber) + raise ParserException( + "Found end tag for root element!", linenumber) current = current.parent return current, linenumber def append(self, element): self.children.append(element) - # v1: has a problem with old protocols that do not use a lot of semicolons - #PATTERN = r"\s*(?<name1>[^{};]+)?{(?<environment>\S+)?\h*(?<name2>[^\n]+)?" - # v2: do not allow newlines in name1 or semicolons in name2 - #PATTERN = r"\s*(?<name1>[^{};\n]+)?{(?<environment>[^\s{};]+)?\h*(?<name2>[^;{}\n]+)?" - # v3: no environment/name2 for normal lists, only for tops - #PATTERN = r"\s*(?<name>[^{};\n]+)?{(?:TOP\h*(?<topname>[^;{}\n]+))?" - # v4: do allow one newline between name and { - PATTERN = r"\s*(?<name>(?:[^{};\n])+)?\n?\s*{(?:TOP\h*(?<topname>[^;{}\n]+))?" + PATTERN = ( + r"\s*(?<name>(?:[^{};\n])+)?\n?\s*{(?:TOP\h*(?<topname>[^;{}\n]+))?") END_PATTERN = r"\s*};?" + PATTERNS = OrderedDict([ (re.compile(Fork.PATTERN), Fork.parse), (re.compile(Fork.END_PATTERN), Fork.parse_end), @@ -589,6 +630,7 @@ TEXT_PATTERNS = OrderedDict([ (re.compile(Text.PATTERN), Text.parse) ]) + def parse(source): linenumber = 1 tree = Fork.create_root() @@ -600,18 +642,24 @@ def parse(source): if match is not None: source = source[len(match.group()):] try: - current, linenumber = PATTERNS[pattern](match, current, linenumber) + current, linenumber = PATTERNS[pattern]( + match, current, linenumber) except ParserException as exc: exc.tree = tree raise exc found = True break if not found: - raise ParserException("No matching syntax element found!", linenumber, tree=tree) + raise ParserException( + "No matching syntax element found!", linenumber, tree=tree) if current is not tree: - raise ParserException("Du hast vergessen, Klammern zu schließen! (die öffnende ist in Zeile {})".format(current.linenumber), linenumber=current.linenumber, tree=tree) + raise ParserException( + "Du hast vergessen, Klammern zu schließen! (die öffnende ist in " + "Zeile {})".format( + current.linenumber), linenumber=current.linenumber, tree=tree) return tree + def main(test_file_name=None): source = "" test_file_name = test_file_name or "source0" @@ -624,7 +672,7 @@ def main(test_file_name=None): print(e) else: print("worked!") - + if __name__ == "__main__": test_file_name = sys.argv[1] if len(sys.argv) > 1 else None diff --git a/server.py b/server.py index 1c594f1a3cf2625f05e45dec8f642ca443f75bb3..505dd067f93d592d59724c0c771f424c4c2f4fe5 100755 --- a/server.py +++ b/server.py @@ -2,35 +2,60 @@ 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, send_file as flask_send_file, Markup +from flask import ( + Flask, request, session, flash, redirect, + url_for, abort, render_template, Response, Markup) from werkzeug.utils import secure_filename from flask_script import Manager, prompt from flask_migrate import Migrate, MigrateCommand -#from flask_socketio import SocketIO from celery import Celery -from sqlalchemy import or_, and_ +from sqlalchemy import or_ from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger -from apscheduler.triggers.interval import IntervalTrigger import atexit import feedgen.feed import icalendar -from io import StringIO, BytesIO +from io import BytesIO import os -from datetime import datetime, time, timedelta +from datetime import datetime, timedelta import math import mimetypes -import subprocess -from dateutil import tz import config -from shared import db, date_filter, datetime_filter, date_filter_long, date_filter_short, time_filter, time_filter_short, user_manager, security_manager, current_user, check_login, login_required, group_required, class_filter, needs_date_test, todostate_name_filter, code_filter, indent_tab_filter -from utils import is_past, mail_manager, get_first_unused_int, set_etherpad_text, get_etherpad_text, split_terms, optional_int_arg, fancy_join, footnote_hash -from decorators import db_lookup, require_public_view_right, require_private_view_right, require_modify_right, require_publish_right, require_admin_right -from models.database import ProtocolType, Protocol, DefaultTOP, TOP, LocalTOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail, DecisionDocument, TodoState, Meta, DefaultMeta, DecisionCategory, Like -from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, generate_protocol_form, TopForm, LocalTopForm, SearchForm, DecisionSearchForm, ProtocolSearchForm, TodoSearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm, DefaultMetaForm, MetaForm, MergeTodosForm, DecisionCategoryForm, DocumentEditForm -from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable, TodoMailsTable, DefaultMetasTable, DecisionCategoriesTable +from shared import ( + db, date_filter, datetime_filter, date_filter_long, + date_filter_short, time_filter, time_filter_short, user_manager, + security_manager, current_user, check_login, login_required, + class_filter, needs_date_test, todostate_name_filter, + code_filter, indent_tab_filter) +from utils import ( + get_first_unused_int, get_etherpad_text, split_terms, optional_int_arg, + fancy_join, footnote_hash, get_git_revision, get_max_page_length_exp, + get_internal_filename, get_csrf_token) +from decorators import ( + db_lookup, protect_csrf, + require_private_view_right, require_modify_right, require_publish_right, + require_admin_right) +from models.database import ( + ProtocolType, Protocol, DefaultTOP, TOP, LocalTOP, + Document, Todo, Decision, MeetingReminder, Error, TodoMail, + DecisionDocument, TodoState, DefaultMeta, DecisionCategory, Like) +from views.forms import ( + LoginForm, ProtocolTypeForm, DefaultTopForm, + MeetingReminderForm, NewProtocolForm, DocumentUploadForm, + KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, + generate_protocol_form, TopForm, LocalTopForm, + DecisionSearchForm, ProtocolSearchForm, TodoSearchForm, + NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm, + DefaultMetaForm, MergeTodosForm, DecisionCategoryForm, + DocumentEditForm) +from views.tables import ( + ProtocolsTable, ProtocolTypesTable, + ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, + TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable, + TodoMailsTable, DefaultMetasTable, DecisionCategoriesTable) from legacy import import_old_todos, import_old_protocols, import_old_todomails +import back app = Flask(__name__) app.config.from_object(config) @@ -39,16 +64,14 @@ migrate = Migrate(app, db) manager = Manager(app) manager.add_command("db", MigrateCommand) + def make_celery(app, config): celery = Celery(app.import_name, broker=config.CELERY_BROKER_URL) celery.conf.update(app.config) return celery -celery = make_celery(app, config) -#def make_socketio(app, config): -# socketio = SocketIO(app) -# return socketio -#socketio = make_socketio(app, config) + +celery = make_celery(app, config) app.jinja_env.trim_blocks = True app.jinja_env.lstrip_blocks = True @@ -58,7 +81,6 @@ app.jinja_env.filters["timify"] = time_filter app.jinja_env.filters["timify_short"] = time_filter_short app.jinja_env.filters["datify_short"] = date_filter_short app.jinja_env.filters["datify_long"] = date_filter_long -#app.jinja_env.filters["url_complete"] = url_manager.complete app.jinja_env.filters["class"] = class_filter app.jinja_env.filters["todo_get_name"] = todostate_name_filter app.jinja_env.filters["code"] = code_filter @@ -67,12 +89,13 @@ app.jinja_env.filters["fancy_join"] = fancy_join app.jinja_env.filters["footnote_hash"] = footnote_hash app.jinja_env.tests["auth_valid"] = security_manager.check_user app.jinja_env.tests["needs_date"] = needs_date_test +app.jinja_env.globals["get_csrf_token"] = get_csrf_token additional_templates = getattr(config, "LATEX_LOCAL_TEMPLATES", None) if additional_templates is not None and os.path.isdir(additional_templates): if additional_templates not in app.jinja_loader.searchpath: app.jinja_loader.searchpath.append(additional_templates) - + import tasks @@ -83,32 +106,20 @@ app.jinja_env.globals.update(min=min) app.jinja_env.globals.update(max=max) app.jinja_env.globals.update(dir=dir) app.jinja_env.globals.update(now=datetime.now) +app.jinja_env.globals["git_revision"] = get_git_revision() -def get_git_revision(): - gitlab_url = "https://git.fsmpi.rwth-aachen.de/protokollsystem/proto3" - commit_hash = subprocess.check_output(["git", "log", "-g", "-1", "--pretty=%H"]).decode("UTF-8").strip() - timestamp = int(subprocess.check_output(["git", "log", "-g", "-1", "--pretty=%at"]).strip()) - commit_date = datetime.fromtimestamp(timestamp) - return {"url": gitlab_url, "hash": commit_hash, "date": commit_date} - -try: - app.jinja_env.globals["git_revision"] = get_git_revision() -except: - pass - -# blueprints here @manager.command def import_legacy(): """Import the old todos and protocols from an sql dump""" filename = prompt("SQL-file") - #filename = "legacy.sql" with open(filename, "rb") as sqlfile: content = sqlfile.read().decode("utf-8") import_old_todos(content) import_old_protocols(content) import_old_todomails(content) + @manager.command def recompile_all(): for protocol in sorted(Protocol.query.all(), key=lambda p: p.date): @@ -116,8 +127,9 @@ def recompile_all(): print(protocol.get_short_identifier()) tasks.parse_protocol(protocol) + @manager.command -def merge_todos(): +def merge_duplicate_todos(): todo_by_id = {} todos = Todo.query.all() for todo in todos: @@ -138,33 +150,42 @@ def merge_todos(): else: todo_by_id[todo_id] = todo + @manager.command def runserver(): app.run() make_scheduler() -# cause uwsgi currently has a bug + def send_file(file_like, cache_timeout, as_attachment, attachment_filename): + """ + Replaces flask.send_file since that uses an uwsgi function that is buggy. + """ mimetype, _ = mimetypes.guess_type(attachment_filename) response = Response(file_like.read(), mimetype) if as_attachment: - response.headers["Content-Disposition"] = 'attachment; filename="{}"'.format(attachment_filename) + response.headers["Content-Disposition"] = ( + 'attachment; filename="{}"'.format(attachment_filename)) content_type = mimetype if mimetype.startswith("text/"): content_type = "{}; charset=utf-8".format(content_type) response.headers["Content-Type"] = content_type - response.headers["Cache-Control"] = "public, max-age={}".format(cache_timeout) + response.headers["Cache-Control"] = ( + "public, max-age={}".format(cache_timeout)) response.headers["Connection"] = "close" return response + @app.route("/") +@back.anchor def index(): user = current_user() protocols = [ protocol for protocol in Protocol.query.all() - if protocol.protocoltype.has_public_view_right(user, - check_networks=False) + if protocol.protocoltype.has_public_view_right( + user, check_networks=False) ] + def _protocol_sort_key(protocol): if protocol.date is not None: return protocol.date @@ -183,8 +204,10 @@ def index(): [ protocol for protocol in protocols if protocol.done and protocol.public - and (protocol.has_private_view_right(user) - or protocol.protocoltype.has_public_view_right(user, check_networks=False)) + and ( + protocol.has_private_view_right(user) + or protocol.protocoltype.has_public_view_right( + user, check_networks=False)) ], key=_protocol_sort_key, reverse=True @@ -195,7 +218,8 @@ def index(): if len(finished_protocols) > 0: protocol = finished_protocols[0] show_private = protocol.has_private_view_right(user) - has_public_view_right = protocol.protocoltype.has_public_view_right(user) + has_public_view_right = ( + protocol.protocoltype.has_public_view_right(user)) todos = None if check_login(): todos = [ @@ -205,87 +229,94 @@ def index(): ] user_todos = [ todo for todo in todos - if user.username.lower() in list(map(str.strip, todo.who.lower().split(","))) + if user.username.lower() + in list(map(str.strip, todo.who.lower().split(","))) ] if len(user_todos) > 0: todos = user_todos + def _todo_sort_key(todo): protocol = todo.get_first_protocol() - return protocol.date if protocol is not None and protocol.date is not None else datetime.now().date() + if protocol is not None and protocol.date is not None: + return protocol.date + return datetime.now().date() todos = sorted(todos, key=_todo_sort_key, reverse=True) - todos_table = TodosTable(todos) if todos is not None else None - return render_template("index.html", open_protocols=open_protocols, protocol=protocol, todos=todos, show_private=show_private, has_public_view_right=has_public_view_right) + return render_template( + "index.html", open_protocols=open_protocols, + protocol=protocol, todos=todos, show_private=show_private, + has_public_view_right=has_public_view_right) @app.route("/documentation") +@back.anchor @login_required def documentation(): return render_template( "documentation.html") @app.route("/documentation/sessionmanagement") -# @back.anchor +@back.anchor @login_required def sessionmanagement_documentation(): return render_template( "documentation-sessionmanagement.html") @app.route("/documentation/sessionmanagement/plan") -# @back.anchor +back.anchor @login_required def plan_sessionmanagement_documentation(): return render_template( "documentation-sessionmanagement-plan.html") @app.route("/documentation/sessionmanagement/write") -# @back.anchor +@back.anchor @login_required def write_sessionmanagement_documentation(): return render_template( "documentation-sessionmanagement-write.html") @app.route("/documentation/sessionmanagement/tracking") -# @back.anchor +@back.anchor @login_required def tracking_sessionmanagement_documentation(): return render_template( "documentation-sessionmanagement-tracking.html") @app.route("/documentation/syntax") -# @back.anchor +@back.anchor @login_required def syntax_documentation(): return render_template( "documentation-syntax.html") @app.route("/documentation/syntax/meta") -# @back.anchor +@back.anchor @login_required def meta_syntax_documentation(): return render_template( "documentation-syntax-meta.html") @app.route("/documentation/syntax/top") -# @back.anchor +@back.anchor @login_required def top_syntax_documentation(): return render_template( "documentation-syntax-top.html") @app.route("/documentation/syntax/lists") -# @back.anchor +@back.anchor @login_required def lists_syntax_documentation(): return render_template("documentation-syntax-lists.html") @app.route("/documentation/syntax/internal") -# @back.anchor +@back.anchor @login_required def internal_syntax_documentation(): return render_template( "documentation-syntax-internal.html") @app.route("/documentation/syntax/tags") -# @back.anchor +@back.anchor @login_required def tags_syntax_documentation(): todostates = list(TodoState) @@ -295,39 +326,41 @@ def tags_syntax_documentation(): name_to_state=name_to_state) @app.route("/documentation/configuration") -# @back.anchor +@back.anchor @login_required def configuration_documentation(): return render_template( "documentation-configuration.html") @app.route("/documentation/configuration/types") -# @back.anchor +@back.anchor @login_required def types_configuration_documentation(): return render_template( "documentation-configuration-types.html") @app.route("/documentation/configuration/todomails") -# @back.anchor +@back.anchor @login_required def todomails_configuration_documentation(): return render_template( "documentation-configuration-todomails.html") @app.route("/types/list") +@back.anchor @login_required def list_types(): - is_logged_in = check_login() user = current_user() types = [ protocoltype for protocoltype in ProtocolType.query.all() if (protocoltype.has_private_view_right(user) - or protocoltype.has_public_view_right(user) - or protocoltype.is_public)] + or protocoltype.has_public_view_right(user) + or protocoltype.is_public)] types = sorted(types, key=lambda t: t.short_name) types_table = ProtocolTypesTable(types) - return render_template("types-list.html", types=types, types_table=types_table) + return render_template( + "types-list.html", types=types, types_table=types_table) + @app.route("/type/new", methods=["GET", "POST"]) @login_required @@ -336,16 +369,19 @@ def new_type(): if form.validate_on_submit(): user = current_user() if form.private_group.data not in user.groups: - flash("Du kannst keinen internen Protokolltypen anlegen, zu dem du selbst keinen Zugang hast.", "alert-error") + flash("Du kannst keinen internen Protokolltypen anlegen, " + "zu dem du selbst keinen Zugang hast.", "alert-error") else: protocoltype = ProtocolType() form.populate_obj(protocoltype) db.session.add(protocoltype) db.session.commit() - flash("Der Protokolltyp {} wurde angelegt.".format(protocoltype.name), "alert-success") - return redirect(request.args.get("next") or url_for("list_types")) + flash("Der Protokolltyp {} wurde angelegt.".format( + protocoltype.name), "alert-success") + return back.redirect("list_types") return render_template("type-new.html", form=form) + @app.route("/type/edit/<int:protocoltype_id>", methods=["GET", "POST"]) @login_required @db_lookup(ProtocolType) @@ -355,38 +391,54 @@ def edit_type(protocoltype): form = ProtocolTypeForm(obj=protocoltype) if form.validate_on_submit(): if form.private_group.data not in user.groups: - flash("Du kannst keinen internen Protokolltypen anlegen, zu dem du selbst keinen Zugang hast.", "alert-error") + flash("Du kannst keinen internen Protokolltypen anlegen, " + "zu dem du selbst keinen Zugang hast.", "alert-error") else: form.populate_obj(protocoltype) db.session.commit() - return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id)) - return render_template("type-edit.html", form=form, protocoltype=protocoltype) + return back.redirect("show_type", protocoltype_id=protocoltype.id) + return render_template( + "type-edit.html", form=form, protocoltype=protocoltype) + @app.route("/type/show/<int:protocoltype_id>") +@back.anchor @login_required @db_lookup(ProtocolType) @require_private_view_right() def show_type(protocoltype): protocoltype_table = ProtocolTypeTable(protocoltype) - default_tops_table = DefaultTOPsTable(protocoltype.default_tops, protocoltype) - reminders_table = MeetingRemindersTable(protocoltype.reminders, protocoltype) + default_tops_table = DefaultTOPsTable( + protocoltype.default_tops, protocoltype) + reminders_table = MeetingRemindersTable( + protocoltype.reminders, protocoltype) metas_table = DefaultMetasTable(protocoltype.metas, protocoltype) - categories_table = DecisionCategoriesTable(protocoltype.decisioncategories, protocoltype) - return render_template("type-show.html", protocoltype=protocoltype, protocoltype_table=protocoltype_table, default_tops_table=default_tops_table, metas_table=metas_table, reminders_table=reminders_table, mail_active=config.MAIL_ACTIVE, categories_table=categories_table) + categories_table = DecisionCategoriesTable( + protocoltype.decisioncategories, protocoltype) + return render_template( + "type-show.html", protocoltype=protocoltype, + protocoltype_table=protocoltype_table, + default_tops_table=default_tops_table, metas_table=metas_table, + reminders_table=reminders_table, mail_active=config.MAIL_ACTIVE, + categories_table=categories_table) + @app.route("/type/delete/<int:protocoltype_id>") @login_required -@group_required(config.ADMIN_GROUP) +@protect_csrf @db_lookup(ProtocolType) +@require_admin_right() @require_modify_right() def delete_type(protocoltype): name = protocoltype.name - db.session.delete(protocoltype) + db.session.delete(protocoltype) db.session.commit() flash("Der Protokolltype {} wurde gelöscht.".format(name), "alert-success") - return redirect(request.args.get("next") or url_for("list_types")) + return back.redirect("list_types") -@app.route("/type/reminders/new/<int:protocoltype_id>", methods=["GET", "POST"]) + +@app.route("/type/reminders/new/<int:protocoltype_id>", + methods=["GET", "POST"]) @login_required @db_lookup(ProtocolType) @require_modify_right() @@ -397,10 +449,13 @@ def new_reminder(protocoltype): form.populate_obj(meetingreminder) db.session.add(meetingreminder) db.session.commit() - return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id)) - return render_template("reminder-new.html", form=form, protocoltype=protocoltype) + return back.redirect("show_type", protocoltype_id=protocoltype.id) + return render_template( + "reminder-new.html", form=form, protocoltype=protocoltype) -@app.route("/type/reminder/edit/<int:meetingreminder_id>", methods=["GET", "POST"]) + +@app.route("/type/reminder/edit/<int:meetingreminder_id>", + methods=["GET", "POST"]) @login_required @db_lookup(MeetingReminder) @require_modify_right() @@ -409,18 +464,23 @@ def edit_reminder(meetingreminder): if form.validate_on_submit(): form.populate_obj(meetingreminder) db.session.commit() - return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id)) - return render_template("reminder-edit.html", form=form, meetingreminder=meetingreminder) + return back.redirect( + "show_type", protocoltype_id=meetingreminder.protocoltype.id) + return render_template( + "reminder-edit.html", form=form, meetingreminder=meetingreminder) + @app.route("/type/reminder/delete/<int:meetingreminder_id>") @login_required +@protect_csrf @db_lookup(MeetingReminder) @require_modify_right() def delete_reminder(meetingreminder): protocoltype = meetingreminder.protocoltype db.session.delete(meetingreminder) db.session.commit() - return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id)) + return back.redirect("show_type", protocoltype_id=protocoltype.id) + @app.route("/type/tops/new/<int:protocoltype_id>", methods=["GET", "POST"]) @login_required @@ -435,15 +495,20 @@ def new_default_top(protocoltype): db.session.commit() for protocol in protocoltype.protocols: if not protocol.done: - localtop = LocalTOP(protocol_id=protocol.id, + localtop = LocalTOP( + protocol_id=protocol.id, defaulttop_id=defaulttop.id, description="") db.session.add(localtop) db.session.commit() - flash("Der Standard-TOP {} wurde für dem Protokolltyp {} hinzugefügt.".format(defaulttop.name, protocoltype.name), "alert-success") - return redirect(request.args.get("next") or url_for("index")) - return render_template("default-top-new.html", form=form, protocoltype=protocoltype) + flash("Der Standard-TOP {} wurde für dem Protokolltyp {} hinzugefügt." + .format(defaulttop.name, protocoltype.name), "alert-success") + return back.redirect() + return render_template( + "default-top-new.html", form=form, protocoltype=protocoltype) -@app.route("/type/tops/edit/<int:protocoltype_id>/<int:defaulttop_id>", methods=["GET", "POST"]) + +@app.route("/type/tops/edit/<int:protocoltype_id>/<int:defaulttop_id>", + methods=["GET", "POST"]) @login_required @db_lookup(ProtocolType, DefaultTOP) @require_modify_right() @@ -452,20 +517,27 @@ def edit_default_top(protocoltype, defaulttop): if form.validate_on_submit(): form.populate_obj(defaulttop) db.session.commit() - return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id)) - return render_template("default-top-edit.html", form=form, protocoltype=protocoltype, defaulttop=defaulttop) + return back.redirect("show_type", protocoltype_id=protocoltype.id) + return render_template( + "default-top-edit.html", form=form, + protocoltype=protocoltype, defaulttop=defaulttop) + @app.route("/type/tops/delete/<int:defaulttop_id>") @login_required +@protect_csrf @db_lookup(DefaultTOP) @require_modify_right() def delete_default_top(defaulttop): db.session.delete(defaulttop) db.session.commit() - return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=defaulttop.protocoltype.id)) + return back.redirect( + "show_type", protocoltype_id=defaulttop.protocoltype.id) + @app.route("/type/tops/move/<int:defaulttop_id>/<diff>/") @login_required +@protect_csrf @db_lookup(DefaultTOP) @require_modify_right() def move_default_top(defaulttop, diff): @@ -474,13 +546,14 @@ def move_default_top(defaulttop, diff): db.session.commit() except ValueError: flash("Die angegebene Differenz ist keine Zahl.", "alert-error") - return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=defaulttop.protocoltype.id)) + return back.redirect( + "show_type", protocoltype_id=defaulttop.protocoltype.id) + @app.route("/protocols/list") +@back.anchor def list_protocols(): - is_logged_in = check_login() user = current_user() - protocoltype = None protocoltype_id = None try: protocoltype_id = int(request.args.get("protocoltype_id")) @@ -492,11 +565,11 @@ def list_protocols(): except (ValueError, TypeError): pass search_term = request.args.get("search") - protocoltypes = ProtocolType.get_public_protocoltypes(user, check_networks=False) + protocoltypes = ProtocolType.get_public_protocoltypes( + user, check_networks=False) search_form = ProtocolSearchForm(protocoltypes) if protocoltype_id is not None: search_form.protocoltype_id.data = protocoltype_id - protocoltype = ProtocolType.query.filter_by(id=protocoltype_id).first() if state_open is not None: search_form.state_open.data = state_open if search_term is not None: @@ -513,14 +586,17 @@ def list_protocols(): )) protocols = [ protocol for protocol in protocol_query.all() - if protocol.protocoltype.has_public_view_right(user, check_networks=False) + if protocol.protocoltype.has_public_view_right( + user, check_networks=False) ] + def _matches_search(content): content = content.lower() for search_term in search_terms: if search_term.lower() not in content: return False return True + def _matches_search_lazy(content): content = content.lower() for search_term in search_terms: @@ -548,7 +624,7 @@ def list_protocols(): and _matches_search(protocol.content_public)) ] for protocol in protocols: - content = protocol.content_private if protocol.protocoltype.has_private_view_right(user) else protocol.content_public + content = protocol.get_visible_content(user) lines = content.splitlines() matches = [line for line in lines if _matches_search_lazy(line)] formatted_lines = [] @@ -557,13 +633,18 @@ def list_protocols(): lower_line = line.lower() last_index = 0 while last_index < len(line): - index_candidates = list(filter(lambda t: t[0] != -1, - [(lower_line.find(term, last_index), term) for term in search_terms])) + index_candidates = list(filter( + lambda t: t[0] != -1, + [ + (lower_line.find(term, last_index), term) + for term in search_terms + ])) if len(index_candidates) == 0: parts.append((line[last_index:], False)) break else: - new_index, term = min(index_candidates, key=lambda t: t[0]) + new_index, term = min( + index_candidates, key=lambda t: t[0]) new_end_index = new_index + len(term) parts.append((line[last_index:new_index], False)) parts.append((line[new_index:new_end_index], True)) @@ -573,7 +654,8 @@ def list_protocols(): for text, matched in parts ])) search_results[protocol] = " …<br />\n".join(formatted_lines) - protocols = sorted(protocols, key=lambda protocol: protocol.date, reverse=True) + protocols = sorted( + protocols, key=lambda protocol: protocol.date, reverse=True) page = _get_page() page_length = _get_page_length() page_count = int(math.ceil(len(protocols) / page_length)) @@ -581,10 +663,17 @@ def list_protocols(): page = 0 begin_index = page * page_length end_index = (page + 1) * page_length - max_page_length_exp = math.ceil(math.log10(len(protocols))) if len(protocols) > 0 else 1 + max_page_length_exp = get_max_page_length_exp(protocols) protocols = protocols[begin_index:end_index] protocols_table = ProtocolsTable(protocols, search_results=search_results) - return render_template("protocols-list.html", protocols=protocols, protocols_table=protocols_table, search_form=search_form, page=page, page_count=page_count, page_diff=config.PAGE_DIFF, protocoltype_id=protocoltype_id, search_term=search_term, state_open=state_open, page_length=page_length, max_page_length_exp=max_page_length_exp) + return render_template( + "protocols-list.html", protocols=protocols, + protocols_table=protocols_table, search_form=search_form, page=page, + page_count=page_count, page_diff=config.PAGE_DIFF, + protocoltype_id=protocoltype_id, search_term=search_term, + state_open=state_open, page_length=page_length, + max_page_length_exp=max_page_length_exp) + @app.route("/protocol/new", methods=["GET", "POST"]) @login_required @@ -595,50 +684,67 @@ def new_protocol(): upload_form = NewProtocolSourceUploadForm(protocoltypes) file_upload_form = NewProtocolFileUploadForm(protocoltypes) if form.validate_on_submit(): - protocoltype = ProtocolType.query.filter_by(id=form.protocoltype_id.data).first() + protocoltype = ProtocolType.query.filter_by( + id=form.protocoltype_id.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.create_new_protocol(protocoltype, - form.date.data, form.start_time.data) - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) + return back.redirect() + protocol = Protocol.create_new_protocol( + protocoltype, form.date.data, form.start_time.data) + return back.redirect("show_protocol", protocol_id=protocol.id) type_id = request.args.get("protocoltype_id") if type_id is not None: form.protocoltype.data = type_id upload_form.protocoltype.data = type_id - return render_template("protocol-new.html", form=form, upload_form=upload_form, file_upload_form=file_upload_form, protocoltypes=protocoltypes) + return render_template( + "protocol-new.html", form=form, + upload_form=upload_form, file_upload_form=file_upload_form, + protocoltypes=protocoltypes) + @app.route("/protocol/show/<int:protocol_id>") +@back.anchor @db_lookup(Protocol) def show_protocol(protocol): user = current_user() errors_table = ErrorsTable(protocol.errors) - if not protocol.protocoltype.has_public_view_right(user, check_networks=False): + if not protocol.protocoltype.has_public_view_right( + user, check_networks=False): flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error") if check_login(): return redirect(url_for("index")) - return redirect(request.args.get("next") or url_for("login", next=request.url)) + return redirect(url_for("login")) visible_documents = [ document for document in protocol.documents - if (not document.is_private and document.protocol.has_public_view_right(user)) - or (document.is_private and document.protocol.protocoltype.has_private_view_right(user)) + if (not document.is_private + and document.protocol.has_public_view_right(user)) + or (document.is_private + and document.protocol.protocoltype.has_private_view_right(user)) ] documents_table = DocumentsTable(visible_documents, protocol) document_upload_form = DocumentUploadForm() source_upload_form = KnownProtocolSourceUploadForm() time_diff = protocol.date - datetime.now().date() large_time_diff = not protocol.is_done() and time_diff.days > 0 - content_html = (protocol.content_html_private + content_html = ( + protocol.content_html_private if protocol.has_private_view_right(user) else protocol.content_html_public) if content_html is not None: content_html = Markup(content_html) - return render_template("protocol-show.html", protocol=protocol, errors_table=errors_table, documents_table=documents_table, document_upload_form=document_upload_form, source_upload_form=source_upload_form, time_diff=time_diff, large_time_diff=large_time_diff, content_html=content_html) + return render_template( + "protocol-show.html", protocol=protocol, + errors_table=errors_table, documents_table=documents_table, + document_upload_form=document_upload_form, + source_upload_form=source_upload_form, time_diff=time_diff, + large_time_diff=large_time_diff, content_html=content_html) + @app.route("/protocol/delete/<int:protocol_id>") @login_required -@group_required(config.ADMIN_GROUP) +@protect_csrf @db_lookup(Protocol) +@require_admin_right() @require_modify_right() def delete_protocol(protocol): name = protocol.get_short_identifier() @@ -646,21 +752,24 @@ def delete_protocol(protocol): 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")) + return back.redirect("list_protocols") + @app.route("/protocol/etherpull/<int:protocol_id>") @login_required +@protect_csrf @db_lookup(Protocol) @require_modify_right() def etherpull_protocol(protocol): if not config.ETHERPAD_ACTIVE: flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error") - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id)) + return back.redirect("show_protocol", protocol_id=protocol.id) protocol.source = get_etherpad_text(protocol.get_identifier()) 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)) + return back.redirect("show_protocol", protocol_id=protocol.id) + @app.route("/protocol/upload/known/<int:protocol_id>", methods=["POST"]) @login_required @@ -682,7 +791,8 @@ def upload_source_to_known_protocol(protocol): 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)) + return back.redirect("show_protocol", protocol_id=protocol.id) + @app.route("/protocol/upload/new/", methods=["POST"]) @login_required @@ -693,26 +803,31 @@ def upload_new_protocol(): if form.validate_on_submit(): if form.source.data is None: flash("Es wurde keine Datei ausgewählt.", "alert-error") - return redirect(request.args.get("fail") or url_for("new_protocol")) + return redirect(request.args.get("fail") + or url_for("new_protocol")) file = form.source.data if file.filename == "": flash("Es wurde keine Datei ausgewählt.", "alert-error") - return redirect(request.args.get("fail") or url_for("new_protocol")) + return redirect(request.args.get("fail") + or url_for("new_protocol")) source = file.stream.read().decode("utf-8") - protocoltype = ProtocolType.query.filter_by(id=form.protocoltype_id.data).first() + protocoltype = ProtocolType.query.filter_by( + id=form.protocoltype_id.data).first() if protocoltype is None or not protocoltype.has_modify_right(user): flash("Invalider Protokolltyp oder keine Rechte.", "alert-error") - return redirect(request.args.get("fail") or url_for("new_protocol")) + return redirect(request.args.get("fail") + or url_for("new_protocol")) protocol = Protocol(protocoltype_id=protocoltype.id, source=source) db.session.add(protocol) db.session.commit() - for local_top in protocol.create_localtops: + for local_top in protocol.create_localtops(): db.session.add(local_top) db.session.commit() tasks.parse_protocol(protocol) - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) + return back.redirect("show_protocol", protocol_id=protocol.id) return redirect(request.args.get("fail") or url_for("new_protocol")) + @app.route("/protocol/upload/new/file/", methods=["POST"]) @login_required def upload_new_protocol_by_file(): @@ -722,42 +837,53 @@ def upload_new_protocol_by_file(): if form.validate_on_submit(): if form.file.data is None: flash("Es wurde keine Datei ausgewählt.", "alert-error") - return redirect(request.args.get("fail") or url_for("new_protocol")) + return redirect(request.args.get("fail") + or url_for("new_protocol")) file = form.file.data if file.filename == "": flash("Es wurde keine Datei ausgewählt.", "alert-error") - return redirect(request.args.get("fail") or url_for("new_protocol")) + return redirect(request.args.get("fail") + or url_for("new_protocol")) filename = secure_filename(file.filename) - protocoltype = ProtocolType.query.filter_by(id=form.protocoltype_id.data).first() + protocoltype = ProtocolType.query.filter_by( + id=form.protocoltype_id.data).first() if protocoltype is None or not protocoltype.has_modify_right(user): flash("Invalider Protokolltyp oder keine Rechte.", "alert-error") - return redirect(request.args.get("fail") or url_for("new_protocol")) - protocol = Protocol(protocoltype_id=protocoltype.id, date=datetime.now().date(), done=True) + return redirect(request.args.get("fail") + or url_for("new_protocol")) + protocol = Protocol( + protocoltype_id=protocoltype.id, + date=datetime.now().date(), done=True) db.session.add(protocol) db.session.commit() - for local_top in protocol.create_localtops: + for local_top in protocol.create_localtops(): db.session.add(local_top) db.session.commit() - document = Document(protocol_id=protocol.id, name=filename, + document = Document( + protocol_id=protocol.id, name=filename, filename="", is_compiled=False) form.populate_obj(document) db.session.add(document) db.session.commit() - internal_filename = "{}-{}-{}".format(protocol.id, document.id, filename) + internal_filename = get_internal_filename( + protocol, document.id, filename) document.filename = internal_filename file.save(os.path.join(config.DOCUMENTS_PATH, internal_filename)) db.session.commit() - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) + return back.redirect("show_protocol", protocol_id=protocol.id) return redirect(request.args.get("fail") or url_for("new_protocol")) + @app.route("/protocol/recompile/<int:protocol_id>") @login_required -@group_required(config.ADMIN_GROUP) +@protect_csrf @db_lookup(Protocol) +@require_admin_right() @require_modify_right() def recompile_protocol(protocol): tasks.parse_protocol(protocol) - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) + return back.redirect("show_protocol", protocol_id=protocol.id) + @app.route("/protocol/source/<int:protocol_id>") @login_required @@ -765,7 +891,10 @@ def recompile_protocol(protocol): @require_modify_right() def get_protocol_source(protocol): 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_short_identifier())) + return send_file( + file_like, cache_timeout=1, as_attachment=True, + attachment_filename="{}.txt".format(protocol.get_short_identifier())) + @app.route("/protocol/template/<int:protocol_id>") @login_required @@ -773,19 +902,25 @@ def get_protocol_source(protocol): @require_modify_right() def get_protocol_template(protocol): 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_short_identifier())) + return send_file( + file_like, cache_timeout=1, as_attachment=True, + attachment_filename="{}-template.txt".format( + protocol.get_short_identifier())) + @app.route("/protocol/etherpush/<int:protocol_id>") @login_required +@protect_csrf @db_lookup(Protocol) @require_modify_right() def etherpush_protocol(protocol): if not config.ETHERPAD_ACTIVE: flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error") - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id)) + return back.redirect("show_protocol", protocol_id=protocol.id) if not protocol.is_done(): tasks.set_etherpad_content(protocol) - return redirect(request.args.get("next") or protocol.get_etherpad_link()) + return redirect(protocol.get_etherpad_link()) + @app.route("/protocol/update/<int:protocol_id>", methods=["GET", "POST"]) @login_required @@ -800,70 +935,82 @@ def update_protocol(protocol): meta.value = getattr(edit_form.metas, meta.name).data db.session.commit() tasks.push_tops_to_calendar(protocol) - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) + return back.redirect("show_protocol", protocol_id=protocol.id) for meta in protocol.metas: getattr(edit_form.metas, meta.name).data = meta.value - return render_template("protocol-update.html", upload_form=upload_form, edit_form=edit_form, protocol=protocol) + return render_template( + "protocol-update.html", upload_form=upload_form, + edit_form=edit_form, protocol=protocol) + @app.route("/protocol/publish/<int:protocol_id>") @login_required +@protect_csrf @db_lookup(Protocol) @require_publish_right() def publish_protocol(protocol): protocol.public = True db.session.commit() - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) + return back.redirect("show_protocol", protocol_id=protocol.id) + @app.route("/prococol/send/private/<int:protocol_id>") @login_required +@protect_csrf @db_lookup(Protocol) @require_modify_right() def send_protocol_private(protocol): if not config.MAIL_ACTIVE: flash("Die Mailfunktion ist nicht aktiviert.", "alert-error") - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id)) + return back.redirect("show_protocol", protocol_id=protocol.id) tasks.send_protocol_private(protocol) flash("Das Protokoll wurde versandt.", "alert-success") - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) + return back.redirect("show_protocol", protocol_id=protocol.id) + @app.route("/prococol/send/public/<int:protocol_id>") @login_required +@protect_csrf @db_lookup(Protocol) @require_publish_right() def send_protocol_public(protocol): if not config.MAIL_ACTIVE: flash("Die Mailfunktion ist nicht aktiviert.", "alert-error") - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id)) + return back.redirect("show_protocol", protocol_id=protocol.id) tasks.send_protocol_public(protocol) flash("Das Protokoll wurde versandt.", "alert-success") - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) + return back.redirect("show_protocol", protocol_id=protocol.id) + @app.route("/protocol/reminder/<int:protocol_id>") @login_required +@protect_csrf @db_lookup(Protocol) @require_modify_right() def send_protocol_reminder(protocol): if not config.MAIL_ACTIVE: flash("Die Mailfunktion ist nicht aktiviert.", "alert-error") - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id)) - meetingreminders = MeetingReminder.query.filter_by(protocoltype_id=protocol.protocoltype.id).all() + return back.redirect("show_protocol", protocol_id=protocol.id) + meetingreminders = protocol.reminders if len(meetingreminders) == 0: - flash("Für diesen Protokolltyp sind keine Einladungsmails konfiguriert.", "alert-error") - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id)) + flash("Für diesen Protokolltyp sind keine Einladungsmails " + "konfiguriert.", "alert-error") + return back.redirect("show_protocol", protocol_id=protocol.id) day_difference = (protocol.date - datetime.now().date()).days past_reminders = [ meetingreminder for meetingreminder in meetingreminders if meetingreminder.days_before > day_difference ] if len(past_reminders) == 0: - flash("Bisher hätte keine Einladungsmail verschickt werden sollen, schicke letzte.", "alert-info") + flash("Bisher hätte keine Einladungsmail verschickt werden sollen, " + "schicke letzte.", "alert-info") past_reminders = meetingreminders past_reminders = sorted(past_reminders, key=lambda r: r.days_before) choosen_reminder = past_reminders[0] tasks.send_reminder(choosen_reminder, protocol) flash("Einladungsmail ist versandt.", "alert-success") - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) - + return back.redirect("show_protocol", protocol_id=protocol.id) + @app.route("/protocol/tops/new/<int:protocol_id>", methods=["GET", "POST"]) @login_required @@ -877,13 +1024,14 @@ def new_top(protocol): db.session.add(top) db.session.commit() tasks.push_tops_to_calendar(top.protocol) - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) + return back.redirect("show_protocol", protocol_id=protocol.id) else: current_numbers = list(map(lambda t: t.number, protocol.tops)) suggested_number = get_first_unused_int(current_numbers) form.number.data = suggested_number return render_template("top-new.html", form=form, protocol=protocol) + @app.route("/protocol/top/edit/<int:top_id>", methods=["GET", "POST"]) @login_required @db_lookup(TOP) @@ -894,11 +1042,13 @@ def edit_top(top): form.populate_obj(top) db.session.commit() tasks.push_tops_to_calendar(top.protocol) - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=top.protocol.id)) + return back.redirect("show_protocol", protocol_id=top.protocol.id) return render_template("top-edit.html", form=form, top=top) + @app.route("/protocol/top/delete/<int:top_id>") @login_required +@protect_csrf @db_lookup(TOP) @require_modify_right() def delete_top(top): @@ -908,10 +1058,12 @@ def delete_top(top): db.session.commit() tasks.push_tops_to_calendar(protocol) flash("Der TOP {} wurde gelöscht.".format(name), "alert-success") - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) + return back.redirect("show_protocol", protocol_id=protocol.id) + @app.route("/protocol/top/move/<int:top_id>/<diff>") @login_required +@protect_csrf @db_lookup(TOP) @require_modify_right() def move_top(top, diff): @@ -921,9 +1073,11 @@ def move_top(top, diff): tasks.push_tops_to_calendar(top.protocol) except ValueError: flash("Die angegebene Differenz ist keine Zahl.", "alert-error") - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=top.protocol.id)) + return back.redirect("show_protocol", protocol_id=top.protocol.id) + -@app.route("/protocol/localtop/edit/<int:localtop_id>", methods=["GET", "POST"]) +@app.route("/protocol/localtop/edit/<int:localtop_id>", + methods=["GET", "POST"]) @login_required @db_lookup(LocalTOP) @require_modify_right() @@ -932,9 +1086,10 @@ def edit_localtop(localtop): if form.validate_on_submit(): form.populate_obj(localtop) db.session.commit() - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=localtop.protocol.id)) + return back.redirect("show_protocol", protocol_id=localtop.protocol.id) return render_template("localtop-edit.html", form=form, localtop=localtop) + def _get_page(): try: page = request.args.get("page") @@ -944,6 +1099,7 @@ def _get_page(): except ValueError: return 0 + def _get_page_length(): try: page_length = request.args.get("page_length") @@ -953,11 +1109,12 @@ def _get_page_length(): except ValueError: return config.PAGE_LENGTH + @app.route("/todos/list") +@back.anchor @login_required def list_todos(): user = current_user() - protocoltype = None protocoltype_id = None try: protocoltype_id = int(request.args.get("protocoltype_id")) @@ -973,7 +1130,6 @@ def list_todos(): search_form = TodoSearchForm(protocoltypes) if protocoltype_id is not None: search_form.protocoltype_id.data = protocoltype_id - protocoltype = ProtocolType.query.filter_by(id=protocoltype_id).first() if state_open is not None: search_form.state_open.data = state_open if search_term is not None: @@ -999,6 +1155,7 @@ def list_todos(): if search_term.lower() in todo.description.lower() or search_term.lower() in todo.who.lower() ] + def _sort_key(todo): return (not todo.is_done(), todo.get_id()) todos = sorted(todos, key=_sort_key, reverse=True) @@ -1009,10 +1166,17 @@ def list_todos(): page = 0 begin_index = page * page_length end_index = (page + 1) * page_length - max_page_length_exp = math.ceil(math.log10(len(todos))) if len(todos) > 0 else 1 + max_page_length_exp = get_max_page_length_exp(todos) todos = todos[begin_index:end_index] 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, state_open=state_open, page_length=page_length, max_page_length_exp=max_page_length_exp) + 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, + state_open=state_open, page_length=page_length, + max_page_length_exp=max_page_length_exp) + @app.route("/todo/new", methods=["GET", "POST"]) @login_required @@ -1025,16 +1189,18 @@ def new_todo(): 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")) + return back.redirect() 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): + 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")) + return back.redirect() todo = Todo() form.populate_obj(todo) if protocol is not None: @@ -1045,13 +1211,16 @@ def new_todo(): 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)) + return back.redirect("show_protocol", protocol_id=protocol.id) else: - return redirect(request.args.get("next") or url_for("list_todos", protocoltype_id=protocoltype_id)) + return back.redirect("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) + return render_template( + "todo-new.html", form=form, protocol=protocol, + protocoltype=protocoltype) + @app.route("/todo/edit/<int:todo_id>", methods=["GET", "POST"]) @login_required @@ -1062,10 +1231,13 @@ def edit_todo(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_id=todo.protocoltype.id)) + return back.redirect( + "list_todos", protocoltype_id=todo.protocoltype.id) return render_template("todo-edit.html", form=form, todo=todo) + @app.route("/todo/show/<int:todo_id>") +@back.anchor @login_required @db_lookup(Todo) @require_private_view_right() @@ -1073,8 +1245,10 @@ def show_todo(todo): 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 +@protect_csrf @db_lookup(Todo) @require_private_view_right() def delete_todo(todo): @@ -1082,16 +1256,17 @@ def delete_todo(todo): 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_id=type_id)) + return back.redirect("list_todos", protocoltype_id=type_id) + @app.route("/todo/merge", methods=["GET", "POST"]) @login_required -@group_required(config.ADMIN_GROUP) +@require_admin_right() def merge_todos(): form = MergeTodosForm(request.args.get("todo_id")) if form.validate_on_submit(): todo1 = Todo.query.filter_by(id=form.todo1.data).first() - todo2 = Todo.query.filter_by(id=todo.todo2.data).first() + todo2 = Todo.query.filter_by(id=form.todo2.data).first() if todo1 is None or todo2 is None: flash("Missing todos.", "alert-error") else: @@ -1104,16 +1279,15 @@ def merge_todos(): db.session.delete(todo2) db.session.commit() flash("Merged todos {} and {}.".format(id1, id2), "alert-success") - return redirect(request.args.get("next") or url_for("list_todos")) - return render_template("todos-merge.html", form=form, next_url=request.args.get("next")) + return back.redirect("list_todos") + return render_template("todos-merge.html", form=form) + @app.route("/decisions/list") +@back.anchor def list_decisions(): - is_logged_In = check_login() user = current_user() - protocoltype = None protocoltype_id = None - decisioncategory = None decisioncategory_id = None try: protocoltype_id = int(request.args.get("protocoltype_id")) @@ -1124,7 +1298,8 @@ def list_decisions(): except (ValueError, TypeError): pass search_term = request.args.get("search") - protocoltypes = ProtocolType.get_public_protocoltypes(user, check_networks=False) + protocoltypes = ProtocolType.get_public_protocoltypes( + user, check_networks=False) decisioncategories = [ category for protocoltype in protocoltypes @@ -1133,10 +1308,8 @@ def list_decisions(): search_form = DecisionSearchForm(protocoltypes, decisioncategories) if protocoltype_id is not None: search_form.protocoltype_id.data = protocoltype_id - protocoltype = ProtocolType.query.filter_by(id=protocoltype_id).first() if decisioncategory_id is not None: search_form.decisioncategory_id.data = decisioncategory_id - decisioncategory = DecisionCategory.query.filter_by(id=decisioncategory_id).first() if search_term is not None: search_form.search.data = search_term decisions = [ @@ -1145,7 +1318,7 @@ def list_decisions(): ] if protocoltype_id is not None and protocoltype_id != -1: decisions = [ - decision for decision in decisions + decision for decision in decisions if decision.protocol.protocoltype.id == protocoltype_id ] if decisioncategory_id is not None and decisioncategory_id != -1: @@ -1159,7 +1332,7 @@ def list_decisions(): if search_term.lower() in decision.content.lower() ] decisions = sorted(decisions, key=lambda d: d.protocol.date, reverse=True) - + page = _get_page() page_length = _get_page_length() @@ -1168,22 +1341,33 @@ def list_decisions(): page = 0 begin_index = page * page_length end_index = (page + 1) * page_length - max_page_length_exp = math.ceil(math.log10(len(decisions))) if len(decisions) > 0 else 1 + max_page_length_exp = get_max_page_length_exp(decisions) decisions = decisions[begin_index:end_index] decisions_table = DecisionsTable(decisions) - return render_template("decisions-list.html", decisions=decisions, decisions_table=decisions_table, search_form=search_form, page=page, page_count=page_count, page_diff=config.PAGE_DIFF, protocoltype_id=protocoltype_id, search_term=search_term, decisioncategory_id=decisioncategory_id, page_length=page_length, max_page_length_exp=max_page_length_exp) + return render_template( + "decisions-list.html", decisions=decisions, + decisions_table=decisions_table, search_form=search_form, page=page, + page_count=page_count, page_diff=config.PAGE_DIFF, + protocoltype_id=protocoltype_id, search_term=search_term, + decisioncategory_id=decisioncategory_id, page_length=page_length, + max_page_length_exp=max_page_length_exp) + @app.route("/document/download/<int:document_id>") @db_lookup(Document) def download_document(document): user = current_user() if ((document.is_private - and not document.protocol.protocoltype.has_private_view_right(user)) + and not document.protocol.protocoltype + .has_private_view_right(user)) or (not document.is_private and not document.protocol.has_public_view_right(user))): flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error") - return redirect(request.args.get("next") or url_for("index")) - return send_file(document.as_file_like(), cache_timeout=1, as_attachment=True, attachment_filename=document.name) + return back.redirect() + return send_file( + document.as_file_like(), cache_timeout=1, + as_attachment=True, attachment_filename=document.name) + @app.route("/document/upload/<int:protocol_id>", methods=["POST"]) @login_required @@ -1193,26 +1377,28 @@ def upload_document(protocol): form = DocumentUploadForm() if form.document.data is None: flash("Es wurde keine Datei ausgewählt.", "alert-error") - return redirect(request.args.get("next") or url_for("index")) + return back.redirect() file = form.document.data if file.filename == "": flash("Es wurde keine Datei ausgewählt.", "alert-error") - return redirect(request.args.get("next") or url_for("index")) - # todo: Dateitypen einschränken? + return back.redirect() if file: filename = secure_filename(file.filename) - document = Document(protocol_id=protocol.id, name=filename, + document = Document( + protocol_id=protocol.id, name=filename, filename="", is_compiled=False) form.populate_obj(document) db.session.add(document) db.session.commit() - internal_filename = "{}-{}-{}".format(protocol.id, document.id, filename) + internal_filename = get_internal_filename( + protocol, document, 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)) + return back.redirect("show_protocol", protocol_id=protocol.id) + @app.route("/document/edit/<int:document_id>", methods=["GET", "POST"]) @login_required @@ -1223,13 +1409,15 @@ def edit_document(document): if form.validate_on_submit(): form.populate_obj(document) db.session.commit() - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=document.protocol.id)) + return back.redirect("show_protocol", protocol_id=document.protocol.id) return render_template("document-edit.html", document=document, form=form) + @app.route("/document/delete/<int:document_id>") @login_required -@group_required(config.ADMIN_GROUP) +@protect_csrf @db_lookup(Document) +@require_admin_right() @require_modify_right() def delete_document(document): name = document.name @@ -1237,34 +1425,46 @@ def delete_document(document): 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 back.redirect("show_protocol", protocol_id=protocol.id) + @app.route("/document/print/<int:document_id>") @login_required +@protect_csrf @db_lookup(Document) @require_modify_right() def print_document(document): if not config.PRINTING_ACTIVE: flash("Die Druckfunktion ist nicht aktiviert.", "alert-error") - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=document.protocol.id)) + return back.redirect("show_protocol", protocol_id=document.protocol.id) tasks.print_file(document.get_filename(), document.protocol) - flash("Das Dokument {} wird gedruckt.".format(document.name), "alert-success") - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=document.protocol.id)) + flash("Das Dokument {} wird gedruckt.".format(document.name), + "alert-success") + return back.redirect("show_protocol", protocol_id=document.protocol.id) + @app.route("/decision/print/<int:decisiondocument_id>") @login_required +@protect_csrf @db_lookup(DecisionDocument) @require_modify_right() def print_decision(decisiondocument): - user = current_user() if not config.PRINTING_ACTIVE: flash("Die Druckfunktion ist nicht aktiviert.", "alert-error") - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=decisiondocument.decision.protocol.id)) - tasks.print_file(decisiondocument.get_filename(), decisiondocument.decision.protocol) - flash("Das Dokument {} wird gedruckt.".format(decisiondocument.name), "alert-success") - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=decisiondocument.decision.protocol.id)) + return back.redirect( + "show_protocol", + protocol_id=decisiondocument.decision.protocol.id) + tasks.print_file( + decisiondocument.get_filename(), + decisiondocument.decision.protocol) + flash("Das Dokument {} wird gedruckt.".format(decisiondocument.name), + "alert-success") + return back.redirect( + "show_protocol", protocol_id=decisiondocument.decision.protocol.id) + @app.route("/errors/list") +@back.anchor @login_required def list_errors(): user = current_user() @@ -1273,18 +1473,24 @@ def list_errors(): if error.protocol.protocoltype.has_private_view_right(user) ] errors_table = ErrorsTable(errors) - return render_template("errors-list.html", errros=errors, errors_table=errors_table) + return render_template( + "errors-list.html", erros=errors, errors_table=errors_table) + @app.route("/error/show/<int:error_id>") +@back.anchor @login_required @db_lookup(Error) @require_modify_right() def show_error(error): error_table = ErrorTable(error) - return render_template("error-show.html", error=error, error_table=error_table) + return render_template( + "error-show.html", error=error, error_table=error_table) + @app.route("/error/delete/<int:error_id>") @login_required +@protect_csrf @db_lookup(Error) @require_modify_right() def delete_error(error): @@ -1292,14 +1498,19 @@ def delete_error(error): db.session.delete(error) db.session.commit() flash("Fehler {} gelöscht.".format(name), "alert-success") - return redirect(request.args.get("next") or url_for("list_errors")) + return back.redirect("list_errors") + @app.route("/todomails/list") +@back.anchor @login_required def list_todomails(): todomails = sorted(TodoMail.query.all(), key=lambda tm: tm.name.lower()) todomails_table = TodoMailsTable(todomails) - return render_template("todomails-list.html", todomails=todomails, todomails_table=todomails_table) + return render_template( + "todomails-list.html", todomails=todomails, + todomails_table=todomails_table) + @app.route("/todomail/new", methods=["GET", "POST"]) @login_required @@ -1310,10 +1521,12 @@ def new_todomail(): form.populate_obj(todomail) db.session.add(todomail) db.session.commit() - flash("Die Todomailzuordnung für {} wurde angelegt.".format(todomail.name), "alert-success") - return redirect(request.args.get("next") or url_for("list_todomails")) + flash("Die Todomailzuordnung für {} wurde angelegt.".format( + todomail.name), "alert-success") + return back.redirect("list_todomails") return render_template("todomail-new.html", form=form) + @app.route("/todomail/edit/<int:todomail_id>", methods=["GET", "POST"]) @login_required @db_lookup(TodoMail) @@ -1323,19 +1536,23 @@ def edit_todomail(todomail): form.populate_obj(todomail) db.session.commit() flash("Die Todo-Mail-Zuordnung wurde geändert.", "alert-success") - return redirect(request.args.get("next") or url_for("list_todomails")) + return back.redirect("list_todomails") return render_template("todomail-edit.html", todomail=todomail, form=form) + @app.route("/todomail/delete/<int:todomail_id>") @login_required +@protect_csrf @db_lookup(TodoMail) def delete_todomail(todomail): name = todomail.name db.session.delete(todomail) db.session.commit() - flash("Die Todo-Mail-Zuordnung für {} wurde gelöscht.".format(name), "alert-success") - return redirect(request.args.get("next") or url_for("list_todomails")) - + flash("Die Todo-Mail-Zuordnung für {} wurde gelöscht.".format(name), + "alert-success") + return back.redirect("list_todomails") + + @app.route("/defaultmeta/new/<int:protocoltype_id>", methods=["GET", "POST"]) @login_required @db_lookup(ProtocolType) @@ -1348,8 +1565,10 @@ def new_defaultmeta(protocoltype): db.session.add(meta) db.session.commit() flash("Metadatenfeld hinzugefügt.", "alert-success") - return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id)) - return render_template("defaultmeta-new.html", form=form, protocoltype=protocoltype) + return back.redirect("show_type", protocoltype_id=protocoltype.id) + return render_template( + "defaultmeta-new.html", form=form, protocoltype=protocoltype) + @app.route("/defaultmeta/edit/<int:defaultmeta_id>", methods=["GET", "POST"]) @login_required @@ -1360,13 +1579,17 @@ def edit_defaultmeta(defaultmeta): if form.validate_on_submit(): form.populate_obj(defaultmeta) db.session.commit() - return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=defaultmeta.protocoltype.id)) - return render_template("defaultmeta-edit.html", form=form, defaultmeta=defaultmeta) + return back.redirect( + "show_type", protocoltype_id=defaultmeta.protocoltype.id) + return render_template( + "defaultmeta-edit.html", form=form, defaultmeta=defaultmeta) + @app.route("/defaultmeta/delete/<int:defaultmeta_id>") @login_required -@group_required(config.ADMIN_GROUP) +@protect_csrf @db_lookup(DefaultMeta) +@require_admin_right() @require_modify_right() def delete_defaultmeta(defaultmeta): name = defaultmeta.name @@ -1374,9 +1597,11 @@ def delete_defaultmeta(defaultmeta): db.session.delete(defaultmeta) db.session.commit() flash("Metadatenfeld '{}' gelöscht.".format(name), "alert-success") - return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=type_id)) + return back.redirect("show_type", protocoltype_id=type_id) + -@app.route("/decisioncategory/new/<int:protocoltype_id>", methods=["GET", "POST"]) +@app.route("/decisioncategory/new/<int:protocoltype_id>", + methods=["GET", "POST"]) @login_required @db_lookup(ProtocolType) @require_modify_right() @@ -1388,10 +1613,13 @@ def new_decisioncategory(protocoltype): db.session.add(category) db.session.commit() flash("Beschlusskategorie hinzugefügt.", "alert-success") - return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id)) - return render_template("decisioncategory-new.html", form=form, protocoltype=protocoltype) + return back.redirect("show_type", protocoltype_id=protocoltype.id) + return render_template( + "decisioncategory-new.html", form=form, protocoltype=protocoltype) + -@app.route("/decisioncategory/edit/<int:decisioncategory_id>", methods=["GET", "POST"]) +@app.route("/decisioncategory/edit/<int:decisioncategory_id>", + methods=["GET", "POST"]) @login_required @db_lookup(DecisionCategory) @require_modify_right() @@ -1400,13 +1628,18 @@ def edit_decisioncategory(decisioncategory): if form.validate_on_submit(): form.populate_obj(decisioncategory) db.session.commit() - return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=decisioncategory.protocoltype.id)) - return render_template("decisioncategory-edit.html", form=form, decisioncategory=decisioncategory) + return back.redirect( + "show_type", protocoltype_id=decisioncategory.protocoltype.id) + return render_template( + "decisioncategory-edit.html", form=form, + decisioncategory=decisioncategory) + @app.route("/decisioncategory/delete/<int:decisioncategory_id>") @login_required -@group_required(config.ADMIN_GROUP) +@protect_csrf @db_lookup(DecisionCategory) +@require_admin_right() @require_modify_right() def delete_decisioncategory(decisioncategory): name = decisioncategory.name @@ -1414,33 +1647,39 @@ def delete_decisioncategory(decisioncategory): db.session.delete(decisioncategory) db.session.commit() flash("Beschlusskategorie {} gelöscht.".format(name), "alert-success") - return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=type_id)) + return back.redirect("show_type", protocoltype_id=type_id) + def create_protocols_feed(protocoltype): if not protocoltype.has_public_anonymous_view_right(): abort(403) - protocols = [protocol - for protocol in protocoltype.protocols + protocols = [ + protocol for protocol in protocoltype.protocols if protocol.is_done() ] feed = feedgen.feed.FeedGenerator() feed.description(protocoltype.name) - feed.generator("Protokollsystem 3", + feed.generator( + "Protokollsystem 3", uri="https://git.fsmpi.rwth-aachen.de/protokollsystem/proto3") - feed.id(url_for("show_type", protocoltype_id=protocoltype.id, _external=True)) - feed.link(href=url_for("list_protocols", protocoltype_id=protocoltype.id, + feed.id(url_for( + "show_type", protocoltype_id=protocoltype.id, _external=True)) + feed.link(href=url_for( + "list_protocols", protocoltype_id=protocoltype.id, state_open=False, _external=True), rel="alternate") feed.title(protocoltype.short_name) for protocol in protocols: entry = feed.add_entry() - entry.id(url_for("show_protocol", - protocol_id=protocol.id, _external=True)) - entry.link(href=url_for("show_protocol", protocol_id=protocol.id, + entry.id(url_for( + "show_protocol", protocol_id=protocol.id, _external=True)) + entry.link(href=url_for( + "show_protocol", protocol_id=protocol.id, _external=True), rel="alternate") document = protocol.get_compiled_document(private=False) if document is not None: - entry.link(href=url_for("download_document", - document_id=document.id, _external=True), rel="enclosure", + entry.link(href=url_for( + "download_document", document_id=document.id, _external=True), + rel="enclosure", title="Protokoll", type="application/pdf") entry.title(protocol.get_title()) entry.summary(",\n".join(top.name for top in protocol.get_tops())) @@ -1452,73 +1691,86 @@ def create_protocols_feed(protocoltype): def create_appointments_feed(protocoltype): if not protocoltype.has_public_anonymous_view_right(): abort(403) - protocols = [protocol - for protocol in protocoltype.protocols + protocols = [ + protocol for protocol in protocoltype.protocols if not protocol.is_done() ] feed = feedgen.feed.FeedGenerator() feed.description(protocoltype.name) - feed.generator("Protokollsystem 3", + feed.generator( + "Protokollsystem 3", uri="https://git.fsmpi.rwth-aachen.de/protokollsystem/proto3") - feed.id(url_for("show_type", protocoltype_id=protocoltype.id, _external=True)) - feed.link(href=url_for("list_protocols", protocoltype_id=protocoltype.id, + feed.id(url_for( + "show_type", protocoltype_id=protocoltype.id, _external=True)) + feed.link(href=url_for( + "list_protocols", protocoltype_id=protocoltype.id, state_open=True, _external=True), rel="alternate") feed.title("{}-Termine".format(protocoltype.short_name)) for protocol in protocols: entry = feed.add_entry() - entry.id(url_for("show_protocol", - protocol_id=protocol.id, _external=True)) - entry.link(href=url_for("show_protocol", protocol_id=protocol.id, - _external=True), rel="alternate") + entry.id(url_for( + "show_protocol", protocol_id=protocol.id, _external=True)) + entry.link(href=url_for( + "show_protocol", protocol_id=protocol.id, _external=True), + rel="alternate") entry.title(protocol.get_title()) entry.summary("\n".join( - [",\n".join([ + [ + ",\n".join([ "Beginn: {}".format(protocol.get_time()) ] + [ "{}: {}".format(meta.name, meta.value) for meta in protocol.metas if not meta.internal - ] - ), - "Tagesordnung:", - ",\n".join( - "TOP {}".format(top.name) - for top in protocol.get_tops() - )])) + ]), + "Tagesordnung:", + ",\n".join( + "TOP {}".format(top.name) + for top in protocol.get_tops() + ) + ])) return feed @app.route("/feed/protocols/rss/<int:protocoltype_id>") @db_lookup(ProtocolType) def feed_protocols_rss(protocoltype): - return Response(create_protocols_feed(protocoltype).rss_str(), + return Response( + create_protocols_feed(protocoltype).rss_str(), mimetype="application/rss+xml") + @app.route("/feed/protocols/atom/<int:protocoltype_id>") @db_lookup(ProtocolType) def feed_protocols_atom(protocoltype): - return Response(create_protocols_feed(protocoltype).atom_str(), + return Response( + create_protocols_feed(protocoltype).atom_str(), mimetype="application/atom+xml") + @app.route("/feed/appointments/rss/<int:protocoltype_id>") @db_lookup(ProtocolType) def feed_appointments_rss(protocoltype): - return Response(create_appointments_feed(protocoltype).rss_str(), + return Response( + create_appointments_feed(protocoltype).rss_str(), mimetype="application/rss+xml") + @app.route("/feed/appointments/atom/<int:protocoltype_id>") @db_lookup(ProtocolType) def feed_appointments_atom(protocoltype): - return Response(create_appointments_feed(protocoltype).atom_str(), + return Response( + create_appointments_feed(protocoltype).atom_str(), mimetype="application/atom+xml") + @app.route("/feed/appointments/ical/<int:protocoltype_id>") @db_lookup(ProtocolType) def feed_appointments_ical(protocoltype): if not protocoltype.has_public_anonymous_view_right(): abort(403) - protocols = [protocol - for protocol in protocoltype.protocols + protocols = [ + protocol for protocol in protocoltype.protocols if not protocol.is_done() ] calendar = icalendar.Calendar() @@ -1534,41 +1786,43 @@ def feed_appointments_ical(protocoltype): event["dtstart"] = to_datetime(start) event["dtend"] = to_datetime(start + timedelta(hours=3)) event["summary"] = protocoltype.short_name - event["description"] = "\n".join(top.name - for top in protocol.get_tops()) + event["description"] = "\n".join( + top.name for top in protocol.get_tops()) calendar.add_component(event) content = calendar.to_ical().decode("utf-8") for key in config.CALENDAR_TIMEZONE_MAP: - content = content.replace("TZID={}:".format(key), + content = content.replace( + "TZID={}:".format(key), "TZID={}:".format(config.CALENDAR_TIMEZONE_MAP[key])) return Response(content.encode("utf-8"), mimetype="text/calendar") @app.route("/like/new") @login_required +@protect_csrf def new_like(): user = current_user() parent = None if "protocol_id" in request.args: - parent = Protocol.query.filter_by(id=request.args.get("protocol_id")).first() + parent = Protocol.first_by_id(request.args.get("protocol_id")) elif "todo_id" in request.args: - parent = Todo.query.filter_by(id=request.args.get("todo_id")).first() + parent = Todo.first_by_id(request.args.get("todo_id")) elif "decision_id" in request.args: - parent = Decision.query.filter_by(id=request.args.get("decision_id")).first() + parent = Decision.first_by_id(request.args.get("decision_id")) elif "top_id" in request.args: - parent = TOP.query.filter_by(id=request.args.get("top_id")).first() + parent = TOP.first_by_id(request.args.get("top_id")) if parent is None or not parent.has_public_view_right(user): flash("Missing object to like.", "alert-error") - return redirect(request.args.get("next") or url_for("index")) + return back.redirect() if len([like for like in parent.likes if like.who == user.username]) > 0: flash("You have liked this already!", "alert-error") - return redirect(request.args.get("next") or url_for("index")) + return back.redirect() like = Like(who=user.username) db.session.add(like) parent.likes.append(like) db.session.commit() flash("Like!", "alert-success") - return redirect(request.args.get("next") or url_for("index")) + return back.redirect() @app.route("/login", methods=["GET", "POST"]) @@ -1578,18 +1832,23 @@ def login(): return redirect(url_for("index")) form = LoginForm() if form.validate_on_submit(): - user = user_manager.login(form.username.data, form.password.data, permanent=form.permanent.data) + user = user_manager.login( + form.username.data, form.password.data, + permanent=form.permanent.data) if user is not None: session["auth"] = security_manager.hash_user(user) session.permanent = form.permanent.data - flash("Login successful, {}!".format(user.username), "alert-success") - return redirect(request.args.get("next") or url_for("index")) + flash("Login successful, {}!".format(user.username), + "alert-success") + return back.redirect() else: flash("Wrong login data. Try again.", "alert-error") return render_template("login.html", form=form) + @app.route("/logout") @login_required +@protect_csrf def logout(): if "auth" in session: session.pop("auth") @@ -1597,18 +1856,19 @@ def logout(): flash("You are not logged in.", "alert-error") return redirect(url_for(".index")) -def make_scheduler(): - pass + try: - from uwsgidecorators import timer as uwsgitimer, signal as uwsgisignal, cron as uwsgicron - print("using uwsgi for cron-like tasks") + from uwsgidecorators import cron as uwsgicron + @uwsgicron(30, -1, -1, -1, -1, target="mule") def uwsgi_timer(signum): if signum == 0: check_and_send_reminders() + + def make_scheduler(): + pass except ImportError as exc: def make_scheduler(): - print("uwsgi not found, falling back to apscheduler for cron-like tasks") scheduler = BackgroundScheduler() scheduler.start() scheduler.add_job( @@ -1619,24 +1879,29 @@ except ImportError as exc: replace_existing=True) atexit.register(scheduler.shutdown) + def check_and_send_reminders(): if not config.MAIL_ACTIVE: return with app.app_context(): current_time = datetime.now() current_day = current_time.date() - for protocol in Protocol.query.filter(Protocol.done != True).all(): + for protocol in Protocol.query.filter(not Protocol.done).all(): day_difference = (protocol.date - current_day).days usual_time = protocol.get_time() - protocol_time = datetime(1, 1, 1, usual_time.hour, usual_time.minute) + protocol_time = datetime( + 1, 1, 1, usual_time.hour, usual_time.minute) hour_difference = (protocol_time - current_time).seconds // 3600 for reminder in protocol.protocoltype.reminders: - if day_difference == reminder.days_before and hour_difference == 0: + if (day_difference == reminder.days_before + and hour_difference == 0): tasks.send_reminder(reminder, protocol) if (day_difference < 0 - and -day_difference > config.MAX_PAST_INDEX_DAYS_BEFORE_REMINDER - and hour_difference == 0): # once per day - tasks.remind_finishing(protocol, -day_difference, + and (-day_difference + > config.MAX_PAST_INDEX_DAYS_BEFORE_REMINDER) + and hour_difference == 0): # once per day + tasks.remind_finishing( + protocol, -day_difference, config.MAX_PAST_INDEX_DAYS_BEFORE_REMINDER) diff --git a/shared.py b/shared.py index 3e091c4303424a19004383c6715b722aea7564b4..83c3ed00611ab12345df0489de2b130dc0f279e1 100644 --- a/shared.py +++ b/shared.py @@ -1,17 +1,20 @@ from flask_sqlalchemy import SQLAlchemy -from flask import session, redirect, url_for, request +from flask import session, redirect, url_for, flash import re from functools import wraps from enum import Enum +import back + import config db = SQLAlchemy() -# the following code is written by Lars Beckers and not to be published without permission +# the following code escape_tex is written by Lars Beckers +# and not to be published without permission latex_chars = [ - ("\\", "\\backslash"), # this needs to be first + ("\\", "\\backslash"), # this needs to be first ("$", "\$"), ('%', '\\%'), ('&', '\\&'), @@ -21,7 +24,6 @@ latex_chars = [ ('}', '\\}'), ('[', '\\['), (']', '\\]'), - #('"', '"\''), ('~', r'$\sim{}$'), ('^', r'\textasciicircum{}'), ('Ë„', r'\textasciicircum{}'), @@ -35,84 +37,119 @@ latex_chars = [ ('<', '$<$'), ('>', '$>$'), ('\\backslashin', '$\\in$'), - ('\\backslash', '$\\backslash$') # this needs to be last + ('\\backslash', '$\\backslash$') # this needs to be last ] + def escape_tex(text): out = text for old, new in latex_chars: out = out.replace(old, new) # beware, the following is carefully crafted code res = '' - k, l = (0, -1) - while k >= 0: - k = out.find('"', l+1) - if k >= 0: - res += out[l+1:k] - l = out.find('"', k+1) - if l >= 0: - res += '\\enquote{' + out[k+1:l] + '}' + start, end = (0, -1) + while start >= 0: + start = out.find('"', end + 1) + if start >= 0: + res += out[end + 1:start] + end = out.find('"', start + 1) + if end >= 0: + res += '\\enquote{' + out[start + 1:end] + '}' else: - res += '"\'' + out[k+1:] - k = l + res += '"\'' + out[start + 1:] + start = end else: - res += out[l+1:] + res += out[end + 1:] # yes, this is not quite escaping latex chars, but anyway... res = re.sub('([a-z])\(', '\\1 (', res) res = re.sub('\)([a-z])', ') \\1', res) - #logging.debug('escape latex ({0}/{1}): {2} --> {3}'.format(len(text), len(res), text.split('\n')[0], res.split('\n')[0])) return res + def unhyphen(text): return " ".join([r"\mbox{" + word + "}" for word in text.split(" ")]) + def date_filter(date): return date.strftime("%d. %B %Y") + + def datetime_filter(date): + if date is None: + return "" return date.strftime("%d. %B %Y, %H:%M") + + def date_filter_long(date): + if date is None: + return "" return date.strftime("%A, %d.%m.%Y, Kalenderwoche %W") + + def date_filter_short(date): + if date is None: + return "" return date.strftime("%d.%m.%Y") + + def time_filter(time): + if time is None: + return "" return time.strftime("%H:%M Uhr") + + def time_filter_short(time): + if time is None: + return "" return time.strftime("%H:%M") + def needs_date_test(todostate): return todostate.needs_date() + + def todostate_name_filter(todostate): return todostate.get_name() + def indent_tab_filter(text): return "\n".join(map(lambda l: "\t{}".format(l), text.splitlines())) + def class_filter(obj): return obj.__class__.__name__ + + def code_filter(text): return "<code>{}</code>".format(text) + from auth import UserManager, SecurityManager, User max_duration = getattr(config, "AUTH_MAX_DURATION") user_manager = UserManager(backends=config.AUTH_BACKENDS) security_manager = SecurityManager(config.SECURITY_KEY, max_duration) + 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 redirect(url_for("login")) return decorated_function + def group_required(group): def decorator(function): @wraps(function) @@ -120,16 +157,19 @@ def group_required(group): 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")) + flash("You do not have the necessary permissions to " + "view this page.") + return back.redirect() return decorated_function return decorator + DATE_KEY = "Datum" START_TIME_KEY = "Beginn" END_TIME_KEY = "Ende" KNOWN_KEYS = [DATE_KEY, START_TIME_KEY, END_TIME_KEY] + class WikiType(Enum): MEDIAWIKI = 0 DOKUWIKI = 1 diff --git a/tasks.py b/tasks.py index 852c3b48b565e716c1bcf8a44308e9562996abda..50d047684b23ee32a6e22e8cf71414cd8f05d8ba 100644 --- a/tasks.py +++ b/tasks.py @@ -9,15 +9,21 @@ import traceback from copy import copy import xmlrpc.client -from models.database import Document, Protocol, Error, Todo, Decision, TOP, DefaultTOP, MeetingReminder, TodoMail, DecisionDocument, TodoState, OldTodo, DecisionCategory +from models.database import ( + Document, Protocol, Todo, Decision, TOP, MeetingReminder, + TodoMail, DecisionDocument, TodoState, OldTodo, DecisionCategory) 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, KNOWN_KEYS, WikiType -from utils import mail_manager, encode_kwargs, decode_kwargs, add_line_numbers, set_etherpad_text, get_etherpad_text, footnote_hash, parse_datetime_from_string -from protoparser import parse, ParserException, Element, Content, Text, Tag, Remark, Fork, RenderType +from shared import ( + db, escape_tex, unhyphen, date_filter, datetime_filter, date_filter_long, + date_filter_short, time_filter, class_filter, KNOWN_KEYS, WikiType) +from utils import ( + mail_manager, add_line_numbers, + set_etherpad_text, parse_datetime_from_string) +from protoparser import parse, ParserException, Tag, Remark, Fork, RenderType from wiki import WikiClient, WikiException from calendarpush import Client as CalendarClient, CalendarException -from legacy import lookup_todo_id, import_old_todos +from legacy import lookup_todo_id import config @@ -32,7 +38,6 @@ texenv.filters["escape_tex"] = escape_tex texenv.filters["unhyphen"] = unhyphen texenv.trim_blocks = True texenv.lstrip_blocks = True -#texenv.filters["url_complete"] = url_manager.complete texenv.filters["datify"] = date_filter texenv.filters["datify_long"] = date_filter_long texenv.filters["datify_short"] = date_filter_short @@ -42,7 +47,9 @@ texenv.filters["class"] = class_filter logo_template = getattr(config, "LATEX_LOGO_TEMPLATE", None) if logo_template is not None: texenv.globals["logo_template"] = logo_template -latex_geometry = getattr(config, "LATEX_GEOMETRY", "vmargin=1.5cm,hmargin={1.5cm,1.2cm},bindingoffset=8mm") +latex_geometry = getattr( + config, "LATEX_GEOMETRY", + "vmargin=1.5cm,hmargin={1.5cm,1.2cm},bindingoffset=8mm") texenv.globals["latex_geometry"] = latex_geometry raw_additional_packages = getattr(config, "LATEX_ADDITIONAL_PACKAGES", None) additional_packages = [] @@ -59,51 +66,67 @@ latex_header_footer = getattr(config, "LATEX_HEADER_FOOTER", False) texenv.globals["latex_header_footer"] = latex_header_footer latex_templates = getattr(config, "LATEX_TEMPLATES", None) + def provide_latex_template(template, documenttype): - _latex_template_documenttype_filename = { - "class": "protokoll2.cls", - "protocol": "protocol.tex", - "decision": "decision.tex" - } - _latex_template_filename = _latex_template_documenttype_filename[documenttype] - _latex_template_foldername = "" - if logo_template is not None: - texenv.globals["logo_template"] = logo_template - texenv.globals["latex_geometry"] = latex_geometry - texenv.globals["additional_packages"] = additional_packages - if latex_pagestyle is not None and latex_pagestyle: - texenv.globals["latex_pagestyle"] = latex_pagestyle - elif "latex_pagestyle" in texenv.globals: - del texenv.globals["latex_pagestyle"] - texenv.globals["latex_header_footer"] = latex_header_footer - if (latex_templates is not None) and (template is not ""): - if template in latex_templates: - if "provides" in latex_templates[template]: - _latex_template_foldername = (template + "/") if documenttype in latex_templates[template]["provides"] else "" - if "logo" in latex_templates[template]: - texenv.globals["logo_template"] = template + "/" + latex_templates[template]["logo"] - if "geometry" in latex_templates[template]: - texenv.globals["latex_geometry"] = latex_templates[template]["geometry"] - if "pagestyle" in latex_templates[template]: - if latex_templates[template]["pagestyle"]: - texenv.globals["latex_pagestyle"] = latex_templates[template]["pagestyle"] - if "additionalpackages" in latex_templates[template]: - _raw_additional_packages = latex_templates[template]["additionalpackages"] - _additional_packages = [] - if _raw_additional_packages is not None: - for _package in _raw_additional_packages: - if "{" not in _package: - _package = "{{{}}}".format(_package) - _additional_packages.append(_package) - texenv.globals["additional_packages"] = _additional_packages - if "headerfooter" in latex_templates[template]: - texenv.globals["latex_header_footer"] = latex_templates[template]["headerfooter"] - return _latex_template_foldername + _latex_template_filename + _DOCUMENTTYPE_FILENAME_MAP = { + "class": "protokoll2.cls", + "protocol": "protocol.tex", + "decision": "decision.tex" + } + _PROVIDES = "provides" + _LOGO_TEMPLATE = "logo_template" + _LOGO = "logo" + _LATEX_GEOMETRY = "latex_geometry" + _GEOMETRY = "geometry" + _ADDITIONAL_PACKAGES = "additional_packages" + _LATEX_PAGESTYLE = "latex_pagestyle" + _PAGESTYLE = "pagestyle" + _LATEX_HEADER_FOOTER = "latex_header_footer" + _HEADER_FOOTER = "headerfooter" + _latex_template_filename = _DOCUMENTTYPE_FILENAME_MAP[documenttype] + _latex_template_foldername = "" + if logo_template is not None: + texenv.globals[_LOGO_TEMPLATE] = logo_template + texenv.globals[_LATEX_GEOMETRY] = latex_geometry + texenv.globals[_ADDITIONAL_PACKAGES] = additional_packages + if latex_pagestyle: + texenv.globals[_LATEX_PAGESTYLE] = latex_pagestyle + elif _LATEX_PAGESTYLE in texenv.globals: + del texenv.globals[_LATEX_PAGESTYLE] + texenv.globals[_LATEX_HEADER_FOOTER] = latex_header_footer + if latex_templates is not None and template != "": + if template in latex_templates: + template_data = latex_templates[template] + if _PROVIDES in template_data: + if documenttype in template_data[_PROVIDES]: + _latex_template_foldername = template + if _LOGO in template_data: + texenv.globals[_LOGO_TEMPLATE] = os.path.join( + template, template_data[_LOGO]) + if _GEOMETRY in template_data: + texenv.globals[_LATEX_GEOMETRY] = template_data[_GEOMETRY] + if _PAGESTYLE in template_data: + if template_data[_PAGESTYLE]: + texenv.globals[_LATEX_PAGESTYLE] = ( + template_data[_PAGESTYLE]) + if _ADDITIONAL_PACKAGES in template_data: + _raw_additional_packages = template_data[_ADDITIONAL_PACKAGES] + _additional_packages = [] + if _raw_additional_packages is not None: + for _package in _raw_additional_packages: + if "{" not in _package: + _package = "{{{}}}".format(_package) + _additional_packages.append(_package) + texenv.globals[_ADDITIONAL_PACKAGES] = _additional_packages + if _HEADER_FOOTER in latex_templates[template]: + texenv.globals[_LATEX_HEADER_FOOTER] = ( + template_data[_HEADER_FOOTER]) + return os.path.join(_latex_template_foldername, _latex_template_filename) + mailenv = app.create_jinja_environment() mailenv.trim_blocks = True mailenv.lstrip_blocks = True -#mailenv.filters["url_complete"] = url_manager.complete mailenv.filters["datify"] = date_filter mailenv.filters["datetimify"] = datetime_filter @@ -124,43 +147,46 @@ wikienv.filters["timify"] = time_filter wikienv.filters["class"] = class_filter +def _make_error(protocol, *args): + error = protocol.create_error(*args) + db.session.add(error) + db.session.commit() + + ID_FIELD_BEGINNING = "id " -def parse_protocol(protocol, **kwargs): - parse_protocol_async.delay(protocol.id, encode_kwargs(kwargs)) + +def parse_protocol(protocol): + parse_protocol_async.delay(protocol.id) + @celery.task -def parse_protocol_async(protocol_id, encoded_kwargs): +def parse_protocol_async(protocol_id): with app.app_context(): with app.test_request_context("/"): try: - kwargs = decode_kwargs(encoded_kwargs) - protocol = Protocol.query.filter_by(id=protocol_id).first() + protocol = Protocol.first_by_id(protocol_id) if protocol is None: raise Exception("No protocol given. Aborting parsing.") - parse_protocol_async_inner(protocol, encoded_kwargs) + parse_protocol_async_inner(protocol) except Exception as exc: stacktrace = traceback.format_exc() - error = protocol.create_error("Parsing", "Exception", + return _make_error( + protocol, "Parsing", "Exception", "{}\n\n{}".format(str(exc), stacktrace)) - db.session.add(error) - db.session.commit() -def parse_protocol_async_inner(protocol, encoded_kwargs): + +def parse_protocol_async_inner(protocol): old_errors = list(protocol.errors) for error in old_errors: protocol.errors.remove(error) db.session.commit() if protocol.source is None or len(protocol.source.strip()) == 0: - error = protocol.create_error("Parsing", "Protocol source is empty", "") - db.session.add(error) - db.session.commit() - return + return _make_error(protocol, "Parsing", "Protocol source is empty", "") if protocol.source == config.EMPTY_ETHERPAD: - error = protocol.create_error("Parsing", "The etherpad is unmodified and does not contain a protocol.", protocol.source) - db.session.add(error) - db.session.commit() - return + return _make_error( + protocol, "Parsing", "The etherpad is unmodified and does not " + "contain a protocol.", protocol.source) tree = None try: tree = parse(protocol.source) @@ -169,59 +195,64 @@ def parse_protocol_async_inner(protocol, encoded_kwargs): if exc.linenumber is not None: source_lines = protocol.source.splitlines() start_index = max(0, exc.linenumber - config.ERROR_CONTEXT_LINES) - end_index = min(len(source_lines) - 1, exc.linenumber + config.ERROR_CONTEXT_LINES) + end_index = min( + len(source_lines) - 1, + exc.linenumber + config.ERROR_CONTEXT_LINES) context = "\n".join(source_lines[start_index:end_index]) if exc.tree is not None: context += "\n\nParsed syntax tree was:\n" + str(exc.tree.dump()) - error = protocol.create_error("Parsing", str(exc), context) - db.session.add(error) - db.session.commit() - return - remarks = {element.name: element for element in tree.children if isinstance(element, Remark)} + return _make_error(protocol, "Parsing", str(exc), context) + remarks = { + element.name: element + for element in tree.children + if isinstance(element, Remark) + } required_fields = copy(KNOWN_KEYS) for default_meta in protocol.protocoltype.metas: required_fields.append(default_meta.key) if not config.PARSER_LAZY: - missing_fields = [field for field in required_fields 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", "Du hast vergessen, Metadaten anzugeben.", ", ".join(missing_fields)) - db.session.add(error) - db.session.commit() - return + return _make_error( + protocol, "Parsing", "Du hast vergessen, Metadaten anzugeben.", + ", ".join(missing_fields)) try: protocol.fill_from_remarks(remarks) except ValueError: - error = protocol.create_error( - "Parsing", "Invalid fields", + return _make_error( + protocol, "Parsing", "Invalid fields", "Date or time fields are not '%d.%m.%Y' respectively '%H:%M', " "but rather {}".format( - ", ".join([ - remarks["Datum"].value.strip(), - remarks["Beginn"].value.strip(), - remarks["Ende"].value.strip() - ]))) - db.session.add(error) - db.session.commit() - return + ", ".join([ + remarks["Datum"].value.strip(), + remarks["Beginn"].value.strip(), + remarks["Ende"].value.strip() + ]))) 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) if exc.original_date is not None else "not present", date_filter(exc.protocol_date) if exc.protocol_date is not None else "not present")) - db.session.add(error) - db.session.commit() - return - # tags + return _make_error( + protocol, "Parsing", "Date not matching", + "This protocol's date should be {}, but the protocol source " + "says {}.".format( + date_filter(exc.original_date) + if exc.original_date is not None + else "not present", + date_filter(exc.protocol_date) + if exc.protocol_date is not None + else "not present")) + # tags tags = tree.get_tags() - elements = tree.get_visible_elements(show_private=True) public_elements = tree.get_visible_elements(show_private=False) for tag in tags: if tag.name not in Tag.KNOWN_TAGS: - error = protocol.create_error("Parsing", "Invalid tag", + return _make_error( + protocol, "Parsing", "Invalid tag", "The tag in line {} has the kind '{}', which is " "not defined. This is probably an error mit a missing " "semicolon.".format(tag.linenumber, tag.name)) - db.session.add(error) - db.session.commit() - return # todos old_todo_number_map = {} for todo in protocol.todos: @@ -233,14 +264,12 @@ def parse_protocol_async_inner(protocol, encoded_kwargs): raw_todos = [] for todo_tag in todo_tags: if len(todo_tag.values) < 2: - error = protocol.create_error("Parsing", "Invalid todo-tag", + return _make_error( + protocol, "Parsing", "Invalid todo-tag", "The todo tag in line {} needs at least " "information on who and what, " "but has less than that. This is probably " "a missing semicolon.".format(todo_tag.linenumber)) - db.session.add(error) - db.session.commit() - return who = todo_tag.values[0] what = todo_tag.values[1] field_id = None @@ -254,39 +283,45 @@ def parse_protocol_async_inner(protocol, encoded_kwargs): try: field_id = int(other_field[len(ID_FIELD_BEGINNING):]) except ValueError: - error = protocol.create_error("Parsing", "Non-numerical todo ID", - "The todo in line {} has a nonnumerical ID, but needs " - "something like \"id 1234\"".format(todo_tag.linenumber)) - db.session.add(error) - db.session.commit() - return + return _make_error( + protocol, "Parsing", "Non-numerical todo ID", + "The todo in line {} has a nonnumerical ID, but needs " + "something like \"id 1234\"".format( + todo_tag.linenumber)) else: try: field_state = TodoState.from_name(other_field) + continue + except ValueError: + pass + try: + field_date = datetime.strptime(other_field, "%d.%m.%Y") + continue except ValueError: - try: - field_date = datetime.strptime(other_field, "%d.%m.%Y") - except ValueError: - try: - field_state, field_date = TodoState.from_name_with_date(other_field.strip(), protocol=protocol) - except ValueError: - try: - field_state = TodoState.from_name_lazy(other_field) - except ValueError: - error = protocol.create_error("Parsing", - "Invalid field", - "The todo in line {} has the field '{}', but " - "this does neither match a date (\"%d.%m.%Y\") " - "nor a state.".format( - todo_tag.linenumber, other_field)) - db.session.add(error) - db.session.commit() - return - raw_todos.append((who, what, field_id, field_state, field_date, todo_tag)) + pass + try: + field_state, field_date = TodoState.from_name_with_date( + other_field.strip(), protocol=protocol) + continue + except ValueError: + pass + try: + field_state = TodoState.from_name_lazy(other_field) + except ValueError: + return _make_error( + protocol, "Parsing", "Invalid field", + "The todo in line {} has the field '{}', but " + "this does neither match a date (\"%d.%m.%Y\") " + "nor a state.".format( + todo_tag.linenumber, other_field)) + raw_todos.append( + (who, what, field_id, field_state, field_date, todo_tag)) for (_, _, field_id, _, _, _) in raw_todos: if field_id is not None: - old_todos = [todo for todo in old_todos - if todo.id != field_id] + old_todos = [ + todo for todo in old_todos + if todo.id != field_id + ] for todo in old_todos: protocol.todos.remove(todo) db.session.commit() @@ -294,28 +329,23 @@ def parse_protocol_async_inner(protocol, encoded_kwargs): if field_state is None: field_state = TodoState.open if field_state.needs_date() and field_date is None: - error = protocol.create_error("Parsing", - "Todo missing date", + return _make_error( + protocol, "Parsing", "Todo missing date", "The todo in line {} has a state that needs a date, " "but the todo does not have one.".format(todo_tag.linenumber)) - db.session.add(error) - db.session.commit() - return who = who.strip() what = what.strip() todo = None if field_id is not None: todo = Todo.query.filter_by(number=field_id).first() if todo is None and not config.PARSER_LAZY: - error = protocol.create_error("Parsing", - "Invalid Todo ID", - "The todo in line {} has the ID {}, but there is no " - "Todo with that ID.".format(todo_tag.linenumber, field_id)) - db.session.add(error) - db.session.commit() - return + return _make_error( + protocol, "Parsing", "Invalid Todo ID", + "The todo in line {} has the ID {}, but there is no " + "Todo with that ID.".format(todo_tag.linenumber, field_id)) if todo is None and field_id is None and what in old_todo_number_map: - todo = Todo(protocoltype_id=protocol.protocoltype.id, + todo = Todo( + protocoltype_id=protocol.protocoltype.id, who=who, description=what, state=field_state, date=field_date, number=old_todo_number_map[what]) db.session.add(todo) @@ -326,7 +356,8 @@ def parse_protocol_async_inner(protocol, encoded_kwargs): OldTodo.protocol_key == protocol_key).all() if len(old_candidates) == 0: # new protocol - todo = Todo(protocoltype_id=protocol.protocoltype.id, + todo = Todo( + protocoltype_id=protocol.protocoltype.id, who=who, description=what, state=field_state, date=field_date) db.session.add(todo) @@ -338,7 +369,8 @@ def parse_protocol_async_inner(protocol, encoded_kwargs): number = field_id or lookup_todo_id(old_candidates, who, what) todo = Todo.query.filter_by(number=number).first() if todo is None: - todo = Todo(protocoltype_id=protocol.protocoltype.id, + todo = Todo( + protocoltype_id=protocol.protocoltype.id, who=who, description=what, state=field_state, date=field_date, number=number) db.session.add(todo) @@ -360,13 +392,12 @@ def parse_protocol_async_inner(protocol, encoded_kwargs): decision_tags = [tag for tag in tags if tag.name == "beschluss"] for decision_tag in decision_tags: if decision_tag not in public_elements: - error = protocol.create_error("Parsing", "Decision in private context.", - "The decision in line {} is in a private context, but decisions are " - "and have to be public. Please move it to a public spot.".format( - decision_tag.linenumber)) - db.session.add(error) - db.session.commit() - return + return _make_error( + protocol, "Parsing", "Decision in private context.", + "The decision in line {} is in a private context, but " + "decisions are and have to be public. " + "Please move it to a public spot.".format( + decision_tag.linenumber)) old_decisions = list(protocol.decisions) for decision in old_decisions: protocol.decisions.remove(decision) @@ -374,36 +405,35 @@ def parse_protocol_async_inner(protocol, encoded_kwargs): decisions_to_render = [] for decision_tag in decision_tags: if len(decision_tag.values) == 0: - error = protocol.create_error("Parsing", "Empty decision found.", - "The decision in line {} is empty.".format(decision_tag.linenumber)) - db.session.add(error) - db.session.commit() - return + return _make_error( + protocol, "Parsing", "Empty decision found.", + "The decision in line {} is empty.".format( + decision_tag.linenumber)) decision_content = decision_tag.values[0] decision_categories = [] for decision_category_name in decision_tag.values[1:]: - decision_category = DecisionCategory.query.filter_by(protocoltype_id=protocol.protocoltype.id, name=decision_category_name).first() + decision_category = DecisionCategory.query.filter_by( + protocoltype_id=protocol.protocoltype.id, + name=decision_category_name).first() if decision_category is None: - category_candidates = DecisionCategory.query.filter_by(protocoltype_id=protocol.protocoltype.id).all() + category_candidates = DecisionCategory.query.filter_by( + protocoltype_id=protocol.protocoltype.id).all() category_names = [ "'{}'".format(category.name) for category in category_candidates ] - error = protocol.create_error("Parsing", - "Unknown decision category", + return _make_error( + protocol, "Parsing", "Unknown decision category", "The decision in line {} has the category {}, " "but there is no such category. " "Known categories are {}".format( decision_tag.linenumber, decision_category_name, ", ".join(category_names))) - db.session.add(error) - db.session.commit() - return else: decision_categories.append(decision_category) - decision = Decision(protocol_id=protocol.id, - content=decision_content) + decision = Decision( + protocol_id=protocol.id, content=decision_content) db.session.add(decision) db.session.commit() for decision_category in decision_categories: @@ -412,66 +442,67 @@ def parse_protocol_async_inner(protocol, encoded_kwargs): decisions_to_render.append((decision, decision_tag)) for decision, decision_tag in decisions_to_render: decision_top = decision_tag.fork.get_top() - decision_content = texenv.get_template(provide_latex_template(protocol.protocoltype.latex_template, "decision")).render( - render_type=RenderType.latex, decision=decision, - protocol=protocol, top=decision_top, show_private=True) + decision_content = texenv.get_template(provide_latex_template( + protocol.protocoltype.latex_template, "decision")).render( + render_type=RenderType.latex, decision=decision, + protocol=protocol, top=decision_top, show_private=True) maxdepth = decision_top.get_maxdepth() compile_decision(decision_content, decision, maxdepth=maxdepth) # Footnotes - footnote_tags = [tag for tag in tags if tag.name == "footnote"] - public_footnote_tags = [tag for tag in footnote_tags if tag in public_elements] + footnote_tags = [ + tag for tag in tags + if tag.name == "footnote" + ] + public_footnote_tags = [ + tag for tag in footnote_tags + if tag in public_elements + ] # new Protocols protocol_tags = [tag for tag in tags if tag.name == "sitzung"] for protocol_tag in protocol_tags: if len(protocol_tag.values) not in {1, 2}: - error = protocol.create_error("Parsing", - "Falsche Verwendung von [sitzung;…].", + return _make_error( + protocol, "Parsing", "Falsche Verwendung von [sitzung;…].", "Der Tag \"sitzung\" benötigt immer ein Datum " "und optional eine Uhrzeit, also ein bis zwei Argumente. " "Stattdessen wurden {} übergeben, nämlich {}".format( - len(protocol_tag.values), - protocol_tag.values)) - db.session.add(error) - db.ession.commit() - return + len(protocol_tag.values), + protocol_tag.values)) else: try: - protocol_date = parse_datetime_from_string( - protocol_tag.values[0]) + parse_datetime_from_string(protocol_tag.values[0]) except ValueError as exc: - error = protocol.create_error("Parsing", "Invalides Datum", + return _make_error( + protocol, "Parsing", "Invalides Datum", "'{}' ist kein valides Datum.".format( protocol_tag.values[0])) - db.session.add(error) - db.session.commit() - return if len(protocol_tag.values) > 1: try: - protocol_time = datetime.strptime(protocol_tag.values[1], "%H:%M") + datetime.strptime(protocol_tag.values[1], "%H:%M") except ValueError: - error = protocol.create_error("Parsing", "Invalide Uhrzeit", + return _make_error( + protocol, "Parsing", "Invalide Uhrzeit", "'{}' ist keine valide Uhrzeit.".format( protocol_tag.values[1])) - db.session.add(error) - db.session.commit() - return for protocol_tag in protocol_tags: new_protocol_date = parse_datetime_from_string(protocol_tag.values[0]) new_protocol_time = None if len(protocol_tag.values) > 1: - new_protocol_time = datetime.strptime(protocol_tag.values[1], "%H:%M") - Protocol.create_new_protocol(protocol.protocoltype, - new_protocol_date, new_protocol_time) + new_protocol_time = datetime.strptime( + protocol_tag.values[1], "%H:%M") + Protocol.create_new_protocol( + protocol.protocoltype, new_protocol_date, new_protocol_time) # TOPs 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=protocol.id, name=fork.name, number=index, + for index, fork in enumerate( + (child for child in tree.children if isinstance(child, Fork))): + top = TOP( + protocol_id=protocol.id, name=fork.name, number=index, planned=False) db.session.add(top) db.session.commit() @@ -485,26 +516,35 @@ def parse_protocol_async_inner(protocol, encoded_kwargs): public_render_kwargs = copy(private_render_kwargs) public_render_kwargs["footnotes"] = public_footnote_tags render_kwargs = {True: private_render_kwargs, False: public_render_kwargs} - + maxdepth = tree.get_maxdepth() privacy_states = [False] - content_private = render_template("protocol.txt", render_type=RenderType.plaintext, show_private=True, **private_render_kwargs) - content_public = render_template("protocol.txt", render_type=RenderType.plaintext, show_private=False, **public_render_kwargs) + content_private = render_template( + "protocol.txt", render_type=RenderType.plaintext, show_private=True, + **private_render_kwargs) + content_public = render_template( + "protocol.txt", render_type=RenderType.plaintext, show_private=False, + **public_render_kwargs) if content_private != content_public: privacy_states.append(True) protocol.content_private = content_private protocol.content_public = content_public - protocol.content_html_private = render_template("protocol.html", - render_type=RenderType.html, show_private=True, **private_render_kwargs) - protocol.content_html_public = render_template("protocol.html", - render_type=RenderType.html, show_private=False, **public_render_kwargs) + protocol.content_html_private = render_template( + "protocol.html", render_type=RenderType.html, show_private=True, + **private_render_kwargs) + protocol.content_html_public = render_template( + "protocol.html", render_type=RenderType.html, show_private=False, + **public_render_kwargs) for show_private in privacy_states: - latex_source = texenv.get_template(provide_latex_template(protocol.protocoltype.latex_template, "protocol")).render( - render_type=RenderType.latex, - show_private=show_private, - **render_kwargs[show_private]) - compile(latex_source, protocol, show_private=show_private, maxdepth=maxdepth) + latex_source = texenv.get_template(provide_latex_template( + protocol.protocoltype.latex_template, "protocol")).render( + render_type=RenderType.latex, + show_private=show_private, + **render_kwargs[show_private]) + compile( + latex_source, protocol, show_private=show_private, + maxdepth=maxdepth) if protocol.protocoltype.use_wiki: wiki_type = WikiType[getattr(config, "WIKI_TYPE", "MEDIAWIKI")] @@ -525,17 +565,21 @@ def parse_protocol_async_inner(protocol, encoded_kwargs): if wiki_type == WikiType.MEDIAWIKI: wiki_infobox_source = wikienv.get_template("infobox.wiki").render( protocoltype=protocol.protocoltype) - push_to_wiki(protocol, wiki_source, wiki_infobox_source, + push_to_wiki( + protocol, wiki_source, wiki_infobox_source, "Automatisch generiert vom Protokollsystem 3.0") elif wiki_type == WikiType.DOKUWIKI: - push_to_dokuwiki(protocol, wiki_source, + push_to_dokuwiki( + protocol, wiki_source, "Automatisch generiert vom Protokollsystem 3.0") protocol.done = True db.session.commit() + def push_to_wiki(protocol, content, infobox_content, summary): push_to_wiki_async.delay(protocol.id, content, infobox_content, summary) + @celery.task def push_to_wiki_async(protocol_id, content, infobox_content, summary): with WikiClient() as wiki_client, app.app_context(): @@ -550,40 +594,48 @@ def push_to_wiki_async(protocol_id, content, infobox_content, summary): content=content, summary=summary) except WikiException as exc: - error = protocol.create_error("Pushing to Wiki", "Pushing to Wiki failed.", str(exc)) - db.session.add(error) - db.session.commit() + return _make_error( + protocol, "Pushing to Wiki", "Pushing to Wiki failed.", + str(exc)) + def push_to_dokuwiki(protocol, content, summary): push_to_dokuwiki_async.delay(protocol.id, content, summary) + @celery.task def push_to_dokuwiki_async(protocol_id, content, summary): with app.app_context(): protocol = Protocol.query.filter_by(id=protocol_id).first() with xmlrpc.client.ServerProxy(config.WIKI_API_URL) as proxy: try: - if not proxy.wiki.putPage(protocol.get_wiki_title(), - content, {"sum": "Automatisch generiert vom Protokollsystem 3."}): - error = protocol.create_error("Pushing to Wiki", + if not proxy.wiki.putPage( + protocol.get_wiki_title(), content, + {"sum": + "Automatisch generiert vom Protokollsystem 3."}): + return _make_error( + protocol, "Pushing to Wiki", "Pushing to Wiki failed." "") - db.session.add(error) - db.session.commit() except xmlrpc.client.Error as exception: - error = protocol.create_error("Pushing to Wiki", - "XML RPC Exception", + return _make_error( + protocol, "Pushing to Wiki", "XML RPC Exception", str(exception)) - db.session.add(error) - db.session.commit() + def compile(content, protocol, show_private, maxdepth): - compile_async.delay(content, protocol.id, show_private=show_private, maxdepth=maxdepth) + compile_async.delay( + content, protocol.id, show_private=show_private, maxdepth=maxdepth) + def compile_decision(content, decision, maxdepth): - compile_async.delay(content, decision.id, use_decision=True, maxdepth=maxdepth) + compile_async.delay( + content, decision.id, use_decision=True, maxdepth=maxdepth) + @celery.task -def compile_async(content, protocol_id, show_private=False, use_decision=False, maxdepth=5): +def compile_async( + content, protocol_id, show_private=False, use_decision=False, + maxdepth=5): with tempfile.TemporaryDirectory() as compile_dir, app.app_context(): decision = None protocol = None @@ -598,10 +650,19 @@ def compile_async(content, protocol_id, show_private=False, use_decision=False, protocol_target_filename = "protocol.pdf" protocol_class_filename = "protokoll2.cls" log_filename = "protocol.log" - with open(os.path.join(compile_dir, protocol_source_filename), "w") as source_file: + with open( + os.path.join(compile_dir, protocol_source_filename), + "w") as source_file: source_file.write(content) - protocol2_class_source = texenv.get_template(provide_latex_template(protocol.protocoltype.latex_template, "class")).render(fonts=config.FONTS, maxdepth=maxdepth, bulletpoints=config.LATEX_BULLETPOINTS) - with open(os.path.join(compile_dir, protocol_class_filename), "w") as protocol2_class_file: + protocol2_class_source = texenv.get_template( + provide_latex_template( + protocol.protocoltype.latex_template, + "class")).render( + fonts=config.FONTS, maxdepth=maxdepth, + bulletpoints=config.LATEX_BULLETPOINTS) + with open( + os.path.join(compile_dir, protocol_class_filename), + "w") as protocol2_class_file: protocol2_class_file.write(protocol2_class_source) os.chdir(compile_dir) command = [ @@ -610,15 +671,23 @@ def compile_async(content, protocol_id, show_private=False, use_decision=False, "-file-line-error", protocol_source_filename ] - 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) document = None if not use_decision: - for old_document in [document for document in protocol.documents if document.is_compiled and document.is_private == show_private]: + 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=protocol.id, + document = Document( + protocol_id=protocol.id, name="protokoll{}_{}_{}.pdf".format( "_intern" if show_private else "", protocol.protocoltype.short_name, @@ -627,7 +696,8 @@ def compile_async(content, protocol_id, show_private=False, use_decision=False, is_compiled=True, is_private=show_private) else: - document = DecisionDocument(decision_id=decision.id, + document = DecisionDocument( + decision_id=decision.id, name="beschluss_{}_{}_{}.pdf".format( protocol.protocoltype.short_name, date_filter_short(protocol.date), @@ -635,48 +705,60 @@ def compile_async(content, protocol_id, show_private=False, use_decision=False, filename="") db.session.add(document) db.session.commit() - target_filename = "compiled-{}-{}.pdf".format(document.id, "internal" if show_private else "public") + target_filename = "compiled-{}-{}.pdf".format( + document.id, "internal" if show_private else "public") if use_decision: - target_filename = "decision-{}-{}-{}.pdf".format(protocol.id, decision.id, document.id) + target_filename = "decision-{}-{}-{}.pdf".format( + protocol.id, decision.id, document.id) document.filename = target_filename - shutil.copy(os.path.join(compile_dir, protocol_target_filename), os.path.join(config.DOCUMENTS_PATH, 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/proto-tex.log") + shutil.copy( + os.path.join(compile_dir, log_filename), + "/tmp/proto-tex.log") except subprocess.SubprocessError: log = "" total_log_filename = os.path.join(compile_dir, log_filename) - total_source_filename = os.path.join(compile_dir, protocol_source_filename) + total_source_filename = os.path.join( + compile_dir, protocol_source_filename) log = "" if os.path.isfile(total_source_filename): with open(total_source_filename, "r") as source_file: log += "Source:\n\n" + add_line_numbers(source_file.read()) - total_class_filename = os.path.join(compile_dir, protocol_class_filename) + total_class_filename = os.path.join( + compile_dir, protocol_class_filename) if os.path.isfile(total_class_filename): with open(total_class_filename, "r") as class_file: - log += "\n\nClass:\n\n" + add_line_numbers(class_file.read()) + log += "\n\nClass:\n\n" + add_line_numbers( + class_file.read()) if os.path.isfile(total_log_filename): with open(total_log_filename, "r") as log_file: log += "\n\nLog:\n\n" + add_line_numbers(log_file.read()) else: log += "\n\nLogfile not found." - error = protocol.create_error("Compiling", "Compiling LaTeX failed", log) - db.session.add(error) - db.session.commit() + _make_error(protocol, "Compiling", "Compiling LaTeX failed", log) finally: os.chdir(current) + def print_file(filename, protocol): if config.PRINTING_ACTIVE: print_file_async.delay(filename, protocol.id) + @celery.task def print_file_async(filename, protocol_id): with app.app_context(): protocol = Protocol.query.filter_by(id=protocol_id).first() if protocol.protocoltype.printer is None: - error = protocol.create_error("Printing", "No printer configured.", "You don't have any printer configured for the protocoltype {}. Please do so before printing a protocol.".format(protocol.protocoltype.name)) - db.session.add(error) - db.session.commit() + return _make_error( + protocol, "Printing", "No printer configured.", + "You don't have any printer configured for the " + "protocoltype {}. " + "Please do so before printing a protocol.".format( + protocol.protocoltype.name)) try: command = [ "/usr/bin/lpr", @@ -685,70 +767,96 @@ def print_file_async(filename, protocol_id): "-U", config.PRINTING_USER, "-T", protocol.get_identifier(), ] - for option in config.PRINTING_PRINTERS[protocol.protocoltype.printer]: - command.extend(["-o", '"{}"'.format(option) if " " in option else option]) + for option in config.PRINTING_PRINTERS[ + protocol.protocoltype.printer]: + command.extend([ + "-o", '"{}"'.format(option) + if " " in option else option]) command.append(filename) - subprocess.check_output(command, universal_newlines=True, stderr=subprocess.STDOUT) + subprocess.check_output( + command, universal_newlines=True, stderr=subprocess.STDOUT) except subprocess.SubprocessError as exception: - error = protocol.create_error("Printing", "Printing {} failed.".format(protocol.get_identifier()), exception.stdout) - db.session.add(error) - db.session.commit() + return _make_error( + protocol, "Printing", "Printing {} failed.".format( + protocol.get_identifier()), exception.stdout) + def send_reminder(reminder, protocol): send_reminder_async.delay(reminder.id, protocol.id) + @celery.task def send_reminder_async(reminder_id, protocol_id): with app.app_context(): reminder = MeetingReminder.query.filter_by(id=reminder_id).first() protocol = Protocol.query.filter_by(id=protocol_id).first() - reminder_text = render_template("reminder-mail.txt", reminder=reminder, protocol=protocol) + reminder_text = render_template( + "reminder-mail.txt", reminder=reminder, protocol=protocol) if reminder.send_public: - print("sending public reminder mail to {}".format(protocol.protocoltype.public_mail)) - send_mail(protocol, protocol.protocoltype.public_mail, + send_mail( + protocol, protocol.protocoltype.public_mail, "Tagesordnung der {}".format(protocol.protocoltype.name), reminder_text, reply_to=protocol.protocoltype.public_mail) if reminder.send_private: - print("sending private reminder mail to {}".format(protocol.protocoltype.private_mail)) - send_mail(protocol, protocol.protocoltype.private_mail, + send_mail( + protocol, protocol.protocoltype.private_mail, "Tagesordnung der {}".format(protocol.protocoltype.name), reminder_text, reply_to=protocol.protocoltype.private_mail) + def remind_finishing(protocol, delay_days, min_delay_days): remind_finishing_async.delay(protocol.id, delay_days, min_delay_days) + @celery.task def remind_finishing_async(protocol_id, delay_days, min_delay_days): with app.app_context(): - protocol = Protocol.query.filter_by(id=protocol_id).first() - mail_text = render_template("remind-finishing-mail.txt", + protocol = Protocol.first_by_id(protocol_id) + mail_text = render_template( + "remind-finishing-mail.txt", protocol=protocol, delay_days=delay_days, min_delay_days=min_delay_days) - send_mail(protocol, protocol.protocoltype.private_mail, + send_mail( + protocol, protocol.protocoltype.private_mail, "Unfertiges Protokoll der {}".format(protocol.protocoltype.name), mail_text, reply_to=protocol.protocoltype.private_mail) + def send_protocol_private(protocol): send_protocol_async.delay(protocol.id, show_private=True) send_todomails_async.delay(protocol.id) + def send_protocol_public(protocol): send_protocol_async.delay(protocol.id, show_private=False) + @celery.task def send_protocol_async(protocol_id, show_private): with app.app_context(): protocol = Protocol.query.filter_by(id=protocol_id).first() - next_protocol = Protocol.query.filter_by(protocoltype_id=protocol.protocoltype.id).filter_by(done=False).filter(Protocol.date > datetime.now()).order_by(Protocol.date).first() - to_addr = protocol.protocoltype.private_mail if show_private else protocol.protocoltype.public_mail - subject = "{}{}-Protokoll vom {}".format("Internes " if show_private else "", protocol.protocoltype.short_name, date_filter(protocol.date)) - mail_content = render_template("protocol-mail.txt", protocol=protocol, show_private=show_private, next_protocol=next_protocol) - appendix = [(document.name, document.as_file_like()) + next_protocol = Protocol.query.filter_by( + protocoltype_id=protocol.protocoltype.id).filter_by( + done=False).filter( + Protocol.date > datetime.now()).order_by(Protocol.date).first() + to_addr = ( + protocol.protocoltype.private_mail + if show_private + else protocol.protocoltype.public_mail) + subject = "{}{}-Protokoll vom {}".format( + "Internes " if show_private else "", + protocol.protocoltype.short_name, date_filter(protocol.date)) + mail_content = render_template( + "protocol-mail.txt", protocol=protocol, show_private=show_private, + next_protocol=next_protocol) + appendix = [ + (document.name, document.as_file_like()) for document in protocol.documents if show_private or not document.is_private ] send_mail(protocol, to_addr, subject, mail_content, appendix) + @celery.task def send_todomails_async(protocol_id): with app.app_context(): @@ -764,7 +872,8 @@ def send_todomails_async(protocol_id): for user in users } subject = "Du hast noch was zu tun!" - todomail_providers = getattr(config, "ADDITIONAL_TODOMAIL_PROVIDERS", None) + todomail_providers = getattr( + config, "ADDITIONAL_TODOMAIL_PROVIDERS", None) additional_todomails = {} if todomail_providers: for provider in todomail_providers: @@ -779,33 +888,42 @@ def send_todomails_async(protocol_id): if user in additional_todomails: todomail = additional_todomails[user] if todomail is None: - error = protocol.create_error("Sending Todomail", "Sending Todomail failed.", "User {} has no Todo-Mail-Assignment.".format(user)) - db.session.add(error) - db.session.commit() + _make_error( + protocol, "Sending Todomail", "Sending Todomail failed.", + "User {} has no Todo-Mail-Assignment.".format(user)) continue to_addr = todomail.get_formatted_mail() - mail_content = render_template("todo-mail.txt", protocol=protocol, todomail=todomail, todos=grouped_todos[user]) - send_mail(protocol, to_addr, subject, mail_content, + mail_content = render_template( + "todo-mail.txt", protocol=protocol, todomail=todomail, + todos=grouped_todos[user]) + send_mail( + protocol, to_addr, subject, mail_content, reply_to=protocol.protocoltype.private_mail) -def send_mail(protocol, to_addr, subject, content, appendix=None, reply_to=None): + +def send_mail(protocol, to_addr, subject, content, appendix=None, + reply_to=None): if to_addr is not None and len(to_addr.strip()) > 0: - send_mail_async.delay(protocol.id, to_addr, subject, content, appendix, reply_to) + send_mail_async.delay( + protocol.id, to_addr, subject, content, appendix, reply_to) + @celery.task -def send_mail_async(protocol_id, to_addr, subject, content, appendix, reply_to): +def send_mail_async(protocol_id, to_addr, subject, content, appendix, + reply_to): with app.app_context(): protocol = Protocol.query.filter_by(id=protocol_id).first() try: mail_manager.send(to_addr, subject, content, appendix, reply_to) except Exception as exc: - error = protocol.create_error("Sending Mail", "Sending mail failed", str(exc)) - db.session.add(error) - db.session.commit() + return _make_error( + protocol, "Sending Mail", "Sending mail failed", str(exc)) + def push_tops_to_calendar(protocol): push_tops_to_calendar_async.delay(protocol.id) + @celery.task def push_tops_to_calendar_async(protocol_id): if not config.CALENDAR_ACTIVE: @@ -817,21 +935,22 @@ def push_tops_to_calendar_async(protocol_id): description = render_template("calendar-tops.txt", protocol=protocol) try: client = CalendarClient(protocol.protocoltype.calendar) - client.set_event_at(begin=protocol.get_datetime(), + client.set_event_at( + begin=protocol.get_datetime(), name=protocol.protocoltype.short_name, description=description) except CalendarException as exc: - error = protocol.create_error("Calendar", + return _make_error( + protocol, "Calendar", "Pushing TOPs to Calendar failed", str(exc)) - db.session.add(error) - db.session.commit() + def set_etherpad_content(protocol): set_etherpad_content_async.delay(protocol.id) + @celery.task def set_etherpad_content_async(protocol_id): with app.app_context(): protocol = Protocol.query.filter_by(id=protocol_id).first() identifier = protocol.get_identifier() return set_etherpad_text(identifier, protocol.get_template()) - diff --git a/templates/decision.tex b/templates/decision.tex index 9a79b296a0bf5cecb98083733009d95db9362e23..5399e3da56fcec5c0fafe18710540dbffee45b7c 100644 --- a/templates/decision.tex +++ b/templates/decision.tex @@ -5,7 +5,9 @@ \usepackage{pdfpages} \usepackage{eurosym} %\usepackage[utf8]{inputenc} -\usepackage[pdfborder={0 0 0}]{hyperref} +\usepackage[hyphens]{url} +\usepackage[pdfborder={0 0 0},breaklinks=true]{hyperref} +\def\UrlBreaks{\do\/\do-\do\&\do.\do,\do;\do\_\do?\do\#} %\usepackage{ngerman} % \usepackage[left]{lineno} %\usepackage{footnote} diff --git a/templates/document-edit.html b/templates/document-edit.html index 7b1689e7170c53d394d95f91b16606b46ae75d13..d3d30fa7571514901199a50f121cd1c8402cf1c6 100644 --- a/templates/document-edit.html +++ b/templates/document-edit.html @@ -4,6 +4,6 @@ {% block content %} <div class="container"> - {{render_form(form, action_url=url_for("edit_document", document_id=document.id, next=request.args.get("next") or url_for("show_protocol", protocol_id=document.protocol.id)), action_text="Ändern")}} + {{render_form(form, action_url=url_for("edit_document", document_id=document.id), action_text="Ändern")}} </div> {% endblock %} diff --git a/templates/layout.html b/templates/layout.html index 8cf4490de01d5598060bf10153b595631f986402..2496975f2b295f028bf7c3bc4e60ea3affb9642d 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -50,7 +50,7 @@ </ul> <ul class="nav navbar-nav navbar-right"> {% if check_login() %} - <li><a href="{{url_for("logout")}}">Logout</a></li> + <li><a href="{{url_for("logout", csrf_token=get_csrf_token())}}">Logout</a></li> {% else %} <li><a href="{{url_for("login")}}">Login</a></li> {% endif %} diff --git a/templates/localtop-edit.html b/templates/localtop-edit.html index c320d727b1b248718899be1aa2eadbe66fa517e0..df048b35b5b001172d7e3bc1854cf5b7bc74d25c 100644 --- a/templates/localtop-edit.html +++ b/templates/localtop-edit.html @@ -5,6 +5,6 @@ {% block content %} <div class="container"> <h3>{{localtop.defaulttop.name}}</h3> - {{render_form(form, action_url=url_for("edit_localtop", localtop_id=localtop.id, next=request.args.get("next") or url_for("show_protocol", protocol_id=localtop.protocol.id)), action_text="Ändern", textarea_rows=5)}} + {{render_form(form, action_url=url_for("edit_localtop", localtop_id=localtop.id), action_text="Ändern", textarea_rows=5)}} </div> {% endblock %} diff --git a/templates/macros.html b/templates/macros.html index 0408a6c06eaba8a19b65b4f06d895fd30d5abc94..e6d0edd7c26a03c329daaccd5abd0e6f33815956 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -175,7 +175,7 @@ to not render a label for the CRSFTokenField --> {% set verb = "likes" %} {% endif %} {% if add_link %} - <a href="{{url_for("new_like", next=request.url, **kwargs)}}"> + <a href="{{url_for("new_like", csrf_token=get_csrf_token(), **kwargs)}}"> {% endif %} <div class="likes-div"> <p>{{likes|length}} <span class="like-sign">👍</span></p> diff --git a/templates/protocol-show.html b/templates/protocol-show.html index d4d99ade2f6afd87fa1affbd6d1b5fa7bc9c80fb..ba615a8260f9bb0158c176c04bbffb21a03a09c6 100644 --- a/templates/protocol-show.html +++ b/templates/protocol-show.html @@ -16,7 +16,7 @@ <div class="btn-group"> {% if has_modify_right %} {% if config.ETHERPAD_ACTIVE and not protocol.public %} - <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> + <a class="btn {% if protocol.source is none %}btn-primary{% else %}btn-default{% endif %}" href="{{url_for("etherpull_protocol", protocol_id=protocol.id, csrf_token=get_csrf_token())}}">Aus Etherpad</a> {% endif %} {% if protocol.source is not none %} <a class="btn btn-primary" href="{{url_for("get_protocol_source", protocol_id=protocol.id)}}">Quelltext</a> @@ -26,23 +26,23 @@ {% endif %} {% if not protocol.public %} {% if config.ETHERPAD_ACTIVE %} - <a class="btn btn-primary" href="{{url_for("etherpush_protocol", protocol_id=protocol.id)}}"{% if large_time_diff %} onclick="return confirm('Bist du dir sicher, dass du das Template bereits in das Etherpad kopieren willst? Die Sitzung ist erst {% if time_diff.days != 1 %}in {{time_diff.days}} Tagen{% else %}morgen{% endif %}.');"{% endif %} target="_blank">Etherpad</a> + <a class="btn btn-primary" href="{{url_for("etherpush_protocol", protocol_id=protocol.id, csrf_token=get_csrf_token())}}"{% if large_time_diff %} onclick="return confirm('Bist du dir sicher, dass du das Template bereits in das Etherpad kopieren willst? Die Sitzung ist erst {% if time_diff.days != 1 %}in {{time_diff.days}} Tagen{% else %}morgen{% endif %}.');"{% endif %} target="_blank">Etherpad</a> {% endif %} {% endif %} {% if not protocol.is_done() %} <a class="btn btn-default" href="{{url_for("get_protocol_template", protocol_id=protocol.id)}}">Vorlage</a> {% if config.MAIL_ACTIVE %} - <a class="btn btn-default" href="{{url_for("send_protocol_reminder", protocol_id=protocol.id)}}" onclick="return confirm('Bist du dir sicher, dass du manuell eine Einladung verschicken willst? Dies wird auch automatisch geschehen.');">Einladung versenden</a> + <a class="btn btn-default" href="{{url_for("send_protocol_reminder", protocol_id=protocol.id, csrf_token=get_csrf_token())}}" onclick="return confirm('Bist du dir sicher, dass du manuell eine Einladung verschicken willst? Dies wird auch automatisch geschehen.');">Einladung versenden</a> {% endif %} {% else %} {% if config.MAIL_ACTIVE %} - <a class="btn btn-default" href="{{url_for("send_protocol_private", protocol_id=protocol.id)}}">Intern versenden</a> + <a class="btn btn-default" href="{{url_for("send_protocol_private", protocol_id=protocol.id, csrf_token=get_csrf_token())}}">Intern versenden</a> {% if protocol.public %} - <a class="btn btn-default" href="{{url_for("send_protocol_public", protocol_id=protocol.id)}}">Öffentlich versenden</a> + <a class="btn btn-default" href="{{url_for("send_protocol_public", protocol_id=protocol.id, csrf_token=get_csrf_token())}}">Öffentlich versenden</a> {% endif %} {% endif %} {% if not protocol.public %} - <a class="btn btn-default" href="{{url_for("publish_protocol", protocol_id=protocol.id)}}">Veröffentlichen</a> + <a class="btn btn-default" href="{{url_for("publish_protocol", protocol_id=protocol.id, csrf_token=get_csrf_token())}}">Veröffentlichen</a> {% endif %} {% endif %} <a class="btn btn-default" href="{{url_for("show_type", protocoltype_id=protocol.protocoltype.id)}}">Typ</a> @@ -50,7 +50,7 @@ <a class="btn btn-success" href="{{url_for("download_document", document_id=protocol.get_compiled_document().id)}}">Download</a> {% endif %} {% if has_admin_right %} - <a class="btn btn-default" href="{{url_for("recompile_protocol", protocol_id=protocol.id)}}">Neu kompilieren</a> + <a class="btn btn-default" href="{{url_for("recompile_protocol", protocol_id=protocol.id, csrf_token=get_csrf_token())}}">Neu kompilieren</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_short_identifier()}} löschen möchtest?');">Löschen</a> {% endif %} {% endif %} @@ -104,7 +104,7 @@ <li> {{decision.content}} {% if config.PRINTING_ACTIVE and has_private_view_right and decision.document is not none %} - <a href="{{url_for("print_decision", decisiondocument_id=decision.document.id)}}">Drucken</a> + <a href="{{url_for("print_decision", decisiondocument_id=decision.document.id, csrf_token=get_csrf_token())}}">Drucken</a> {% endif %} {{render_likes(decision.likes, decision_id=decision.id)}}</h2> </li> diff --git a/templates/protocol-tops-include.html b/templates/protocol-tops-include.html index 6be2c79f2fd726683af93a9e5132e2828f921ec4..1d9fbc21aaa11dc08148f41c011f1189fbd0e7d0 100644 --- a/templates/protocol-tops-include.html +++ b/templates/protocol-tops-include.html @@ -27,9 +27,9 @@ {% endif %} {% 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> - <a href="{{url_for('delete_top', top_id=top.id)}}" onclick="return confirm('Bist du dir sicher, dass du den TOP {{top.name}} löschen möchtest?');">Löschen</a> + <a href="{{url_for('move_top', top_id=top.id, diff=1, csrf_token=get_csrf_token())}}">Runter</a> + <a href="{{url_for('move_top', top_id=top.id, diff=-1, csrf_token=get_csrf_token())}}">Hoch</a> + <a href="{{url_for('delete_top', top_id=top.id, csrf_token=get_csrf_token())}}" onclick="return confirm('Bist du dir sicher, dass du den TOP {{top.name}} löschen möchtest?');">Löschen</a> {% endif %} {% if has_private_view_right and top.description is not none and top.description|length > 0 %} <span class="glyphicon glyphicon-info-sign"></span> diff --git a/templates/top-edit.html b/templates/top-edit.html index 07d56be8f257d106a9e9da490a3e2a266a5286a3..adabf99a3e46b5ceb8d63c988e97dd9b7a8f6e16 100644 --- a/templates/top-edit.html +++ b/templates/top-edit.html @@ -4,6 +4,6 @@ {% block content %} <div class="container"> - {{render_form(form, action_url=url_for("edit_top", top_id=top.id, next=request.args.get("next") or url_for("show_protocol", protocol_id=top.protocol.id)), action_text="Ändern", textarea_rows=5)}} + {{render_form(form, action_url=url_for("edit_top", top_id=top.id), action_text="Ändern", textarea_rows=5)}} </div> {% endblock %} diff --git a/utils.py b/utils.py index 95baf755c9a03172d0b8a9771a8f6860b0c0cadc..5e80b9cbebc40ae8cacf6cff9201fa12c1577e17 100644 --- a/utils.py +++ b/utils.py @@ -1,28 +1,34 @@ -from flask import render_template, request +from flask import request, session import random import string -import regex import math import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.application import MIMEApplication -from datetime import datetime, date, timedelta +from datetime import datetime import requests from io import BytesIO import ipaddress from socket import getfqdn from uuid import uuid4 +import subprocess +import os +import hashlib import config + def random_string(length): - return "".join((random.choice(string.ascii_letters) for i in range(length))) + return "".join((random.choice(string.ascii_letters) + for i in range(length))) + def is_past(some_date): return (datetime.now() - some_date).total_seconds() > 0 + def encode_kwargs(kwargs): encoded_kwargs = {} for key in kwargs: @@ -33,6 +39,7 @@ def encode_kwargs(kwargs): encoded_kwargs[key] = (type(value), value, False) return encoded_kwargs + def decode_kwargs(encoded_kwargs): kwargs = {} for name in encoded_kwargs: @@ -42,28 +49,7 @@ def decode_kwargs(encoded_kwargs): else: kwargs[name] = id return kwargs - -class UrlManager: - def __init__(self, config): - self.pattern = regex.compile(r"(?:(?<proto>https?):\/\/)?(?<hostname>[[:alnum:]_.]+(?:\:[[:digit:]]+)?)?(?<path>(?:\/[[:alnum:]_#]*)+)?(?:\?(?<params>.*))?") - self.base = "{}://{}{}{}" - self.proto = getattr(config, "URL_PROTO", "https") - self.root = getattr(config, "URL_ROOT", "example.com") - self.path = getattr(config, "URL_PATH", "/") - self.params = getattr(config, "URL_PARAMS", "") - - def complete(self, url): - match = self.pattern.match(url) - if match is None: - return None - proto = match.group("proto") or self.proto - root = match.group("hostname") or self.root - path = match.group("path") or self.path - params = match.group("params") or self.params - return self.base.format(proto, root, path, "?" + params if len(params) > 0 else "") - -#url_manager = UrlManager(config) class MailManager: def __init__(self, config): @@ -75,12 +61,17 @@ class MailManager: self.use_tls = getattr(config, "MAIL_USE_TLS", True) self.use_starttls = getattr(config, "MAIL_USE_STARTTLS", False) + def _get_smtp(self): + if self.use_tls: + return smtplib.SMTP_SSL + return smtplib.SMTP + def send(self, to_addr, subject, content, appendix=None, reply_to=None): if (not self.active - or not self.hostname - or not self.from_addr): + or not self.hostname + or not self.from_addr): return - msg = MIMEMultipart("mixed") # todo: test if clients accept attachment-free mails set to multipart/mixed + msg = MIMEMultipart("mixed") msg["From"] = self.from_addr msg["To"] = to_addr msg["Subject"] = subject @@ -91,9 +82,10 @@ class MailManager: if appendix is not None: for name, file_like in appendix: part = MIMEApplication(file_like.read(), "octet-stream") - part["Content-Disposition"] = 'attachment; filename="{}"'.format(name) + part["Content-Disposition"] = ( + 'attachment; filename="{}"'.format(name)) msg.attach(part) - server = (smtplib.SMTP_SSL if self.use_tls else smtplib.SMTP)(self.hostname) + server = self._get_smtp()(self.hostname) if self.use_starttls: server.starttls() if self.username not in [None, ""] and self.password not in [None, ""]: @@ -101,36 +93,48 @@ class MailManager: server.sendmail(self.from_addr, to_addr.split(","), msg.as_string()) server.quit() + mail_manager = MailManager(config) + def get_first_unused_int(numbers): positive_numbers = [number for number in numbers if number >= 0] if len(positive_numbers) == 0: return 0 highest = max(positive_numbers) - for given, linear in zip(positive_numbers, range(highest+1)): + for given, linear in zip(positive_numbers, range(highest + 1)): if linear < given: return linear return highest + 1 + def normalize_pad(pad): return pad.replace(" ", "_") + + def get_etherpad_url(pad): return "{}/p/{}".format(config.ETHERPAD_URL, normalize_pad(pad)) + + def get_etherpad_export_url(pad): return "{}/p/{}/export/txt".format(config.ETHERPAD_URL, normalize_pad(pad)) + + def get_etherpad_import_url(pad): return "{}/p/{}/import".format(config.ETHERPAD_URL, normalize_pad(pad)) + def get_etherpad_text(pad): req = requests.get(get_etherpad_export_url(pad)) return req.text + def set_etherpad_text(pad, text, only_if_default=True): print(pad) if only_if_default: current_text = get_etherpad_text(pad) - if current_text != config.EMPTY_ETHERPAD and len(current_text.strip()) > 0: + if (current_text != config.EMPTY_ETHERPAD + and len(current_text.strip()) > 0): return False file_like = BytesIO(text.encode("utf-8")) files = {"file": file_like} @@ -138,7 +142,8 @@ def set_etherpad_text(pad, text, only_if_default=True): print(url) req = requests.post(url, files=files) return req.status_code == 200 - + + def split_terms(text, quote_chars="\"'", separators=" \t\n"): terms = [] in_quote = False @@ -168,26 +173,30 @@ def split_terms(text, quote_chars="\"'", separators=" \t\n"): terms.append(current_term) return terms + def optional_int_arg(name): try: return int(request.args.get(name)) except (ValueError, TypeError): return None + def add_line_numbers(text): raw_lines = text.splitlines() linenumber_length = math.ceil(math.log10(len(raw_lines)) + 1) lines = [] for linenumber, line in enumerate(raw_lines): lines.append("{} {}".format( - str(linenumber+1).rjust(linenumber_length), + str(linenumber + 1).rjust(linenumber_length), line )) return "\n".join(lines) + def check_ip_in_networks(networks_string): address = ipaddress.ip_address(request.remote_addr) - if address == ipaddress.ip_address("127.0.0.1") and "X-Real-Ip" in request.headers: + if (address == ipaddress.ip_address("127.0.0.1") + and "X-Real-Ip" in request.headers): address = ipaddress.ip_address(request.headers["X-Real-Ip"]) try: for network_string in networks_string.split(","): @@ -198,6 +207,7 @@ def check_ip_in_networks(networks_string): except ValueError: return False + def fancy_join(values, sep1=" und ", sep2=", "): values = list(values) if len(values) <= 1: @@ -206,20 +216,53 @@ def fancy_join(values, sep1=" und ", sep2=", "): start = values[:-1] return "{}{}{}".format(sep2.join(start), sep1, last) + def footnote_hash(text, length=5): return str(sum(ord(c) * i for i, c in enumerate(text)) % 10**length) + def parse_datetime_from_string(text): text = text.strip() for format in ("%d.%m.%Y", "%d.%m.%y", "%Y-%m-%d", - "%d. %B %Y", "%d. %b %Y", "%d. %B %y", "%d. %b %y"): + "%d. %B %Y", "%d. %b %Y", "%d. %B %y", "%d. %b %y"): try: return datetime.strptime(text, format) except ValueError: pass for format in ("%d.%m.", "%d. %m.", "%d.%m", "%d.%m"): try: - return datetime.strptime(text, format).replace(year=datetime.now().year) - except ValueError as exc: - print(exc) + return datetime.strptime(text, format).replace( + year=datetime.now().year) + except ValueError: + pass raise ValueError("Date '{}' does not match any known format!".format(text)) + + +def get_git_revision(): + try: + gitlab_url = "https://git.fsmpi.rwth-aachen.de/protokollsystem/proto3" + commit_hash = subprocess.check_output( + ["git", "log", "-g", "-1", "--pretty=%H"]).decode("UTF-8").strip() + timestamp = int(subprocess.check_output( + ["git", "log", "-g", "-1", "--pretty=%at"]).strip()) + commit_date = datetime.fromtimestamp(timestamp) + return {"url": gitlab_url, "hash": commit_hash, "date": commit_date} + except subprocess.SubprocessError: + pass + + +def get_max_page_length_exp(objects): + length = len(objects) + if length > 0: + return math.ceil(math.log10(length)) + return 1 + + +def get_internal_filename(protocol, document, filename): + return "{}-{}-{}".format(protocol.id, document.id, filename) + + +def get_csrf_token(): + if "_csrf" not in session: + session["_csrf"] = hashlib.sha1(os.urandom(64)).hexdigest() + return session["_csrf"] diff --git a/validators.py b/validators.py index 36847e467f957d0bf43ddeae26c33df3dd061fe8..4e262bf7a04e84188083cb25f4d1694f56dd82bb 100644 --- a/validators.py +++ b/validators.py @@ -1,7 +1,7 @@ from models.database import TodoState from wtforms import ValidationError from wtforms.validators import InputRequired -from shared import db + class CheckTodoDateByState: def __init__(self): @@ -16,4 +16,3 @@ class CheckTodoDateByState: date_check(form, form.date) except ValueError: raise ValidationError("Invalid state.") - diff --git a/views/forms.py b/views/forms.py index 7ec3a3f63c0b1b677ce025370710b5d36ee9cc4e..843cae1b4d949b60fbcab5b41d704f8fe325722a 100644 --- a/views/forms.py +++ b/views/forms.py @@ -1,5 +1,7 @@ from flask_wtf import FlaskForm -from wtforms import StringField, PasswordField, BooleanField, HiddenField, IntegerField, SelectField, FileField, DateTimeField, TextAreaField, Field, widgets, FormField +from wtforms import ( + StringField, PasswordField, BooleanField, IntegerField, SelectField, + FileField, DateTimeField, TextAreaField, Field, FormField, widgets) from wtforms.fields.html5 import DateField from wtforms.validators import InputRequired, Optional @@ -11,6 +13,7 @@ from shared import current_user import config + def get_protocoltype_choices(protocoltypes, add_all=True): choices = [ (protocoltype.id, protocoltype.short_name) @@ -21,6 +24,7 @@ def get_protocoltype_choices(protocoltypes, add_all=True): choices.insert(0, (-1, "Alle Typen")) return choices + def get_category_choices(categories, add_all=True): choices = [ (category.id, category.name) @@ -31,12 +35,14 @@ def get_category_choices(categories, add_all=True): choices.insert(0, (-1, "Alle Kategorien")) return choices + def get_todostate_choices(): return [ (state, state.get_name()) for state in TodoState ] + def get_calendar_choices(protocoltype=None): from calendarpush import Client as CalendarClient calendars = CalendarClient().get_calendars() @@ -46,19 +52,21 @@ def get_calendar_choices(protocoltype=None): choices = list(zip(calendars, calendars)) else: if (protocoltype is not None - and protocoltype.calendar is not None - and protocoltype.calendar != ""): + and protocoltype.calendar is not None + and protocoltype.calendar != ""): choices.append((protocoltype.calendar, protocoltype.calendar)) choices.insert(0, ("", "Kein Kalender")) return choices + def get_printer_choices(): choices = [] if config.PRINTING_PRINTERS is not None: choices = list(zip(config.PRINTING_PRINTERS, config.PRINTING_PRINTERS)) choices.insert(0, ("", "Nicht drucken")) return choices - + + def get_latex_template_choices(): choices = [] _latex_templates = getattr(config, "LATEX_TEMPLATES", None) @@ -71,6 +79,7 @@ def get_latex_template_choices(): choices.insert(0, ("", "Standardvorlage")) return choices + def get_group_choices(): user = current_user() groups = sorted(user.groups) @@ -78,12 +87,14 @@ def get_group_choices(): choices.insert(0, ("", "Keine Gruppe")) return choices + def coerce_todostate(key): if isinstance(key, str): class_part, key_part = key.split(".") key = TodoState[key_part] return key + class IPNetworkField(Field): widget = widgets.TextInput() @@ -109,30 +120,53 @@ class IPNetworkField(Field): except ValueError as exc: print(exc) self.data = None - raise ValueError(self.gettext("Not a valid IP Network: {}".format(str(exc)))) + raise ValueError( + self.gettext("Not a valid IP Network: {}".format( + str(exc)))) self.data = ",".join(map(str, result_parts)) + class FocusedStringField(StringField): def __call__(self, **kwargs): kwargs['autofocus'] = True return super().__call__(**kwargs) + class LoginForm(FlaskForm): - username = FocusedStringField("Benutzer", validators=[InputRequired("Bitte gib deinen Benutzernamen ein.")]) - password = PasswordField("Passwort", validators=[InputRequired("Bitte gib dein Passwort ein.")]) + username = FocusedStringField( + "Benutzer", + validators=[InputRequired("Bitte gib deinen Benutzernamen ein.")]) + password = PasswordField( + "Passwort", + validators=[InputRequired("Bitte gib dein Passwort ein.")]) permanent = BooleanField("Eingeloggt bleiben?") + class ProtocolTypeForm(FlaskForm): - name = StringField("Name", validators=[InputRequired("Du musst einen Namen angeben.")]) - short_name = StringField("Abkürzung", validators=[InputRequired("Du musst eine Abkürzung angebene.")]) - organization = StringField("Organisation", validators=[InputRequired("Du musst eine zugehörige Organisation angeben.")]) - usual_time = DateTimeField("Üblicher Beginn", validators=[InputRequired("Bitte gib die Zeit an, zu der die Sitzung beginnt.")], format="%H:%M") + name = StringField( + "Name", + validators=[InputRequired("Du musst einen Namen angeben.")]) + short_name = StringField( + "Abkürzung", + validators=[InputRequired("Du musst eine Abkürzung angebene.")]) + organization = StringField( + "Organisation", + validators=[ + InputRequired("Du musst eine zugehörige Organisation angeben.") + ]) + usual_time = DateTimeField( + "Üblicher Beginn", + validators=[ + InputRequired("Bitte gib die Zeit an, zu der die Sitzung beginnt.") + ], + format="%H:%M") is_public = BooleanField("Öffentlich sichtbar") publish_group = SelectField("Verwaltungsgruppe", choices=[]) modify_group = SelectField("Bearbeitungsgruppe", choices=[]) private_group = SelectField("Interne Gruppe", choices=[]) public_group = SelectField("Öffentliche Gruppe", choices=[]) - non_reproducible_pad_links = BooleanField("nicht nachvollziehbare Etherpad-Links") + non_reproducible_pad_links = BooleanField( + "nicht nachvollziehbare Etherpad-Links") private_mail = StringField("Interner Verteiler") public_mail = StringField("Öffentlicher Verteiler") wiki_category = StringField("Wiki-Kategorie") @@ -156,44 +190,65 @@ class ProtocolTypeForm(FlaskForm): self.private_group.choices = group_choices self.public_group.choices = group_choices + class DefaultTopForm(FlaskForm): - name = StringField("Name", validators=[InputRequired("Du musst einen Namen angeben.")]) - number = IntegerField("Nummer", validators=[InputRequired("Du musst eine Nummer angeben.")]) + name = StringField( + "Name", + validators=[InputRequired("Du musst einen Namen angeben.")]) + number = IntegerField( + "Priorität", + validators=[InputRequired("Du musst eine Priorität angeben.")]) description = TextAreaField("Standardinhalt") + class MeetingReminderForm(FlaskForm): - days_before = IntegerField("Tage vor Sitzung", validators=[InputRequired("Du musst eine Dauer angeben.")]) + days_before = IntegerField( + "Tage vor Sitzung", + validators=[InputRequired("Du musst eine Dauer angeben.")]) send_public = BooleanField("Öffentlich einladen") send_private = BooleanField("Intern einladen") additional_text = TextAreaField("Zusätzlicher Mailinhalt") + class NewProtocolForm(FlaskForm): protocoltype_id = SelectField("Typ", choices=[], coerce=int) - date = DateField("Datum", validators=[InputRequired("Du musst ein Datum angeben.")]) - start_time = DateTimeField("Uhrzeit (HH:MM, optional)", validators=[Optional()], format="%H:%M") + date = DateField( + "Datum", + validators=[InputRequired("Du musst ein Datum angeben.")]) + start_time = DateTimeField( + "Uhrzeit (HH:MM, optional)", + validators=[Optional()], + format="%H:%M") def __init__(self, protocoltypes, **kwargs): super().__init__(**kwargs) - self.protocoltype_id.choices = get_protocoltype_choices(protocoltypes, add_all=False) + self.protocoltype_id.choices = get_protocoltype_choices( + protocoltypes, add_all=False) + class DocumentEditForm(FlaskForm): name = StringField("Dateiname") is_private = BooleanField("Intern") + class DocumentUploadForm(FlaskForm): document = FileField("Datei") is_private = BooleanField("Intern") + class KnownProtocolSourceUploadForm(FlaskForm): source = FileField("Quellcode") + class NewProtocolSourceUploadForm(FlaskForm): source = FileField("Quellcode") protocoltype_id = SelectField("Typ", choices=[], coerce=int) def __init__(self, protocoltypes, **kwargs): super().__init__(**kwargs) - self.protocoltype_id.choices = get_protocoltype_choices(protocoltypes, add_all=False) + self.protocoltype_id.choices = get_protocoltype_choices( + protocoltypes, add_all=False) + class NewProtocolFileUploadForm(FlaskForm): file = FileField("Datei") @@ -202,31 +257,46 @@ class NewProtocolFileUploadForm(FlaskForm): def __init__(self, protocoltypes, **kwargs): super().__init__(**kwargs) - self.protocoltype_id.choices = get_protocoltype_choices(protocoltypes, add_all=False) + self.protocoltype_id.choices = get_protocoltype_choices( + protocoltypes, add_all=False) + def generate_protocol_form(protocol): class ProtocolMetasForm(FlaskForm): pass for meta in protocol.metas: setattr(ProtocolMetasForm, meta.name, StringField(meta.name)) + class ProtocolForm(FlaskForm): - date = DateField("Datum", validators=[InputRequired("Bitte gib das Datum des Protkolls an.")]) - start_time = DateTimeField("Beginn (%H:%M)", format="%H:%M", validators=[Optional()]) - end_time = DateTimeField("Ende (%H:%M)", format="%H:%M", validators=[Optional()]) + date = DateField( + "Datum", + validators=[ + InputRequired("Bitte gib das Datum des Protkolls an.") + ]) + start_time = DateTimeField( + "Beginn (%H:%M)", format="%H:%M", validators=[Optional()]) + end_time = DateTimeField( + "Ende (%H:%M)", format="%H:%M", validators=[Optional()]) metas = FormField(ProtocolMetasForm) done = BooleanField("Fertig") public = BooleanField("Veröffentlicht") return ProtocolForm - + class TopForm(FlaskForm): - name = StringField("TOP", validators=[InputRequired("Du musst den Namen des TOPs angeben.")]) - number = IntegerField("Sortierung", validators=[InputRequired("Du musst eine Sortierung in der Reihenfolge angebene.")]) + name = StringField( + "TOP", + validators=[InputRequired("Du musst den Namen des TOPs angeben.")]) + number = IntegerField( + "Priorität", + validators=[InputRequired("Du musst eine Priorität angeben.")]) description = TextAreaField("Beschreibung") + class LocalTopForm(FlaskForm): description = TextAreaField("Beschreibung") + class SearchForm(FlaskForm): search = StringField("Suchbegriff") protocoltype_id = SelectField("Typ", choices=[], coerce=int) @@ -235,6 +305,7 @@ class SearchForm(FlaskForm): super().__init__(**kwargs) self.protocoltype_id.choices = get_protocoltype_choices(protocoltypes) + class DecisionSearchForm(SearchForm): decisioncategory_id = SelectField("Kategorie", choices=[], coerce=int) @@ -242,52 +313,104 @@ class DecisionSearchForm(SearchForm): super().__init__(protocoltypes=protocoltypes, **kwargs) self.decisioncategory_id.choices = get_category_choices(categories) + class ProtocolSearchForm(SearchForm): - state_open = SelectField("Offen", choices=[(-1, "Alle"), (0, "Geplant"), (1, "Fertig")], coerce=int) + state_open = SelectField( + "Offen", + choices=[(-1, "Alle"), (0, "Geplant"), (1, "Fertig")], + coerce=int) + class TodoSearchForm(SearchForm): - state_open = SelectField("Offen", choices=[(-1, "Alle"), (0, "Offen"), (1, "Erledigt")], coerce=int) + state_open = SelectField( + "Offen", + choices=[(-1, "Alle"), (0, "Offen"), (1, "Erledigt")], + coerce=int) + 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.")]) - state = SelectField("Status", choices=[], coerce=coerce_todostate, validators=[CheckTodoDateByState()]) - date = DateField("Datum)", validators=[Optional()]) - + 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.") + ]) + state = SelectField( + "Status", + choices=[], + coerce=coerce_todostate, + validators=[CheckTodoDateByState()]) + date = DateField("Datum", validators=[Optional()]) + def __init__(self, protocoltypes, **kwargs): super().__init__(**kwargs) - self.protocoltype_id.choices = get_protocoltype_choices(protocoltypes, add_all=False) + self.protocoltype_id.choices = get_protocoltype_choices( + protocoltypes, add_all=False) self.state.choices = get_todostate_choices() + class TodoForm(FlaskForm): who = StringField("Person") - description = StringField("Aufgabe", validators=[InputRequired("Bitte gib an, was erledigt werden soll.")]) - state = SelectField("Status", choices=[], coerce=coerce_todostate, validators=[CheckTodoDateByState()]) + description = StringField( + "Aufgabe", + validators=[InputRequired("Bitte gib an, was erledigt werden soll.")]) + state = SelectField( + "Status", + choices=[], + coerce=coerce_todostate, + validators=[CheckTodoDateByState()]) date = DateField("Datum", validators=[Optional()]) def __init__(self, **kwargs): super().__init__(**kwargs) self.state.choices = get_todostate_choices() + class TodoMailForm(FlaskForm): - name = StringField("Name", validators=[InputRequired("Du musst den Namen angeben, der zugeordnet werden soll.")]) - mail = StringField("Mail", validators=[InputRequired("Du musst die Mailadresse angeben, die zugeordnet werden soll.")]) + name = StringField( + "Name", + validators=[ + InputRequired("Du musst den Namen angeben, der zugeordnet werden " + "soll.")]) + mail = StringField( + "Mail", + validators=[ + InputRequired("Du musst die Mailadresse angeben, die zugeordnet " + "werden soll.")]) + class MetaForm(FlaskForm): - name = StringField("Name", validators=[InputRequired("Bitte gib den Namen der Metadaten an.")]) + name = StringField( + "Name", + validators=[InputRequired("Bitte gib den Namen der Metadaten an.")]) value = StringField("Wert") internal = BooleanField("Intern") + class DefaultMetaForm(FlaskForm): - key = StringField("Key", validators=[InputRequired("Bitte gib den Protokoll-Syntax-Schlüssel der Metadaten an.")]) - name = StringField("Name", validators=[InputRequired("Bitte gib den Namen der Metadaten an.")]) + key = StringField( + "Key", + validators=[ + InputRequired("Bitte gib den Protokoll-Syntax-Schlüssel der " + "Metadaten an.") + ]) + name = StringField( + "Name", + validators=[InputRequired("Bitte gib den Namen der Metadaten an.")]) value = StringField("Standardwert") internal = BooleanField("Intern") prior = BooleanField("Planungsrelevant") + class DecisionCategoryForm(FlaskForm): - name = StringField("Name", validators=[InputRequired("Bitte gib den Namen der Kategorie an.")]) + name = StringField( + "Name", + validators=[InputRequired("Bitte gib den Namen der Kategorie an.")]) + class MergeTodosForm(FlaskForm): todo1 = IntegerField("todo 1", validators=[InputRequired()]) diff --git a/views/tables.py b/views/tables.py index 8b59ea0fc5de52d255fb5e6d3d5bec63da03771e..e3f60f581c0c7a6e44ef31506be60da537e425bc 100644 --- a/views/tables.py +++ b/views/tables.py @@ -1,10 +1,10 @@ -# coding: utf-8 -from flask import Markup, url_for, request -from models.database import Protocol, ProtocolType, DefaultTOP, TOP, Todo, Decision, Meta, DefaultMeta -from shared import date_filter, datetime_filter, date_filter_short, current_user, check_login +from flask import Markup, url_for +from shared import date_filter, datetime_filter, time_filter, current_user +from utils import get_csrf_token import config + class Table: def __init__(self, title, values, newlink=None, newtext=None): self.title = title @@ -13,31 +13,53 @@ class Table: self.newtext = newtext or "Neu" def rows(self): - return [row for row in [self.row(value) for value in self.values] if row is not None] + return [ + row for row in [self.row(value) for value in self.values] + if row is not None] def classes(self): return [None for header in self.headers()] @staticmethod - def link(target, text, confirm=None): - confirmation = "" + def link(target, text, confirm=None, css_class=None): + attributes = [ + "href=\"{}\"".format(target) + ] if confirm: - confirmation = " onclick=\"return confirm('{}');\"".format(confirm) - return Markup("<a href=\"{}\"{}>{}</a>".format(target, confirmation, text)) + attributes.append( + "onclick=\"return confirm('{}');\"".format(confirm)) + if css_class: + attributes.append("class=\"{}\"".format(css_class)) + return Markup("<a {}>{}</a>".format(" ".join(attributes), text)) @staticmethod - def button(target, icon, style, confirm=None): - confirmation = "" - if confirm: - confirmation = " onclick=\"return confirm('{}');\"".format(confirm) + def glyphicon(name, text=None): + if text is None: + text = "" + else: + text = " {}".format(text) return Markup( - '''<a href="{target}" class="btn btn-{style}" {confirmation}> - <span class="glyphicon glyphicon-{icon}"></span> - </a>'''.format(target=target, style=style, confirmation=confirmation, icon=icon)) - + "<span class=\"glyphicon glyphicon-{}\"></span>{}".format( + name, text)) + + @staticmethod + def button(target, icon, style, confirm=None): + return Table.link( + target=target, + text=Table.glyphicon(icon), + css_class="btn btn-{}".format(style)) + + @staticmethod + def button_group(buttons, size="xs"): + return Markup("".join([ + Markup("<div class=\"btn-group btn-group-{}\">").format(size), + "".join(buttons), + Markup("</div>"), + ])) + @staticmethod def mail(target): - return Markup("<a href=\"mailto:{}\">{}</a>".format(target, target)) + return Table.link("mailto:{}".format(target), target) @staticmethod def bool(value): @@ -46,11 +68,11 @@ class Table: @staticmethod def concat(values): return Markup(", ".join(values)) - #if len(values) <= 1: - # return "".join(values) - #else: - # return "{} and {}".format(", ".join(values[:-1]), values[-1]) - + + @staticmethod + def concat_lines(values): + return Markup("<br>".join(values)) + class SingleValueTable: def __init__(self, title, value, newlink=None, newtext=None): @@ -62,16 +84,17 @@ class SingleValueTable: def rows(self): return [self.row()] + class ProtocolsTable(Table): def __init__(self, protocols, search_results=None): - super().__init__("Protokolle", protocols, newlink=url_for("new_protocol")) + super().__init__( + "Protokolle", protocols, newlink=url_for("new_protocol")) self.search_results = search_results def headers(self): - user = current_user() result = ["Sitzung", "Sitzung", "Datum"] - state_part = ["Status", "Status",""] - search_part = ["Suchergebnis",""] + state_part = ["Uhrzeit", "Status", "Status", ""] + search_part = ["Suchergebnis", ""] if self.search_results is None: result.extend(state_part) else: @@ -79,50 +102,58 @@ class ProtocolsTable(Table): return result def classes(self): + _MOBILE = ["hidden-sm hidden-md hidden-lg"] + _STANDARD = ["hidden-xs"] + _ALL = [""] if self.search_results is None: - result = ["hidden-sm hidden-md hidden-lg", "hidden-xs", "hidden-xs", "hidden-sm hidden-md hidden-lg", "hidden-xs", ""] + return _MOBILE + 3 * _STANDARD + _MOBILE + _STANDARD + _ALL else: - result = ["hidden-sm hidden-md hidden-lg", "hidden-xs", "hidden-xs", "", "hidden-xs","hidden-xs"] - return result + return _MOBILE + 2 * _STANDARD + _ALL + 2 * _STANDARD def row(self, protocol): user = current_user() protocol_link = url_for("show_protocol", protocol_id=protocol.id) result = [ - Markup("<br>").join([Table.link(protocol_link, protocol.protocoltype.name), date_filter(protocol.date)]), + # Protocol (mobile) + Table.concat_lines([ + Table.link(protocol_link, protocol.protocoltype.name), + date_filter(protocol.date)]), + # Protocol (standard) Table.link(protocol_link, protocol.protocoltype.name), - date_filter(protocol.date), + date_filter(protocol.date) ] if self.search_results is None: - result.append(Markup('<span class="glyphicon glyphicon-{state}"></span>'.format(state=protocol.get_state_glyph()))) - result.append(Markup('<span class="glyphicon glyphicon-{glyph}"></span> {state}'.format(state=protocol.get_state_name(),glyph=protocol.get_state_glyph()))) + result.append(Markup(time_filter(protocol.start_time))) + # State (mobile) + result.append(Table.glyphicon(protocol.get_state_glyph())) + # State (standard) + result.append(Table.glyphicon( + protocol.get_state_glyph(), protocol.get_state_name())) elif protocol in self.search_results: result.append(Markup(self.search_results[protocol])) - result.append(Markup('<span class="glyphicon glyphicon-{state}"></span>'.format(state=protocol.get_state_glyph()))) - - login_part1="" - login_part2="" + result.append(Table.glyphicon(protocol.get_state_glyph())) + + buttons = [] if protocol.has_public_view_right(user): user_right = protocol.has_private_view_right(user) document = protocol.get_compiled_document(user_right) if document is not None: - login_part1 = Table.button( + buttons.append(Table.button( url_for("download_document", document_id=document.id), - icon="download", style="success") + icon="download", style="success")) if protocol.protocoltype.has_admin_right(user): - login_part2 = Table.button( + buttons.append(Table.button( url_for("delete_protocol", protocol_id=protocol.id), icon="trash", style="danger", - confirm="Bist du dir sicher, dass du das Protokoll {} löschen möchtest?") - - result.append(Markup( - '<div class="btn-group btn-group-xs"> {} </div>'.format( - "".join((login_part1, login_part2))))) + confirm="Bist du dir sicher, dass du das Protokoll {} " + "löschen möchtest?")) + result.append(Table.button_group(buttons)) return result + class ProtocolTypesTable(Table): def __init__(self, types): super().__init__("Protokolltypen", types, newlink=url_for("new_type")) @@ -144,37 +175,51 @@ class ProtocolTypesTable(Table): user = current_user() has_private_view_right = protocoltype.has_private_view_right(user) has_modify_right = protocoltype.has_modify_right(user) - protocoltype_link = url_for("show_type", protocoltype_id=protocoltype.id) - protocol_link = (url_for("show_protocol", protocol_id=protocol.id) + protocoltype_link = url_for( + "show_type", protocoltype_id=protocoltype.id) + protocol_link = ( + url_for("show_protocol", protocol_id=protocol.id) if protocol is not None else "") - new_protocol_link = url_for("new_protocol", protocoltype_id=protocoltype.id) - mobile_name = "{} ({})".format(protocoltype.name, protocoltype.short_name) + new_protocol_link = url_for( + "new_protocol", protocoltype_id=protocoltype.id) + mobile_name = "{} ({})".format( + protocoltype.name, protocoltype.short_name) mobile_links = [] if protocol is not None: - mobile_links.append(Table.link(protocol_link, protocol.get_short_identifier())) + mobile_links.append(Table.link( + protocol_link, protocol.get_short_identifier())) if has_modify_right: - mobile_links.append(Table.link(new_protocol_link, "Neues Protokoll")) + mobile_links.append( + Table.link(new_protocol_link, "Neues Protokoll")) mobile_part = [ - Table.link(protocoltype_link, mobile_name) if has_private_view_right else mobile_name, + Table.link(protocoltype_link, mobile_name) + if has_private_view_right else mobile_name, Markup("<br>".join(mobile_links)) ] desktop_part = [ - Table.link(protocoltype_link, protocoltype.short_name) if has_private_view_right else protocoltype.short_name, + Table.link(protocoltype_link, protocoltype.short_name) + if has_private_view_right else protocoltype.short_name, protocoltype.name, - Table.link(protocol_link, protocol.get_short_identifier()) if protocol is not None else "Noch kein Protokoll", - Table.link(new_protocol_link, "Neues Protokoll") if has_modify_right else "" - "" # TODO: add link for modify, delete + Table.link(protocol_link, protocol.get_short_identifier()) + if protocol is not None else "Noch kein Protokoll", + Table.link(new_protocol_link, "Neues Protokoll") + if has_modify_right else "" + "" # TODO: add link for modify, delete ] return mobile_part + desktop_part + class ProtocolTypeTable(SingleValueTable): def __init__(self, protocoltype): - super().__init__(protocoltype.name, protocoltype, newlink=url_for("edit_type", protocoltype_id=protocoltype.id)) + super().__init__( + protocoltype.name, protocoltype, + newlink=url_for("edit_type", protocoltype_id=protocoltype.id)) def headers(self): - general_headers = ["Name", "Abkürzung", "Organisation", "Beginn", - "Öffentlich", "Verwaltungsgruppe", "Bearbeitungsgruppe", "Interne Gruppe", - "Öffentliche Gruppe"] + general_headers = [ + "Name", "Abkürzung", "Organisation", "Beginn", + "Öffentlich", "Verwaltungsgruppe", "Bearbeitungsgruppe", + "Interne Gruppe", "Öffentliche Gruppe"] etherpad_headers = ["Nicht-reproduzierbare Etherpadlinks"] if not config.ETHERPAD_ACTIVE: etherpad_headers = [] @@ -193,13 +238,17 @@ class ProtocolTypeTable(SingleValueTable): network_headers = ["Netzwerke einschränken", "Erlaubte Netzwerke"] action_headers = ["Aktion"] feed_headers = [] - latex_template_headers = ["LaTeX Vorlage"] if getattr(config, "LATEX_TEMPLATES", None) is not None else [] + latex_template_headers = ["LaTeX Vorlage"] if getattr( + config, "LATEX_TEMPLATES", None) is not None else [] if self.value.has_public_anonymous_view_right(): - feed_headers = [Markup("<img height=\"18px\" src=\"{}\" /> Feed".format( - url_for("static", filename="images/feed-icon.svg")))] - return (general_headers + etherpad_headers + mail_headers + feed_headers = [ + Markup("<img height=\"18px\" src=\"{}\" /> Feed".format( + url_for("static", filename="images/feed-icon.svg")))] + return ( + general_headers + etherpad_headers + mail_headers + printing_headers + wiki_headers + calendar_headers - + network_headers + latex_template_headers + feed_headers + action_headers) + + network_headers + latex_template_headers + feed_headers + + action_headers) def row(self): user = current_user() @@ -207,7 +256,8 @@ class ProtocolTypeTable(SingleValueTable): self.value.name, self.value.short_name, self.value.organization, - self.value.usual_time.strftime("%H:%M") if self.value.usual_time is not None else "", # todo: remove if, this field is required + self.value.usual_time.strftime("%H:%M") + if self.value.usual_time is not None else "", Table.bool(self.value.is_public), self.value.publish_group, self.value.modify_group, @@ -229,49 +279,79 @@ class ProtocolTypeTable(SingleValueTable): if not config.PRINTING_ACTIVE: printing_part = [] wiki_part = [ - (Table.bool(self.value.use_wiki) + ((", " + ("Öffentlich" if self.value.wiki_only_public else "Intern")) if self.value.use_wiki else "")) + (Table.bool(self.value.use_wiki) + + ((", " + + ("Öffentlich" + if self.value.wiki_only_public + else "Intern") + ) if self.value.use_wiki else "")) ] if self.value.use_wiki: wiki_part.append(self.value.wiki_category) if not config.WIKI_ACTIVE: wiki_part = [] - calendar_part = [self.value.calendar if self.value.calendar is not None else ""] + calendar_part = [ + self.value.calendar + if self.value.calendar is not None else ""] if not config.CALENDAR_ACTIVE: calendar_part = [] network_part = [Table.bool(self.value.restrict_networks)] if self.value.allowed_networks is not None: - network_part.append(", ".join(map(str.strip, self.value.allowed_networks.split(",")))) + network_part.append( + ", ".join(map( + str.strip, self.value.allowed_networks.split(",")))) else: network_part.append("") _latex_templates = getattr(config, "LATEX_TEMPLATES", None) if _latex_templates is not None: - latex_template_part = [_latex_templates[self.value.latex_template]['name'] if self.value.latex_template is not (None or "") else "Standardvorlage"] + latex_template_part = [ + _latex_templates[self.value.latex_template]['name'] + if (self.value.latex_template is not None + and self.value.latex_template != "") + else "Standardvorlage"] else: latex_template_part = [] feed_part = [] if self.value.has_public_anonymous_view_right(): feed_part = [Markup(", ".join([ - Table.link(url_for("feed_protocols_rss", + Table.link(url_for( + "feed_protocols_rss", protocoltype_id=self.value.id), "Protokolle (RSS)"), - Table.link(url_for("feed_protocols_atom", + Table.link(url_for( + "feed_protocols_atom", protocoltype_id=self.value.id), "Protokolle (Atom)"), - Table.link(url_for("feed_appointments_rss", + Table.link(url_for( + "feed_appointments_rss", protocoltype_id=self.value.id), "Sitzungen (RSS)"), - Table.link(url_for("feed_appointments_atom", + Table.link(url_for( + "feed_appointments_atom", protocoltype_id=self.value.id), "Sitzungen (Atom)"), - Table.link(url_for("feed_appointments_ical", + Table.link(url_for( + "feed_appointments_ical", protocoltype_id=self.value.id), "Sitzungen (iCal)"), ]))] - action_part = [Table.link(url_for("delete_type", protocoltype_id=self.value.id), "Löschen", confirm="Bist du dir sicher, dass du den Protokolltype {} löschen möchtest?".format(self.value.name))] + action_part = [ + Table.link( + url_for("delete_type", protocoltype_id=self.value.id, + csrf_token=get_csrf_token()), + "Löschen", + confirm="Bist du dir sicher, dass du den Protokolltype " + "{} löschen möchtest?".format(self.value.name)) + ] if not self.value.has_admin_right(user): action_part = [""] - return (general_part + etherpad_part + mail_part + printing_part - + wiki_part + calendar_part + network_part + latex_template_part + feed_part - + action_part) + return ( + general_part + etherpad_part + mail_part + printing_part + + wiki_part + calendar_part + network_part + latex_template_part + + feed_part + action_part) + class DefaultTOPsTable(Table): def __init__(self, tops, protocoltype=None): - super().__init__("Standard-TOPs", tops, newlink=url_for("new_default_top", protocoltype_id=protocoltype.id) if protocoltype is not None else None) + super().__init__( + "Standard-TOPs", tops, + newlink=url_for("new_default_top", protocoltype_id=protocoltype.id) + if protocoltype is not None else None) self.protocoltype = protocoltype def headers(self): @@ -282,31 +362,58 @@ class DefaultTOPsTable(Table): top.name, top.number, Table.concat([ - Table.link(url_for("move_default_top", defaulttop_id=top.id, diff=1), "Runter"), - Table.link(url_for("move_default_top", defaulttop_id=top.id, diff=-1), "Hoch"), - Table.link(url_for("edit_default_top", protocoltype_id=self.protocoltype.id, defaulttop_id=top.id), "Ändern"), - Table.link(url_for("delete_default_top", defaulttop_id=top.id), "Löschen", confirm="Bist du dir sicher, dass du den Standard-TOP {} löschen willst?".format(top.name)) + Table.link( + url_for("move_default_top", defaulttop_id=top.id, diff=1, + csrf_token=get_csrf_token()), + "Runter"), + Table.link( + url_for("move_default_top", defaulttop_id=top.id, diff=-1, + csrf_token=get_csrf_token()), + "Hoch"), + Table.link( + url_for( + "edit_default_top", + protocoltype_id=self.protocoltype.id, + defaulttop_id=top.id), + "Ändern"), + Table.link( + url_for("delete_default_top", defaulttop_id=top.id, + csrf_token=get_csrf_token()), + "Löschen", + confirm="Bist du dir sicher, dass du den Standard-TOP " + "{} löschen willst?".format(top.name)) ]) ] + class MeetingRemindersTable(Table): def __init__(self, reminders, protocoltype=None): - super().__init__("Einladungsmails", reminders, newlink=url_for("new_reminder", protocoltype_id=protocoltype.id) if protocoltype is not None else None) + super().__init__( + "Einladungsmails", reminders, + newlink=url_for("new_reminder", protocoltype_id=protocoltype.id) + if protocoltype is not None else None) self.protocoltype = protocoltype def headers(self): return ["Zeit", "Einladen", "Zusätzlicher Mailinhalt", ""] def row(self, reminder): - user = current_user() general_part = [ "{} Tage".format(reminder.days_before), self.get_send_summary(reminder), reminder.additional_text or "" ] action_links = [ - Table.link(url_for("edit_reminder", meetingreminder_id=reminder.id), "Ändern"), - Table.link(url_for("delete_reminder", meetingreminder_id=reminder.id), "Löschen", confirm="Bist du dir sicher, dass du die Einladungsmail {} Tage vor der Sitzung löschen willst?".format(reminder.days_before)) + Table.link( + url_for("edit_reminder", meetingreminder_id=reminder.id), + "Ändern"), + Table.link( + url_for("delete_reminder", meetingreminder_id=reminder.id, + csrf_token=get_csrf_token()), + "Löschen", + confirm="Bist du dir sicher, dass du die Einladungsmail {} " + "Tage vor der Sitzung löschen willst?".format( + reminder.days_before)) ] action_part = [Table.concat(action_links)] return general_part + action_part @@ -319,26 +426,36 @@ class MeetingRemindersTable(Table): parts.append("Intern") return " und ".join(parts) + class ErrorsTable(Table): def __init__(self, errors): super().__init__("Fehler", errors) def headers(self): - return ["Protokoll", "Aktion", "Fehler", "Zeitpunkt", "Beschreibung", ""] + return [ + "Protokoll", "Aktion", "Fehler", "Zeitpunkt", "Beschreibung", ""] def classes(self): return [None, None, None, None, "hidden-xs", "hidden-xs"] def row(self, error): return [ - Table.link(url_for("show_protocol", protocol_id=error.protocol.id), error.protocol.get_short_identifier()), + Table.link( + url_for("show_protocol", protocol_id=error.protocol.id), + error.protocol.get_short_identifier()), error.action, Table.link(url_for("show_error", error_id=error.id), error.name), datetime_filter(error.datetime), error.get_short_description(), - Table.link(url_for("delete_error", error_id=error.id, next=request.path), "Löschen", confirm="Bist du dir sicher, dass du den Fehler löschen möchtest?") + Table.link( + url_for("delete_error", error_id=error.id, + csrf_token=get_csrf_token()), + "Löschen", + confirm="Bist du dir sicher, dass du den Fehler löschen " + "möchtest?") ] + class ErrorTable(SingleValueTable): def __init__(self, error): super().__init__(error.action, error) @@ -348,12 +465,15 @@ class ErrorTable(SingleValueTable): def row(self): return [ - Table.link(url_for("show_protocol", protocol_id=self.value.protocol.id), self.value.protocol.get_short_identifier()), + Table.link( + url_for("show_protocol", protocol_id=self.value.protocol.id), + self.value.protocol.get_short_identifier()), self.value.action, self.value.name, datetime_filter(self.value.datetime) ] + class TodosTable(Table): def __init__(self, todos): super().__init__("Todos", todos, newlink=url_for("new_todo")) @@ -362,34 +482,48 @@ class TodosTable(Table): return ["Todo", "ID", "Status", "Sitzung", "Name", "Aufgabe", ""] def classes(self): - return ["hidden-sm hidden-md hidden-lg", "hidden-xs", "hidden-xs", "hidden-xs", "hidden-xs", None, "hidden-xs"] + return [ + "hidden-sm hidden-md hidden-lg", + "hidden-xs", "hidden-xs", "hidden-xs", "hidden-xs", + None, "hidden-xs"] def row(self, todo): user = current_user() protocol = todo.get_first_protocol() - mobile_parts = [Table.link(url_for("show_todo", todo_id=todo.id), todo.get_state())] + mobile_parts = [Table.link( + url_for("show_todo", todo_id=todo.id), + todo.get_state())] if protocol is not None: - mobile_parts.append(Table.link(url_for("show_protocol", protocol_id=protocol.id), todo.protocoltype.short_name)) + mobile_parts.append(Table.link( + url_for("show_protocol", protocol_id=protocol.id), + todo.protocoltype.short_name)) mobile_parts.append(todo.who) row = [ Markup("<br>").join(mobile_parts), 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_short_identifier()) - if protocol is not None - else Table.link(url_for("list_protocols", protocoltype_id=todo.protocoltype.id), todo.protocoltype.short_name), + Table.link( + url_for("show_protocol", protocol_id=protocol.id), + protocol.get_short_identifier()) + if protocol is not None + else Table.link( + url_for( + "list_protocols", protocoltype_id=todo.protocoltype.id), + todo.protocoltype.short_name), todo.who, todo.description, ] if todo.protocoltype.has_modify_right(user): row.append(Table.concat([ Table.link(url_for("edit_todo", todo_id=todo.id), "Ändern"), - Table.link(url_for("delete_todo", todo_id=todo.id), "Löschen") + Table.link(url_for("delete_todo", todo_id=todo.id, + csrf_token=get_csrf_token()), "Löschen") ])) else: row.append("") return row + class TodoTable(SingleValueTable): def __init__(self, todo): super().__init__("Todo", todo) @@ -399,26 +533,33 @@ class TodoTable(SingleValueTable): def row(self): user = current_user() - protocol = self.value.get_first_protocol() row = [ self.value.get_id(), self.value.get_state_plain(), Table.concat([ - Table.link(url_for("show_protocol", protocol_id=protocol.id), protocol.get_short_identifier()) - for protocol in self.value.protocols + Table.link( + url_for("show_protocol", protocol_id=protocol.id), + protocol.get_short_identifier()) + for protocol in self.value.protocols ]), self.value.who, self.value.description ] 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?") + Table.link( + url_for("edit_todo", todo_id=self.value.id), "Ändern"), + Table.link( + url_for("delete_todo", todo_id=self.value.id, + csrf_token=get_csrf_token()), "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): super().__init__("Beschlüsse", decisions) @@ -438,21 +579,29 @@ class DecisionsTable(Table): def row(self, decision): user = current_user() content_part = [ - Table.link(url_for("show_protocol", protocol_id=decision.protocol.id), decision.protocol.get_short_identifier()), + Table.link( + url_for("show_protocol", protocol_id=decision.protocol.id), + decision.protocol.get_short_identifier()), decision.content ] category_part = [decision.get_categories_str()] if not self.category_present: category_part = [] action_part = [ - Table.link(url_for("print_decision", decisiondocument_id=decision.document.id), "Drucken") - if config.PRINTING_ACTIVE + Table.link( + url_for( + "print_decision", + decisiondocument_id=decision.document.id, + csrf_token=get_csrf_token()), + "Drucken") + if (config.PRINTING_ACTIVE and decision.protocol.protocoltype.has_modify_right(user) - and decision.document is not None - else "" + and decision.document is not None) + else "" ] return content_part + category_part + action_part + class DocumentsTable(Table): def __init__(self, documents, protocol): super().__init__("Anhang", documents) @@ -464,7 +613,7 @@ class DocumentsTable(Table): visibility_headers = [] if self.protocol.has_private_view_right(user): visibility_headers = ["Sichtbarkeit"] - action_headers=[""] + action_headers = [""] return general_headers + visibility_headers + action_headers def classes(self): @@ -480,24 +629,41 @@ class DocumentsTable(Table): user = current_user() links = [] if document.protocol.has_modify_right(user): - links.append(Table.link(url_for("edit_document", document_id=document.id), "Bearbeiten")) + links.append(Table.link( + url_for("edit_document", document_id=document.id), + "Bearbeiten")) if config.PRINTING_ACTIVE and document.protocol.has_modify_right(user): - links.append(Table.link(url_for("print_document", document_id=document.id), "Drucken")) + links.append(Table.link( + url_for("print_document", document_id=document.id, + csrf_token=get_csrf_token()), + "Drucken")) if document.protocol.protocoltype.has_admin_right(user): - links.append(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))) + links.append(Table.link( + url_for("delete_document", document_id=document.id, + csrf_token=get_csrf_token()), + "Löschen", + confirm="Bist du dir sicher, dass du das Dokument {} löschen " + "willst?".format(document.name))) general_part = [ document.id, - Table.link(url_for("download_document", document_id=document.id), document.name), + Table.link( + url_for("download_document", document_id=document.id), + document.name), ] visibility_part = [] if document.protocol.has_private_view_right(user): - visibility_part = ["Intern" if document.is_private else "Öffentlich"] + visibility_part = [ + "Intern" + if document.is_private + else "Öffentlich"] action_part = [Table.concat(links)] return general_part + visibility_part + action_part + class TodoMailsTable(Table): def __init__(self, todomails): - super().__init__("Todo-Mail-Zuordnungen", todomails, url_for("new_todomail")) + super().__init__( + "Todo-Mail-Zuordnungen", todomails, url_for("new_todomail")) def headers(self): return ["Name", "Mail", ""] @@ -507,11 +673,20 @@ class TodoMailsTable(Table): todomail.name, todomail.mail, Table.concat([ - Table.link(url_for("edit_todomail", todomail_id=todomail.id), "Ändern"), - Table.link(url_for("delete_todomail", todomail_id=todomail.id), "Löschen", confirm="Bist du dir sicher, dass du die Todomailzuordnung {} zu {} löschen willst?".format(todomail.name, todomail.mail)) + Table.link( + url_for("edit_todomail", todomail_id=todomail.id), + "Ändern"), + Table.link( + url_for("delete_todomail", todomail_id=todomail.id, + csrf_token=get_csrf_token()), + "Löschen", + confirm="Bist du dir sicher, dass du die " + "Todomailzuordnung {} zu {} löschen " + "willst?".format(todomail.name, todomail.mail)) ]) ] + class DefaultMetasTable(Table): def __init__(self, metas, protocoltype): super().__init__( @@ -524,7 +699,6 @@ class DefaultMetasTable(Table): return ["Name", "Key", "Standardwert", "Intern", "Vorher", ""] def row(self, meta): - user = current_user() general_part = [ meta.name, meta.key, @@ -533,17 +707,24 @@ class DefaultMetasTable(Table): Table.bool(meta.prior) ] links = [ - Table.link(url_for("edit_defaultmeta", defaultmeta_id=meta.id), "Ändern"), - Table.link(url_for("delete_defaultmeta", defaultmeta_id=meta.id), "Löschen", confirm="Bist du dir sicher, dass du das Metadatenfeld {} löschen willst?".format(meta.name)) + Table.link( + url_for("edit_defaultmeta", defaultmeta_id=meta.id), "Ändern"), + Table.link( + url_for("delete_defaultmeta", defaultmeta_id=meta.id, + csrf_token=get_csrf_token()), + "Löschen", + confirm="Bist du dir sicher, dass du das Metadatenfeld {} " + "löschen willst?".format(meta.name)) ] link_part = [Table.concat(links)] return general_part + link_part + class DecisionCategoriesTable(Table): def __init__(self, categories, protocoltype): super().__init__( "Beschlusskategorien", - categories, + categories, url_for("new_decisioncategory", protocoltype_id=protocoltype.id) ) @@ -551,13 +732,23 @@ class DecisionCategoriesTable(Table): return ["Name", ""] def row(self, category): - user = current_user() general_part = [category.name] action_part = [ Table.concat([ - Table.link(url_for("edit_decisioncategory", decisioncategory_id=category.id), "Ändern"), - Table.link(url_for("delete_decisioncategory", decisioncategory_id=category.id), "Löschen", confirm="Bist du dir sicher, dass du die Beschlusskategorie {} löschen willst?".format(category.name)) + Table.link( + url_for( + "edit_decisioncategory", + decisioncategory_id=category.id), + "Ändern"), + Table.link( + url_for( + "delete_decisioncategory", + decisioncategory_id=category.id, + csrf_token=get_csrf_token()), + "Löschen", + confirm="Bist du dir sicher, dass du die " + "Beschlusskategorie {} löschen " + "willst?".format(category.name)) ]) ] return general_part + action_part - diff --git a/wiki.py b/wiki.py index 4ae80405ea65e2e22cc085720884fc722138b8c1..a393e69326d0d2f37e9a19bd072ad71159508822 100644 --- a/wiki.py +++ b/wiki.py @@ -1,14 +1,15 @@ import requests -import json import config HTTP_STATUS_OK = 200 HTTP_STATUS_AUTHENTICATE = 401 + class WikiException(Exception): pass + def _filter_params(params): result = {} for key, value in sorted(params.items(), key=lambda t: t[0] == "token"): @@ -19,14 +20,20 @@ def _filter_params(params): result[key] = value return result + class WikiClient: - def __init__(self, active=None, endpoint=None, anonymous=None, user=None, password=None, domain=None): - self.active = active if active is not None else config.WIKI_ACTIVE - self.endpoint = endpoint if endpoint is not None else config.WIKI_API_URL - self.anonymous = anonymous if anonymous is not None else config.WIKI_ANONYMOUS - self.user = user if user is not None else config.WIKI_USER - self.password = password if password is not None else config.WIKI_PASSWORD - self.domain = domain if domain is not None else config.WIKI_DOMAIN + def __init__(self, active=None, endpoint=None, anonymous=None, user=None, + password=None, domain=None): + def _or_default(value, default): + if value is None: + return default + return value + self.active = _or_default(active, config.WIKI_ACTIVE) + self.endpoint = _or_default(endpoint, config.WIKI_API_URL) + self.anonymous = _or_default(anonymous, config.WIKI_ANONYMOUS) + self.user = _or_default(user, config.WIKI_USER) + self.password = _or_default(password, config.WIKI_PASSWORD) + self.domain = _or_default(domain, config.WIKI_DOMAIN) self.token = None self.cookies = requests.cookies.RequestsCookieJar() @@ -45,30 +52,33 @@ class WikiClient: def login(self): if not self.active: return - # todo: Change this to the new MediaWiki tokens api once the wiki is updated + # todo: Change this to the new MediaWiki tokens api token_answer = self.do_action("login", method="post", lgname=self.user) if "login" not in token_answer or "token" not in token_answer["login"]: raise WikiException("No token in login answer.") lgtoken = token_answer["login"]["token"] - login_answer = self.do_action("login", method="post", lgname=self.user, lgpassword=self.password, lgdomain=self.domain, lgtoken=lgtoken) + login_answer = self.do_action( + "login", method="post", lgname=self.user, lgpassword=self.password, + lgdomain=self.domain, lgtoken=lgtoken) if ("login" not in login_answer - or "result" not in login_answer["login"] - or login_answer["login"]["result"] != "Success"): + or "result" not in login_answer["login"] + or login_answer["login"]["result"] != "Success"): raise WikiException("Login not successful.") - def logout(self): if not self.active: return self.do_action("logout") - def edit_page(self, title, content, summary, recreate=True, createonly=False): + def edit_page(self, title, content, summary, recreate=True, + createonly=False): if not self.active: return # todo: port to new api once the wiki is updated - prop_answer = self.do_action("query", method="get", prop="info", intoken="edit", titles=title) + prop_answer = self.do_action( + "query", method="get", prop="info", intoken="edit", titles=title) if ("query" not in prop_answer - or "pages" not in prop_answer["query"]): + or "pages" not in prop_answer["query"]): raise WikiException("Can't get token for page {}".format(title)) pages = prop_answer["query"]["pages"] edit_token = None @@ -78,7 +88,8 @@ class WikiClient: break if edit_token is None: raise WikiException("Can't get token for page {}".format(title)) - edit_answer = self.do_action(action="edit", method="post", data={"text": content}, + self.do_action( + action="edit", method="post", data={"text": content}, token=edit_token, title=title, summary=summary, recreate=recreate, createonly=createonly, bot=True) @@ -89,18 +100,28 @@ class WikiClient: kwargs["action"] = action kwargs["format"] = "json" params = _filter_params(kwargs) + def _do_request(): if method == "get": - return requests.get(self.endpoint, cookies=self.cookies, params=params, auth=requests.auth.HTTPBasicAuth(self.user, self.password)) + return requests.get( + self.endpoint, cookies=self.cookies, params=params, + auth=requests.auth.HTTPBasicAuth(self.user, self.password)) elif method == "post": - return requests.post(self.endpoint, cookies=self.cookies, data=data, params=params, auth=requests.auth.HTTPBasicAuth(self.user, self.password)) + return requests.post( + self.endpoint, cookies=self.cookies, data=data, + params=params, auth=requests.auth.HTTPBasicAuth( + self.user, self.password)) req = _do_request() if req.status_code != HTTP_STATUS_OK: - raise WikiException("HTTP status code {} on action {}.".format(req.status_code, action)) + raise WikiException( + "HTTP status code {} on action {}.".format( + req.status_code, action)) self.cookies.update(req.cookies) return req.json() + def main(): with WikiClient() as client: - client.edit_page(title="Test", content="This is a very long text.", summary="API client test") - + client.edit_page( + title="Test", content="This is a very long text.", + summary="API client test")