Fix version display, deterministic sort, remove duplicate sort
get_version() now returns version.name instead of str(version), which returned the numeric ID from redminelib.resources.Version. Falls back to str() when .name is absent (#14). Add issue.id as tertiary sort key in build_grouped_report so tasks within the same project+version group always appear in the same order regardless of API response ordering (#33). Remove redundant sort from client.fetch_issues_with_spent_time — the sort already runs in report_builder.build_grouped_report, so doing it twice wastes CPU. Also remove the now-unused get_version import (#32). Closes #14, closes #32, closes #33
This commit is contained in:
@@ -4,7 +4,6 @@ from redminelib import Redmine
|
||||
from redminelib.resources import Issue
|
||||
|
||||
from .config import Config
|
||||
from .utils import get_version
|
||||
|
||||
|
||||
def _get_redmine_auth_kwargs() -> Dict[str, Any]:
|
||||
@@ -54,13 +53,12 @@ def fetch_issues_with_spent_time(
|
||||
issue_list_str = ",".join(str(i) for i in issue_ids)
|
||||
issues = redmine.issue.filter(issue_id=issue_list_str, status_id="*", sort="project:asc")
|
||||
|
||||
# Сопоставляем задачи с суммарным временем
|
||||
# Сопоставляем задачи с суммарным временем.
|
||||
# Сортировка выполняется в report_builder.build_grouped_report,
|
||||
# здесь оставляем порядок API как есть.
|
||||
result = []
|
||||
for issue in issues:
|
||||
total_hours = spent_time.get(issue.id, 0.0)
|
||||
result.append((issue, total_hours))
|
||||
|
||||
# Сортируем по (проект, версия)
|
||||
result.sort(key=lambda x: (str(x[0].project), get_version(x[0])))
|
||||
|
||||
return result
|
||||
|
||||
@@ -35,7 +35,7 @@ def build_grouped_report(
|
||||
"""
|
||||
|
||||
# Защитная сортировка -- гарантирует корректную группировку независимо от порядка на входе
|
||||
issue_hours = sorted(issue_hours, key=lambda x: (str(x[0].project), get_version(x[0])))
|
||||
issue_hours = sorted(issue_hours, key=lambda x: (str(x[0].project), get_version(x[0]), x[0].id))
|
||||
|
||||
rows: List[ReportRow] = []
|
||||
prev_project: str = ""
|
||||
|
||||
@@ -33,7 +33,8 @@ def get_version(issue) -> str:
|
||||
version = getattr(issue, "fixed_version", None)
|
||||
if version is None:
|
||||
return "<N/A>"
|
||||
return str(version)
|
||||
name = getattr(version, "name", None)
|
||||
return str(name) if name else str(version)
|
||||
|
||||
|
||||
def hours_to_human(hours: float) -> str:
|
||||
|
||||
@@ -105,3 +105,15 @@ def test_build_grouped_report_preserves_issue_id_and_subject():
|
||||
rows = build_grouped_report(issues)
|
||||
assert rows[0]["issue_id"] == 42
|
||||
assert rows[0]["subject"] == "Моя задача"
|
||||
|
||||
|
||||
def test_build_grouped_report_deterministic_order_within_group():
|
||||
"""Задачи в одной группе проект+версия упорядочены по issue.id
|
||||
независимо от порядка на входе (#33)."""
|
||||
issues = [
|
||||
(MockIssue("P", "Task C", "New", "v1.0", 103), 1.0),
|
||||
(MockIssue("P", "Task A", "New", "v1.0", 101), 1.0),
|
||||
(MockIssue("P", "Task B", "New", "v1.0", 102), 1.0),
|
||||
]
|
||||
rows = build_grouped_report(issues)
|
||||
assert [r["issue_id"] for r in rows] == [101, 102, 103]
|
||||
|
||||
@@ -78,3 +78,35 @@ def test_get_version_none_attribute():
|
||||
fixed_version = None
|
||||
|
||||
assert get_version(MockIssue()) == "<N/A>"
|
||||
|
||||
|
||||
def test_get_version_with_redminelib_version_object():
|
||||
"""redminelib Version: str() возвращает ID, .name — человекочитаемое имя."""
|
||||
|
||||
class MockVersion:
|
||||
"""Имитирует redminelib.resources.Version — str() даёт ID."""
|
||||
|
||||
def __init__(self, vid, name):
|
||||
self.id = vid
|
||||
self.name = name
|
||||
|
||||
def __str__(self):
|
||||
return str(self.id)
|
||||
|
||||
class MockIssue:
|
||||
fixed_version = MockVersion(42, "v2.5.0")
|
||||
|
||||
assert get_version(MockIssue()) == "v2.5.0"
|
||||
|
||||
|
||||
def test_get_version_falls_back_to_str_when_no_name():
|
||||
"""Если у объекта версии нет .name — fallback на str()."""
|
||||
|
||||
class MockVersionNoName:
|
||||
def __str__(self):
|
||||
return "fallback-id"
|
||||
|
||||
class MockIssue:
|
||||
fixed_version = MockVersionNoName()
|
||||
|
||||
assert get_version(MockIssue()) == "fallback-id"
|
||||
|
||||
Reference in New Issue
Block a user