Skip to content

Commit 1800eb6

Browse files
new column 'grooming', added help text, started drag and drop, finding lint/empty projects
1 parent 1556860 commit 1800eb6

File tree

8 files changed

+147
-16
lines changed

8 files changed

+147
-16
lines changed

resources/demo.sqlite3

0 Bytes
Binary file not shown.

resources/kanban.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ a { color: #333; text-decoration: none; }
8080
.color5 {background:#2DB4B2;}
8181
.color6 {background:#37C57A;}
8282
.color7 {background:#1297FF;}
83+
.color8 {background:#6278D9;}
8384

8485
.loading {
8586
font-size:xx-large;
@@ -129,4 +130,5 @@ a { color: #333; text-decoration: none; }
129130
.color5 {background:#420043;}
130131
.color6 {background:#217944;}
131132
.color7 {background:#0F54A3;}
133+
.color8 {background:#3248A9;}
132134
}

resources/kanban.js

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@ function get_rows(rows) {
1717
var css_class = "hasNoProject";
1818
var task = row.title;
1919
var context = row.context;
20+
var uuid = 0;
2021

2122
if (row.uuid !== null) {
22-
task = `<a href='things:///show?id=${row.uuid}' target='_blank'>${row.title}</a>`;
23+
task = `<a draggable='false' href='things:///show?id=${row.uuid}' target='_blank'>${row.title}</a>`;
2324
}
2425
if (row.context_uuid !== null) {
25-
context = `<a href='things:///show?id=${row.context_uuid}' target='_blank'>` +
26+
context = `<a draggable='false' href='things:///show?id=${row.context_uuid}' target='_blank'>` +
2627
`${row.context}</a>`;
2728
}
2829
if (row.context !== null) {
@@ -36,7 +37,7 @@ function get_rows(rows) {
3637
row.due = "";
3738
}
3839

39-
fragment += "<div class='box'>" + task +
40+
fragment += `<div class='box' draggable='false' ondragstart='onDragStart(event);' id='${row.uuid}'>` + task +
4041
"<div class='deadline'>" + row.due + "</div>" +
4142
"<div class='area " + css_class + "'>" +
4243
context + "</div>" +
@@ -45,17 +46,17 @@ function get_rows(rows) {
4546
return fragment;
4647
}
4748

48-
function setup_html_column(cssclass, header, number, query) {
49-
return "<div class='column' id='"+header+"'>" +
49+
function setup_html_column(cssclass, header, number, query, help) {
50+
return "<div class='column' ondrop='onDrop(event);' ondragleave='onDragLeave(event);' ondragover='onDragOver(event);' id='"+header+"' title='"+help+"'>" +
5051
" <div class=''>" +
51-
" <a href='things:///show?" + query + "' target='_blank'><h2 class='" + cssclass + "'>" + header +
52+
" <a draggable='false' href='things:///show?" + query + "' target='_blank'><h2 class='" + cssclass + "'>" + header +
5253
" <span class='size'>" + number + "</span>" +
5354
" </h2></a>";
5455
}
5556

56-
function add(color, title, data, query) {
57+
function add(color, title, data, query, help) {
5758
var rows = JSON.parse(data.response);
58-
var fragment = setup_html_column(color, title, rows.length, query);
59+
var fragment = setup_html_column(color, title, rows.length, query, help);
5960
fragment += get_rows(rows);
6061
fragment += "</div></div>";
6162
if (document.getElementById(title) !== null) {
@@ -86,13 +87,69 @@ var makeRequest = function (url, method) {
8687
};
8788

8889
async function refresh() {
89-
await makeRequest("api/backlog").then(function (data) {add("color1", "Backlog", data, "id=someday");}).catch(function (result) { document.getElementById('loading').innerHTML = 'Error: ' + (result.statusText || 'no reply from database');})
90-
await makeRequest("api/upcoming").then(function (data) {add("color5", "Upcoming", data, "id=upcoming");}).catch(function (result) { document.getElementById('loading').innerHTML = 'Error: ' + (result.statusText || 'no reply from database');})
91-
await makeRequest("api/waiting").then(function (data) {add("color3", "Waiting", data, "query=Waiting");}).catch(function (result) { document.getElementById('loading').innerHTML = 'Error: ' + (result.statusText || 'no reply from database');})
92-
await makeRequest("api/inbox").then(function (data) {add("color4", "Inbox", data, "id=inbox");}).catch(function (result) { document.getElementById('loading').innerHTML = 'Error: ' + (result.statusText || 'no reply from database');})
93-
await makeRequest("api/mit").then(function (data) {add("color2", "MIT", data, "query=MIT");}).catch(function (result) { document.getElementById('loading').innerHTML = 'Error: ' + (result.statusText || 'no reply from database');})
94-
await makeRequest("api/today").then(function (data) {add("color6", "Today", data, "id=today");}).catch(function (result) { document.getElementById('loading').innerHTML = 'Error: ' + (result.statusText || 'no reply from database');})
95-
await makeRequest("api/next").then(function (data) {add("color7", "Next", data, "id=anytime");}).catch(function (result) { document.getElementById('loading').innerHTML = 'Error: ' + (result.statusText || 'no reply from database');})
90+
await makeRequest("api/backlog").then(function (data) {add("color1", "Backlog", data, "id=someday", "tasks in someday projects");}).catch(function (result) { document.getElementById('loading').innerHTML = 'Error: ' + (result.statusText || 'no reply from database');})
91+
await makeRequest("api/cleanup").then(function (data) {add("color8", "Grooming", data, "id=empty", "empty projects, tasks with no parent, items with tag 'Cleanup'");}).catch(function (result) { document.getElementById('loading').innerHTML = 'Error: ' + (result.statusText || 'no reply from database');})
92+
await makeRequest("api/upcoming").then(function (data) {add("color5", "Upcoming", data, "id=upcoming", "scheduled tasks");}).catch(function (result) { document.getElementById('loading').innerHTML = 'Error: ' + (result.statusText || 'no reply from database');})
93+
await makeRequest("api/waiting").then(function (data) {add("color3", "Waiting", data, "query=Waiting", "tasks with the tag 'Waiting'");}).catch(function (result) { document.getElementById('loading').innerHTML = 'Error: ' + (result.statusText || 'no reply from database');})
94+
await makeRequest("api/inbox").then(function (data) {add("color4", "Inbox", data, "id=inbox", "tasks in the inbox");}).catch(function (result) { document.getElementById('loading').innerHTML = 'Error: ' + (result.statusText || 'no reply from database');})
95+
await makeRequest("api/mit").then(function (data) {add("color2", "MIT", data, "query=MIT", "most important tasks with the tag 'MIT'");}).catch(function (result) { document.getElementById('loading').innerHTML = 'Error: ' + (result.statusText || 'no reply from database');})
96+
await makeRequest("api/today").then(function (data) {add("color6", "Today", data, "id=today", "tasks for today");}).catch(function (result) { document.getElementById('loading').innerHTML = 'Error: ' + (result.statusText || 'no reply from database');})
97+
await makeRequest("api/next").then(function (data) {add("color7", "Next", data, "id=anytime", "anytime tasks that are not in today");}).catch(function (result) { document.getElementById('loading').innerHTML = 'Error: ' + (result.statusText || 'no reply from database');})
98+
}
99+
100+
function onDragStart(event) {
101+
event
102+
.dataTransfer
103+
.setData('text/plain', event.target.id);
104+
105+
event
106+
.currentTarget
107+
.style
108+
.border = '2px solid green';
109+
}
110+
111+
function onDragOver(event) {
112+
event.preventDefault();
113+
event
114+
.currentTarget
115+
.style
116+
.border = '2px solid red';
117+
}
118+
119+
function onDragLeave(event) {
120+
event.preventDefault();
121+
event
122+
.currentTarget
123+
.style
124+
.border = '0';
125+
}
126+
127+
function onDrop(event) {
128+
event.preventDefault();
129+
event
130+
.currentTarget
131+
.style
132+
.border = '0';
133+
134+
const id = event
135+
.dataTransfer
136+
.getData('text');
137+
138+
const draggableElement = document.getElementById(id);
139+
const dropzone = event.target;
140+
141+
draggableElement
142+
.style
143+
.border = '0';
144+
145+
dropzone.appendChild(draggableElement);
146+
147+
event
148+
.dataTransfer
149+
.clearData();
150+
151+
console.log(dropzone.id)
152+
//refresh();
96153
}
97154

98155
window.onfocus = refresh;

tests/test_things3.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,21 @@ def test_due(self):
8888
tasks = self.things3.get_due()
8989
self.assertEqual(1, len(tasks))
9090

91+
def test_lint(self):
92+
"""Test tasks that should be cleaned up."""
93+
tasks = self.things3.get_lint()
94+
self.assertEqual(4, len(tasks))
95+
96+
def test_empty_projects(self):
97+
"""Test projects that are emptÿ."""
98+
tasks = self.things3.get_empty_projects()
99+
self.assertEqual(1, len(tasks))
100+
101+
def test_cleanup(self):
102+
"""Test tasks that should be cleaned up."""
103+
tasks = self.things3.get_cleanup()
104+
self.assertEqual(6, len(tasks))
105+
91106
def test_anonymize(self):
92107
"""Test anonymized tasks."""
93108
tasks = self.things3.get_today()

things3/things3.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,19 @@
2121
import getpass
2222

2323

24+
# pylint: disable=R0904
2425
class Things3():
2526
"""Simple read-only API for Things 3."""
2627
# Variables
28+
debug = False
2729
database = None
2830
json = False
2931
tag_waiting = "Waiting" if not environ.get('TAG_WAITING') \
3032
else environ.get('TAG_WAITING')
3133
tag_mit = "MIT" if not environ.get('TAG_MIT') \
3234
else environ.get('TAG_MIT')
35+
tag_cleanup = "Cleanup" if not environ.get('TAG_CLEANUP') \
36+
else environ.get('TAG_CLEANUP')
3337
anonymize = bool(environ.get('ANONYMIZE'))
3438

3539
# Database info
@@ -318,6 +322,45 @@ def get_due(self):
318322
"""
319323
return self.get_rows(query)
320324

325+
def get_lint(self):
326+
"""Get tasks that float around"""
327+
query = f"""
328+
TASK.{self.IS_NOT_TRASHED} AND
329+
TASK.{self.IS_OPEN} AND
330+
TASK.{self.IS_TASK} AND
331+
(TASK.{self.IS_SOMEDAY} OR TASK.{self.IS_ANYTIME}) AND
332+
TASK.project IS NULL AND
333+
TASK.area IS NULL AND
334+
TASK.actionGroup IS NULL
335+
"""
336+
return self.get_rows(query)
337+
338+
def get_empty_projects(self):
339+
"""Get projects that are empty"""
340+
query = f"""
341+
TASK.{self.IS_NOT_TRASHED} AND
342+
TASK.{self.IS_OPEN} AND
343+
TASK.{self.IS_PROJECT}
344+
GROUP BY TASK.uuid
345+
HAVING
346+
(SELECT COUNT(uuid)
347+
FROM TMTask
348+
WHERE
349+
project = TASK.uuid AND
350+
{self.IS_NOT_TRASHED} AND
351+
{self.IS_OPEN}
352+
) = 0
353+
"""
354+
return self.get_rows(query)
355+
356+
def get_cleanup(self):
357+
"""Tasks and projects that need work."""
358+
result = []
359+
result.extend(self.get_lint())
360+
result.extend(self.get_empty_projects())
361+
result.extend(self.get_tag(self.tag_cleanup))
362+
return result
363+
321364
@staticmethod
322365
def get_not_implemented():
323366
"""Not implemented warning."""
@@ -366,11 +409,17 @@ def get_rows(self, sql):
366409
WHERE
367410
""" + sql
368411

412+
if self.debug is True:
413+
print(sql)
414+
369415
try:
370416
cursor = sqlite3.connect(self.database).cursor()
371417
cursor.execute(sql)
372418
tasks = cursor.fetchall()
373419
tasks = self.anonymize_tasks(tasks)
420+
if self.debug:
421+
for task in tasks:
422+
print(task)
374423
return tasks
375424
except sqlite3.OperationalError as error:
376425
print(f"Could not query the database at: {self.database}.")
@@ -411,4 +460,7 @@ def convert_tasks_to_model(self, tasks):
411460
"trashed": get_trashed,
412461
"all": get_all,
413462
"due": get_due,
463+
"lint": get_lint,
464+
"empty": get_empty_projects,
465+
"cleanup": get_cleanup
414466
}

things3/things3_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def main(self):
6464
f'{things3_api.Things3API.PORT}/{self.FILE}',
6565
width=1024,
6666
min_size=(1024, 600),
67-
frameless=False)
67+
frameless=True)
6868
self.api_thread = Thread(target=self.open_api)
6969

7070
try:

things3/things3_cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,16 @@ def get_parser(cls):
8787
help='Exports tasks as CSV')
8888
subparsers.add_parser('due',
8989
help='Shows tasks with due dates')
90+
subparsers.add_parser('empty',
91+
help='Shows projects that are empty')
9092
subparsers.add_parser('headings',
9193
help='Shows headings')
9294
subparsers.add_parser('hours',
9395
help='Shows hours planned today')
9496
subparsers.add_parser('ical',
9597
help='Shows tasks ordered by due date as iCal')
98+
subparsers.add_parser('lint',
99+
help='Shows tasks that float around')
96100
subparsers.add_parser('logbook',
97101
help='Shows tasks completed today')
98102
subparsers.add_parser('mostClosed',

things3/things3_kanban.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def write_html_columns(file):
102102
"""Write HTML columns."""
103103

104104
write_html_column("color1", file, "Backlog", THINGS3.get_someday())
105+
write_html_column("color8", file, "Grooming", THINGS3.get_cleanup())
105106
write_html_column("color5", file, "Upcoming", THINGS3.get_upcoming())
106107
write_html_column("color3", file, "Waiting", THINGS3.get_waiting())
107108
write_html_column("color4", file, "Inbox", THINGS3.get_inbox())

0 commit comments

Comments
 (0)