Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions tests/test_things.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,17 @@ def test_thingstime(self):
test_task = things.tasks("7F4vqUNiTvGKaCUfv5pqYG")
self.assertEqual(test_task.get("reminder_time"), "12:34")

def test_status_mapping(self):
task = things.tasks(uuid = '5HLnvorXMbqcbjUuPN6ywi')
assert task['title'] == 'Cancelled To-Do in Project'
assert task['status'] == 'canceled'
task = things.tasks(uuid='5u2yGhP4rMQUmPQYEpGYDd')
assert task['title'] == 'Completed To-Do in Project'
assert task['status'] == 'completed'
task = things.tasks(uuid='7F4vqUNiTvGKaCUfv5pqYG')
assert task['title'] == 'To-Do in Upcoming'
assert task['status'] == 'incomplete'


if __name__ == "__main__":
unittest.main()
Expand Down
27 changes: 27 additions & 0 deletions tests/with_fixtures/testloop.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/zsh


echo "-----1: ----$1"
if [ $1 ]; then
subdir=$1
else
subdir=tasktinder
fi

# echo "-----subdir: ----$subdir"

while true; do
clear
pipenv run pytest .

# pytest parameter
# -x fail after first failur
# -v
# -vv

# ----- fswatch
# -1 -> exit after one change
fswatch **/*.py -1
# fswatch lib -1

done
30 changes: 6 additions & 24 deletions things/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
from textwrap import dedent
from typing import Optional, Union
import weakref


from .database_mappings import *
# --------------------------------------------------
# Core constants
# --------------------------------------------------
Expand Down Expand Up @@ -43,12 +42,6 @@
"Someday": "start = 2",
}

STATUS_TO_FILTER = {
"incomplete": "status = 0",
"canceled": "status = 2",
"completed": "status = 3",
}

TRASHED_TO_FILTER = {True: "trashed = 1", False: "trashed = 0"}

TYPE_TO_FILTER = {
Expand Down Expand Up @@ -121,9 +114,7 @@
IS_HEADING = TYPE_TO_FILTER["heading"]

# Status
IS_INCOMPLETE = STATUS_TO_FILTER["incomplete"]
IS_CANCELED = STATUS_TO_FILTER["canceled"]
IS_COMPLETED = STATUS_TO_FILTER["completed"]
# moved to database_mappings

# Start
IS_INBOX = START_TO_FILTER["Inbox"]
Expand Down Expand Up @@ -247,7 +238,7 @@ def get_tasks( # pylint: disable=R0914,R0917
validate("start", start, [None] + list(START_TO_FILTER))
validate_date("start_date", start_date)
validate_date("stop_date", stop_date)
validate("status", status, [None] + list(STATUS_TO_FILTER))
validate("status", status, [None] + valid_values_for("status"))
validate("trashed", trashed, [None] + list(TRASHED_TO_FILTER))
validate("type", type, [None] + list(TYPE_TO_FILTER))
validate("context_trashed", context_trashed, [None, True, False])
Expand All @@ -263,7 +254,6 @@ def get_tasks( # pylint: disable=R0914,R0917
# See: https://docs.python.org/3/library/sqlite3.html#sqlite3.Cursor.execute

start_filter: str = START_TO_FILTER.get(start, "") # type: ignore
status_filter: str = STATUS_TO_FILTER.get(status, "") # type: ignore
trashed_filter: str = TRASHED_TO_FILTER.get(trashed, "") # type: ignore
type_filter: str = TYPE_TO_FILTER.get(type, "") # type: ignore

Expand Down Expand Up @@ -291,7 +281,7 @@ def get_tasks( # pylint: disable=R0914,R0917
{project_of_heading_trashed_filter}
{type_filter and f"AND TASK.{type_filter}"}
{start_filter and f"AND TASK.{start_filter}"}
{status_filter and f"AND TASK.{status_filter}"}
{make_complete_sql_filter_for('status', status)}
{make_filter('TASK.uuid', uuid)}
{make_filter("TASK.area", area)}
{project_filter}
Expand Down Expand Up @@ -374,11 +364,7 @@ def get_checklist_items(self, todo_uuid=None):
sql_query = f"""
SELECT
CHECKLIST_ITEM.title,
CASE
WHEN CHECKLIST_ITEM.{IS_INCOMPLETE} THEN 'incomplete'
WHEN CHECKLIST_ITEM.{IS_CANCELED} THEN 'canceled'
WHEN CHECKLIST_ITEM.{IS_COMPLETED} THEN 'completed'
END AS status,
{make_optional_field_select('status','CHECKLIST_ITEM')}
date(CHECKLIST_ITEM.stopDate, "unixepoch", "localtime") AS stop_date,
'checklist-item' as type,
CHECKLIST_ITEM.uuid,
Expand Down Expand Up @@ -547,11 +533,7 @@ def make_tasks_sql_query(where_predicate=None, order_predicate=None):
WHEN TASK.{IS_TRASHED} THEN 1
END AS trashed,
TASK.title,
CASE
WHEN TASK.{IS_INCOMPLETE} THEN 'incomplete'
WHEN TASK.{IS_CANCELED} THEN 'canceled'
WHEN TASK.{IS_COMPLETED} THEN 'completed'
END AS status,
{make_optional_field_select('status','TASK')}
CASE
WHEN AREA.uuid IS NOT NULL THEN AREA.uuid
END AS area,
Expand Down
97 changes: 97 additions & 0 deletions things/database_mappings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""
all repetitions of the status value mapping.

STATUS_TO_FILTER is used in these forms:

- in get_tasks:

[x] list(STATUS_TO_FILTER)


[x] status_filter: str = STATUS_TO_FILTER.get(status, "") # type: ignore


IS_INCOMPLETE etc is used in these forms:

- in get_checklist_items
[x] CASE
WHEN CHECKLIST_ITEM.{IS_INCOMPLETE} THEN 'incomplete'
WHEN CHECKLIST_ITEM.{IS_CANCELED} THEN 'canceled'
WHEN CHECKLIST_ITEM.{IS_COMPLETED} THEN 'completed'
- in make_tasks_sql_query
[x] CASE
WHEN TASK.{IS_INCOMPLETE} THEN 'incomplete'
WHEN TASK.{IS_CANCELED} THEN 'canceled'
WHEN TASK.{IS_COMPLETED} THEN 'completed'



- [x] status_filter is used only once:

status_filter
status_filter: str = make_sql_filter_for("status", status) # type: ignore
{status_filter and f"AND TASK.{status_filter}"}

"""

# this is the only place where the mapping is defined

VALUE_TO_SQL = {
'status': {
"incomplete": 0,
"canceled": 2,
"completed": 3,
}
}

# the inverse could be used to create test data:
VALUE_TO_API = {'status' : {v: k for k, v in VALUE_TO_SQL['status'].items()}}
# for more fields, above and below need to be created in a loop over the field names
VALID_VALUES = {
"status" : list(VALUE_TO_SQL['status']),
}

# these are more generic than needed just for status by taking the field parameter
def value_to_sql(field,value):
if field in list(VALUE_TO_SQL):
return VALUE_TO_SQL[field][value]
raise NotImplementedError

def value_to_api(field,value):
if field in list(VALUE_TO_API):
return VALUE_TO_API[field][value]
raise NotImplementedError

def valid_values_for(field):
if field in VALID_VALUES.keys():
return VALID_VALUES[field]
raise NotImplementedError


def _make_sql_filter_for(field, value):
if value in valid_values_for(field):
return f"{field} = {value_to_sql(field,value)}"
return ""

def make_complete_sql_filter_for(field, value):
if value in valid_values_for(field):
return f"AND TASK.{_make_sql_filter_for(field, value)}"
return ""

def make_optional_field_select(field,table):
indentation = " "
wcl = []
for value in valid_values_for(field):
wcl.append(f"{indentation} WHEN {table}.{_make_sql_filter_for(field, value)} THEN '{value}'")
when_clauses = "\n".join(wcl)
optional_select = f"""
CASE
{when_clauses}
{indentation}END AS status,
"""
return optional_select





66 changes: 66 additions & 0 deletions things/database_mappings_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@

from things.database_mappings import *
from things.database_mappings import (_make_sql_filter_for,) # not included in *
import pytest

# old constants, kept for testing purposes
_STATUS_TO_FILTER = {
"incomplete": "status = 0",
"canceled": "status = 2",
"completed": "status = 3",
}

# Status
_IS_INCOMPLETE = _STATUS_TO_FILTER["incomplete"]
_IS_CANCELED = _STATUS_TO_FILTER["canceled"]
_IS_COMPLETED = _STATUS_TO_FILTER["completed"]

def test_list():
assert list(_STATUS_TO_FILTER) == ["incomplete", "canceled", "completed"]

status_test_cases = [
("incomplete","status = 0"),("completed","status = 3"),("canceled","status = 2"),("",""),("something else",""),
]

@pytest.mark.parametrize("status,old_behaviour", status_test_cases)
def test_make_sql_filter_characterization(status,old_behaviour):
assert old_behaviour == _STATUS_TO_FILTER.get(status, "")

@pytest.mark.parametrize("status,expected", status_test_cases)
def test_make_sql_filter(status,expected):
assert _make_sql_filter_for("status", status) == expected


status_field_select_test_cases = [("status","TASK",
f"""
CASE
WHEN TASK.{_IS_INCOMPLETE} THEN 'incomplete'
WHEN TASK.{_IS_CANCELED} THEN 'canceled'
WHEN TASK.{_IS_COMPLETED} THEN 'completed'
END AS status,
"""),("status","CHECKLIST_ITEM",
f"""
CASE
WHEN CHECKLIST_ITEM.{_IS_INCOMPLETE} THEN 'incomplete'
WHEN CHECKLIST_ITEM.{_IS_CANCELED} THEN 'canceled'
WHEN CHECKLIST_ITEM.{_IS_COMPLETED} THEN 'completed'
END AS status,
"""

)]


@pytest.mark.parametrize("field,table,expected", status_field_select_test_cases)
def test_make_optional_field_select(field, table, expected):
canned_answers = {
status_field_select_test_cases[0][1]: status_field_select_test_cases[0][2],
status_field_select_test_cases[1][1]: status_field_select_test_cases[1][2]}

assert make_optional_field_select(field,table) == expected

def test_complete_status_filter():
status_filter = _make_sql_filter_for('status', 'completed')
complete_status_filter = status_filter and f"AND TASK.{status_filter}"
assert complete_status_filter == 'AND TASK.status = 3'
expected = 'AND TASK.status = 3'
assert make_complete_sql_filter_for('status','completed') == expected
Loading