database.py 29.3 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
from dateutil import tz
13

14
import os
15

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

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

Robin Sonnabend's avatar
Robin Sonnabend committed
23
24
25
26
27
28
29
30
31
32
33
34
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
35
36
37
    def has_publish_right(self, user):
        return self.get_parent().has_publish_right(user)

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

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

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

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

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

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

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

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

Robin Sonnabend's avatar
Robin Sonnabend committed
113
114
115
116
117
    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))

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

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

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

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

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

148

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

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

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

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

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

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

190
    def fill_from_remarks(self, remarks):
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
219
220
        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
221
222
223
224
225
226
227
        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()
228
                meta = Meta(protocol_id=self.id, name=default_meta.name, value=value, internal=default_meta.internal)
Robin Sonnabend's avatar
Robin Sonnabend committed
229
230
                db.session.add(meta)
        db.session.commit()
231

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

241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
    def get_state_glyph(self):
        if self.is_done():
            state = "unchecked" #"Fertig"
            if self.public:
                state = "check" #"Veröffentlicht"
        else:
            state = "pencil" #"Geplant"
        return state

    def get_state_name(self):
        if self.is_done():
            state = "Fertig"
            if self.public:
                state = "Veröffentlicht"
        else:
            state = "Geplant"
        return state

Robin Sonnabend's avatar
Robin Sonnabend committed
259
    def get_identifier(self):
Administrator's avatar
Administrator committed
260
261
        if self.pad_identifier is not None:
            return self.pad_identifier
Robin Sonnabend's avatar
Robin Sonnabend committed
262
263
        if self.date is None:
            return None
264
265
266
        return self.get_short_identifier()

    def get_short_identifier(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
267
268
269
270
        return "{}-{}".format(
            self.protocoltype.short_name.lower(),
            self.date.strftime("%y-%m-%d"))

271
272
273
274
    def get_title(self):
        return "Protokoll: {}, {:%d.%m.%Y}".format(
            self.protocoltype.short_name, self.date)

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

Robin Sonnabend's avatar
Robin Sonnabend committed
279
    def get_etherpad_link(self):
Administrator's avatar
Administrator committed
280
281
282
        if self.pad_identifier is None:
            identifier = self.get_identifier()
            if self.protocoltype.non_reproducible_pad_links:
283
                identifier = "{}-{}".format(identifier, str(uuid4()).replace("-", ""))[:50]
Administrator's avatar
Administrator committed
284
285
286
            self.pad_identifier = identifier
            db.session.commit()
        return get_etherpad_url(self.pad_identifier)
Robin Sonnabend's avatar
Robin Sonnabend committed
287

288
289
290
291
292
    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
293
    def get_datetime(self):
294
        time = self.get_time()
295
        return datetime(self.date.year, self.date.month, self.date.day, time.hour, time.minute)
Robin Sonnabend's avatar
Robin Sonnabend committed
296

Robin Sonnabend's avatar
Robin Sonnabend committed
297
298
299
300
301
    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()]
302

303
304
305
306
307
308
    def get_open_todos(self):
        return [
            todo for todo in self.protocoltype.todos
            if not todo.is_done()
        ]

309
    def has_compiled_document(self, private=None):
Robin Sonnabend's avatar
Robin Sonnabend committed
310
311
312
        candidates = [
            document for document in self.documents
            if document.is_compiled
313
                and (private is None or document.is_private == private)
Robin Sonnabend's avatar
Robin Sonnabend committed
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
        ]
        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
331
332
333
    def get_template(self):
        return render_template("protocol-template.txt", protocol=self)

Robin Sonnabend's avatar
Robin Sonnabend committed
334
335
336
    def delete_orphan_todos(self):
        orphan_todos = [
            todo for todo in self.todos
Robin Sonnabend's avatar
Robin Sonnabend committed
337
            if len(todo.protocols) <= 1
Robin Sonnabend's avatar
Robin Sonnabend committed
338
339
340
341
342
        ]
        for todo in orphan_todos:
            self.todos.remove(todo)
            db.session.delete(todo)

