database.py 30.7 KB
Newer Older
1
from flask import render_template
2

3
4
from datetime import datetime
from io import BytesIO
5
from enum import Enum
6
from uuid import uuid4
7

8
9
from shared import (
    db, date_filter_short, escape_tex, DATE_KEY, START_TIME_KEY, END_TIME_KEY,
10
    current_user, config)
11
from utils import get_etherpad_url, split_terms, check_ip_in_networks
Robin Sonnabend's avatar
Robin Sonnabend committed
12
from models.errors import DateNotMatchingException
13
from dateutil import tz
14

15
import os
16

17
from sqlalchemy import event
18
from sqlalchemy.orm import relationship, backref
19

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

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))

51
52
53
54
    @classmethod
    def first_by_id(cls, instance_id):
        return cls.query.filter_by(id=instance_id).first()

55

Robin Sonnabend's avatar
Robin Sonnabend committed
56
class ProtocolType(DatabaseModel):
57
    __tablename__ = "protocoltypes"
Robin Sonnabend's avatar
Robin Sonnabend committed
58
    __model_name__ = "protocoltype"
59
60
61
62
    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
63
    usual_time = db.Column(db.Time)
64
    is_public = db.Column(db.Boolean)
65
    modify_group = db.Column(db.String)
66
67
    private_group = db.Column(db.String)
    public_group = db.Column(db.String)
Robin Sonnabend's avatar
Robin Sonnabend committed
68
    publish_group = db.Column(db.String)
69
70
    private_mail = db.Column(db.String)
    public_mail = db.Column(db.String)
71
    non_reproducible_pad_links = db.Column(db.Boolean)
Robin Sonnabend's avatar
Robin Sonnabend committed
72
73
74
    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
75
    printer = db.Column(db.String)
Robin Sonnabend's avatar
Robin Sonnabend committed
76
    calendar = db.Column(db.String)
77
78
    restrict_networks = db.Column(db.Boolean)
    allowed_networks = db.Column(db.String)
79
    latex_template = db.Column(db.String)
80

81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
    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")
    reminders = relationship(
        "MeetingReminder", backref=backref("protocoltype"),
        cascade="all, delete-orphan", order_by="MeetingReminder.days_before")
    todos = relationship(
        "Todo", backref=backref("protocoltype"), order_by="Todo.id")
    metas = relationship(
        "DefaultMeta", backref=backref("protocoltype"),
        cascade="all, delete-orphan")
    decisioncategories = relationship(
        "DecisionCategory", backref=backref("protocoltype"),
        cascade="all, delete-orphan")
98

Robin Sonnabend's avatar
Robin Sonnabend committed
99
    def get_latest_protocol(self):
100
101
102
        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
103
104
105
106
        if len(candidates) == 0:
            return None
        return candidates[0]

107
108
109
110
111
112
    def get_protocols_on_date(self, protocol_date):
        return [
            protocol for protocol in self.protocols
            if protocol.date == protocol_date
        ]

113
    def has_public_view_right(self, user, check_networks=True):
114
115
116
117
        return (
            self.has_public_anonymous_view_right(check_networks=check_networks)
            or (user is not None
                and self.has_public_authenticated_view_right(user))
Robin Sonnabend's avatar
Robin Sonnabend committed
118
            or self.has_admin_right(user))
119

120
    def has_public_anonymous_view_right(self, check_networks=True):
121
122
        return (
            self.is_public
123
            and ((not self.restrict_networks or not check_networks)
124
                 or check_ip_in_networks(self.allowed_networks)))
125
126

    def has_public_authenticated_view_right(self, user):
127
128
129
130
        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
131
132

    def has_private_view_right(self, user):
133
134
135
136
        return (
            (user is not None
             and (self.private_group != ""
                  and self.private_group in user.groups))
Robin Sonnabend's avatar
Robin Sonnabend committed
137
            or self.has_admin_right(user))
Robin Sonnabend's avatar
Robin Sonnabend committed
138
139

    def has_modify_right(self, user):
