Commit 2cdf562e authored by Robin Sonnabend's avatar Robin Sonnabend
Browse files

More legacy, can now import every old protocol

parent 58a13bf0
...@@ -3,6 +3,7 @@ import random ...@@ -3,6 +3,7 @@ import random
import quopri import quopri
from caldav import DAVClient, Principal, Calendar, Event from caldav import DAVClient, Principal, Calendar, Event
from caldav.lib.error import PropfindError
from vobject.base import ContentLine from vobject.base import ContentLine
import config import config
...@@ -14,17 +15,33 @@ class Client: ...@@ -14,17 +15,33 @@ class Client:
def __init__(self, calendar=None, url=None): def __init__(self, calendar=None, url=None):
self.url = url if url is not None else config.CALENDAR_URL self.url = url if url is not None else config.CALENDAR_URL
self.client = DAVClient(self.url) self.client = DAVClient(self.url)
self.principal = None
for _ in range(config.CALENDAR_MAX_REQUESTS):
try:
self.principal = self.client.principal() self.principal = self.client.principal()
break
except PropfindError as exc:
print(exc)
if self.principal is None:
raise CalendarException("Got {} PropfindErrors 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:
self.calendar = calendar self.calendar = calendar
def get_calendars(self): def get_calendars(self):
if not config.CALENDAR_ACTIVE:
return
for _ in range(config.CALENDAR_MAX_REQUESTS):
try:
return [ return [
calendar.name calendar.name
for calendar in self.principal.calendars() for calendar in self.principal.calendars()
] ]
except PropfindError as exc:
print(exc)
raise CalendarException("Got {} PropfindErrors 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()
...@@ -34,6 +51,8 @@ class Client: ...@@ -34,6 +51,8 @@ class Client:
raise CalendarException("No calendar named {}.".format(calendar_name)) raise CalendarException("No calendar named {}.".format(calendar_name))
def set_event_at(self, begin, name, description): def set_event_at(self, begin, name, description):
if not config.CALENDAR_ACTIVE:
return
candidates = [ candidates = [
Event.from_raw_event(raw_event) Event.from_raw_event(raw_event)
for raw_event in self.calendar.date_search(begin) for raw_event in self.calendar.date_search(begin)
...@@ -126,10 +145,3 @@ def get_timezone_offset(): ...@@ -126,10 +145,3 @@ def get_timezone_offset():
def encode_quopri(text): def encode_quopri(text):
return quopri.encodestring(text.encode("utf-8")).replace(b"\n", b"=0D=0A").decode("utf-8") return quopri.encodestring(text.encode("utf-8")).replace(b"\n", b"=0D=0A").decode("utf-8")
def main():
client = Client("Protokolltest")
client.set_event_at(datetime(2017, 2, 27, 19, 0), "FSS", "Tagesordnung\nTOP 1")
if __name__ == "__main__":
if config.CALENDAR_ACTIVE:
main()
...@@ -85,7 +85,7 @@ ADMIN_MAIL = "admin@example.com" ...@@ -85,7 +85,7 @@ ADMIN_MAIL = "admin@example.com"
PARSER_LAZY = False PARSER_LAZY = False
# minimum similarity (0-100) todos need to have to be considered equal # minimum similarity (0-100) todos need to have to be considered equal
FUZZY_MIN_SCORE = 50 FUZZY_MIN_SCORE = 90
# choose something nice from fc-list # choose something nice from fc-list
# Nimbus Sans looks very much like Computer Modern # Nimbus Sans looks very much like Computer Modern
...@@ -122,3 +122,9 @@ DOCUMENTS_PATH = "documents" ...@@ -122,3 +122,9 @@ DOCUMENTS_PATH = "documents"
# keywords indicating private protocol parts # keywords indicating private protocol parts
PRIVATE_KEYWORDS = ["private", "internal", "privat", "intern"] PRIVATE_KEYWORDS = ["private", "internal", "privat", "intern"]
LATEX_BULLETPOINTS = [
r"\textbullet",
r"\normalfont \bfseries \textendash",
r"\textasteriskcentered",
r"\textperiodcentered"
]
from models.database import Todo, OldTodo from datetime import datetime
from fuzzywuzzy import fuzz, process from fuzzywuzzy import fuzz, process
import tempfile
from models.database import Todo, OldTodo, Protocol, ProtocolType
from shared import db from shared import db
import config import config
def log_fuzzy(text):
with tempfile.NamedTemporaryFile(delete=False, mode="w") as tmpfile:
tmpfile.write(text + "\n\n")
print(text)
def lookup_todo_id(old_candidates, new_who, new_description): def lookup_todo_id(old_candidates, new_who, new_description):
# Check for perfect matches # Check for perfect matches
for candidate in old_candidates: for candidate in old_candidates:
...@@ -22,25 +29,62 @@ def lookup_todo_id(old_candidates, new_who, new_description): ...@@ -22,25 +29,62 @@ def lookup_todo_id(old_candidates, new_who, new_description):
best_match, best_match_score = process.extractOne( best_match, best_match_score = process.extractOne(
new_description, content_to_number.keys()) new_description, content_to_number.keys())
if best_match_score >= config.FUZZY_MIN_SCORE: if best_match_score >= config.FUZZY_MIN_SCORE:
print("Used fuzzy matching on '{}', got '{}' with score {}.".format( log_fuzzy("Used fuzzy matching on '{}', got '{}' with score {}.".format(
new_description, best_match, best_match_score)) new_description, best_match, best_match_score))
return content_to_number[best_match] return content_to_number[best_match]
else: else:
print("Best match for '{}' is '{}' with score {}, rejecting.".format( log_fuzzy("Best match for '{}' is '{}' with score {}, rejecting.".format(
new_description, best_match, best_match_score)) new_description, best_match, best_match_score))
return None return None
INSERT_PROTOCOLTYPE = "INSERT INTO `protocolManager_protocoltype`"
INSERT_PROTOCOL = "INSERT INTO `protocolManager_protocol`"
INSERT_TODO = "INSERT INTO `protocolManager_todo`"
def import_old_protocols(sql_text):
protocoltype_lines = []
protocol_lines = []
for line in sql_text.splitlines():
if line.startswith(INSERT_PROTOCOLTYPE):
protocoltype_lines.append(line)
elif line.startswith(INSERT_PROTOCOL):
protocol_lines.append(line)
if (len(protocoltype_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):
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):
date = datetime.strptime(date, "%Y-%m-%d")
handle = type_id_to_handle[int(old_type_id)]
type = ProtocolType.query.filter(ProtocolType.short_name.ilike(handle)).first()
if type is None:
raise KeyError("No protocoltype for handle '{}'.".format(handle))
protocol = Protocol(type.id, date, source=source)
db.session.add(protocol)
db.session.commit()
import tasks
protocols.append(protocol)
for protocol in sorted(protocols, key=lambda p: p.date):
print(protocol.date)
tasks.parse_protocol(protocol)
def import_old_todos(sql_text): def import_old_todos(sql_text):
protocoltype_lines = [] protocoltype_lines = []
protocol_lines = [] protocol_lines = []
todo_lines = [] todo_lines = []
for line in sql_text.splitlines(): for line in sql_text.splitlines():
if line.startswith("INSERT INTO `protocolManager_protocoltype`"): if line.startswith(INSERT_PROTOCOLTYPE):
protocoltype_lines.append(line) protocoltype_lines.append(line)
elif line.startswith("INSERT INTO `protocolManager_protocol`"): elif line.startswith(INSERT_PROTOCOL):
protocol_lines.append(line) protocol_lines.append(line)
elif line.startswith("INSERT INTO `protocolManager_todo`"): elif line.startswith(INSERT_TODO):
todo_lines.append(line) todo_lines.append(line)
if (len(protocoltype_lines) == 0 if (len(protocoltype_lines) == 0
or len(protocol_lines) == 0 or len(protocol_lines) == 0
...@@ -125,6 +169,15 @@ def _split_base_level(text, begin="(", end=")", separator=",", string_terminator ...@@ -125,6 +169,15 @@ def _split_base_level(text, begin="(", end=")", separator=",", string_terminator
escaped = False escaped = False
for char in part: for char in part:
if escaped: if escaped:
if char == "n":
current_field += "\n"
elif char == "r":
current_field += "\r"
elif char == "t":
current_field += "\t"
else:
if char not in "\"'\\":
print("escaped char: '{}'".format(char))
current_field += char current_field += char
escaped = False escaped = False
elif in_string: elif in_string:
......
...@@ -43,7 +43,7 @@ class ProtocolType(db.Model): ...@@ -43,7 +43,7 @@ class ProtocolType(db.Model):
def __init__(self, name, short_name, organization, usual_time, def __init__(self, name, short_name, organization, usual_time,
is_public, private_group, public_group, private_mail, public_mail, is_public, private_group, public_group, private_mail, public_mail,
use_wiki, wiki_category, wiki_only_public, printer): use_wiki, wiki_category, wiki_only_public, printer, calendar):
self.name = name self.name = name
self.short_name = short_name self.short_name = short_name
self.organization = organization self.organization = organization
...@@ -416,6 +416,44 @@ class TodoState(Enum): ...@@ -416,6 +416,44 @@ class TodoState(Enum):
raise ValueError("Unknown state: '{}'".format(name)) raise ValueError("Unknown state: '{}'".format(name))
return NAME_TO_STATE[name] return NAME_TO_STATE[name]
@staticmethod
def from_name_lazy(name):
name = name.strip().lower()
STATE_TO_NAME, NAME_TO_STATE = make_states(TodoState)
for key in NAME_TO_STATE:
if name.startswith(key):
return NAME_TO_STATE[key]
raise ValueError("{} does not start with a state.".format(name))
@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))
name_part, date_part = name.split(" ", 1)
state = TodoState.from_name(name_part)
date = None
last_exc = None
formats = [("%d.%m.%Y", False)]
if config.PARSER_LAZY:
formats.extend([("%d.%m.", True), ("%d.%m", True)])
for format, year_missing in formats:
try:
date = datetime.strptime(date_part.strip(), format).date()
if year_missing:
year = datetime.now().year
if protocol is not None:
year = protocol.date.year
date = datetime(year=year, month=date.month, day=date.day).date()
break
except ValueError as exc:
last_exc = exc
continue
if date is None:
raise last_exc
return state, date
class Todo(db.Model): class Todo(db.Model):
__tablename__ = "todos" __tablename__ = "todos"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
...@@ -443,9 +481,9 @@ class Todo(db.Model): ...@@ -443,9 +481,9 @@ class Todo(db.Model):
def is_done(self): def is_done(self):
if self.state.needs_date(): if self.state.needs_date():
if self.state == TodoState.after: if self.state == TodoState.after:
return datetime.now().date() >= self.date
elif self.state == TodoState.before:
return datetime.now().date() <= self.date return datetime.now().date() <= self.date
elif self.state == TodoState.before:
return datetime.now().date() >= self.date
return self.state.is_done() return self.state.is_done()
def get_id(self): def get_id(self):
......
...@@ -156,7 +156,7 @@ class Content(Element): ...@@ -156,7 +156,7 @@ class Content(Element):
# v2: does not require the semicolon, but the newline # v2: does not require the semicolon, but the newline
#PATTERN = r"\s*(?<content>(?:[^\[\];\r\n]+)?(?:\[[^\]\r\n]+\][^;\[\]\r\n]*)*);?" #PATTERN = r"\s*(?<content>(?:[^\[\];\r\n]+)?(?:\[[^\]\r\n]+\][^;\[\]\r\n]*)*);?"
# v3: does not allow braces in the content # v3: does not allow braces in the content
PATTERN = r"\s*(?<content>(?:[^\[\];\r\n{}]+)?(?:\[[^\]\r\n]+\][^;\[\]\r\n]*)*);?" PATTERN = r"\s*(?<content>(?:[^\[\];\r\n{}]+)?(?:\[[^\]\r\n{}]+\][^;\[\]\r\n{}]*)*);?"
class Text: class Text:
def __init__(self, text, linenumber, fork): def __init__(self, text, linenumber, fork):
...@@ -188,7 +188,10 @@ class Text: ...@@ -188,7 +188,10 @@ class Text:
raise ParserException("Text is empty!", linenumber) raise ParserException("Text is empty!", linenumber)
return Text(content, linenumber, current) return Text(content, linenumber, current)
PATTERN = r"(?<text>[^\[]+)(?:(?=\[)|$)" # 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>\[?[^\[{}]+)(?:(?=\[)|$)"
class Tag: class Tag:
...@@ -206,7 +209,7 @@ class Tag: ...@@ -206,7 +209,7 @@ class Tag:
if not show_private: if not show_private:
return "" return ""
return self.todo.render_latex(current_protocol=protocol) return self.todo.render_latex(current_protocol=protocol)
return r"\textbf{{{}:}} {}".format(escape_tex(self.name.capitalize()), escape_tex(self.values[0])) return r"\textbf{{{}:}} {}".format(escape_tex(self.name.capitalize()), escape_tex(";".join(self.values)))
elif render_type == RenderType.plaintext: elif render_type == RenderType.plaintext:
if self.name == "url": if self.name == "url":
return self.values[0] return self.values[0]
...@@ -214,7 +217,7 @@ class Tag: ...@@ -214,7 +217,7 @@ class Tag:
if not show_private: if not show_private:
return "" return ""
return self.values[0] return self.values[0]
return "{}: {}".format(self.name.capitalize(), self.values[0]) return "{}: {}".format(self.name.capitalize(), ";".join(self.values))
elif render_type == RenderType.wikitext: elif render_type == RenderType.wikitext:
if self.name == "url": if self.name == "url":
return "[{0} {0}]".format(self.values[0]) return "[{0} {0}]".format(self.values[0])
...@@ -222,7 +225,7 @@ class Tag: ...@@ -222,7 +225,7 @@ class Tag:
if not show_private: if not show_private:
return "" return ""
return self.todo.render_wikitext(current_protocol=protocol) return self.todo.render_wikitext(current_protocol=protocol)
return "'''{}:''' {}".format(self.name.capitalize(), self.values[0]) return "'''{}:''' {}".format(self.name.capitalize(), ";".join(self.values))
else: else:
raise _not_implemented(self, render_type) raise _not_implemented(self, render_type)
...@@ -241,7 +244,11 @@ class Tag: ...@@ -241,7 +244,11 @@ class Tag:
parts = content.split(";") parts = content.split(";")
return Tag(parts[0], parts[1:], linenumber, current) return Tag(parts[0], parts[1:], linenumber, current)
PATTERN = r"\[(?<content>(?:[^;\]]*;)*(?:[^;\]]*))\]" # v1: matches [text without semicolons]
#PATTERN = r"\[(?<content>(?:[^;\]]*;)*(?:[^;\]]*))\]"
# v2: needs at least two parts separated by a semicolon
PATTERN = r"\[(?<content>(?:[^;\]]*;)+(?:[^;\]]*))\]"
class Empty(Element): class Empty(Element):
def __init__(self, linenumber): def __init__(self, linenumber):
...@@ -301,8 +308,8 @@ class Remark(Element): ...@@ -301,8 +308,8 @@ class Remark(Element):
PATTERN = r"\s*\#(?<content>[^\n]+)" PATTERN = r"\s*\#(?<content>[^\n]+)"
class Fork(Element): class Fork(Element):
def __init__(self, environment, name, parent, linenumber, children=None): def __init__(self, is_top, name, parent, linenumber, children=None):
self.environment = environment if environment is None or len(environment) > 0 else 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 is not None and len(name) > 0) else None
self.parent = parent self.parent = parent
self.linenumber = linenumber self.linenumber = linenumber
...@@ -311,22 +318,19 @@ class Fork(Element): ...@@ -311,22 +318,19 @@ class Fork(Element):
def dump(self, level=None): def dump(self, level=None):
if level is None: if level is None:
level = 0 level = 0
result_lines = ["{}fork: {}".format(INDENT_LETTER * level, self.name)] result_lines = ["{}fork: {}'{}'".format(INDENT_LETTER * level, "TOP " if self.is_top else "", self.name)]
for child in self.children: for child in self.children:
result_lines.append(child.dump(level + 1)) result_lines.append(child.dump(level + 1))
return "\n".join(result_lines) return "\n".join(result_lines)
def test_private(self, name): def test_private(self, name):
if name is None:
return False
stripped_name = name.replace(":", "").strip() stripped_name = name.replace(":", "").strip()
return stripped_name in config.PRIVATE_KEYWORDS return stripped_name in config.PRIVATE_KEYWORDS
def render(self, render_type, show_private, level, protocol=None): def render(self, render_type, show_private, level, protocol=None):
name_parts = [] name_line = escape_tex(self.name if self.name is not None else "")
if self.environment is not None:
name_parts.append(self.environment)
if self.name is not None:
name_parts.append(self.name)
name_line = " ".join(name_parts)
if level == 0 and self.name == "Todos" and not show_private: if level == 0 and self.name == "Todos" and not show_private:
return "" return ""
if render_type == RenderType.latex: if render_type == RenderType.latex:
...@@ -341,6 +345,8 @@ class Fork(Element): ...@@ -341,6 +345,8 @@ class Fork(Element):
part = r"\item {}".format(part) part = r"\item {}".format(part)
content_parts.append(part) content_parts.append(part)
content_lines = "\n".join(content_parts) content_lines = "\n".join(content_parts)
if len(content_lines.strip()) == 0:
content_lines = "\\item Nichts\n"
if level == 0: if level == 0:
return "\n".join([begin_line, content_lines, end_line]) return "\n".join([begin_line, content_lines, end_line])
elif self.test_private(self.name): elif self.test_private(self.name):
...@@ -388,7 +394,7 @@ class Fork(Element): ...@@ -388,7 +394,7 @@ class Fork(Element):
return tags return tags
def is_anonymous(self): def is_anonymous(self):
return self.environment == None return self.name == None
def is_root(self): def is_root(self):
return self.parent is None return self.parent is None
...@@ -398,6 +404,17 @@ class Fork(Element): ...@@ -398,6 +404,17 @@ class Fork(Element):
return self return self
return self.parent.get_top() return self.parent.get_top()
def get_maxdepth(self):
child_depths = [
child.get_maxdepth()
for child in self.children
if isinstance(child, Fork)
]
if len(child_depths) > 0:
return max(child_depths) + 1
else:
return 1
@staticmethod @staticmethod
def create_root(): def create_root():
return Fork(None, None, None, 0) return Fork(None, None, None, 0)
...@@ -405,18 +422,13 @@ class Fork(Element): ...@@ -405,18 +422,13 @@ class Fork(Element):
@staticmethod @staticmethod
def parse(match, current, linenumber=None): def parse(match, current, linenumber=None):
linenumber = Element.parse_inner(match, current, linenumber) linenumber = Element.parse_inner(match, current, linenumber)
environment = match.group("environment") topname = match.group("topname")
name1 = match.group("name1") name = match.group("name")
name2 = match.group("name2") is_top = False
name = "" if topname is not None:
if name1 is not None: is_top = True
name = name1 name = topname
if name2 is not None: element = Fork(is_top, name, current, linenumber)
if len(name) > 0:
name += " {}".format(name2)
else:
name = name2
element = Fork(environment, name, current, linenumber)
current = Element.parse_outer(element, current) current = Element.parse_outer(element, current)
return current, linenumber return current, linenumber
...@@ -434,7 +446,11 @@ class Fork(Element): ...@@ -434,7 +446,11 @@ class Fork(Element):
# v1: has a problem with old protocols that do not use a lot of semicolons # v1: has a problem with old protocols that do not use a lot of semicolons
#PATTERN = r"\s*(?<name1>[^{};]+)?{(?<environment>\S+)?\h*(?<name2>[^\n]+)?" #PATTERN = r"\s*(?<name1>[^{};]+)?{(?<environment>\S+)?\h*(?<name2>[^\n]+)?"
# v2: do not allow newlines in name1 or semicolons in name2 # v2: do not allow newlines in name1 or semicolons in name2
PATTERN = r"\s*(?<name1>[^{};\n]+)?{(?<environment>\S+)?\h*(?<name2>[^;\n]+)?" #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]+))?"
END_PATTERN = r"\s*};?" END_PATTERN = r"\s*};?"
PATTERNS = OrderedDict([ PATTERNS = OrderedDict([
...@@ -446,8 +462,8 @@ PATTERNS = OrderedDict([ ...@@ -446,8 +462,8 @@ PATTERNS = OrderedDict([
]) ])
TEXT_PATTERNS = OrderedDict([ TEXT_PATTERNS = OrderedDict([
(re.compile(Text.PATTERN), Text.parse), (re.compile(Tag.PATTERN), Tag.parse),
(re.compile(Tag.PATTERN), Tag.parse) (re.compile(Text.PATTERN), Text.parse)
]) ])
def parse(source): def parse(source):
...@@ -460,11 +476,15 @@ def parse(source): ...@@ -460,11 +476,15 @@ def parse(source):
match = pattern.match(source) match = pattern.match(source)
if match is not None: if match is not None:
source = source[len(match.group()):] 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 found = True
break break
if not found: if not found:
raise ParserException("No matching syntax element found!", linenumber) raise ParserException("No matching syntax element found!", linenumber, tree=tree)
if current is not tree: if current is not tree:
raise ParserException("Source ended within fork! (started at line {})".format(current.linenumber), linenumber=current.linenumber, tree=tree) raise ParserException("Source ended within fork! (started at line {})".format(current.linenumber), linenumber=current.linenumber, tree=tree)
return tree return tree
......
...@@ -24,7 +24,7 @@ from utils import is_past, mail_manager, url_manager, get_first_unused_int, set_ ...@@ -24,7 +24,7 @@ from utils import is_past, mail_manager, url_manager, get_first_unused_int, set_
from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail, DecisionDocument, TodoState from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail, DecisionDocument, TodoState
from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, ProtocolForm, TopForm, SearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, ProtocolForm, TopForm, SearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm
from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable, TodoMailsTable from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable, TodoMailsTable
from legacy import import_old_todos from legacy import import_old_todos, import_old_protocols