- #13 (critical): Add CoveredTableCell elements to ODT for valid row spans (ODF 1.2) - #28: Move "Total issues" info message from stdout to stderr (clean pipe output) - #27: Wrap HTML export in full document with DOCTYPE and meta charset utf-8 - #26: Save CSV with utf-8-sig encoding (UTF-8 BOM for Excel compatibility) - #31: Document CSV uses full project/version values (not display_* like console/MD) - #30: Fix ODT header formatting when author is empty (no leading dot/space) - #36: Remove test_cli_smoke_empty testing unreachable code path (return []) - #37: Remove unused mock_path variable in ODT test fixture - #34: Remove unreachable len(parts) != 2 check in parse_date_range Closes #13, #28, #27, #26, #31, #30, #36, #37, #34
86 lines
3.3 KiB
Python
86 lines
3.3 KiB
Python
from html import escape
|
|
from typing import List
|
|
|
|
from ..report_builder import group_rows_by_project_and_version
|
|
from ..types import ReportRow
|
|
from .base import Formatter
|
|
|
|
|
|
class HTMLFormatter(Formatter):
|
|
"""Форматтер для экспорта отчёта в HTML."""
|
|
|
|
def __init__(self, **_kwargs):
|
|
super().__init__()
|
|
|
|
def format(self, rows: List[ReportRow]) -> str:
|
|
projects = group_rows_by_project_and_version(rows)
|
|
|
|
lines = [
|
|
"<!DOCTYPE html>",
|
|
'<html lang="ru">',
|
|
"<head>",
|
|
' <meta charset="utf-8">',
|
|
"</head>",
|
|
"<body>",
|
|
'<table border="1" cellpadding="6" cellspacing="0" style="border-collapse: collapse; font-family: Arial, sans-serif;">',
|
|
" <thead>",
|
|
" <tr>",
|
|
" <th>Наименование Проекта</th>",
|
|
" <th>Номер версии*</th>",
|
|
" <th>Задача</th>",
|
|
" <th>Статус Готовность*</th>",
|
|
" <th>Затрачено за отчетный период</th>",
|
|
" </tr>",
|
|
" </thead>",
|
|
" <tbody>",
|
|
]
|
|
|
|
for project, versions in projects.items():
|
|
project_text = escape(project)
|
|
total_project_rows = sum(len(tasks) for tasks in versions.values())
|
|
first_version_in_project = True
|
|
|
|
for version, task_rows in versions.items():
|
|
version_text = escape(version)
|
|
row_span_version = len(task_rows)
|
|
first_row_in_version = True
|
|
|
|
for r in task_rows:
|
|
task_cell = escape(f"{r['issue_id']}. {r['subject']}")
|
|
status_text = escape(r["status_ru"])
|
|
time_text = escape(r["time_text"])
|
|
lines.append(" <tr>")
|
|
|
|
# Ячейка "Проект" - только в первой строке проекта
|
|
if first_version_in_project and first_row_in_version:
|
|
lines.append(
|
|
f' <td rowspan="{total_project_rows}" style="vertical-align: top;">{project_text}</td>'
|
|
)
|
|
|
|
# Ячейка "Версия" - только в первой строке версии
|
|
if first_row_in_version:
|
|
lines.append(
|
|
f' <td rowspan="{row_span_version}" style="vertical-align: top;">{version_text}</td>'
|
|
)
|
|
first_row_in_version = False
|
|
|
|
# Остальные колонки
|
|
lines.append(f" <td>{task_cell}</td>")
|
|
lines.append(f" <td>{status_text}</td>")
|
|
lines.append(f" <td>{time_text}</td>")
|
|
|
|
lines.append(" </tr>")
|
|
|
|
first_version_in_project = False
|
|
|
|
lines.append(" </tbody>")
|
|
lines.append("</table>")
|
|
lines.append("</body>")
|
|
lines.append("</html>")
|
|
return "\n".join(lines)
|
|
|
|
def save(self, rows: List[ReportRow], output_path: str) -> None:
|
|
content = self.format(rows)
|
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
f.write(content)
|