140
141
142
143
        return (
            (user is not None
             and (self.modify_group != ""
                  and self.modify_group in user.groups))
Robin Sonnabend's avatar
Robin Sonnabend committed
144
            or self.has_admin_right(user))
Robin Sonnabend's avatar
Robin Sonnabend committed
145

Robin Sonnabend's avatar
Robin Sonnabend committed
146
    def has_publish_right(self, user):
147
148
149
150
        return (
            (user is not None
             and (self.publish_group != ""
                  and self.publish_group in user.groups))
Robin Sonnabend's avatar
Robin Sonnabend committed
151
152
            or self.has_admin_right(user))

153
154
155
    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
156
    @staticmethod
Robin Sonnabend's avatar
Robin Sonnabend committed
157
    def get_modifiable_protocoltypes(user):
Robin Sonnabend's avatar
Robin Sonnabend committed
158
159
160
161
162
        return [
            protocoltype for protocoltype in ProtocolType.query.all()
            if protocoltype.has_modify_right(user)
        ]

Robin Sonnabend's avatar
Robin Sonnabend committed
163
    @staticmethod
164
    def get_public_protocoltypes(user, check_networks=True):
Robin Sonnabend's avatar
Robin Sonnabend committed
165
166
        return [
            protocoltype for protocoltype in ProtocolType.query.all()
167
168
            if protocoltype.has_public_view_right(
                user, check_networks=check_networks)
Robin Sonnabend's avatar
Robin Sonnabend committed
169
170
171
172
173
174
175
176
177
        ]

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

178
179
180
181
182
183
    def get_wiki_infobox(self):
        return "Infobox {}".format(self.short_name)

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

184

Robin Sonnabend's avatar
Robin Sonnabend committed
185
class Protocol(DatabaseModel):
186
    __tablename__ = "protocols"
Robin Sonnabend's avatar
Robin Sonnabend committed
187
    __model_name__ = "protocol"
188
189
    id = db.Column(db.Integer, primary_key=True)
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
190
191
192
    source = db.Column(db.String)
    content_public = db.Column(db.String)
    content_private = db.Column(db.String)
193
194
    content_html_public = db.Column(db.String)
    content_html_private = db.Column(db.String)
195
196
197
    date = db.Column(db.Date)
    start_time = db.Column(db.Time)
    end_time = db.Column(db.Time)
198
    done = db.Column(db.Boolean, nullable=False, default=False)
199
    public = db.Column(db.Boolean)
200
    pad_identifier = db.Column(db.String)
201

202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
    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")
    metas = relationship(
        "Meta", backref=backref("protocol"), cascade="all, delete-orphan")
    localtops = relationship(
        "LocalTOP", backref=backref("protocol"), cascade="all, delete-orphan")
218

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

Robin Sonnabend's avatar
Robin Sonnabend committed
221
222
223
    def get_parent(self):
        return self.protocoltype

224
225
    def create_error(self, action, name, description):
        now = datetime.now()
226
227
        return Error(
            protocol_id=self.id, action=action, name=name,
228
            datetime=now, description=description)
229

230
231
232
    def create_localtops(self):
        local_tops = []
        for default_top in self.protocoltype.default_tops:
233
234
235
            local_tops.append(LocalTOP(
                defaulttop_id=default_top.id, protocol_id=self.id,
                description=default_top.description or ""))
236
237
        return local_tops

238
    def fill_from_remarks(self, remarks):
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
        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:
262
263
                    raise DateNotMatchingException(
                        original_date=self.date, protocol_date=new_date)
264
265
266
267
268
269
            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
270
271
272
273
274
275
276
        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()
277
278
279
                meta = Meta(
                    protocol_id=self.id, name=default_meta.name, value=value,
                    internal=default_meta.internal)
Robin Sonnabend's avatar
Robin Sonnabend committed
280
281
                db.session.add(meta)
        db.session.commit()
282

283
284
285
286
287
288
    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)
        )

289
290
291
292
293
    def get_visible_content(self, user):
        if self.has_private_view_right(user):
            return self.content_private
        return self.content_public

