Extract grouping logic into shared function (#19)
Add group_rows_by_project_and_version() to report_builder.py and replace the duplicated inline grouping in both html.py and odt.py with a single call to the shared function. This ensures consistent grouping behavior across formatters and removes a maintenance burden — any change to grouping logic now happens in one place. Closes #19
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
from html import escape
|
from html import escape
|
||||||
from typing import Dict, List
|
from typing import List
|
||||||
|
|
||||||
|
from ..report_builder import group_rows_by_project_and_version
|
||||||
from ..types import ReportRow
|
from ..types import ReportRow
|
||||||
from .base import Formatter
|
from .base import Formatter
|
||||||
|
|
||||||
@@ -12,16 +13,7 @@ class HTMLFormatter(Formatter):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def format(self, rows: List[ReportRow]) -> str:
|
def format(self, rows: List[ReportRow]) -> str:
|
||||||
# Сгруппируем данные
|
projects = group_rows_by_project_and_version(rows)
|
||||||
projects: Dict[str, Dict[str, List[ReportRow]]] = {}
|
|
||||||
for r in rows:
|
|
||||||
proj = r["project"]
|
|
||||||
ver = r["version"]
|
|
||||||
if proj not in projects:
|
|
||||||
projects[proj] = {}
|
|
||||||
if ver not in projects[proj]:
|
|
||||||
projects[proj][ver] = []
|
|
||||||
projects[proj][ver].append(r)
|
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
'<table border="1" cellpadding="6" cellspacing="0" style="border-collapse: collapse; font-family: Arial, sans-serif;">',
|
'<table border="1" cellpadding="6" cellspacing="0" style="border-collapse: collapse; font-family: Arial, sans-serif;">',
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from importlib import resources
|
from importlib import resources
|
||||||
from typing import Dict, List
|
from typing import List
|
||||||
|
|
||||||
from odf.opendocument import OpenDocument, load
|
from odf.opendocument import OpenDocument, load
|
||||||
from odf.style import Style, TableCellProperties, TableColumnProperties
|
from odf.style import Style, TableCellProperties, TableColumnProperties
|
||||||
from odf.table import Table, TableCell, TableColumn, TableRow
|
from odf.table import Table, TableCell, TableColumn, TableRow
|
||||||
from odf.text import P
|
from odf.text import P
|
||||||
|
|
||||||
|
from ..report_builder import group_rows_by_project_and_version
|
||||||
from ..types import ReportRow
|
from ..types import ReportRow
|
||||||
from ..utils import get_month_name_from_range
|
from ..utils import get_month_name_from_range
|
||||||
from .base import Formatter
|
from .base import Formatter
|
||||||
@@ -69,18 +70,7 @@ class ODTFormatter(Formatter):
|
|||||||
header_row.addElement(cell)
|
header_row.addElement(cell)
|
||||||
table.addElement(header_row)
|
table.addElement(header_row)
|
||||||
|
|
||||||
projects: Dict[str, Dict[str, List[ReportRow]]] = {}
|
projects = group_rows_by_project_and_version(rows)
|
||||||
for r in rows:
|
|
||||||
project = r["project"]
|
|
||||||
version = r["version"]
|
|
||||||
|
|
||||||
if project not in projects:
|
|
||||||
projects[project] = {}
|
|
||||||
|
|
||||||
if version not in projects[project]:
|
|
||||||
projects[project][version] = []
|
|
||||||
|
|
||||||
projects[project][version].append(r)
|
|
||||||
|
|
||||||
# Данные с двухуровневой группировкой и объединением ячеек
|
# Данные с двухуровневой группировкой и объединением ячеек
|
||||||
for project, versions in projects.items():
|
for project, versions in projects.items():
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import List, Tuple, cast
|
from typing import Dict, List, Tuple, cast
|
||||||
|
|
||||||
from redminelib.resources import Issue
|
from redminelib.resources import Issue
|
||||||
|
|
||||||
@@ -71,3 +71,28 @@ def build_grouped_report(
|
|||||||
prev_version = version
|
prev_version = version
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def group_rows_by_project_and_version(
|
||||||
|
rows: List[ReportRow],
|
||||||
|
) -> Dict[str, Dict[str, List[ReportRow]]]:
|
||||||
|
"""
|
||||||
|
Группирует плоский список строк отчёта в иерархию project → version → [rows].
|
||||||
|
|
||||||
|
Предполагается, что rows уже отсортирован (build_grouped_report это гарантирует).
|
||||||
|
Возвращает обычный dict, сохраняющий порядок вставки (Python 3.7+).
|
||||||
|
Используется форматтерами HTML и ODT для объединения ячеек.
|
||||||
|
"""
|
||||||
|
projects: Dict[str, Dict[str, List[ReportRow]]] = {}
|
||||||
|
for r in rows:
|
||||||
|
project = r["project"]
|
||||||
|
version = r["version"]
|
||||||
|
|
||||||
|
if project not in projects:
|
||||||
|
projects[project] = {}
|
||||||
|
if version not in projects[project]:
|
||||||
|
projects[project][version] = []
|
||||||
|
|
||||||
|
projects[project][version].append(r)
|
||||||
|
|
||||||
|
return projects
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
from redmine_reporter.report_builder import STATUS_TRANSLATION, build_grouped_report
|
from redmine_reporter.report_builder import (
|
||||||
|
STATUS_TRANSLATION,
|
||||||
|
build_grouped_report,
|
||||||
|
group_rows_by_project_and_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MockIssue:
|
class MockIssue:
|
||||||
@@ -117,3 +121,104 @@ def test_build_grouped_report_deterministic_order_within_group():
|
|||||||
]
|
]
|
||||||
rows = build_grouped_report(issues)
|
rows = build_grouped_report(issues)
|
||||||
assert [r["issue_id"] for r in rows] == [101, 102, 103]
|
assert [r["issue_id"] for r in rows] == [101, 102, 103]
|
||||||
|
|
||||||
|
|
||||||
|
# -- #19: Общая функция группировки --
|
||||||
|
|
||||||
|
|
||||||
|
def test_group_rows_basic():
|
||||||
|
"""Два проекта, в одном две версии — структура корректна."""
|
||||||
|
rows = [
|
||||||
|
{
|
||||||
|
"project": "A",
|
||||||
|
"version": "v1",
|
||||||
|
"issue_id": 1,
|
||||||
|
"subject": "T1",
|
||||||
|
"status_ru": "S",
|
||||||
|
"time_text": "1ч",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"project": "A",
|
||||||
|
"version": "v2",
|
||||||
|
"issue_id": 2,
|
||||||
|
"subject": "T2",
|
||||||
|
"status_ru": "S",
|
||||||
|
"time_text": "2ч",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"project": "B",
|
||||||
|
"version": "v1",
|
||||||
|
"issue_id": 3,
|
||||||
|
"subject": "T3",
|
||||||
|
"status_ru": "S",
|
||||||
|
"time_text": "3ч",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
grouped = group_rows_by_project_and_version(rows)
|
||||||
|
|
||||||
|
assert list(grouped.keys()) == ["A", "B"]
|
||||||
|
assert list(grouped["A"].keys()) == ["v1", "v2"]
|
||||||
|
assert list(grouped["B"].keys()) == ["v1"]
|
||||||
|
assert len(grouped["A"]["v1"]) == 1
|
||||||
|
assert len(grouped["A"]["v2"]) == 1
|
||||||
|
assert len(grouped["B"]["v1"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_group_rows_multiple_tasks_per_version():
|
||||||
|
"""Несколько задач в одной версии — все попадают в список."""
|
||||||
|
rows = [
|
||||||
|
{
|
||||||
|
"project": "A",
|
||||||
|
"version": "v1",
|
||||||
|
"issue_id": 1,
|
||||||
|
"subject": "T1",
|
||||||
|
"status_ru": "S",
|
||||||
|
"time_text": "1ч",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"project": "A",
|
||||||
|
"version": "v1",
|
||||||
|
"issue_id": 2,
|
||||||
|
"subject": "T2",
|
||||||
|
"status_ru": "S",
|
||||||
|
"time_text": "2ч",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"project": "A",
|
||||||
|
"version": "v1",
|
||||||
|
"issue_id": 3,
|
||||||
|
"subject": "T3",
|
||||||
|
"status_ru": "S",
|
||||||
|
"time_text": "3ч",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
grouped = group_rows_by_project_and_version(rows)
|
||||||
|
|
||||||
|
assert len(grouped["A"]["v1"]) == 3
|
||||||
|
assert [r["issue_id"] for r in grouped["A"]["v1"]] == [1, 2, 3]
|
||||||
|
|
||||||
|
|
||||||
|
def test_group_rows_preserves_row_data():
|
||||||
|
"""Строки в группировке — те же объекты, без потери данных."""
|
||||||
|
rows = [
|
||||||
|
{
|
||||||
|
"project": "P",
|
||||||
|
"version": "v1",
|
||||||
|
"issue_id": 42,
|
||||||
|
"subject": "Task",
|
||||||
|
"status_ru": "Готово",
|
||||||
|
"time_text": "5ч",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
grouped = group_rows_by_project_and_version(rows)
|
||||||
|
|
||||||
|
row = grouped["P"]["v1"][0]
|
||||||
|
assert row["issue_id"] == 42
|
||||||
|
assert row["subject"] == "Task"
|
||||||
|
assert row["status_ru"] == "Готово"
|
||||||
|
assert row["time_text"] == "5ч"
|
||||||
|
|
||||||
|
|
||||||
|
def test_group_rows_empty():
|
||||||
|
"""Пустой список — пустой словарь."""
|
||||||
|
assert group_rows_by_project_and_version([]) == {}
|
||||||
|
|||||||
Reference in New Issue
Block a user