Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

time only support for watson add #282

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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
9 changes: 5 additions & 4 deletions docs/user-guide/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ Add time for project with tag(s) that was not tracked live.
Example:


$ watson add --from "2018-03-20 12:00:00" --to "2018-03-20 13:00:00" \
programming +addfeature
$ watson add --from "2018-03-20 12:00:00" --to "2018-03-20 13:00:00" <project> +<tag>
$ watson add --from "07:00" --to "09:00" <project> +<tag>
$ watson add --from "2018-03-20 07:00" --to "09:00" <project> +<tag>

### Options

Flag | Help
-----|-----
`-f, --from DATE` | Date and time of start of tracked activity [required]
`-t, --to DATE` | Date and time of end of tracked activity [required]
`-f, --from TEXT` | Date and time of start of tracked activity. If date is omitted todays date is used. [required]
`-t, --to TEXT` | Date and time of end of tracked activity. If date is omitted todays date is used. [required]
`-c, --confirm-new-project` | Confirm addition of new project.
`-b, --confirm-new-tag` | Confirm creation of new tag.
`--help` | Show this message and exit.
Expand Down
100 changes: 100 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from watson import cli
from click.testing import CliRunner
import json
import os
import arrow
import pytest

@pytest.fixture
def setup(tmpdir):
os.environ["WATSON_DIR"] = tmpdir.strpath

# test for watson add <project> --from "hh:mm" --to "hh:mm"
def test_time_only(setup):

runner = CliRunner()

test_data = [("13:00", "15:00"),
("3:00", "17:00"),
("1:00", "3:41")]

project = "test_time_only"
data_index = 0

for data in test_data:
from_time = data[0]
to_time = data[1]

result = runner.invoke(cli.cli, ["add", project, '--from' , from_time, '--to', to_time])

result = runner.invoke(cli.cli, ["log", "-j"])
output = json.loads(result.output)[data_index]

assert output["project"] == project
today = arrow.now().floor("day").format("YYYY-MM-DD")
assert today in output["start"]
assert from_time in output["start"]
assert today in output["stop"]
assert to_time in output["stop"]

data_index += 1

# test for watson add <project> --from "yyyy-mm-dd hh:mm" --to "hh:mm"
def test_from_date_time(setup):
runner = CliRunner()

test_data = [("2016-03-24", "07:32", "08:54"),
("2017-04-28", "14:00", "17:31")]

project = "test_date_time"

for data in test_data:
date = data[0]
from_time = data[1]
to_time = data[2]

result = runner.invoke(cli.cli, ["add", project, '--from' , "{} {}".format(date, from_time), '--to', to_time])

# as watson log -a crashes, I use -f <date>
result = runner.invoke(cli.cli, ["log", "-f", date, "-j"])
runner.invoke(cli.cli, ["log"])
output = json.loads(result.output)[0]

assert output["project"] == project
assert date in output["start"]
assert from_time in output["start"]
assert date in output["stop"]
assert to_time in output["stop"]

# test for watson add <project> --from "yyyy-mm-dd hh:mm" --to ""yyyy-mm-dd hh:mm"
def test_date_time(setup):
runner = CliRunner()

test_data = [("2016-03-24", "07:32", "08:54"),
("2017-04-28", "14:00", "17:31")]

project = "test_date_time"

for data in test_data:
date = data[0]
from_time = data[1]
to_time = data[2]

result = runner.invoke(cli.cli, ["add", project, '--from' , "{} {}".format(date, from_time), '--to', "{} {}".format(date, to_time)])

# as watson log -a crashes, I use -f <date>
result = runner.invoke(cli.cli, ["log", "-f", date, "-j"])
runner.invoke(cli.cli, ["log"])
output = json.loads(result.output)[0]

assert output["project"] == project
assert date in output["start"]
assert from_time in output["start"]
assert date in output["stop"]
assert to_time in output["stop"]