343
344
345
346
347
348
349
350
351
352
353
    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

354
355
356
357
    def get_timezone_aware_start_date(self):
        return datetime.combine(self.date, self.get_time()).replace(
            tzinfo=tz.tzlocal())

358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
    @staticmethod
    def create_new_protocol(protocoltype, date, start_time=None):
        if start_time is None:
            start_time = protocoltype.usual_time
        protocol = Protocol(protocoltype_id=protocoltype.id,
            date=date, start_time=start_time)
        db.session.add(protocol)
        db.session.commit()
        for local_top in protocol.create_localtops():
            db.session.add(local_top)
        for default_meta in protocoltype.metas:
            if default_meta.prior:
                meta = Meta(protocol_id=protocol.id, name=default_meta.name,
                    internal=default_meta.internal, value=default_meta.value)
                db.session.add(meta)
        db.session.commit()
        import tasks
        tasks.push_tops_to_calendar(protocol)
        return protocol

        

Robin Sonnabend's avatar
Robin Sonnabend committed
380
381
382
383
384
@event.listens_for(Protocol, "before_delete")
def on_protocol_delete(mapper, connection, protocol):
    protocol.delete_orphan_todos()


Robin Sonnabend's avatar
Robin Sonnabend committed
385
class DefaultTOP(DatabaseModel):
386
    __tablename__ = "defaulttops"
Robin Sonnabend's avatar
Robin Sonnabend committed
387
    __model_name__ = "defaulttop"
388
389
390
391
    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)
392
    description = db.Column(db.String)
393

394
395
    localtops = relationship("LocalTOP", backref=backref("defaulttop"), cascade="all, delete-orphan")

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

