Add ~ marker to exclude entries from CSV export
Prefix the project name or note column with ~ to mark an entry as count-but-don't-export. Marked entries are included in summary and status totals but omitted from all csv output (both --raw and aggregated, single-day and weekly). | 09:00 | 17:00 | 8:00 | ~Leave | | Day off | | 09:00 | 17:00 | 8:00 | Leave | | ~Day off | The ~ is stripped from whichever field carries it before any downstream processing, so project map resolution is unaffected. Implementation: - parse_table sets skip_csv=True on marked rows and strips the ~ - new filter_skip_csv() helper in parser.py - to_csv_entries() skips skip_csv rows - _cmd_csv calls filter_skip_csv() before aggregate_rows()
This commit is contained in:
parent
8b6f0b24e2
commit
de46399010
6 changed files with 209 additions and 25 deletions
|
|
@ -8,6 +8,7 @@ from timesheets.parser import (
|
|||
detect_has_duration_column,
|
||||
extract_table_blocks,
|
||||
filter_rows_by_date,
|
||||
filter_skip_csv,
|
||||
parse_document,
|
||||
parse_table,
|
||||
resolve_overlaps,
|
||||
|
|
@ -351,6 +352,127 @@ class TestBuildDescription:
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# skip_csv marker (~project)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestParseTableSkipCsv:
|
||||
_LINES = [
|
||||
"| Start | End | Duration | Project | Story | Note |",
|
||||
"|-------|-------|----------|---------|-------|----------|",
|
||||
"| 09:00 | 17:00 | 8:00 | ~Leave | | Day off |",
|
||||
"| 09:00 | 10:00 | 1:00 | work | st | |",
|
||||
]
|
||||
|
||||
def test_tilde_sets_skip_csv(self):
|
||||
rows = parse_table(self._LINES)
|
||||
leave = next(r for r in rows if "Leave" in r["project"])
|
||||
assert leave["skip_csv"] is True
|
||||
|
||||
def test_tilde_stripped_from_project(self):
|
||||
rows = parse_table(self._LINES)
|
||||
leave = next(r for r in rows if r.get("skip_csv"))
|
||||
assert leave["project"] == "Leave"
|
||||
|
||||
def test_tilde_with_space_stripped(self):
|
||||
lines = [
|
||||
"| Start | End | Duration | Project | Story | Note |",
|
||||
"|-------|-------|----------|----------|-------|------|",
|
||||
"| 09:00 | 17:00 | 8:00 | ~ Leave | | |",
|
||||
]
|
||||
rows = parse_table(lines)
|
||||
assert rows[0]["skip_csv"] is True
|
||||
assert rows[0]["project"] == "Leave"
|
||||
|
||||
def test_normal_row_has_no_skip_csv(self):
|
||||
rows = parse_table(WITH_DURATION)
|
||||
assert all("skip_csv" not in r for r in rows)
|
||||
|
||||
def test_skip_csv_on_open_entry(self):
|
||||
lines = [
|
||||
"| Start | End | Project | Story | Note |",
|
||||
"|-------|-----|---------|-------|------|",
|
||||
"| 09:00 | | ~Leave | | |",
|
||||
]
|
||||
rows = parse_table(lines, has_duration_col=False)
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["skip_csv"] is True
|
||||
assert rows[0]["project"] == "Leave"
|
||||
|
||||
def test_note_tilde_sets_skip_csv(self):
|
||||
lines = [
|
||||
"| Start | End | Duration | Project | Story | Note |",
|
||||
"|-------|-------|----------|---------|-------|----------|",
|
||||
"| 09:00 | 17:00 | 8:00 | Leave | | ~Day off |",
|
||||
]
|
||||
rows = parse_table(lines)
|
||||
assert rows[0]["skip_csv"] is True
|
||||
|
||||
def test_note_tilde_stripped_from_note(self):
|
||||
lines = [
|
||||
"| Start | End | Duration | Project | Story | Note |",
|
||||
"|-------|-------|----------|---------|-------|----------|",
|
||||
"| 09:00 | 17:00 | 8:00 | Leave | | ~Day off |",
|
||||
]
|
||||
rows = parse_table(lines)
|
||||
assert rows[0]["note"] == "Day off"
|
||||
|
||||
def test_note_tilde_with_space_stripped(self):
|
||||
lines = [
|
||||
"| Start | End | Duration | Project | Story | Note |",
|
||||
"|-------|-------|----------|---------|-------|-------|",
|
||||
"| 09:00 | 17:00 | 8:00 | Leave | | ~ Day off |",
|
||||
]
|
||||
rows = parse_table(lines)
|
||||
assert rows[0]["skip_csv"] is True
|
||||
assert rows[0]["note"] == "Day off"
|
||||
|
||||
def test_note_tilde_without_duration_col(self):
|
||||
lines = [
|
||||
"| Start | End | Project | Story | Note |",
|
||||
"|-------|-------|---------|-------|----------|",
|
||||
"| 09:00 | 17:00 | Leave | | ~Day off |",
|
||||
]
|
||||
rows = parse_table(lines, has_duration_col=False)
|
||||
assert rows[0]["skip_csv"] is True
|
||||
assert rows[0]["note"] == "Day off"
|
||||
|
||||
def test_note_tilde_does_not_affect_normal_row(self):
|
||||
rows = parse_table(WITH_DURATION)
|
||||
assert all("skip_csv" not in r for r in rows)
|
||||
|
||||
|
||||
class TestFilterSkipCsv:
|
||||
def test_removes_skip_csv_rows(self):
|
||||
rows = [
|
||||
{"project": "Leave", "skip_csv": True, "duration_hours": 8.0},
|
||||
{"project": "work", "duration_hours": 1.0},
|
||||
]
|
||||
result = filter_skip_csv(rows)
|
||||
assert len(result) == 1
|
||||
assert result[0]["project"] == "work"
|
||||
|
||||
def test_keeps_all_normal_rows(self):
|
||||
rows = [
|
||||
{"project": "work", "duration_hours": 1.0},
|
||||
{"project": "scrum", "duration_hours": 0.5},
|
||||
]
|
||||
assert filter_skip_csv(rows) == rows
|
||||
|
||||
def test_empty_input(self):
|
||||
assert filter_skip_csv([]) == []
|
||||
|
||||
def test_all_skip_csv_returns_empty(self):
|
||||
rows = [{"project": "Leave", "skip_csv": True, "duration_hours": 8.0}]
|
||||
assert filter_skip_csv(rows) == []
|
||||
|
||||
def test_rows_without_key_treated_as_normal(self):
|
||||
"""Rows that never had the key at all should pass through."""
|
||||
rows = [{"project": "work", "duration_hours": 1.0}]
|
||||
assert filter_skip_csv(rows) == rows
|
||||
|
||||
|
||||
class TestAggregateRows:
|
||||
def test_same_project_story_summed(self):
|
||||
rows = parse_table(WITH_DURATION)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue