Passed
Push — master ( a777de...a4e14d )
by Alexander
01:39
created

things3.things3.Things3.feedback()   A

Complexity

Conditions 1

Size

Total Lines 35
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
nop 1
dl 0
loc 35
rs 10
c 0
b 0
f 0
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
from things3 import __version__
8
9
import sqlite3
0 ignored issues
show
introduced by
standard import "import sqlite3" should be placed before "from things3 import __version__"
Loading history...
10
import sys
0 ignored issues
show
introduced by
standard import "import sys" should be placed before "from things3 import __version__"
Loading history...
11
from random import shuffle
0 ignored issues
show
introduced by
standard import "from random import shuffle" should be placed before "from things3 import __version__"
Loading history...
12
import os
0 ignored issues
show
introduced by
standard import "import os" should be placed before "from things3 import __version__"
Loading history...
13
from os import environ, path
0 ignored issues
show
introduced by
standard import "from os import environ, path" should be placed before "from things3 import __version__"
Loading history...
14
import getpass
0 ignored issues
show
introduced by
standard import "import getpass" should be placed before "from things3 import __version__"
Loading history...
15
import configparser
0 ignored issues
show
introduced by
standard import "import configparser" should be placed before "from things3 import __version__"
Loading history...
16
from pathlib import Path
0 ignored issues
show
introduced by
standard import "from pathlib import Path" should be placed before "from things3 import __version__"
Loading history...
17
18
# the new core library, migration ongoing
19
import things
0 ignored issues
show
introduced by
third party import "import things" should be placed before "from things3 import __version__"
Loading history...
20
21
# pylint: disable=R0904,R0902
22
23
24
class Things3:
25
    """Simple read-only API for Things 3."""
26
27
    # Database info
28
    FILE_CONFIG = str(Path.home()) + "/.kanbanviewrc"
29
    FILE_DB = (
30
        "/Library/Group Containers/"
31
        "JLMPQHK86H.com.culturedcode.ThingsMac/"
32
        "Things Database.thingsdatabase/main.sqlite"
33
    )
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"  # noqa
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"  # noqa
49
    IS_RECURRING = "recurrenceRule IS NOT NULL"
50
    IS_NOT_RECURRING = "recurrenceRule IS NULL"  # noqa
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, encoding="utf-8")
80
    mode = "to-do"
81
    filter_project = None
82
    filter_area = None
83
    debug_text = ""
84
85
    # pylint: disable=R0913
86
    def __init__(
87
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
88
        database=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
89
        tag_waiting=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
90
        tag_mit=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
91
        tag_cleanup=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
92
        tag_a=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
93
        tag_b=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
94
        tag_c=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
95
        tag_d=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
96
        stat_days=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
97
        anonymize=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
98
        debug_text="",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
99
    ):
100
        self.debug_text = debug_text
101
102
        cfg = self.get_from_config(tag_waiting, "TAG_WAITING")
103
        self.tag_waiting = cfg if cfg else self.tag_waiting
104
        self.set_config("TAG_WAITING", self.tag_waiting)
105
106
        cfg = self.get_from_config(anonymize, "ANONYMIZE")
107
        self.anonymize = (cfg == "True") if (cfg == "True") else self.anonymize
108
        self.set_config("ANONYMIZE", self.anonymize)
109
110
        cfg = self.get_from_config(tag_mit, "TAG_MIT")
111
        self.tag_mit = cfg if cfg else self.tag_mit
112
        self.set_config("TAG_MIT", self.tag_mit)
113
114
        cfg = self.get_from_config(tag_cleanup, "TAG_CLEANUP")
115
        self.tag_cleanup = cfg if cfg else self.tag_cleanup
116
        self.set_config("TAG_CLEANUP", self.tag_cleanup)
117
118
        cfg = self.get_from_config(tag_a, "TAG_A")
119
        self.tag_a = cfg if cfg else self.tag_a
120
        self.set_config("TAG_A", self.tag_a)
