Passed
Push — master ( 6d0e9b...a52cbb )
by Alexander
10:01
created

things3.things3   F

Complexity

Total Complexity 66

Size/Duplication

Total Lines 782
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 274
dl 0
loc 782
rs 3.12
c 0
b 0
f 0
wmc 66

34 Methods

Rating   Name   Duplication   Size   Complexity  
A Things3.get_trashed() 0 8 1
A Things3.dict_factory() 0 7 2
A Things3.set_config() 0 8 5
A Things3.get_from_config() 0 10 5
A Things3.get_someday() 0 22 1
A Things3.get_waiting() 0 3 1
A Things3.get_config() 0 6 3
F Things3.__init__() 0 60 14
A Things3.get_cleanup() 0 8 1
A Things3.get_empty_projects() 0 25 1
A Things3.get_largest_projects() 0 25 1
A Things3.get_not_implemented() 0 4 1
A Things3.get_upcoming() 0 22 1
A Things3.get_completed() 0 9 1
A Things3.get_anytime() 0 45 2
A Things3.get_areas() 0 18 1
A Things3.anonymize_tasks() 0 7 3
A Things3.mode_task() 0 3 1
A Things3.anonymize_string() 0 9 2
A Things3.get_tag_today() 0 27 1
A Things3.get_inbox() 0 10 1
A Things3.get_rows() 0 61 1
A Things3.get_lint() 0 12 1
A Things3.get_cancelled() 0 9 1
A Things3.get_mit() 0 3 1
A Things3.get_due() 0 20 1
A Things3.get_tag() 0 24 1
A Things3.get_minutes_today() 0 38 1
A Things3.get_daystats() 0 51 1
A Things3.get_all() 0 17 1
A Things3.get_projects() 0 23 1
A Things3.mode_project() 0 3 1
A Things3.get_today() 0 25 1
B Things3.execute_query() 0 21 5

How to fix   Complexity   

Complexity

Complex classes like things3.things3 often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
4
"""Simple read-only API for Things 3."""
5
6
from __future__ import print_function
7
8
__author__ = "Alexander Willner"
9
__copyright__ = "2020 Alexander Willner"
10
__credits__ = ["Alexander Willner"]
11
__license__ = "Apache License 2.0"
12
__version__ = "2.6.2"
13
__maintainer__ = "Alexander Willner"
14
__email__ = "[email protected]"
15
__status__ = "Development"
16
17
import sqlite3
18
import sys
19
from random import shuffle
20
from os import environ, path
21
import getpass
22
import configparser
23
from pathlib import Path
24
25
26
# pylint: disable=R0904,R0902
27
class Things3():
28
    """Simple read-only API for Things 3."""
29
30
    # Database info
31
    FILE_CONFIG = str(Path.home()) + '/.kanbanviewrc'
32
    FILE_DB = '/Library/Group Containers/'\
33
              'JLMPQHK86H.com.culturedcode.ThingsMac/Things.sqlite3'
34
    TABLE_TASK = "TMTask"
35
    TABLE_AREA = "TMArea"
36
    TABLE_TAG = "TMTag"
37
    TABLE_TASKTAG = "TMTaskTag"
38
    DATE_CREATE = "creationDate"
39
    DATE_MOD = "userModificationDate"
40
    DATE_DUE = "dueDate"
41
    DATE_START = "startDate"
42
    DATE_STOP = "stopDate"
43
    IS_INBOX = "start = 0"
44
    IS_ANYTIME = "start = 1"
45
    IS_SOMEDAY = "start = 2"
46
    IS_SCHEDULED = f"{DATE_START} IS NOT NULL"
47
    IS_NOT_SCHEDULED = f"{DATE_START} IS NULL"
48
    IS_DUE = f"{DATE_DUE} IS NOT NULL"
49
    IS_RECURRING = "recurrenceRule IS NOT NULL"
50
    IS_NOT_RECURRING = "recurrenceRule IS NULL"
51
    IS_TASK = "type = 0"
52
    IS_PROJECT = "type = 1"
53
    IS_HEADING = "type = 2"
54
    IS_TRASHED = "trashed = 1"
55
    IS_NOT_TRASHED = "trashed = 0"
56
    IS_OPEN = "status = 0"
57
    IS_CANCELLED = "status = 2"
58
    IS_DONE = "status = 3"
