11#!/usr/bin/env python3
22"""Export copydeck error messages to CSV for Google Sheets review.
33
4+ Produces the column layout the learning team works with, so the export can seed
5+ (or refresh) their review sheet directly:
6+
7+ ID, Error_type, Link to demo / code, Has been reviewed by learning team?,
8+ variant_index, if_condition, Title, Summary, Why, Step_1 .. Step_5
9+
10+ * ID - stable 1-based row number
11+ * Link to demo / code - deep link to the matching demo example,
12+ built from docs/demo-examples.js
13+ * Has been reviewed ... - defaults to FALSE (reviewers flip to TRUE)
14+
15+ The reviewed CSV is fed back in via import_copydeck_csv.py, which only applies
16+ rows marked TRUE by default.
17+
418Usage:
519 python scripts/export_copydeck_csv.py
620 python scripts/export_copydeck_csv.py --copydeck copydecks/fr/copydeck.json --out review_fr.csv
923import argparse
1024import csv
1125import json
26+ import re
1227import sys
1328from pathlib import Path
1429
15- COLUMNS = ["error_type" , "variant_index" , "if_condition" , "title" , "summary" , "why" ,
16- "step_1" , "step_2" , "step_3" , "step_4" , "step_5" ]
30+ COLUMNS = ["ID" , "Error_type" , "Link to demo / code" , "Has been reviewed by learning team?" ,
31+ "variant_index" , "if_condition" , "Title" , "Summary" , "Why" ,
32+ "Step_1" , "Step_2" , "Step_3" , "Step_4" , "Step_5" ]
33+
34+ DEFAULT_DEMO_BASE_URL = "https://raspberrypifoundation.github.io/python-friendly-error-messages"
35+
36+
37+ def slugify (title : str ) -> str :
38+ """Match docs/index.html getRowId(): lowercase, non-alphanumeric runs -> '-', trim '-'."""
39+ return re .sub (r"[^a-z0-9]+" , "-" , title .lower ()).strip ("-" )
1740
1841
19- def export (copydeck_path : Path , out_path : Path ) -> None :
42+ def load_demo_index (demo_path : Path ) -> dict :
43+ """Map expectedVariantId -> (example_number, demo_title) by reading demo-examples.js in order.
44+
45+ The demo page anchors each example as `example-{position}-{slug(demo_title)}`, so we need the
46+ example's display position and its (demo-specific) title, keyed by the variant it demonstrates.
47+ """
48+ text = demo_path .read_text (encoding = "utf-8" )
49+ pairs = re .findall (
50+ r'title:\s*"((?:[^"\\]|\\.)*)".*?expectedVariantId:\s*"([^"]+)"' ,
51+ text ,
52+ re .S ,
53+ )
54+ index = {}
55+ for position , (raw_title , variant_id ) in enumerate (pairs , start = 1 ):
56+ title = json .loads (f'"{ raw_title } "' ) # decode any JSON string escapes
57+ index [variant_id ] = (position , title )
58+ return index
59+
60+
61+ def export (copydeck_path : Path , out_path : Path , demo_path : Path , demo_base_url : str ) -> None :
2062 with open (copydeck_path ) as f :
2163 data = json .load (f )
2264
65+ demo_index = load_demo_index (demo_path ) if demo_path and demo_path .exists () else {}
66+ if not demo_index :
67+ print (f"Note: no demo examples found at { demo_path } ; 'Link to demo / code' will be blank." ,
68+ file = sys .stderr )
69+
2370 rows = []
71+ row_id = 0
72+ missing_links = 0
2473 for error_type , error_def in data ["errors" ].items ():
2574 for i , variant in enumerate (error_def ["variants" ]):
75+ row_id += 1
76+ variant_id = f"{ error_type } /variants/{ i } "
77+
78+ link = ""
79+ if variant_id in demo_index :
80+ position , demo_title = demo_index [variant_id ]
81+ link = f"{ demo_base_url } /#example-{ position } -{ slugify (demo_title )} "
82+ elif demo_index :
83+ missing_links += 1
84+
2685 steps = variant .get ("steps" , [])
2786 if_condition = variant .get ("if" )
2887 row = {
29- "error_type" : error_type ,
88+ "ID" : row_id ,
89+ "Error_type" : error_type ,
90+ "Link to demo / code" : link ,
91+ "Has been reviewed by learning team?" : "FALSE" ,
3092 "variant_index" : i ,
3193 "if_condition" : json .dumps (if_condition ) if if_condition else "" ,
32- "title " : variant .get ("title" , "" ),
33- "summary " : variant .get ("summary" , "" ),
34- "why " : variant .get ("why" , "" ),
94+ "Title " : variant .get ("title" , "" ),
95+ "Summary " : variant .get ("summary" , "" ),
96+ "Why " : variant .get ("why" , "" ),
3597 }
3698 for j in range (1 , 6 ):
37- row [f"step_ { j } " ] = steps [j - 1 ] if j - 1 < len (steps ) else ""
99+ row [f"Step_ { j } " ] = steps [j - 1 ] if j - 1 < len (steps ) else ""
38100 rows .append (row )
39101
40102 with open (out_path , "w" , newline = "" , encoding = "utf-8" ) as f :
@@ -43,6 +105,9 @@ def export(copydeck_path: Path, out_path: Path) -> None:
43105 writer .writerows (rows )
44106
45107 print (f"Exported { len (rows )} variants to { out_path } " )
108+ if missing_links :
109+ print (f"Warning: { missing_links } variant(s) had no matching demo example; their link is blank." ,
110+ file = sys .stderr )
46111
47112
48113def main ():
@@ -51,14 +116,18 @@ def main():
51116 help = "Path to source copydeck JSON (default: copydecks/en/copydeck.json)" )
52117 parser .add_argument ("--out" , default = "copydeck_review.csv" ,
53118 help = "Output CSV path (default: copydeck_review.csv)" )
119+ parser .add_argument ("--demo" , default = "docs/demo-examples.js" ,
120+ help = "Path to demo-examples.js used to build demo links (default: docs/demo-examples.js)" )
121+ parser .add_argument ("--demo-base-url" , default = DEFAULT_DEMO_BASE_URL ,
122+ help = "Base URL of the published demo (default: the project's GitHub Pages site)" )
54123 args = parser .parse_args ()
55124
56125 copydeck_path = Path (args .copydeck )
57126 if not copydeck_path .exists ():
58127 print (f"Error: { copydeck_path } not found" , file = sys .stderr )
59128 sys .exit (1 )
60129
61- export (copydeck_path , Path (args .out ))
130+ export (copydeck_path , Path (args .out ), Path ( args . demo ), args . demo_base_url . rstrip ( "/" ) )
62131
63132
64133if __name__ == "__main__" :
0 commit comments