121
122
        cfg = self.get_from_config(tag_b, "TAG_B")
123
        self.tag_b = cfg if cfg else self.tag_b
124
        self.set_config("TAG_B", self.tag_b)
125
126
        cfg = self.get_from_config(tag_c, "TAG_C")
127
        self.tag_c = cfg if cfg else self.tag_c
128
        self.set_config("TAG_C", self.tag_c)
129
130
        cfg = self.get_from_config(tag_d, "TAG_D")
131
        self.tag_d = cfg if cfg else self.tag_d
132
        self.set_config("TAG_D", self.tag_d)
133
134
        cfg = self.get_from_config(stat_days, "STAT_DAYS")
135
        self.stat_days = cfg if cfg else self.stat_days
136
        self.set_config("STAT_DAYS", self.stat_days)
137
138
        cfg = self.get_from_config(database, "THINGSDB")
139
        self.database = cfg if cfg else self.database
140
        # Automated migration to new database location in Things 3.12.6/3.13.1
141
        # --------------------------------
142
        try:
143
            with open(self.database, encoding="utf-8") as f_d:
144
                if "Your database file has been moved there" in f_d.readline():
145
                    self.database = f"/Users/{self.user}/{self.FILE_DB}"
146
        except (UnicodeDecodeError, FileNotFoundError, PermissionError):
147
            pass  # binary file (old database) or doesn't exist
148
        # --------------------------------
149
        self.set_config("THINGSDB", self.database)
150
151
    def set_config(self, key, value, domain="DATABASE"):
152
        """Write variable to config."""
153
        if domain not in self.config:
154
            self.config.add_section(domain)
155
        if value is not None and key is not None:
156
            self.config.set(domain, str(key), str(value))
157
            with open(self.FILE_CONFIG, "w+", encoding="utf-8") as configfile:
158
                self.config.write(configfile)
159
160
    def get_config(self, key, domain="DATABASE"):
161
        """Get variable from config."""
162
        result = None
163
        if domain in self.config and key in self.config[domain]:
164
            result = path.expanduser(self.config[domain][key])
165
        return result
166
167
    def get_from_config(self, variable, key, domain="DATABASE"):
168
        """Set variable. Priority: input, environment, config"""
169
        result = None
170
        if variable is not None:
171
            result = variable
172
        elif environ.get(key):
173
            result = environ.get(key)
174
        elif domain in self.config and key in self.config[domain]:
175
            result = path.expanduser(self.config[domain][key])
176
        return result
177
178
    @staticmethod
179
    def anonymize_string(string):
180
        """Scramble text."""
181
        if string is None:
182
            return None
183
        string = list(string)
184
        shuffle(string)
185
        string = "".join(string)
186
        return string
187
188
    @staticmethod
189
    def dict_factory(cursor, row):
190
        """Convert SQL result into a dictionary"""
191
        dictionary = {}
192
        for idx, col in enumerate(cursor.description):
193
            dictionary[col[0]] = row[idx]
194
        return dictionary
195
196
    def anonymize_tasks(self, tasks):
197
        """Scramble output for screenshots."""
198
        if self.anonymize:
199
            for task in tasks:
200
                task["title"] = self.anonymize_string(task["title"])
201
                task["context"] = (
202
                    self.anonymize_string(task["context"]) if "context" in task else ""
203
                )
204
        return tasks
205
206
    def defaults(self):
207
        """Some default options for the new API."""
208
        return dict(
209
            type=self.mode,
210
            project=self.filter_project,
211
            area=self.filter_area,
212
            filepath=self.database,
213
        )
214
215
    def convert_new_things_lib(self, tasks):
216
        """Convert tasks from new library to old expectations."""
217
        for task in tasks:
218
            task["context"] = (
219
                task.get("project_title")
220
                or task.get("area_title")
221
                or task.get("heading_title")
222
            )
223
            task["context_uuid"] = (
224
                task.get("project") or task.get("area") or task.get("heading")
225
            )
