feat: track task scope status and version
This commit is contained in:
+102
-23
@@ -40,6 +40,15 @@ UUID_RE = re.compile(
|
||||
UUID_PATH_RE = re.compile(
|
||||
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
)
|
||||
SCOPE_TASK_STATUS_LABELS = {
|
||||
"not-started": "brak",
|
||||
"draft": "robocze",
|
||||
"in-progress": "w trakcie",
|
||||
"review": "do przeglądu",
|
||||
"ready": "opracowane",
|
||||
"blocked": "zablokowane",
|
||||
}
|
||||
SCOPE_TASK_VERSION_RE = re.compile(r"^v\d{2}\.\d{2}$")
|
||||
|
||||
|
||||
def configure_card_root(card_root: Path) -> None:
|
||||
@@ -850,25 +859,72 @@ def render_front_matter(card: dict) -> list[str]:
|
||||
return lines
|
||||
|
||||
|
||||
def scope_task_status_label(value: object) -> str:
|
||||
status = str(value or "").strip()
|
||||
if not status:
|
||||
return ""
|
||||
try:
|
||||
return SCOPE_TASK_STATUS_LABELS[status]
|
||||
except KeyError as error:
|
||||
allowed = ", ".join(SCOPE_TASK_STATUS_LABELS)
|
||||
raise ValueError(f"nieznany status tasku {status!r}; dozwolone: {allowed}") from error
|
||||
|
||||
|
||||
def scope_task_version(value: object) -> str:
|
||||
version = str(value or "").strip()
|
||||
if version and not SCOPE_TASK_VERSION_RE.fullmatch(version):
|
||||
raise ValueError(
|
||||
f"niepoprawna wersja tasku {version!r}; oczekiwany format v00.01"
|
||||
)
|
||||
return version
|
||||
|
||||
|
||||
def scope_table_columns(rows: list[dict]) -> list[str]:
|
||||
columns = ["chapter"]
|
||||
if any(str(row.get("task", "")).strip() for row in rows):
|
||||
columns.append("task")
|
||||
columns.extend(["idea", "priority"])
|
||||
if any(str(row.get("status", "")).strip() for row in rows):
|
||||
columns.append("status")
|
||||
if any(str(row.get("version", "")).strip() for row in rows):
|
||||
columns.append("version")
|
||||
return columns
|
||||
|
||||
|
||||
def scope_table_default_headers(columns: list[str]) -> list[str]:
|
||||
labels = {
|
||||
"chapter": "K&R",
|
||||
"task": "Numer tasku",
|
||||
"idea": "Najważniejsza idea",
|
||||
"priority": "Priorytet",
|
||||
"status": "Status",
|
||||
"version": "Version",
|
||||
}
|
||||
return [labels[column] for column in columns]
|
||||
|
||||
|
||||
def render_front_scope_table_tex(scope: dict) -> list[str]:
|
||||
table = scope.get("scope_table") or {}
|
||||
rows = table.get("rows") or []
|
||||
if not rows:
|
||||
return []
|
||||
has_task_column = any(str(row.get("task", "")).strip() for row in rows)
|
||||
default_headers = (
|
||||
["K&R", "Numer tasku", "Najważniejsza idea", "Priorytet"]
|
||||
if has_task_column
|
||||
else ["K&R", "Najważniejsza idea", "Priorytet"]
|
||||
)
|
||||
columns = scope_table_columns(rows)
|
||||
has_task_column = "task" in columns
|
||||
has_status_column = "status" in columns
|
||||
has_version_column = "version" in columns
|
||||
default_headers = scope_table_default_headers(columns)
|
||||
headers = table.get("headers") or default_headers
|
||||
if len(headers) != len(default_headers):
|
||||
headers = default_headers
|
||||
column_spec = (
|
||||
r"@{}>{\ttfamily}p{1.05cm}>{\ttfamily}p{1.55cm}X>{\raggedright\arraybackslash}p{3.25cm}@{}"
|
||||
if has_task_column
|
||||
else r"@{}>{\ttfamily}p{1.25cm}X>{\raggedright\arraybackslash}p{3.55cm}@{}"
|
||||
)
|
||||
column_parts = [r">{\ttfamily\raggedright\arraybackslash}p{0.85cm}"]
|
||||
if has_task_column:
|
||||
column_parts.append(r">{\ttfamily\raggedright\arraybackslash}p{1.45cm}")
|
||||
column_parts.extend(["X", r">{\raggedright\arraybackslash}p{2.45cm}"])
|
||||
if has_status_column:
|
||||
column_parts.append(r">{\raggedright\arraybackslash}p{1.75cm}")
|
||||
if has_version_column:
|
||||
column_parts.append(r">{\ttfamily\raggedright\arraybackslash}p{1.50cm}")
|
||||
column_spec = "@{}" + "".join(column_parts) + "@{}"
|
||||
lines = [
|
||||
r"\vspace{0.55em}",
|
||||
rf"\small\begin{{tabularx}}{{\textwidth}}{{{column_spec}}}",
|
||||
@@ -879,7 +935,12 @@ def render_front_scope_table_tex(scope: dict) -> list[str]:
|
||||
if has_task_column:
|
||||
cells.append(rf"\texttt{{{tex_escape(row.get('task', ''))}}}")
|
||||
cells.extend([row.get("idea_tex", ""), tex_escape(row.get("priority", ""))])
|
||||
if str(row.get("task", "")).strip().lower() == "task04":
|
||||
if has_status_column:
|
||||
cells.append(tex_escape(scope_task_status_label(row.get("status", ""))))
|
||||
if has_version_column:
|
||||
cells.append(rf"\texttt{{{tex_escape(scope_task_version(row.get('version', '')))}}}")
|
||||
is_key = bool(row.get("key", str(row.get("task", "")).strip().lower() == "task04"))
|
||||
if is_key:
|
||||
framed_cells = []
|
||||
for index, cell in enumerate(cells):
|
||||
alignment = "|l|" if index == 0 else "l|"
|
||||
@@ -3080,23 +3141,32 @@ def render_main_html(data: dict) -> str:
|
||||
if scope.get("scope_title"):
|
||||
table = scope.get("scope_table") or {}
|
||||
rows = table.get("rows") or []
|
||||
has_task_column = any(str(row.get("task", "")).strip() for row in rows)
|
||||
default_headers = (
|
||||
["K&R", "Numer tasku", "Najważniejsza idea", "Priorytet"]
|
||||
if has_task_column
|
||||
else ["K&R", "Najważniejsza idea", "Priorytet"]
|
||||
)
|
||||
columns = scope_table_columns(rows)
|
||||
has_task_column = "task" in columns
|
||||
has_status_column = "status" in columns
|
||||
has_version_column = "version" in columns
|
||||
default_headers = scope_table_default_headers(columns)
|
||||
headers = table.get("headers") or default_headers
|
||||
if len(headers) != len(default_headers):
|
||||
headers = default_headers
|
||||
scope_table_html = ""
|
||||
if rows:
|
||||
table_rows = "".join(
|
||||
f'<tr{" class=\"scope-row--key\"" if str(row.get("task", "")).strip().lower() == "task04" else ""}>'
|
||||
f'<tr{" class=\"scope-row--key\"" if bool(row.get("key", str(row.get("task", "")).strip().lower() == "task04")) else ""}>'
|
||||
f'<td><code>{html_escape(row.get("chapter", ""))}</code></td>'
|
||||
+ (f'<td><code>{html_escape(row.get("task", ""))}</code></td>' if has_task_column else "")
|
||||
+ f'<td>{latex_inline_to_html(row.get("idea_tex", ""), equation_numbers, figure_numbers)}</td>'
|
||||
+ f'<td>{html_escape(row.get("priority", ""))}</td></tr>'
|
||||
+ f'<td>{html_escape(row.get("priority", ""))}</td>'
|
||||
+ (
|
||||
f'<td><span class="scope-status" data-status="{html_escape(row.get("status", ""))}">'
|
||||
f'{html_escape(scope_task_status_label(row.get("status", "")))}</span></td>'
|
||||
if has_status_column else ""
|
||||
)
|
||||
+ (
|
||||
f'<td><code>{html_escape(scope_task_version(row.get("version", "")))}</code></td>'
|
||||
if has_version_column else ""
|
||||
)
|
||||
+ '</tr>'
|
||||
for row in rows
|
||||
)
|
||||
table_class = "scope-table scope-table--task" if has_task_column else "scope-table"
|
||||
@@ -3133,7 +3203,7 @@ def render_main_html(data: dict) -> str:
|
||||
.hero h1{font-size:24px;line-height:1.15;margin:0 0 4px;border-bottom:1px solid #111;padding-bottom:4px}.hero .byline{float:right;margin-top:-31px;font-size:15px}
|
||||
.meta{clear:both;display:grid;grid-template-columns:148px minmax(0,1fr);gap:4px 18px;padding:34px 0 24px;border-bottom:1px solid #111;font-size:17px}.meta dt{color:#333}.meta dd{margin:0;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace}
|
||||
.front-scope{padding:0 0 3mm;border-bottom:1px solid var(--line-soft)}.front-scope h2{font-size:24px;line-height:1.15;margin:0 0 3mm}.front-scope p{font-size:16px}
|
||||
.scope-table-wrap{margin:3mm 0;overflow:hidden}.scope-table{width:100%;border-collapse:collapse;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px;line-height:1.22}.scope-table th,.scope-table td{border:0;border-bottom:1px solid #888;padding:4px 7px;text-align:left;vertical-align:top}.scope-table th{border-bottom:2px solid #555;background:#f3f3f3;font-weight:700}.scope-table th:first-child,.scope-table td:first-child{width:12%}.scope-table th:last-child,.scope-table td:last-child{width:34%}.scope-table--task th:first-child,.scope-table--task td:first-child{width:9%}.scope-table--task th:nth-child(2),.scope-table--task td:nth-child(2){width:14%}.scope-table--task th:last-child,.scope-table--task td:last-child{width:29%}.scope-table tr.scope-row--key td{font-weight:700;background:#f4f4f4;border-top:2px solid #111;border-bottom:2px solid #111}.scope-table tr.scope-row--key td:first-child{border-left:2px solid #111}.scope-table tr.scope-row--key td:last-child{border-right:2px solid #111}.scope-table td p{font:inherit;margin:0}.scope-table code{font-size:inherit}
|
||||
.scope-table-wrap{margin:3mm 0;overflow:hidden}.scope-table{width:100%;border-collapse:collapse;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px;line-height:1.22}.scope-table th,.scope-table td{border:0;border-bottom:1px solid #888;padding:4px 7px;text-align:left;vertical-align:top}.scope-table th{border-bottom:2px solid #555;background:#f3f3f3;font-weight:700}.scope-table th:first-child,.scope-table td:first-child{width:7%}.scope-table--task th:nth-child(2),.scope-table--task td:nth-child(2){width:11%}.scope-table--task th:nth-child(4),.scope-table--task td:nth-child(4){width:19%}.scope-table--task th:nth-child(5),.scope-table--task td:nth-child(5){width:14%}.scope-table--task th:nth-child(6),.scope-table--task td:nth-child(6){width:10%}.scope-table tr.scope-row--key td{font-weight:700;background:#f4f4f4;border-top:2px solid #111;border-bottom:2px solid #111}.scope-table tr.scope-row--key td:first-child{border-left:2px solid #111}.scope-table tr.scope-row--key td:last-child{border-right:2px solid #111}.scope-table td p{font:inherit;margin:0}.scope-table code{font-size:inherit}.scope-status{display:inline-block;white-space:nowrap;border:1px solid #aaa;border-radius:2px;padding:1px 4px;font-weight:600;background:#f5f5f5}.scope-status[data-status="in-progress"]{border-color:#307da1;background:#eaf5fa;color:#174f69}.scope-status[data-status="ready"]{border-color:#4c7d56;background:#edf6ee;color:#295b33}.scope-status[data-status="draft"],.scope-status[data-status="review"]{border-color:#9a7932;background:#faf4e5;color:#694f16}.scope-status[data-status="blocked"]{border-color:#9c4b4b;background:#faeded;color:#712f2f}
|
||||
.card-section h2{font-size:24px;line-height:1.15;margin:0 0 4mm}.section-heading{display:flex;align-items:flex-start;gap:0}.section-heading .section-index{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:23px;margin-right:12mm;font-weight:400}.section-heading .section-title{min-width:0}.task-identity{margin-left:auto;padding-left:22px;text-align:right;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;line-height:1.25;color:#555;white-space:nowrap}.task-identity strong,.task-identity-name,.task-identity code{display:block}.task-identity strong{font-size:12px;color:#111}.task-identity-name{max-width:240px;overflow:hidden;text-overflow:ellipsis}.task-identity code{font-size:9px;color:#777}
|
||||
p{font-size:16px;margin:0 0 3mm}.pdf-main ul{font-size:16px;margin:2mm 0 3mm 7mm;padding:0}.pdf-main li{margin:1.2mm 0}
|
||||
.pdf-margin{position:absolute;top:34mm;width:18mm;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:5px;line-height:1.12;color:#333;max-height:245mm;overflow:hidden}.pdf-margin.left{left:2mm;text-align:left}.pdf-margin.right{right:2mm;text-align:left}
|
||||
@@ -7182,6 +7252,11 @@ def react_card_model(data: dict) -> dict:
|
||||
)
|
||||
scope = data.get("front_page_scope", {}) or {}
|
||||
scope_table = scope.get("scope_table", {}) or {}
|
||||
scope_rows = scope_table.get("rows", []) or []
|
||||
scope_columns = scope_table_columns(scope_rows)
|
||||
scope_headers = list(scope_table.get("headers", []))
|
||||
if len(scope_headers) != len(scope_columns):
|
||||
scope_headers = scope_table_default_headers(scope_columns)
|
||||
front = {
|
||||
"header_html": render_page_header_html(data, 1, total_pages),
|
||||
"left_margin_html": front_left,
|
||||
@@ -7194,7 +7269,8 @@ def react_card_model(data: dict) -> dict:
|
||||
"scope_html": render_latex_fragment_html(
|
||||
str(scope.get("scope_content_tex", "")), equation_numbers, figure_numbers
|
||||
),
|
||||
"scope_headers": list(scope_table.get("headers", [])),
|
||||
"scope_columns": scope_columns,
|
||||
"scope_headers": scope_headers,
|
||||
"scope_rows": [
|
||||
{
|
||||
"chapter": str(row.get("chapter", "")),
|
||||
@@ -7203,9 +7279,12 @@ def react_card_model(data: dict) -> dict:
|
||||
str(row.get("idea_tex", row.get("idea", ""))), equation_numbers, figure_numbers
|
||||
),
|
||||
"priority": str(row.get("priority", "")),
|
||||
"status": str(row.get("status", "")),
|
||||
"status_label": scope_task_status_label(row.get("status", "")),
|
||||
"version": scope_task_version(row.get("version", "")),
|
||||
"key": bool(row.get("key", str(row.get("task", "")).lower() == "task04")),
|
||||
}
|
||||
for row in scope_table.get("rows", []) or []
|
||||
for row in scope_rows
|
||||
],
|
||||
}
|
||||
section_models: list[dict] = []
|
||||
|
||||
Reference in New Issue
Block a user