Commit 6d9c3fdd authored by Robin Sonnabend's avatar Robin Sonnabend

Improve *.py code quality

Also fixes some latent, rather unimportant bugs
parent 663a9d49
import hmac, hashlib import hmac
import hashlib
import ssl import ssl
from datetime import datetime from datetime import datetime
class User: class User:
def __init__(self, username, groups, timestamp=None, obsolete=False, def __init__(self, username, groups, timestamp=None, obsolete=False,
permanent=False): permanent=False):
self.username = username self.username = username
self.groups = groups self.groups = groups
if timestamp is not None: if timestamp is not None:
...@@ -16,8 +17,9 @@ class User: ...@@ -16,8 +17,9 @@ class User:
self.permanent = permanent self.permanent = permanent
def summarize(self): def summarize(self):
return "{}:{}:{}:{}:{}".format(self.username, ",".join(self.groups), return ":".join((
str(self.timestamp.timestamp()), self.obsolete, self.permanent) self.username, ",".join(self.groups),
str(self.timestamp.timestamp()), self.obsolete, self.permanent))
@staticmethod @staticmethod
def from_summary(summary): def from_summary(summary):
...@@ -45,7 +47,8 @@ class UserManager: ...@@ -45,7 +47,8 @@ class UserManager:
for backend in self.backends: for backend in self.backends:
if backend.authenticate(username, password): if backend.authenticate(username, password):
groups = sorted(list(set(backend.groups(username, password)))) groups = sorted(list(set(backend.groups(username, password))))
return User(username, groups, obsolete=backend.obsolete, return User(
username, groups, obsolete=backend.obsolete,
permanent=permanent) permanent=permanent)
return None return None
...@@ -77,8 +80,8 @@ class SecurityManager: ...@@ -77,8 +80,8 @@ class SecurityManager:
if user is None: if user is None:
return False return False
session_duration = datetime.now() - user.timestamp session_duration = datetime.now() - user.timestamp
macs_equal = hmac.compare_digest(maccer.hexdigest().encode("utf-8"), macs_equal = hmac.compare_digest(
hash) maccer.hexdigest().encode("utf-8"), hash)
time_short = int(session_duration.total_seconds()) < self.max_duration time_short = int(session_duration.total_seconds()) < self.max_duration
return macs_equal and (time_short or user.permanent) return macs_equal and (time_short or user.permanent)
...@@ -97,23 +100,22 @@ class StaticUserManager: ...@@ -97,23 +100,22 @@ class StaticUserManager:
def authenticate(self, username, password): def authenticate(self, username, password):
return (username in self.passwords return (username in self.passwords
and self.passwords[username] == password) and self.passwords[username] == password)
def groups(self, username, password=None): def groups(self, username, password=None):
if username in self.group_map: if username in self.group_map:
yield from self.group_map[username] yield from self.group_map[username]
def all_groups(self): def all_groups(self):
yield from list(set(group for group in groups.values())) yield from list(set(group for group in self.group_map.values()))
try: try:
import ldap3 import ldap3
from ldap3.utils.dn import parse_dn
class LdapManager: class LdapManager:
def __init__(self, host, user_dn, group_dn, port=636, use_ssl=True, def __init__(self, host, user_dn, group_dn, port=636, use_ssl=True,
obsolete=False): obsolete=False):
self.server = ldap3.Server(host, port=port, use_ssl=use_ssl) self.server = ldap3.Server(host, port=port, use_ssl=use_ssl)
self.user_dn = user_dn self.user_dn = user_dn
self.group_dn = group_dn self.group_dn = group_dn
...@@ -121,8 +123,8 @@ try: ...@@ -121,8 +123,8 @@ try:
def authenticate(self, username, password): def authenticate(self, username, password):
try: try:
connection = ldap3.Connection(self.server, connection = ldap3.Connection(
self.user_dn.format(username), password) self.server, self.user_dn.format(username), password)
return connection.bind() return connection.bind()
except ldap3.core.exceptions.LDAPSocketOpenError: except ldap3.core.exceptions.LDAPSocketOpenError:
return False return False
...@@ -144,16 +146,15 @@ try: ...@@ -144,16 +146,15 @@ try:
for group in group_reader.search(): for group in group_reader.search():
yield group.cn.value yield group.cn.value
class ADManager: class ADManager:
def __init__(self, host, domain, user_dn, group_dn, def __init__(self, host, domain, user_dn, group_dn,
port=636, use_ssl=True, ca_cert=None, obsolete=False): port=636, use_ssl=True, ca_cert=None, obsolete=False):
tls_config = ldap3.Tls(validate=ssl.CERT_REQUIRED) tls_config = ldap3.Tls(validate=ssl.CERT_REQUIRED)
if ca_cert is not None: if ca_cert is not None:
tls_config = ldap3.Tls(validate=ssl.CERT_REQUIRED, tls_config = ldap3.Tls(
ca_certs_file=ca_cert) validate=ssl.CERT_REQUIRED, ca_certs_file=ca_cert)
self.server = ldap3.Server(host, port=port, use_ssl=use_ssl, self.server = ldap3.Server(
tls=tls_config) host, port=port, use_ssl=use_ssl, tls=tls_config)
self.domain = domain self.domain = domain
self.user_dn = user_dn self.user_dn = user_dn
self.group_dn = group_dn self.group_dn = group_dn
...@@ -176,11 +177,13 @@ try: ...@@ -176,11 +177,13 @@ try:
connection.bind() connection.bind()
obj_def = ldap3.ObjectDef("user", connection) obj_def = ldap3.ObjectDef("user", connection)
name_filter = "cn:={}".format(username) name_filter = "cn:={}".format(username)
user_reader = ldap3.Reader(connection, obj_def, self.user_dn, user_reader = ldap3.Reader(
name_filter) connection, obj_def, self.user_dn, name_filter)
group_def = ldap3.ObjectDef("group", connection) group_def = ldap3.ObjectDef("group", connection)
def _yield_recursive_groups(group_dn): def _yield_recursive_groups(group_dn):
group_reader = ldap3.Reader(connection, group_def, group_dn, None) group_reader = ldap3.Reader(
connection, group_def, group_dn, None)
for entry in group_reader.search(): for entry in group_reader.search():
yield entry.name.value yield entry.name.value
for child in entry.memberOf: for child in entry.memberOf:
...@@ -189,20 +192,22 @@ try: ...@@ -189,20 +192,22 @@ try:
for group_dn in result.memberOf: for group_dn in result.memberOf:
yield from _yield_recursive_groups(group_dn) yield from _yield_recursive_groups(group_dn)
def all_groups(self): def all_groups(self):
connection = self.prepare_connection() connection = self.prepare_connection()
connection.bind() connection.bind()
obj_def = ldap3.ObjectDef("group", connection) obj_def = ldap3.ObjectDef("group", connection)
group_reader = ldap3.Reader(connection, obj_def, self.group_dn) group_reader = ldap3.Reader(connection, obj_def, self.group_dn)
for result in reader.search(): for result in group_reader.search():
yield result.name.value yield result.name.value
except ModuleNotFoundError: except ModuleNotFoundError:
pass pass
try: try:
import grp, pwd, pam import grp
import pwd
import pam
class PAMManager: class PAMManager:
def __init__(self, obsolete=False): def __init__(self, obsolete=False):
...@@ -224,4 +229,3 @@ try: ...@@ -224,4 +229,3 @@ try:
yield group.gr_name yield group.gr_name
except ModuleNotFoundError: except ModuleNotFoundError:
pass pass
...@@ -10,6 +10,7 @@ import config ...@@ -10,6 +10,7 @@ import config
cookie = getattr(config, "REDIRECT_BACK_COOKIE", "back") cookie = getattr(config, "REDIRECT_BACK_COOKIE", "back")
default_view = getattr(config, "REDIRECT_BACK_DEFAULT", "index") default_view = getattr(config, "REDIRECT_BACK_DEFAULT", "index")
def anchor(func, cookie=cookie): def anchor(func, cookie=cookie):
@functools.wraps(func) @functools.wraps(func)
def result(*args, **kwargs): def result(*args, **kwargs):
...@@ -17,8 +18,10 @@ def anchor(func, cookie=cookie): ...@@ -17,8 +18,10 @@ def anchor(func, cookie=cookie):
return func(*args, **kwargs) return func(*args, **kwargs)
return result return result
def url(default=default_view, cookie=cookie, **url_args): def url(default=default_view, cookie=cookie, **url_args):
return session.get(cookie, url_for(default, **url_args)) return session.get(cookie, url_for(default, **url_args))
def redirect(default=default_view, cookie=cookie, **url_args): def redirect(default=default_view, cookie=cookie, **url_args):
return flask_redirect(url(default, cookie, **url_args)) return flask_redirect(url(default, cookie, **url_args))
...@@ -2,15 +2,16 @@ from datetime import datetime, timedelta ...@@ -2,15 +2,16 @@ from datetime import datetime, timedelta
import random import random
import quopri import quopri
from caldav import DAVClient, Principal, Calendar, Event from caldav import DAVClient
from caldav.lib.error import PropfindError
from vobject.base import ContentLine from vobject.base import ContentLine
import config import config
class CalendarException(Exception): class CalendarException(Exception):
pass pass
class Client: class Client:
def __init__(self, calendar=None, url=None): def __init__(self, calendar=None, url=None):
if not config.CALENDAR_ACTIVE: if not config.CALENDAR_ACTIVE:
...@@ -23,9 +24,12 @@ class Client: ...@@ -23,9 +24,12 @@ class Client:
self.principal = self.client.principal() self.principal = self.client.principal()
break break
except Exception as exc: 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: 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: if calendar is not None:
self.calendar = self.get_calendar(calendar) self.calendar = self.get_calendar(calendar)
else: else:
...@@ -41,9 +45,11 @@ class Client: ...@@ -41,9 +45,11 @@ class Client:
for calendar in self.principal.calendars() for calendar in self.principal.calendars()
] ]
except Exception as exc: except Exception as exc:
print("Got exception {} from caldav, retrying".format(str(exc))) print("Got exception {} from caldav, retrying".format(
raise CalendarException("Got {} CalDAV Errors from the CalDAV server.".format(config.CALENDAR_MAX_REQUESTS)) str(exc)))
raise CalendarException(
"Got {} CalDAV Errors from the CalDAV server.".format(
config.CALENDAR_MAX_REQUESTS))
def get_calendar(self, calendar_name): def get_calendar(self, calendar_name):
candidates = self.principal.calendars() candidates = self.principal.calendars()
...@@ -57,12 +63,14 @@ class Client: ...@@ -57,12 +63,14 @@ class Client:
return return
candidates = [ candidates = [
Event.from_raw_event(raw_event) 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] candidates = [event for event in candidates if event.name == name]
event = None event = None
if len(candidates) == 0: if len(candidates) == 0:
event = Event(None, name, description, begin, event = Event(
None, name, description, begin,
begin + timedelta(hours=config.CALENDAR_DEFAULT_DURATION)) begin + timedelta(hours=config.CALENDAR_DEFAULT_DURATION))
vevent = self.calendar.add_event(event.to_vcal()) vevent = self.calendar.add_event(event.to_vcal())
event.vevent = vevent event.vevent = vevent
...@@ -76,11 +84,14 @@ NAME_KEY = "summary" ...@@ -76,11 +84,14 @@ NAME_KEY = "summary"
DESCRIPTION_KEY = "description" DESCRIPTION_KEY = "description"
BEGIN_KEY = "dtstart" BEGIN_KEY = "dtstart"
END_KEY = "dtend" END_KEY = "dtend"
def _get_item(content, key): def _get_item(content, key):
if key in content: if key in content:
return content[key][0].value return content[key][0].value
return None return None
class Event: class Event:
def __init__(self, vevent, name, description, begin, end): def __init__(self, vevent, name, description, begin, end):
self.vevent = vevent self.vevent = vevent
...@@ -97,7 +108,8 @@ class Event: ...@@ -97,7 +108,8 @@ class Event:
description = _get_item(content, DESCRIPTION_KEY) description = _get_item(content, DESCRIPTION_KEY)
begin = _get_item(content, BEGIN_KEY) begin = _get_item(content, BEGIN_KEY)
end = _get_item(content, END_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) begin=begin, end=end)
def set_description(self, description): def set_description(self, description):
...@@ -105,7 +117,8 @@ class Event: ...@@ -105,7 +117,8 @@ class Event:
self.description = description self.description = description
encoded = encode_quopri(description) encoded = encode_quopri(description)
if DESCRIPTION_KEY not in raw_event.contents: 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: else:
content_line = raw_event.contents[DESCRIPTION_KEY][0] content_line = raw_event.contents[DESCRIPTION_KEY][0]
content_line.value = encoded content_line.value = encoded
...@@ -129,21 +142,28 @@ SUMMARY:{summary} ...@@ -129,21 +142,28 @@ SUMMARY:{summary}
DESCRIPTION;ENCODING=QUOTED-PRINTABLE:{description} DESCRIPTION;ENCODING=QUOTED-PRINTABLE:{description}
END:VEVENT END:VEVENT
END:VCALENDAR""".format( END:VCALENDAR""".format(
uid=create_uid(), now=date_format(datetime.now()-offset), uid=create_uid(),
begin=date_format(self.begin-offset), end=date_format(self.end-offset), now=date_format(datetime.now() - offset),
begin=date_format(self.begin - offset),
end=date_format(self.end - offset),
summary=self.name, summary=self.name,
description=encode_quopri(self.description)) description=encode_quopri(self.description))
def create_uid(): def create_uid():
return str(random.randint(0, 1e10)).rjust(10, "0") return str(random.randint(0, 1e10)).rjust(10, "0")
def date_format(dt): def date_format(dt):
return dt.strftime("%Y%m%dT%H%M%SZ") return dt.strftime("%Y%m%dT%H%M%SZ")
def get_timezone_offset(): def get_timezone_offset():
difference = datetime.now() - datetime.utcnow() 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")
...@@ -3,14 +3,20 @@ import regex as re ...@@ -3,14 +3,20 @@ import regex as re
import os import os
import sys 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 = "[\"']" 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 = "." ROOT_DIR = "."
ENDINGS = [".py", ".html", ".txt"] ENDINGS = [".py", ".html", ".txt"]
MAX_DEPTH = 2 MAX_DEPTH = 2
def list_dir(dir, level=0): def list_dir(dir, level=0):
if level >= MAX_DEPTH: if level >= MAX_DEPTH:
return return
...@@ -23,7 +29,8 @@ def list_dir(dir, level=0): ...@@ -23,7 +29,8 @@ def list_dir(dir, level=0):
if file.endswith(ending): if file.endswith(ending):
yield path yield path
elif os.path.isdir(path): elif os.path.isdir(path):
yield from list_dir(path, level+1) yield from list_dir(path, level + 1)
class Route: class Route:
def __init__(self, file, name, parameters): def __init__(self, file, name, parameters):
...@@ -38,13 +45,15 @@ class Route: ...@@ -38,13 +45,15 @@ class Route:
def get_parameter_set(self): def get_parameter_set(self):
return {parameter.name for parameter in self.parameters} return {parameter.name for parameter in self.parameters}
class Parameter: class Parameter:
def __init__(self, name, type=None): def __init__(self, name, type=None):
self.name = name self.name = name
self.type = type self.type = type
def __repr__(self): 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 @staticmethod
def from_string(text): def from_string(text):
...@@ -53,6 +62,7 @@ class Parameter: ...@@ -53,6 +62,7 @@ class Parameter:
return Parameter(name, type) return Parameter(name, type)
return Parameter(text) return Parameter(text)
def split_url_parameters(url): def split_url_parameters(url):
params = [] params = []
current_param = None current_param = None
...@@ -68,9 +78,11 @@ def split_url_parameters(url): ...@@ -68,9 +78,11 @@ def split_url_parameters(url):
current_param += char current_param += char
return params return params
def split_function_parameters(parameters): def split_function_parameters(parameters):
return list(map(str.strip, parameters.split(","))) return list(map(str.strip, parameters.split(",")))
def read_url_for_parameters(content): def read_url_for_parameters(content):
params = [] params = []
bracket_level = 1 bracket_level = 1
...@@ -92,6 +104,7 @@ def read_url_for_parameters(content): ...@@ -92,6 +104,7 @@ def read_url_for_parameters(content):
elif char == ")": elif char == ")":
bracket_level -= 1 bracket_level -= 1
class UrlFor: class UrlFor:
def __init__(self, file, name, parameters): def __init__(self, file, name, parameters):
self.file = file self.file = file
...@@ -99,8 +112,10 @@ class UrlFor: ...@@ -99,8 +112,10 @@ class UrlFor:
self.parameters = parameters self.parameters = parameters
def __repr__(self): def __repr__(self):
return "UrlFor(file={file}, name={name}, parameters={parameters})".format( return (
file=self.file, name=self.name, parameters=self.parameters) "UrlFor(file={file}, name={name}, parameters={parameters})".format(
file=self.file, name=self.name, parameters=self.parameters))
routes = {} routes = {}
url_fors = [] url_fors = []
...@@ -109,24 +124,29 @@ for file in list_dir(ROOT_DIR): ...@@ -109,24 +124,29 @@ for file in list_dir(ROOT_DIR):
content = infile.read() content = infile.read()
for match in re.finditer(ROUTE_PATTERN, content): for match in re.finditer(ROUTE_PATTERN, content):
name = match.group("name") 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")) url_parameters = split_url_parameters(match.group("url"))
routes[name] = Route(file, name, url_parameters) routes[name] = Route(file, name, url_parameters)
for match in re.finditer(URL_FOR_PATTERN, content): for match in re.finditer(URL_FOR_PATTERN, content):
name = match.group("name") name = match.group("name")
begin, end = match.span() begin, end = match.span()
parameters = read_url_for_parameters(content[end:]) 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: for url_for in url_fors:
if url_for.name not in routes: 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 continue
route = routes[url_for.name] route = routes[url_for.name]
route_parameters = route.get_parameter_set() route_parameters = route.get_parameter_set()
url_parameters = set(url_for.parameters) url_parameters = set(url_for.parameters)
if len(route_parameters ^ url_parameters) > 0: 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_route = route_parameters - url_parameters
only_url = url_parameters - route_parameters only_url = url_parameters - route_parameters
if len(only_route) > 0: if len(only_route) > 0:
......
from flask import redirect, flash, request, url_for from flask import flash
from functools import wraps from functools import wraps
from models.database import ALL_MODELS from models.database import ALL_MODELS
from shared import db, current_user from shared import current_user
import back import back
ID_KEY = "id" ID_KEY = "id"
...@@ -12,12 +12,15 @@ OBJECT_DOES_NOT_EXIST_MESSAGE = "There is no {} with id {}." ...@@ -12,12 +12,15 @@ OBJECT_DOES_NOT_EXIST_MESSAGE = "There is no {} with id {}."
MISSING_VIEW_RIGHT = "Dir fehlenden die nötigen Zugriffsrechte." MISSING_VIEW_RIGHT = "Dir fehlenden die nötigen Zugriffsrechte."
def default_redirect(): def default_redirect():
return back.redirect() return back.redirect()
def login_redirect(): def login_redirect():
return back.redirect("login") return back.redirect("login")
def db_lookup(*models, check_exists=True): def db_lookup(*models, check_exists=True):
def _decorator(function): def _decorator(function):
@wraps(function) @wraps(function)
...@@ -32,7 +35,8 @@ def db_lookup(*models, check_exists=True): ...@@ -32,7 +35,8 @@ def db_lookup(*models, check_exists=True):
obj = model.query.filter_by(id=obj_id).first() obj = model.query.filter_by(id=obj_id).first()
if check_exists and obj is None: if check_exists and obj is None:
model_name = model.__class__.__name__ 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") "alert-error")
return default_redirect() return default_redirect()
kwargs[key] = obj kwargs[key] = obj
...@@ -41,8 +45,10 @@ def db_lookup(*models, check_exists=True): ...@@ -41,8 +45,10 @@ def db_lookup(*models, check_exists=True):
return _decorated_function return _decorated_function
return _decorator return _decorator
def require_right(right, require_exist): def require_right(right, require_exist):
necessary_right_name = "has_{}_right".format(right) necessary_right_name = "has_{}_right".format(right)
def _decorator(function): def _decorator(function):
@wraps(function) @wraps(function)
def _decorated_function(*args, **kwargs): def _decorated_function(*args, **kwargs):
...@@ -65,17 +71,22 @@ def require_right(right, require_exist): ...@@ -65,17 +71,22 @@ def require_right(right, require_exist):
return _decorated_function return _decorated_function
return _decorator return _decorator
def require_public_view_right(require_exist=True): def require_public_view_right(require_exist=True):
return require_right("public_view", require_exist) return require_right("public_view", require_exist)
def require_private_view_right(require_exist=True): def require_private_view_right(require_exist=True):
return require_right("private_view", require_exist) return require_right("private_view", require_exist)
def require_modify_right(require_exist=True): def require_modify_right(require_exist=True):
return require_right("modify", require_exist) return require_right("modify", require_exist)
def require_publish_right(require_exist=True): def require_publish_right(require_exist=True):
return require_right("publish", require_exist) return require_right("publish", require_exist)
def require_admin_right(require_exist=True): def require_admin_right(require_exist=True):
return require_right("admin", require_exist) return require_right("admin", require_exist)
from datetime import datetime from datetime import datetime
from fuzzywuzzy import fuzz, process from fuzzywuzzy import process
import tempfile
from models.database import Todo, OldTodo, Protocol, ProtocolType, TodoMail from models.database import OldTodo, Protocol, ProtocolType, TodoMail
from shared import db