database.py 27.5 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
Robin Sonnabend's avatar
Robin Sonnabend committed
10
from utils import random_string, 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
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)

Robin Sonnabend's avatar
Robin Sonnabend committed
34
35
36
    def has_publish_right(self, user):
        return self.get_parent().has_publish_right(user)

Robin Sonnabend's avatar
Robin Sonnabend committed
37
38
39
    def has_admin_right(self, user):
        return self.get_parent().has_admin_right(user)

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

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

Robin Sonnabend's avatar
Robin Sonnabend committed
81
    def get_latest_protocol(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
82
        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
83
84
85
86
        if len(candidates) == 0:
            return None
        return candidates[0]

87
88
    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
89
90
            or (user is not None and self.has_public_authenticated_view_right(user))
            or self.has_admin_right(user))
91

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

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

    def has_modify_right(self, user):
Robin Sonnabend's avatar
Robin Sonnabend committed
107
108
109
        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
110

Robin Sonnabend's avatar
Robin Sonnabend committed
111
112
113
114
115
    def has_publish_right(self, user):
        return ((user is not None
            and (self.publish_group != "" and self.publish_group in user.groups))
            or self.has_admin_right(user))

116
117
118
    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
119
    @staticmethod
Robin Sonnabend's avatar
Robin Sonnabend committed
120
    def get_modifiable_protocoltypes(user):
Robin Sonnabend's avatar
Robin Sonnabend committed
121
122
123
124
125
        return [
            protocoltype for protocoltype in ProtocolType.query.all()
            if protocoltype.has_modify_right(user)
        ]

Robin Sonnabend's avatar
Robin Sonnabend committed
126
    @staticmethod
127
    def get_public_protocoltypes(user, check_networks=True):
Robin Sonnabend's avatar
Robin Sonnabend committed
128
129
        return [
            protocoltype for protocoltype in ProtocolType.query.all()
130
            if protocoltype.has_public_view_right(user, check_networks=check_networks)
Robin Sonnabend's avatar
Robin Sonnabend committed
131
132
133
134
135
136
137
138
139
        ]

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

140
141
142
143
144
145
    def get_wiki_infobox(self):
        return "Infobox {}".format(self.short_name)

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

146

Robin Sonnabend's avatar
Robin Sonnabend committed
147
class Protocol(DatabaseModel):
148
    __tablename__ = "protocols"
Robin Sonnabend's avatar
Robin Sonnabend committed
149
    __model_name__ = "protocol"
150
151
    id = db.Column(db.Integer, primary_key=True)
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
152
153
154
    source = db.Column(db.String)
    content_public = db.Column(db.String)
    content_private = db.Column(db.String)
155
156
    content_html_public = db.Column(db.String)
    content_html_private = db.Column(db.String)
157
158
159
    date = db.Column(db.Date)
    start_time = db.Column(db.Time)
    end_time = db.Column(db.Time)
160
    done = db.Column(db.Boolean, nullable=False, default=False)
161
    public = db.Column(db.Boolean)
162
    pad_identifier = db.Column(db.String)
163
164
165
166
167

    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
168
    metas = relationship("Meta", backref=backref("protocol"), cascade="all, delete-orphan")
169
    localtops = relationship("LocalTOP", backref=backref("protocol"), cascade="all, delete-orphan")
170

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

Robin Sonnabend's avatar
Robin Sonnabend committed
173
174
175
    def get_parent(self):
        return self.protocoltype

176
177
    def create_error(self, action, name, description):
        now = datetime.now()
178
179
        return Error(protocol_id=self.id, action=action, name=name,
            datetime=now, description=description)
180

181
182
183
    def create_localtops(self):
        local_tops = []
        for default_top in self.protocoltype.default_tops:
184
            local_tops.append(LocalTOP(defaulttop_id=default_top.id,
185
                protocol_id=self.id, description=default_top.description or ""))
186
187
        return local_tops

188
    def fill_from_remarks(self, remarks):
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
        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
219
220
221
222
223
224
225
        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()
226
                meta = Meta(protocol_id=self.id, name=default_meta.name, value=value, internal=default_meta.internal)
Robin Sonnabend's avatar
Robin Sonnabend committed
227
228
                db.session.add(meta)
        db.session.commit()
229

230
231
232
233
234
235
    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
236
237
238
    def is_done(self):
        return self.done

Robin Sonnabend's avatar
Robin Sonnabend committed
239
    def get_identifier(self):
Administrator's avatar
Administrator committed
240
241
        if self.pad_identifier is not None:
            return self.pad_identifier
Robin Sonnabend's avatar
Robin Sonnabend committed
242
243
        if self.date is None:
            return None