59
    RECURRING_IS_NOT_PAUSED = "instanceCreationPaused = 0"
60
    RECURRING_HAS_NEXT_STARTDATE = "nextInstanceStartDate IS NOT NULL"
61
    MODE_TASK = "type = 0"
62
    MODE_PROJECT = "type = 1"
63
64
    # Variables
65
    debug = False
66
    user = getpass.getuser()
67
    database = f"/Users/{user}/{FILE_DB}"
68
    filter = ""
69
    tag_waiting = "Waiting"
70
    tag_mit = "MIT"
71
    tag_cleanup = "Cleanup"
72
    tag_a = "A"
73
    tag_b = "B"
74
    tag_c = "C"
75
    tag_d = "D"
76
    stat_days = 365
77
    anonymize = False
78
    config = configparser.ConfigParser()
79
    config.read(FILE_CONFIG)
80
81
    # pylint: disable=R0913
82
    def __init__(self,
83
                 database=None,
84
                 tag_waiting=None,
85
                 tag_mit=None,
86
                 tag_cleanup=None,
87
                 tag_a=None,
88
                 tag_b=None,
89
                 tag_c=None,
90
                 tag_d=None,
91
                 stat_days=None,
92
                 anonymize=None):
93
94
        cfg = self.get_from_config(tag_waiting, 'TAG_WAITING')
95
        self.tag_waiting = cfg if cfg else self.tag_waiting
96
        self.set_config('TAG_WAITING', self.tag_waiting)
97
98
        cfg = self.get_from_config(anonymize, 'ANONYMIZE')
99
        self.anonymize = (cfg == 'True') if (cfg == 'True') else self.anonymize
100
        self.set_config('ANONYMIZE', self.anonymize)
101
102
        cfg = self.get_from_config(tag_mit, 'TAG_MIT')
103
        self.tag_mit = cfg if cfg else self.tag_mit
104
        self.set_config('TAG_MIT', self.tag_mit)
105
106
        cfg = self.get_from_config(tag_cleanup, 'TAG_CLEANUP')
107
        self.tag_cleanup = cfg if cfg else self.tag_cleanup
108
        self.set_config('TAG_CLEANUP', self.tag_cleanup)
109
110
        cfg = self.get_from_config(tag_a, 'TAG_A')
111
        self.tag_a = cfg if cfg else self.tag_a
112
        self.set_config('TAG_A', self.tag_a)
113
114
        cfg = self.get_from_config(tag_b, 'TAG_B')
115
        self.tag_b = cfg if cfg else self.tag_b
116
        self.set_config('TAG_B', self.tag_b)
117
118
        cfg = self.get_from_config(tag_c, 'TAG_C')
119
        self.tag_c = cfg if cfg else self.tag_c
120
        self.set_config('TAG_C', self.tag_c)
121
122
        cfg = self.get_from_config(tag_d, 'TAG_D')
123
        self.tag_d = cfg if cfg else self.tag_d
124
        self.set_config('TAG_D', self.tag_d)
125
126
        cfg = self.get_from_config(stat_days, 'STAT_DAYS')
127
        self.stat_days = cfg if cfg else self.stat_days
128
        self.set_config('STAT_DAYS', self.stat_days)
129
130
        cfg = self.get_from_config(database, 'THINGSDB')
131
        self.database = cfg if cfg else self.database
132
        # Automated migration to new database location in Things 3.12.6
133
        # --------------------------------
134
        try:
135
            with open(self.database) as f_d:
136
                if "Your database file has been moved there" in f_d.readline():
137
                    self.database = f"/Users/{self.user}/{self.FILE_DB}"
138
        except UnicodeDecodeError:
139
            pass  # binary file, so it is still the old database
140
        # --------------------------------
141
        self.set_config('THINGSDB', self.database)
142
143
    def set_config(self, key, value, domain='DATABASE'):
144
        """Write variable to config."""
145
        if domain not in self.config:
146
            self.config.add_section(domain)
147
        if value is not None and key is not None:
148
            self.config.set(domain, str(key), str(value))
149
            with open(self.FILE_CONFIG, "w+") as configfile:
150
                self.config.write(configfile)
151
152
    def get_config(self, key, domain='DATABASE'):
153
        """Get variable from config."""
154
        result = None
155
        if domain in self.config and key in self.config[domain]:
