Files
redmine-reporter/redmine_reporter/formatters/html.py
Артём Кокос 14219564dd Fix 9 bugs: ODT covered-cells, CSV BOM, HTML charset, stderr, dead code
- #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
2026-06-27 13:01:32 +07:00

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)