def test_format_not_supported(setup):
runner = CliRunner()

result = runner.invoke(cli.cli, ["add", "test", "--from", "crash 13:30", "--to", "13:37"])
assert "Format not supported" in result.output
50 changes: 50 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
safe_save,
parse_tags,
PY2,
isTime,
isDateTime,
getDateTimeToday,
getMergedDateTime
)
from . import mock_datetime

Expand Down Expand Up @@ -228,3 +232,49 @@ def test_confirm_tags_reject_raises_abort(confirm):
watson_tags = ['a', 'b']
with pytest.raises(Abort):
confirm_project(tags, watson_tags)


@pytest.mark.parametrize("date_time_string, result", [
("2018-05-28 7:00", False),
("2018-05.28 7:00", False),
("7:00", True),
("17:00", True),
("117:00", False),
("117", False),
])
def test_isTime(date_time_string, result):
assert isTime(date_time_string) == result


@pytest.mark.parametrize("date_time_string, result", [
("2018-05-28 7:00", True),
("2018-05-28 07:00", True),
("2018-05-28 127:00", False),
("2018-05-f 7:00", False),
("2018-05-28 7.00", False),
("7:00", False),
])
def test_isDateTime(date_time_string, result):
assert isDateTime(date_time_string) == result


@pytest.mark.parametrize("date_time, time_string", [
("2014-04-01", "07:00"),
("2014-04-01", "17:00"),
])
def test_getMergedDateTime(date_time, time_string):
result = arrow.get("{} {}".format(date_time, time_string), 'YYYY-MM-DD HH:mm')
date = arrow.get(date_time, 'YYYY-MM-DD')
assert getMergedDateTime(date, time_string) == result


@pytest.mark.parametrize("time_string", [
("07:00"),
("17:00"),
("09:05"),
("13:37"),
])
def test_getDateTimeToday(time_string):
today = datetime.date.today().strftime("%Y-%m-%d")
result = arrow.get("{} {}".format(today, time_string), 'YYYY-MM-DD HH:mm').replace(tzinfo='local')
assert getDateTimeToday(time_string) == result
39 changes: 32 additions & 7 deletions watson/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
sorted_groupby,
style,
parse_tags,
isTime,
isDateTime,
getDateTimeToday,
getMergedDateTime
)


Expand Down Expand Up @@ -1013,13 +1017,14 @@ def frames(watson):
for frame in watson.frames:
click.echo(style('short_id', frame.id))