156
            result = path.expanduser(self.config[domain][key])
157
        return result
158
159
    def get_from_config(self, variable, key, domain='DATABASE'):
160
        """Set variable. Priority: input, environment, config"""
161
        result = None
162
        if variable is not None:
163
            result = variable
164
        elif environ.get(key):
165
            result = environ.get(key)
166
        elif domain in self.config and key in self.config[domain]:
167
            result = path.expanduser(self.config[domain][key])
168
        return result
169
170
    @staticmethod
171
    def anonymize_string(string):
172
        """Scramble text."""
173
        if string is None:
174
            return None
175
        string = list(string)
176
        shuffle(string)
177
        string = ''.join(string)
178
        return string
179
180
    @staticmethod
181
    def dict_factory(cursor, row):
182
        """Convert SQL result into a dictionary"""
183
        dictionary = {}
184
        for idx, col in enumerate(cursor.description):
185
            dictionary[col[0]] = row[idx]
186
        return dictionary
187
188
    def anonymize_tasks(self, tasks):
189
        """Scramble output for screenshots."""
190
        if self.anonymize:
191
            for task in tasks:
192
                task['title'] = self.anonymize_string(task['title'])
193
                task['context'] = self.anonymize_string(task['context'])
194
        return tasks
195
196
    def get_inbox(self):
197
        """Get all tasks from the inbox."""
198
        query = f"""
199
                TASK.{self.IS_NOT_TRASHED} AND
200
                TASK.{self.IS_TASK} AND
201
                TASK.{self.IS_OPEN} AND
202
                TASK.{self.IS_INBOX}
203
                ORDER BY TASK.duedate DESC , TASK.todayIndex
204
                """
205
        return self.get_rows(query)
206
207
    def get_today(self):
208
        """Get all tasks from the todays list."""
209
        query = f"""
210
                TASK.{self.IS_NOT_TRASHED} AND
211
                TASK.{self.IS_TASK} AND
212
                TASK.{self.IS_OPEN} AND
213
                (TASK.{self.IS_ANYTIME} OR (
214
                     TASK.{self.IS_SOMEDAY} AND
215
                     TASK.{self.DATE_START} <= strftime('%s', 'now')
216
                     )
217
                ) AND
218
                TASK.{self.IS_SCHEDULED} AND (
219
                    (
220
                        PROJECT.title IS NULL OR (
221
                            PROJECT.{self.IS_NOT_TRASHED}
222
                        )
223
                    ) AND (
224
                        HEADPROJ.title IS NULL OR (
225
                            HEADPROJ.{self.IS_NOT_TRASHED}
226
                        )
227
                    )
228
                )
229
                ORDER BY TASK.duedate DESC , TASK.todayIndex
230
                """
231
        return self.get_rows(query)
232
233
    def get_someday(self):
234
        """Get someday tasks."""
235
        query = f"""
236
                TASK.{self.IS_NOT_TRASHED} AND
237
                TASK.{self.IS_TASK} AND
238
                TASK.{self.IS_OPEN} AND
239
                TASK.{self.IS_SOMEDAY} AND
240
                TASK.{self.IS_NOT_SCHEDULED} AND
241
                TASK.{self.IS_NOT_RECURRING} AND (
242
                    (
243
                        PROJECT.title IS NULL OR (
244
                            PROJECT.{self.IS_NOT_TRASHED}
245
                        )
246
                    ) AND (
247
                        HEADPROJ.title IS NULL OR (
248
                            HEADPROJ.{self.IS_NOT_TRASHED}
249
                        )
250
                    )
251
                )
252
                ORDER BY TASK.duedate DESC, TASK.{self.DATE_CREATE} DESC
253
                """
254
        return self.get_rows(query)
255
256
    def get_upcoming(self):
257
        """Get upcoming tasks."""
258
        query = f"""
259
                TASK.{self.IS_NOT_TRASHED} AND
260
                TASK.{self.IS_TASK} AND
261
                TASK.{self.IS_OPEN} AND
262
                TASK.{self.IS_SOMEDAY} AND
263
                TASK.{self.IS_SCHEDULED} AND
264
                TASK.{self.IS_NOT_RECURRING} AND (
265
                    (
266
                        PROJECT.title IS NULL OR (
267
                            PROJECT.{self.IS_NOT_TRASHED}
268
                        )
269
                    ) AND (
270
                        HEADPROJ.title IS NULL OR (
271
                            HEADPROJ.{self.IS_NOT_TRASHED}
272
                        )
273
                    )
274
                )
275
                ORDER BY TASK.startdate, TASK.todayIndex
276
                """