399
    def is_at_end(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
400
        return self.number > 0
401

402
    def get_localtop(self, protocol):
403
        return LocalTOP.query.filter_by(defaulttop_id=self.id,
404
405
406
407
408
409
410
411
            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
412
class TOP(DatabaseModel):
413
    __tablename__ = "tops"
Robin Sonnabend's avatar
Robin Sonnabend committed
414
    __model_name__ = "top"
415
416
417
    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
418
    number = db.Column(db.Integer)
419
    planned = db.Column(db.Boolean)
420
    description = db.Column(db.String)
421

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

Robin Sonnabend's avatar
Robin Sonnabend committed
424
425
426
    def get_parent(self):
        return self.protocol

427
428
429
430
431
432
433
434
435
436
437
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

438
439
440
441
442
443
444
445
446
447
448
449
    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
450
class Document(DatabaseModel):
451
    __tablename__ = "documents"
Robin Sonnabend's avatar
Robin Sonnabend committed
452
    __model_name__ = "document"
453
454
455
    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
456
    filename = db.Column(db.String)
457
    is_compiled = db.Column(db.Boolean)
458
    is_private = db.Column(db.Boolean)
459

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

463
464
465
    def get_filename(self):
        return os.path.join(config.DOCUMENTS_PATH, self.filename)

466
467
468
469
    def as_file_like(self):
        with open(self.get_filename(), "rb") as file:
            return BytesIO(file.read())

470
@event.listens_for(Document, "before_delete")
Robin Sonnabend's avatar
Robin Sonnabend committed
471
def on_document_delete(mapper, connection, document):
472
    if document.filename is not None:
Robin Sonnabend's avatar
Robin Sonnabend committed
473
        document_path = document.get_filename()
474
475
        if os.path.isfile(document_path):
            os.remove(document_path)
476

Robin Sonnabend's avatar
Robin Sonnabend committed
477
class DecisionDocument(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
478
    __tablename__ = "decisiondocuments"
Robin Sonnabend's avatar
Robin Sonnabend committed
479
    __model_name__ = "decisiondocument"
Robin Sonnabend's avatar
Robin Sonnabend committed
480
481
482
483
484
    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
485
486
487
    def get_parent(self):
        return self.decision

Robin Sonnabend's avatar
Robin Sonnabend committed
488
489
490
491
492
493
494
495
496
497
498
499
500
501
    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)

502
503
504
505
506
507
508
509
510
511
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
512

513
514
515
    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
516
517
518
519
520
521
522
523
524
525
    
    @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
526
527
528
529
530
531
532
533
534
535
536
537
538
539

    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
540

541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
    @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
579
class Todo(DatabaseModel):
580
    __tablename__ = "todos"
Robin Sonnabend's avatar
Robin Sonnabend committed
581
    __model_name__ = "todo"
582
    id = db.Column(db.Integer, primary_key=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
583
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
Robin Sonnabend's avatar
Robin Sonnabend committed
584
    number = db.Column(db.Integer)
585
586
    who = db.Column(db.String)
    description = db.Column(db.String)
587
588
    state = db.Column(db.Enum(TodoState), nullable=False)
    date = db.Column(db.Date, nullable=True)
589
590

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

Robin Sonnabend's avatar
Robin Sonnabend committed
593
594
595
    def get_parent(self):
        return self.protocoltype

596
    def is_done(self):
597
598
599
        if self.state.needs_date():
            if self.state == TodoState.after:
                return datetime.now().date() <= self.date
600
601
            elif self.state == TodoState.before:
                return datetime.now().date() >= self.date
602
        return self.state.is_done()
Robin Sonnabend's avatar
Robin Sonnabend committed
603
604
605

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

Robin Sonnabend's avatar
Robin Sonnabend committed
607
608
609
610
611
612
    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
613
614
615
616
617
618
    def get_users(self):
        return [
            user.lower().strip()
            for user in split_terms(self.who, separators=" ,\t")
        ]

Robin Sonnabend's avatar
Robin Sonnabend committed
619
    def get_state(self):
620
        return "[{}]".format(self.get_state_plain())
Robin Sonnabend's avatar
Robin Sonnabend committed
621
    def get_state_plain(self):
622
623
        result = self.state.get_name()
        if self.state.needs_date():
624
            result = "{} {}".format(result, date_filter_short(self.date))
625
        return result
Robin Sonnabend's avatar
Robin Sonnabend committed
626
627
628
629
630
631
632
    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
633

634
    def render_html(self, current_protocol=None):
Robin Sonnabend's avatar
Robin Sonnabend committed
635
636
637
638
639
640
641
        parts = [
            self.get_state(),
            "<strong>{}:</strong>".format(self.who),
            self.description
        ]
        return " ".join(parts)

Robin Sonnabend's avatar
Robin Sonnabend committed
642
    def render_latex(self, current_protocol=None):
643
        return r"\Todo{{{}}}{{{}}}{{{}}}{{{}}}".format(
Robin Sonnabend's avatar
Robin Sonnabend committed
644
            "Neuer Todo" if self.is_new(current_protocol) else "Todo",
645
646
647
            escape_tex(self.who),
            escape_tex(self.description),
            escape_tex(self.get_state_tex())
Robin Sonnabend's avatar
Robin Sonnabend committed
648
649
        )

650
651
652
653
654
    def render_wikitext(self, current_protocol=None, use_dokuwiki=False):
        bold = "'''"
        if use_dokuwiki:
            bold = "**"
        return "{0}{1}:{0} {2}: {3} - {4}".format(bold,
Robin Sonnabend's avatar
Robin Sonnabend committed
655
656
657
658
659
660
            "Neuer Todo" if self.is_new(current_protocol) else "Todo",
            self.who,
            self.description,
            self.get_state_plain()
        )

661
662
663
    def render_template(self):
        parts = ["todo", self.who, self.description, self.state.get_name()]
        if self.state.needs_date():
664
            parts.append(date_filter_short(self.date))
665
666
667
        parts.append("id {}".format(self.get_id()))
        return "[{}]".format(";".join(parts))

Robin Sonnabend's avatar
Robin Sonnabend committed
668
class TodoProtocolAssociation(DatabaseModel):
669
670
671
672
    __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
673
class Decision(DatabaseModel):
674
    __tablename__ = "decisions"
Robin Sonnabend's avatar
Robin Sonnabend committed
675
    __model_name__ = "decision"
676
677
678
679
    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
680
681
    document = relationship("DecisionDocument", backref=backref("decision"), cascade="all, delete-orphan", uselist=False)

682
    categories = relationship("DecisionCategory", secondary="decisioncategoryassociations")
Robin Sonnabend's avatar
Robin Sonnabend committed
683
684
    likes = relationship("Like", secondary="likedecisionassociations")

Robin Sonnabend's avatar
Robin Sonnabend committed
685
686
687
    def get_parent(self):
        return self.protocol

688
689
690
    def get_categories_str(self):
        return ", ".join(map(lambda c: c.name, self.categories))

691
692
693
694
695
696
697
698
699
700
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

701
702
703
704
705
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
706
class MeetingReminder(DatabaseModel):
707
    __tablename__ = "meetingreminders"
Robin Sonnabend's avatar
Robin Sonnabend committed
708
    __model_name__ = "meetingreminder"
709
710
    id = db.Column(db.Integer, primary_key=True)
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
711
    days_before = db.Column(db.Integer)
712
713
    send_public = db.Column(db.Boolean)
    send_private = db.Column(db.Boolean)
Robin Sonnabend's avatar
Robin Sonnabend committed
714
    additional_text = db.Column(db.String)
715

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

class Error(DatabaseModel):
720
    __tablename__ = "errors"
Robin Sonnabend's avatar
Robin Sonnabend committed
721
    __model_name__ = "error"
722
723
724
725
726
727
728
    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
729
730
731
    def get_parent(self):
        return self.protocol

Robin Sonnabend's avatar
Robin Sonnabend committed
732
    def get_short_description(self):
733
734
        if not self.description:
            return ""
Robin Sonnabend's avatar
Robin Sonnabend committed
735
736
737
        lines = self.description.splitlines()
        if len(lines) <= 4:
            return "\n".join(lines)
738
        return "\n".join(["\n".join(lines[:2]), "…", "\n".join(lines[-2:])])
Robin Sonnabend's avatar
Robin Sonnabend committed
739

Robin Sonnabend's avatar
Robin Sonnabend committed
740
class TodoMail(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
741
    __tablename__ = "todomails"
Robin Sonnabend's avatar
Robin Sonnabend committed
742
    __model_name__ = "todomail"
Robin Sonnabend's avatar
Robin Sonnabend committed
743
744
745
746
747
748
    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)
749

Robin Sonnabend's avatar
Robin Sonnabend committed
750
class OldTodo(DatabaseModel):
751
    __tablename__ = "oldtodos"
Robin Sonnabend's avatar
Robin Sonnabend committed
752
    __model_name__ = "oldtodo"
753
754
755
756
757
758
    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
759
class DefaultMeta(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
760
    __tablename__ = "defaultmetas"
Robin Sonnabend's avatar
Robin Sonnabend committed
761
    __model_name__ = "defaultmeta"
Robin Sonnabend's avatar
Robin Sonnabend committed
762
763
764
765
    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)
766
    value = db.Column(db.String)
767
    internal = db.Column(db.Boolean)
768
    prior = db.Column(db.Boolean, default=False, nullable=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
769

Robin Sonnabend's avatar
Robin Sonnabend committed
770
771
772
773
    def get_parent(self):
        return self.protocoltype

class Meta(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
774
    __tablename__ = "metas"
Robin Sonnabend's avatar
Robin Sonnabend committed
775
    __model_name__ = "meta"
Robin Sonnabend's avatar
Robin Sonnabend committed
776
777
778
779
    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)
780
    internal = db.Column(db.Boolean)
Robin Sonnabend's avatar
Robin Sonnabend committed
781

Robin Sonnabend's avatar
Robin Sonnabend committed
782
783
784
    def get_parent(self):
        return self.protocol

Robin Sonnabend's avatar
Robin Sonnabend committed
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
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
812
813
ALL_MODELS = [
    ProtocolType, Protocol, DefaultTOP, TOP, Document, DecisionDocument,
814
    Todo, Decision, MeetingReminder, Error, DefaultMeta, Meta, DecisionCategory
Robin Sonnabend's avatar
Robin Sonnabend committed
815
]