Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ docs/_build/
.coverage
.hypothesis
*.tmp
.mypy_cache/

todoman/version.py
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ Authors are listed in alphabetical order.
* Swati Garg <swati4star@gmail.com>
* Thomas Glanzmann <thomas@glanzmann.de>
* https://github.com/Pikrass
* https://github.com/powerjungle
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ v4.7.0
by setuptools no longer has performance issues.
* Added shell completions for fish.
* Implement a new ``lists`` command to display all lists.
* Added support for RELATED-TO property and RELTYPE parameter, allowing the
parsing and creation of subtasks. This requires adding two new columns to
the database. The `SCHEMA_VERSION` has been incremented, so the cache will
be recreated. No changes to the configuration or lists is needed. The tasks
are now printed as a tree when subtasks are present.

v4.6.0
------
Expand Down
5 changes: 5 additions & 0 deletions docs/source/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ without them being properly documented.
Run ``pip install -e .`` to install todoman and its dependencies into a
virtualenv.

If the database is changed in a breaking way, the ``SCHEMA_VERSION`` variable
in the class ``Cache`` has to be incremented to allow the cache to be recreated
after todoman has been updated. An example would be adding new fields or
removing old unnecessary fields and etc.

Comment on lines +26 to +30
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice addition!

We use ``pre-commit`` to run style and convention checks. Run ``pre-commit
install``` to install our git-hooks. These will check code style and inform you
of any issues when attempting to commit. This will also run ``black`` to
Expand Down
6 changes: 6 additions & 0 deletions tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ def test_supported_fields_are_serializeable() -> None:
def test_vtodo_serialization(todo_factory: Callable) -> None:
"""Test VTODO serialization: one field of each type."""
description = "A tea would be nice, thanks."
related_val = "1292818859927632133"
related_val_reltype = "PARENT"
todo = todo_factory(
categories=["tea", "drinking", "hot"],
description=description,
Expand All @@ -58,6 +60,8 @@ def test_vtodo_serialization(todo_factory: Callable) -> None:
status="IN-PROCESS",
summary="Some tea",
rrule="FREQ=MONTHLY",
related_to=related_val,
related_to_reltype=related_val_reltype,
)
writer = VtodoWriter(todo)
vtodo = writer.serialize()
Expand All @@ -69,6 +73,8 @@ def test_vtodo_serialization(todo_factory: Callable) -> None:
assert vtodo.decoded("dtstart") == date(3000, 3, 21)
assert str(vtodo.get("status")) == "IN-PROCESS"
assert vtodo.get("rrule") == icalendar.vRecur.from_ical("FREQ=MONTHLY")
assert vtodo.get("related-to") == related_val
assert vtodo.get("related-to").params.get("reltype") == related_val_reltype


@freeze_time("2017-04-04 20:11:57")
Expand Down
16 changes: 5 additions & 11 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,13 +484,9 @@ def test_color_due_dates(
assert not result.exception
due_str = due.strftime("%Y-%m-%d")
if hours == 72:
expected = (
f"[ ] 1 \x1b[35m\x1b[0m \x1b[37m{due_str}\x1b[0m aaa @default\x1b[0m\n"
)
expected = f"[ ] 1 \x1b[37m{due_str}\x1b[0m aaa @default\x1b[0m\n"
else:
expected = (
f"[ ] 1 \x1b[35m\x1b[0m \x1b[31m{due_str}\x1b[0m aaa @default\x1b[0m\n"
)
expected = f"[ ] 1 \x1b[31m{due_str}\x1b[0m aaa @default\x1b[0m\n"
assert result.output == expected


Expand All @@ -499,17 +495,15 @@ def test_color_flag(runner: CliRunner, todo_factory: Callable) -> None:

result = runner.invoke(cli, ["--color", "always"], color=True)
assert (
result.output.strip()
== "[ ] 1 \x1b[35m\x1b[0m \x1b[31m2007-03-22\x1b[0m YARR! @default\x1b[0m"
result.output.strip() == "[ ] 1 \x1b[31m2007-03-22\x1b[0m YARR! @default\x1b[0m"
)
result = runner.invoke(cli, color=True)
assert (
result.output.strip()
== "[ ] 1 \x1b[35m\x1b[0m \x1b[31m2007-03-22\x1b[0m YARR! @default\x1b[0m"
result.output.strip() == "[ ] 1 \x1b[31m2007-03-22\x1b[0m YARR! @default\x1b[0m"
)

result = runner.invoke(cli, ["--color", "never"], color=True)
assert result.output.strip() == "[ ] 1 2007-03-22 YARR! @default"
assert result.output.strip() == "[ ] 1 2007-03-22 YARR! @default"


def test_flush(
Expand Down
10 changes: 6 additions & 4 deletions tests/test_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,17 @@ def test_format_datetime(default_formatter: DefaultFormatter) -> None:
def test_detailed_format(runner: CliRunner, todo_factory: Callable) -> None:
todo_factory(
description=(
"Test detailed formatting\nThis includes multiline descriptions\nBlah!"
"Test detailed formatting\n"
+ "This includes multiline descriptions\n"
+ "Blah!"
),
location="Over the hills, and far away",
)

# TODO:use formatter instead of runner?
# TODO: use formatter instead of runner?
result = runner.invoke(cli, ["show", "1"])
expected = [
"[ ] 1 (no due date) YARR! @default",
"[ ] 1 (no due date) YARR! @default",
"",
"Description:",
"Test detailed formatting",
Expand Down Expand Up @@ -202,7 +204,7 @@ def test_format_multiple_with_list(
assert todo.list
assert (
default_formatter.compact_multiple([todo])
== "[ ] 1 \x1b[35m\x1b[0m \x1b[37m(no due date)\x1b[0m YARR! @default\x1b[0m"
== "[ ] 1 \x1b[37m(no due date)\x1b[0m YARR! @default\x1b[0m"
)


Expand Down
10 changes: 10 additions & 0 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,12 @@ def test_todo_setters(todo_factory: Callable) -> None:
todo.due = None
assert todo.due is None

todo.related_to = "123"
assert todo.related_to == "123"

todo.related_to_reltype = "CHILD"
assert todo.related_to_reltype == "CHILD"


@freeze_time("2017-03-19-15")
def test_is_completed() -> None:
Expand Down Expand Up @@ -401,6 +407,8 @@ def test_clone() -> None:
todo.uid = "123"
todo.id = 123
todo.filename = "123.ics"
todo.related_to = "12345678"
todo.related_to_reltype = "PARENT"

clone = todo.clone()

Expand All @@ -412,6 +420,8 @@ def test_clone() -> None:
assert clone.id is None
assert todo.filename != clone.filename
assert clone.uid in clone.filename
assert todo.related_to == clone.related_to
assert todo.related_to_reltype == clone.related_to_reltype


@freeze_time("2017, 3, 20")
Expand Down
15 changes: 14 additions & 1 deletion tests/test_porcelain.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ def test_list_all(tmpdir: py.path.local, runner: CliRunner, create: Callable) ->
"DUE;VALUE=DATE-TIME;TZID=CET:20160102T000000\n"
"DTSTART:20160101T000000Z\n"
"PERCENT-COMPLETE:26\n"
"LOCATION:Wherever\n",
"LOCATION:Wherever\n"
"RELATED-TO;RELTYPE=PARENT:123456789\n",
)
result = runner.invoke(cli, ["--porcelain", "list", "--status", "ANY"])

Expand All @@ -40,6 +41,8 @@ def test_list_all(tmpdir: py.path.local, runner: CliRunner, create: Callable) ->
"percent": 26,
"priority": 0,
"recurring": False,
"related_to": "123456789",
"related_to_reltype": "PARENT",
"start": 1451606400,
"summary": "Do stuff",
}
Expand Down Expand Up @@ -78,6 +81,8 @@ def test_list_start_date(
"percent": 26,
"priority": 0,
"recurring": False,
"related_to": "",
"related_to_reltype": "",
Comment on lines +84 to +85
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should exclude the fields when absent, instead of including empty fields.

(same applies to other instances below)

"start": 1451692800,
"summary": "Do stuff",
}
Expand Down Expand Up @@ -114,6 +119,8 @@ def test_list_due_date(
"percent": 26,
"priority": 0,
"recurring": False,
"related_to": "",
"related_to_reltype": "",
"start": None,
"summary": "Do stuff",
}
Expand Down Expand Up @@ -143,6 +150,8 @@ def test_list_nodue(tmpdir: py.path.local, runner: CliRunner, create: Callable)
"location": "",
"percent": 12,
"recurring": False,
"related_to": "",
"related_to_reltype": "",
"priority": 4,
"start": None,
"summary": "Do stuff",
Expand Down Expand Up @@ -216,6 +225,8 @@ def test_show(tmpdir: py.path.local, runner: CliRunner, create: Callable) -> Non
"percent": 0,
"priority": 5,
"recurring": False,
"related_to": "",
"related_to_reltype": "",
"start": None,
"summary": "harhar",
}
Expand All @@ -241,6 +252,8 @@ def test_simple_action(todo_factory: Callable) -> None:
"percent": 0,
"priority": 0,
"recurring": False,
"related_to": "",
"related_to_reltype": "",
"start": None,
"summary": "YARR!",
}
Expand Down
49 changes: 48 additions & 1 deletion todoman/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ def wrapper(*a, **kw) -> _T:


TODO_ID_MIN = 1
with_id_arg = click.argument("id", type=click.IntRange(min=TODO_ID_MIN))
CLICK_TYPE_ID = click.IntRange(min=TODO_ID_MIN)

with_id_arg = click.argument("id", type=CLICK_TYPE_ID)


def _validate_lists_param(
Expand Down Expand Up @@ -238,6 +240,13 @@ def _todo_property_options(command: Callable) -> Callable:
help="When the task starts.",
)(command)

# Merges all different property arguments into one dictionary
# `todo_properties` argument, so that it can be directly looped through
# easily for directly setting the `todoman.model.Todo` class attributes
# from within a command function.
#
# The names of the options are the same as the
# `todoman.model.Todo` class attributes.
@functools.wraps(command)
def command_wrap(*a, **kw) -> click.Command:
Comment on lines +243 to 251
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: this should be a docstring rather than comment.

kw["todo_properties"] = {
Expand Down Expand Up @@ -284,6 +293,21 @@ def formatter(self) -> formatters.Formatter:
help="Go into interactive mode before saving the task.",
)

_subtask_option = click.option(
"--subtask-for",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of calling this --parent?

Code-wise it's a bit more obvious, but I'm not sure how obvious it is for a user interface.

is_flag=False,
default=None,
type=CLICK_TYPE_ID,
help="Set task to be a subtask for the given id.",
)

_not_subtask_option = click.option(
"--not-subtask",
is_flag=True,
default=False,
help="Make task no longer be a subtask.",
)


@click.group(invoke_without_command=True)
@click_log.simple_verbosity_option()
Expand Down Expand Up @@ -423,6 +447,7 @@ def repl(ctx: click.Context) -> None:
)
@_todo_property_options
@_interactive_option
@_subtask_option
@pass_ctx
@catch_errors
def new(
Expand All @@ -432,6 +457,7 @@ def new(
todo_properties: dict,
read_description: bool,
interactive: bool,
subtask_for: int | None,
) -> None:
"""
Create a new task with SUMMARY.
Expand All @@ -453,6 +479,11 @@ def new(
setattr(todo, key, value)
todo.summary = " ".join(summary)

if subtask_for is not None:
parent_todo = ctx.db.todo(subtask_for)
todo.related_to = parent_todo.uid
todo.related_to_reltype = "PARENT"

if read_description:
todo.description = sys.stdin.read()

Expand Down Expand Up @@ -487,6 +518,8 @@ def new(
)
@_todo_property_options
@_interactive_option
@_subtask_option
@_not_subtask_option
@with_id_arg
@catch_errors
def edit(
Expand All @@ -496,6 +529,8 @@ def edit(
interactive: bool,
read_description: bool,
raw: bool,
subtask_for: int | None,
not_subtask: bool,
) -> None:
"""
Edit the task with id ID.
Expand All @@ -516,6 +551,17 @@ def edit(
changes = True
todo.description = sys.stdin.read()

if subtask_for is not None and not_subtask is False:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If subtask_for and not_subtask, we should return an error (invalid arguments).

changes = True
parent_todo = ctx.db.todo(subtask_for)
todo.related_to = parent_todo.uid
todo.related_to_reltype = "PARENT"

if not_subtask:
changes = True
todo.related_to = ""
todo.related_to_reltype = ""

if interactive or (not changes and interactive is None):
ui = TodoEditor(todo, ctx.db.lists(), ctx.ui_formatter)
ui.edit()
Expand All @@ -528,6 +574,7 @@ def edit(
ctx.db.save(todo)
if old_list != new_list:
ctx.db.move(todo, new_list=new_list, from_list=old_list)

click.echo(ctx.formatter.detailed(todo))


Expand Down
Loading