277
        return self.get_rows(query)
278
279
    def get_waiting(self):
280
        """Get waiting tasks."""
281
        return self.get_tag(self.tag_waiting)
282
283
    def get_mit(self):
284
        """Get most important tasks."""
285
        return self.get_tag(self.tag_mit)
286
287
    def get_tag(self, tag):
288
        """Get task with specific tag"""
289
        query = f"""
290
                TASK.{self.IS_NOT_TRASHED} AND
291
                TASK.{self.IS_TASK} AND
292
                TASK.{self.IS_OPEN} AND
293
                TASK.{self.IS_NOT_RECURRING} AND
294
                TAGS.tags=(SELECT uuid FROM {self.TABLE_TAG}
295
                             WHERE title='{tag}'
296
                          )
297
                AND (
298
                    (
299
                        PROJECT.title IS NULL OR (
300
                            PROJECT.{self.IS_NOT_TRASHED}
301
                        )
302
                    ) AND (
303
                        HEADPROJ.title IS NULL OR (
304
                            HEADPROJ.{self.IS_NOT_TRASHED}
305
                        )
306
                    )
307
                )
308
                ORDER BY TASK.duedate DESC , TASK.todayIndex
309
                """
310
        return self.get_rows(query)
311
312
    def get_tag_today(self, tag):
313
        """Get today tasks with specific tag"""
314
        query = f"""
315
                TASK.{self.IS_NOT_TRASHED} AND
316
                TASK.{self.IS_TASK} AND
317
                TASK.{self.IS_OPEN} AND
318
                (TASK.{self.IS_ANYTIME} OR (
319
                     TASK.{self.IS_SOMEDAY} AND
320
                     TASK.{self.DATE_START} <= strftime('%s', 'now')
321
                     )
322
                ) AND
323
                TAGS.tags=(SELECT uuid FROM {self.TABLE_TAG}
324
                             WHERE title='{tag}') AND
325
                TASK.{self.IS_SCHEDULED} AND (
326
                    (
327
                        PROJECT.title IS NULL OR (
328
                            PROJECT.{self.IS_NOT_TRASHED}
329
                        )
330
                    ) AND (
331
                        HEADPROJ.title IS NULL OR (
332
                            HEADPROJ.{self.IS_NOT_TRASHED}
333
                        )
334
                    )
335
                )
336
                ORDER BY TASK.duedate DESC , TASK.todayIndex
337
            """
338
        return self.get_rows(query)
339
340
    def get_anytime(self):
341
        """Get anytime tasks."""
342
        query = f"""
343
                TASK.{self.IS_NOT_TRASHED} AND
344
                TASK.{self.IS_TASK} AND
345
                TASK.{self.IS_OPEN} AND
346
                TASK.{self.IS_ANYTIME} AND
347
                TASK.{self.IS_NOT_SCHEDULED} AND (
348
                    (
349
                        PROJECT.title IS NULL OR (
350
                            PROJECT.{self.IS_ANYTIME} AND
351
                            PROJECT.{self.IS_NOT_SCHEDULED} AND
352
                            PROJECT.{self.IS_NOT_TRASHED}
353
                        )
354
                    ) AND (
355
                        HEADPROJ.title IS NULL OR (
356
                            HEADPROJ.{self.IS_ANYTIME} AND
357
                            HEADPROJ.{self.IS_NOT_SCHEDULED} AND
358
                            HEADPROJ.{self.IS_NOT_TRASHED}
359
                        )
360
                    )
361
                )
362
                ORDER BY TASK.duedate DESC , TASK.todayIndex
363
                """
364
        if self.filter:
365
            # ugly hack for Kanban task view on project
