feat(stories): add stories subcommand
- Add stories subcommand listing stories worked on, grouped by project - Preserve story_raw in parser row dicts alongside the stripped story, so markdown links are available for display - print_stories() filters to rows with a non-empty story field, deduplicates by stripped story text (preferring the linked version), sums hours per story, and outputs an indented Markdown list - Project names resolved through project_map (same as csv/summary) - -w/--weekly flag aggregates stories across the full week - Add tests for print_stories covering deduplication, link preservation, grouping, empty rows, and story-less row exclusion - Fix flex daily target in status: use projected hours per prior day rather than fixed 8h when computing remaining hours for today
This commit is contained in:
parent
f372a691d4
commit
2d60624e0e
5 changed files with 206 additions and 17 deletions
|
|
@ -3,7 +3,7 @@ import io
|
|||
|
||||
import pytest
|
||||
|
||||
from timesheets.output import print_summary, write_csv
|
||||
from timesheets.output import print_stories, print_summary, write_csv
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared fixtures
|
||||
|
|
@ -111,3 +111,82 @@ class TestPrintSummary:
|
|||
assert "01:00" in out
|
||||
assert "00:30" in out
|
||||
assert "00:15" in out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# print_stories
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _story_row(project, story, story_raw, hours):
|
||||
return {
|
||||
"project": project,
|
||||
"story": story,
|
||||
"story_raw": story_raw,
|
||||
"note": "",
|
||||
"duration_hours": hours,
|
||||
}
|
||||
|
||||
|
||||
class TestPrintStories:
|
||||
def test_basic_output(self, capsys):
|
||||
rows = [_story_row("bugs", "ticket 1", "ticket 1", 1.0)]
|
||||
print_stories(rows)
|
||||
out = capsys.readouterr().out
|
||||
assert "- bugs" in out
|
||||
assert " - ticket 1" in out
|
||||
assert "01:00" in out
|
||||
|
||||
def test_rows_without_story_excluded(self, capsys):
|
||||
rows = [
|
||||
_story_row("bugs", "ticket 1", "ticket 1", 1.0),
|
||||
{
|
||||
"project": "scrum",
|
||||
"story": "",
|
||||
"story_raw": "",
|
||||
"note": "dsu",
|
||||
"duration_hours": 0.25,
|
||||
},
|
||||
]
|
||||
print_stories(rows)
|
||||
out = capsys.readouterr().out
|
||||
assert "scrum" not in out
|
||||
assert "dsu" not in out
|
||||
|
||||
def test_deduplication_sums_hours(self, capsys):
|
||||
rows = [
|
||||
_story_row("bugs", "ticket 1", "ticket 1", 0.5),
|
||||
_story_row("bugs", "ticket 1", "ticket 1", 0.5),
|
||||
]
|
||||
print_stories(rows)
|
||||
out = capsys.readouterr().out
|
||||
assert out.count("ticket 1") == 1
|
||||
assert "01:00" in out
|
||||
|
||||
def test_markdown_link_preserved(self, capsys):
|
||||
rows = [
|
||||
_story_row("bugs", "ticket 1", "ticket 1", 0.5),
|
||||
_story_row("bugs", "ticket 1", "[ticket 1](:/abc123)", 0.5),
|
||||
]
|
||||
print_stories(rows)
|
||||
out = capsys.readouterr().out
|
||||
assert "[ticket 1](:/abc123)" in out
|
||||
|
||||
def test_grouped_by_project(self, capsys):
|
||||
rows = [
|
||||
_story_row("bugs", "ticket 1", "ticket 1", 1.0),
|
||||
_story_row("rate", "story A", "story A", 2.0),
|
||||
]
|
||||
print_stories(rows)
|
||||
out = capsys.readouterr().out
|
||||
assert "- bugs" in out
|
||||
assert "- rate" in out
|
||||
bugs_line = next(l for l in out.splitlines() if "bugs" in l)
|
||||
rate_line = next(l for l in out.splitlines() if "rate" in l)
|
||||
assert not bugs_line.startswith(" ") # top-level
|
||||
assert not rate_line.startswith(" ")
|
||||
|
||||
def test_empty_rows(self, capsys):
|
||||
print_stories([])
|
||||
out = capsys.readouterr().out
|
||||
assert out == ""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue