Passed
Push — master ( 212cd8...309224 )
by Alexander
02:03 queued 11s
created

things3_to_kanban   A

Complexity

Total Complexity 11

Size/Duplication

Total Lines 172
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 11
eloc 102
dl 0
loc 172
rs 10
c 0
b 0
f 0

6 Functions

Rating   Name   Duplication   Size   Complexity  
A write_html_footer() 0 8 1
B write_html_column() 0 33 5
A anonymize() 0 8 2
A write_html_columns() 0 9 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.0.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/com.culturedcode.ThingsMac/Data/Library/'\
26
              'Application Support/Cultured Code/Things/Things.sqlite3'\
27
    if not environ.get('THINGSDB') else environ.get('THINGSDB')
28
ANONYMIZE = bool(environ.get('ANONYMIZE'))
29
TAG_WAITING = "Waiting" if not environ.get('TAG_WAITING') \
30
    else environ.get('TAG_WAITING')
31
32
# Basic variables
33
FILE_SQLITE = expanduser(FILE_SQLITE)
34
FILE_HTML = dirname(realpath(__file__)) + '/kanban.html'
35
36
CURSOR = sqlite3.connect(FILE_SQLITE).cursor()
37
38
# Database layout info
39
TASKTABLE = "TMTask"
40
AREATABLE = "TMArea"
41
TAGTABLE = "TMTag"
42
TASKTAGTABLE = "TMTaskTag"
43
ISNOTTRASHED = "TASK.trashed = 0"
44
ISTRASHED = "TASK.trashed = 1"
45
ISOPEN = "TASK.status = 0"
46
ISNOTSTARTED = "TASK.start = 0"
47
ISCANCELLED = "TASK.status = 2"
48
ISCOMPLETED = "TASK.status = 3"
49
ISSTARTED = "TASK.start = 1"
50
ISPOSTPONED = "TASK.start = 2"
51
ISTASK = "TASK.type = 0"
52
ISPROJECT = "TASK.type = 1"
53
ISHEADING = "TASK.type = 2"
54
ISOPENTASK = ISTASK + " AND " + ISNOTTRASHED + " AND " + ISOPEN
55
56
# Queries
57
LIST_SOMEDAY = ISOPENTASK + " AND " + ISPOSTPONED
58
LIST_INBOX = ISOPENTASK + " AND " + ISNOTSTARTED
59
LIST_ANYTIME = ISOPENTASK + " AND " + ISSTARTED + \
60
    " AND (TASK.area NOT NULL OR TASK.project in (SELECT uuid FROM " + \
61
    TASKTABLE + \
62
    " WHERE uuid=TASK.project AND " + ISSTARTED + \
63
    " AND " + ISNOTTRASHED + "))"
64
LIST_TODAY = ISOPENTASK + " AND " + ISSTARTED + \
65
    " AND TASK.startdate is NOT NULL"
66
LIST_UPCOMING = ISOPENTASK + " AND " + ISPOSTPONED + \
67
    " AND (TASK.startDate NOT NULL OR TASK.recurrenceRule NOT NULL)"
68
LIST_WAITING = ISOPENTASK + \
69
    " AND TAGS.tags=(SELECT uuid FROM " + TAGTABLE + \
70
    " WHERE title='" + TAG_WAITING + "')"
71
72
73
def anonymize(word):
74
    """Scramble output for screenshots."""
75
76
    if ANONYMIZE is True:
77
        word = list(word)
78
        shuffle(word)
79
        word = ''.join(word)
80
    return word
81
82
83
def write_html_column(uid, file, title, sql):
84
    """Create a column in the output."""
85
86
    sql = """
87
        SELECT
88
            TASK.uuid, TASK.title, PROJECT.title,
89
            PROJECT.uuid, TASK.dueDate
90
        FROM
91
            TMTask AS TASK
92
        LEFT JOIN
93
            TMTaskTag TAGS ON TAGS.tasks = TASK.uuid
94
        LEFT OUTER JOIN
95
            TMTask PROJECT ON TASK.project = PROJECT.uuid
96
        WHERE """ + sql + """
97
        ORDER BY
98
            TASK.duedate, TASK.startdate, TASK.todayIndex"""
99
    CURSOR.execute(sql)
100
    rows = CURSOR.fetchall()
101
102
    file.write('<div id="left' + str(uid) + '"><div class="inner"><h2>' +
103
               title + ' <span class="size">' +
104
               str(len(rows)) + '</span></h2>')
105
    for row in rows:
106
        project_name = '<a href="things:///show?id=' + \
107
            row[3] + '">' + anonymize(str(row[2])) + \
108
            '</a>' if row[2] is not None else 'None'
109
        css_class = 'hasProject' if row[2] is not None else 'hasNoProject'
110
        css_class = 'hasDeadline' if row[4] is not None else css_class
111
        file.write('<div id="box">' +
112
                   '<a href="things:///show?id=' + row[0] + '">' +
113
                   anonymize(row[1]) + '</a> <div class="area ' +
114
                   css_class + '">' + project_name + '</div></div>')
115
    file.write("</div></div>")
116
117
118
def write_html_header(file):
119
    """Write HTML header."""
120
121
    message = """<head>
122
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
123
        <link rel="stylesheet" href="../resources/style.css">
124
        </head>
125
126
        <body>
127
        <img id="logo" src="../resources/logo.png" alt="logo" />
128
        """
129
    file.write(message)
130
131
132
def write_html_footer(file):
133
    """Write HTML footer."""
134
135
    message = """
136
        <div id="foot"><br />
137
        Copyright &copy;2018 Luc Beaulieu / 2020 Alexander Willner
138
        </div></body></html>"""
139
    file.write(message)
140
141
142
def write_html_columns(file):
143
    """Write HTML columns."""
144
145
    write_html_column(1, file, "Backlog", LIST_SOMEDAY)
146
    write_html_column(2, file, "Upcoming", LIST_UPCOMING)
147
    write_html_column(3, file, "Waiting", LIST_WAITING)
148
    write_html_column(4, file, "Inbox", LIST_INBOX)
149
    write_html_column(5, file, "Today", LIST_TODAY)
150
    write_html_column(6, file, "Next", LIST_ANYTIME)
151
152
153
def main():
154
    """Convert Things 3 database to Kanban HTML view."""
155
156
    file = codecs.open(FILE_HTML, 'w', 'utf-8')
157
158
    write_html_header(file)
159
160
    write_html_columns(file)
161
162
    write_html_footer(file)
163
164
    file.close()
165
    CURSOR.close()
166
167
    webbrowser.open_new_tab('file://' + FILE_HTML)
168
169
170
if __name__ == "__main__":
171
    main()
172