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

Robin Sonnabend's avatar
Robin Sonnabend committed
9
from shared import db, date_filter, date_filter_short, escape_tex, DATE_KEY, START_TIME_KEY, END_TIME_KEY
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
101
        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
102
103

    def has_modify_right(self, user):
Robin Sonnabend's avatar
Robin Sonnabend committed
104
105
106
        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
107

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

Robin Sonnabend's avatar
Robin Sonnabend committed
118
119
120
121
122
123
124
125
126
127
128
129
130
131
    @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)
        ]

132
133
134
135
136
137
    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
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)
Robin Sonnabend's avatar
Robin Sonnabend committed
149
    done = db.Column(db.Boolean)
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

Robin Sonnabend's avatar
Robin Sonnabend committed
159
160
161
    def get_parent(self):
        return self.protocoltype

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

    def fill_from_remarks(self, remarks):
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
194
195
196
197
        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
198
199
200
201
202
203
204
        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()
205
                meta = Meta(protocol_id=self.id, name=default_meta.name, value=value)
Robin Sonnabend's avatar
Robin Sonnabend committed
206
207
                db.session.add(meta)
        db.session.commit()
208

209
210
211
212
213
214
    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
215
216
217
    def is_done(self):
        return self.done

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

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

Robin Sonnabend's avatar
Robin Sonnabend committed
228
    def get_etherpad_link(self):
229
230
        if self.pad_identifier is not None:
            return self.pad_identifier
Robin Sonnabend's avatar
Robin Sonnabend committed
231
        identifier = self.get_identifier()
232
        if self.protocoltype.non_reproducible_pad_links:
233
            identifier = "{}-{}".format(identifier, str(uuid4()))
234
235
236
        self.pad_identifier = identifier
        db.session.commit()
        return get_etherpad_url(identifier)
Robin Sonnabend's avatar
Robin Sonnabend committed
237

Robin Sonnabend's avatar
Robin Sonnabend committed
238
239
240
    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
241
242
243
244
245
    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()]
246

247
248
249
250
251
252
    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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
    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
274
275
276
    def get_template(self):
        return render_template("protocol-template.txt", protocol=self)

Robin Sonnabend's avatar
Robin Sonnabend committed
277
278
279
    def delete_orphan_todos(self):
        orphan_todos = [
            todo for todo in self.todos
Robin Sonnabend's avatar
Robin Sonnabend committed
280
            if len(todo.protocols) <= 1
Robin Sonnabend's avatar
Robin Sonnabend committed
281
282
283
284
285
286
287
288
289
290
        ]
        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
291
class DefaultTOP(DatabaseModel):
292
    __tablename__ = "defaulttops"
Robin Sonnabend's avatar
Robin Sonnabend committed
293
    __model_name__ = "defaulttop"
294
295
296
297
298
    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
299
300
301
    def get_parent(self):
        return self.protocoltype