226
            task["due"] = task.get("deadline")
227
            task["started"] = task.get("start_date")
228
            task["size"] = things.projects(
229
                task["uuid"], count_only=True, filepath=self.database
230
            )
231
        tasks.sort(key=lambda task: task["title"] or "", reverse=False)
232
        tasks = self.anonymize_tasks(tasks)
233
        return tasks
234
235
    def get_inbox(self):
236
        """Get tasks from inbox."""
237
        tasks = things.inbox(**self.defaults())
238
        tasks = self.convert_new_things_lib(tasks)
239
        return tasks
240
241
    def get_today(self):
242
        """Get tasks from today."""
243
        tasks = things.today(**self.defaults())
244
        tasks = self.convert_new_things_lib(tasks)
245
        tasks.sort(key=lambda task: task.get("started", ""), reverse=True)
246
        tasks.sort(key=lambda task: task.get("todayIndex", ""), reverse=False)
247
        return tasks
248
249
    def get_task(self, area=None, project=None):
250
        """Get tasks."""
251
        tasks = things.tasks(area=area, project=project, filepath=self.database)
252
        tasks = self.convert_new_things_lib(tasks)
253
        return tasks
254
255
    def get_someday(self):
256
        """Get someday tasks."""
257
        tasks = things.someday(**self.defaults())
258
        tasks = self.convert_new_things_lib(tasks)
259
        tasks.sort(key=lambda task: task["deadline"] or "", reverse=True)
260
        return tasks
261
262
    def get_upcoming(self):
263
        """Get upcoming tasks."""
264
        tasks = things.upcoming(**self.defaults())
265
        tasks = self.convert_new_things_lib(tasks)
266
        tasks.sort(key=lambda task: task["started"] or "", reverse=False)
267
        return tasks
268
269
    def get_waiting(self):
270
        """Get waiting tasks."""
271
        tasks = self.get_tag(self.tag_waiting)
272
        tasks.sort(key=lambda task: task["started"] or "", reverse=False)
273
        return tasks
274
275
    def get_mit(self):
276
        """Get most important tasks."""
277
        return self.get_tag(self.tag_mit)
278
279
    def get_tag(self, tag):
280
        """Get task with specific tag."""
281
        try:
282
            tasks = things.tasks(tag=tag, **self.defaults())
283
            tasks = self.convert_new_things_lib(tasks)
284
        except ValueError:
285
            tasks = []
286
        if tag in [self.tag_waiting]:
287
            tasks.sort(key=lambda task: task["started"] or "", reverse=False)
288
        return tasks
289
290
    def get_tag_today(self, tag):
291
        """Get today tasks with specific tag."""
292
        tasks = things.today(tag=tag, **self.defaults())
293
        tasks = self.convert_new_things_lib(tasks)
294
        return tasks
295
296
    def get_anytime(self):
297
        """Get anytime tasks."""
298
        query = f"""
299
                TASK.{self.IS_NOT_TRASHED} AND
300
                TASK.{self.IS_TASK} AND
301
                TASK.{self.IS_OPEN} AND
302
                TASK.{self.IS_ANYTIME} AND
303
                TASK.{self.IS_NOT_SCHEDULED} AND (
304
                    (
305
                        PROJECT.title IS NULL OR (
306
                            PROJECT.{self.IS_ANYTIME} AND
307
                            PROJECT.{self.IS_NOT_SCHEDULED} AND
308
                            PROJECT.{self.IS_NOT_TRASHED}
309
                        )
310
                    ) AND (
311
                        HEADPROJ.title IS NULL OR (
312
                            HEADPROJ.{self.IS_ANYTIME} AND
313
                            HEADPROJ.{self.IS_NOT_SCHEDULED} AND
314
                            HEADPROJ.{self.IS_NOT_TRASHED}
315
                        )
316
                    )
317
                )
318
                ORDER BY TASK.duedate DESC , TASK.todayIndex
319
                """