366
            query = f"""
367
                TASK.{self.IS_NOT_TRASHED} AND
368
                TASK.{self.IS_TASK} AND
369
                TASK.{self.IS_OPEN} AND
370
                TASK.{self.IS_ANYTIME} AND
371
                TASK.{self.IS_NOT_SCHEDULED} AND (
372
                    (
373
                        PROJECT.title IS NULL OR (
374
                            PROJECT.{self.IS_NOT_TRASHED}
375
                        )
376
                    ) AND (
377
                        HEADPROJ.title IS NULL OR (
378
                            HEADPROJ.{self.IS_NOT_TRASHED}
379
                        )
380
                    )
381
                )
382
                ORDER BY TASK.duedate DESC , TASK.todayIndex
383
                """
384
        return self.get_rows(query)
385
386
    def get_completed(self):
387
        """Get completed tasks."""
388
        query = f"""
389
                TASK.{self.IS_NOT_TRASHED} AND
390
                TASK.{self.IS_TASK} AND
391
                TASK.{self.IS_DONE}
392
                ORDER BY TASK.{self.DATE_STOP}
393
                """
394
        return self.get_rows(query)
395
396
    def get_cancelled(self):
397
        """Get cancelled tasks."""
398
        query = f"""
399
                TASK.{self.IS_NOT_TRASHED} AND
400
                TASK.{self.IS_TASK} AND
401
                TASK.{self.IS_CANCELLED}
402
                ORDER BY TASK.{self.DATE_STOP}
403
                """
404
        return self.get_rows(query)
405
406
    def get_trashed(self):
407
        """Get trashed tasks."""
408
        query = f"""
409
                TASK.{self.IS_TRASHED} AND
410
                TASK.{self.IS_TASK}
411
                ORDER BY TASK.{self.DATE_STOP}
412
                """
413
        return self.get_rows(query)
414
415
    def get_projects(self):
416
        """Get projects."""
417
        query = f"""
418
                SELECT
419
                    TASK.uuid,
420
                    TASK.title,
421
                    NULL as context,
422
                    (SELECT COUNT(uuid)
423
                     FROM TMTask AS PROJECT_TASK
424
                     WHERE
425
                       PROJECT_TASK.project = TASK.uuid AND
426
                       PROJECT_TASK.{self.IS_NOT_TRASHED} AND
427
                       PROJECT_TASK.{self.IS_OPEN}
428
                    ) AS size
429
                FROM
430
                    {self.TABLE_TASK} AS TASK
431
                WHERE
432
                    TASK.{self.IS_NOT_TRASHED} AND
433
                    TASK.{self.IS_PROJECT} AND
434
                    TASK.{self.IS_OPEN}
435
                ORDER BY TASK.title COLLATE NOCASE
436
                """
437
        return self.execute_query(query)
438
439
    def get_areas(self):
440
        """Get areas."""
441
        query = f"""
442
                SELECT
443
                    AREA.uuid AS uuid,
444
                    AREA.title AS title,
445
                    (SELECT COUNT(uuid)
446
                        FROM TMTask AS PROJECT
447
                        WHERE
448
                        PROJECT.area = AREA.uuid AND
449
                        PROJECT.{self.IS_NOT_TRASHED} AND
450
                        PROJECT.{self.IS_OPEN}
451
                    ) AS size
452
                FROM
453
                    {self.TABLE_AREA} AS AREA
454
                ORDER BY AREA.title COLLATE NOCASE
455
                """
456
        return self.execute_query(query)
457
458
    def get_all(self):
459
        """Get all tasks."""
460
        query = f"""
461
                TASK.{self.IS_NOT_TRASHED} AND
462
                TASK.{self.IS_TASK} AND (
463
                    (
464
                        PROJECT.title IS NULL OR (
465
                            PROJECT.{self.IS_NOT_TRASHED}
466
                        )
467
                    ) AND (
468
                        HEADPROJ.title IS NULL OR (
469
                            HEADPROJ.{self.IS_NOT_TRASHED}
470
                        )
471
                    )
472
                )
473
                """
474
        return self.get_rows(query)
475
476
    def get_due(self):
477
        """Get due tasks."""
478
        query = f"""
479
                TASK.{self.IS_NOT_TRASHED} AND
480
                TASK.{self.IS_TASK} AND
481
                TASK.{self.IS_OPEN} AND
482
                TASK.{self.IS_DUE} AND (
483
                    (
484
                        PROJECT.title IS NULL OR (
485
                            PROJECT.{self.IS_NOT_TRASHED}
486
                        )
487
                    ) AND (
488
                        HEADPROJ.title IS NULL OR (
489
                            HEADPROJ.{self.IS_NOT_TRASHED}
490
                        )
491
                    )
492
                )
493
                ORDER BY TASK.{self.DATE_DUE}
494
                """
