database.py 21.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

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

12
import os
13

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

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

Robin Sonnabend's avatar
Robin Sonnabend committed
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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)

36
37
38
39
40
41
42
43
44
45
    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
46
class ProtocolType(DatabaseModel):
47
    __tablename__ = "protocoltypes"
Robin Sonnabend's avatar
Robin Sonnabend committed
48
    __model_name__ = "protocoltype"
49
50
51
52
    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
53
    usual_time = db.Column(db.Time)
54
    is_public = db.Column(db.Boolean)
55
    modify_group = db.Column(db.String)
56
57
58
59
    private_group = db.Column(db.String)
    public_group = db.Column(db.String)
    private_mail = db.Column(db.String)
    public_mail = db.Column(db.String)
Robin Sonnabend's avatar
Robin Sonnabend committed
60
61
62
    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
63
    printer = db.Column(db.String)
Robin Sonnabend's avatar
Robin Sonnabend committed
64
    calendar = db.Column(db.String)
65
66
    restrict_networks = db.Column(db.Boolean)
    allowed_networks = db.Column(db.String)
67
68
69

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

Robin Sonnabend's avatar
Robin Sonnabend committed
74
    def get_latest_protocol(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
75
        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
76
77
78
79
        if len(candidates) == 0:
            return None
        return candidates[0]

80
81
    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
82
83
            or (user is not None and self.has_public_authenticated_view_right(user))
            or self.has_admin_right(user))
84

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

    def has_private_view_right(self, user):
Robin Sonnabend's avatar
Robin Sonnabend committed
95
96
97
98
        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
99
100

    def has_modify_right(self, user):
Robin Sonnabend's avatar
Robin Sonnabend committed
101
102
103
        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
104

105
106
107
    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
108
    @staticmethod
Robin Sonnabend's avatar
Robin Sonnabend committed
109
    def get_modifiable_protocoltypes(user):
Robin Sonnabend's avatar
Robin Sonnabend committed
110
111
112
113
114
        return [
            protocoltype for protocoltype in ProtocolType.query.all()
            if protocoltype.has_modify_right(user)
        ]

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

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

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

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

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

    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
153
    metas = relationship("Meta", backref=backref("protocol"), cascade="all, delete-orphan")
154

Robin Sonnabend's avatar
Robin Sonnabend committed
155
156
157
    def get_parent(self):
        return self.protocoltype

158
159
    def create_error(self, action, name, description):
        now = datetime.now()
160
161
        return Error(protocol_id=self.id, action=action, name=name,
            datetime=now, description=description)
162
163

    def fill_from_remarks(self, remarks):
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
        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
194
195
196
197
198
199
200
201
202
203
        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()
                meta = Meta(self.id, default_meta.name, value)
                db.session.add(meta)
        db.session.commit()
204

205
206
207
208
209
210
    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
211
212
213
    def is_done(self):
        return self.done

Robin Sonnabend's avatar
Robin Sonnabend committed
214
    def get_identifier(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
215
216
        if self.date is None:
            return None
Robin Sonnabend's avatar
Robin Sonnabend committed
217
218
219
220
        return "{}-{}".format(
            self.protocoltype.short_name.lower(),
            self.date.strftime("%y-%m-%d"))

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

Robin Sonnabend's avatar
Robin Sonnabend committed
224
    def get_etherpad_link(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
225
226
227
        identifier = self.get_identifier()
        if identifier is None:
            return ""
Robin Sonnabend's avatar
Robin Sonnabend committed
228
        return get_etherpad_url(self.get_identifier())
Robin Sonnabend's avatar
Robin Sonnabend committed
229

Robin Sonnabend's avatar
Robin Sonnabend committed
230
231
232
    def get_datetime(self):
        return datetime(self.date.year, self.date.month, self.date.day, self.protocoltype.usual_time.hour, self.protocoltype.usual_time.minute)

Robin Sonnabend's avatar
Robin Sonnabend committed
233
234
235
236
237
    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()]
238

239
240
241
242
243
244
    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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
    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
266
267
268
    def get_template(self):
        return render_template("protocol-template.txt", protocol=self)

Robin Sonnabend's avatar
Robin Sonnabend committed
269
270
271
    def delete_orphan_todos(self):
        orphan_todos = [
            todo for todo in self.todos
Robin Sonnabend's avatar
Robin Sonnabend committed
272
            if len(todo.protocols) <= 1
Robin Sonnabend's avatar
Robin Sonnabend committed
273
274
275
276
277
278
279
280
281
282
        ]
        for todo in orphan_todos:
            self.todos.remove(todo)
            db.session.delete(todo)

@event.listens_for(Protocol, "before_delete")
def on_protocol_delete(mapper, connection, protocol):
    protocol.delete_orphan_todos()


Robin Sonnabend's avatar
Robin Sonnabend committed
283
class DefaultTOP(DatabaseModel):
284
    __tablename__ = "defaulttops"
Robin Sonnabend's avatar
Robin Sonnabend committed
285
    __model_name__ = "defaulttop"
286
287
288
289
290
    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)

