feat: track task scope status and version
This commit is contained in:
@@ -45,6 +45,41 @@ której powstaje `https://<url_host_uuid>/<doc_uuid>`.
|
|||||||
`show_repository_qr: true` wymaga `repository_url`. Adres podglądu `localhost`
|
`show_repository_qr: true` wymaga `repository_url`. Adres podglądu `localhost`
|
||||||
nie powinien być publikowany na karcie.
|
nie powinien być publikowany na karcie.
|
||||||
|
|
||||||
|
### Status i wersja tasków w zakresie karty
|
||||||
|
|
||||||
|
Wiersze `front_page_scope.scope_table.rows[]` mogą opisywać stan opracowania
|
||||||
|
pojedynczego tasku. JSON przechowuje stabilny angielski klucz `status`, a
|
||||||
|
renderer pokazuje jego krótką polską etykietę w HTML i PDF.
|
||||||
|
|
||||||
|
| Klucz | Etykieta | Znaczenie |
|
||||||
|
|---|---|---|
|
||||||
|
| `not-started` | brak | task nie został jeszcze opracowany |
|
||||||
|
| `draft` | robocze | istnieje niepełna wersja robocza |
|
||||||
|
| `in-progress` | w trakcie | task jest aktualnie opracowywany |
|
||||||
|
| `review` | do przeglądu | materiał czeka na sprawdzenie |
|
||||||
|
| `ready` | opracowane | materiał jest sprawdzony i gotowy do użycia |
|
||||||
|
| `blocked` | zablokowane | dalsza praca wymaga rozwiązania zależności |
|
||||||
|
|
||||||
|
Pole `version` ma format `vNN.NN`, na przykład `v00.01`. Drugi człon
|
||||||
|
zwiększamy przy zaakceptowanej rewizji treści lub materiałów tasku. Pierwszy
|
||||||
|
człon zwiększamy, gdy zmienia się kontrakt dydaktyczny albo wymagany rezultat;
|
||||||
|
po takim zwiększeniu drugi człon zaczyna się od `00`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"chapter": "5.4",
|
||||||
|
"task": "Task04",
|
||||||
|
"idea_tex": "arytmetyka adresów, alokator",
|
||||||
|
"priority": "obowiązkowe",
|
||||||
|
"status": "in-progress",
|
||||||
|
"version": "v00.01",
|
||||||
|
"key": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Status i wersję zmieniamy wyłącznie w źródłowym `card_source.json`; pliki
|
||||||
|
HTML, TeX i PDF pozostają artefaktami generatora.
|
||||||
|
|
||||||
### Efekty i kryteria
|
### Efekty i kryteria
|
||||||
|
|
||||||
- `educational_requirements` — wymagania `WE`;
|
- `educational_requirements` — wymagania `WE`;
|
||||||
|
|||||||
+23
-8
@@ -1775,7 +1775,27 @@ function NeovimPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ScopeTableCell({ column, row }: { column: string; row: any }) {
|
||||||
|
if (column === "chapter" || column === "task" || column === "version") {
|
||||||
|
return <td><code>{row[column]}</code></td>;
|
||||||
|
}
|
||||||
|
if (column === "idea") {
|
||||||
|
return <td dangerouslySetInnerHTML={{ __html: row.idea_html }} />;
|
||||||
|
}
|
||||||
|
if (column === "status") {
|
||||||
|
return (
|
||||||
|
<td>
|
||||||
|
<span className="scope-status" data-status={row.status} title={row.status}>
|
||||||
|
{row.status_label}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <td>{row[column]}</td>;
|
||||||
|
}
|
||||||
|
|
||||||
function FrontPage({ front }: { front: any }) {
|
function FrontPage({ front }: { front: any }) {
|
||||||
|
const scopeColumns: string[] = front.scope_columns ?? ["chapter", "task", "idea", "priority"];
|
||||||
return (
|
return (
|
||||||
<Sheet
|
<Sheet
|
||||||
header={front.header_html}
|
header={front.header_html}
|
||||||
@@ -1805,14 +1825,9 @@ function FrontPage({ front }: { front: any }) {
|
|||||||
key={`${row.chapter}-${row.task}`}
|
key={`${row.chapter}-${row.task}`}
|
||||||
className={row.key ? "scope-row--key" : ""}
|
className={row.key ? "scope-row--key" : ""}
|
||||||
>
|
>
|
||||||
<td>
|
{scopeColumns.map(column => (
|
||||||
<code>{row.chapter}</code>
|
<ScopeTableCell key={column} column={column} row={row} />
|
||||||
</td>
|
))}
|
||||||
<td>
|
|
||||||
<code>{row.task}</code>
|
|
||||||
</td>
|
|
||||||
<td dangerouslySetInnerHTML={{ __html: row.idea_html }} />
|
|
||||||
<td>{row.priority}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -42,6 +42,9 @@
|
|||||||
"title_block": {
|
"title_block": {
|
||||||
"$ref": "#/$defs/titleBlock"
|
"$ref": "#/$defs/titleBlock"
|
||||||
},
|
},
|
||||||
|
"front_page_scope": {
|
||||||
|
"$ref": "#/$defs/frontPageScope"
|
||||||
|
},
|
||||||
"dictionary_refs": {
|
"dictionary_refs": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -355,6 +358,53 @@
|
|||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"frontPageScope": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["title", "content_tex"],
|
||||||
|
"properties": {
|
||||||
|
"title": { "type": "string", "minLength": 1 },
|
||||||
|
"content_tex": { "type": "string" },
|
||||||
|
"scope_title": { "type": "string", "minLength": 1 },
|
||||||
|
"scope_content_tex": { "type": "string" },
|
||||||
|
"scope_footer_tex": { "type": "string" },
|
||||||
|
"scope_table": { "$ref": "#/$defs/scopeTable" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"scopeTable": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["rows"],
|
||||||
|
"properties": {
|
||||||
|
"headers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string", "minLength": 1 }
|
||||||
|
},
|
||||||
|
"rows": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/scopeTaskRow" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"scopeTaskRow": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["chapter", "idea_tex", "priority"],
|
||||||
|
"properties": {
|
||||||
|
"chapter": { "type": "string", "minLength": 1 },
|
||||||
|
"task": { "type": "string" },
|
||||||
|
"idea_tex": { "type": "string" },
|
||||||
|
"priority": { "type": "string" },
|
||||||
|
"status": {
|
||||||
|
"enum": ["not-started", "draft", "in-progress", "review", "ready", "blocked"]
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^v[0-9]{2}\\.[0-9]{2}$"
|
||||||
|
},
|
||||||
|
"key": { "type": "boolean" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"sideMarginLayout": {
|
"sideMarginLayout": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["columns"],
|
"required": ["columns"],
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ SCHEMAS = ROOT / "schemas"
|
|||||||
TEMPLATES = ROOT / "templates"
|
TEMPLATES = ROOT / "templates"
|
||||||
EXAMPLE_CARD = ROOT / "examples" / "card"
|
EXAMPLE_CARD = ROOT / "examples" / "card"
|
||||||
EXAMPLE_SERIES = ROOT / "examples" / "series" / "series.json"
|
EXAMPLE_SERIES = ROOT / "examples" / "series" / "series.json"
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from tools.render_card import ( # noqa: E402
|
||||||
|
SCOPE_TASK_STATUS_LABELS,
|
||||||
|
scope_table_columns,
|
||||||
|
scope_task_status_label,
|
||||||
|
scope_task_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_json(path: Path) -> dict:
|
def load_json(path: Path) -> dict:
|
||||||
@@ -47,6 +55,51 @@ def check_json_files() -> None:
|
|||||||
assert series.get("cards")
|
assert series.get("cards")
|
||||||
|
|
||||||
|
|
||||||
|
def check_scope_task_metadata() -> None:
|
||||||
|
rows = [
|
||||||
|
{
|
||||||
|
"chapter": "5.4",
|
||||||
|
"task": "Task04",
|
||||||
|
"idea_tex": "alokator",
|
||||||
|
"priority": "obowiązkowe",
|
||||||
|
"status": "in-progress",
|
||||||
|
"version": "v00.01",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert scope_table_columns(rows) == [
|
||||||
|
"chapter",
|
||||||
|
"task",
|
||||||
|
"idea",
|
||||||
|
"priority",
|
||||||
|
"status",
|
||||||
|
"version",
|
||||||
|
]
|
||||||
|
assert SCOPE_TASK_STATUS_LABELS == {
|
||||||
|
"not-started": "brak",
|
||||||
|
"draft": "robocze",
|
||||||
|
"in-progress": "w trakcie",
|
||||||
|
"review": "do przeglądu",
|
||||||
|
"ready": "opracowane",
|
||||||
|
"blocked": "zablokowane",
|
||||||
|
}
|
||||||
|
assert scope_task_status_label("in-progress") == "w trakcie"
|
||||||
|
assert scope_task_version("v00.01") == "v00.01"
|
||||||
|
for invalid in ("started", "done"):
|
||||||
|
try:
|
||||||
|
scope_task_status_label(invalid)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise AssertionError(f"status {invalid!r} nie został odrzucony")
|
||||||
|
for invalid in ("0.1", "v0.01", "v00.1"):
|
||||||
|
try:
|
||||||
|
scope_task_version(invalid)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise AssertionError(f"wersja {invalid!r} nie została odrzucona")
|
||||||
|
|
||||||
|
|
||||||
def check_optional_json_schema() -> None:
|
def check_optional_json_schema() -> None:
|
||||||
try:
|
try:
|
||||||
import jsonschema
|
import jsonschema
|
||||||
@@ -269,6 +322,7 @@ def check_external_references() -> None:
|
|||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
check_json_files()
|
check_json_files()
|
||||||
|
check_scope_task_metadata()
|
||||||
check_optional_json_schema()
|
check_optional_json_schema()
|
||||||
check_render()
|
check_render()
|
||||||
check_section_assets()
|
check_section_assets()
|
||||||
|
|||||||
+102
-23
@@ -40,6 +40,15 @@ UUID_RE = re.compile(
|
|||||||
UUID_PATH_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}$"
|
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:
|
def configure_card_root(card_root: Path) -> None:
|
||||||
@@ -850,25 +859,72 @@ def render_front_matter(card: dict) -> list[str]:
|
|||||||
return lines
|
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]:
|
def render_front_scope_table_tex(scope: dict) -> list[str]:
|
||||||
table = scope.get("scope_table") or {}
|
table = scope.get("scope_table") or {}
|
||||||
rows = table.get("rows") or []
|
rows = table.get("rows") or []
|
||||||
if not rows:
|
if not rows:
|
||||||
return []
|
return []
|
||||||
has_task_column = any(str(row.get("task", "")).strip() for row in rows)
|
columns = scope_table_columns(rows)
|
||||||
default_headers = (
|
has_task_column = "task" in columns
|
||||||
["K&R", "Numer tasku", "Najważniejsza idea", "Priorytet"]
|
has_status_column = "status" in columns
|
||||||
if has_task_column
|
has_version_column = "version" in columns
|
||||||
else ["K&R", "Najważniejsza idea", "Priorytet"]
|
default_headers = scope_table_default_headers(columns)
|
||||||
)
|
|
||||||
headers = table.get("headers") or default_headers
|
headers = table.get("headers") or default_headers
|
||||||
if len(headers) != len(default_headers):
|
if len(headers) != len(default_headers):
|
||||||
headers = default_headers
|
headers = default_headers
|
||||||
column_spec = (
|
column_parts = [r">{\ttfamily\raggedright\arraybackslash}p{0.85cm}"]
|
||||||
r"@{}>{\ttfamily}p{1.05cm}>{\ttfamily}p{1.55cm}X>{\raggedright\arraybackslash}p{3.25cm}@{}"
|
if has_task_column:
|
||||||
if has_task_column
|
column_parts.append(r">{\ttfamily\raggedright\arraybackslash}p{1.45cm}")
|
||||||
else r"@{}>{\ttfamily}p{1.25cm}X>{\raggedright\arraybackslash}p{3.55cm}@{}"
|
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 = [
|
lines = [
|
||||||
r"\vspace{0.55em}",
|
r"\vspace{0.55em}",
|
||||||
rf"\small\begin{{tabularx}}{{\textwidth}}{{{column_spec}}}",
|
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:
|
if has_task_column:
|
||||||
cells.append(rf"\texttt{{{tex_escape(row.get('task', ''))}}}")
|
cells.append(rf"\texttt{{{tex_escape(row.get('task', ''))}}}")
|
||||||
cells.extend([row.get("idea_tex", ""), tex_escape(row.get("priority", ""))])
|
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 = []
|
framed_cells = []
|
||||||
for index, cell in enumerate(cells):
|
for index, cell in enumerate(cells):
|
||||||
alignment = "|l|" if index == 0 else "l|"
|
alignment = "|l|" if index == 0 else "l|"
|
||||||
@@ -3080,23 +3141,32 @@ def render_main_html(data: dict) -> str:
|
|||||||
if scope.get("scope_title"):
|
if scope.get("scope_title"):
|
||||||
table = scope.get("scope_table") or {}
|
table = scope.get("scope_table") or {}
|
||||||
rows = table.get("rows") or []
|
rows = table.get("rows") or []
|
||||||
has_task_column = any(str(row.get("task", "")).strip() for row in rows)
|
columns = scope_table_columns(rows)
|
||||||
default_headers = (
|
has_task_column = "task" in columns
|
||||||
["K&R", "Numer tasku", "Najważniejsza idea", "Priorytet"]
|
has_status_column = "status" in columns
|
||||||
if has_task_column
|
has_version_column = "version" in columns
|
||||||
else ["K&R", "Najważniejsza idea", "Priorytet"]
|
default_headers = scope_table_default_headers(columns)
|
||||||
)
|
|
||||||
headers = table.get("headers") or default_headers
|
headers = table.get("headers") or default_headers
|
||||||
if len(headers) != len(default_headers):
|
if len(headers) != len(default_headers):
|
||||||
headers = default_headers
|
headers = default_headers
|
||||||
scope_table_html = ""
|
scope_table_html = ""
|
||||||
if rows:
|
if rows:
|
||||||
table_rows = "".join(
|
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("chapter", ""))}</code></td>'
|
||||||
+ (f'<td><code>{html_escape(row.get("task", ""))}</code></td>' if has_task_column else "")
|
+ (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>{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
|
for row in rows
|
||||||
)
|
)
|
||||||
table_class = "scope-table scope-table--task" if has_task_column else "scope-table"
|
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}
|
.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}
|
.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}
|
.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}
|
.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}
|
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}
|
.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 = data.get("front_page_scope", {}) or {}
|
||||||
scope_table = scope.get("scope_table", {}) 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 = {
|
front = {
|
||||||
"header_html": render_page_header_html(data, 1, total_pages),
|
"header_html": render_page_header_html(data, 1, total_pages),
|
||||||
"left_margin_html": front_left,
|
"left_margin_html": front_left,
|
||||||
@@ -7194,7 +7269,8 @@ def react_card_model(data: dict) -> dict:
|
|||||||
"scope_html": render_latex_fragment_html(
|
"scope_html": render_latex_fragment_html(
|
||||||
str(scope.get("scope_content_tex", "")), equation_numbers, figure_numbers
|
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": [
|
"scope_rows": [
|
||||||
{
|
{
|
||||||
"chapter": str(row.get("chapter", "")),
|
"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
|
str(row.get("idea_tex", row.get("idea", ""))), equation_numbers, figure_numbers
|
||||||
),
|
),
|
||||||
"priority": str(row.get("priority", "")),
|
"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")),
|
"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] = []
|
section_models: list[dict] = []
|
||||||
|
|||||||
Reference in New Issue
Block a user