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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ site/
.vagrant/
.venv/
.pytest_cache/
data/

# files

Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ 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, fixed some bugs, added some features

## [3.1.1] - 2024-10-07

- ???

## [2.1.0] - 2022-05-16

### Added
Expand Down
10 changes: 5 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
PYTHON ?= python
PIP ?= pip

VENV = virtualenv
VENV_ARGS = -p $(PYTHON)
VENV = $(PYTHON) -m venv
VENV_ARGS =
VENV_DIR = $(CURDIR)/.venv
VENV_WATSON_DIR = $(CURDIR)/data

Expand All @@ -22,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:
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

10 changes: 10 additions & 0 deletions docs/user-guide/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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`
Expand Down
9 changes: 9 additions & 0 deletions docs/user-guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down
111 changes: 108 additions & 3 deletions tests/test_watson.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,54 @@ def test_frames_with_empty_given_state(config_dir, mocker):
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

def test_empty_config_dir():
Expand Down Expand Up @@ -356,6 +404,32 @@ def test_stop_started_project_at(watson):
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

def test_cancel_started_project(watson):
Expand Down Expand Up @@ -419,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': []}
assert result == {'project': 'foo', 'start': 4000, 'tags': [],
'note': None}

watson.current = {}
watson.save()
Expand Down Expand Up @@ -779,9 +854,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
Expand All @@ -790,6 +868,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']
)
Expand All @@ -799,16 +884,36 @@ 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
Expand Down
Loading