320
        if self.filter:
321
            # ugly hack for Kanban task view on project
322
            query = f"""
323
                TASK.{self.IS_NOT_TRASHED} AND
324
                TASK.{self.IS_TASK} AND
325
                TASK.{self.IS_OPEN} AND
326
                TASK.{self.IS_ANYTIME} AND
327
                TASK.{self.IS_NOT_SCHEDULED} AND (
328
                    (
329
                        PROJECT.title IS NULL OR (
330
                            PROJECT.{self.IS_NOT_TRASHED}
331
                        )
332
                    ) AND (
333
                        HEADPROJ.title IS NULL OR (
334
                            HEADPROJ.{self.IS_NOT_TRASHED}
335
                        )
336
                    )
337
                )
338
                ORDER BY TASK.duedate DESC , TASK.todayIndex
339
                """
340
        return self.get_rows(query)
341
342
    def get_completed(self):
343
        """Get completed tasks."""
344
        tasks = things.completed(**self.defaults())
345
        tasks = self.convert_new_things_lib(tasks)
346
        return tasks
347
348
    def get_cancelled(self):
349
        """Get cancelled tasks."""
350
        tasks = things.canceled(**self.defaults())
351
        tasks = self.convert_new_things_lib(tasks)
352
        return tasks
353
354
    def get_trashed(self):
355
        """Get trashed tasks."""
356
        query = f"""
357
                TASK.{self.IS_TRASHED} AND
358
                TASK.{self.IS_TASK}
359
                ORDER BY TASK.{self.DATE_STOP}
360
                """
361
        return self.get_rows(query)
362
363
    def get_projects(self, area=None):
364
        """Get projects."""
365
        tasks = things.projects(area=area, filepath=self.database)
366
        tasks = self.convert_new_things_lib(tasks)
367
        return tasks
368
369
    def get_areas(self):
370
        """Get areas."""
371
        tasks = things.areas(filepath=self.database)
372
        tasks = self.convert_new_things_lib(tasks)
373
        for task in tasks:
374
            task["size"] = things.areas(
375
                task["uuid"], count_only=True, filepath=self.database
376
            )
377
        return tasks
378
379
    def get_all(self):
380
        """Get all tasks."""
381
        tasks = things.tasks(**self.defaults())
382
        tasks = self.convert_new_things_lib(tasks)
383
        return tasks
384
385
    def get_due(self):
386
        """Get due tasks."""
387
        tasks = things.deadlines(**self.defaults())
388
        tasks = self.convert_new_things_lib(tasks)
389
        tasks.sort(key=lambda task: task["deadline"] or "", reverse=False)
390
        return tasks
391
392
    def get_lint(self):
393
        """Get tasks that float around"""
394
        query = f"""
395
            TASK.{self.IS_NOT_TRASHED} AND
396
            TASK.{self.IS_OPEN} AND
397
            TASK.{self.IS_TASK} AND
398
            (TASK.{self.IS_SOMEDAY} OR TASK.{self.IS_ANYTIME}) AND
399
            TASK.project IS NULL AND
400
            TASK.area IS NULL AND
401
            TASK.actionGroup IS NULL
402
            """
403
        return self.get_rows(query)
404
405
    def get_empty_projects(self):
406
        """Get projects that are empty"""
407
        query = f"""
408
            TASK.{self.IS_NOT_TRASHED} AND
409
            TASK.{self.IS_OPEN} AND
410
            TASK.{self.IS_PROJECT} AND
411
            TASK.{self.IS_ANYTIME}
412
            GROUP BY TASK.uuid
413
            HAVING
414
                (SELECT COUNT(uuid)
415
                 FROM TMTask AS PROJECT_TASK
416
                 WHERE
417
                   PROJECT_TASK.project = TASK.uuid AND
418
                   PROJECT_TASK.{self.IS_NOT_TRASHED} AND
419
                   PROJECT_TASK.{self.IS_OPEN} AND
420
                   (PROJECT_TASK.{self.IS_ANYTIME} OR
421
                    PROJECT_TASK.{self.IS_SCHEDULED} OR
422
                      (PROJECT_TASK.{self.IS_RECURRING} AND
423
                       PROJECT_TASK.{self.RECURRING_IS_NOT_PAUSED} AND
424
                       PROJECT_TASK.{self.RECURRING_HAS_NEXT_STARTDATE}
425
                      )
426
                   )
427
                ) = 0
428
            """
