database.py 26.7 KB
Newer Older
1 2
from flask import render_template, send_file, url_for, redirect, flash, request

Robin Sonnabend's avatar
Robin Sonnabend committed
3
from datetime import datetime, time, date, timedelta
4
import math
5
from io import StringIO, BytesIO
6
from enum import Enum
7
from uuid import uuid4
8

9
from shared import db, date_filter, date_filter_short, escape_tex, DATE_KEY, START_TIME_KEY, END_TIME_KEY, current_user
10
from utils import random_string, url_manager, get_etherpad_url, split_terms, check_ip_in_networks
Robin Sonnabend's avatar
Robin Sonnabend committed
11
from models.errors import DateNotMatchingException
12

13
import os
14

15 16
from sqlalchemy import event
from sqlalchemy.orm import relationship, backref, sessionmaker
Robin Sonnabend's avatar
Robin Sonnabend committed
17
from sqlalchemy.ext.hybrid import hybrid_method
18

Robin Sonnabend's avatar
Robin Sonnabend committed
19
import config
20
from todostates import make_states
Robin Sonnabend's avatar
Robin Sonnabend committed
21

Robin Sonnabend's avatar
Robin Sonnabend committed
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
class DatabaseModel(db.Model):
    __abstract__ = True

    def has_public_view_right(self, user):
        return self.get_parent().has_public_view_right(user)

    def has_private_view_right(self, user):
        return self.get_parent().has_private_view_right(user)

    def has_modify_right(self, user):
        return self.get_parent().has_modify_right(user)

    def has_admin_right(self, user):
        return self.get_parent().has_admin_right(user)

37 38 39 40 41 42 43 44 45 46
    def __repr__(self):
        columns = []
        for column in self.__table__.columns:
            column_name = column.key
            value = getattr(self, column_name)
            if isinstance(value, str):
                value = "'" + value + "'"
            columns.append("{}={}".format(column_name, value))
        return "{}({})".format(self.__class__.__name__, ", ".join(columns))

Robin Sonnabend's avatar
Robin Sonnabend committed
47
class ProtocolType(DatabaseModel):
48
    __tablename__ = "protocoltypes"
Robin Sonnabend's avatar
Robin Sonnabend committed
49
    __model_name__ = "protocoltype"
50 51 52 53
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String, unique=True)
    short_name = db.Column(db.String, unique=True)
    organization = db.Column(db.String)
Robin Sonnabend's avatar
Robin Sonnabend committed
54
    usual_time = db.Column(db.Time)
55
    is_public = db.Column(db.Boolean)
56
    modify_group = db.Column(db.String)
57 58 59 60
    private_group = db.Column(db.String)
    public_group = db.Column(db.String)
    private_mail = db.Column(db.String)
    public_mail = db.Column(db.String)
61
    non_reproducible_pad_links = db.Column(db.Boolean)
Robin Sonnabend's avatar
Robin Sonnabend committed
62 63 64
    use_wiki = db.Column(db.Boolean)
    wiki_category = db.Column(db.String)
    wiki_only_public = db.Column(db.Boolean)
Robin Sonnabend's avatar
Robin Sonnabend committed
65
    printer = db.Column(db.String)
Robin Sonnabend's avatar
Robin Sonnabend committed
66
    calendar = db.Column(db.String)
67 68
    restrict_networks = db.Column(db.Boolean)
    allowed_networks = db.Column(db.String)
69 70 71

    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")
72
    reminders = relationship("MeetingReminder", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="MeetingReminder.days_before")
Robin Sonnabend's avatar
Robin Sonnabend committed
73
    todos = relationship("Todo", backref=backref("protocoltype"), order_by="Todo.id")
Robin Sonnabend's avatar
Robin Sonnabend committed
74
    metas = relationship("DefaultMeta", backref=backref("protocoltype"), cascade="all, delete-orphan")
75
    decisioncategories = relationship("DecisionCategory", backref=backref("protocoltype"), cascade="all, delete-orphan")