Robin Sonnabend's avatar
Robin Sonnabend committed
294
295
296
    def is_done(self):
        return self.done

297
298
    def get_state_glyph(self):
        if self.is_done():
299
            state = "unchecked"  # Fertig
300
            if self.public:
301
                state = "check"  # Veröffentlicht
302
        else:
303
            state = "pencil"  # Geplant
304
305
306
307
308
309
310
311
312
313
314
        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
315
    def get_identifier(self):
Administrator's avatar
Administrator committed
316
317
        if self.pad_identifier is not None:
            return self.pad_identifier
Robin Sonnabend's avatar
Robin Sonnabend committed
318
319
        if self.date is None:
            return None
320
321
322
        return self.get_short_identifier()

    def get_short_identifier(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
323
324
325
326
        return "{}-{}".format(
            self.protocoltype.short_name.lower(),
            self.date.strftime("%y-%m-%d"))

327
328
329
330
    def get_title(self):
        return "Protokoll: {}, {:%d.%m.%Y}".format(
            self.protocoltype.short_name, self.date)

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

Robin Sonnabend's avatar
Robin Sonnabend committed
335
    def get_etherpad_link(self):
Administrator's avatar
Administrator committed
336
337
338
        if self.pad_identifier is None:
            identifier = self.get_identifier()
            if self.protocoltype.non_reproducible_pad_links:
339
340
341
                identifier = "{}-{}".format(
                    identifier,
                    str(uuid4()).replace("-", ""))[:50]
Administrator's avatar
Administrator committed
342
343
344
            self.pad_identifier = identifier
            db.session.commit()
        return get_etherpad_url(self.pad_identifier)
Robin Sonnabend's avatar
Robin Sonnabend committed
345

346
347
348
349
350
    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
351
    def get_datetime(self):
352
        time = self.get_time()
353
354
355
        return datetime(
            self.date.year, self.date.month, self.date.day, time.hour,
            time.minute)
Robin Sonnabend's avatar
Robin Sonnabend committed
356

Robin Sonnabend's avatar
Robin Sonnabend committed
357
358
359
360
    def has_nonplanned_tops(self):
        return len([top for top in self.tops if not top.planned]) > 0

    def get_originating_todos(self):
361
362
363
364
        return [
            todo for todo in self.todos
            if self == todo.get_first_protocol()
        ]
365

366
367
368
369
370
371
    def get_open_todos(self):
        return [
            todo for todo in self.protocoltype.todos
            if not todo.is_done()
        ]

372
    def has_compiled_document(self, private=None):
Robin Sonnabend's avatar
Robin Sonnabend committed
373
374
375
        candidates = [
            document for document in self.documents
            if document.is_compiled
376
            and (private is None or document.is_private == private)
Robin Sonnabend's avatar
Robin Sonnabend committed
377
378
379
380
381
382
383
        ]
        return len(candidates) > 0

    def get_compiled_document(self, private=None):
        candidates = [
            document for document in self.documents
            if document.is_compiled
384
385
386
387
388
389
390
391
        ]
        private_candidates = [
            document for document in candidates
            if document.is_private
        ]
        public_candidates = [
            document for document in candidates
            if not document.is_private
Robin Sonnabend's avatar
Robin Sonnabend committed
392
        ]
393
394
395
396
397
398
399
400

        def _get_candidates():
            if private is None or private:
                return private_candidates + public_candidates
            return public_candidates
        candidates = _get_candidates()
        if candidates:
            return candidates[0]
Robin Sonnabend's avatar
Robin Sonnabend committed
401
402
        return None

Robin Sonnabend's avatar
Robin Sonnabend committed
403
404
405
    def get_template(self):
        return render_template("protocol-template.txt", protocol=self)

Robin Sonnabend's avatar
Robin Sonnabend committed
406
407
408
    def delete_orphan_todos(self):
        orphan_todos = [
            todo for todo in self.todos
Robin Sonnabend's avatar
Robin Sonnabend committed
409
            if len(todo.protocols) <= 1
Robin Sonnabend's avatar
Robin Sonnabend committed
410
411
412
413
414
        ]
        for todo in orphan_todos:
            self.todos.remove(todo)
            db.session.delete(todo)

415
416
417
418
419
420
421
422
423
424
425
    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

426
427
428
429
    def get_timezone_aware_start_date(self):
        return datetime.combine(self.date, self.get_time()).replace(
            tzinfo=tz.tzlocal())

430
    @staticmethod
431
432
433
434
435
436
    def create_new_protocol(
            protocoltype, date, start_time=None, allow_duplicate=False):
        if not allow_duplicate:
            duplicate_candidates = protocoltype.get_protocols_on_date(date)
            if duplicate_candidates:
                return duplicate_candidates[0]
437
438
        if start_time is None:
            start_time = protocoltype.usual_time
439
440
        protocol = Protocol(
            protocoltype_id=protocoltype.id, date=date, start_time=start_time)
441
442
443
444
445
446
        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:
447
448
                meta = Meta(
                    protocol_id=protocol.id, name=default_meta.name,
449
450
451
452
453
454
455
456
                    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
457
458
459
460
461
@event.listens_for(Protocol, "before_delete")
def on_protocol_delete(mapper, connection, protocol):
    protocol.delete_orphan_todos()


Robin Sonnabend's avatar
Robin Sonnabend committed
462
class DefaultTOP(DatabaseModel):
463
    __tablename__ = "defaulttops"
Robin Sonnabend's avatar
Robin Sonnabend committed
464
    __model_name__ = "defaulttop"
465
466
467
468
    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)
469
    description = db.Column(db.String)
470

471
472
473
    localtops = relationship(
        "LocalTOP", backref=backref("defaulttop"),
        cascade="all, delete-orphan")
474

Robin Sonnabend's avatar
Robin Sonnabend committed
475
476
477
    def get_parent(self):
        return self.protocoltype

478
    def is_at_end(self):
Robin Sonnabend's avatar
Robin Sonnabend committed
479
        return self.number > 0
480

481
    def get_localtop(self, protocol):
482
483
        return LocalTOP.query.filter_by(
            defaulttop_id=self.id, protocol_id=protocol.id).first()
484
485
486

    def get_top(self, protocol):
        localtop = self.get_localtop(protocol)
487
488
        top = TOP(
            protocol_id=protocol.id, name=self.name,
489
490
491
            description=localtop.description)
        return top

492

Robin Sonnabend's avatar
Robin Sonnabend committed
493
class TOP(DatabaseModel):
494
    __tablename__ = "tops"
Robin Sonnabend's avatar
Robin Sonnabend committed
495
    __model_name__ = "top"
496
497
498
    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
499
    number = db.Column(db.Integer)
500
    planned = db.Column(db.Boolean)
501
    description = db.Column(db.String)
502

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

Robin Sonnabend's avatar
Robin Sonnabend committed
505
506
507
    def get_parent(self):
        return self.protocol

508

509
510
511
512
513
514
515
516
517
518
519
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

520
521
    def is_expandable(self):
        user = current_user()
522
523
        return (
            self.has_private_view_right(user)
524
525
526
527
528
529
530
531
532
            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

533

Robin Sonnabend's avatar
Robin Sonnabend committed
534
class Document(DatabaseModel):
535
    __tablename__ = "documents"
Robin Sonnabend's avatar
Robin Sonnabend committed
536
    __model_name__ = "document"
537
538
539
    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
540
    filename = db.Column(db.String)
541
    is_compiled = db.Column(db.Boolean)
542
    is_private = db.Column(db.Boolean)
543

Robin Sonnabend's avatar
Robin Sonnabend committed
544
545
546
    def get_parent(self):
        return self.protocol

547
548
549
    def get_filename(self):
        return os.path.join(config.DOCUMENTS_PATH, self.filename)

550
551
552
553
    def as_file_like(self):
        with open(self.get_filename(), "rb") as file:
            return BytesIO(file.read())

554

555
@event.listens_for(Document, "before_delete")
Robin Sonnabend's avatar
Robin Sonnabend committed
556
def on_document_delete(mapper, connection, document):
557
    if document.filename is not None:
Robin Sonnabend's avatar
Robin Sonnabend committed
558
        document_path = document.get_filename()
559
560
        if os.path.isfile(document_path):
            os.remove(document_path)
561

562

Robin Sonnabend's avatar
Robin Sonnabend committed
563
class DecisionDocument(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
564
    __tablename__ = "decisiondocuments"
Robin Sonnabend's avatar
Robin Sonnabend committed
565
    __model_name__ = "decisiondocument"
Robin Sonnabend's avatar
Robin Sonnabend committed
566
567
568
569
570
    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
571
572
573
    def get_parent(self):
        return self.decision

Robin Sonnabend's avatar
Robin Sonnabend committed
574
575
576
577
578
579
580
    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())

581

Robin Sonnabend's avatar
Robin Sonnabend committed
582
583
584
585
586
587
588
@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)

589

590
591
592
593
594
595
596
597
598
599
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
600

601
602
603
    def get_name(self):
        STATE_TO_NAME, NAME_TO_STATE = make_states(TodoState)
        return STATE_TO_NAME[self]
604

Robin Sonnabend's avatar
Robin Sonnabend committed
605
606
607
608
609
610
611
612
613
    @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
614
615
616
617
618
619
620
621
622
623
624
625
626
627

    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
628

629
630
631
632
633
634
635
636
637
638
639
640
    @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()
641
642
643
644
        if " " not in name:
            raise ValueError(
                "{} does not contain a state and a date".format(
                    name))
645
646
647
648
649
650
651
652
653
654
655
656
657
658
        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
659
660
                    date = datetime(
                        year=year, month=date.month, day=date.day).date()
661
662
663
664
665
666
667
668
669
                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
670
class Todo(DatabaseModel):
671
    __tablename__ = "todos"
Robin Sonnabend's avatar
Robin Sonnabend committed
672
    __model_name__ = "todo"
673
    id = db.Column(db.Integer, primary_key=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
674
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
Robin Sonnabend's avatar
Robin Sonnabend committed
675
    number = db.Column(db.Integer)
676
677
    who = db.Column(db.String)
    description = db.Column(db.String)
678
679
    state = db.Column(db.Enum(TodoState), nullable=False)
    date = db.Column(db.Date, nullable=True)
680

681
682
    protocols = relationship(
        "Protocol", secondary="todoprotocolassociations", backref="todos")
Robin Sonnabend's avatar
Robin Sonnabend committed
683
    likes = relationship("Like", secondary="liketodoassociations")
684

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

688
    def is_done(self):
689
690
691
        if self.state.needs_date():
            if self.state == TodoState.after:
                return datetime.now().date() <= self.date
692
693
            elif self.state == TodoState.before:
                return datetime.now().date() >= self.date
694
        return self.state.is_done()
Robin Sonnabend's avatar
Robin Sonnabend committed
695
696
697

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

Robin Sonnabend's avatar
Robin Sonnabend committed
699
700
701
702
703
704
    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
705
706
707
708
709
710
    def get_users(self):
        return [
            user.lower().strip()
            for user in split_terms(self.who, separators=" ,\t")
        ]

Robin Sonnabend's avatar
Robin Sonnabend committed
711
    def get_state(self):
712
        return "[{}]".format(self.get_state_plain())
713

Robin Sonnabend's avatar
Robin Sonnabend committed
714
    def get_state_plain(self):
715
716
        result = self.state.get_name()
        if self.state.needs_date():
717
            result = "{} {}".format(result, date_filter_short(self.date))
718
        return result
719

Robin Sonnabend's avatar
Robin Sonnabend committed
720
721
722
723
724
725
726
    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
727

728
    def render_html(self, current_protocol=None):
Robin Sonnabend's avatar
Robin Sonnabend committed
729
730
731
732
733
734
735
        parts = [
            self.get_state(),
            "<strong>{}:</strong>".format(self.who),
            self.description
        ]
        return " ".join(parts)

Robin Sonnabend's avatar
Robin Sonnabend committed
736
    def render_latex(self, current_protocol=None):
737
        return r"\Todo{{{}}}{{{}}}{{{}}}{{{}}}".format(
Robin Sonnabend's avatar
Robin Sonnabend committed
738
            "Neuer Todo" if self.is_new(current_protocol) else "Todo",
739
740
741
            escape_tex(self.who),
            escape_tex(self.description),
            escape_tex(self.get_state_tex())
Robin Sonnabend's avatar
Robin Sonnabend committed
742
743
        )

744
745
746
747
    def render_wikitext(self, current_protocol=None, use_dokuwiki=False):
        bold = "'''"
        if use_dokuwiki:
            bold = "**"
748
749
        return "{0}{1}:{0} {2}: {3} - {4}".format(
            bold,
Robin Sonnabend's avatar
Robin Sonnabend committed
750
751
752
753
754
755
            "Neuer Todo" if self.is_new(current_protocol) else "Todo",
            self.who,
            self.description,
            self.get_state_plain()
        )

756
757
758
    def render_template(self):
        parts = ["todo", self.who, self.description, self.state.get_name()]
        if self.state.needs_date():
759
            parts.append(date_filter_short(self.date))
760
761
762
        parts.append("id {}".format(self.get_id()))
        return "[{}]".format(";".join(parts))

763

Robin Sonnabend's avatar
Robin Sonnabend committed
764
class TodoProtocolAssociation(DatabaseModel):
765
    __tablename__ = "todoprotocolassociations"
766
767
768
769
770
    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)

771

Robin Sonnabend's avatar
Robin Sonnabend committed
772
class Decision(DatabaseModel):
773
    __tablename__ = "decisions"
Robin Sonnabend's avatar
Robin Sonnabend committed
774
    __model_name__ = "decision"
775
776
777
778
    id = db.Column(db.Integer, primary_key=True)
    protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
    content = db.Column(db.String)

779
780
781
    document = relationship(
        "DecisionDocument", backref=backref("decision"),
        cascade="all, delete-orphan", uselist=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
782

783
784
    categories = relationship(
        "DecisionCategory", secondary="decisioncategoryassociations")
Robin Sonnabend's avatar
Robin Sonnabend committed
785
786
    likes = relationship("Like", secondary="likedecisionassociations")

Robin Sonnabend's avatar
Robin Sonnabend committed
787
788
789
    def get_parent(self):
        return self.protocol

790
791
792
    def get_categories_str(self):
        return ", ".join(map(lambda c: c.name, self.categories))

793

794
795
796
797
798
799
800
801
802
803
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

804

805
806
class DecisionCategoryAssociation(DatabaseModel):
    __tablename__ = "decisioncategoryassociations"
807
808
809
810
811
    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)

812

Robin Sonnabend's avatar
Robin Sonnabend committed
813
class MeetingReminder(DatabaseModel):
814
    __tablename__ = "meetingreminders"
Robin Sonnabend's avatar
Robin Sonnabend committed
815
    __model_name__ = "meetingreminder"
816
817
    id = db.Column(db.Integer, primary_key=True)
    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
818
    days_before = db.Column(db.Integer)
819
820
    send_public = db.Column(db.Boolean)
    send_private = db.Column(db.Boolean)
Robin Sonnabend's avatar
Robin Sonnabend committed
821
    additional_text = db.Column(db.String)
822

Robin Sonnabend's avatar
Robin Sonnabend committed
823
824
825
    def get_parent(self):
        return self.protocoltype

826

Robin Sonnabend's avatar
Robin Sonnabend committed
827
class Error(DatabaseModel):
828
    __tablename__ = "errors"
Robin Sonnabend's avatar
Robin Sonnabend committed
829
    __model_name__ = "error"
830
831
832
833
834
835
836
    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
837
838
839
    def get_parent(self):
        return self.protocol

Robin Sonnabend's avatar
Robin Sonnabend committed
840
    def get_short_description(self):
841
842
        if not self.description:
            return ""
Robin Sonnabend's avatar
Robin Sonnabend committed
843
844
845
        lines = self.description.splitlines()
        if len(lines) <= 4:
            return "\n".join(lines)
846
        return "\n".join(["\n".join(lines[:2]), "…", "\n".join(lines[-2:])])
Robin Sonnabend's avatar
Robin Sonnabend committed
847

848

Robin Sonnabend's avatar
Robin Sonnabend committed
849
class TodoMail(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
850
    __tablename__ = "todomails"
Robin Sonnabend's avatar
Robin Sonnabend committed
851
    __model_name__ = "todomail"
Robin Sonnabend's avatar
Robin Sonnabend committed
852
853
854
855
856
857
    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)
858

859

Robin Sonnabend's avatar
Robin Sonnabend committed
860
class OldTodo(DatabaseModel):
861
    __tablename__ = "oldtodos"
Robin Sonnabend's avatar
Robin Sonnabend committed
862
    __model_name__ = "oldtodo"
863
864
865
866
867
868
    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)