429
        return self.get_rows(query)
430
431
    def get_largest_projects(self):
432
        """Get projects that are empty"""
433
        query = f"""
434
            SELECT
435
                TASK.uuid,
436
                TASK.title AS title,
437
                {self.DATE_CREATE} AS created,
438
                {self.DATE_MOD} AS modified,
439
                (SELECT COUNT(uuid)
440
                 FROM TMTask AS PROJECT_TASK
441
                 WHERE
442
                   PROJECT_TASK.project = TASK.uuid AND
443
                   PROJECT_TASK.{self.IS_NOT_TRASHED} AND
444
                   PROJECT_TASK.{self.IS_OPEN}
445
                ) AS tasks
446
            FROM
447
                {self.TABLE_TASK} AS TASK
448
            WHERE
449
               TASK.{self.IS_NOT_TRASHED} AND
450
               TASK.{self.IS_OPEN} AND
451
               TASK.{self.IS_PROJECT}
452
            GROUP BY TASK.uuid
453
            ORDER BY tasks COLLATE NOCASE DESC
454
            """
455
        return self.execute_query(query)
456
457
    def get_daystats(self):
458
        """Get a history of task activities"""
459
        query = f"""
460
                WITH RECURSIVE timeseries(x) AS (
461
                    SELECT 0
462
                    UNION ALL
463
                    SELECT x+1 FROM timeseries
464
                    LIMIT {self.stat_days}
465
                )
466
                SELECT
467
                    date(julianday("now", "-{self.stat_days} days"),
468
                         "+" || x || " days") as date,
469
                    CREATED.TasksCreated as created,
470
                    CLOSED.TasksClosed as completed,
471
                    CANCELLED.TasksCancelled as cancelled,
472
                    TRASHED.TasksTrashed as trashed
473
                FROM timeseries
474
                LEFT JOIN
475
                    (SELECT COUNT(uuid) AS TasksCreated,
476
                        date({self.DATE_CREATE},"unixepoch") AS DAY
477
                        FROM {self.TABLE_TASK} AS TASK
478
                        WHERE DAY NOT NULL
479
                          AND TASK.{self.IS_TASK}
480
                        GROUP BY DAY)
481
                    AS CREATED ON CREATED.DAY = date
482
                LEFT JOIN
483
                    (SELECT COUNT(uuid) AS TasksCancelled,
484
                        date(stopDate,"unixepoch") AS DAY
485
                        FROM {self.TABLE_TASK} AS TASK
486
                        WHERE DAY NOT NULL
487
                          AND TASK.{self.IS_CANCELLED} AND TASK.{self.IS_TASK}
488
                        GROUP BY DAY)
489
                        AS CANCELLED ON CANCELLED.DAY = date
490
                LEFT JOIN
491
                    (SELECT COUNT(uuid) AS TasksTrashed,
492
                        date({self.DATE_MOD},"unixepoch") AS DAY
493
                        FROM {self.TABLE_TASK} AS TASK
494
                        WHERE DAY NOT NULL
495
                          AND TASK.{self.IS_TRASHED} AND TASK.{self.IS_TASK}
496
                        GROUP BY DAY)
497
                        AS TRASHED ON TRASHED.DAY = date
498
                LEFT JOIN
499
                    (SELECT COUNT(uuid) AS TasksClosed,
500
                        date(stopDate,"unixepoch") AS DAY
501
                        FROM {self.TABLE_TASK} AS TASK
502
                        WHERE DAY NOT NULL
503
                          AND TASK.{self.IS_DONE} AND TASK.{self.IS_TASK}
504
                        GROUP BY DAY)
505
                        AS CLOSED ON CLOSED.DAY = date
506
                """