76

Robin Sonnabend's avatar
Robin Sonnabend committed
77
    def get_latest_protocol(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
78
        candidates = sorted([protocol for protocol in self.protocols if protocol.is_done()], key=lambda p: p.date, reverse=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
79 80 81 82
        if len(candidates) == 0:
            return None
        return candidates[0]

83 84
    def has_public_view_right(self, user, check_networks=True):
        return (self.has_public_anonymous_view_right(check_networks=check_networks)
Robin Sonnabend's avatar
Robin Sonnabend committed
85 86
            or (user is not None and self.has_public_authenticated_view_right(user))
            or self.has_admin_right(user))
87

88
    def has_public_anonymous_view_right(self, check_networks=True):
Robin Sonnabend's avatar
Robin Sonnabend committed
89
        return (self.is_public
90
            and ((not self.restrict_networks or not check_networks)
91 92 93 94 95
                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))
Robin Sonnabend's avatar
Robin Sonnabend committed
96 97

    def has_private_view_right(self, user):
Robin Sonnabend's avatar
Robin Sonnabend committed
98 99 100
        return ((user is not None
            and (self.private_group != "" and self.private_group in user.groups))
            or self.has_admin_right(user))
Robin Sonnabend's avatar
Robin Sonnabend committed
101 102

    def has_modify_right(self, user):
Robin Sonnabend's avatar
Robin Sonnabend committed
103 104 105
        return ((user is not None
            and (self.modify_group != "" and self.modify_group in user.groups))
            or self.has_admin_right(user))
Robin Sonnabend's avatar
Robin Sonnabend committed
106

107 108 109
    def has_admin_right(self, user):
        return (user is not None and config.ADMIN_GROUP in user.groups)

Robin Sonnabend's avatar
Robin Sonnabend committed
110
    @staticmethod
Robin Sonnabend's avatar
Robin Sonnabend committed
111
    def get_modifiable_protocoltypes(user):
Robin Sonnabend's avatar
Robin Sonnabend committed
112 113 114 115 116
        return [
            protocoltype for protocoltype in ProtocolType.query.all()
            if protocoltype.has_modify_right(user)
        ]

Robin Sonnabend's avatar
Robin Sonnabend committed
117
    @staticmethod
118
    def get_public_protocoltypes(user, check_networks=True):
Robin Sonnabend's avatar
Robin Sonnabend committed
119 120
        return [
            protocoltype for protocoltype in ProtocolType.query.all()
121
            if protocoltype.has_public_view_right(user, check_networks=check_networks)
Robin Sonnabend's avatar
Robin Sonnabend committed
122 123 124 125 126 127 128 129 130
        ]

    @staticmethod
    def get_private_protocoltypes(user):
        return [
            protocoltype for protocoltype in ProtocolType.query.all()
            if protocoltype.has_private_view_right(user)
        ]

131 132 133 134 135 136
    def get_wiki_infobox(self):
        return "Infobox {}".format(self.short_name)

    def get_wiki_infobox_title(self):
        return "Vorlage:{}".format(self.get_wiki_infobox())

137

Robin Sonnabend's avatar
Robin Sonnabend committed
138
class Protocol(DatabaseModel):
139
    __tablename__ = "protocols"
Robin Sonnabend's avatar
Robin Sonnabend committed
140
    __model_name__ = "protocol"
141 142
    id = db.Column(db.Integer, primary_key=True)
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
143 144 145
    source = db.Column(db.String)
    content_public = db.Column(db.String)
    content_private = db.Column(db.String)
146 147 148
    date = db.Column(db.Date)
    start_time = db.Column(db.Time)
    end_time = db.Column(db.Time)
149
    done = db.Column(db.Boolean, nullable=False, default=False)
150
    public = db.Column(db.Boolean)
151
    pad_identifier = db.Column(db.String)
152 153 154 155 156

    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")
Robin Sonnabend's avatar
Robin Sonnabend committed
157
    metas = relationship("Meta", backref=backref("protocol"), cascade="all, delete-orphan")
158
    localtops = relationship("LocalTOP", backref=backref("protocol"), cascade="all, delete-orphan")
159

Robin Sonnabend's avatar
Robin Sonnabend committed
160 161
    likes = relationship("Like", secondary="likeprotocolassociations")

Robin Sonnabend's avatar
Robin Sonnabend committed
162 163 164
    def get_parent(self):
        return self.protocoltype

165 166
    def create_error(self, action, name, description):
        now = datetime.now()
167 168
        return Error(protocol_id=self.id, action=action, name=name,
            datetime=now, description=description)
169

170 171 172
    def create_localtops(self):
        local_tops = []
        for default_top in self.protocoltype.default_tops:
173
            local_tops.append(LocalTOP(defaulttop_id=default_top.id,
174
                protocol_id=self.id, description=default_top.description or ""))
175 176
        return local_tops

177
    def fill_from_remarks(self, remarks):
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
        def _date_or_lazy(key, get_date=False, get_time=False):
            formats = []
            if get_date:
                formats.append("%d.%m.%Y")
            if get_time:
                formats.append("%H:%M")
            format = " ".join(formats)
            try:
                date = datetime.strptime(remarks[key].value.strip(), format)
                if (get_time and get_date) or (not get_time and not get_date):
                    return date
                elif get_time:
                    return date.time()
                elif get_date:
                    return date.date()
            except ValueError as exc:
                if config.PARSER_LAZY:
                    return None
                raise exc
        if DATE_KEY in remarks:
            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)
            else:
                self.date = new_date
        if START_TIME_KEY in remarks:
            self.start_time = _date_or_lazy(START_TIME_KEY, get_time=True)
        if END_TIME_KEY in remarks:
            self.end_time = _date_or_lazy(END_TIME_KEY, get_time=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
208 209 210 211 212 213 214
        old_metas = list(self.metas)
        for meta in old_metas:
            db.session.delete(meta)
        db.session.commit()
        for default_meta in self.protocoltype.metas:
            if default_meta.key in remarks:
                value = remarks[default_meta.key].value.strip()
215
                meta = Meta(protocol_id=self.id, name=default_meta.name, value=value, internal=default_meta.internal)
Robin Sonnabend's avatar
Robin Sonnabend committed
216 217
                db.session.add(meta)
        db.session.commit()
218

219 220 221 222 223 224
    def has_public_view_right(self, user):
        return (
            (self.public and self.protocoltype.has_public_view_right(user))
            or self.protocoltype.has_private_view_right(user)
        )

Robin Sonnabend's avatar
Robin Sonnabend committed
225 226 227
    def is_done(self):
        return self.done

Robin Sonnabend's avatar
Robin Sonnabend committed
228
    def get_identifier(self):
Administrator's avatar
Administrator committed
229 230
        if self.pad_identifier is not None:
            return self.pad_identifier
Robin Sonnabend's avatar
Robin Sonnabend committed
231 232
        if self.date is None:
            return None
233 234 235
        return self.get_short_identifier()

    def get_short_identifier(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
236 237 238 239
        return "{}-{}".format(
            self.protocoltype.short_name.lower(),
            self.date.strftime("%y-%m-%d"))

Robin Sonnabend's avatar
Robin Sonnabend committed
240 241 242
    def get_wiki_title(self):
        return "Protokoll:{}-{:%Y-%m-%d}".format(self.protocoltype.short_name, self.date)

Robin Sonnabend's avatar
Robin Sonnabend committed
243
    def get_etherpad_link(self):
Administrator's avatar
Administrator committed
244 245 246
        if self.pad_identifier is None:
            identifier = self.get_identifier()
            if self.protocoltype.non_reproducible_pad_links:
247
                identifier = "{}-{}".format(identifier, str(uuid4()).replace("-", ""))[:50]
Administrator's avatar
Administrator committed
248 249 250
            self.pad_identifier = identifier
            db.session.commit()
        return get_etherpad_url(self.pad_identifier)
Robin Sonnabend's avatar
Robin Sonnabend committed
251

252 253 254 255 256
    def get_time(self):
        if self.start_time is not None:
            return self.start_time
        return self.protocoltype.usual_time

Robin Sonnabend's avatar
Robin Sonnabend committed
257
    def get_datetime(self):
258 259
        time = self.get_time()
        return datetime(self.date.year, self.date.month, self.date.day, time.usual_time.hour, time.usual_time.minute)
Robin Sonnabend's avatar
Robin Sonnabend committed
260

Robin Sonnabend's avatar
Robin Sonnabend committed
261 262 263 264 265
    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()]
266

267 268 269 270 271 272
    def get_open_todos(self):
        return [
            todo for todo in self.protocoltype.todos
            if not todo.is_done()
        ]

Robin Sonnabend's avatar
Robin Sonnabend committed
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
    def has_compiled_document(self):
        candidates = [
            document for document in self.documents
            if document.is_compiled
        ]
        return len(candidates) > 0

    def get_compiled_document(self, private=None):
        candidates = [
            document for document in self.documents
            if document.is_compiled
               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]
        if len(private_candidates) > 0:
            return private_candidates[0]
        elif len(public_candidates) > 0:
            return public_candidates[0]
        return None

Robin Sonnabend's avatar
Robin Sonnabend committed
294 295 296
    def get_template(self):
        return render_template("protocol-template.txt", protocol=self)

Robin Sonnabend's avatar
Robin Sonnabend committed
297 298 299
    def delete_orphan_todos(self):
        orphan_todos = [
            todo for todo in self.todos
Robin Sonnabend's avatar
Robin Sonnabend committed
300
            if len(todo.protocols) <= 1
Robin Sonnabend's avatar
Robin Sonnabend committed
301 302 303 304 305
        ]
        for todo in orphan_todos:
            self.todos.remove(todo)
            db.session.delete(todo)

306 307 308 309 310 311 312 313 314 315 316
    def get_tops(self):
        tops_before, tops_after = [], []
        if not self.has_nonplanned_tops():
            for default_top in self.protocoltype.default_tops:
                top = default_top.get_top(self)
                if default_top.is_at_end():
                    tops_after.append(top)
                else:
                    tops_before.append(top)
        return tops_before + self.tops + tops_after

Robin Sonnabend's avatar
Robin Sonnabend committed
317 318 319 320 321
@event.listens_for(Protocol, "before_delete")
def on_protocol_delete(mapper, connection, protocol):
    protocol.delete_orphan_todos()


Robin Sonnabend's avatar
Robin Sonnabend committed
322
class DefaultTOP(DatabaseModel):
323
    __tablename__ = "defaulttops"
Robin Sonnabend's avatar
Robin Sonnabend committed
324
    __model_name__ = "defaulttop"
325 326 327 328
    id = db.Column(db.Integer, primary_key=True)
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
    name = db.Column(db.String)
    number = db.Column(db.Integer)
329
    description = db.Column(db.String)
330

331 332
    localtops = relationship("LocalTOP", backref=backref("defaulttop"), cascade="all, delete-orphan")

Robin Sonnabend's avatar
Robin Sonnabend committed
333 334 335
    def get_parent(self):
        return self.protocoltype

336
    def is_at_end(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
337
        return self.number > 0
338

339
    def get_localtop(self, protocol):
340
        return LocalTOP.query.filter_by(defaulttop_id=self.id,
341 342 343 344 345 346 347 348
            protocol_id=protocol.id).first()

    def get_top(self, protocol):
        localtop = self.get_localtop(protocol)
        top = TOP(protocol_id=protocol.id, name=self.name,
            description=localtop.description)
        return top

Robin Sonnabend's avatar
Robin Sonnabend committed
349
class TOP(DatabaseModel):
350
    __tablename__ = "tops"
Robin Sonnabend's avatar
Robin Sonnabend committed
351
    __model_name__ = "top"
352 353 354
    id = db.Column(db.Integer, primary_key=True)
    protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
    name = db.Column(db.String)
Robin Sonnabend's avatar
Robin Sonnabend committed
355
    number = db.Column(db.Integer)
356
    planned = db.Column(db.Boolean)
357
    description = db.Column(db.String)
358

Robin Sonnabend's avatar
Robin Sonnabend committed
359 360
    likes = relationship("Like", secondary="liketopassociations")

Robin Sonnabend's avatar
Robin Sonnabend committed
361 362 363
    def get_parent(self):
        return self.protocol

364 365 366 367 368 369 370 371 372 373 374
class LocalTOP(DatabaseModel):
    __tablename__ = "localtops"
    __model_name__ = "localtop"
    id = db.Column(db.Integer, primary_key=True)
    protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
    defaulttop_id = db.Column(db.Integer, db.ForeignKey("defaulttops.id"))
    description = db.Column(db.String)

    def get_parent(self):
        return self.protocol

375 376 377 378 379 380 381 382 383 384 385 386
    def is_expandable(self):
        user = current_user()
        return (self.has_private_view_right(user)
            and self.description is not None
            and len(self.description) > 0)

    def get_css_classes(self):
        classes = ["defaulttop"]
        if self.is_expandable():
            classes.append("expansion-button")
        return classes

Robin Sonnabend's avatar
Robin Sonnabend committed
387
class Document(DatabaseModel):
388
    __tablename__ = "documents"
Robin Sonnabend's avatar
Robin Sonnabend committed
389
    __model_name__ = "document"
390 391 392
    id = db.Column(db.Integer, primary_key=True)
    protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
    name = db.Column(db.String)
Robin Sonnabend's avatar
Robin Sonnabend committed
393
    filename = db.Column(db.String)
394
    is_compiled = db.Column(db.Boolean)
395
    is_private = db.Column(db.Boolean)
396

Robin Sonnabend's avatar
Robin Sonnabend committed
397 398 399
    def get_parent(self):
        return self.protocol

400 401 402
    def get_filename(self):
        return os.path.join(config.DOCUMENTS_PATH, self.filename)

403 404 405 406
    def as_file_like(self):
        with open(self.get_filename(), "rb") as file:
            return BytesIO(file.read())

407
@event.listens_for(Document, "before_delete")
Robin Sonnabend's avatar
Robin Sonnabend committed
408
def on_document_delete(mapper, connection, document):
409
    if document.filename is not None:
Robin Sonnabend's avatar
Robin Sonnabend committed
410
        document_path = document.get_filename()
411 412
        if os.path.isfile(document_path):
            os.remove(document_path)
413

Robin Sonnabend's avatar
Robin Sonnabend committed
414
class DecisionDocument(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
415
    __tablename__ = "decisiondocuments"
Robin Sonnabend's avatar
Robin Sonnabend committed
416
    __model_name__ = "decisiondocument"
Robin Sonnabend's avatar
Robin Sonnabend committed
417 418 419 420 421
    id = db.Column(db.Integer, primary_key=True)
    decision_id = db.Column(db.Integer, db.ForeignKey("decisions.id"))
    name = db.Column(db.String)
    filename = db.Column(db.String)

Robin Sonnabend's avatar
Robin Sonnabend committed
422 423 424
    def get_parent(self):
        return self.decision

Robin Sonnabend's avatar
Robin Sonnabend committed
425 426 427 428 429 430 431 432 433 434 435 436 437 438
    def get_filename(self):
        return os.path.join(config.DOCUMENTS_PATH, self.filename)

    def as_file_like(self):
        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:
        document_path = document.get_filename()
        if os.path.isfile(document_path):
            os.remove(document_path)

439 440 441 442 443 444 445 446 447 448
class TodoState(Enum):
    open = 0
    waiting = 1
    in_progress = 2
    after = 3
    before = 4
    orphan = 5
    done = 6
    rejected = 7
    obsolete = 8
Robin Sonnabend's avatar
Robin Sonnabend committed
449

450 451 452
    def get_name(self):
        STATE_TO_NAME, NAME_TO_STATE = make_states(TodoState)
        return STATE_TO_NAME[self]
Robin Sonnabend's avatar
Robin Sonnabend committed
453 454 455 456 457 458 459 460 461 462
    
    @staticmethod
    def get_name_to_state():
        STATE_TO_NAME, NAME_TO_STATE = make_states(TodoState)
        return NAME_TO_STATE

    @staticmethod
    def get_state_to_name():
        STATE_TO_NAME, NAME_TO_STATE = make_states(TodoState)
        return STATE_TO_NAME
463 464 465 466 467 468 469 470 471 472 473 474 475 476

    def needs_date(self):
        return self in [TodoState.after, TodoState.before]

    def is_done(self):
        return self in [TodoState.done, TodoState.rejected, TodoState.obsolete]

    @staticmethod
    def from_name(name):
        name = name.strip().lower()
        STATE_TO_NAME, NAME_TO_STATE = make_states(TodoState)
        if name not in NAME_TO_STATE:
            raise ValueError("Unknown state: '{}'".format(name))
        return NAME_TO_STATE[name]
Robin Sonnabend's avatar
Robin Sonnabend committed
477

478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515
    @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


Robin Sonnabend's avatar
Robin Sonnabend committed
516
class Todo(DatabaseModel):
517
    __tablename__ = "todos"
Robin Sonnabend's avatar
Robin Sonnabend committed
518
    __model_name__ = "todo"
519
    id = db.Column(db.Integer, primary_key=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
520
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
Robin Sonnabend's avatar
Robin Sonnabend committed
521
    number = db.Column(db.Integer)
522 523
    who = db.Column(db.String)
    description = db.Column(db.String)
524 525
    state = db.Column(db.Enum(TodoState), nullable=False)
    date = db.Column(db.Date, nullable=True)
526 527

    protocols = relationship("Protocol", secondary="todoprotocolassociations", backref="todos")
Robin Sonnabend's avatar
Robin Sonnabend committed
528
    likes = relationship("Like", secondary="liketodoassociations")
529

Robin Sonnabend's avatar
Robin Sonnabend committed
530 531 532
    def get_parent(self):
        return self.protocoltype

533
    def is_done(self):
534 535 536
        if self.state.needs_date():
            if self.state == TodoState.after:
                return datetime.now().date() <= self.date
537 538
            elif self.state == TodoState.before:
                return datetime.now().date() >= self.date
539
        return self.state.is_done()
Robin Sonnabend's avatar
Robin Sonnabend committed
540 541 542

    def get_id(self):
        return self.number if self.number is not None else self.id
543

Robin Sonnabend's avatar
Robin Sonnabend committed
544 545 546 547 548 549
    def get_first_protocol(self):
        candidates = sorted(self.protocols, key=lambda p: p.date)
        if len(candidates) == 0:
            return None
        return candidates[0]

Robin Sonnabend's avatar
Robin Sonnabend committed
550 551 552 553 554 555
    def get_users(self):
        return [
            user.lower().strip()
            for user in split_terms(self.who, separators=" ,\t")
        ]

Robin Sonnabend's avatar
Robin Sonnabend committed
556
    def get_state(self):
557
        return "[{}]".format(self.get_state_plain())
Robin Sonnabend's avatar
Robin Sonnabend committed
558
    def get_state_plain(self):
559 560
        result = self.state.get_name()
        if self.state.needs_date():
561
            result = "{} {}".format(result, date_filter_short(self.date))
562
        return result
Robin Sonnabend's avatar
Robin Sonnabend committed
563 564 565 566 567 568 569
    def get_state_tex(self):
        return self.get_state_plain()

    def is_new(self, current_protocol=None):
        if current_protocol is not None:
            return self.get_first_protocol() == current_protocol
        return len(self.protocols) == 1
Robin Sonnabend's avatar
Robin Sonnabend committed
570 571 572 573 574 575 576 577 578

    def render_html(self):
        parts = [
            self.get_state(),
            "<strong>{}:</strong>".format(self.who),
            self.description
        ]
        return " ".join(parts)

Robin Sonnabend's avatar
Robin Sonnabend committed
579 580
    def render_latex(self, current_protocol=None):
        return r"\textbf{{{}}}: {}: {} -- {}".format(
Robin Sonnabend's avatar
Robin Sonnabend committed
581
            "Neuer Todo" if self.is_new(current_protocol) else "Todo",
582 583 584
            escape_tex(self.who),
            escape_tex(self.description),
            escape_tex(self.get_state_tex())
Robin Sonnabend's avatar
Robin Sonnabend committed
585 586
        )

Robin Sonnabend's avatar
Robin Sonnabend committed
587 588 589 590 591 592 593 594
    def render_wikitext(self, current_protocol=None):
        return "'''{}:''' {}: {} - {}".format(
            "Neuer Todo" if self.is_new(current_protocol) else "Todo",
            self.who,
            self.description,
            self.get_state_plain()
        )

595 596 597
    def render_template(self):
        parts = ["todo", self.who, self.description, self.state.get_name()]
        if self.state.needs_date():
598
            parts.append(date_filter(self.date))
599 600 601
        parts.append("id {}".format(self.get_id()))
        return "[{}]".format(";".join(parts))

Robin Sonnabend's avatar
Robin Sonnabend committed
602
class TodoProtocolAssociation(DatabaseModel):
603 604 605 606
    __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)

Robin Sonnabend's avatar
Robin Sonnabend committed
607
class Decision(DatabaseModel):
608
    __tablename__ = "decisions"
Robin Sonnabend's avatar
Robin Sonnabend committed
609
    __model_name__ = "decision"
610 611 612
    id = db.Column(db.Integer, primary_key=True)
    protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
    content = db.Column(db.String)
613
    category_id = db.Column(db.Integer, db.ForeignKey("decisioncategories.id"), nullable=True)
614

Robin Sonnabend's avatar
Robin Sonnabend committed
615 616
    document = relationship("DecisionDocument", backref=backref("decision"), cascade="all, delete-orphan", uselist=False)

Robin Sonnabend's avatar
Robin Sonnabend committed
617 618
    likes = relationship("Like", secondary="likedecisionassociations")

Robin Sonnabend's avatar
Robin Sonnabend committed
619 620 621
    def get_parent(self):
        return self.protocol

622 623 624 625 626 627 628 629 630 631 632 633
class DecisionCategory(DatabaseModel):
    __tablename__ = "decisioncategories"
    __model_name__ = "decisioncategory"
    id = db.Column(db.Integer, primary_key=True)
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
    name = db.Column(db.String)

    decisions = relationship("Decision", backref=backref("category"), order_by="Decision.id")

    def get_parent(self):
        return self.protocoltype

Robin Sonnabend's avatar
Robin Sonnabend committed
634
class MeetingReminder(DatabaseModel):
635
    __tablename__ = "meetingreminders"
Robin Sonnabend's avatar
Robin Sonnabend committed
636
    __model_name__ = "meetingreminder"
637 638
    id = db.Column(db.Integer, primary_key=True)
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
639
    days_before = db.Column(db.Integer)
640 641
    send_public = db.Column(db.Boolean)
    send_private = db.Column(db.Boolean)
Robin Sonnabend's avatar
Robin Sonnabend committed
642
    additional_text = db.Column(db.String)
643

Robin Sonnabend's avatar
Robin Sonnabend committed
644 645 646 647
    def get_parent(self):
        return self.protocoltype

class Error(DatabaseModel):
648
    __tablename__ = "errors"
Robin Sonnabend's avatar
Robin Sonnabend committed
649
    __model_name__ = "error"
650 651 652 653 654 655 656
    id = db.Column(db.Integer, primary_key=True)
    protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
    action = db.Column(db.String)
    name = db.Column(db.String)
    datetime = db.Column(db.DateTime)
    description = db.Column(db.String)

Robin Sonnabend's avatar
Robin Sonnabend committed
657 658 659
    def get_parent(self):
        return self.protocol

Robin Sonnabend's avatar
Robin Sonnabend committed
660 661 662 663
    def get_short_description(self):
        lines = self.description.splitlines()
        if len(lines) <= 4:
            return "\n".join(lines)
Robin Sonnabend's avatar
Robin Sonnabend committed
664
        return "\n".join([*lines[:2], "…", *lines[-2:]])
Robin Sonnabend's avatar
Robin Sonnabend committed
665

Robin Sonnabend's avatar
Robin Sonnabend committed
666
class TodoMail(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
667
    __tablename__ = "todomails"
Robin Sonnabend's avatar
Robin Sonnabend committed
668
    __model_name__ = "todomail"
Robin Sonnabend's avatar
Robin Sonnabend committed
669 670 671 672 673 674
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String, unique=True)
    mail = db.Column(db.String)

    def get_formatted_mail(self):
        return "{} <{}>".format(self.name, self.mail)
675

Robin Sonnabend's avatar
Robin Sonnabend committed
676
class OldTodo(DatabaseModel):
677
    __tablename__ = "oldtodos"
Robin Sonnabend's avatar
Robin Sonnabend committed
678
    __model_name__ = "oldtodo"
679 680 681 682 683 684
    id = db.Column(db.Integer, primary_key=True)
    old_id = db.Column(db.Integer)
    who = db.Column(db.String)
    description = db.Column(db.String)
    protocol_key = db.Column(db.String)

Robin Sonnabend's avatar
Robin Sonnabend committed
685
class DefaultMeta(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
686
    __tablename__ = "defaultmetas"
Robin Sonnabend's avatar
Robin Sonnabend committed
687
    __model_name__ = "defaultmeta"
Robin Sonnabend's avatar
Robin Sonnabend committed
688 689 690 691
    id = db.Column(db.Integer, primary_key=True)
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
    key = db.Column(db.String)
    name = db.Column(db.String)
692
    value = db.Column(db.String)
693
    internal = db.Column(db.Boolean)
694
    prior = db.Column(db.Boolean, default=False, nullable=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
695

Robin Sonnabend's avatar
Robin Sonnabend committed
696 697 698 699
    def get_parent(self):
        return self.protocoltype

class Meta(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
700
    __tablename__ = "metas"
Robin Sonnabend's avatar
Robin Sonnabend committed
701
    __model_name__ = "meta"
Robin Sonnabend's avatar
Robin Sonnabend committed
702 703 704 705
    id = db.Column(db.Integer, primary_key=True)
    protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
    name = db.Column(db.String)
    value = db.Column(db.String)
706
    internal = db.Column(db.Boolean)
Robin Sonnabend's avatar
Robin Sonnabend committed
707

Robin Sonnabend's avatar
Robin Sonnabend committed
708 709 710
    def get_parent(self):
        return self.protocol

Robin Sonnabend's avatar
Robin Sonnabend committed
711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737
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)

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)

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)

class LikeTOPAssociation(DatabaseModel):
    __tablename__ = "liketopassociations"
    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)


Robin Sonnabend's avatar
Robin Sonnabend committed
738 739
ALL_MODELS = [
    ProtocolType, Protocol, DefaultTOP, TOP, Document, DecisionDocument,
740
    Todo, Decision, MeetingReminder, Error, DefaultMeta, Meta, DecisionCategory
Robin Sonnabend's avatar
Robin Sonnabend committed
741
]