feat(activity): add --by-activity flag to break down spent time by activity type
- Load time entry activity names from Redmine enumeration. - Aggregate hours per issue and per activity in fetch_issues_with_spent_time. - Add --by-activity CLI flag and propagate it through report builder, summary, and all formatters (console, CSV, HTML, JSON, ODT). - Keep backward-compatible 2-tuple input in build_grouped_report. - Bump version to 1.8.0. Closes #41
This commit is contained in:
@@ -12,13 +12,14 @@ class TableFormatter(Formatter):
|
||||
def format(self, rows: List[ReportRow]) -> str:
|
||||
table_rows = [["Проект", "Версия", "Задача", "Статус", "Затрачено"]]
|
||||
for r in rows:
|
||||
time_text = r["time_text"].replace("\n", " / ")
|
||||
table_rows.append(
|
||||
[
|
||||
r["display_project"],
|
||||
r["display_version"],
|
||||
f"{r['issue_id']}. {r['subject']}",
|
||||
r["status_ru"],
|
||||
r["time_text"],
|
||||
time_text,
|
||||
]
|
||||
)
|
||||
return tabulate(table_rows, headers="firstrow", tablefmt="fancy_grid")
|
||||
@@ -35,9 +36,10 @@ class CompactFormatter(Formatter):
|
||||
def format(self, rows: List[ReportRow]) -> str:
|
||||
lines = []
|
||||
for r in rows:
|
||||
time_text = r["time_text"].replace("\n", " / ")
|
||||
lines.append(
|
||||
f"{r['display_project']} | {r['display_version']} | "
|
||||
f"{r['issue_id']}. {r['subject']} | {r['status_ru']} | {r['time_text']}"
|
||||
f"{r['issue_id']}. {r['subject']} | {r['status_ru']} | {time_text}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ class CSVFormatter(Formatter):
|
||||
headers.append("Spent Time")
|
||||
writer.writerow(headers)
|
||||
for r in rows:
|
||||
time_text = r["time_text"].replace("\n", " / ")
|
||||
data = [
|
||||
r["project"],
|
||||
r["version"],
|
||||
@@ -35,7 +36,7 @@ class CSVFormatter(Formatter):
|
||||
r["status_ru"],
|
||||
]
|
||||
if not self.no_time:
|
||||
data.append(r["time_text"])
|
||||
data.append(time_text)
|
||||
writer.writerow(data)
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class HTMLFormatter(Formatter):
|
||||
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"])
|
||||
time_text = escape(r["time_text"]).replace("\n", "<br>")
|
||||
lines.append(" <tr>")
|
||||
|
||||
# Ячейка "Проект" - только в первой строке проекта
|
||||
|
||||
@@ -12,8 +12,9 @@ class JSONFormatter(Formatter):
|
||||
super().__init__()
|
||||
|
||||
def format(self, rows: List[ReportRow]) -> str:
|
||||
data = [
|
||||
{
|
||||
data = []
|
||||
for r in rows:
|
||||
item = {
|
||||
"project": r["project"],
|
||||
"version": r["version"],
|
||||
"issue_id": r["issue_id"],
|
||||
@@ -21,8 +22,10 @@ class JSONFormatter(Formatter):
|
||||
"status": r["status_ru"],
|
||||
"time": r["time_text"],
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
activities = r.get("activities")
|
||||
if activities:
|
||||
item["activities"] = activities
|
||||
data.append(item)
|
||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
||||
|
||||
def save(self, rows: List[ReportRow], output_path: str) -> None:
|
||||
|
||||
@@ -125,8 +125,12 @@ class ODTFormatter(Formatter):
|
||||
row.addElement(status_cell)
|
||||
|
||||
time_cell = TableCell(stylename=cell_style_name)
|
||||
p = P(stylename=para_style_name, text=r["time_text"])
|
||||
time_cell.addElement(p)
|
||||
time_lines = r["time_text"].split("\n")
|
||||
for i, line in enumerate(time_lines):
|
||||
p = P(stylename=para_style_name, text=line)
|
||||
time_cell.addElement(p)
|
||||
if i < len(time_lines) - 1:
|
||||
time_cell.addElement(P(stylename=para_style_name, text=""))
|
||||
row.addElement(time_cell)
|
||||
|
||||
table.addElement(row)
|
||||
|
||||
Reference in New Issue
Block a user