507
        return self.execute_query(query)
508
509
    def get_minutes_today(self):
510
        """Count the planned minutes for today."""
511
512
        tasks = things.today(**self.defaults())
513
        tasks = self.convert_new_things_lib(tasks)
514
        minutes = 0
515
        for task in tasks:
516
            for tag in task.get("tags", []):
517
                try:
518
                    minutes += int(tag)
519
                except ValueError:
520
                    pass
521
        return [{"minutes": minutes}]
522
523
    def get_cleanup(self):
524
        """Tasks and projects that need work."""
525
        result = []
526
        result.extend(self.get_lint())
527
        result.extend(self.get_empty_projects())
528
        result.extend(self.get_tag(self.tag_cleanup))
529
        result = [i for n, i in enumerate(result) if i not in result[n + 1 :]]
530
        return result
531
532
    def reset_config(self):
533
        """Reset the configuration."""
534
        print("Deleting: " + self.FILE_CONFIG)
535
        os.remove(self.FILE_CONFIG)
536
537
    def feedback(self):
538
        """Send feedback."""
539
        import webbrowser
0 ignored issues
show
introduced by
Import outside toplevel (webbrowser)
Loading history...
540
541
        recipient = "[email protected]"
542
        subject = "[KanbanView] Feedback"
543
        body = f"""
544
Description: 
545
Version: {__version__}
546
547
Steps that will reproduce the problem?
548
1. 
549
2. 
550
3. 
551
552
What is the expected result?
553
554
555
What happens instead?
556
557
558
Possible workaround:
559
560
561
Any additional information:
562
========= DEBUG INFORMATION =========
563
{self.debug_text}
564
========= DEBUG INFORMATION =========
565
        """
566
        # with open("body.txt", "r") as b:
567
        #     body = b.read()
568
        # body = body.replace(" ", "%20")
569
        print(body)
570
        webbrowser.open(
571
            "mailto:?to=" + recipient + "&subject=" + subject + "&body=" + body, new=1
572
        )
573
574
    @staticmethod
575
    def get_not_implemented():
576
        """Not implemented warning."""
577
        return [{"title": "not implemented"}]
578
579
    def get_rows(self, sql):
580
        """Query Things database."""