244
245
246
        return self.get_short_identifier()

    def get_short_identifier(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
247
248
249
250
        return "{}-{}".format(
            self.protocoltype.short_name.lower(),
            self.date.strftime("%y-%m-%d"))

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

Robin Sonnabend's avatar
Robin Sonnabend committed
254
    def get_etherpad_link(self):
Administrator's avatar
Administrator committed
255
256
257
        if self.pad_identifier is None:
            identifier = self.get_identifier()
            if self.protocoltype.non_reproducible_pad_links:
258
                identifier = "{}-{}".format(identifier, str(uuid4()).replace("-", ""))[:50]
Administrator's avatar
Administrator committed
259
260
261
            self.pad_identifier = identifier
            db.session.commit()
        return get_etherpad_url(self.pad_identifier)
Robin Sonnabend's avatar
Robin Sonnabend committed
262

263
264
265
266
267
    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
268
    def get_datetime(self):
269
        time = self.get_time()
270
        return datetime(self.date.year, self.date.month, self.date.day, time.hour, time.minute)
Robin Sonnabend's avatar
Robin Sonnabend committed
271

Robin Sonnabend's avatar
Robin Sonnabend committed
272
273
274
275
276
    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()]
277

278
279
280
281
282
283
    def get_open_todos(self):
        return [
            todo for todo in self.protocoltype.todos
            if not todo.is_done()
        ]

284
    def has_compiled_document(self, private=None):
Robin Sonnabend's avatar
Robin Sonnabend committed
285
286
287
        candidates = [
            document for document in self.documents
            if document.is_compiled
288
                and (private is None or document.is_private == private)
Robin Sonnabend's avatar
Robin Sonnabend committed
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
        ]
        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
306
307
308
    def get_template(self):
        return render_template("protocol-template.txt", protocol=self)

Robin Sonnabend's avatar
Robin Sonnabend committed
309
310
311
    def delete_orphan_todos(self):
        orphan_todos = [
            todo for todo in self.todos
Robin Sonnabend's avatar
Robin Sonnabend committed
312
            if len(todo.protocols) <= 1
Robin Sonnabend's avatar
Robin Sonnabend committed
313
314
315
316
317
        ]
        for todo in orphan_todos:
            self.todos.remove(todo)
            db.session.delete(todo)

318
319
320
321
322
323
324
325
326
327
328
    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
329
330
331
332
333
@event.listens_for(Protocol, "before_delete")
def on_protocol_delete(mapper, connection, protocol):
    protocol.delete_orphan_todos()


Robin Sonnabend's avatar
Robin Sonnabend committed
334
class DefaultTOP(DatabaseModel):
335
    __tablename__ = "defaulttops"
Robin Sonnabend's avatar
Robin Sonnabend committed
336
    __model_name__ = "defaulttop"
337
338
339
340
    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)
341
    description = db.Column(db.String)
342

343
344
    localtops = relationship("LocalTOP", backref=backref("defaulttop"), cascade="all, delete-orphan")

Robin Sonnabend's avatar
Robin Sonnabend committed
345
346
347
    def get_parent(self):
        return self.protocoltype

