Skip to content

Commit d166892

Browse files
committed
feat: switch to using lastReviewed date in copydeck export/import scripts
1 parent 20dd6b6 commit d166892

2 files changed

Lines changed: 37 additions & 25 deletions

File tree

scripts/export_copydeck_csv.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@
44
Produces the column layout the learning team works with, so the export can seed
55
(or refresh) their review sheet directly:
66
7-
ID, Error_type, Link to demo / code, Has been reviewed by learning team?,
7+
ID, Error_type, Link to demo / code, Last reviewed (YYYY-MM-DD),
88
variant_index, if_condition, Title, Summary, Why, Step_1 .. Step_5
99
1010
* ID - stable 1-based row number
1111
* Link to demo / code - deep link to the matching demo example,
1212
built from docs/demo-examples.js
13-
* Has been reviewed ... - defaults to FALSE (reviewers flip to TRUE)
13+
* Last reviewed (YYYY-MM-DD) - the variant's _lastReviewed date, if set;
14+
reviewers fill/update it to mark a row reviewed
1415
15-
The reviewed CSV is fed back in via import_copydeck_csv.py, which only applies
16-
rows marked TRUE by default.
16+
The reviewed CSV is fed back in via import_copydeck_csv.py, which by default only
17+
applies rows that carry a Last reviewed date.
1718
1819
Usage:
1920
python scripts/export_copydeck_csv.py
@@ -27,7 +28,7 @@
2728
import sys
2829
from pathlib import Path
2930