581
582
        sql = f"""
583
            SELECT DISTINCT
584
                TASK.uuid,
585
                TASK.title,
586
                CASE
587
                    WHEN AREA.title IS NOT NULL THEN AREA.title
588
                    WHEN PROJECT.title IS NOT NULL THEN PROJECT.title
589
                    WHEN HEADING.title IS NOT NULL THEN HEADING.title
590
                END AS context,
591
                CASE
592
                    WHEN AREA.uuid IS NOT NULL THEN AREA.uuid
593
                    WHEN PROJECT.uuid IS NOT NULL THEN PROJECT.uuid
594
                END AS context_uuid,
595
                CASE
596
                    WHEN TASK.recurrenceRule IS NULL
597
                    THEN strftime('%d.%m.', TASK.dueDate,"unixepoch") ||
598
                         substr(strftime('%Y', TASK.dueDate,"unixepoch"),3, 2)
599
                ELSE NULL
600
                END AS due,
601
                date(TASK.{self.DATE_CREATE},"unixepoch") as created,
602
                date(TASK.{self.DATE_MOD},"unixepoch") as modified,
603
                strftime('%d.%m.', TASK.startDate,"unixepoch") ||
604
                  substr(strftime('%Y', TASK.startDate,"unixepoch"),3, 2)
605
                  as started,
606
                date(TASK.stopDate,"unixepoch") as stopped,
607
                (SELECT COUNT(uuid)
608
                 FROM TMTask AS PROJECT_TASK
609
                 WHERE
610
                   PROJECT_TASK.project = TASK.uuid AND
611
                   PROJECT_TASK.{self.IS_NOT_TRASHED} AND
612
                   PROJECT_TASK.{self.IS_OPEN}
613
                ) AS size,
614
                CASE
615
                    WHEN TASK.{self.IS_TASK} THEN 'task'
616
                    WHEN TASK.{self.IS_PROJECT} THEN 'project'
617
                    WHEN TASK.{self.IS_HEADING} THEN 'heading'
618
                END AS type,
619
                CASE
620
                    WHEN TASK.{self.IS_OPEN} THEN 'open'
621
                    WHEN TASK.{self.IS_CANCELLED} THEN 'cancelled'
622
                    WHEN TASK.{self.IS_DONE} THEN 'done'
623
                END AS status,
624
                TASK.notes
625
            FROM
626
                {self.TABLE_TASK} AS TASK
627
            LEFT OUTER JOIN
628
                {self.TABLE_TASK} PROJECT ON TASK.project = PROJECT.uuid
629
            LEFT OUTER JOIN
630
                {self.TABLE_AREA} AREA ON TASK.area = AREA.uuid
631
            LEFT OUTER JOIN
632
                {self.TABLE_TASK} HEADING ON TASK.actionGroup = HEADING.uuid
633
            LEFT OUTER JOIN
634
                {self.TABLE_TASK} HEADPROJ ON HEADING.project = HEADPROJ.uuid
635
            LEFT OUTER JOIN
636
                {self.TABLE_TASKTAG} TAGS ON TASK.uuid = TAGS.tasks
637
            LEFT OUTER JOIN
638
                {self.TABLE_TAG} TAG ON TAGS.tags = TAG.uuid
639
            WHERE
640
                {self.filter}
641
                {sql}
642
                """
643
644
        return self.execute_query(sql)
645
646
    def execute_query(self, sql):
647
        """Run the actual query"""
648
        if self.debug is True:
649
            print(self.database)
650
            print(sql)
651
        try:
652
            connection = sqlite3.connect(  # pylint: disable=E1101
653
                "file:" + self.database + "?mode=ro", uri=True
654
            )
655
            connection.row_factory = Things3.dict_factory
656
            cursor = connection.cursor()
657
            cursor.execute(sql)
658
            tasks = cursor.fetchall()
659
            tasks = self.anonymize_tasks(tasks)
660
            if self.debug:
661
                for task in tasks:
662
                    print(task)
663
            return tasks
664
        except sqlite3.OperationalError as error:  # pylint: disable=E1101
665
            print(f"Could not query the database at: {self.database}.")
666
            print(f"Details: {error}.")
667
            sys.exit(2)
668
669
    # pylint: disable=C0103
670
    def mode_project(self):
671
        """Hack to switch to project view"""
672
        self.mode = "project"
673
        self.IS_TASK = self.MODE_PROJECT
674
675
    # pylint: disable=C0103
676
    def mode_task(self):
677
        """Hack to switch to project view"""
678
        self.mode = "to-do"
679
        self.IS_TASK = self.MODE_TASK
680
681
    functions = {
682
        "inbox": get_inbox,
683
        "today": get_today,
684
        "next": get_anytime,
685
        "backlog": get_someday,
686
        "upcoming": get_upcoming,
687
        "waiting": get_waiting,
688
        "mit": get_mit,
689
        "completed": get_completed,
690
        "cancelled": get_cancelled,
691
        "trashed": get_trashed,
692
        "projects": get_projects,
693
        "areas": get_areas,
694
        "all": get_all,
695
        "due": get_due,
696
        "lint": get_lint,
697
        "empty": get_empty_projects,
698
        "cleanup": get_cleanup,
699
        "top-proj": get_largest_projects,
700
        "stats-day": get_daystats,
701
        "stats-min-today": get_minutes_today,
702
        "reset": reset_config,
703
        "feedback": feedback,
704
    }
705