Passed
Push — master ( c0c210...f1da93 )
by Alexander
01:33
created

things3_to_kanban   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 215
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 17
eloc 125
dl 0
loc 215
rs 10
c 0
b 0
f 0

6 Functions

Rating   Name   Duplication   Size   Complexity  
A write_html_footer() 0 8 1
C write_html_column() 0 59 11
A anonymize() 0 8 2
A write_html_columns() 0 10 1
A main() 0 15 1
A write_html_header() 0 12 1
1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
4
"""KanbanView for Things 3."""
5
6
from __future__ import print_function
7
8
__author__ = "Luc Beaulieu and Alexander Willner"
9
__copyright__ = "Copyright 2018 Luc Beaulieu / 2020 Alexander Willner"
10
__credits__ = ["Luc Beaulieu", "Alexander Willner"]
11
__license__ = "unknown"
12
__version__ = "1.1.0"
13
__maintainer__ = "Alexander Willner"
14
__email__ = "[email protected]"
15
__status__ = "Development"
16
17
import sqlite3
18
import webbrowser
19
import codecs
20
from os.path import expanduser, dirname, realpath
21
from os import environ
22
from random import shuffle
23
24
# Basic config
25
FILE_SQLITE = '~/Library/Containers/'\
26
              'com.culturedcode.ThingsMac.beta/Data/Library/'\
27
              'Application Support/Cultured Code/Things/Things.sqlite3'\
28
    if not environ.get('THINGSDB') else environ.get('THINGSDB')
29
ANONYMIZE = bool(environ.get('ANONYMIZE'))
30
TAG_WAITING = "Waiting" if not environ.get('TAG_WAITING') \
31
    else environ.get('TAG_WAITING')
32
TAG_MIT = "MIT" if not environ.get('TAG_MIT') \
33
    else environ.get('TAG_MIT')
34
35
# Basic variables
36
FILE_SQLITE = expanduser(FILE_SQLITE)
37
FILE_HTML = dirname(realpath(__file__)) + '/kanban.html'
38
39
CURSOR = sqlite3.connect(FILE_SQLITE).cursor()
40
41
# Database layout info
42
TASKTABLE = "TMTask"
43
AREATABLE = "TMArea"
44
TAGTABLE = "TMTag"
45
TASKTAGTABLE = "TMTaskTag"
46
ISNOTTRASHED = "TASK.trashed = 0"
47
ISTRASHED = "TASK.trashed = 1"
48
ISOPEN = "TASK.status = 0"
49
ISNOTSTARTED = "TASK.start = 0"
50
ISCANCELLED = "TASK.status = 2"
51
ISCOMPLETED = "TASK.status = 3"
52
ISSTARTED = "TASK.start = 1"
53
ISPOSTPONED = "TASK.start = 2"
54
ISTASK = "TASK.type = 0"
55
ISPROJECT = "TASK.type = 1"
56
ISHEADING = "TASK.type = 2"
57
ISOPENTASK = ISTASK + " AND " + ISNOTTRASHED + " AND " + ISOPEN
58
59
# Queries
60
LIST_SOMEDAY = ISOPENTASK + " AND " + ISPOSTPONED + \
61
    " AND TASK.startdate IS NULL AND TASK.recurrenceRule IS NULL" + \
62
    " ORDER BY TASK.duedate DESC, TASK.creationdate DESC"
63
LIST_INBOX = ISOPENTASK + " AND " + ISNOTSTARTED + \
64
    " ORDER BY TASK.duedate DESC , TASK.todayIndex"
65
LIST_ANYTIME = ISOPENTASK + " AND " + ISSTARTED + \
66
    " AND TASK.startdate is NULL" + \
67
    " AND (TASK.area NOT NULL OR TASK.project in (SELECT uuid FROM " + \
68
    TASKTABLE + \
69
    " WHERE uuid=TASK.project AND start=1" + \
70
    " AND trashed=0))" + \
71
    " ORDER BY TASK.duedate DESC , TASK.todayIndex"
72
LIST_TODAY = ISOPENTASK + " AND " + ISSTARTED + \
73
    " AND TASK.startdate is NOT NULL" + \
74
    " ORDER BY TASK.duedate DESC , TASK.todayIndex"
75
LIST_UPCOMING = ISOPENTASK + " AND " + ISPOSTPONED + \
76
    " AND (TASK.startDate NOT NULL OR TASK.recurrenceRule NOT NULL)" + \
77
    " ORDER BY TASK.startdate, TASK.todayIndex"
78
LIST_WAITING = ISOPENTASK + \
79
    " AND TAGS.tags=(SELECT uuid FROM " + TAGTABLE + \
80
    " WHERE title='" + TAG_WAITING + "')" + \
81
    " ORDER BY TASK.duedate DESC , TASK.todayIndex"
82
LIST_MIT = ISOPENTASK + " AND " + ISSTARTED + \
83
    " AND TASK.startdate is NOT NULL" + \