30-
COLUMNS = ["ID", "Error_type", "Link to demo / code", "Has been reviewed by learning team?",
31+
COLUMNS = ["ID", "Error_type", "Link to demo / code", "Last reviewed (YYYY-MM-DD)",
3132
"variant_index", "if_condition", "Title", "Summary", "Why",
3233
"Step_1", "Step_2", "Step_3", "Step_4", "Step_5"]
3334

@@ -88,7 +89,7 @@ def export(copydeck_path: Path, out_path: Path, demo_path: Path, demo_base_url:
8889
"ID": row_id,
8990
"Error_type": error_type,
9091
"Link to demo / code": link,
91-
"Has been reviewed by learning team?": "FALSE",
92+
"Last reviewed (YYYY-MM-DD)": variant.get("_lastReviewed", ""),
9293
"variant_index": i,
9394
"if_condition": json.dumps(if_condition) if if_condition else "",
9495
"Title": variant.get("title", ""),

scripts/import_copydeck_csv.py

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,21 @@
22
"""Import revised copy from a reviewed CSV back into a copydeck JSON.
33
44
Reads a reviewed CSV (originally produced by export_copydeck_csv.py, then edited
5-
in a spreadsheet) and updates title/summary/why/steps for each variant, writing
6-
the result to a JSON file. The original if_condition and any other metadata are
7-
preserved from the source copydeck - reviewers cannot change match logic.
5+
in a spreadsheet) and updates title/summary/why/steps for each variant, plus the
6+
_lastReviewed date when a "Last reviewed" column is present, writing the result to
7+
a JSON file. The original if_condition and any other metadata are preserved from
8+
the source copydeck - reviewers cannot change match logic.
89
910
The reader tolerates the mutations a spreadsheet round-trip introduces:
1011
* blank rows above the header (eg. a frozen spacer row)
1112
* header capitalisation ("Title", "Error_type", "Step_1", ...)
1213
* extra columns added by reviewers (ID, links, status flags), in any order
1314
1415
Review gating:
15-
If the CSV has a "reviewed" column (any header containing "reviewed"), only
16-
rows marked TRUE are applied by default; unreviewed rows keep their current
17-
copydeck wording. Pass --all to apply every row regardless, or --reviewed-only
18-
to force gating (errors if the column is missing).
16+
A row counts as reviewed when its "Last reviewed" column holds a date. By
17+
default only those rows are applied; dateless rows keep their current copydeck
18+
wording. Pass --all to apply every row regardless, or --reviewed-only to force
19+
gating (errors if the column is missing).
1920
2021
Usage:
2122
python scripts/import_copydeck_csv.py --csv copydeck-import_en.csv
@@ -25,13 +26,14 @@
2526
import argparse
2627
import csv
2728
import json
29+
import re
2830
import sys
2931
from pathlib import Path
3032

3133
# Columns that must be present (after normalisation) for a row to be a header.
3234
REQUIRED = {"error_type", "variant_index", "title", "summary"}
33-
# Values (normalised) treated as "reviewed = yes".
34-
TRUTHY = {"true", "yes", "y", "1", "x", "✓", "checked", "done"}
35+
# Expected shape of the _lastReviewed date (ISO YYYY-MM-DD); warned on, not enforced.
36+
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
3537

3638

3739
def _norm(s: str) -> str:
@@ -74,11 +76,12 @@ def import_csv(csv_path: Path, copydeck_path: Path, out_path: Path, mode: str) -
7476

7577
header, rows = read_rows(csv_path)
7678

77-
reviewed_col = next((h for h in header if "reviewed" in h), None)
78-
if mode == "reviewed-only" and reviewed_col is None:
79-
print("Error: --reviewed-only was set but no 'reviewed' column was found in the CSV.", file=sys.stderr)
79+
# A row is "reviewed" when its Last reviewed column carries a date.
80+
last_reviewed_col = next((h for h in header if "last" in h and "review" in h), None)
81+
if mode == "reviewed-only" and last_reviewed_col is None:
82+
print("Error: --reviewed-only was set but no 'Last reviewed' column was found in the CSV.", file=sys.stderr)
8083
sys.exit(1)
81-
gating = reviewed_col is not None and mode != "all"
84+
gating = last_reviewed_col is not None and mode != "all"
8285

8386
updated = 0
8487
skipped_unreviewed = 0
@@ -88,7 +91,7 @@ def import_csv(csv_path: Path, copydeck_path: Path, out_path: Path, mode: str) -
8891
if not error_type or not vi_raw:
8992
continue # not a real data row
9093

91-
if gating and _norm(row.get(reviewed_col, "")) not in TRUTHY:
94+
if gating and not (row.get(last_reviewed_col) or "").strip():
9295
skipped_unreviewed += 1
9396
continue
9497

@@ -118,6 +121,14 @@ def import_csv(csv_path: Path, copydeck_path: Path, out_path: Path, mode: str) -
118121
elif "steps" in variant:
119122
del variant["steps"]
120123

124+
if last_reviewed_col:
125+
last_reviewed = (row.get(last_reviewed_col) or "").strip()
126+
if last_reviewed:
127+
if not DATE_RE.match(last_reviewed):
128+
print(f"Warning: '{last_reviewed}' for {error_type}[{variant_index}] is not a YYYY-MM-DD "
129+
"date; storing it anyway", file=sys.stderr)
130+
variant["_lastReviewed"] = last_reviewed
131+
121132
updated += 1
122133

123134
with open(out_path, "w", encoding="utf-8") as f:
@@ -126,9 +137,9 @@ def import_csv(csv_path: Path, copydeck_path: Path, out_path: Path, mode: str) -
126137

127138
print(f"Applied {updated} variant(s) → {out_path}")
128139
if gating:
129-
print(f"Skipped {skipped_unreviewed} unreviewed row(s) (column '{reviewed_col}'). Pass --all to include them.")
130-
elif reviewed_col:
131-
print(f"Applied all rows; review column '{reviewed_col}' was ignored (--all).")
140+
print(f"Skipped {skipped_unreviewed} undated row(s) (column '{last_reviewed_col}'). Pass --all to include them.")
141+
elif last_reviewed_col:
142+
print(f"Applied all rows; review dates in '{last_reviewed_col}' did not gate (--all).")
132143

133144

134145
def main():
@@ -141,9 +152,9 @@ def main():
141152
help="Output JSON path (default: overwrites source copydeck)")
142153
group = parser.add_mutually_exclusive_group()
143154
group.add_argument("--all", action="store_true",
144-
help="Apply every row, ignoring any review-status column")
155+
help="Apply every row, regardless of whether it has a Last reviewed date")
145156
group.add_argument("--reviewed-only", action="store_true",
146-
help="Only apply rows whose review column is TRUE (errors if no such column)")
157+
help="Only apply rows with a Last reviewed date (errors if no such column)")
147158
args = parser.parse_args()
148159

149160
for p in [Path(args.csv), Path(args.copydeck)]:

0 commit comments

Comments
 (0)