Skip to content

Commit ab73287

Browse files
authored
Merge pull request #80 from AlexanderWillner/main
Patch to show for #79: show "yellow" To-Dos in Today and remove them from Upcoming
2 parents 4141f5c + 747cae0 commit ab73287

File tree

6 files changed

+158
-65
lines changed

6 files changed

+158
-65
lines changed

tests/main.sqlite

0 Bytes
Binary file not shown.

tests/main.sqlite-shm

0 Bytes
Binary file not shown.

tests/main.sqlite-wal

0 Bytes
Binary file not shown.

tests/test_things.py

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,31 @@
1414

1515
THINGSDB = things.database.ENVIRONMENT_VARIABLE_WITH_FILEPATH # type: ignore
1616

17+
# AW: to be continued, helps updating the test expectations when modifying the DB
18+
HEADINGS = 3
19+
INBOX = 2
20+
TRASHED_TODOS = 2
21+
TRASHED_PROJECTS = 1
22+
TRASHED_CANCELLED = 1
23+
TRASHED_COMPLETED = 1
24+
TRASHED_PROJECT_TODOS = 1
25+
TRASHED_PROJECT_TRASHED_TODOS = 1
26+
TRASHED = (
27+
TRASHED_TODOS
28+
+ TRASHED_PROJECTS
29+
+ TRASHED_CANCELLED
30+
+ TRASHED_COMPLETED
31+
+ TRASHED_PROJECT_TRASHED_TODOS
32+
)
33+
PROJECTS = 4
34+
UPCOMING = 1
35+
DEADLINE_PAST = 3
36+
DEADLINE_FUTURE = 1
37+
DEADLINE = DEADLINE_PAST + DEADLINE_FUTURE
38+
TODAY_PROJECTS = 1
39+
TODAY_TASKS = 4
40+
TODAY = TODAY_PROJECTS + TODAY_TASKS
41+
1742

1843
class ThingsCase(unittest.TestCase): # noqa: V103 pylint: disable=R0904
1944
"""Class documentation goes here."""
@@ -62,48 +87,68 @@ def test_search(self):
6287
# "To-Do in Heading",
6388
# "Completed To-Do in Heading",
6489
# "Canceled To-Do in Heading"
65-
self.assertEqual(3, len(todos))
90+
self.assertEqual(HEADINGS, len(todos))
6691

6792
def test_inbox(self):
6893
tasks = things.inbox()
69-
self.assertEqual(2, len(tasks))
94+
self.assertEqual(INBOX, len(tasks))
7095

7196
def test_trashed(self):
72-
tasks = things.trash()
73-
self.assertEqual(5, len(tasks))
7497
todos = things.todos(trashed=True)
75-
self.assertEqual(2, len(todos))
98+
self.assertEqual(TRASHED_TODOS, len(todos))
7699
projects = things.projects(trashed=True)
77-
self.assertEqual(1, len(projects))
100+
self.assertEqual(TRASHED_PROJECTS, len(projects))
78101
projects = things.projects(trashed=None)
79-
self.assertEqual(4, len(projects))
102+
self.assertEqual(PROJECTS, len(projects))
80103
projects = things.trash(type="project")
81-
self.assertEqual(1, len(projects))
104+
self.assertEqual(TRASHED_PROJECTS, len(projects))
105+
tasks = things.trash()
106+
self.assertEqual(TRASHED, len(tasks))
82107

83108
projects = things.trash(type="project", include_items=True)
84109
project_items = projects[0]["items"]
85-
self.assertEqual(1, len(project_items))
110+
self.assertEqual(TRASHED_PROJECTS, len(project_items))
86111
filtered_project_items = [
87112
item for item in project_items if "in Deleted Project" in item["title"]
88113
]
89-
self.assertEqual(1, len(filtered_project_items))
114+
self.assertEqual(TRASHED_PROJECT_TODOS, len(filtered_project_items))
90115

91116
# TK: Add this test case to the database:
92117
# to-do with trashed = 1 and whose project also has trashed = 1.
118+
# AW: These are actually not shown in the GUI
93119
tasks = things.tasks(type="to-do", trashed=True, context_trashed=True)
94120
self.assertEqual(0, len(tasks))
95121

96122
def test_upcoming(self):
97123
tasks = things.upcoming()
98-
self.assertEqual(1, len(tasks))
124+
self.assertEqual(UPCOMING, len(tasks))
99125

