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 redminelib.resources import Issue
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .utils import get_version
|
|
||||||
|
|
||||||
|
|
||||||
def _get_redmine_auth_kwargs() -> Dict[str, Any]:
|
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)
|
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")
|
issues = redmine.issue.filter(issue_id=issue_list_str, status_id="*", sort="project:asc")
|
||||||
|
|
||||||
# Сопоставляем задачи с суммарным временем
|
# Сопоставляем задачи с суммарным временем.
|
||||||
|
# Сортировка выполняется в report_builder.build_grouped_report,
|
||||||
|
# здесь оставляем порядок API как есть.
|
||||||
result = []
|
result = []
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
total_hours = spent_time.get(issue.id, 0.0)
|
total_hours = spent_time.get(issue.id, 0.0)
|
||||||
result.append((issue, total_hours))
|
result.append((issue, total_hours))
|
||||||
|
|
||||||
# Сортируем по (проект, версия)
|
|
||||||
result.sort(key=lambda x: (str(x[0].project), get_version(x[0])))
|
|
||||||
|
|
||||||
return result
|
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] = []
|
rows: List[ReportRow] = []
|
||||||
prev_project: str = ""
|
prev_project: str = ""
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ def get_version(issue) -> str:
|
|||||||
version = getattr(issue, "fixed_version", None)
|
version = getattr(issue, "fixed_version", None)
|
||||||
if version is None:
|
if version is None:
|
||||||
return "<N/A>"
|
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:
|
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)
|
rows = build_grouped_report(issues)
|
||||||
assert rows[0]["issue_id"] == 42
|
assert rows[0]["issue_id"] == 42
|
||||||
assert rows[0]["subject"] == "Моя задача"
|
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
|
fixed_version = None
|
||||||
|
|
||||||
assert get_version(MockIssue()) == "<N/A>"
|
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