84
    " AND TAGS.tags=(SELECT uuid FROM " + TAGTABLE + \
85
    " WHERE title='" + TAG_MIT + "')" + \
86
    " ORDER BY TASK.duedate DESC , TASK.todayIndex"
87
88
89
def anonymize(word):
90
    """Scramble output for screenshots."""
91
92
    if ANONYMIZE is True:
93
        word = list(word)
94
        shuffle(word)
95
        word = ''.join(word)
96
    return word
97
98
99
def write_html_column(uid, file, header, sql):
100
    """Create a column in the output."""
101
102
    sql = """
103
        SELECT DISTINCT
104
            TASK.uuid,
105
            TASK.title,
106
            CASE
107
                WHEN AREA.title IS NOT NULL THEN AREA.title
108
                WHEN PROJECT.title IS NOT NULL THEN PROJECT.title
109
                WHEN HEADING.title IS NOT NULL THEN HEADING.title
110
            END,
111
            CASE
112
                WHEN AREA.uuid IS NOT NULL THEN AREA.uuid
113
                WHEN PROJECT.uuid IS NOT NULL THEN PROJECT.uuid
114
            END,
115
            CASE
116
                WHEN TASK.recurrenceRule IS NULL
117
                THEN date(TASK.dueDate,"unixepoch")
118
            ELSE NULL
119
            END
120
        FROM
121
            TMTask AS TASK
122
        LEFT JOIN
123
            TMTaskTag TAGS ON TAGS.tasks = TASK.uuid
124
        LEFT OUTER JOIN
125
            TMTask PROJECT ON TASK.project = PROJECT.uuid
126
        LEFT OUTER JOIN
127
            TMArea AREA ON TASK.area = AREA.uuid
128
        LEFT OUTER JOIN
129
            TMTask HEADING ON TASK.actionGroup = HEADING.uuid
130
        WHERE """ + sql
131
    CURSOR.execute(sql)
132
    rows = CURSOR.fetchall()
133
134
    file.write('<div id="left' + str(uid) + '"><div class="inner"><h2>' +
135
               header + ' <span class="size">' +
136
               str(len(rows)) + '</span></h2>')
137
138
    for row in rows:
139
        task_uuid = str(row[0]) if row[0] is not None else ''
140
        task_title = anonymize(str(row[1])) if row[1] is not None else ''
141
        context_title = anonymize(str(row[2])) if row[2] is not None else ''
142
        context_uuid = str(row[3]) if row[3] is not None else ''
143
        deadline = str(row[4]) if row[4] is not None else ''
144
145
        task_link = '<a href="things:///show?id=' + task_uuid + '">' + \
146
            task_title + '</a>' if task_uuid != '' else task_title
147
        context_link = '<a href="things:///show?id=' + context_uuid + '">' + \
148
            context_title + '</a>' if context_uuid != '' else context_title
149
        css_class = 'hasProject' if context_title != '' else 'hasNoProject'
150
        css_class = 'hasDeadline' if deadline != '' else css_class
151
152
        file.write('<div id="box">' + task_link +
153
                   '<div class="deadline">' + deadline + '</div>' +
154
                   '<div class="area ' + css_class + '">' + context_link +
155
                   '</div>' +
156
                   '</div>')
157
    file.write("</div></div>")
158
159
160
def write_html_header(file):
161
    """Write HTML header."""
162
163
    message = """<head>
164
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
165
        <link rel="stylesheet" href="../resources/style.css">
166
        </head>
167
168
        <body>
169
        <img id="logo" src="../resources/logo.png" alt="logo" />
170
        """
171
    file.write(message)
172
173
174
def write_html_footer(file):
175
    """Write HTML footer."""
176
177
    message = """
178
        <div id="foot"><br />
179
        Copyright &copy;2018 Luc Beaulieu / 2020 Alexander Willner
180
        </div></body></html>"""
181
    file.write(message)
182
183
184
def write_html_columns(file):
185
    """Write HTML columns."""
186
187
    write_html_column(1, file, "Backlog", LIST_SOMEDAY)
188
    write_html_column(2, file, "Upcoming", LIST_UPCOMING)
189
    write_html_column(3, file, "Waiting", LIST_WAITING)
190
    write_html_column(4, file, "Inbox", LIST_INBOX)
191
    write_html_column(5, file, "MIT", LIST_MIT)
192
    write_html_column(6, file, "Today", LIST_TODAY)
193
    write_html_column(7, file, "Next", LIST_ANYTIME)
194
195
196
def main():
197
    """Convert Things 3 database to Kanban HTML view."""
198
199
    file = codecs.open(FILE_HTML, 'w', 'utf-8')
200
201
    write_html_header(file)
202
203
    write_html_columns(file)
204
205
    write_html_footer(file)
206
207
    file.close()
208
    CURSOR.close()
209
210
    webbrowser.open_new_tab('file://' + FILE_HTML)
211
212
213
if __name__ == "__main__":
214
    main()
215