869

Robin Sonnabend's avatar
Robin Sonnabend committed
870
class DefaultMeta(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
871
    __tablename__ = "defaultmetas"
Robin Sonnabend's avatar
Robin Sonnabend committed
872
    __model_name__ = "defaultmeta"
Robin Sonnabend's avatar
Robin Sonnabend committed
873
874
875
876
    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)
877
    value = db.Column(db.String)
878
    internal = db.Column(db.Boolean)
879
    prior = db.Column(db.Boolean, default=False, nullable=False)
Robin Sonnabend's avatar
Robin Sonnabend committed
880

Robin Sonnabend's avatar
Robin Sonnabend committed
881
882
883
    def get_parent(self):
        return self.protocoltype

884

Robin Sonnabend's avatar
Robin Sonnabend committed
885
class Meta(DatabaseModel):
Robin Sonnabend's avatar
Robin Sonnabend committed
886
    __tablename__ = "metas"
Robin Sonnabend's avatar
Robin Sonnabend committed
887
    __model_name__ = "meta"
Robin Sonnabend's avatar
Robin Sonnabend committed
888
889
890
891
    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)
892
    internal = db.Column(db.Boolean)
Robin Sonnabend's avatar
Robin Sonnabend committed
893

Robin Sonnabend's avatar
Robin Sonnabend committed
894
895
896
    def get_parent(self):
        return self.protocol