100126
def test_deadlines(self):
127+
tasks = things.tasks(deadline="past")
128+
self.assertEqual(DEADLINE_PAST, len(tasks))
129+
tasks = things.tasks(deadline="future")
130+
self.assertEqual(DEADLINE_FUTURE, len(tasks))
101131
tasks = things.deadlines()
102-
self.assertEqual(1, len(tasks))
132+
self.assertEqual(DEADLINE, len(tasks))
133+
with self.assertRaises(ValueError):
134+
tasks = things.tasks(deadline="invalid_value")
103135

104136
def test_today(self):
137+
projects = things.today(type="project")
138+
self.assertEqual(TODAY_PROJECTS, len(projects))
139+
tasks = things.today(type="to-do")
140+
self.assertEqual(TODAY_TASKS, len(tasks))
105141
tasks = things.today()
106-
self.assertEqual(3, len(tasks))
142+
self.assertEqual(TODAY, len(tasks))
143+
tasks_today = [
144+
"Upcoming To-Do in Today (yellow)",
145+
"Project in Today",
146+
"To-Do in Today",
147+
"Repeating To-Do",
148+
"Overdue Todo automatically shown in Today",
149+
]
150+
for count, value in enumerate(tasks_today):
151+
self.assertEqual(value, tasks[count]["title"])
107152

108153
def test_checklist(self):
109154
checklist_items = things.checklist_items("3Eva4XFof6zWb9iSfYy4ej")
@@ -113,7 +158,7 @@ def test_checklist(self):
113158

114159
def test_anytime(self):
115160
tasks = things.anytime()
116-
self.assertEqual(12, len(tasks))
161+
self.assertEqual(14, len(tasks))
117162
self.assertTrue(any(task.get("area_title") == "Area 1" for task in tasks))
118163

119164
def test_logbook(self):
@@ -141,28 +186,28 @@ def test_get_by_uuid(self):
141186
self.assertEqual(4, len(task.keys())) # type: ignore
142187

143188
def test_todos(self):
144-
todos = things.todos(start="Anytime")
145-
self.assertEqual(8, len(todos))
146189
todos = things.todos(start="Anytime", status="completed")
147190
self.assertEqual(6, len(todos))
191+
todos = things.todos(start="Anytime")
192+
self.assertEqual(10, len(todos))
148193
todos = things.todos(status="completed")
149194
self.assertEqual(10, len(todos))
150195
todos = things.todos(include_items=True)
151-
self.assertEqual(12, len(todos))
196+
self.assertEqual(15, len(todos))
152197
tasks = things.tasks(include_items=True)
153-
self.assertEqual(16, len(tasks))
198+
self.assertEqual(19, len(tasks))
154199
with self.assertRaises(ValueError):
155200
things.todos(status="invalid_value")
156201
todo = things.todos("A2oPvtt4dXoypeoLc8uYzY")
157202
self.assertEqual(16, len(todo.keys())) # type: ignore
158203

159204
def test_tags(self):
205+
tags = things.tasks(tag="Errand")
206+
self.assertEqual(1, len(tags))
160207
tags = things.tags()
161208
self.assertEqual(5, len(tags))
162209
tags = things.tags(include_items=True)
163210
self.assertEqual(5, len(tags))
164-
tags = things.tasks(tag="Errand")
165-
self.assertEqual(1, len(tags))
166211
tag = things.tags(title="Errand")
167212
self.assertEqual("Errand", tag["title"]) # type: ignore
168213

@@ -174,7 +219,7 @@ def test_projects(self):
174219
projects = things.projects()
175220
self.assertEqual(3, len(projects))
176221
projects = things.projects(include_items=True)
177-
self.assertEqual(2, len(projects[0]["items"]))
222+
self.assertEqual(4, len(projects[0]["items"]))
178223

179224
def test_areas(self):
180225
areas = things.areas()
@@ -197,7 +242,7 @@ def test_last(self):
197242
self.assertEqual(len(last_tasks), 0)
198243

199244
last_tasks = things.last("10000w")
200-
self.assertEqual(len(last_tasks), 16)
245+
self.assertEqual(len(last_tasks), 19)
201246

202247
last_tasks = things.last("100y", status="completed")
203248
self.assertEqual(len(last_tasks), 10)