302
    def is_at_end(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
303
        return self.number > 0
304

Robin Sonnabend's avatar
Robin Sonnabend committed
305
class TOP(DatabaseModel):
306
    __tablename__ = "tops"
Robin Sonnabend's avatar
Robin Sonnabend committed
307
    __model_name__ = "top"
308
309
310
    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
311
    number = db.Column(db.Integer)
312
    planned = db.Column(db.Boolean)
313
    description = db.Column(db.String)
314

Robin Sonnabend's avatar
Robin Sonnabend committed
315
316
317
318
    def get_parent(self):
        return self.protocol

class Document(DatabaseModel):
319
    __tablename__ = "documents"
Robin Sonnabend's avatar
Robin Sonnabend committed
320
    __model_name__ = "document"
321
322
323
    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
324
    filename = db.Column(db.String)
325
    is_compiled = db.Column(db.Boolean)
326
    is_private = db.Column(db.Boolean)
327

Robin Sonnabend's avatar
Robin Sonnabend committed
328
329
330
    def get_parent(self):
        return self.protocol

331
332
333
    def get_filename(self):
        return os.path.join(config.DOCUMENTS_PATH, self.filename)

334
335
336
337
    def as_file_like(self):
        with open(self.get_filename(), "rb") as file:
            return BytesIO(file.read())

338
@event.listens_for(Document, "before_delete")
Robin Sonnabend's avatar
Robin Sonnabend committed
339
def on_document_delete(mapper, connection, document):
340
    if document.filename is not None:
Robin Sonnabend's avatar
Robin Sonnabend committed
341
        document_path = document.get_filename()
342
343
        if os.path.isfile(document_path):
            os.remove(document_path)
344

Robin Sonnabend's avatar
Robin Sonnabend committed
345
class DecisionDocument(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
346
    __tablename__ = "decisiondocuments"
Robin Sonnabend's avatar
Robin Sonnabend committed
347
    __model_name__ = "decisiondocument"
Robin Sonnabend's avatar
Robin Sonnabend committed
348
349
350
351
352
    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
353
354
355
    def get_parent(self):
        return self.decision

Robin Sonnabend's avatar
Robin Sonnabend committed
356
357
358
359
360
361
362
363
364
365
366
367
368
369
    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)

370
371
372
373
374
375
376
377
378
379
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
380

381
382
383
    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
384
385
386
387
388
389
390
391
392
393
    
    @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
394
395
396
397
398
399
400
401
402
403
404
405
406
407

    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
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
439
440
441
442
443
444
445
446
    @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
447
class Todo(DatabaseModel):
448
    __tablename__ = "todos"
Robin Sonnabend's avatar
Robin Sonnabend committed
449
    __model_name__ = "todo"
450
    id = db.Column(db.Integer, primary_key=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
451
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
Robin Sonnabend's avatar
Robin Sonnabend committed
452
    number = db.Column(db.Integer)
453
454
    who = db.Column(db.String)
    description = db.Column(db.String)
455
456
    state = db.Column(db.Enum(TodoState), nullable=False)
    date = db.Column(db.Date, nullable=True)
457
458
459

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

Robin Sonnabend's avatar
Robin Sonnabend committed
460
461
462
    def get_parent(self):
        return self.protocoltype

463
    def is_done(self):
464
465
466
        if self.state.needs_date():
            if self.state == TodoState.after:
                return datetime.now().date() <= self.date
467
468
            elif self.state == TodoState.before:
                return datetime.now().date() >= self.date
469
        return self.state.is_done()
Robin Sonnabend's avatar
Robin Sonnabend committed
470
471
472

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

Robin Sonnabend's avatar
Robin Sonnabend committed
474
475
476
477
478
479
    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
480
481
482
483
484
485
    def get_users(self):
        return [
            user.lower().strip()
            for user in split_terms(self.who, separators=" ,\t")
        ]

Robin Sonnabend's avatar
Robin Sonnabend committed
486
    def get_state(self):
487
        return "[{}]".format(self.get_state_plain())
Robin Sonnabend's avatar
Robin Sonnabend committed
488
    def get_state_plain(self):
489
490
        result = self.state.get_name()
        if self.state.needs_date():
491
            result = "{} {}".format(result, date_filter_short(self.date))
492
        return result
Robin Sonnabend's avatar
Robin Sonnabend committed
493
494
495
496
497
498
499
    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
500
501
502
503
504
505
506
507
508

    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
509
510
    def render_latex(self, current_protocol=None):
        return r"\textbf{{{}}}: {}: {} -- {}".format(
Robin Sonnabend's avatar
Robin Sonnabend committed
511
            "Neuer Todo" if self.is_new(current_protocol) else "Todo",
512
513
514
            escape_tex(self.who),
            escape_tex(self.description),
            escape_tex(self.get_state_tex())
Robin Sonnabend's avatar
Robin Sonnabend committed
515
516
        )

Robin Sonnabend's avatar
Robin Sonnabend committed
517
518
519
520
521
522
523
524
    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()
        )

525
526
527
    def render_template(self):
        parts = ["todo", self.who, self.description, self.state.get_name()]
        if self.state.needs_date():
528
            parts.append(date_filter(self.date))
529
530
531
        parts.append("id {}".format(self.get_id()))
        return "[{}]".format(";".join(parts))

Robin Sonnabend's avatar
Robin Sonnabend committed
532
class TodoProtocolAssociation(DatabaseModel):
533
534
535
536
    __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
537
class Decision(DatabaseModel):
538
    __tablename__ = "decisions"
Robin Sonnabend's avatar
Robin Sonnabend committed
539
    __model_name__ = "decision"
540
541
542
    id = db.Column(db.Integer, primary_key=True)
    protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
    content = db.Column(db.String)
543
    category_id = db.Column(db.Integer, db.ForeignKey("decisioncategories.id"), nullable=True)
544

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

Robin Sonnabend's avatar
Robin Sonnabend committed
547
548
549
    def get_parent(self):
        return self.protocol

550
551
552
553
554
555
556
557
558
559
560
561
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
562
class MeetingReminder(DatabaseModel):
563
    __tablename__ = "meetingreminders"
Robin Sonnabend's avatar
Robin Sonnabend committed
564
    __model_name__ = "meetingreminder"
565
566
    id = db.Column(db.Integer, primary_key=True)
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
567
    days_before = db.Column(db.Integer)
568
569
    send_public = db.Column(db.Boolean)
    send_private = db.Column(db.Boolean)
Robin Sonnabend's avatar
Robin Sonnabend committed
570
    additional_text = db.Column(db.String)
571

Robin Sonnabend's avatar
Robin Sonnabend committed
572
573
574
575
    def get_parent(self):
        return self.protocoltype

class Error(DatabaseModel):
576
    __tablename__ = "errors"
Robin Sonnabend's avatar
Robin Sonnabend committed
577
    __model_name__ = "error"
578
579
580
581
582
583
584
    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
585
586
587
    def get_parent(self):
        return self.protocol

Robin Sonnabend's avatar
Robin Sonnabend committed
588
589
590
591
    def get_short_description(self):
        lines = self.description.splitlines()
        if len(lines) <= 4:
            return "\n".join(lines)
Robin Sonnabend's avatar
Robin Sonnabend committed
592
        return "\n".join([*lines[:2], "…", *lines[-2:]])
Robin Sonnabend's avatar
Robin Sonnabend committed
593

Robin Sonnabend's avatar
Robin Sonnabend committed
594
class TodoMail(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
595
    __tablename__ = "todomails"
Robin Sonnabend's avatar
Robin Sonnabend committed
596
    __model_name__ = "todomail"
Robin Sonnabend's avatar
Robin Sonnabend committed
597
598
599
600
601
602
    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)
603

Robin Sonnabend's avatar
Robin Sonnabend committed
604
class OldTodo(DatabaseModel):
605
    __tablename__ = "oldtodos"
Robin Sonnabend's avatar
Robin Sonnabend committed
606
    __model_name__ = "oldtodo"
607
608
609
610
611
612
    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
613
class DefaultMeta(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
614
    __tablename__ = "defaultmetas"
Robin Sonnabend's avatar
Robin Sonnabend committed
615
    __model_name__ = "defaultmeta"
Robin Sonnabend's avatar
Robin Sonnabend committed
616
617
618
619
620
    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
621
622
623
624
    def get_parent(self):
        return self.protocoltype

class Meta(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
625
    __tablename__ = "metas"
Robin Sonnabend's avatar
Robin Sonnabend committed
626
    __model_name__ = "meta"
Robin Sonnabend's avatar
Robin Sonnabend committed
627
628
629
630
631
    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
632
633
634
635
636
    def get_parent(self):
        return self.protocol

ALL_MODELS = [
    ProtocolType, Protocol, DefaultTOP, TOP, Document, DecisionDocument,
637
    Todo, Decision, MeetingReminder, Error, DefaultMeta, Meta, DecisionCategory
Robin Sonnabend's avatar
Robin Sonnabend committed
638
639
]