495
        return self.get_rows(query)
496
497
    def get_lint(self):
498
        """Get tasks that float around"""
499
        query = f"""
500
            TASK.{self.IS_NOT_TRASHED} AND
501
            TASK.{self.IS_OPEN} AND
502
            TASK.{self.IS_TASK} AND
503
            (TASK.{self.IS_SOMEDAY} OR TASK.{self.IS_ANYTIME}) AND
504
            TASK.project IS NULL AND
505
            TASK.area IS NULL AND
506
            TASK.actionGroup IS NULL
507
            """
508
        return self.get_rows(query)
509
510
    def get_empty_projects(self):
511
        """Get projects that are empty"""
512
        query = f"""
513
            TASK.{self.IS_NOT_TRASHED} AND
514
            TASK.{self.IS_OPEN} AND
515
            TASK.{self.IS_PROJECT} AND
516
            TASK.{self.IS_ANYTIME}
517
            GROUP BY TASK.uuid
518
            HAVING
519
                (SELECT COUNT(uuid)
520
                 FROM TMTask AS PROJECT_TASK
521
                 WHERE
522
                   PROJECT_TASK.project = TASK.uuid AND
523
                   PROJECT_TASK.{self.IS_NOT_TRASHED} AND
524
                   PROJECT_TASK.{self.IS_OPEN} AND
525
                   (PROJECT_TASK.{self.IS_ANYTIME} OR
526
                    PROJECT_TASK.{self.IS_SCHEDULED} OR
527
                      (PROJECT_TASK.{self.IS_RECURRING} AND
528
                       PROJECT_TASK.{self.RECURRING_IS_NOT_PAUSED} AND
529
                       PROJECT_TASK.{self.RECURRING_HAS_NEXT_STARTDATE}
530
                      )
531
                   )
532
                ) = 0
533
            """
534
        return self.get_rows(query)
535
536
    def get_largest_projects(self):
537
        """Get projects that are empty"""
538
        query = f"""
539
            SELECT
540
                TASK.uuid,
541
                TASK.title AS title,
542
                {self.DATE_CREATE} AS created,
543
                {self.DATE_MOD} AS modified,
544
                (SELECT COUNT(uuid)
545
                 FROM TMTask AS PROJECT_TASK
546
                 WHERE
547
                   PROJECT_TASK.project = TASK.uuid AND
548
                   PROJECT_TASK.{self.IS_NOT_TRASHED} AND
549
                   PROJECT_TASK.{self.IS_OPEN}
550
                ) AS tasks
551
            FROM
552
                {self.TABLE_TASK} AS TASK
553
            WHERE
554
               TASK.{self.IS_NOT_TRASHED} AND
555
               TASK.{self.IS_OPEN} AND
556
               TASK.{self.IS_PROJECT}
557
            GROUP BY TASK.uuid
558
            ORDER BY tasks COLLATE NOCASE DESC
559
            """
560
        return self.execute_query(query)
561
562
    def get_daystats(self):
563
        """Get a history of task activities"""