Robin Sonnabend's avatar
Robin Sonnabend committed
291
292
293
    def get_parent(self):
        return self.protocoltype

294
    def is_at_end(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
295
        return self.number > 0
296

Robin Sonnabend's avatar
Robin Sonnabend committed
297
class TOP(DatabaseModel):
298
    __tablename__ = "tops"
Robin Sonnabend's avatar
Robin Sonnabend committed
299
    __model_name__ = "top"
300
301
302
    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
303
    number = db.Column(db.Integer)
304
    planned = db.Column(db.Boolean)
305
    description = db.Column(db.String)
306

Robin Sonnabend's avatar
Robin Sonnabend committed
307
308
309
310
    def get_parent(self):
        return self.protocol

class Document(DatabaseModel):
311
    __tablename__ = "documents"
Robin Sonnabend's avatar
Robin Sonnabend committed
312
    __model_name__ = "document"
313
314
315
    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
316
    filename = db.Column(db.String)
317
    is_compiled = db.Column(db.Boolean)
318
    is_private = db.Column(db.Boolean)
319

Robin Sonnabend's avatar
Robin Sonnabend committed
320
321
322
    def get_parent(self):
        return self.protocol

323
324
325
    def get_filename(self):
        return os.path.join(config.DOCUMENTS_PATH, self.filename)

326
327
328
329
    def as_file_like(self):
        with open(self.get_filename(), "rb") as file:
            return BytesIO(file.read())

330
@event.listens_for(Document, "before_delete")
Robin Sonnabend's avatar
Robin Sonnabend committed
331
def on_document_delete(mapper, connection, document):
332
    if document.filename is not None:
Robin Sonnabend's avatar
Robin Sonnabend committed
333
        document_path = document.get_filename()
334
335
        if os.path.isfile(document_path):
            os.remove(document_path)
336

Robin Sonnabend's avatar
Robin Sonnabend committed
337
class DecisionDocument(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
338
    __tablename__ = "decisiondocuments"
Robin Sonnabend's avatar
Robin Sonnabend committed
339
    __model_name__ = "decisiondocument"
Robin Sonnabend's avatar
Robin Sonnabend committed
340
341
342
343
344
    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
345
346
347
    def get_parent(self):
        return self.decision

Robin Sonnabend's avatar
Robin Sonnabend committed
348
349
350
351
352
353
354
355
356
357
358
359
360
361
    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)

362
363
364
365
366
367
368
369
370
371
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
372

373
374
375
    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
376
377
378
379
380
381
382
383
384
385
    
    @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
386
387
388
389
390
391
392
393
394
395
396
397
398
399

    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
400

401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
    @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
439
class Todo(DatabaseModel):
440
    __tablename__ = "todos"
Robin Sonnabend's avatar
Robin Sonnabend committed
441
    __model_name__ = "todo"
442
    id = db.Column(db.Integer, primary_key=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
443
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
Robin Sonnabend's avatar
Robin Sonnabend committed
444
    number = db.Column(db.Integer)
445
446
    who = db.Column(db.String)
    description = db.Column(db.String)
447
448
    state = db.Column(db.Enum(TodoState), nullable=False)
    date = db.Column(db.Date, nullable=True)
449
450
451

    protocols = relationship("Protocol", secondary="todoprotocolassociations", backref="todos")

Robin Sonnabend's avatar
Robin Sonnabend committed
452
453
454
    def get_parent(self):
        return self.protocoltype

455
    def is_done(self):
456
457
458
        if self.state.needs_date():
            if self.state == TodoState.after:
                return datetime.now().date() <= self.date
459
460
            elif self.state == TodoState.before:
                return datetime.now().date() >= self.date
461
        return self.state.is_done()
Robin Sonnabend's avatar
Robin Sonnabend committed
462
463
464

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

Robin Sonnabend's avatar
Robin Sonnabend committed
466
467
468
469
470
471
    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
472
473
474
475
476
477
    def get_users(self):
        return [
            user.lower().strip()
            for user in split_terms(self.who, separators=" ,\t")
        ]

Robin Sonnabend's avatar
Robin Sonnabend committed
478
    def get_state(self):
479
        return "[{}]".format(self.get_state_plain())
Robin Sonnabend's avatar
Robin Sonnabend committed
480
    def get_state_plain(self):
481
482
        result = self.state.get_name()
        if self.state.needs_date():
483
            result = "{} {}".format(result, date_filter_short(self.date))
484
        return result
Robin Sonnabend's avatar
Robin Sonnabend committed
485
486
487
488
489
490
491
    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
492
493
494
495
496
497
498
499
500

    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
501
502
    def render_latex(self, current_protocol=None):
        return r"\textbf{{{}}}: {}: {} -- {}".format(
Robin Sonnabend's avatar
Robin Sonnabend committed
503
            "Neuer Todo" if self.is_new(current_protocol) else "Todo",
504
505
506
            escape_tex(self.who),
            escape_tex(self.description),
            escape_tex(self.get_state_tex())
Robin Sonnabend's avatar
Robin Sonnabend committed
507
508
        )

Robin Sonnabend's avatar
Robin Sonnabend committed
509
510
511
512
513
514
515
516
    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()
        )

517
518
519
    def render_template(self):
        parts = ["todo", self.who, self.description, self.state.get_name()]
        if self.state.needs_date():
520
            parts.append(date_filter(self.date))
521
522
523
        parts.append("id {}".format(self.get_id()))
        return "[{}]".format(";".join(parts))

Robin Sonnabend's avatar
Robin Sonnabend committed
524
class TodoProtocolAssociation(DatabaseModel):
525
526
527
528
    __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
529
class Decision(DatabaseModel):
530
    __tablename__ = "decisions"
Robin Sonnabend's avatar
Robin Sonnabend committed
531
    __model_name__ = "decision"
532
533
534
535
    id = db.Column(db.Integer, primary_key=True)
    protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
    content = db.Column(db.String)

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

Robin Sonnabend's avatar
Robin Sonnabend committed
538
539
540
541
    def get_parent(self):
        return self.protocol

class MeetingReminder(DatabaseModel):
542
    __tablename__ = "meetingreminders"
Robin Sonnabend's avatar
Robin Sonnabend committed
543
    __model_name__ = "meetingreminder"
544
545
    id = db.Column(db.Integer, primary_key=True)
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
546
    days_before = db.Column(db.Integer)
547
548
    send_public = db.Column(db.Boolean)
    send_private = db.Column(db.Boolean)
Robin Sonnabend's avatar
Robin Sonnabend committed
549
    additional_text = db.Column(db.String)
550

Robin Sonnabend's avatar
Robin Sonnabend committed
551
552
553
554
    def get_parent(self):
        return self.protocoltype

class Error(DatabaseModel):
555
    __tablename__ = "errors"
Robin Sonnabend's avatar
Robin Sonnabend committed
556
    __model_name__ = "error"
557
558
559
560
561
562
563
    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
564
565
566
    def get_parent(self):
        return self.protocol

Robin Sonnabend's avatar
Robin Sonnabend committed
567
568
569
570
    def get_short_description(self):
        lines = self.description.splitlines()
        if len(lines) <= 4:
            return "\n".join(lines)
Robin Sonnabend's avatar
Robin Sonnabend committed
571
        return "\n".join([*lines[:2], "…", *lines[-2:]])
Robin Sonnabend's avatar
Robin Sonnabend committed
572

Robin Sonnabend's avatar
Robin Sonnabend committed
573
class TodoMail(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
574
    __tablename__ = "todomails"
Robin Sonnabend's avatar
Robin Sonnabend committed
575
    __model_name__ = "todomail"
Robin Sonnabend's avatar
Robin Sonnabend committed
576
577
578
579
580
581
    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)
582

Robin Sonnabend's avatar
Robin Sonnabend committed
583
class OldTodo(DatabaseModel):
584
    __tablename__ = "oldtodos"
Robin Sonnabend's avatar
Robin Sonnabend committed
585
    __model_name__ = "oldtodo"
586
587
588
589
590
591
    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
592
class DefaultMeta(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
593
    __tablename__ = "defaultmetas"
Robin Sonnabend's avatar
Robin Sonnabend committed
594
    __model_name__ = "defaultmeta"
Robin Sonnabend's avatar
Robin Sonnabend committed
595
596
597
598
599
    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)

Robin Sonnabend's avatar
Robin Sonnabend committed
600
601
602
603
    def get_parent(self):
        return self.protocoltype

class Meta(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
604
    __tablename__ = "metas"
Robin Sonnabend's avatar
Robin Sonnabend committed
605
    __model_name__ = "meta"
Robin Sonnabend's avatar
Robin Sonnabend committed
606
607
608
609
610
    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)

Robin Sonnabend's avatar
Robin Sonnabend committed
611
612
613
614
615
616
617
618
    def get_parent(self):
        return self.protocol

ALL_MODELS = [
    ProtocolType, Protocol, DefaultTOP, TOP, Document, DecisionDocument,
    Todo, Decision, MeetingReminder, Error, DefaultMeta, Meta
]