@cli.command(context_settings={'ignore_unknown_options': True})
@click.argument('args', nargs=-1)
@click.option('-f', '--from', 'from_', required=True, type=Date,
help="Date and time of start of tracked activity")
@click.option('-t', '--to', required=True, type=Date,
help="Date and time of end of tracked activity")
@click.option('-f', '--from', 'from_', required=True,
help="Date and time of start of tracked activity. "
"If date is omitted todays date is used.")
@click.option('-t', '--to', required=True,
help="Date and time of end of tracked activity. "
"If date is omitted todays date is used.")
@click.option('-c', '--confirm-new-project', is_flag=True, default=False,
help="Confirm addition of new project.")
@click.option('-b', '--confirm-new-tag', is_flag=True, default=False,
Expand All @@ -1032,8 +1037,9 @@ def add(watson, args, from_, to, confirm_new_project, confirm_new_tag):
Example:

\b
$ watson add --from "2018-03-20 12:00:00" --to "2018-03-20 13:00:00" \\
programming +addfeature
$ watson add --from "2018-03-20 12:00:00" --to "2018-03-20 13:00:00" <project> +<tag>
$ watson add --from "07:00" --to "09:00" <project> +<tag>
$ watson add --from "2018-03-20 07:00" --to "09:00" <project> +<tag>
"""
# parse project name from args
project = ' '.join(
Expand All @@ -1053,6 +1059,25 @@ def add(watson, args, from_, to, confirm_new_project, confirm_new_tag):
confirm_new_tag):
confirm_tags(tags, watson.tags)

# check if <from> and <to> are time without a date, if yes
# convert to todays datetime
if isDateTime(from_) and isDateTime(to):
from_ = arrow.get(from_).replace(tzinfo='local')
to = arrow.get(to).replace(tzinfo='local')
elif isDateTime(from_) and isTime(to):
from_ = arrow.get(from_).replace(tzinfo='local')
to = getMergedDateTime(from_.floor("day"), to)
elif isTime(from_) and isTime(to):
from_ = getDateTimeToday(from_)
to = getDateTimeToday(to)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if Arrow doesn't provide something to help with that. It feels a bit tedious to introduce our own regexp to parse the date & times.

Copy link
Author

Choose a reason for hiding this comment

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

I have checked the api and didn't find s.th. appropriate. But I am open for suggestions.

Copy link
Contributor

@davidag davidag May 23, 2019

Choose a reason for hiding this comment

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

I've been looking into it and there's something I don't understand: why are there two different click types Date and Time in cli.py?

It turns out that both accept the full date, but using different formats!

  • TimeParamType (aka, Time) is only used in watson stop:
$ watson stop --at 09:00
$ watson stop --at 2019-05-23T12:00
  • DateParamType (aka, Date) is used in every other place where a date is required:
$ watson report -f 2019-05-23
$ watson report -f "2019-05-23 12:00"
$ watson report -f "2019-05-23T12:00"

In my opinion, the following changes might solve issue #278 in a simpler way:

  1. Rename Date to DateTime which is more appropriate to what it does. Of course, rename also the corresponding ParamType classes.
  2. Allow DateTime to receive time only dates, which is precisely what the Time type does. You could reuse the code.
  3. Use DateTime where Time was used (i.e. watson stop).
  4. Remove Time and TimeParamType.

Please, remember that I'm not a member of this project, so I might be missing important stuff!! :-) what do you think @k4nar?

I think date/time input format is one of the aspects where Watson could get a nice improvement, so it's great that there's someone working on it!

Copy link
Author

Choose a reason for hiding this comment

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

I like the idea. I didn't dive deep so I was not aware of TimeParamType which is basically doing what I implemented to handle time only.
I also couldn't agree more that the api is not consistent for the different cmds and it felt like adding another balcony.
The only drawback I see so far is that it will be hard to handle the case where we want to reuse the date specified for --from for --to as the parameter processing would be done without the knowledge about the other parameter.

else:
raise click.ClickException(
style('error', "Format not supported\n"
"Usage:\n"
"watson add --from \"hh:mm\" --to \"hh:mm\"\n"
"watson add --from \"yyyy-mm-dd hh:mm\" --to \"hh:mm\"\n"
"watson add --from \"yyyy-mm-dd hh:mm\" --to \"yyyy-mm-dd hh:mm\"\n"))

# add a new frame, call watson save to update state files
frame = watson.add(project=project, tags=tags, from_date=from_, to_date=to)
click.echo(
Expand Down
21 changes: 21 additions & 0 deletions watson/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import click
import arrow
import re

from .fullmoon import get_last_full_moon

Expand Down Expand Up @@ -282,3 +283,23 @@ def parse_tags(values_list):
))
for i, w in enumerate(values_list) if w.startswith('+')
)))) # pile of pancakes !


def isTime(date_time_string):
match = re.search("^\d{1,2}:\d{1,2}$", date_time_string)
return match != None


def isDateTime(date_time_string):
match = re.search("^\d{4}-\d{1,2}-\d{1,2}\s\d{1,2}:\d{1,2}$", date_time_string)
return match != None


def getDateTimeToday(time_string):
date_today = arrow.now().floor("day")
return getMergedDateTime(date_today, time_string)


def getMergedDateTime(date_time, time_string):
hours, minutes = time_string.split(":")
return date_time.shift(hours=int(hours), minutes=int(minutes))