348
    def is_at_end(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
349
        return self.number > 0
350

351
    def get_localtop(self, protocol):
352
        return LocalTOP.query.filter_by(defaulttop_id=self.id,
353
354
355
356
357
358
359
360
            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
361
class TOP(DatabaseModel):
362
    __tablename__ = "tops"
Robin Sonnabend's avatar
Robin Sonnabend committed
363
    __model_name__ = "top"
364
365
366
    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
367
    number = db.Column(db.Integer)
368
    planned = db.Column(db.Boolean)
369
    description = db.Column(db.String)
370

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

Robin Sonnabend's avatar
Robin Sonnabend committed
373
374
375
    def get_parent(self):
        return self.protocol

376
377
378
379
380
381
382
383
384
385
386
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

387
388
389
390
391
392
393
394
395
396
397
398
    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
399
class Document(DatabaseModel):
400
    __tablename__ = "documents"
Robin Sonnabend's avatar
Robin Sonnabend committed
401
    __model_name__ = "document"
402
403
404
    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
405
    filename = db.Column(db.String)
406
    is_compiled = db.Column(db.Boolean)
407
    is_private = db.Column(db.Boolean)
408

Robin Sonnabend's avatar
Robin Sonnabend committed
409
410
411
    def get_parent(self):
        return self.protocol

412
413
414
    def get_filename(self):
        return os.path.join(config.DOCUMENTS_PATH, self.filename)

415
416
417
418
    def as_file_like(self):
        with open(self.get_filename(), "rb") as file:
            return BytesIO(file.read())

419
@event.listens_for(Document, "before_delete")
Robin Sonnabend's avatar
Robin Sonnabend committed
420
def on_document_delete(mapper, connection, document):
421
    if document.filename is not None:
Robin Sonnabend's avatar
Robin Sonnabend committed
422
        document_path = document.get_filename()
423
424
        if os.path.isfile(document_path):
            os.remove(document_path)
425

Robin Sonnabend's avatar
Robin Sonnabend committed
426
class DecisionDocument(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
427
    __tablename__ = "decisiondocuments"
Robin Sonnabend's avatar
Robin Sonnabend committed
428
    __model_name__ = "decisiondocument"
Robin Sonnabend's avatar
Robin Sonnabend committed
429
430
431
432
433
    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
434
435
436
    def get_parent(self):
        return self.decision

Robin Sonnabend's avatar
Robin Sonnabend committed
437
438
439
440
441
442
443
444
445
446
447
448
449
450
    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)

451
452
453
454
455
456
457
458
459
460
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
461

462
463
464
    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
465
466
467
468
469
470
471
472
473
474
    
    @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
475
476
477
478
479
480
481
482
483
484
485
486
487
488

    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
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
516
517
518
519
520
521
522
523
524
525
526
527
    @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
528
class Todo(DatabaseModel):
529
    __tablename__ = "todos"
Robin Sonnabend's avatar
Robin Sonnabend committed
530
    __model_name__ = "todo"
531
    id = db.Column(db.Integer, primary_key=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
532
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
Robin Sonnabend's avatar
Robin Sonnabend committed
533
    number = db.Column(db.Integer)
534
535
    who = db.Column(db.String)
    description = db.Column(db.String)
536
537
    state = db.Column(db.Enum(TodoState), nullable=False)
    date = db.Column(db.Date, nullable=True)
538
539

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

Robin Sonnabend's avatar
Robin Sonnabend committed
542
543
544
    def get_parent(self):
        return self.protocoltype

545
    def is_done(self):
546
547
548
        if self.state.needs_date():
            if self.state == TodoState.after:
                return datetime.now().date() <= self.date
549
550
            elif self.state == TodoState.before:
                return datetime.now().date() >= self.date
551
        return self.state.is_done()
Robin Sonnabend's avatar
Robin Sonnabend committed
552
553
554

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

Robin Sonnabend's avatar
Robin Sonnabend committed
556
557
558
559
560
561
    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
562
563
564
565
566
567
    def get_users(self):
        return [
            user.lower().strip()
            for user in split_terms(self.who, separators=" ,\t")
        ]

Robin Sonnabend's avatar
Robin Sonnabend committed
568
    def get_state(self):
569
        return "[{}]".format(self.get_state_plain())
Robin Sonnabend's avatar
Robin Sonnabend committed
570
    def get_state_plain(self):
571
572
        result = self.state.get_name()
        if self.state.needs_date():
573
            result = "{} {}".format(result, date_filter_short(self.date))
574
        return result
Robin Sonnabend's avatar
Robin Sonnabend committed
575
576
577
578
579
580
581
    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
582

583
    def render_html(self, current_protocol=None):
Robin Sonnabend's avatar
Robin Sonnabend committed
584
585
586
587
588
589
590
        parts = [
            self.get_state(),
            "<strong>{}:</strong>".format(self.who),
            self.description
        ]
        return " ".join(parts)

Robin Sonnabend's avatar
Robin Sonnabend committed
591
592
    def render_latex(self, current_protocol=None):
        return r"\textbf{{{}}}: {}: {} -- {}".format(
Robin Sonnabend's avatar
Robin Sonnabend committed
593
            "Neuer Todo" if self.is_new(current_protocol) else "Todo",
594
595
596
            escape_tex(self.who),
            escape_tex(self.description),
            escape_tex(self.get_state_tex())
Robin Sonnabend's avatar
Robin Sonnabend committed
597
598
        )

Robin Sonnabend's avatar
Robin Sonnabend committed
599
600
601
602
603
604
605
606
    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()
        )

607
608
609
    def render_template(self):
        parts = ["todo", self.who, self.description, self.state.get_name()]
        if self.state.needs_date():
610
            parts.append(date_filter_short(self.date))
611
612
613
        parts.append("id {}".format(self.get_id()))
        return "[{}]".format(";".join(parts))

Robin Sonnabend's avatar
Robin Sonnabend committed
614
class TodoProtocolAssociation(DatabaseModel):
615
616
617
618
    __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
619
class Decision(DatabaseModel):
620
    __tablename__ = "decisions"
Robin Sonnabend's avatar
Robin Sonnabend committed
621
    __model_name__ = "decision"
622
623
624
625
    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
626
627
    document = relationship("DecisionDocument", backref=backref("decision"), cascade="all, delete-orphan", uselist=False)

628
    categories = relationship("DecisionCategory", secondary="decisioncategoryassociations")
Robin Sonnabend's avatar
Robin Sonnabend committed
629
630
    likes = relationship("Like", secondary="likedecisionassociations")

Robin Sonnabend's avatar
Robin Sonnabend committed
631
632
633
    def get_parent(self):
        return self.protocol

634
635
636
    def get_categories_str(self):
        return ", ".join(map(lambda c: c.name, self.categories))

637
638
639
640
641
642
643
644
645
646
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)

    def get_parent(self):
        return self.protocoltype

647
648
649
650
651
class DecisionCategoryAssociation(DatabaseModel):
    __tablename__ = "decisioncategoryassociations"
    decision_id = db.Column(db.Integer, db.ForeignKey("decisions.id"), primary_key=True)
    decisioncategory_id = db.Column(db.Integer, db.ForeignKey("decisioncategories.id"), primary_key=True)

Robin Sonnabend's avatar
Robin Sonnabend committed
652
class MeetingReminder(DatabaseModel):
653
    __tablename__ = "meetingreminders"
Robin Sonnabend's avatar
Robin Sonnabend committed
654
    __model_name__ = "meetingreminder"
655
656
    id = db.Column(db.Integer, primary_key=True)
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
657
    days_before = db.Column(db.Integer)
658
659
    send_public = db.Column(db.Boolean)
    send_private = db.Column(db.Boolean)
Robin Sonnabend's avatar
Robin Sonnabend committed
660
    additional_text = db.Column(db.String)
661

Robin Sonnabend's avatar
Robin Sonnabend committed
662
663
664
665
    def get_parent(self):
        return self.protocoltype

class Error(DatabaseModel):
666
    __tablename__ = "errors"
Robin Sonnabend's avatar
Robin Sonnabend committed
667
    __model_name__ = "error"
668
669
670
671
672
673
674
    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
675
676
677
    def get_parent(self):
        return self.protocol

Robin Sonnabend's avatar
Robin Sonnabend committed
678
679
680
681
    def get_short_description(self):
        lines = self.description.splitlines()
        if len(lines) <= 4:
            return "\n".join(lines)
682
        return "\n".join(["\n".join(lines[:2]), "…", "\n".join(lines[-2:])])
Robin Sonnabend's avatar
Robin Sonnabend committed
683

Robin Sonnabend's avatar
Robin Sonnabend committed
684
class TodoMail(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
685
    __tablename__ = "todomails"
Robin Sonnabend's avatar
Robin Sonnabend committed
686
    __model_name__ = "todomail"
Robin Sonnabend's avatar
Robin Sonnabend committed
687
688
689
690
691
692
    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)
693

Robin Sonnabend's avatar
Robin Sonnabend committed
694
class OldTodo(DatabaseModel):
695
    __tablename__ = "oldtodos"
Robin Sonnabend's avatar
Robin Sonnabend committed
696
    __model_name__ = "oldtodo"
697
698
699
700
701
702
    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
703
class DefaultMeta(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
704
    __tablename__ = "defaultmetas"
Robin Sonnabend's avatar
Robin Sonnabend committed
705
    __model_name__ = "defaultmeta"
Robin Sonnabend's avatar
Robin Sonnabend committed
706
707
708
709
    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)
710
    value = db.Column(db.String)
711
    internal = db.Column(db.Boolean)
712
    prior = db.Column(db.Boolean, default=False, nullable=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
713

Robin Sonnabend's avatar
Robin Sonnabend committed
714
715
716
717
    def get_parent(self):
        return self.protocoltype

class Meta(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
718
    __tablename__ = "metas"
Robin Sonnabend's avatar
Robin Sonnabend committed
719
    __model_name__ = "meta"
Robin Sonnabend's avatar
Robin Sonnabend committed
720
721
722
723
    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)
724
    internal = db.Column(db.Boolean)
Robin Sonnabend's avatar
Robin Sonnabend committed
725

Robin Sonnabend's avatar
Robin Sonnabend committed
726
727
728
    def get_parent(self):
        return self.protocol

Robin Sonnabend's avatar
Robin Sonnabend committed
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
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
756
757
ALL_MODELS = [
    ProtocolType, Protocol, DefaultTOP, TOP, Document, DecisionDocument,
758
    Todo, Decision, MeetingReminder, Error, DefaultMeta, Meta, DecisionCategory
Robin Sonnabend's avatar
Robin Sonnabend committed
759
]