feat: filter Wishlist from --my-tasks; document the mode (#71)

Tasks in the "Wishlist" bucket version are now excluded from the list
and summary entirely (no due date, noise backlog); exact case match,
tasks without a version are unaffected. Document --my-tasks in README
and the user guide (sorting, summary metrics, OSC 8 links, stream
separation); all examples use synthetic data. Declare ruff
known-first-party in pyproject so the lint result is CWD-independent.
This commit is contained in:
Кокос Артем Николаевич
2026-10-28 15:53:50 +07:00
parent 5d1cf4ab74
commit f00c6705ba
13 changed files with 224 additions and 1 deletions

View File

@@ -3,6 +3,7 @@ from datetime import date
from unittest import mock
import pytest
from redmine_reporter.cli import main, parse_date_range
from redmine_reporter.client import RedmineAPIError
@@ -2115,6 +2116,45 @@ class TestMyTasksFlag:
assert "потрачено:" in captured.err
assert "Открытых:" not in captured.out
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
@mock.patch("redmine_reporter.cli.fetch_open_issues")
def test_my_tasks_wishlist_filtered_out_of_table_and_summary(
self, mock_fetch, capsys
):
"""Задача из Wishlist не попадает ни в таблицу, ни в саммари."""
wishlist = _make_task_issue(50, subject="Идея на потом")
wishlist.fixed_version = mock.MagicMock()
wishlist.fixed_version.name = "Wishlist"
normal = _make_task_issue(51, subject="Рабочая задача")
mock_fetch.return_value = [(wishlist, None), (normal, None)]
code = main(["--my-tasks"])
captured = capsys.readouterr()
assert code == 0
assert _expected_link(51) in captured.out
assert "Рабочая задача" in captured.out
assert _expected_link(50) not in captured.out
assert "Идея на потом" not in captured.out
assert "Открытых: 1" in captured.err
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
@mock.patch("redmine_reporter.cli.fetch_open_issues")
def test_my_tasks_only_wishlist_reports_empty_and_exits_zero(
self, mock_fetch, capsys
):
"""Все задачи в Wishlist -- выборка пуста: 'Открытых задач нет', код 0."""
wishlist = _make_task_issue(60, subject="Ещё идея")
wishlist.fixed_version = mock.MagicMock()
wishlist.fixed_version.name = "Wishlist"
mock_fetch.return_value = [(wishlist, None)]
code = main(["--my-tasks"])
captured = capsys.readouterr()
assert code == 0
assert "Открытых задач нет" in captured.err
assert captured.out == ""
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
@mock.patch("redmine_reporter.cli.fetch_open_issues")
def test_my_tasks_api_error_exits_one(self, mock_fetch, capsys):

View File

@@ -3,6 +3,7 @@ from datetime import date
from unittest import mock
import pytest
from redmine_reporter.client import fetch_issues_with_spent_time, fetch_open_issues
from redmine_reporter.config import DEFAULT_REDMINE_VERIFY

View File

@@ -6,6 +6,7 @@ from typing import ClassVar
from unittest import mock
import pytest
from redmine_reporter.config import AppConfig, Config, EmailConfig

View File

@@ -4,6 +4,7 @@ from unittest import mock
import pytest
from odf.opendocument import OpenDocument, OpenDocumentText
from redmine_reporter.formatters.console import CompactFormatter, TableFormatter
from redmine_reporter.formatters.csv import CSVFormatter
from redmine_reporter.formatters.factory import get_formatter_by_extension

View File

@@ -2,6 +2,7 @@ import smtplib
from unittest import mock
import pytest
from redmine_reporter.client import RedmineAPIError
from redmine_reporter.config import EmailConfig, SmtpConfig
from redmine_reporter.mailer import build_message, send_report

View File

@@ -4,9 +4,11 @@ from datetime import date
from unittest import mock
from redmine_reporter.task_list import (
EXCLUDED_VERSION_NAMES,
PRIORITY_RANK,
build_task_rows,
calculate_task_summary,
filter_excluded_versions,
format_summary_line,
priority_rank,
)
@@ -100,6 +102,72 @@ def test_rows_without_due_go_last_and_tiebreak_by_id():
assert [r["issue_id"] for r in rows] == [30, 12, 25]
# -- Фильтр версий-исключений (Wishlist) --
def test_excluded_version_names_constant():
"""Точный состав бакет-версий, исключаемых из --my-tasks."""
assert EXCLUDED_VERSION_NAMES == frozenset({"Wishlist"})
def test_filter_excludes_wishlist_issue():
"""Задача в версии Wishlist удаляется из выборки."""
items = [
(make_issue(1, fixed_version="Wishlist"), None),
(make_issue(2, fixed_version="v2.5.0"), date(2026, 11, 1)),
]
kept = filter_excluded_versions(items)
assert [issue.id for issue, _ in kept] == [2]
def test_filter_keeps_issue_without_version():
"""Задача без версии фильтром не трогается."""
items = [(make_issue(3), None)]
kept = filter_excluded_versions(items)
assert [issue.id for issue, _ in kept] == [3]
def test_filter_keeps_regular_version_without_due():
"""Обычная версия без срока -- не бакет, задача остаётся."""
items = [(make_issue(4, fixed_version="v3.0.0"), None)]
kept = filter_excluded_versions(items)
assert [issue.id for issue, _ in kept] == [4]
def test_filter_is_case_sensitive():
"""Сравнение имени версии точное: wishlist строчными -- НЕ исключение."""
items = [
(make_issue(5, fixed_version="wishlist"), None),
(make_issue(6, fixed_version="WISHLIST"), None),
(make_issue(7, fixed_version="Wishlist"), None),
]
kept = filter_excluded_versions(items)
assert [issue.id for issue, _ in kept] == [5, 6]
def test_filter_returns_only_excluded_when_all_match():
items = [(make_issue(8, fixed_version="Wishlist"), None)]
assert filter_excluded_versions(items) == []
def test_filter_preserves_due_pairing():
"""Фильтр не теряет парную к задаче дату срока."""
due = date(2026, 12, 1)
items = [(make_issue(9, fixed_version="v1"), due)]
((issue, kept_due),) = filter_excluded_versions(items)
assert issue.id == 9
assert kept_due is due
def test_filter_tolerates_version_without_name_attribute():
"""fixed_version без атрибута name -- задача остаётся (getattr-безопасность)."""
issue = make_issue(10)
issue.fixed_version = mock.MagicMock(spec=[])
items = [(issue, None)]
kept = filter_excluded_versions(items)
assert [i.id for i, _ in kept] == [10]
# -- Просроченность --

View File

@@ -4,6 +4,7 @@ import re
from typing import cast
import pytest
from redmine_reporter.formatters.task_table import TaskTableFormatter
from redmine_reporter.types import TaskRow

View File

@@ -5,6 +5,7 @@ from pathlib import Path
from unittest import mock
import yaml
from redmine_reporter.yaml_config import (
check_file_permissions,
ensure_config_dir,