564
        query = f"""
565
                WITH RECURSIVE timeseries(x) AS (
566
                    SELECT 0
567
                    UNION ALL
568
                    SELECT x+1 FROM timeseries
569
                    LIMIT {self.stat_days}
570
                )
571
                SELECT
572
                    date(julianday("now", "-{self.stat_days} days"),
573
                         "+" || x || " days") as date,
574
                    CREATED.TasksCreated as created,
575
                    CLOSED.TasksClosed as completed,
576
                    CANCELLED.TasksCancelled as cancelled,
577
                    TRASHED.TasksTrashed as trashed
578
                FROM timeseries
579
                LEFT JOIN
580
                    (SELECT COUNT(uuid) AS TasksCreated,
581
                        date({self.DATE_CREATE},"unixepoch") AS DAY
582
                        FROM {self.TABLE_TASK} AS TASK
583
                        WHERE DAY NOT NULL
584
                          AND TASK.{self.IS_TASK}
585
                        GROUP BY DAY)
586
                    AS CREATED ON CREATED.DAY = date
587
                LEFT JOIN
588
                    (SELECT COUNT(uuid) AS TasksCancelled,
589
                        date(stopDate,"unixepoch") AS DAY
590
                        FROM {self.TABLE_TASK} AS TASK
591
                        WHERE DAY NOT NULL
592
                          AND TASK.{self.IS_CANCELLED} AND TASK.{self.IS_TASK}
593
                        GROUP BY DAY)
594
                        AS CANCELLED ON CANCELLED.DAY = date
595
                LEFT JOIN
596
                    (SELECT COUNT(uuid) AS TasksTrashed,
597
                        date({self.DATE_MOD},"unixepoch") AS DAY
598
                        FROM {self.TABLE_TASK} AS TASK
599
                        WHERE DAY NOT NULL
600
                          AND TASK.{self.IS_TRASHED} AND TASK.{self.IS_TASK}
601
                        GROUP BY DAY)
602
                        AS TRASHED ON TRASHED.DAY = date
603
                LEFT JOIN
604
                    (SELECT COUNT(uuid) AS TasksClosed,
605
                        date(stopDate,"unixepoch") AS DAY
606
                        FROM {self.TABLE_TASK} AS TASK
607
                        WHERE DAY NOT NULL
608
                          AND TASK.{self.IS_DONE} AND TASK.{self.IS_TASK}
609
                        GROUP BY DAY)
610
                        AS CLOSED ON CLOSED.DAY = date
611
                """
612
        return self.execute_query(query)
613
614
    def get_minutes_today(self):
615
        """Count the planned minutes for today."""
616
        query = f"""
617
                SELECT
618
                    SUM(TAG.title) AS minutes
619
                FROM
620
                    {self.TABLE_TASK} AS TASK
621
                LEFT OUTER JOIN
622
                TMTask PROJECT ON TASK.project = PROJECT.uuid
623
                LEFT OUTER JOIN
624
                    TMArea AREA ON TASK.area = AREA.uuid
625
                LEFT OUTER JOIN
626
                    TMTask HEADING ON TASK.actionGroup = HEADING.uuid
627
                LEFT OUTER JOIN
628
                    TMTask HEADPROJ ON HEADING.project = HEADPROJ.uuid
629
                LEFT OUTER JOIN
630
                    TMTaskTag TAGS ON TASK.uuid = TAGS.tasks
631
                LEFT OUTER JOIN
632
                    TMTag TAG ON TAGS.tags = TAG.uuid
633
                WHERE
634
                    printf("%d", TAG.title) = TAG.title AND
635
                    TASK.{self.IS_NOT_TRASHED} AND
636
                    TASK.{self.IS_TASK} AND
637
                    TASK.{self.IS_OPEN} AND
638
                    TASK.{self.IS_ANYTIME} AND
639
                    TASK.{self.IS_SCHEDULED} AND (
640
                        (
641
                            PROJECT.title IS NULL OR (
642
                                PROJECT.{self.IS_NOT_TRASHED}
643
                            )
644
                        ) AND (
645
                            HEADPROJ.title IS NULL OR (
646
                                HEADPROJ.{self.IS_NOT_TRASHED}
647
                            )
648
                        )
649
                    )
650
                """
651
        return self.execute_query(query)
652
653
    def get_cleanup(self):
654
        """Tasks and projects that need work."""
655
        result = []
656
        result.extend(self.get_lint())
657
        result.extend(self.get_empty_projects())
658
        result.extend(self.get_tag(self.tag_cleanup))
659
        result = [i for n, i in enumerate(result) if i not in result[n + 1:]]
660
        return result
661
662
    @staticmethod
663
    def get_not_implemented():
664
        """Not implemented warning."""
665
        return [{"title": "not implemented"}]
666
667
    def get_rows(self, sql):
668
        """Query Things database."""
669
670
        sql = f"""
671
            SELECT DISTINCT
672
                TASK.uuid,
673
                TASK.title,
674
                CASE
675
                    WHEN AREA.title IS NOT NULL THEN AREA.title
676
                    WHEN PROJECT.title IS NOT NULL THEN PROJECT.title
677
                    WHEN HEADING.title IS NOT NULL THEN HEADING.title
678
                END AS context,
679
                CASE
680
                    WHEN AREA.uuid IS NOT NULL THEN AREA.uuid
681
                    WHEN PROJECT.uuid IS NOT NULL THEN PROJECT.uuid
682
                END AS context_uuid,
683
                CASE
684
                    WHEN TASK.recurrenceRule IS NULL
685
                    THEN strftime('%d.%m.', TASK.dueDate,"unixepoch") ||
686
                         substr(strftime('%Y', TASK.dueDate,"unixepoch"),3, 2)
687
                ELSE NULL
688
                END AS due,
689
                date(TASK.{self.DATE_CREATE},"unixepoch") as created,
690
                date(TASK.{self.DATE_MOD},"unixepoch") as modified,
691
                strftime('%d.%m.', TASK.startDate,"unixepoch") ||
692
                  substr(strftime('%Y', TASK.startDate,"unixepoch"),3, 2)
693
                  as started,
694
                date(TASK.stopDate,"unixepoch") as stopped,
695
                (SELECT COUNT(uuid)
696
                 FROM TMTask AS PROJECT_TASK
697
                 WHERE
698
                   PROJECT_TASK.project = TASK.uuid AND
699
                   PROJECT_TASK.{self.IS_NOT_TRASHED} AND
700
                   PROJECT_TASK.{self.IS_OPEN}
701
                ) AS size,
702
                CASE
703
                    WHEN TASK.{self.IS_TASK} THEN 'task'
704
                    WHEN TASK.{self.IS_PROJECT} THEN 'project'
705
                    WHEN TASK.{self.IS_HEADING} THEN 'heading'
706
                END AS type,
707
                TASK.notes
708
            FROM
709
                {self.TABLE_TASK} AS TASK
710
            LEFT OUTER JOIN
711
                {self.TABLE_TASK} PROJECT ON TASK.project = PROJECT.uuid
712
            LEFT OUTER JOIN
713
                {self.TABLE_AREA} AREA ON TASK.area = AREA.uuid
714
            LEFT OUTER JOIN
715
                {self.TABLE_TASK} HEADING ON TASK.actionGroup = HEADING.uuid
716
            LEFT OUTER JOIN
717
                {self.TABLE_TASK} HEADPROJ ON HEADING.project = HEADPROJ.uuid
718
            LEFT OUTER JOIN
719
                {self.TABLE_TASKTAG} TAGS ON TASK.uuid = TAGS.tasks
720
            LEFT OUTER JOIN
721
                {self.TABLE_TAG} TAG ON TAGS.tags = TAG.uuid
722
            WHERE
723
                {self.filter}
724
                {sql}
725
                """
726
727
        return self.execute_query(sql)
728
729
    def execute_query(self, sql):
730
        """Run the actual query"""
731
        if self.debug is True:
732
            print(self.database)
733
            print(sql)
734
        try:
735
            connection = sqlite3.connect(
736
                'file:' + self.database + '?mode=ro', uri=True)
737
            connection.row_factory = Things3.dict_factory
738
            cursor = connection.cursor()
739
            cursor.execute(sql)
740
            tasks = cursor.fetchall()
741
            tasks = self.anonymize_tasks(tasks)
742
            if self.debug:
743
                for task in tasks:
744
                    print(task)
745
            return tasks
746
        except sqlite3.OperationalError as error:
747
            print(f"Could not query the database at: {self.database}.")
748
            print(f"Details: {error}.")
749
            sys.exit(2)
750
751
    # pylint: disable=C0103
752
    def mode_project(self):
753
        """Hack to switch to project view"""
754
        self.IS_TASK = self.MODE_PROJECT
755
756
    # pylint: disable=C0103
757
    def mode_task(self):
758
        """Hack to switch to project view"""
759
        self.IS_TASK = self.MODE_TASK
760
761
    functions = {
762
        "inbox": get_inbox,
763
        "today": get_today,
764
        "next": get_anytime,
765
        "backlog": get_someday,
766
        "upcoming": get_upcoming,
767
        "waiting": get_waiting,
768
        "mit": get_mit,
769
        "completed": get_completed,
770
        "cancelled": get_cancelled,
771
        "trashed": get_trashed,
772
        "projects": get_projects,
773
        "areas": get_areas,
774
        "all": get_all,
775
        "due": get_due,
776
        "lint": get_lint,
777
        "empty": get_empty_projects,
778
        "cleanup": get_cleanup,
779
        "top-proj": get_largest_projects,
780
        "stats-day": get_daystats,
781
        "stats-min-today": get_minutes_today
782
    }
783