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:
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import ClassVar
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from redmine_reporter.config import AppConfig, Config, EmailConfig
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
# -- Просроченность --
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user