From c782c3d42c75398b7a34135326f722f9cef89ed7 Mon Sep 17 00:00:00 2001 From: Djings Date: Thu, 21 Nov 2024 13:28:54 +0100 Subject: [PATCH 01/10] manually applied the work of https://github.com/aero31aero from the PR 405 in the original jazzband watson repo https://github.com/jazzband/Watson/pull/405 to watson-next repo * added some FIXMEs in the process --- CHANGELOG.md | 4 ++ Makefile | 6 +- README.md | 17 +++++ docs/user-guide/commands.md | 10 +++ tests/test_utils.py | 4 +- tests/test_watson.py | 114 +++++++++++++++++++++++++++++- watson/cli.py | 137 ++++++++++++++++++++++++++++-------- watson/frames.py | 12 ++-- watson/utils.py | 12 +++- watson/watson.py | 39 ++++++++-- 10 files changed, 305 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d6e3a8..8257806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] + - notes can be added to frames, + reapplied aero31aeros work on the original watson https://github.com/jazzband/Watson/pull/405 + to watson-next + ## [2.1.0] - 2022-05-16 ### Added diff --git a/Makefile b/Makefile index b3c8427..dee50b0 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,10 @@ PYTHON ?= python PIP ?= pip -VENV = virtualenv -VENV_ARGS = -p $(PYTHON) +VENV = python3 +VENV_ARGS = -m venv +#VENV = virtualenv +#VENV_ARGS = -p $(PYTHON) VENV_DIR = $(CURDIR)/.venv VENV_WATSON_DIR = $(CURDIR)/data diff --git a/README.md b/README.md index bf0dc8a..d7c6b6e 100644 --- a/README.md +++ b/README.md @@ -61,3 +61,20 @@ License Watson-next is released under the MIT License. See the bundled LICENSE file for details. + +Develop +------- + +1. Make an environment +``` +make env +``` +2. Activate your environment +``` +source .venv/bin/activate +``` +3. Make it dev +``` +make install-dev +``` + diff --git a/docs/user-guide/commands.md b/docs/user-guide/commands.md index 09dba25..dd91366 100644 --- a/docs/user-guide/commands.md +++ b/docs/user-guide/commands.md @@ -28,6 +28,7 @@ Flag | Help `-t, --to DATETIME` | Date and time of end of tracked activity [required] `-c, --confirm-new-project` | Confirm addition of new project. `-b, --confirm-new-tag` | Confirm creation of new tag. +`-n, --note TEXT` | Add log message with the added project frame. `--help` | Show this message and exit. ## `aggregate` @@ -663,6 +664,7 @@ Flag | Help `-g, --gap / -G, --no-gap` | (Don't) leave gap between end time of previous project and start time of the current. `-c, --confirm-new-project` | Confirm addition of new project. `-b, --confirm-new-tag` | Confirm creation of new tag. +`-n, --note TEXT` | Add log message to started frame. `--help` | Show this message and exit. ## `status` @@ -710,17 +712,25 @@ If `--at` option is given, the provided stopping time is used. The specified time must be after the beginning of the to-be-ended frame and must not be in the future. +You can optionally pass a log message to be saved with the frame via +the ``-n/--note`` option. + Example: $ watson stop --at 13:37 Stopping project apollo11, started an hour ago and stopped 30 minutes ago. (id: e9ccd52) # noqa: E501 + $ watson stop -n "Done some thinking" + Stopping project apollo11, started a minute ago. (id: e7ccd52) + Log message: Done some thinking + ### Options Flag | Help -----|----- `--at DATETIME` | Stop frame at this time. Must be in (YYYY-MM-DDT)?HH:MM(:SS)? format. +`-n, --note TEXT` | Save given log message with the project frame. `--help` | Show this message and exit. ## `sync` diff --git a/tests/test_utils.py b/tests/test_utils.py index b993767..71854ab 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -300,7 +300,7 @@ def test_frames_to_csv(watson): result = frames_to_csv(watson.frames) read_csv = list(csv.reader(StringIO(result))) - header = ['id', 'start', 'stop', 'project', 'tags'] + header = ['id', 'start', 'stop', 'project', 'tags', 'note'] assert len(read_csv) == 2 assert read_csv[0] == header assert read_csv[1][3] == 'foo' @@ -319,7 +319,7 @@ def test_frames_to_json(watson): result = json.loads(frames_to_json(watson.frames)) - keys = {'id', 'start', 'stop', 'project', 'tags'} + keys = {'id', 'start', 'stop', 'project', 'tags', 'note'} assert len(result) == 1 assert set(result[0].keys()) == keys assert result[0]['project'] == 'foo' diff --git a/tests/test_watson.py b/tests/test_watson.py index 0261835..66c866b 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -197,6 +197,58 @@ def test_frames_with_empty_given_state(config_dir, mocker): mocker.patch('builtins.open', mocker.mock_open(read_data=content)) assert len(watson.frames) == 0 +def test_frames_with_note(mocker, watson): + """Test loading frames with notes.""" + content = json.dumps([ + [3601, 3610, 'foo', 'abcdefg', ['A', 'B', 'C'], 3650, + "My hovercraft is full of eels"] + ]) + + mocker.patch('builtins.open', mocker.mock_open(read_data=content)) + assert len(watson.frames) == 1 + frame = watson.frames['abcdefg'] + assert frame.id == 'abcdefg' + assert frame.project == 'foo' + assert frame.start == arrow.get(3601) + assert frame.stop == arrow.get(3610) + assert frame.tags == ['A', 'B', 'C'] + assert frame.note == "My hovercraft is full of eels" + + +def test_frames_without_note(mocker, watson): + """Test loading frames without notes.""" + content = json.dumps([ + [3601, 3610, 'foo', 'abcdefg'], + [3611, 3620, 'foo', 'hijklmn', ['A', 'B', 'C']], + [3621, 3630, 'foo', 'opqrstu', ['A', 'B', 'C'], 3630] + ]) + + mocker.patch('builtins.open', mocker.mock_open(read_data=content)) + assert len(watson.frames) == 3 + frame = watson.frames['abcdefg'] + assert frame.id == 'abcdefg' + assert frame.project == 'foo' + assert frame.start == arrow.get(3601) + assert frame.stop == arrow.get(3610) + assert frame.tags == [] + assert frame.note is None + + frame = watson.frames['hijklmn'] + assert frame.id == 'hijklmn' + assert frame.tags == ['A', 'B', 'C'] + assert frame.note is None + + frame = watson.frames['opqrstu'] + assert frame.id == 'opqrstu' + assert frame.tags == ['A', 'B', 'C'] + assert frame.updated_at == arrow.get(3630) + assert frame.note is None + + + + + + # config @@ -355,6 +407,30 @@ def test_stop_started_project_at(watson): watson.stop(stop_at=now) assert watson.frames[-1].stop == now +def test_stop_started_project_without_note(watson): + """Test stopping watson without adding a note.""" + watson.start('foo') + watson.stop() + + assert watson.current == {} + assert watson.is_started is False + assert len(watson.frames) == 1 + frame = watson.frames[0] + assert frame.project == 'foo' + assert frame.note is None + + +def test_stop_started_project_with_note(watson): + """Test stopping watson when adding a note.""" + watson.start('foo') + watson.stop(None, "My hovercraft is full of eels") + + assert watson.current == {} + assert watson.is_started is False + assert len(watson.frames) == 1 + frame = watson.frames[0] + assert frame.project == 'foo' + assert frame.note == "My hovercraft is full of eels" # cancel @@ -419,7 +495,7 @@ def test_save_empty_current(config_dir, mocker, json_mock): assert json_mock.call_count == 1 result = json_mock.call_args[0][0] - assert result == {'project': 'foo', 'start': 4000, 'tags': []} + assert result == {'project': 'foo', 'start': 4000, 'tags': [], 'note': None} watson.current = {} watson.save() @@ -779,9 +855,12 @@ def test_report(watson): assert 'time' in report['projects'][0]['tags'][0] assert report['projects'][0]['tags'][1]['name'] == 'B' assert 'time' in report['projects'][0]['tags'][1] + assert len(report['projects'][0]['notes']) == 0 + assert len(report['projects'][0]['tags'][0]['notes']) == 0 + assert len(report['projects'][0]['tags'][1]['notes']) == 0 watson.start('bar', tags=['C']) - watson.stop() + watson.stop(note='bar note') report = watson.report(arrow.now(), arrow.now()) assert len(report['projects']) == 2 @@ -790,6 +869,13 @@ def test_report(watson): assert len(report['projects'][0]['tags']) == 1 assert report['projects'][0]['tags'][0]['name'] == 'C' + assert len(report['projects'][1]['notes']) == 0 + assert len(report['projects'][1]['tags'][0]['notes']) == 0 + assert len(report['projects'][1]['tags'][1]['notes']) == 0 + assert len(report['projects'][0]['notes']) == 0 + assert len(report['projects'][0]['tags'][0]['notes']) == 1 + assert report['projects'][0]['tags'][0]['notes'][0] == 'bar note' + report = watson.report( arrow.now(), arrow.now(), projects=['foo'], tags=['B'] ) @@ -799,16 +885,38 @@ def test_report(watson): assert report['projects'][0]['tags'][0]['name'] == 'B' watson.start('baz', tags=['D']) - watson.stop() + watson.stop(note='baz note') + + watson.start('foo') + watson.stop(note='foo no tags') + + watson.start('foo', tags=['A']) + watson.stop(note='foo one tag A') report = watson.report(arrow.now(), arrow.now(), projects=["foo"]) + assert len(report['projects']) == 1 + assert len(report['projects'][0]['notes']) == 1 + # A project-level note because this frame has no tags + assert report['projects'][0]['notes'][0] == 'foo no tags' + assert len(report['projects'][0]['tags']) == 2 + assert report['projects'][0]['tags'][0]['name'] == 'A' + assert report['projects'][0]['tags'][1]['name'] == 'B' + assert len(report['projects'][0]['tags'][0]['notes']) == 1 + assert len(report['projects'][0]['tags'][1]['notes']) == 0 + # A tag-level note because this frame has tags + assert report['projects'][0]['tags'][0]['notes'][0] == 'foo one tag A' + + report = watson.report(arrow.now(), arrow.now(), ignore_projects=["bar"]) assert len(report['projects']) == 2 report = watson.report(arrow.now(), arrow.now(), tags=["A"]) assert len(report['projects']) == 1 + assert len(report['projects'][0]['notes']) == 0 + assert len(report['projects'][0]['tags'][0]['notes']) == 1 + assert report['projects'][0]['tags'][0]['notes'][0] == 'foo one tag A' report = watson.report(arrow.now(), arrow.now(), ignore_tags=["D"]) assert len(report['projects']) == 2 diff --git a/watson/cli.py b/watson/cli.py index 80790dd..5e6b305 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -28,6 +28,7 @@ confirm_tags, create_watson, flatten_report_for_csv, + format_note, format_timedelta, frames_to_csv, frames_to_json, @@ -184,17 +185,19 @@ def help(ctx, command): click.echo(cmd.get_help(ctx)) -def _start(watson, project, tags, restart=False, start_at=None, gap=True): +def _start(watson, project, tags, restart=False, start_at=None, gap=True, note=None): """ Start project with given list of tags and save status. """ current = watson.start(project, tags, restart=restart, start_at=start_at, - gap=gap,) + gap=gap, note=note) click.echo("Starting project {}{} at {}".format( style('project', project), (" " if current['tags'] else "") + style('tags', current['tags']), style('time', "{:HH:mm}".format(current['start'])) )) + if note: + click.echo(format_note(note)) watson.save() @@ -213,11 +216,13 @@ def _start(watson, project, tags, restart=False, start_at=None, gap=True): help="Confirm addition of new project.") @click.option('-b', '--confirm-new-tag', is_flag=True, default=False, help="Confirm creation of new tag.") +@click.option('-n', '--note', type=str, default=None, + help="A brief note that describe time entry being started") @click.pass_obj @click.pass_context @catch_watson_error def start(ctx, watson, confirm_new_project, confirm_new_tag, args, at_, - gap_=True): + gap_=True, note=None): """ Start monitoring time for the given project. You can add tags indicating more specifically what you are working on with @@ -280,16 +285,18 @@ def start(ctx, watson, confirm_new_project, confirm_new_tag, args, at_, watson.config.getboolean('options', 'stop_on_start')): ctx.invoke(stop, at_=at_) - _start(watson, project, tags, start_at=at_, gap=gap_) + _start(watson, project, tags, start_at=at_, gap=gap_, note=note) @cli.command(context_settings={'ignore_unknown_options': True}) @click.option('--at', 'at_', type=DateTime, default=None, help=('Stop frame at this time. Must be in ' '(YYYY-MM-DDT)?HH:MM(:SS)? format.')) +@click.option('-n', '--note', type=str, default=None, + help="A brief note that describe time entry being stopped") @click.pass_obj @catch_watson_error -def stop(watson, at_): +def stop(watson, at_, note): """ Stop monitoring time for the current project. @@ -297,13 +304,20 @@ def stop(watson, at_): specified time must be after the beginning of the to-be-ended frame and must not be in the future. - Example: + You can optionally pass a log message to be saved with the frame via + the ``-n/--note`` option. + + Examples: \b $ watson stop --at 13:37 Stopping project apollo11, started an hour ago and stopped 30 minutes ago. (id: e9ccd52) # noqa: E501 + + $ watson stop -n "Done some thinking" + Stopping project apollo11, started a minute ago. (id: e7ccd52) + >> Done some thinking """ - frame = watson.stop(stop_at=at_) + frame = watson.stop(stop_at=at_, note=note) output_str = "Stopping project {}{}, started {} and stopped {}. (id: {})" click.echo(output_str.format( style('project', frame.project), @@ -312,6 +326,10 @@ def stop(watson, at_): style('time', frame.stop.humanize()), style('short_id', frame.id), )) + + if frame.note: + click.echo(format_note(frame.note)) + watson.save() @@ -474,6 +492,12 @@ def status(watson, project, tags, elapsed): style('time', current['start'].strftime(timefmt)) )) + if current['note']: + click.echo(u"{}{}".format( + style('note', '>> '), + style('note', current['note']) + )) + _SHORTCUT_OPTIONS = ['all', 'year', 'month', 'luna', 'week', 'day'] _SHORTCUT_OPTIONS_VALUES = { @@ -546,11 +570,13 @@ def status(watson, project, tags, elapsed): help="Format output in plain text (default)") @click.option('-g/-G', '--pager/--no-pager', 'pager', default=None, help="(Don't) view output through a pager.") +@click.option('-n', '--notes', 'show_notes', default=False, is_flag=True, + help="Show frame notes in report.") @click.pass_obj @catch_watson_error def report(watson, current, from_, to, projects, tags, ignore_projects, ignore_tags, year, month, week, day, luna, all, output_format, - pager, aggregated=False, include_partial_frames=True): + pager, aggregated=False, include_partial_frames=True, show_notes=False): """ Display a report of the time spent on each project. @@ -576,6 +602,10 @@ def report(watson, current, from_, to, projects, tags, ignore_projects, If you are outputting to the terminal, you can selectively enable a pager through the `--pager` option. + You can include frame notes in the report by passing the --notes + option. Messages will always be present in *JSON* reports. Messages are + never included in *CSV* reports. + You can change the output format for the report from *plain text* to *JSON* using the `--json` option or to *CSV* using the `--csv` option. Only one of these two options can be used at once. @@ -630,14 +660,17 @@ def report(watson, current, from_, to, projects, tags, ignore_projects, "tags": [ { "name": "export", - "time": 530.0 + "time": 530.0, + "notes": ["working hard"] }, { "name": "report", "time": 530.0 } ], - "time": 530.0 + "time": 530.0, + "notes": ["fixing bug #74", "refactor tests"] + } ], "time": 530.0, @@ -737,6 +770,13 @@ def _final_print(lines): project=style('project', project['name']) )) + if show_notes: + for note in project['notes']: + _print(u'{tab}{note}'.format( + tab=tab, + note=format_note(note), + )) + tags = project['tags'] if tags: longest_tag = max(len(tag) for tag in tags or ['']) @@ -750,6 +790,13 @@ def _final_print(lines): tag['name'], longest_tag )), )) + + if show_notes: + for note in tag['notes']: + _print(u'\t{tab}{note}'.format( + tab=tab, + note=format_note(note), + )) _print("") # if this is a report invoked from `aggregate` return the lines; do not @@ -808,11 +855,13 @@ def _final_print(lines): help="Format output in plain text (default)") @click.option('-g/-G', '--pager/--no-pager', 'pager', default=None, help="(Don't) view output through a pager.") +@click.option('-n', '--notes', 'show_notes', default=False, is_flag=True, + help="Show frame notes in report.") @click.pass_obj @click.pass_context @catch_watson_error def aggregate(ctx, watson, current, from_, to, projects, tags, output_format, - pager, aggregation): + pager, aggregation, show_notes): """ Display a report of the time spent on each project aggregated by day. @@ -909,7 +958,8 @@ def aggregate(ctx, watson, current, from_, to, projects, tags, output_format, to=end, projects=projects, tags=tags, output_format=output_format, pager=pager, aggregated=True, - include_partial_frames=True) + include_partial_frames=True, + show_notes=show_notes) if 'json' in output_format: lines.append(output) @@ -999,10 +1049,12 @@ def aggregate(ctx, watson, current, from_, to, projects, tags, output_format, help="Format output in plain text (default)") @click.option('-g/-G', '--pager/--no-pager', 'pager', default=None, help="(Don't) view output through a pager.") +@click.option('-n/-N', '--notes/--no-notes', 'show_notes', default=True, + help="(Don't) output notes.") @click.pass_obj @catch_watson_error def log(watson, current, reverse, from_, to, projects, tags, ignore_projects, - ignore_tags, year, month, week, day, luna, all, output_format, pager): + ignore_tags, year, month, week, day, luna, all, output_format, pager, show_notes): """ Display each recorded session during the given timespan. @@ -1029,6 +1081,9 @@ def log(watson, current, reverse, from_, to, projects, tags, ignore_projects, `--json` option or to *CSV* using the `--csv` option. Only one of these two options can be used at once. + You can control whether or not notes for each frame are displayed by + passing --notes or --no-notes. + Example: \b @@ -1059,12 +1114,12 @@ def log(watson, current, reverse, from_, to, projects, tags, ignore_projects, 1070ddb 13:48 to 16:17 2h 29m 11s voyager1 [antenna, sensors] \b $ watson log --from 2014-04-16 --to 2014-04-17 --csv - id,start,stop,project,tags - a96fcde,2014-04-17 09:15,2014-04-17 09:43,hubble,"lens, camera, transmission" - 5e91316,2014-04-17 10:19,2014-04-17 12:59,hubble,"camera, transmission" - 761dd51,2014-04-17 14:42,2014-04-17 15:54,voyager1,antenna - 02cb269,2014-04-16 09:53,2014-04-16 12:43,apollo11,wheels - 1070ddb,2014-04-16 13:48,2014-04-16 16:17,voyager1,"antenna, sensors" + id,start,stop,project,tags,note + a96fcde,2014-04-17 09:15,2014-04-17 09:43,hubble,"lens, camera, transmission", + 5e91316,2014-04-17 10:19,2014-04-17 12:59,hubble,"camera, transmission", + 761dd51,2014-04-17 14:42,2014-04-17 15:54,voyager1,antenna, + 02cb269,2014-04-16 09:53,2014-04-16 12:43,apollo11,wheels, + 1070ddb,2014-04-16 13:48,2014-04-16 16:17,voyager1,"antenna, sensors", """ # noqa for start_time in (_ for _ in [day, week, month, luna, year, all] if _ is not None): @@ -1087,7 +1142,7 @@ def log(watson, current, reverse, from_, to, projects, tags, ignore_projects, watson.config.getboolean('options', 'log_current')): cur = watson.current watson.frames.add(cur['project'], cur['start'], arrow.utcnow(), - cur['tags'], id="current") + cur['tags'], id="current", note=cur['note']) if reverse is None: reverse = watson.config.getboolean('options', 'reverse_log', True) @@ -1150,8 +1205,13 @@ def _final_print(lines): ) ) + def get_note_string(frame): + if frame.note is not None and frame.note != '' and show_notes: + return u"\t{}{}".format(" "*9, format_note(frame.note)) + return '' + _print("\n".join( - "\t{id} {start} to {stop} {delta:>11} {project}{tags}".format( + "\t{id} {start} to {stop} {delta:>11} {project}{tags}{notes}".format( delta=format_timedelta(frame.stop - frame.start), project=style('project', '{:>{}}'.format( frame.project, longest_project @@ -1159,7 +1219,8 @@ def _final_print(lines): tags=(" "*2 if frame.tags else "") + style('tags', frame.tags), start=style('time', '{:HH:mm}'.format(frame.start)), stop=style('time', '{:HH:mm}'.format(frame.stop)), - id=style('short_id', frame.id) + id=style('short_id', frame.id), + notes=get_note_string(frame) ) for frame in frames )) @@ -1246,6 +1307,7 @@ def frames(watson): help="Confirm addition of new project.") @click.option('-b', '--confirm-new-tag', is_flag=True, default=False, help="Confirm creation of new tag.") +# FIXME: accept -n 'note' here! @click.pass_obj @catch_watson_error def add(watson, args, from_, to, confirm_new_project, confirm_new_tag): @@ -1325,7 +1387,7 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): id = frame.id elif watson.is_started: frame = Frame(watson.current['start'], None, watson.current['project'], - None, watson.current['tags']) + None, watson.current['tags'], None, watson.current['note']) elif watson.frames: frame = watson.frames[-1] id = frame.id @@ -1338,6 +1400,7 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): 'start': frame.start.format(datetime_format), 'project': frame.project, 'tags': frame.tags, + 'note' : "" if frame.note is None else frame.note, } if id: @@ -1382,6 +1445,7 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): raise ValueError("Start time cannot be in the future") if stop and stop > arrow.utcnow(): raise ValueError("Stop time cannot be in the future") + note = data.get('note') # break out of while loop and continue execution of # the edit function normally break @@ -1402,9 +1466,18 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): # we reach this when we break out of the while loop above if id: - watson.frames[id] = (project, start, stop, tags) + # This is new: + if all((project == frame.project, start == frame.start, + stop == frame.stop, tags == frame.tags, + note == frame.note)): + updated_at = frame.updated_at + else: + updated_at = arrow.utcnow() + + watson.frames[id] = (project, start, stop, tags, id, updated_at, note) + else: - watson.current = dict(start=start, project=project, tags=tags) + watson.current = dict(start=start, project=project, tags=tags, note=note) watson.save() click.echo( @@ -1424,6 +1497,10 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): ) ) + if note is not None and note != '': + click.echo("Note: {}".format(style('note', note))) + + @cli.command(context_settings={'ignore_unknown_options': True}) @click.argument('id', shell_complete=get_frames) @@ -1666,7 +1743,8 @@ def merge(watson, frames_with_conflict, force): 'project': original_frame.project, 'start': original_frame.start.format(date_format), 'stop': original_frame.stop.format(date_format), - 'tags': original_frame.tags + 'tags': original_frame.tags, + 'note': original_frame.note } click.echo("frame {}:".format(style('short_id', original_frame.id))) click.echo("{}".format('\n'.join('<' + line for line in json.dumps( @@ -1698,7 +1776,8 @@ def merge(watson, frames_with_conflict, force): 'project': conflict_frame_copy.project, 'start': conflict_frame_copy.start.format(date_format), 'stop': conflict_frame_copy.stop.format(date_format), - 'tags': conflict_frame_copy.tags + 'tags': conflict_frame_copy.tags, + 'note': conflict_frame_copy.note } click.echo("{}".format('\n'.join('>' + line for line in json.dumps( conflict_frame_data, indent=4, ensure_ascii=False).splitlines()))) @@ -1712,9 +1791,9 @@ def merge(watson, frames_with_conflict, force): # merge in any non-conflicting frames for frame in merging: - start, stop, project, id, tags, updated_at = frame.dump() + start, stop, project, id, tags, updated_at, note = frame.dump() original_frames.add(project, start, stop, tags=tags, id=id, - updated_at=updated_at) + updated_at=updated_at, note=note) watson.frames = original_frames watson.frames.changed = True diff --git a/watson/frames.py b/watson/frames.py index 0e51157..3133aae 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -4,11 +4,11 @@ from collections import namedtuple -HEADERS = ('start', 'stop', 'project', 'id', 'tags', 'updated_at') +HEADERS = ('start', 'stop', 'project', 'id', 'tags', 'updated_at', 'note') class Frame(namedtuple('Frame', HEADERS)): - def __new__(cls, start, stop, project, id, tags=None, updated_at=None,): + def __new__(cls, start, stop, project, id, tags=None, updated_at=None,note=None): try: if not isinstance(start, arrow.Arrow): start = arrow.get(start) @@ -33,7 +33,7 @@ def __new__(cls, start, stop, project, id, tags=None, updated_at=None,): tags = [] return super(Frame, cls).__new__( - cls, start, stop, project, id, tags, updated_at + cls, start, stop, project, id, tags, updated_at, note, ) def dump(self): @@ -41,7 +41,7 @@ def dump(self): stop = self.stop.to('utc').int_timestamp if self.stop else None updated_at = self.updated_at.int_timestamp - return (start, stop, self.project, self.id, self.tags, updated_at) + return (start, stop, self.project, self.id, self.tags, updated_at, self.note) @property def day(self): @@ -139,11 +139,11 @@ def add(self, *args, **kwargs): return frame def new_frame(self, project, start, stop, tags=None, id=None, - updated_at=None): + updated_at=None, note=None): if not id: id = uuid.uuid4().hex return Frame(start, stop, project, id, tags=tags, - updated_at=updated_at) + updated_at=updated_at, note=note) def dump(self): return tuple(frame.dump() for frame in self._rows) diff --git a/watson/utils.py b/watson/utils.py index c276860..e6d2935 100644 --- a/watson/utils.py +++ b/watson/utils.py @@ -68,7 +68,8 @@ def _style_short_id(id): 'error': {'fg': 'red'}, 'date': {'fg': 'cyan'}, 'short_id': _style_short_id, - 'id': {'fg': 'white'} + 'id': {'fg': 'white'}, + 'note': {'fg': 'white'}, } fmt = formats.get(name, {}) @@ -298,6 +299,7 @@ def frames_to_json(frames): ('stop', frame.stop.isoformat()), ('project', frame.project), ('tags', frame.tags), + ('note', frame.note), ]) for frame in frames ] @@ -320,6 +322,7 @@ def frames_to_csv(frames): ('stop', frame.stop.format('YYYY-MM-DD HH:mm:ss')), ('project', frame.project), ('tags', ', '.join(frame.tags)), + ('note', frame.note if frame.note else "") ]) for frame in frames ] @@ -400,3 +403,10 @@ def json_arrow_encoder(obj): return obj.for_json() raise TypeError("Object {} is not JSON serializable".format(obj)) + +def format_note(note): + return u"{}{}".format( + style('note', '>> '), + style('note', note.replace('\n', '\n' + ' '*20)) + ) + diff --git a/watson/watson.py b/watson/watson.py index 52460f4..241395c 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -144,6 +144,7 @@ def save(self): 'project': self.current['project'], 'start': self._format_date(self.current['start']), 'tags': self.current['tags'], + 'note': self.current.get('note'), } else: current = {} @@ -205,7 +206,8 @@ def current(self, value): self._current = { 'project': value['project'], 'start': start, - 'tags': value.get('tags') or [] + 'tags': value.get('tags') or [], + 'note': value.get('note'), } if self._old_state is None: @@ -235,6 +237,7 @@ def last_sync(self, value): def is_started(self): return bool(self.current) + # FIXME: add "note" parameter def add(self, project, from_date, to_date, tags): if not project: raise WatsonError("No project given.") @@ -244,11 +247,12 @@ def add(self, project, from_date, to_date, tags): default_tags = self.config.getlist('default_tags', project) tags = (tags or []) + default_tags + # FIXME: add "note" parameter frame = self.frames.add(project, from_date, to_date, tags=tags) return frame def start(self, project, tags=None, restart=False, start_at=None, - gap=True): + gap=True, note=None): if self.is_started: raise WatsonError( "Project {} is already started.".format( @@ -272,7 +276,7 @@ def start(self, project, tags=None, restart=False, start_at=None, if start_at > arrow.now(): raise WatsonError('Task cannot start in the future.') - new_frame = {'project': project, 'tags': deduplicate(tags)} + new_frame = {'project': project, 'tags': deduplicate(tags), 'note': note} new_frame['start'] = start_at if not gap: stop_of_prev_frame = self.frames[-1].stop @@ -280,7 +284,7 @@ def start(self, project, tags=None, restart=False, start_at=None, self.current = new_frame return self.current - def stop(self, stop_at=None): + def stop(self, stop_at=None, note=None): if not self.is_started: raise WatsonError("No project started.") @@ -299,7 +303,7 @@ def stop(self, stop_at=None): raise WatsonError('Task cannot end in the future.') frame = self.frames.add( - old['project'], old['start'], stop_at, tags=old['tags'] + old['project'], old['start'], stop_at, tags=old['tags'], note=note ) self.current = None @@ -548,6 +552,7 @@ def report(self, from_, to, current=None, projects=None, tags=None, if self.current and current: cur = self.current + # FIXME: this seems to need note! self.frames.add(cur['project'], cur['start'], arrow.utcnow(), cur['tags'], id="current") @@ -585,10 +590,27 @@ def report(self, from_, to, current=None, projects=None, tags=None, ) total += delta + project_notes = [] + for frame in frames: + # If the user is trying to print out all frames in the project + # (tags will be empty because no tags were passed) + if not tags and frame.note: + # And this frame has no tags... + if not frame.tags: + # Add it to the project-level notes because it + # won't get included in the tag-level notes + # because it has no tag. + project_notes.append(frame.note) + # And this frame has a tag... + else: + # Let the tag-level filter handle this frame later on + pass + project_report = { 'name': project, 'time': delta.total_seconds(), - 'tags': [] + 'tags': [], + 'notes': project_notes, } if tags is None: @@ -606,9 +628,12 @@ def report(self, from_, to, current=None, projects=None, tags=None, datetime.timedelta() ) + tag_notes = [frame.note for frame in frames if tag in frame.tags and frame.note] + project_report['tags'].append({ 'name': tag, - 'time': delta.total_seconds() + 'time': delta.total_seconds(), + 'notes': tag_notes, }) report['projects'].append(project_report) From 51cd11c1580b00d8901a8c405df7fbe7f1c56393 Mon Sep 17 00:00:00 2001 From: Djings Date: Thu, 21 Nov 2024 14:03:27 +0100 Subject: [PATCH 02/10] added code to accept the original note from watson start, or concatenate notes if start and stop notes ares specified --- watson/watson.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/watson/watson.py b/watson/watson.py index 241395c..dbeec8b 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -284,7 +284,7 @@ def start(self, project, tags=None, restart=False, start_at=None, self.current = new_frame return self.current - def stop(self, stop_at=None, note=None): + def stop(self, stop_at=None, note=None, note_handling="concat"): if not self.is_started: raise WatsonError("No project started.") @@ -302,6 +302,15 @@ def stop(self, stop_at=None, note=None): if stop_at > arrow.now(): raise WatsonError('Task cannot end in the future.') + # no note specified, use that from the current frame + if note is None: + note = old.get('note') + else: + if note_handling == "concat": + note = old.get('note', "") + note + else: + note = old.get('note') + frame = self.frames.add( old['project'], old['start'], stop_at, tags=old['tags'], note=note ) From c5f1ba0c61326f0e0a6247320945320441a20c4a Mon Sep 17 00:00:00 2001 From: Djings Date: Thu, 21 Nov 2024 14:06:41 +0100 Subject: [PATCH 03/10] fixed, show note in report if --current is included --- watson/watson.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/watson/watson.py b/watson/watson.py index dbeec8b..adb72ec 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -561,9 +561,8 @@ def report(self, from_, to, current=None, projects=None, tags=None, if self.current and current: cur = self.current - # FIXME: this seems to need note! self.frames.add(cur['project'], cur['start'], arrow.utcnow(), - cur['tags'], id="current") + cur['tags'], note=cur['note'], id="current") span = self.frames.span(from_, to) From 3bd3a3a1b5ab44f856bde94e20fcb4448779c72a Mon Sep 17 00:00:00 2001 From: Djings Date: Thu, 21 Nov 2024 14:52:24 +0100 Subject: [PATCH 04/10] added flag to stop command to allow concatenation of notes from start/stop * added a configuration option for that flag --- docs/user-guide/configuration.md | 9 +++++++++ watson/cli.py | 9 ++++++--- watson/watson.py | 12 +++++------- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md index fbccd93..5fb750d 100644 --- a/docs/user-guide/configuration.md +++ b/docs/user-guide/configuration.md @@ -184,6 +184,15 @@ Globally configure which day corresponds to the start of a week. Allowable values are `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, and `sunday`. +#### `options.notes_concatenate` (default: `false`) + +Using `--note` notes can be specified on start and stop of a frame. + +If you stop a frame and specify a note on a frame that already has a note +assigend, it will by default replace the note with the last one specified. + +Changing this option to `true` will instead concatenate both note strings. + ### Default tags diff --git a/watson/cli.py b/watson/cli.py index 5e6b305..e621c98 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -293,10 +293,13 @@ def start(ctx, watson, confirm_new_project, confirm_new_tag, args, at_, help=('Stop frame at this time. Must be in ' '(YYYY-MM-DDT)?HH:MM(:SS)? format.')) @click.option('-n', '--note', type=str, default=None, - help="A brief note that describe time entry being stopped") + help="A brief note that describe time entry being stopped." + + "Unless -c is specified, will by default replace a note specified on start.") +@click.option('-c', '--concat-note', is_flag=True, default=False, + help="Will concatenate the note specified at start to the note specified on stop.") @click.pass_obj @catch_watson_error -def stop(watson, at_, note): +def stop(watson, at_, note, concat_note): """ Stop monitoring time for the current project. @@ -317,7 +320,7 @@ def stop(watson, at_, note): Stopping project apollo11, started a minute ago. (id: e7ccd52) >> Done some thinking """ - frame = watson.stop(stop_at=at_, note=note) + frame = watson.stop(stop_at=at_, note=note, concat_note=concat_note) output_str = "Stopping project {}{}, started {} and stopped {}. (id: {})" click.echo(output_str.format( style('project', frame.project), diff --git a/watson/watson.py b/watson/watson.py index adb72ec..a32b84a 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -284,7 +284,7 @@ def start(self, project, tags=None, restart=False, start_at=None, self.current = new_frame return self.current - def stop(self, stop_at=None, note=None, note_handling="concat"): + def stop(self, stop_at=None, note=None, concat_note=False): if not self.is_started: raise WatsonError("No project started.") @@ -302,14 +302,12 @@ def stop(self, stop_at=None, note=None, note_handling="concat"): if stop_at > arrow.now(): raise WatsonError('Task cannot end in the future.') - # no note specified, use that from the current frame + concat_note = concat_note or self.config.getboolean('options', 'notes_concatenate') + if note is None: note = old.get('note') - else: - if note_handling == "concat": - note = old.get('note', "") + note - else: - note = old.get('note') + elif concat_note: + note = old.get('note', "") + note frame = self.frames.add( old['project'], old['start'], stop_at, tags=old['tags'], note=note From c9184346d52ff907c11fbd09182823ebf63294eb Mon Sep 17 00:00:00 2001 From: Djings Date: Thu, 21 Nov 2024 15:01:23 +0100 Subject: [PATCH 05/10] added --note to watson add --- watson/cli.py | 10 +++++++--- watson/watson.py | 6 ++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/watson/cli.py b/watson/cli.py index e621c98..b071eae 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -1310,10 +1310,11 @@ def frames(watson): help="Confirm addition of new project.") @click.option('-b', '--confirm-new-tag', is_flag=True, default=False, help="Confirm creation of new tag.") -# FIXME: accept -n 'note' here! +@click.option('-n', '--note', type=str, default=None, + help="A brief note that describe time entry being added.") @click.pass_obj @catch_watson_error -def add(watson, args, from_, to, confirm_new_project, confirm_new_tag): +def add(watson, args, from_, to, confirm_new_project, confirm_new_tag, note): """ Add time to a project with tag(s) that was not tracked live. @@ -1344,7 +1345,7 @@ def add(watson, args, from_, to, confirm_new_project, confirm_new_tag): confirm_tags(tags, watson.tags) # add a new frame, call watson save to update state files - frame = watson.add(project=project, tags=tags, from_date=from_, to_date=to) + frame = watson.add(project=project, tags=tags, from_date=from_, to_date=to, note=note) click.echo( "Adding project {}{}, started {} and stopped {}. (id: {})".format( style('project', frame.project), @@ -1354,6 +1355,9 @@ def add(watson, args, from_, to, confirm_new_project, confirm_new_tag): style('short_id', frame.id) ) ) + if note: + click.echo(format_note(note)) + watson.save() diff --git a/watson/watson.py b/watson/watson.py index a32b84a..0b7ba70 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -237,8 +237,7 @@ def last_sync(self, value): def is_started(self): return bool(self.current) - # FIXME: add "note" parameter - def add(self, project, from_date, to_date, tags): + def add(self, project, from_date, to_date, tags, note): if not project: raise WatsonError("No project given.") if from_date > to_date: @@ -247,8 +246,7 @@ def add(self, project, from_date, to_date, tags): default_tags = self.config.getlist('default_tags', project) tags = (tags or []) + default_tags - # FIXME: add "note" parameter - frame = self.frames.add(project, from_date, to_date, tags=tags) + frame = self.frames.add(project, from_date, to_date, tags=tags, note=note) return frame def start(self, project, tags=None, restart=False, start_at=None, From 4ea82e99a74b63cb43155b3d0553a7367fb597ae Mon Sep 17 00:00:00 2001 From: Djings Date: Thu, 21 Nov 2024 15:17:40 +0100 Subject: [PATCH 06/10] made note for watson.add an optional argument --- watson/watson.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watson/watson.py b/watson/watson.py index 0b7ba70..5a61473 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -237,7 +237,7 @@ def last_sync(self, value): def is_started(self): return bool(self.current) - def add(self, project, from_date, to_date, tags, note): + def add(self, project, from_date, to_date, tags, note=None): if not project: raise WatsonError("No project given.") if from_date > to_date: From 39866edf654a7b1ef80d73f67bd112507aad355a Mon Sep 17 00:00:00 2001 From: Djings Date: Thu, 21 Nov 2024 15:19:40 +0100 Subject: [PATCH 07/10] made makefile to use -m venv and removed setup.py usage --- Makefile | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index dee50b0..545c427 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,8 @@ PYTHON ?= python PIP ?= pip -VENV = python3 -VENV_ARGS = -m venv -#VENV = virtualenv -#VENV_ARGS = -p $(PYTHON) +VENV = $(PYTHON) -m venv +VENV_ARGS = VENV_DIR = $(CURDIR)/.venv VENV_WATSON_DIR = $(CURDIR)/data @@ -24,16 +22,16 @@ env: $(VENV_DIR) .PHONY: install install: - $(PYTHON) setup.py install + $(PYTHON) -m pip install . .PHONY: install-dev install-dev: $(PIP) install -r requirements-dev.txt - $(PYTHON) setup.py develop + $(PYTHON) -m pip install --editable . .PHONY: check check: clean - $(PYTHON) setup.py test + pytest .PHONY: clean clean: From 9435789d74abd4267e25d9f748a58f4aadaaf235 Mon Sep 17 00:00:00 2001 From: Djings Date: Thu, 21 Nov 2024 15:45:30 +0100 Subject: [PATCH 08/10] added data/ directory to .gitignore --- .gitignore | 1 + CHANGELOG.md | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 41ea6bd..7ef80db 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ site/ .vagrant/ .venv/ .pytest_cache/ +data/ # files diff --git a/CHANGELOG.md b/CHANGELOG.md index 8257806..455be6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - - notes can be added to frames, - reapplied aero31aeros work on the original watson https://github.com/jazzband/Watson/pull/405 - to watson-next +- notes can be added to frames, + reapplied aero31aeros work on the original watson https://github.com/jazzband/Watson/pull/405 + to watson-next, fixed some bugs, added some features ## [2.1.0] - 2022-05-16 From edadd5b7797f6b56a0ebdb0b81acd43bc0dffec2 Mon Sep 17 00:00:00 2001 From: Djings Date: Thu, 21 Nov 2024 15:52:48 +0100 Subject: [PATCH 09/10] added at least the missing version number to changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 455be6f..a4e99ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 reapplied aero31aeros work on the original watson https://github.com/jazzband/Watson/pull/405 to watson-next, fixed some bugs, added some features +## [3.1.1] - 2024-10-07 + + - ??? + ## [2.1.0] - 2022-05-16 ### Added From 4a49efa65894c60abfcf6b8258fe1ca60f950998 Mon Sep 17 00:00:00 2001 From: Djings Date: Mon, 2 Dec 2024 08:21:13 +0100 Subject: [PATCH 10/10] Added lots of linebreaks fix linter issues --- tests/test_watson.py | 13 +++++-------- watson/cli.py | 30 +++++++++++++++++++----------- watson/frames.py | 6 ++++-- watson/utils.py | 2 +- watson/watson.py | 12 ++++++++---- 5 files changed, 37 insertions(+), 26 deletions(-) diff --git a/tests/test_watson.py b/tests/test_watson.py index 66c866b..e108171 100644 --- a/tests/test_watson.py +++ b/tests/test_watson.py @@ -197,6 +197,7 @@ def test_frames_with_empty_given_state(config_dir, mocker): mocker.patch('builtins.open', mocker.mock_open(read_data=content)) assert len(watson.frames) == 0 + def test_frames_with_note(mocker, watson): """Test loading frames with notes.""" content = json.dumps([ @@ -245,11 +246,6 @@ def test_frames_without_note(mocker, watson): assert frame.note is None - - - - - # config def test_empty_config_dir(): @@ -407,6 +403,7 @@ def test_stop_started_project_at(watson): watson.stop(stop_at=now) assert watson.frames[-1].stop == now + def test_stop_started_project_without_note(watson): """Test stopping watson without adding a note.""" watson.start('foo') @@ -432,6 +429,7 @@ def test_stop_started_project_with_note(watson): assert frame.project == 'foo' assert frame.note == "My hovercraft is full of eels" + # cancel def test_cancel_started_project(watson): @@ -495,7 +493,8 @@ def test_save_empty_current(config_dir, mocker, json_mock): assert json_mock.call_count == 1 result = json_mock.call_args[0][0] - assert result == {'project': 'foo', 'start': 4000, 'tags': [], 'note': None} + assert result == {'project': 'foo', 'start': 4000, 'tags': [], + 'note': None} watson.current = {} watson.save() @@ -907,8 +906,6 @@ def test_report(watson): # A tag-level note because this frame has tags assert report['projects'][0]['tags'][0]['notes'][0] == 'foo one tag A' - - report = watson.report(arrow.now(), arrow.now(), ignore_projects=["bar"]) assert len(report['projects']) == 2 diff --git a/watson/cli.py b/watson/cli.py index b071eae..40d5106 100644 --- a/watson/cli.py +++ b/watson/cli.py @@ -185,7 +185,8 @@ def help(ctx, command): click.echo(cmd.get_help(ctx)) -def _start(watson, project, tags, restart=False, start_at=None, gap=True, note=None): +def _start(watson, project, tags, restart=False, start_at=None, gap=True, + note=None): """ Start project with given list of tags and save status. """ @@ -294,9 +295,11 @@ def start(ctx, watson, confirm_new_project, confirm_new_tag, args, at_, '(YYYY-MM-DDT)?HH:MM(:SS)? format.')) @click.option('-n', '--note', type=str, default=None, help="A brief note that describe time entry being stopped." + - "Unless -c is specified, will by default replace a note specified on start.") + "Unless -c is specified, will by default replace a " + + "note specified on start.") @click.option('-c', '--concat-note', is_flag=True, default=False, - help="Will concatenate the note specified at start to the note specified on stop.") + help="Will concatenate the note specified at start " + + "to the note specified on stop.") @click.pass_obj @catch_watson_error def stop(watson, at_, note, concat_note): @@ -579,7 +582,8 @@ def status(watson, project, tags, elapsed): @catch_watson_error def report(watson, current, from_, to, projects, tags, ignore_projects, ignore_tags, year, month, week, day, luna, all, output_format, - pager, aggregated=False, include_partial_frames=True, show_notes=False): + pager, aggregated=False, include_partial_frames=True, + show_notes=False): """ Display a report of the time spent on each project. @@ -1057,7 +1061,8 @@ def aggregate(ctx, watson, current, from_, to, projects, tags, output_format, @click.pass_obj @catch_watson_error def log(watson, current, reverse, from_, to, projects, tags, ignore_projects, - ignore_tags, year, month, week, day, luna, all, output_format, pager, show_notes): + ignore_tags, year, month, week, day, luna, all, output_format, pager, + show_notes): """ Display each recorded session during the given timespan. @@ -1214,7 +1219,8 @@ def get_note_string(frame): return '' _print("\n".join( - "\t{id} {start} to {stop} {delta:>11} {project}{tags}{notes}".format( + ("\t{id} {start} to {stop} {delta:>11} " + "{project}{tags}{notes}").format( delta=format_timedelta(frame.stop - frame.start), project=style('project', '{:>{}}'.format( frame.project, longest_project @@ -1345,7 +1351,8 @@ def add(watson, args, from_, to, confirm_new_project, confirm_new_tag, note): confirm_tags(tags, watson.tags) # add a new frame, call watson save to update state files - frame = watson.add(project=project, tags=tags, from_date=from_, to_date=to, note=note) + frame = watson.add(project=project, tags=tags, from_date=from_, to_date=to, + note=note) click.echo( "Adding project {}{}, started {} and stopped {}. (id: {})".format( style('project', frame.project), @@ -1394,7 +1401,8 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): id = frame.id elif watson.is_started: frame = Frame(watson.current['start'], None, watson.current['project'], - None, watson.current['tags'], None, watson.current['note']) + None, watson.current['tags'], None, + watson.current['note']) elif watson.frames: frame = watson.frames[-1] id = frame.id @@ -1407,7 +1415,7 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): 'start': frame.start.format(datetime_format), 'project': frame.project, 'tags': frame.tags, - 'note' : "" if frame.note is None else frame.note, + 'note': "" if frame.note is None else frame.note, } if id: @@ -1484,7 +1492,8 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): watson.frames[id] = (project, start, stop, tags, id, updated_at, note) else: - watson.current = dict(start=start, project=project, tags=tags, note=note) + watson.current = dict(start=start, project=project, tags=tags, + note=note) watson.save() click.echo( @@ -1508,7 +1517,6 @@ def edit(watson, confirm_new_project, confirm_new_tag, id): click.echo("Note: {}".format(style('note', note))) - @cli.command(context_settings={'ignore_unknown_options': True}) @click.argument('id', shell_complete=get_frames) @click.option('-f', '--force', is_flag=True, diff --git a/watson/frames.py b/watson/frames.py index 3133aae..c9b3ea1 100644 --- a/watson/frames.py +++ b/watson/frames.py @@ -8,7 +8,8 @@ class Frame(namedtuple('Frame', HEADERS)): - def __new__(cls, start, stop, project, id, tags=None, updated_at=None,note=None): + def __new__(cls, start, stop, project, id, tags=None, updated_at=None, + note=None): try: if not isinstance(start, arrow.Arrow): start = arrow.get(start) @@ -41,7 +42,8 @@ def dump(self): stop = self.stop.to('utc').int_timestamp if self.stop else None updated_at = self.updated_at.int_timestamp - return (start, stop, self.project, self.id, self.tags, updated_at, self.note) + return (start, stop, self.project, self.id, self.tags, updated_at, + self.note) @property def day(self): diff --git a/watson/utils.py b/watson/utils.py index e6d2935..9fc4c8f 100644 --- a/watson/utils.py +++ b/watson/utils.py @@ -404,9 +404,9 @@ def json_arrow_encoder(obj): raise TypeError("Object {} is not JSON serializable".format(obj)) + def format_note(note): return u"{}{}".format( style('note', '>> '), style('note', note.replace('\n', '\n' + ' '*20)) ) - diff --git a/watson/watson.py b/watson/watson.py index 5a61473..b61ce5d 100644 --- a/watson/watson.py +++ b/watson/watson.py @@ -246,7 +246,8 @@ def add(self, project, from_date, to_date, tags, note=None): default_tags = self.config.getlist('default_tags', project) tags = (tags or []) + default_tags - frame = self.frames.add(project, from_date, to_date, tags=tags, note=note) + frame = self.frames.add(project, from_date, to_date, tags=tags, + note=note) return frame def start(self, project, tags=None, restart=False, start_at=None, @@ -274,7 +275,8 @@ def start(self, project, tags=None, restart=False, start_at=None, if start_at > arrow.now(): raise WatsonError('Task cannot start in the future.') - new_frame = {'project': project, 'tags': deduplicate(tags), 'note': note} + new_frame = {'project': project, 'tags': deduplicate(tags), + 'note': note} new_frame['start'] = start_at if not gap: stop_of_prev_frame = self.frames[-1].stop @@ -300,7 +302,8 @@ def stop(self, stop_at=None, note=None, concat_note=False): if stop_at > arrow.now(): raise WatsonError('Task cannot end in the future.') - concat_note = concat_note or self.config.getboolean('options', 'notes_concatenate') + concat_note = concat_note or self.config.getboolean( + 'options', 'notes_concatenate') if note is None: note = old.get('note') @@ -632,7 +635,8 @@ def report(self, from_, to, current=None, projects=None, tags=None, datetime.timedelta() ) - tag_notes = [frame.note for frame in frames if tag in frame.tags and frame.note] + tag_notes = [frame.note for frame in frames if + tag in frame.tags and frame.note] project_report['tags'].append({ 'name': tag,