Quick fixes & tests
This commit is contained in:
@@ -74,12 +74,23 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
if args.output:
|
||||
output_ext = os.path.splitext(args.output)[1].lower()
|
||||
|
||||
if not output_ext:
|
||||
print(
|
||||
"❌ Файл без расширения. Укажите расширение: .odt, .csv, .md или .html",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
formatter = get_formatter_by_extension(
|
||||
output_ext, author=Config.get_author(args.author), from_date=from_date, to_date=to_date
|
||||
)
|
||||
|
||||
if not formatter:
|
||||
print(f"❌ Неизвестный формат файла: {output_ext}", file=sys.stderr)
|
||||
known_exts = ", ".join([".odt", ".csv", ".md", ".html"])
|
||||
print(
|
||||
f"❌ Неизвестный формат файла: {output_ext!r}. Поддерживаются: {known_exts}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
try:
|
||||
@@ -92,7 +103,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
print(f"❌ Import error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
except Exception as e:
|
||||
fmt = "ODT" if output_ext == ".odt" else ("CSV" if output_ext == ".csv" else "Markdown")
|
||||
fmt = output_ext.lstrip(".").upper()
|
||||
print(f"❌ {fmt} export error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
from typing import Any, List
|
||||
from ..types import ReportRow
|
||||
|
||||
|
||||
@@ -7,13 +7,19 @@ class Formatter(ABC):
|
||||
"""
|
||||
Абстрактный базовый класс для всех форматтеров.
|
||||
Определяет общий интерфейс для форматирования отчета.
|
||||
|
||||
Контракт:
|
||||
- format() возвращает строку для текстовых форматтеров (CSV, HTML, Markdown, console)
|
||||
и объект OpenDocument для ODTFormatter.
|
||||
- save() сохраняет результат в файл; консольные форматтеры бросают NotImplementedError.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def format(self, rows: List[ReportRow]) -> str:
|
||||
def format(self, rows: List[ReportRow]) -> Any:
|
||||
"""
|
||||
Форматирует список строк отчета в нужный формат.
|
||||
Возвращает строковое представление отчета.
|
||||
Для текстовых форматтеров возвращает str.
|
||||
Для ODTFormatter возвращает объект OpenDocument.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -22,6 +28,6 @@ class Formatter(ABC):
|
||||
"""
|
||||
Сохраняет отформатированный отчет в файл по указанному пути.
|
||||
Для форматтеров, которые не поддерживают сохранение (например, консольные),
|
||||
можно вызывать `format` и записывать результат вручную.
|
||||
бросает NotImplementedError.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -8,7 +8,7 @@ from ..types import ReportRow
|
||||
class CSVFormatter(Formatter):
|
||||
"""Форматтер для экспорта в CSV."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, **_kwargs):
|
||||
super().__init__()
|
||||
|
||||
def format(self, rows: List[ReportRow]) -> str:
|
||||
|
||||
@@ -6,7 +6,7 @@ from ..types import ReportRow
|
||||
class HTMLFormatter(Formatter):
|
||||
"""Форматтер для экспорта отчёта в HTML."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, **_kwargs):
|
||||
super().__init__()
|
||||
|
||||
def format(self, rows: List[ReportRow]) -> str:
|
||||
|
||||
@@ -6,7 +6,7 @@ from ..types import ReportRow
|
||||
class MarkdownFormatter(Formatter):
|
||||
"""Форматтер для экспорта в Markdown."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, **_kwargs):
|
||||
super().__init__()
|
||||
|
||||
def format(self, rows: List[ReportRow]) -> str:
|
||||
|
||||
@@ -103,7 +103,7 @@ class ODTFormatter(Formatter):
|
||||
if first_version_in_project and first_row_in_version:
|
||||
cell_project = TableCell(stylename=cell_style_name)
|
||||
cell_project.setAttribute("numberrowsspanned", str(total_project_rows))
|
||||
p = P(stylename=para_style_name, text=project) # Полное название проекта
|
||||
p = P(stylename=para_style_name, text=project)
|
||||
cell_project.addElement(p)
|
||||
row.addElement(cell_project)
|
||||
|
||||
@@ -115,9 +115,6 @@ class ODTFormatter(Formatter):
|
||||
cell_version.addElement(p)
|
||||
row.addElement(cell_version)
|
||||
first_row_in_version = False
|
||||
else:
|
||||
# Пропускаем - уже объединена
|
||||
pass
|
||||
|
||||
# Остальные колонки
|
||||
task_cell = TableCell(stylename=cell_style_name)
|
||||
@@ -137,7 +134,8 @@ class ODTFormatter(Formatter):
|
||||
row.addElement(time_cell)
|
||||
|
||||
table.addElement(row)
|
||||
first_version_in_project = False
|
||||
|
||||
first_version_in_project = False
|
||||
|
||||
doc.text.addElement(table)
|
||||
doc.text.addElement(P(stylename=para_style_name, text=""))
|
||||
|
||||
@@ -27,8 +27,14 @@ def build_grouped_report(
|
||||
"""
|
||||
Преобразует список задач с затраченным временем в плоский список строк отчёта,
|
||||
с учётом группировки по проекту и версии (пустые ячейки для повторяющихся значений).
|
||||
|
||||
Предусловие: issue_hours должен быть отсортирован по (project, version).
|
||||
Функция выполняет сортировку самостоятельно для защиты от несортированного ввода.
|
||||
"""
|
||||
|
||||
# Защитная сортировка -- гарантирует корректную группировку независимо от порядка на входе
|
||||
issue_hours = sorted(issue_hours, key=lambda x: (str(x[0].project), get_version(x[0])))
|
||||
|
||||
rows: List[ReportRow] = []
|
||||
prev_project: str = ""
|
||||
prev_version: str = ""
|
||||
|
||||
Reference in New Issue
Block a user