Skip to content

Commit

Permalink
new column 'grooming', added help text, started drag and drop, findin…
Browse files Browse the repository at this point in the history
…g lint/empty projects
  • Loading branch information
AlexanderWillner committed Apr 17, 2020
1 parent 1556860 commit 1800eb6
Showing 8 changed files with 147 additions and 16 deletions.
Binary file modified resources/demo.sqlite3
Binary file not shown.
2 changes: 2 additions & 0 deletions resources/kanban.css
Original file line number Diff line number Diff line change
@@ -80,6 +80,7 @@ a { color: #333; text-decoration: none; }
.color5 {background:#2DB4B2;}
.color6 {background:#37C57A;}
.color7 {background:#1297FF;}
.color8 {background:#6278D9;}

.loading {
font-size:xx-large;
@@ -129,4 +130,5 @@ a { color: #333; text-decoration: none; }
.color5 {background:#420043;}
.color6 {background:#217944;}
.color7 {background:#0F54A3;}
.color8 {background:#3248A9;}
}
87 changes: 72 additions & 15 deletions resources/kanban.js
Original file line number Diff line number Diff line change
@@ -17,12 +17,13 @@ function get_rows(rows) {
var css_class = "hasNoProject";
var task = row.title;
var context = row.context;
var uuid = 0;

if (row.uuid !== null) {
task = `<a href='things:///show?id=${row.uuid}' target='_blank'>${row.title}</a>`;
task = `<a draggable='false' href='things:///show?id=${row.uuid}' target='_blank'>${row.title}</a>`;
}
if (row.context_uuid !== null) {
context = `<a href='things:///show?id=${row.context_uuid}' target='_blank'>` +
context = `<a draggable='false' href='things:///show?id=${row.context_uuid}' target='_blank'>` +
`${row.context}</a>`;
}
if (row.context !== null) {
@@ -36,7 +37,7 @@ function get_rows(rows) {
row.due = "";
}

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

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

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

async function refresh() {
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');})
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');})
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');})
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');})
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');})
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');})
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');})
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');})
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');})
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');})
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');})
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');})
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');})
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');})
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');})
}

function onDragStart(event) {
event
.dataTransfer
.setData('text/plain', event.target.id);

event
.currentTarget
.style
.border = '2px solid green';
}

function onDragOver(event) {
event.preventDefault();
event
.currentTarget
.style
.border = '2px solid red';
}

function onDragLeave(event) {
event.preventDefault();
event
.currentTarget
.style
.border = '0';
}

function onDrop(event) {
event.preventDefault();
event
.currentTarget
.style
.border = '0';

const id = event
.dataTransfer
.getData('text');

const draggableElement = document.getElementById(id);
const dropzone = event.target;

draggableElement
.style
.border = '0';

dropzone.appendChild(draggableElement);

event
.dataTransfer
.clearData();

console.log(dropzone.id)
//refresh();
}

window.onfocus = refresh;
15 changes: 15 additions & 0 deletions tests/test_things3.py
Original file line number Diff line number Diff line change
@@ -88,6 +88,21 @@ def test_due(self):
tasks = self.things3.get_due()
self.assertEqual(1, len(tasks))

def test_lint(self):
"""Test tasks that should be cleaned up."""
tasks = self.things3.get_lint()
self.assertEqual(4, len(tasks))

def test_empty_projects(self):
"""Test projects that are emptÿ."""
tasks = self.things3.get_empty_projects()
self.assertEqual(1, len(tasks))

def test_cleanup(self):
"""Test tasks that should be cleaned up."""
tasks = self.things3.get_cleanup()
self.assertEqual(6, len(tasks))

def test_anonymize(self):
"""Test anonymized tasks."""
tasks = self.things3.get_today()
52 changes: 52 additions & 0 deletions things3/things3.py
Original file line number Diff line number Diff line change
@@ -21,15 +21,19 @@
import getpass


# pylint: disable=R0904
class Things3():
"""Simple read-only API for Things 3."""
# Variables
debug = False
database = None
json = False
tag_waiting = "Waiting" if not environ.get('TAG_WAITING') \
else environ.get('TAG_WAITING')
tag_mit = "MIT" if not environ.get('TAG_MIT') \
else environ.get('TAG_MIT')
tag_cleanup = "Cleanup" if not environ.get('TAG_CLEANUP') \
else environ.get('TAG_CLEANUP')
anonymize = bool(environ.get('ANONYMIZE'))

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

def get_lint(self):
"""Get tasks that float around"""
query = f"""
TASK.{self.IS_NOT_TRASHED} AND
TASK.{self.IS_OPEN} AND
TASK.{self.IS_TASK} AND
(TASK.{self.IS_SOMEDAY} OR TASK.{self.IS_ANYTIME}) AND
TASK.project IS NULL AND
TASK.area IS NULL AND
TASK.actionGroup IS NULL
"""
return self.get_rows(query)

def get_empty_projects(self):
"""Get projects that are empty"""
query = f"""
TASK.{self.IS_NOT_TRASHED} AND
TASK.{self.IS_OPEN} AND
TASK.{self.IS_PROJECT}
GROUP BY TASK.uuid
HAVING
(SELECT COUNT(uuid)
FROM TMTask
WHERE
project = TASK.uuid AND
{self.IS_NOT_TRASHED} AND
{self.IS_OPEN}
) = 0
"""
return self.get_rows(query)

def get_cleanup(self):
"""Tasks and projects that need work."""
result = []
result.extend(self.get_lint())
result.extend(self.get_empty_projects())
result.extend(self.get_tag(self.tag_cleanup))
return result

@staticmethod
def get_not_implemented():
"""Not implemented warning."""
@@ -366,11 +409,17 @@ def get_rows(self, sql):
WHERE
""" + sql

if self.debug is True:
print(sql)

try:
cursor = sqlite3.connect(self.database).cursor()
cursor.execute(sql)
tasks = cursor.fetchall()
tasks = self.anonymize_tasks(tasks)
if self.debug:
for task in tasks:
print(task)
return tasks
except sqlite3.OperationalError as error:
print(f"Could not query the database at: {self.database}.")
@@ -411,4 +460,7 @@ def convert_tasks_to_model(self, tasks):
"trashed": get_trashed,
"all": get_all,
"due": get_due,
"lint": get_lint,
"empty": get_empty_projects,
"cleanup": get_cleanup
}
2 changes: 1 addition & 1 deletion things3/things3_app.py
Original file line number Diff line number Diff line change
@@ -64,7 +64,7 @@ def main(self):
f'{things3_api.Things3API.PORT}/{self.FILE}',
width=1024,
min_size=(1024, 600),
frameless=False)
frameless=True)
self.api_thread = Thread(target=self.open_api)

try:
4 changes: 4 additions & 0 deletions things3/things3_cli.py
Original file line number Diff line number Diff line change
@@ -87,12 +87,16 @@ def get_parser(cls):
help='Exports tasks as CSV')
subparsers.add_parser('due',
help='Shows tasks with due dates')
subparsers.add_parser('empty',
help='Shows projects that are empty')
subparsers.add_parser('headings',
help='Shows headings')
subparsers.add_parser('hours',
help='Shows hours planned today')
subparsers.add_parser('ical',
help='Shows tasks ordered by due date as iCal')
subparsers.add_parser('lint',
help='Shows tasks that float around')
subparsers.add_parser('logbook',
help='Shows tasks completed today')
subparsers.add_parser('mostClosed',
1 change: 1 addition & 0 deletions things3/things3_kanban.py
Original file line number Diff line number Diff line change
@@ -102,6 +102,7 @@ def write_html_columns(file):
"""Write HTML columns."""

write_html_column("color1", file, "Backlog", THINGS3.get_someday())
write_html_column("color8", file, "Grooming", THINGS3.get_cleanup())
write_html_column("color5", file, "Upcoming", THINGS3.get_upcoming())
write_html_column("color3", file, "Waiting", THINGS3.get_waiting())
write_html_column("color4", file, "Inbox", THINGS3.get_inbox())

0 comments on commit 1800eb6

Please sign in to comment.