897

Robin Sonnabend's avatar
Robin Sonnabend committed
898
899
900
901
902
903
class Like(DatabaseModel):
    __tablename__ = "likes"
    __model_name__ = "like"
    id = db.Column(db.Integer, primary_key=True)
    who = db.Column(db.String)

904

Robin Sonnabend's avatar
Robin Sonnabend committed
905
906
class LikeProtocolAssociation(DatabaseModel):
    __tablename__ = "likeprotocolassociations"
907
908
909
910
911
    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)

Robin Sonnabend's avatar
Robin Sonnabend committed
912
913
914

class LikeTodoAssociation(DatabaseModel):
    __tablename__ = "liketodoassociations"
915
916
917
918
919
    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)

Robin Sonnabend's avatar
Robin Sonnabend committed
920
921
922

class LikeDecisionAssociation(DatabaseModel):
    __tablename__ = "likedecisionassociations"
923
924
925
926
927
    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)

Robin Sonnabend's avatar
Robin Sonnabend committed
928
929
930

class LikeTOPAssociation(DatabaseModel):
    __tablename__ = "liketopassociations"
931
932
    like_id = db.Column(
        db.Integer, db.ForeignKey("likes.id"), primary_key=True)
Robin Sonnabend's avatar
Robin Sonnabend committed
933
934
935
    top_id = db.Column(db.Integer, db.ForeignKey("tops.id"), primary_key=True)


Robin Sonnabend's avatar
Robin Sonnabend committed
936
937
ALL_MODELS = [
    ProtocolType, Protocol, DefaultTOP, TOP, Document, DecisionDocument,
938
    Todo, Decision, MeetingReminder, Error, DefaultMeta, Meta, DecisionCategory
Robin Sonnabend's avatar
Robin Sonnabend committed
939
]