things/api.py

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
import os
1111
from shlex import quote
12-
import sys
1312
from typing import Dict, List, Union
1413

1514
from things.database import Database
@@ -86,16 +85,27 @@ def tasks(uuid=None, include_items=False, **kwargs): # noqa: C901
8685
- `tag == True`, only include tasks _with_ tags.
8786
- `tag == None` (default), then include all tasks.
8887
89-
start_date : bool or None, optional
88+
start_date : bool, str or None, optional
9089
- `start_date == False`, only include tasks _without_ a start date.
9190
- `start_date == True`, only include tasks _with_ a start date.
91+
- `start_date == 'future'`, only include tasks with a future start date.
92+
- `start_date == 'past'`, only include tasks with a past start date.
9293
- `start_date == None` (default), then include all tasks.
9394
94-
deadline : bool or None, optional
95+
deadline : bool, str or None, optional
9596
- `deadline == False`, only include tasks _without_ a deadline.
9697
- `deadline == True`, only include tasks _with_ a deadline.
98+
- `deadline == 'future'`, only include tasks with a deadline in the future.
99+
- `deadline == 'past'`, only include tasks with a deadline in the past.
97100
- `deadline == None` (default), then include all tasks.
98101
102+
deadline_suppressed : bool or None, optional
103+
Handle overdue tasks that have been moved from Today to Inbox, Anytime, or Someday.
104+
- `deadline_suppressed == True`, only include tasks with an overdue deadline
105+
that have been moved from the today view (Inbox, Anytime, Someday).
106+
- `deadline_suppressed == False`, skip tasks with an overdue deadline
107+
that have been moved from the today view to another view (Inbox, Anytime, Someday).
108+
99109
trashed : bool or None, optional, default False
100110
- `trashed == False` (default), only include non-trashed tasks.
101111
- `trashed == True`, only include trashed tasks.
@@ -476,30 +486,45 @@ def today(**kwargs):
476486
"""
477487
Read Today's tasks into dicts.
478488
479-
Note: This might not produce desired results if the Things app hasn't
480-
been opened yet today and the yellow "OK" button clicked for new tasks.
481-
In general, you can assume that whatever state the Things app was in
482-
when you last opened it, that's the state reflected here by the API.
489+
Note: This method is a bit tricky.
490+
The Things database reflects the status of the Things app when it was last opened.
491+
When you open the Things app, new to-dos might have appeared in the Today view.
492+
Those to-dos are indicated by a yellow dot in the app.
493+
We try to predict what those new to-dos would be and return them as well.
494+
Some cases, however, are not yet covered. This includes repeating to-dos.
483495
484496
See `things.api.tasks` for details on the optional parameters.
485497
"""
486-
database = pop_database(kwargs)
487-
if not database.was_modified_today(): # pragma: no cover
488-
print(
489-
"[NOTE] The results reflect the state of the Things app "
490-
"when it was last run. If the results seem out of date, "
491-
"then run the Things app and click the yellow 'OK' button "
492-
"to update Today's to-dos and projects.",
493-
file=sys.stderr,
494-
)
495-
return tasks(
498+
regular_today_tasks = tasks(
496499
start_date=True,
497500
start="Anytime",
498501
index="todayIndex",
499-
database=database,
500502
**kwargs,
501503
)
502504

505+
unconfirmed_scheduled_tasks = tasks(
506+
start_date="past",
507+
start="Someday",
508+
index="todayIndex",
509+
**kwargs,
510+
)
511+
512+
unconfirmed_overdue_tasks = tasks(
513+
start_date=False,
514+
deadline="past",
515+
deadline_suppressed=False,
516+
**kwargs,
517+
)
518+
519+
result = [
520+
*regular_today_tasks,
521+
*unconfirmed_scheduled_tasks,
522+
*unconfirmed_overdue_tasks,
523+
]
524+
result.sort(key=lambda task: (task["today_index"], task["start_date"]))
525+
526+
return result
527+
503528

504529
def upcoming(**kwargs):
505530
"""
@@ -510,7 +535,7 @@ def upcoming(**kwargs):
510535
511536
For details on parameters, see `things.api.tasks`.
512537
"""
513-
return tasks(start_date=True, start="Someday", **kwargs)
538+
return tasks(start_date="future", start="Someday", **kwargs)
514539

515540

516541
def anytime(**kwargs):

0 commit comments

Comments
 (0)