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:
@@ -22,6 +22,7 @@ CLI-инструмент для генерации отчётов по зада
|
|||||||
- Гибкий выбор периода: `--date`, env `FROM_DATE`, YAML `period.from`, по умолчанию — текущий месяц.
|
- Гибкий выбор периода: `--date`, env `FROM_DATE`, YAML `period.from`, по умолчанию — текущий месяц.
|
||||||
- `--commit`: сохранение отчёта в файл и авто-сдвиг начала периода в конфиге для следующего запуска.
|
- `--commit`: сохранение отчёта в файл и авто-сдвиг начала периода в конфиге для следующего запуска.
|
||||||
- `--send`: отправка отчёта по email через SMTP (plain-text или HTML-письмо) с авто-сдвигом начала периода при успешной отправке.
|
- `--send`: отправка отчёта по email через SMTP (plain-text или HTML-письмо) с авто-сдвигом начала периода при успешной отправке.
|
||||||
|
- `--my-tasks`: список назначенных вам открытых задач -- таблица с сортировкой по срочности (приоритет, срок версии) и сводкой; см. [руководство](docs/USER_GUIDE.md#список-открытых-задач).
|
||||||
- YAML-конфиг с секретами через `${VAR}`; приоритет: CLI-флаги > env > .env > YAML > дефолты (нюанс с `--config` — см. docs/CONFIG.md).
|
- YAML-конфиг с секретами через `${VAR}`; приоритет: CLI-флаги > env > .env > YAML > дефолты (нюанс с `--config` — см. docs/CONFIG.md).
|
||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
@@ -57,6 +58,9 @@ redmine-reporter
|
|||||||
|
|
||||||
# Сохранить в файл и сдвинуть начало периода для следующего запуска
|
# Сохранить в файл и сдвинуть начало периода для следующего запуска
|
||||||
redmine-reporter --commit
|
redmine-reporter --commit
|
||||||
|
|
||||||
|
# Список ваших открытых задач (сортировка по срочности)
|
||||||
|
redmine-reporter --my-tasks
|
||||||
```
|
```
|
||||||
|
|
||||||
## Документация
|
## Документация
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
- [Первоначальная настройка](#первоначальная-настройка)
|
- [Первоначальная настройка](#первоначальная-настройка)
|
||||||
- [Выбор периода](#выбор-периода)
|
- [Выбор периода](#выбор-периода)
|
||||||
- [Отчёт за другого пользователя](#отчёт-за-другого-пользователя)
|
- [Отчёт за другого пользователя](#отчёт-за-другого-пользователя)
|
||||||
|
- [Список открытых задач](#список-открытых-задач)
|
||||||
- [Экспорт в файл](#экспорт-в-файл)
|
- [Экспорт в файл](#экспорт-в-файл)
|
||||||
- [Содержимое отчёта](#содержимое-отчёта)
|
- [Содержимое отчёта](#содержимое-отчёта)
|
||||||
- [Отправка по email](#отправка-по-email)
|
- [Отправка по email](#отправка-по-email)
|
||||||
@@ -115,6 +116,70 @@ redmine-reporter --user-name "Иванов Иван Иванович"
|
|||||||
- Несколько совпадений по имени → ошибка «Multiple users match...» со списком логинов (при дублях логина — ID).
|
- Несколько совпадений по имени → ошибка «Multiple users match...» со списком логинов (при дублях логина — ID).
|
||||||
- Ноль совпадений → ошибка «User '...' not found...».
|
- Ноль совпадений → ошибка «User '...' not found...».
|
||||||
|
|
||||||
|
## Список открытых задач
|
||||||
|
|
||||||
|
`--my-tasks` -- отдельный режим: вместо отчёта по затраченному времени выводит
|
||||||
|
открытые задачи, назначенные на вас, по всем проектам. Отчётное окно периода
|
||||||
|
к режиму не применяется.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
redmine-reporter --my-tasks # задачи владельца API-ключа
|
||||||
|
redmine-reporter --my-tasks --user-id 42 # чужой список по ID
|
||||||
|
redmine-reporter --my-tasks --user-login ivanov # по логину
|
||||||
|
```
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
|
||||||
|
- `--user-id` / `--user-login` / `--user-name` выбирают, чей список показать; по умолчанию -- владелец API-ключа. Разрешение пользователей то же, что для отчёта (см. [Отчёт за другого пользователя](#отчёт-за-другого-пользователя)).
|
||||||
|
- `--url`, `--api-key`, `--config`, `--config-path`, `--verbose`, `--debug` работают как обычно.
|
||||||
|
- Совмещение с отчётными флагами (`--date`, `--output`, `--compact`, `--summary`, `--no-time`, `--by-activity`, `--commit`, `--send`) -- ошибка «--my-tasks cannot be used with report-generating flags.», exit 1.
|
||||||
|
- Задачи в версии `Wishlist` исключаются из режима целиком: не попадают ни в таблицу, ни в сводку.
|
||||||
|
|
||||||
|
Сортировка -- от срочного к менее срочному:
|
||||||
|
|
||||||
|
1. Приоритет `P1..P5`: меньший номер срочнее; неизвестное имя приоритета получает низший ранг.
|
||||||
|
2. Внутри приоритетной группы -- срок версии (due date) по возрастанию.
|
||||||
|
3. Задачи без версии или без срока -- в конец своей приоритетной группы.
|
||||||
|
4. При прочих равных -- по возрастанию ID.
|
||||||
|
|
||||||
|
Таблица печатается в stdout, сводка -- в stderr перед таблицей. При
|
||||||
|
перенаправлении (`redmine-reporter --my-tasks > tasks.txt`) в файл попадает
|
||||||
|
только таблица.
|
||||||
|
|
||||||
|
Колонки таблицы: ID | Тема | Статус | Приор. | Версия | Срок. Пример
|
||||||
|
(синтетические данные, ID в терминале кликабелен):
|
||||||
|
|
||||||
|
```
|
||||||
|
ID Тема Статус Приор. Версия Срок
|
||||||
|
---- -------------------------------- -------------- -------- -------- --------------
|
||||||
|
101 Критический дефект сборки образа В работе P1 v2.4.0 2026-10-15 (!)
|
||||||
|
102 Обновить документацию API Новая P2 v2.5.0 2026-12-01
|
||||||
|
103 Ревью pull request Обратная связь P3 - -
|
||||||
|
```
|
||||||
|
|
||||||
|
- ID -- гиперссылка (OSC 8) на страницу задачи `<url>/issues/<id>`: в терминалах с поддержкой кликабельна, без поддержки остаётся обычным номером.
|
||||||
|
- Срок -- дата релиза целевой версии; просроченный (раньше сегодняшней даты) помечается `(!)`.
|
||||||
|
- Пустые «Версия» и «Срок» -- прочерк `-`.
|
||||||
|
- Статусы переводятся на русский тем же словарём, что и в отчётах: встроенный словарь + `report.status_translation` (см. [Перевод статусов](#перевод-статусов)).
|
||||||
|
|
||||||
|
Сводка (пример с синтетическими данными):
|
||||||
|
|
||||||
|
```
|
||||||
|
Открытых: 3 | просроченных: 1 | без версии: 1 | без срока: 0 | потрачено: 12.5h | оценка: 8h (по 2 из 3)
|
||||||
|
```
|
||||||
|
|
||||||
|
- «просроченных» -- задачи со сроком версии раньше сегодняшней даты.
|
||||||
|
- «без версии» -- задачи без целевой версии; «без срока» -- задачи с версией, но без известной даты релиза.
|
||||||
|
- «потрачено» -- сумма полей `spent_hours` задач списка.
|
||||||
|
- «оценка» -- сумма полей `estimated_hours`; «(по N из M)» -- у скольких задач из списка есть оценка. Если оценок нет ни у одной задачи, блок «оценка» не выводится.
|
||||||
|
|
||||||
|
Сроки версий дозагружаются отдельным запросом на каждую уникальную версию
|
||||||
|
списка. Недоступная версия -- предупреждение в stderr и пустой срок у её
|
||||||
|
задач, вывод не прерывается.
|
||||||
|
|
||||||
|
Пустой список (в том числе когда все открытые задачи попали в исключаемую
|
||||||
|
версию `Wishlist`): «ℹ️ Открытых задач нет.» (stderr), exit 0.
|
||||||
|
|
||||||
## Экспорт в файл
|
## Экспорт в файл
|
||||||
|
|
||||||
Флаг `--output` определяется по четырём правилам:
|
Флаг `--output` определяется по четырём правилам:
|
||||||
@@ -284,6 +349,7 @@ redmine-reporter --send --commit
|
|||||||
| `--config-path` | Путь к YAML-конфигу: загрузка, запись `--init-config` и `--commit`. По умолчанию `~/.config/redmine-reporter/config.yml`. |
|
| `--config-path` | Путь к YAML-конфигу: загрузка, запись `--init-config` и `--commit`. По умолчанию `~/.config/redmine-reporter/config.yml`. |
|
||||||
| `--commit` | Сохранить отчёт в файл и сдвинуть начало периода (`period.from` = конец окна) в YAML. |
|
| `--commit` | Сохранить отчёт в файл и сдвинуть начало периода (`period.from` = конец окна) в YAML. |
|
||||||
| `--send` | Отправить отчёт по email после сохранения файла; при подтверждении и успехе сдвинуть начало периода. |
|
| `--send` | Отправить отчёт по email после сохранения файла; при подтверждении и успехе сдвинуть начало периода. |
|
||||||
|
| `--my-tasks` | Вместо отчёта вывести открытые задачи, назначенные на вас (или на пользователя из `--user-id`/`--user-login`/`--user-name`): таблица с сортировкой по срочности + сводка в stderr. Несовместим с `--date`, `--output`, `--compact`, `--summary`, `--no-time`, `--by-activity`, `--commit`, `--send`. Подробнее: [Список открытых задач](#список-открытых-задач). |
|
||||||
|
|
||||||
## Устранение неполадок
|
## Устранение неполадок
|
||||||
|
|
||||||
@@ -294,6 +360,7 @@ redmine-reporter --send --commit
|
|||||||
| «User '...' not found...» | Пользователь не найден ни по ID, ни по логину, ни по имени | Проверьте написание; логин сравнивается точно с учётом регистра. |
|
| «User '...' not found...» | Пользователь не найден ни по ID, ни по логину, ни по имени | Проверьте написание; логин сравнивается точно с учётом регистра. |
|
||||||
| «Multiple users match...» | По имени найдено несколько пользователей (или несколько дублей логина) | Используйте `--user-login` с логином из списка или уточните числовой ID для `--user-id`. |
|
| «Multiple users match...» | По имени найдено несколько пользователей (или несколько дублей логина) | Используйте `--user-login` с логином из списка или уточните числовой ID для `--user-id`. |
|
||||||
| Указано несколько `--user-*` | Флаги взаимоисключающие | Оставьте только один из `--user-id` / `--user-login` / `--user-name`. |
|
| Указано несколько `--user-*` | Флаги взаимоисключающие | Оставьте только один из `--user-id` / `--user-login` / `--user-name`. |
|
||||||
|
| «--my-tasks cannot be used with report-generating flags.» (exit 1) | `--my-tasks` совмещён с отчётными флагами (`--date`, `--output`, `--compact`, `--summary`, `--no-time`, `--by-activity`, `--commit`, `--send`) | Запускайте режим отдельно: список задач и отчёт -- разные команды (см. [Список открытых задач](#список-открытых-задач)). |
|
||||||
| «Неизвестный формат файла: '.xyz'. Поддерживаются: .odt, .csv, .md, .html, .json, .xlsx» (exit 1) | `--output` с неизвестным расширением | Используйте одно из поддерживаемых расширений. |
|
| «Неизвестный формат файла: '.xyz'. Поддерживаются: .odt, .csv, .md, .html, .json, .xlsx» (exit 1) | `--output` с неизвестным расширением | Используйте одно из поддерживаемых расширений. |
|
||||||
| Ошибка про `odfpy` при `.odt` | Пакет `odfpy` не установлен | `pip install odfpy` или выберите другой формат. |
|
| Ошибка про `odfpy` при `.odt` | Пакет `odfpy` не установлен | `pip install odfpy` или выберите другой формат. |
|
||||||
| «Email не настроен. Добавьте секцию 'email' в конфиг...» (exit 1) | `--send` без секции `email` в YAML или с пустым `smtp.host` | Добавьте секцию `email` (см. [CONFIG.md](CONFIG.md)). |
|
| «Email не настроен. Добавьте секцию 'email' в конфиг...» (exit 1) | `--send` без секции `email` в YAML или с пустым `smtp.host` | Добавьте секцию `email` (см. [CONFIG.md](CONFIG.md)). |
|
||||||
@@ -303,6 +370,7 @@ redmine-reporter --send --commit
|
|||||||
| «Ошибка отправки письма: ...» | Прочая ошибка SMTP | Смотрите детали после двоеточия; файл отчёта уже сохранён на диске. |
|
| «Ошибка отправки письма: ...» | Прочая ошибка SMTP | Смотрите детали после двоеточия; файл отчёта уже сохранён на диске. |
|
||||||
| Предупреждение о правах конфига при старте | Права `config.yml` шире `0600` | `chmod 600 ~/.config/redmine-reporter/config.yml`. |
|
| Предупреждение о правах конфига при старте | Права `config.yml` шире `0600` | `chmod 600 ~/.config/redmine-reporter/config.yml`. |
|
||||||
| Предупреждение о MITM при старте | `verify_ssl: false` / `REDMINE_VERIFY=false` — проверка TLS отключена | Включите проверку или используйте только в доверенной сети. |
|
| Предупреждение о MITM при старте | `verify_ssl: false` / `REDMINE_VERIFY=false` — проверка TLS отключена | Включите проверку или используйте только в доверенной сети. |
|
||||||
|
| «⚠️ Could not load version ...: Due date will be empty for its issues.» в режиме `--my-tasks` | Версия из списка задач недоступна (нет прав, удалена, сбой сети) | Некритично: срок этой версии останется пустым; проверьте права доступа к проекту версии. |
|
||||||
| Не виден traceback при ошибке | Traceback показывается только с `--verbose`/`--debug` | Повторите запуск с `--debug`. |
|
| Не виден traceback при ошибке | Traceback показывается только с `--verbose`/`--debug` | Повторите запуск с `--debug`. |
|
||||||
|
|
||||||
API-ключ в текстах ошибок маскируется (`***`).
|
API-ключ в текстах ошибок маскируется (`***`).
|
||||||
|
|||||||
@@ -62,3 +62,6 @@ ignore_missing_imports = true
|
|||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
profile = "black"
|
profile = "black"
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
known-first-party = ["redmine_reporter"]
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ from .formatters.factory import get_console_formatter, get_formatter_by_extensio
|
|||||||
from .formatters.task_table import TaskTableFormatter
|
from .formatters.task_table import TaskTableFormatter
|
||||||
from .mailer import send_report
|
from .mailer import send_report
|
||||||
from .report_builder import build_grouped_report, calculate_summary
|
from .report_builder import build_grouped_report, calculate_summary
|
||||||
from .task_list import build_task_rows, calculate_task_summary, format_summary_line
|
from .task_list import (
|
||||||
|
build_task_rows,
|
||||||
|
calculate_task_summary,
|
||||||
|
filter_excluded_versions,
|
||||||
|
format_summary_line,
|
||||||
|
)
|
||||||
from .yaml_config import ensure_config_dir, resolve_output_path, save_period_to_config
|
from .yaml_config import ensure_config_dir, resolve_output_path, save_period_to_config
|
||||||
|
|
||||||
# Маскируем API-ключ Redmine в тексте ошибок (#55):
|
# Маскируем API-ключ Redmine в тексте ошибок (#55):
|
||||||
@@ -287,6 +292,10 @@ def _run_my_tasks(args: argparse.Namespace) -> int:
|
|||||||
logging.exception("Unexpected error") # noqa: LOG015
|
logging.exception("Unexpected error") # noqa: LOG015
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
# Бакет-версии (Wishlist) исключаются до проверки на пустоту: если все
|
||||||
|
# открытые задачи в них, пользователь видит "Открытых задач нет".
|
||||||
|
items = filter_excluded_versions(items)
|
||||||
|
|
||||||
if not items:
|
if not items:
|
||||||
print("ℹ️ Открытых задач нет.", file=sys.stderr)
|
print("ℹ️ Открытых задач нет.", file=sys.stderr)
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -22,6 +22,31 @@ def _issue_version_name(issue: Issue) -> str:
|
|||||||
return get_version(issue, default="")
|
return get_version(issue, default="")
|
||||||
|
|
||||||
|
|
||||||
|
# "Wishlist" -- постоянная бакет-версия без срока (личный бакет Wendy, факт
|
||||||
|
# из трекера): задачи в ней -- отложенные идеи, создающие мусорный фон в
|
||||||
|
# персональной выборке, поэтому режим --my-tasks исключает их целиком --
|
||||||
|
# из таблицы и из саммари. Сравнение имени точное, без нормализации регистра.
|
||||||
|
EXCLUDED_VERSION_NAMES: frozenset[str] = frozenset({"Wishlist"})
|
||||||
|
|
||||||
|
|
||||||
|
def filter_excluded_versions(
|
||||||
|
items: list[tuple[Issue, date | None]],
|
||||||
|
) -> list[tuple[Issue, date | None]]:
|
||||||
|
"""Убирает задачи версий-исключений из выборки --my-tasks.
|
||||||
|
|
||||||
|
Доступ к fixed_version.name getattr-безопасный: задача без версии или
|
||||||
|
с версией без имени остаётся в выборке. Даты в парах сохраняются как есть.
|
||||||
|
"""
|
||||||
|
kept: list[tuple[Issue, date | None]] = []
|
||||||
|
for item in items:
|
||||||
|
version = getattr(item[0], "fixed_version", None)
|
||||||
|
name = getattr(version, "name", None) if version is not None else None
|
||||||
|
if name and str(name) in EXCLUDED_VERSION_NAMES:
|
||||||
|
continue
|
||||||
|
kept.append(item)
|
||||||
|
return kept
|
||||||
|
|
||||||
|
|
||||||
def _sort_key(item: tuple[Issue, date | None]) -> tuple[int, bool, date, int]:
|
def _sort_key(item: tuple[Issue, date | None]) -> tuple[int, bool, date, int]:
|
||||||
"""Ключ сортировки: приоритет, затем срок по возрастанию, затем id.
|
"""Ключ сортировки: приоритет, затем срок по возрастанию, затем id.
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from datetime import date
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from redmine_reporter.cli import main, parse_date_range
|
from redmine_reporter.cli import main, parse_date_range
|
||||||
from redmine_reporter.client import RedmineAPIError
|
from redmine_reporter.client import RedmineAPIError
|
||||||
|
|
||||||
@@ -2115,6 +2116,45 @@ class TestMyTasksFlag:
|
|||||||
assert "потрачено:" in captured.err
|
assert "потрачено:" in captured.err
|
||||||
assert "Открытых:" not in captured.out
|
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.dict(os.environ, VALID_ENV, clear=True)
|
||||||
@mock.patch("redmine_reporter.cli.fetch_open_issues")
|
@mock.patch("redmine_reporter.cli.fetch_open_issues")
|
||||||
def test_my_tasks_api_error_exits_one(self, mock_fetch, capsys):
|
def test_my_tasks_api_error_exits_one(self, mock_fetch, capsys):
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from datetime import date
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from redmine_reporter.client import fetch_issues_with_spent_time, fetch_open_issues
|
from redmine_reporter.client import fetch_issues_with_spent_time, fetch_open_issues
|
||||||
from redmine_reporter.config import DEFAULT_REDMINE_VERIFY
|
from redmine_reporter.config import DEFAULT_REDMINE_VERIFY
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from typing import ClassVar
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from redmine_reporter.config import AppConfig, Config, EmailConfig
|
from redmine_reporter.config import AppConfig, Config, EmailConfig
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from unittest import mock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from odf.opendocument import OpenDocument, OpenDocumentText
|
from odf.opendocument import OpenDocument, OpenDocumentText
|
||||||
|
|
||||||
from redmine_reporter.formatters.console import CompactFormatter, TableFormatter
|
from redmine_reporter.formatters.console import CompactFormatter, TableFormatter
|
||||||
from redmine_reporter.formatters.csv import CSVFormatter
|
from redmine_reporter.formatters.csv import CSVFormatter
|
||||||
from redmine_reporter.formatters.factory import get_formatter_by_extension
|
from redmine_reporter.formatters.factory import get_formatter_by_extension
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import smtplib
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from redmine_reporter.client import RedmineAPIError
|
from redmine_reporter.client import RedmineAPIError
|
||||||
from redmine_reporter.config import EmailConfig, SmtpConfig
|
from redmine_reporter.config import EmailConfig, SmtpConfig
|
||||||
from redmine_reporter.mailer import build_message, send_report
|
from redmine_reporter.mailer import build_message, send_report
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ from datetime import date
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from redmine_reporter.task_list import (
|
from redmine_reporter.task_list import (
|
||||||
|
EXCLUDED_VERSION_NAMES,
|
||||||
PRIORITY_RANK,
|
PRIORITY_RANK,
|
||||||
build_task_rows,
|
build_task_rows,
|
||||||
calculate_task_summary,
|
calculate_task_summary,
|
||||||
|
filter_excluded_versions,
|
||||||
format_summary_line,
|
format_summary_line,
|
||||||
priority_rank,
|
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]
|
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
|
from typing import cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from redmine_reporter.formatters.task_table import TaskTableFormatter
|
from redmine_reporter.formatters.task_table import TaskTableFormatter
|
||||||
from redmine_reporter.types import TaskRow
|
from redmine_reporter.types import TaskRow
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from pathlib import Path
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from redmine_reporter.yaml_config import (
|
from redmine_reporter.yaml_config import (
|
||||||
check_file_permissions,
|
check_file_permissions,
|
||||||
ensure_config_dir,
|
ensure_config_dir,
|
||||||
|
|||||||
Reference in New Issue
Block a user