22"""Import revised copy from a reviewed CSV back into a copydeck JSON.
33
44Reads 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
910The 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
1415Review 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
2021Usage:
2122 python scripts/import_copydeck_csv.py --csv copydeck-import_en.csv
2526import argparse
2627import csv
2728import json
29+ import re
2830import sys
2931from pathlib import Path
3032
3133# Columns that must be present (after normalisation) for a row to be a header.
3234REQUIRED = {"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
3739def _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
134145def 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