sharanyaswarup commited on
Commit
13b211e
Β·
1 Parent(s): 8593ea0

Updated mapping manager

Browse files
Files changed (5) hide show
  1. README.md +12 -9
  2. USER_MANUAL.md +136 -0
  3. admin_rename_sf_district.py +13 -3
  4. app.py +156 -20
  5. hf_store.py +33 -19
README.md CHANGED
@@ -19,11 +19,11 @@ An automated, end-to-end data pipeline to harvest school data from the KYS UDISE
19
  2. **Intelligent Retries:** Automatically detects CAPTCHA failures and retries only missing data. The terminal UI wipes clean automatically before each operation.
20
  3. **UDISE Geo-Decoding:** For non-actual state scrapes (e.g., KVS, NVS, NAVY, IAF), it parses the UDISE code to determine the *real* State, District, and Block.
21
  4. **Automated UDISE-Tracking Math Engine:** When building the master sheet, an intelligent math engine automatically resolves renamed districts and blocks!
22
- - It cross-references newly scraped UDISE codes against a cloud-stored Salesforce Baseline (`baseline_master.parquet`).
23
  - If a district was simply renamed (e.g., EAST DISTRICT -> GANGTOK), the math engine statistically tracks the UDISE codes, proves the rename, and automatically generates a mapping rule in the cloud (`manual_district_mapping.parquet`).
24
  - For complex fractured districts, it drills down to the Block level (`manual_block_mapping.parquet`) to perfectly map schools to their old districts.
25
  5. **HuggingFace Cloud Architecture:** Syncs directly to a HuggingFace dataset organized into 3 folders:
26
- - `district_reference`: Source of truth for Salesforce district tracking (Dataset 1).
27
  - `mapping_rules`: Automated + manual district & block mapping rules (Dataset 2) & the SF Baseline Master.
28
  - `scraped_data`: Holds both `raw` per-state files and the final `mapped` master sheet.
29
  6. **Smart District Flagging (Hybrid System):** Because the math engine automatically handles all simple renames behind the scenes, the UI only flags *genuinely brand-new* or highly ambiguous districts for your manual review in the Master Sheet tab!
@@ -84,22 +84,25 @@ The core scraping workflow, all on one screen:
84
 
85
  Combines all raw state data from HuggingFace into a single mapped master Excel file, and manages flagged districts.
86
 
87
- - **State Coverage Table:** Connects to your HuggingFace dataset and displays which states are ready to build. You can delete raw data directly from here.
88
- - **Review New Districts:** Automatically detects newly scraped districts not in your Salesforce database. You can instantly map them to older Salesforce districts or rename them before building.
89
  - **Build Master Sheet:** Automatically:
90
  1. Pulls all raw complete state parquets.
91
  2. Applies geo-decoding for KVS/NVS/NAVY schools.
92
  3. Applies district back-mapping based on your rules.
93
- 4. Tags schools with their `SF_Status` (e.g., `in_sf` or `pending_sf_addition`).
94
  5. Saves the final mapped file to `scraped_data/mapped/` on HuggingFace and provides an Excel download.
95
 
96
  ---
97
 
98
  ### πŸ—ΊοΈ Tab 3: Mapping Manager
99
 
100
- A full database management dashboard to maintain your Salesforce mappings.
101
 
102
- - **SF District Reference:** Full CRUD table to view, filter, add, rename, and delete states and districts. If you rename a district here, it perfectly cascades and updates your manual mapping rules automatically!
 
 
 
103
 
104
  ---
105
 
@@ -114,7 +117,7 @@ A full database management dashboard to maintain your Salesforce mappings.
114
  If you need to make global, architectural changes to the baseline data outside of the UI, use the provided admin scripts:
115
 
116
  **`admin_rename_sf_district.py`**
117
- If an old Salesforce district name is permanently outdated and you want to rename it everywhere (becoming the new Baseline truth):
118
  ```powershell
119
  python admin_rename_sf_district.py --state "SIKKIM" --old "EAST DISTRICT (GANGTOK)" --new "GANGTOK"
120
  ```
@@ -155,7 +158,7 @@ python export_to_excel.py --state "GOA"
155
  | `output/goa_district_id_map.json` | District dropdown mapping IDs for the portal |
156
  | `output/goa_schools_by_category.json` | Raw scraped JSON arrays & API responses |
157
  | `output_excel/goa_Schools.xlsx` | Raw Excel Export (before mapping) |
158
- | `HF Dataset: district_reference/` | Dataset 1: The SF reference mapping |
159
  | `HF Dataset: mapping_rules/` | Dataset 2: District and Block mapping rules |
160
  | `HF Dataset: scraped_data/raw/{state}.parquet` | Per-state raw scraped data |
161
  | `HF Dataset: scraped_data/mapped/` | Final combined mapped master output |
 
19
  2. **Intelligent Retries:** Automatically detects CAPTCHA failures and retries only missing data. The terminal UI wipes clean automatically before each operation.
20
  3. **UDISE Geo-Decoding:** For non-actual state scrapes (e.g., KVS, NVS, NAVY, IAF), it parses the UDISE code to determine the *real* State, District, and Block.
21
  4. **Automated UDISE-Tracking Math Engine:** When building the master sheet, an intelligent math engine automatically resolves renamed districts and blocks!
22
+ - It cross-references newly scraped UDISE codes against a cloud-stored Scholarship Application Baseline (`baseline_master.parquet`).
23
  - If a district was simply renamed (e.g., EAST DISTRICT -> GANGTOK), the math engine statistically tracks the UDISE codes, proves the rename, and automatically generates a mapping rule in the cloud (`manual_district_mapping.parquet`).
24
  - For complex fractured districts, it drills down to the Block level (`manual_block_mapping.parquet`) to perfectly map schools to their old districts.
25
  5. **HuggingFace Cloud Architecture:** Syncs directly to a HuggingFace dataset organized into 3 folders:
26
+ - `district_reference`: Source of truth for Scholarship Application district tracking (Dataset 1).
27
  - `mapping_rules`: Automated + manual district & block mapping rules (Dataset 2) & the SF Baseline Master.
28
  - `scraped_data`: Holds both `raw` per-state files and the final `mapped` master sheet.
29
  6. **Smart District Flagging (Hybrid System):** Because the math engine automatically handles all simple renames behind the scenes, the UI only flags *genuinely brand-new* or highly ambiguous districts for your manual review in the Master Sheet tab!
 
84
 
85
  Combines all raw state data from HuggingFace into a single mapped master Excel file, and manages flagged districts.
86
 
87
+ - **State Coverage Table:** Connects to your HuggingFace dataset and displays which states are ready to build. You can delete raw data directly from here (which strictly auto-cleans any pending districts for that state!).
88
+ - **Review New Districts:** Automatically detects newly scraped districts not in your Scholarship Application database. You can instantly map them to older Scholarship Application districts or rename them before building.
89
  - **Build Master Sheet:** Automatically:
90
  1. Pulls all raw complete state parquets.
91
  2. Applies geo-decoding for KVS/NVS/NAVY schools.
92
  3. Applies district back-mapping based on your rules.
93
+ 4. Tags schools with their `Status` (e.g., `present` or `new districts found`).
94
  5. Saves the final mapped file to `scraped_data/mapped/` on HuggingFace and provides an Excel download.
95
 
96
  ---
97
 
98
  ### πŸ—ΊοΈ Tab 3: Mapping Manager
99
 
100
+ A full database management dashboard to maintain your Scholarship Application mappings.
101
 
102
+ - **Scholarship Application District Reference:** View and manage your master district list.
103
+ - **Click-to-Edit Rows:** Click any row in the table to instantly populate a quick-edit dropdown for updating its Status.
104
+ - **Bulk Excel Import/Export:** Expand the accordion to download the table, make bulk edits in Excel, and drag-and-drop it back to seamlessly sync with the cloud.
105
+ - **Smart CRUD Tools:** Add, Rename, and Delete districts. If you rename a district here, it perfectly cascades and updates your manual mapping rules automatically!
106
 
107
  ---
108
 
 
117
  If you need to make global, architectural changes to the baseline data outside of the UI, use the provided admin scripts:
118
 
119
  **`admin_rename_sf_district.py`**
120
+ If an old Scholarship Application district name is permanently outdated and you want to rename it everywhere (becoming the new Baseline truth):
121
  ```powershell
122
  python admin_rename_sf_district.py --state "SIKKIM" --old "EAST DISTRICT (GANGTOK)" --new "GANGTOK"
123
  ```
 
158
  | `output/goa_district_id_map.json` | District dropdown mapping IDs for the portal |
159
  | `output/goa_schools_by_category.json` | Raw scraped JSON arrays & API responses |
160
  | `output_excel/goa_Schools.xlsx` | Raw Excel Export (before mapping) |
161
+ | `HF Dataset: district_reference/` | Dataset 1: The Scholarship Application reference mapping |
162
  | `HF Dataset: mapping_rules/` | Dataset 2: District and Block mapping rules |
163
  | `HF Dataset: scraped_data/raw/{state}.parquet` | Per-state raw scraped data |
164
  | `HF Dataset: scraped_data/mapped/` | Final combined mapped master output |
USER_MANUAL.md ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🏫 School Data Fetcher - Complete User Manual
2
+
3
+ Welcome to the **School Data Fetcher**. This application is designed to scrape, clean, geo-decode, and map school data from UDISE+ and prepare it for seamless integration into your Scholarship Application database.
4
+
5
+ This manual provides a detailed, step-by-step guide to using every feature of the app.
6
+
7
+ ---
8
+
9
+ ## πŸ“‘ Table of Contents
10
+ 1. [Overview of the Workflow](#1-overview-of-the-workflow)
11
+ 2. [Tab 1: πŸ” Scraper](#2-tab-1--scraper)
12
+ 3. [Tab 2: πŸ“‹ Master Sheet (Builder)](#3-tab-2--master-sheet-builder)
13
+ 4. [Tab 3: πŸ—ΊοΈ Mapping Manager](#4-tab-3--mapping-manager)
14
+ 5. [Tab 4: πŸ“₯ Download History](#5-tab-4--download-history)
15
+ 6. [Best Practices & Tips](#6-best-practices--tips)
16
+
17
+ ---
18
+
19
+ ## 1. Overview of the Workflow
20
+
21
+ The standard operational workflow for using this app is:
22
+ 1. **Scrape Data:** Select a state and pull the latest data from the government portal.
23
+ 2. **Handle Missing Data:** Retry any failed records to ensure 100% data capture.
24
+ 3. **Check for New Districts:** Go to the Master Sheet tab to see if the state created any new districts this year.
25
+ 4. **Update Reference:** If new districts exist, the scraper will have already automatically added them to the Mapping Manager (Tab 3) as `new districts found`. You just need to add them to your actual Scholarship Application system and update their status to `present` in Tab 3.
26
+ 5. **Map & Build:** Create rules to map old/split districts to new ones, then build the final Excel file.
27
+
28
+ ---
29
+
30
+ ## 2. Tab 1: πŸ” Scraper
31
+
32
+ This is where you fetch the raw data directly from UDISE+.
33
+
34
+ ### πŸ“ Location & πŸ“š School Types
35
+ 1. **Select a State:** Choose the state (or Pan-India organization like KVS/NVS) from the dropdown.
36
+ 2. **Select Categories:** By default, all 7 categories are selected. It is highly recommended to leave all of them selected unless you only need a very specific subset of data.
37
+
38
+ ### β–Ά Start Scraping
39
+ Click **Start Scraping** to begin the process.
40
+ - The app will securely navigate the government portal and bypass captchas.
41
+ - You will see live progress updates (e.g., `[14/50] Processing...`).
42
+ - Once finished, the app will instantly **Auto-Push** the raw data to your secure cloud storage on HuggingFace.
43
+
44
+ ### ↻ Fix Missing Data
45
+ Sometimes, the government website times out or fails a captcha repeatedly.
46
+ - If the final status says **"⚠️ Some Records Are Incomplete"**, the **Fix Missing Data** button will appear.
47
+ - Click it. The app will *only* retry the specific schools that failed, saving you from having to restart the entire scrape.
48
+ - Keep clicking it until the status turns green (**βœ… All Data Retrieved!**).
49
+
50
+ ### βš™οΈ Advanced Settings
51
+ - **Retry attempts:** You can increase the number of times the scraper tries to solve a captcha per school (default is 5). Higher numbers are more thorough but slower.
52
+
53
+ ---
54
+
55
+ ## 3. Tab 2: πŸ“‹ Master Sheet (Builder)
56
+
57
+ Once you have successfully scraped a state, you must process the raw data into a clean "Master Sheet".
58
+
59
+ ### The Review Process
60
+ When you open this tab and select your newly scraped state, the app will automatically compare the raw data against your known **Scholarship Application District Reference**.
61
+
62
+ You will see one of two things:
63
+
64
+ #### Scenario A: "βœ… No flagged districts β€” all clear!"
65
+ This means the scraped state hasn't added any new districts, and all data perfectly matches what your Scholarship Application already knows.
66
+ - You can immediately click **Build Master Sheet**.
67
+
68
+ #### Scenario B: The Flagged Districts Table Appears
69
+ This means the scraped state contains districts that your Scholarship Application doesn't know about yet. A red table will appear showing you the anomalous districts.
70
+
71
+ **How to handle new districts:**
72
+ 1. **Valid New District:** If the district is genuinely new (e.g., the state split a district in two), you must first add it to your Scholarship Application system. Then, go to **Tab 3: Mapping Manager**, mark it as `present`, and come back here.
73
+ 2. **Name Mismatch:** If the district is just spelled differently (e.g., "PONDICHERRY" vs "PUDUCHERRY"), use the **Create Mapping Rule** section below the table to permanently teach the app how to map the new spelling to your expected spelling.
74
+
75
+ ### Creating Mapping Rules
76
+ If you need to fix a name mismatch or map a split block to an old district:
77
+ 1. Select the **Raw District Name** (how it appears in the scrape).
78
+ 2. Select the **Target District Name** (how it should appear in your Scholarship Application).
79
+ 3. (Optional) Select a specific Block if the rule should only apply to that block.
80
+ 4. Click **Add Rule**. The app will remember this rule forever!
81
+
82
+ ### πŸ—οΈ Build Final Master Sheet
83
+ Once all anomalies are resolved, click **Build Master Sheet**.
84
+ - The app will geo-decode the data, apply all mapping rules, and filter out strictly out-of-scope regions (like Navy schools located in non-operational states).
85
+ - A final, polished Excel file will be generated for you to download.
86
+
87
+ ---
88
+
89
+ ## 4. Tab 3: πŸ—ΊοΈ Mapping Manager
90
+
91
+ This tab manages your **Scholarship Application Reference** (Dataset 1). This is the master list of all States and Districts that currently exist in your database.
92
+
93
+ It tracks whether a district is `present` (fully set up in your system) or `new districts found` (detected by the scraper, but not yet configured by your IT team).
94
+
95
+ You have three powerful ways to manage this data:
96
+
97
+ ### Method 1: Click-to-Edit Rows (Fastest for quick fixes)
98
+ 1. The main reference table is intentionally locked to prevent typos.
99
+ 2. Click **any cell** in the table, and the app will instantly copy that row's State and District into the **Edit Status** form directly below the table.
100
+ 3. Use the **Status Dropdown** to select either `present` or `new districts found`.
101
+ 4. Click the blue **πŸ’Ύ Update Row** button to permanently save your changes to the cloud.
102
+
103
+ ### Method 2: Bulk Excel Import / Export (Best for team collaboration)
104
+ 1. Click the **πŸ“¦ Bulk Excel Import / Export** accordion to expand it.
105
+ 2. Click **πŸ“¦ Prepare Current View for Download** and download the Excel file.
106
+ 3. You can send this file to your team or make massive bulk edits in Microsoft Excel.
107
+ 4. Once updated, drag and drop the Excel file into the **Upload Updated Excel** box. The app will instantly read the file and sync all changes to the cloud.
108
+
109
+ ### Method 3: Smart Dropdown Tools (Best for structural changes)
110
+ At the bottom of the tab, you'll find tools to Add, Rename, or Delete districts.
111
+ - **Add a District:** Use this if you are manually creating a district in your Scholarship Application before scraping.
112
+ - **Rename a District:** Use this if your Scholarship Application changes a district name. **Crucially, using this tool will automatically cascade the name change to all of your existing mapping rules!**
113
+ - *Note: Some fields (like Add a District) are standard textboxes, while others (like Rename and Delete) use smart dropdowns so you can easily select existing names.*
114
+
115
+ ---
116
+
117
+ ## 5. Tab 4: πŸ“₯ Download History
118
+
119
+ This tab serves as your cloud backup and archive.
120
+
121
+ - Every time you click **Build Master Sheet** in Tab 2, a permanent snapshot of that final Excel file is saved to the HuggingFace cloud.
122
+ - If you ever lose a file or need to access a master sheet from a previous month, simply go to this tab.
123
+ - Click **πŸ”„ Refresh File List**, select the file by its timestamp, and download it instantly.
124
+
125
+ ---
126
+
127
+ ## 6. Best Practices & Tips
128
+
129
+ - **Do Not Interrupt the Scraper:** If you close the browser tab while Tab 1 is actively scraping, the process will stop. Let it run in the background.
130
+ - **Always Resolve Anomalies:** Never ignore red tables in Tab 2. If you force a build without mapping new districts, those schools will carry the anomalous names into your final database, which will cause data ingestion errors.
131
+ - **Use the Rename Tool for Name Changes:** If you need to rename a district that has mapping rules attached to it, always use the **Rename a District** tool in Tab 3 rather than editing the table directly. The tool ensures your mapping rules don't break.
132
+ - **Smart Raw Deletion Cleanup:** If you decide you don't want a scraped state anymore and click "πŸ—‘οΈ Delete" in Tab 2, the app will auto-clean your database. It will delete all `new districts found` for that state, but perfectly preserve any `present` districts to ensure your established database is never corrupted.
133
+ - **Clear Filters Before Exporting:** In Tab 3, the Excel Export downloads exactly what is visible on your screen. If you only want to download "Delhi", apply the filter first before clicking Prepare for Download!
134
+
135
+ ---
136
+ *End of Manual*
admin_rename_sf_district.py CHANGED
@@ -63,14 +63,24 @@ def rename_sf_district(state: str, old_name: str, new_name: str):
63
  # Pull existing rules
64
  current_dist, current_block = pull_mapping_rules(token, repo)
65
 
66
- # Drop all old automated rules for this state to ensure a clean slate
67
  if not current_dist.empty:
68
- current_dist = current_dist[current_dist["State"] != state]
 
 
 
69
  if not current_block.empty:
70
- current_block = current_block[current_block["State"] != state]
 
 
71
 
72
  merged_dist = pd.concat([current_dist, auto_dist], ignore_index=True) if not auto_dist.empty else current_dist
 
 
 
73
  merged_block = pd.concat([current_block, auto_block], ignore_index=True) if not auto_block.empty else current_block
 
 
74
 
75
  push_mapping_rules(merged_dist, merged_block, token, repo)
76
  print("Mapping rules successfully updated!")
 
63
  # Pull existing rules
64
  current_dist, current_block = pull_mapping_rules(token, repo)
65
 
66
+ # Safely cascade the rename to existing mapping rules instead of dropping them
67
  if not current_dist.empty:
68
+ mask = (current_dist["State"] == state) & (current_dist["Dist_NEW"] == old_name)
69
+ if mask.sum() > 0:
70
+ current_dist.loc[mask, "Dist_NEW"] = new_name
71
+
72
  if not current_block.empty:
73
+ mask = (current_block["State"] == state) & (current_block["Dist_NEW"] == old_name)
74
+ if mask.sum() > 0:
75
+ current_block.loc[mask, "Dist_NEW"] = new_name
76
 
77
  merged_dist = pd.concat([current_dist, auto_dist], ignore_index=True) if not auto_dist.empty else current_dist
78
+ if not merged_dist.empty:
79
+ merged_dist = merged_dist.drop_duplicates(subset=["State", "Dist_OLD", "Dist_NEW"])
80
+
81
  merged_block = pd.concat([current_block, auto_block], ignore_index=True) if not auto_block.empty else current_block
82
+ if not merged_block.empty:
83
+ merged_block = merged_block.drop_duplicates(subset=["State", "Dist_OLD", "Block_OLD", "Dist_NEW"])
84
 
85
  push_mapping_rules(merged_dist, merged_block, token, repo)
86
  print("Mapping rules successfully updated!")
app.py CHANGED
@@ -251,9 +251,13 @@ def _stream(pytest_args: list, state: str, max_retries: int, mode: str, target_c
251
  re_done_s = re.compile(r"DONE\.\s+Total:\s*(\d+)\s*\|.*?Success:\s*(\d+).*?No Data:\s*(\d+).*?Captcha Failed:\s*(\d+)")
252
  re_done_r = re.compile(r"RETRY DONE\.\s+Attempted:\s*(\d+)\s*\|.*?Resolved:\s*(\d+).*?Still captcha-failed:\s*(\d+)")
253
 
 
254
  def _auto_export_excel():
255
  """Auto-generate Excel from JSON after scrape completes."""
256
  excel = get_excel_file(state)
 
 
 
257
  try:
258
  proc = subprocess.run(
259
  [sys.executable, "export_to_excel.py", "--state", state],
@@ -261,6 +265,7 @@ def _stream(pytest_args: list, state: str, max_retries: int, mode: str, target_c
261
  cwd=os.path.dirname(os.path.abspath(__file__))
262
  )
263
  if proc.returncode == 0 and os.path.exists(excel):
 
264
  return gr.update(value=excel, visible=True)
265
  except Exception:
266
  pass
@@ -317,7 +322,7 @@ def _stream(pytest_args: list, state: str, max_retries: int, mode: str, target_c
317
  if success_count == 0 and failed_count == 0:
318
  phase_text = "β—‹ No schools found in selected categories."; banner_rendered = _banner_html("β—‹", "No Schools Found", "The selected categories have no schools.", "nodata")
319
  elif not has_failures:
320
- phase_text = "βœ… All data retrieved!"; banner_rendered = _banner_html("βœ…", "All Data Retrieved!", "All school data fetched successfully.", "success")
321
  else:
322
  phase_text = f"⚠️ {failed_count} record(s) incomplete."; banner_rendered = _banner_html("⚠️", "Some Records Are Incomplete", f"{failed_count} record(s) could not be fetched. Please proceed to Step 2 to fetch missing data.", "warning")
323
  mr = re_done_r.search(line)
@@ -326,7 +331,7 @@ def _stream(pytest_args: list, state: str, max_retries: int, mode: str, target_c
326
  success_count = resolved; failed_count = still_failed; no_data_count = 0; current = total = attempted; is_done = True; has_failures = still_failed > 0
327
  stats_rendered = _stats_html(resolved, 0, still_failed, attempted)
328
  if not has_failures:
329
- phase_text = "βœ… All data retrieved!"; banner_rendered = _banner_html("βœ…", "All Data Retrieved!", "All missing data fetched. Please proceed to Step 3 to export.", "success")
330
  else:
331
  phase_text = f"⚠️ {still_failed} record(s) still incomplete."; banner_rendered = _banner_html("⚠️", "Some Records Still Incomplete", f"{still_failed} record(s) failed. You can run Step 2 again or proceed to Step 3.", "warning")
332
  yield _emit()
@@ -338,7 +343,7 @@ def _stream(pytest_args: list, state: str, max_retries: int, mode: str, target_c
338
  fc = check_missed_schools(out_file)
339
  if fc is not None:
340
  if fc == 0 and success_count > 0:
341
- is_done, has_failures = True, False; phase_text = "βœ… All data retrieved!"; banner_rendered = _banner_html("βœ…", "All Data Retrieved!", "All school data fetched successfully.", "success")
342
  elif fc > 0:
343
  is_done, has_failures = True, True; failed_count = fc; phase_text = f"⚠️ {fc} record(s) incomplete."; banner_rendered = _banner_html("⚠️", "Some Records Are Incomplete", f"{fc} record(s) failed. Please proceed to Step 2 to fetch missing data.", "warning")
344
  yield _emit()
@@ -1005,7 +1010,7 @@ with gr.Blocks(title="School Data Fetcher", css=css, theme=custom_theme) as app:
1005
  flagged_table = gr.Dataframe(
1006
  label="New districts detected (Type the Old SF name in the last column to rename it)",
1007
  headers=["State", "District", "Reason", "Rename District (Optional)"],
1008
- interactive=True, wrap=True
1009
  )
1010
  flagged_status_html = gr.HTML(value="")
1011
 
@@ -1219,65 +1224,108 @@ with gr.Blocks(title="School Data Fetcher", css=css, theme=custom_theme) as app:
1219
  border:1px solid rgba(99,102,241,0.2); border-radius:14px; padding:18px 22px; margin-bottom:18px;">
1220
  <div style="font-weight:700; font-size:1.05em; margin-bottom:6px;">πŸ—ΊοΈ Mapping Manager</div>
1221
  <div style="font-size:.9em; color:var(--body-text-color-subdued); line-height:1.7;">
1222
- <b>SF Reference</b>: The mirror of what Salesforce currently knows. When you add a new district to Salesforce, find it here and change its status from <i>pending_sf_addition</i> to <i>in_sf</i>.
1223
  </div>
1224
  </div>
1225
  """)
1226
 
1227
-
1228
- # ── SF District Reference CRUD ─────────────────────────
1229
- gr.HTML("<div class='section-label'>πŸ“ SF District Reference</div>")
1230
  with gr.Row():
1231
  refresh_ref_btn = gr.Button("πŸ”„ Load from Dataset", variant="secondary", scale=1)
1232
 
1233
  # Filters
1234
  with gr.Row():
1235
  ref_filter_state = gr.Dropdown(label="Filter by State", choices=["All"], value="All", scale=1, interactive=True)
1236
- ref_filter_status = gr.Dropdown(label="Filter by Status", choices=["All", "in_sf", "pending_sf_addition"], value="All", scale=1, interactive=True)
1237
  ref_filter_dist = gr.Textbox(label="Search District", placeholder="Type to search...", scale=2, interactive=True)
1238
 
1239
  # The DataFrame
1240
  ref_full_state = gr.State(pd.DataFrame())
1241
  ref_table = gr.Dataframe(
1242
- label="SF District Reference (State | District | SF_Status)",
1243
  interactive=False, wrap=True
1244
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1245
  ref_status_html = gr.HTML(value="")
1246
 
1247
  gr.HTML("<div style='font-size:.8em;font-weight:600;margin:10px 0 4px 0;'>Add a District</div>")
1248
  with gr.Row():
1249
  ref_state_add = gr.Textbox(label="State", placeholder="e.g. ANDHRA PRADESH", scale=2)
1250
  ref_dist_add = gr.Textbox(label="District", placeholder="e.g. ALLURI SITHARAMA RAJU", scale=2)
1251
- ref_status_add = gr.Dropdown(choices=["in_sf", "pending_sf_addition"], value="in_sf", label="SF Status", scale=1)
1252
  ref_add_btn = gr.Button("βž• Add District", variant="primary", scale=1)
1253
  ref_add_status = gr.HTML(value="")
1254
 
1255
  gr.HTML("<div style='font-size:.8em;font-weight:600;margin:10px 0 4px 0;'>Rename a District (automatically updates mappings)</div>")
1256
  with gr.Row():
1257
- ref_ren_state = gr.Textbox(label="State", placeholder="e.g. ANDHRA PRADESH", scale=2)
1258
- ref_ren_old = gr.Textbox(label="Old Name", placeholder="e.g. VISAKHAPATNAM", scale=2)
1259
  ref_ren_new = gr.Textbox(label="New Name", placeholder="e.g. VISAKHAPATNAM (NEW)", scale=2)
1260
  ref_rename_btn = gr.Button("✏️ Rename", variant="secondary", scale=1)
1261
  ref_rename_status = gr.HTML(value="")
1262
 
1263
  gr.HTML("<div style='font-size:.8em;font-weight:600;margin:10px 0 4px 0;'>Delete Actions</div>")
1264
  with gr.Row():
1265
- ref_del_state = gr.Textbox(label="State", placeholder="e.g. ANDHRA PRADESH", scale=2)
1266
- ref_del_dist = gr.Textbox(label="District", placeholder="e.g. OLD_DISTRICT", scale=2)
1267
  ref_del_dist_btn = gr.Button("πŸ—‘οΈ Delete District", variant="stop", scale=1)
1268
  ref_del_state_btn = gr.Button("🚨 Delete Entire State", variant="stop", scale=1)
1269
  ref_del_status = gr.HTML(value="")
1270
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1271
  def ui_refresh_ref():
1272
  from hf_store import get_hf_credentials, pull_district_reference
1273
  token, repo = get_hf_credentials()
1274
  if not token or not repo:
1275
- return pd.DataFrame(), pd.DataFrame(), gr.update(), "<p style='color:#ef4444;'>⚠️ HF credentials not set.</p>"
 
 
1276
  df = pull_district_reference(token, repo)
1277
  if df.empty:
1278
- return df, df, gr.update(), "<p style='color:#f59e0b;'>⚠️ SF Reference not seeded yet. Use the Seed section above.</p>"
 
 
1279
  states = sorted(list(df["State"].unique()))
1280
- return df, df, gr.update(choices=["All"] + states, value="All"), f"<p style='color:#22c55e;font-weight:600;'>βœ… Loaded {len(df)} rows.</p>"
 
 
 
 
1281
 
1282
  def filter_ref_table(df, filter_state, filter_status, search_dist):
1283
  if df is None or df.empty: return df
@@ -1285,11 +1333,87 @@ with gr.Blocks(title="School Data Fetcher", css=css, theme=custom_theme) as app:
1285
  if filter_state and filter_state != "All":
1286
  res = res[res["State"] == filter_state]
1287
  if filter_status and filter_status != "All":
1288
- res = res[res["SF_Status"] == filter_status]
1289
  if search_dist:
1290
  res = res[res["District"].str.contains(search_dist.upper(), na=False)]
1291
  return res
1292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1293
  def ui_ref_add(state, district, sf_status):
1294
  from hf_store import get_hf_credentials, update_district_reference_add
1295
  token, repo = get_hf_credentials()
@@ -1334,11 +1458,23 @@ with gr.Blocks(title="School Data Fetcher", css=css, theme=custom_theme) as app:
1334
  except Exception as ex:
1335
  return f"<p style='color:#ef4444;'>❌ Error: {ex}</p>"
1336
 
1337
- refresh_ref_btn.click(fn=ui_refresh_ref, outputs=[ref_table, ref_full_state, ref_filter_state, ref_status_html])
 
 
 
 
 
 
 
1338
  ref_filter_state.change(fn=filter_ref_table, inputs=[ref_full_state, ref_filter_state, ref_filter_status, ref_filter_dist], outputs=[ref_table])
1339
  ref_filter_status.change(fn=filter_ref_table, inputs=[ref_full_state, ref_filter_state, ref_filter_status, ref_filter_dist], outputs=[ref_table])
1340
  ref_filter_dist.change(fn=filter_ref_table, inputs=[ref_full_state, ref_filter_state, ref_filter_status, ref_filter_dist], outputs=[ref_table])
1341
 
 
 
 
 
 
1342
  ref_add_btn.click(fn=ui_ref_add, inputs=[ref_state_add, ref_dist_add, ref_status_add], outputs=[ref_add_status])
1343
  ref_rename_btn.click(fn=ui_ref_rename, inputs=[ref_ren_state, ref_ren_old, ref_ren_new], outputs=[ref_rename_status])
1344
  ref_del_dist_btn.click(fn=ui_ref_del_dist, inputs=[ref_del_state, ref_del_dist], outputs=[ref_del_status])
 
251
  re_done_s = re.compile(r"DONE\.\s+Total:\s*(\d+)\s*\|.*?Success:\s*(\d+).*?No Data:\s*(\d+).*?Captcha Failed:\s*(\d+)")
252
  re_done_r = re.compile(r"RETRY DONE\.\s+Attempted:\s*(\d+)\s*\|.*?Resolved:\s*(\d+).*?Still captcha-failed:\s*(\d+)")
253
 
254
+ excel_cache = [None]
255
  def _auto_export_excel():
256
  """Auto-generate Excel from JSON after scrape completes."""
257
  excel = get_excel_file(state)
258
+ if excel_cache[0] == excel and os.path.exists(excel):
259
+ return gr.update(value=excel, visible=True)
260
+
261
  try:
262
  proc = subprocess.run(
263
  [sys.executable, "export_to_excel.py", "--state", state],
 
265
  cwd=os.path.dirname(os.path.abspath(__file__))
266
  )
267
  if proc.returncode == 0 and os.path.exists(excel):
268
+ excel_cache[0] = excel
269
  return gr.update(value=excel, visible=True)
270
  except Exception:
271
  pass
 
322
  if success_count == 0 and failed_count == 0:
323
  phase_text = "β—‹ No schools found in selected categories."; banner_rendered = _banner_html("β—‹", "No Schools Found", "The selected categories have no schools.", "nodata")
324
  elif not has_failures:
325
+ phase_text = "βœ… All data retrieved!"; banner_rendered = _banner_html("βœ…", "All Data Retrieved!", "All school data fetched successfully.<br><br><span style='color:#3b82f6;font-weight:600;'>⏳ Pushing data to cloud... please wait.</span>", "success")
326
  else:
327
  phase_text = f"⚠️ {failed_count} record(s) incomplete."; banner_rendered = _banner_html("⚠️", "Some Records Are Incomplete", f"{failed_count} record(s) could not be fetched. Please proceed to Step 2 to fetch missing data.", "warning")
328
  mr = re_done_r.search(line)
 
331
  success_count = resolved; failed_count = still_failed; no_data_count = 0; current = total = attempted; is_done = True; has_failures = still_failed > 0
332
  stats_rendered = _stats_html(resolved, 0, still_failed, attempted)
333
  if not has_failures:
334
+ phase_text = "βœ… All data retrieved!"; banner_rendered = _banner_html("βœ…", "All Data Retrieved!", "All missing data fetched.<br><br><span style='color:#3b82f6;font-weight:600;'>⏳ Pushing data to cloud... please wait.</span>", "success")
335
  else:
336
  phase_text = f"⚠️ {still_failed} record(s) still incomplete."; banner_rendered = _banner_html("⚠️", "Some Records Still Incomplete", f"{still_failed} record(s) failed. You can run Step 2 again or proceed to Step 3.", "warning")
337
  yield _emit()
 
343
  fc = check_missed_schools(out_file)
344
  if fc is not None:
345
  if fc == 0 and success_count > 0:
346
+ is_done, has_failures = True, False; phase_text = "βœ… All data retrieved!"; banner_rendered = _banner_html("βœ…", "All Data Retrieved!", "All school data fetched successfully.<br><br><span style='color:#3b82f6;font-weight:600;'>⏳ Pushing data to cloud... please wait.</span>", "success")
347
  elif fc > 0:
348
  is_done, has_failures = True, True; failed_count = fc; phase_text = f"⚠️ {fc} record(s) incomplete."; banner_rendered = _banner_html("⚠️", "Some Records Are Incomplete", f"{fc} record(s) failed. Please proceed to Step 2 to fetch missing data.", "warning")
349
  yield _emit()
 
1010
  flagged_table = gr.Dataframe(
1011
  label="New districts detected (Type the Old SF name in the last column to rename it)",
1012
  headers=["State", "District", "Reason", "Rename District (Optional)"],
1013
+ interactive=False, wrap=True
1014
  )
1015
  flagged_status_html = gr.HTML(value="")
1016
 
 
1224
  border:1px solid rgba(99,102,241,0.2); border-radius:14px; padding:18px 22px; margin-bottom:18px;">
1225
  <div style="font-weight:700; font-size:1.05em; margin-bottom:6px;">πŸ—ΊοΈ Mapping Manager</div>
1226
  <div style="font-size:.9em; color:var(--body-text-color-subdued); line-height:1.7;">
1227
+ <b>Scholarship Application Reference</b>: The mirror of what Scholarship Application currently knows. When you add a new district to Scholarship Application, find it here and change its status from <i>new districts found</i> to <i>present</i>.
1228
  </div>
1229
  </div>
1230
  """)
1231
 
1232
+ gr.HTML("<div class='section-label'>πŸ“ Scholarship Application District Reference</div>")
 
 
1233
  with gr.Row():
1234
  refresh_ref_btn = gr.Button("πŸ”„ Load from Dataset", variant="secondary", scale=1)
1235
 
1236
  # Filters
1237
  with gr.Row():
1238
  ref_filter_state = gr.Dropdown(label="Filter by State", choices=["All"], value="All", scale=1, interactive=True)
1239
+ ref_filter_status = gr.Dropdown(label="Filter by Status", choices=["All", "present", "new districts found"], value="All", scale=1, interactive=True)
1240
  ref_filter_dist = gr.Textbox(label="Search District", placeholder="Type to search...", scale=2, interactive=True)
1241
 
1242
  # The DataFrame
1243
  ref_full_state = gr.State(pd.DataFrame())
1244
  ref_table = gr.Dataframe(
1245
+ label="Scholarship Application District Reference (Editable)",
1246
  interactive=False, wrap=True
1247
  )
1248
+
1249
+ with gr.Accordion("πŸ“¦ Bulk Excel Import / Export", open=False):
1250
+ with gr.Row():
1251
+ ref_prep_dl_btn = gr.Button("πŸ“¦ Prepare Current View for Download")
1252
+ ref_dl_btn = gr.DownloadButton("πŸ“₯ Download Excel", visible=False)
1253
+
1254
+ with gr.Row():
1255
+ ref_upload = gr.File(label="Upload Updated Excel", file_types=[".xlsx"])
1256
+
1257
+ gr.HTML("<div style='font-size:.8em;font-weight:600;margin:10px 0 4px 0; padding-top:10px; border-top:1px solid #ddd;'>Click any row in the table above to edit its status here:</div>")
1258
+ with gr.Row():
1259
+ ref_edit_state = gr.Textbox(label="State", interactive=False, scale=2)
1260
+ ref_edit_dist = gr.Textbox(label="District", interactive=False, scale=2)
1261
+ ref_edit_status = gr.Dropdown(choices=["present", "new districts found"], label="Status", scale=1)
1262
+ ref_edit_btn = gr.Button("πŸ’Ύ Update Row", variant="primary", scale=1)
1263
+
1264
  ref_status_html = gr.HTML(value="")
1265
 
1266
  gr.HTML("<div style='font-size:.8em;font-weight:600;margin:10px 0 4px 0;'>Add a District</div>")
1267
  with gr.Row():
1268
  ref_state_add = gr.Textbox(label="State", placeholder="e.g. ANDHRA PRADESH", scale=2)
1269
  ref_dist_add = gr.Textbox(label="District", placeholder="e.g. ALLURI SITHARAMA RAJU", scale=2)
1270
+ ref_status_add = gr.Dropdown(choices=["present", "new districts found"], value="present", label="Status", scale=1)
1271
  ref_add_btn = gr.Button("βž• Add District", variant="primary", scale=1)
1272
  ref_add_status = gr.HTML(value="")
1273
 
1274
  gr.HTML("<div style='font-size:.8em;font-weight:600;margin:10px 0 4px 0;'>Rename a District (automatically updates mappings)</div>")
1275
  with gr.Row():
1276
+ ref_ren_state = gr.Dropdown(label="State", choices=[], allow_custom_value=True, scale=2)
1277
+ ref_ren_old = gr.Dropdown(label="Old Name", choices=[], allow_custom_value=True, scale=2)
1278
  ref_ren_new = gr.Textbox(label="New Name", placeholder="e.g. VISAKHAPATNAM (NEW)", scale=2)
1279
  ref_rename_btn = gr.Button("✏️ Rename", variant="secondary", scale=1)
1280
  ref_rename_status = gr.HTML(value="")
1281
 
1282
  gr.HTML("<div style='font-size:.8em;font-weight:600;margin:10px 0 4px 0;'>Delete Actions</div>")
1283
  with gr.Row():
1284
+ ref_del_state = gr.Dropdown(label="State", choices=[], allow_custom_value=True, scale=2)
1285
+ ref_del_dist = gr.Dropdown(label="District", choices=[], allow_custom_value=True, scale=2)
1286
  ref_del_dist_btn = gr.Button("πŸ—‘οΈ Delete District", variant="stop", scale=1)
1287
  ref_del_state_btn = gr.Button("🚨 Delete Entire State", variant="stop", scale=1)
1288
  ref_del_status = gr.HTML(value="")
1289
 
1290
+ def on_ref_table_select(df, evt: gr.SelectData):
1291
+ row = evt.index[0]
1292
+ if df is None or df.empty or row >= len(df):
1293
+ return gr.update(), gr.update(), gr.update(), ""
1294
+
1295
+ state = df.iloc[row]["State"]
1296
+ dist = df.iloc[row]["District"]
1297
+ status = df.iloc[row]["Status"]
1298
+ return gr.update(value=state), gr.update(value=dist), gr.update(value=status), ""
1299
+
1300
+ def ui_ref_edit_row(state, district, new_status):
1301
+ from hf_store import get_hf_credentials, update_district_reference_add
1302
+ token, repo = get_hf_credentials()
1303
+ if not token or not repo or not state or not district:
1304
+ return "<p style='color:#ef4444;'>⚠️ Please select a row from the table first.</p>"
1305
+ try:
1306
+ update_district_reference_add(state, district, new_status, token, repo)
1307
+ return f"<p style='color:#22c55e;font-weight:600;'>βœ… Successfully updated {district} to '{new_status}'. Refresh the table to see changes.</p>"
1308
+ except Exception as ex:
1309
+ return f"<p style='color:#ef4444;'>❌ Error: {ex}</p>"
1310
+
1311
  def ui_refresh_ref():
1312
  from hf_store import get_hf_credentials, pull_district_reference
1313
  token, repo = get_hf_credentials()
1314
  if not token or not repo:
1315
+ empty = pd.DataFrame()
1316
+ u = gr.update()
1317
+ return empty, empty, u, u, u, u, u, "<p style='color:#ef4444;'>⚠️ HF credentials not set.</p>"
1318
  df = pull_district_reference(token, repo)
1319
  if df.empty:
1320
+ empty = pd.DataFrame()
1321
+ u = gr.update()
1322
+ return empty, empty, u, u, u, u, u, "<p style='color:#f59e0b;'>⚠️ Scholarship Application Reference empty.</p>"
1323
  states = sorted(list(df["State"].unique()))
1324
+ dists = sorted(list(df["District"].unique()))
1325
+ st_up = gr.update(choices=["All"] + states, value="All")
1326
+ st_dd = gr.update(choices=states)
1327
+ dist_dd = gr.update(choices=dists)
1328
+ return df, df, st_up, st_dd, dist_dd, st_dd, dist_dd, f"<p style='color:#22c55e;font-weight:600;'>βœ… Loaded {len(df)} rows.</p>"
1329
 
1330
  def filter_ref_table(df, filter_state, filter_status, search_dist):
1331
  if df is None or df.empty: return df
 
1333
  if filter_state and filter_state != "All":
1334
  res = res[res["State"] == filter_state]
1335
  if filter_status and filter_status != "All":
1336
+ res = res[res["Status"] == filter_status]
1337
  if search_dist:
1338
  res = res[res["District"].str.contains(search_dist.upper(), na=False)]
1339
  return res
1340
 
1341
+ def ui_save_table_edits(df, full_df, filter_state, filter_status, search_dist):
1342
+ from hf_store import get_hf_credentials, push_district_reference
1343
+ token, repo = get_hf_credentials()
1344
+ if df is None or df.empty:
1345
+ return "<p style='color:#ef4444;'>⚠️ Table is empty.</p>", full_df
1346
+ # The user edited 'df'. We need to merge it back into 'full_df'
1347
+ # This is a bit complex if they deleted rows or changed keys.
1348
+ # Actually, if they are filtering, df only contains a subset.
1349
+ # Let's just overwrite the subset in full_df based on index if we kept index,
1350
+ # but Gradio df doesn't keep original indices easily.
1351
+ # Simplest way: They should only save edits if viewing All/All without search.
1352
+ if filter_state != "All" or filter_status != "All" or search_dist:
1353
+ return "<p style='color:#ef4444;'>⚠️ Please clear all filters (set to 'All', clear search) before saving direct table edits.</p>", full_df
1354
+
1355
+ try:
1356
+ df["State"] = df["State"].astype(str).str.strip().str.upper()
1357
+ df["District"] = df["District"].astype(str).str.strip().str.upper()
1358
+ df["Status"] = df["Status"].astype(str).str.strip()
1359
+
1360
+ invalid_statuses = df[~df["Status"].isin(["present", "new districts found"])]
1361
+ if not invalid_statuses.empty:
1362
+ return "<p style='color:#ef4444;'>⚠️ Invalid Status found. You can only use 'present' or 'new districts found'.</p>", full_df
1363
+
1364
+ df = df.drop_duplicates()
1365
+ push_district_reference(df, token, repo)
1366
+ return f"<p style='color:#22c55e;font-weight:600;'>βœ… Saved {len(df)} rows directly to cloud.</p>", df
1367
+ except Exception as ex:
1368
+ return f"<p style='color:#ef4444;'>❌ Error: {ex}</p>", full_df
1369
+
1370
+ def ui_prep_excel_download(df):
1371
+ if df is None or df.empty:
1372
+ return gr.update(visible=False), "<p style='color:#ef4444;'>⚠️ No data to download.</p>"
1373
+ import tempfile
1374
+ import os
1375
+ fd, temp_excel = tempfile.mkstemp(suffix=".xlsx")
1376
+ os.close(fd)
1377
+ df.to_excel(temp_excel, index=False)
1378
+ return gr.update(value=temp_excel, visible=True), "<p style='color:#22c55e;'>βœ… Ready to download!</p>"
1379
+
1380
+ def ui_upload_excel(file):
1381
+ if not file: return pd.DataFrame(), "<p style='color:#ef4444;'>⚠️ No file uploaded.</p>"
1382
+ import pandas as pd
1383
+ from hf_store import get_hf_credentials, push_district_reference, pull_district_reference
1384
+ token, repo = get_hf_credentials()
1385
+ try:
1386
+ uploaded_df = pd.read_excel(file.name)
1387
+ if "State" not in uploaded_df.columns or "District" not in uploaded_df.columns or "Status" not in uploaded_df.columns:
1388
+ return pd.DataFrame(), "<p style='color:#ef4444;'>⚠️ Excel must have State, District, and Status columns.</p>"
1389
+
1390
+ uploaded_df["State"] = uploaded_df["State"].astype(str).str.strip().str.upper()
1391
+ uploaded_df["District"] = uploaded_df["District"].astype(str).str.strip().str.upper()
1392
+ uploaded_df["Status"] = uploaded_df["Status"].astype(str).str.strip()
1393
+
1394
+ invalid_statuses = uploaded_df[~uploaded_df["Status"].isin(["present", "new districts found"])]
1395
+ if not invalid_statuses.empty:
1396
+ return pd.DataFrame(), "<p style='color:#ef4444;'>⚠️ Invalid Status found in Excel. You can only use 'present' or 'new districts found'.</p>"
1397
+
1398
+ uploaded_df = uploaded_df.drop_duplicates(subset=["State", "District"])
1399
+
1400
+ full_df = pull_district_reference(token, repo)
1401
+ if not full_df.empty:
1402
+ full_df.set_index(["State", "District"], inplace=True)
1403
+ uploaded_df.set_index(["State", "District"], inplace=True)
1404
+ full_df.update(uploaded_df)
1405
+ new_rows = uploaded_df[~uploaded_df.index.isin(full_df.index)]
1406
+ full_df = pd.concat([full_df, new_rows])
1407
+ full_df.reset_index(inplace=True)
1408
+ df_to_push = full_df
1409
+ else:
1410
+ df_to_push = uploaded_df.copy()
1411
+
1412
+ push_district_reference(df_to_push, token, repo)
1413
+ return df_to_push, f"<p style='color:#22c55e;font-weight:600;'>βœ… Successfully merged {len(uploaded_df)} uploaded rows into the dataset!</p>"
1414
+ except Exception as ex:
1415
+ return pd.DataFrame(), f"<p style='color:#ef4444;'>❌ Error: {ex}</p>"
1416
+
1417
  def ui_ref_add(state, district, sf_status):
1418
  from hf_store import get_hf_credentials, update_district_reference_add
1419
  token, repo = get_hf_credentials()
 
1458
  except Exception as ex:
1459
  return f"<p style='color:#ef4444;'>❌ Error: {ex}</p>"
1460
 
1461
+ # Wirings
1462
+ refresh_ref_btn.click(
1463
+ fn=ui_refresh_ref,
1464
+ outputs=[ref_table, ref_full_state, ref_filter_state,
1465
+ ref_ren_state, ref_ren_old,
1466
+ ref_del_state, ref_del_dist,
1467
+ ref_status_html]
1468
+ )
1469
  ref_filter_state.change(fn=filter_ref_table, inputs=[ref_full_state, ref_filter_state, ref_filter_status, ref_filter_dist], outputs=[ref_table])
1470
  ref_filter_status.change(fn=filter_ref_table, inputs=[ref_full_state, ref_filter_state, ref_filter_status, ref_filter_dist], outputs=[ref_table])
1471
  ref_filter_dist.change(fn=filter_ref_table, inputs=[ref_full_state, ref_filter_state, ref_filter_status, ref_filter_dist], outputs=[ref_table])
1472
 
1473
+ ref_table.select(fn=on_ref_table_select, inputs=[ref_table], outputs=[ref_edit_state, ref_edit_dist, ref_edit_status, ref_status_html])
1474
+ ref_edit_btn.click(fn=ui_ref_edit_row, inputs=[ref_edit_state, ref_edit_dist, ref_edit_status], outputs=[ref_status_html])
1475
+ ref_prep_dl_btn.click(fn=ui_prep_excel_download, inputs=[ref_table], outputs=[ref_dl_btn, ref_status_html])
1476
+ ref_upload.upload(fn=ui_upload_excel, inputs=[ref_upload], outputs=[ref_table, ref_status_html])
1477
+
1478
  ref_add_btn.click(fn=ui_ref_add, inputs=[ref_state_add, ref_dist_add, ref_status_add], outputs=[ref_add_status])
1479
  ref_rename_btn.click(fn=ui_ref_rename, inputs=[ref_ren_state, ref_ren_old, ref_ren_new], outputs=[ref_rename_status])
1480
  ref_del_dist_btn.click(fn=ui_ref_del_dist, inputs=[ref_del_state, ref_del_dist], outputs=[ref_del_status])
hf_store.py CHANGED
@@ -122,7 +122,7 @@ DISTRICT_REF_PATH = "district_reference/district_reference.parquet"
122
  def push_district_reference(df: pd.DataFrame, token: str, repo: str) -> str:
123
  """
124
  Push the SF district reference table to HF dataset.
125
- Expected columns: State, District, SF_Status ('in_sf' | 'pending_sf_addition')
126
  Returns commit URL.
127
  """
128
  return _upload_parquet(df, DISTRICT_REF_PATH, token, repo, "Update district_reference")
@@ -131,20 +131,20 @@ def push_district_reference(df: pd.DataFrame, token: str, repo: str) -> str:
131
  def pull_district_reference(token: str, repo: str) -> pd.DataFrame:
132
  """
133
  Pull the SF district reference from HF dataset.
134
- Returns DataFrame with columns: State, District, SF_Status
135
  Returns empty DataFrame if not yet seeded.
136
  """
137
  df = _download_parquet(DISTRICT_REF_PATH, token, repo)
138
  if df.empty:
139
- return pd.DataFrame(columns=["State", "District", "SF_Status"])
140
  return df
141
 
142
 
143
  def seed_district_reference_from_csv(csv_path: str, token: str, repo: str) -> tuple:
144
  """
145
  Seed Dataset 1 from a CSV (like master_all_states.csv or SF mapping CSV).
146
- All states/districts in the CSV β†’ SF_Status = 'in_sf'
147
- Any states in current scraped data NOT in CSV β†’ SF_Status = 'pending_sf_addition'
148
  Returns (df, commit_url)
149
  """
150
  csv_df = pd.read_csv(csv_path)
@@ -163,9 +163,9 @@ def seed_district_reference_from_csv(csv_path: str, token: str, repo: str) -> tu
163
  csv_df = csv_df.dropna(subset=[state_col])
164
  csv_df["State"] = csv_df[state_col].astype(str).str.strip().str.upper()
165
  csv_df["District"] = csv_df[district_col].astype(str).str.strip().str.upper()
166
- csv_df["SF_Status"] = "in_sf"
167
 
168
- ref_df = csv_df[["State", "District", "SF_Status"]].drop_duplicates()
169
 
170
  # Pull existing scraped metadata to check for any states not in the CSV
171
  meta = _pull_metadata(token, repo)
@@ -176,7 +176,7 @@ def seed_district_reference_from_csv(csv_path: str, token: str, repo: str) -> tu
176
  if state_upper not in csv_states:
177
  # State exists in scraped data but not in SF CSV β†’ pending
178
  # We don't know districts yet; they'll be added when detected
179
- new_rows.append({"State": state_upper, "District": "ALL", "SF_Status": "pending_sf_addition"})
180
 
181
  if new_rows:
182
  ref_df = pd.concat([ref_df, pd.DataFrame(new_rows)], ignore_index=True)
@@ -192,7 +192,7 @@ def update_district_reference_add(state: str, district: str, sf_status: str, tok
192
  district = district.strip().upper()
193
  # Remove existing row if present
194
  df = df[~((df["State"] == state) & (df["District"] == district))]
195
- new_row = pd.DataFrame([{"State": state, "District": district, "SF_Status": sf_status}])
196
  df = pd.concat([df, new_row], ignore_index=True)
197
  return push_district_reference(df, token, repo)
198
 
@@ -240,7 +240,7 @@ def delete_state_reference(state: str, token: str, repo: str) -> str:
240
 
241
  def bulk_accept_flagged_districts(token: str, repo: str) -> None:
242
  """
243
- Take all currently flagged districts, add them to Dataset 1 with 'pending_sf_addition',
244
  and clear the flagged_districts list from metadata.
245
  """
246
  meta = _pull_metadata(token, repo)
@@ -255,7 +255,7 @@ def bulk_accept_flagged_districts(token: str, repo: str) -> None:
255
  state = f["state"].strip().upper()
256
  dist = f["district"].strip().upper()
257
  if not ((df["State"] == state) & (df["District"] == dist)).any():
258
- new_rows.append({"State": state, "District": dist, "SF_Status": "pending_sf_addition"})
259
 
260
  if new_rows:
261
  df = pd.concat([df, pd.DataFrame(new_rows)], ignore_index=True)
@@ -361,9 +361,9 @@ def delete_mapping_rule(state: str, dist_old: str, dist_new: str, token: str, re
361
  # ─────────────────────────────────────────────────────────────────────────────
362
 
363
  def get_flagged_districts(token: str, repo: str) -> list:
364
- """Return all districts from Dataset 1 that have SF_Status == 'pending_sf_addition', filtered by states with raw data."""
365
  ref_df = pull_district_reference(token, repo)
366
- if ref_df.empty or "SF_Status" not in ref_df.columns:
367
  return []
368
 
369
  try:
@@ -379,7 +379,7 @@ def get_flagged_districts(token: str, repo: str) -> list:
379
  except:
380
  active_states = set(ref_df["State"].str.upper()) # fallback
381
 
382
- pending = ref_df[(ref_df["SF_Status"] == "pending_sf_addition") & (ref_df["State"].str.upper().isin(active_states))]
383
 
384
  dist_df, _ = pull_mapping_rules(token, repo)
385
  mapped = set()
@@ -397,7 +397,7 @@ def get_flagged_districts(token: str, repo: str) -> list:
397
  flags.append({
398
  "state": state,
399
  "district": district,
400
- "reason": "Pending SF Addition"
401
  })
402
  return flags
403
 
@@ -409,7 +409,7 @@ def rename_district_in_reference(state: str, district: str, new_name: str, token
409
  exists = not ref_df[(ref_df["State"] == state) & (ref_df["District"] == new_name)].empty
410
 
411
  # Target row to update/delete
412
- mask = (ref_df["State"] == state) & (ref_df["District"] == district) & (ref_df["SF_Status"] == "pending_sf_addition")
413
 
414
  if not ref_df[mask].empty:
415
  if exists:
@@ -483,7 +483,7 @@ def _detect_and_flag_new_districts(state_name: str, df: pd.DataFrame, token: str
483
  """
484
  Compare districts in newly scraped df against Dataset 1 and Dataset 2.
485
  Any district not in Dataset 1 AND not mapped in Dataset 2 is truly new.
486
- Add them straight to Dataset 1 with SF_Status = 'pending_sf_addition'.
487
  """
488
  state_upper = state_name.strip().upper()
489
  if state_upper in NON_ACTUAL_STATES:
@@ -555,13 +555,13 @@ def _detect_and_flag_new_districts(state_name: str, df: pd.DataFrame, token: str
555
  return
556
 
557
  # Add directly to Dataset 1 as pending
558
- new_rows = [{"State": state_upper, "District": d, "SF_Status": "pending_sf_addition"} for d in new_districts]
559
  updated_ref = pd.concat([ref_df, pd.DataFrame(new_rows)], ignore_index=True)
560
  push_district_reference(updated_ref, token, repo)
561
 
562
 
563
  def delete_state_file(state_name: str, token: str, repo: str) -> None:
564
- """Delete a state's raw dataset file and remove it from metadata.json."""
565
  api = _hf()
566
  skey = _state_key(state_name)
567
  try:
@@ -579,6 +579,20 @@ def delete_state_file(state_name: str, token: str, repo: str) -> None:
579
  if state_name in meta:
580
  del meta[state_name]
581
  _push_metadata(meta, token, repo)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582
 
583
 
584
  def pull_state_file(state_name: str, token: str, repo: str) -> pd.DataFrame:
 
122
  def push_district_reference(df: pd.DataFrame, token: str, repo: str) -> str:
123
  """
124
  Push the SF district reference table to HF dataset.
125
+ Expected columns: State, District, Status ('present' | 'new districts found')
126
  Returns commit URL.
127
  """
128
  return _upload_parquet(df, DISTRICT_REF_PATH, token, repo, "Update district_reference")
 
131
  def pull_district_reference(token: str, repo: str) -> pd.DataFrame:
132
  """
133
  Pull the SF district reference from HF dataset.
134
+ Returns DataFrame with columns: State, District, Status
135
  Returns empty DataFrame if not yet seeded.
136
  """
137
  df = _download_parquet(DISTRICT_REF_PATH, token, repo)
138
  if df.empty:
139
+ return pd.DataFrame(columns=["State", "District", "Status"])
140
  return df
141
 
142
 
143
  def seed_district_reference_from_csv(csv_path: str, token: str, repo: str) -> tuple:
144
  """
145
  Seed Dataset 1 from a CSV (like master_all_states.csv or SF mapping CSV).
146
+ All states/districts in the CSV β†’ Status = 'present'
147
+ Any states in current scraped data NOT in CSV β†’ Status = 'new districts found'
148
  Returns (df, commit_url)
149
  """
150
  csv_df = pd.read_csv(csv_path)
 
163
  csv_df = csv_df.dropna(subset=[state_col])
164
  csv_df["State"] = csv_df[state_col].astype(str).str.strip().str.upper()
165
  csv_df["District"] = csv_df[district_col].astype(str).str.strip().str.upper()
166
+ csv_df["Status"] = "present"
167
 
168
+ ref_df = csv_df[["State", "District", "Status"]].drop_duplicates()
169
 
170
  # Pull existing scraped metadata to check for any states not in the CSV
171
  meta = _pull_metadata(token, repo)
 
176
  if state_upper not in csv_states:
177
  # State exists in scraped data but not in SF CSV β†’ pending
178
  # We don't know districts yet; they'll be added when detected
179
+ new_rows.append({"State": state_upper, "District": "ALL", "Status": "new districts found"})
180
 
181
  if new_rows:
182
  ref_df = pd.concat([ref_df, pd.DataFrame(new_rows)], ignore_index=True)
 
192
  district = district.strip().upper()
193
  # Remove existing row if present
194
  df = df[~((df["State"] == state) & (df["District"] == district))]
195
+ new_row = pd.DataFrame([{"State": state, "District": district, "Status": sf_status}])
196
  df = pd.concat([df, new_row], ignore_index=True)
197
  return push_district_reference(df, token, repo)
198
 
 
240
 
241
  def bulk_accept_flagged_districts(token: str, repo: str) -> None:
242
  """
243
+ Take all currently flagged districts, add them to Dataset 1 with 'new districts found',
244
  and clear the flagged_districts list from metadata.
245
  """
246
  meta = _pull_metadata(token, repo)
 
255
  state = f["state"].strip().upper()
256
  dist = f["district"].strip().upper()
257
  if not ((df["State"] == state) & (df["District"] == dist)).any():
258
+ new_rows.append({"State": state, "District": dist, "Status": "new districts found"})
259
 
260
  if new_rows:
261
  df = pd.concat([df, pd.DataFrame(new_rows)], ignore_index=True)
 
361
  # ─────────────────────────────────────────────────────────────────────────────
362
 
363
  def get_flagged_districts(token: str, repo: str) -> list:
364
+ """Return all districts from Dataset 1 that have Status == 'new districts found', filtered by states with raw data."""
365
  ref_df = pull_district_reference(token, repo)
366
+ if ref_df.empty or "Status" not in ref_df.columns:
367
  return []
368
 
369
  try:
 
379
  except:
380
  active_states = set(ref_df["State"].str.upper()) # fallback
381
 
382
+ pending = ref_df[(ref_df["Status"] == "new districts found") & (ref_df["State"].str.upper().isin(active_states))]
383
 
384
  dist_df, _ = pull_mapping_rules(token, repo)
385
  mapped = set()
 
397
  flags.append({
398
  "state": state,
399
  "district": district,
400
+ "reason": "New district detected"
401
  })
402
  return flags
403
 
 
409
  exists = not ref_df[(ref_df["State"] == state) & (ref_df["District"] == new_name)].empty
410
 
411
  # Target row to update/delete
412
+ mask = (ref_df["State"] == state) & (ref_df["District"] == district) & (ref_df["Status"] == "new districts found")
413
 
414
  if not ref_df[mask].empty:
415
  if exists:
 
483
  """
484
  Compare districts in newly scraped df against Dataset 1 and Dataset 2.
485
  Any district not in Dataset 1 AND not mapped in Dataset 2 is truly new.
486
+ Add them straight to Dataset 1 with Status = 'new districts found'.
487
  """
488
  state_upper = state_name.strip().upper()
489
  if state_upper in NON_ACTUAL_STATES:
 
555
  return
556
 
557
  # Add directly to Dataset 1 as pending
558
+ new_rows = [{"State": state_upper, "District": d, "Status": "new districts found"} for d in new_districts]
559
  updated_ref = pd.concat([ref_df, pd.DataFrame(new_rows)], ignore_index=True)
560
  push_district_reference(updated_ref, token, repo)
561
 
562
 
563
  def delete_state_file(state_name: str, token: str, repo: str) -> None:
564
+ """Delete a state's raw dataset file and remove it from metadata.json, and also clean up pending newly found districts for that state."""
565
  api = _hf()
566
  skey = _state_key(state_name)
567
  try:
 
579
  if state_name in meta:
580
  del meta[state_name]
581
  _push_metadata(meta, token, repo)
582
+
583
+ # Also clean up any "new districts found" for this deleted state in the District Reference
584
+ try:
585
+ ref_df = pull_district_reference(token, repo)
586
+ if not ref_df.empty:
587
+ # We want to KEEP rows that are NOT (State == state_name and Status == 'new districts found')
588
+ original_len = len(ref_df)
589
+ state_upper = state_name.upper()
590
+ mask = ~((ref_df["State"].str.upper() == state_upper) & (ref_df["Status"] == "new districts found"))
591
+ cleaned_ref = ref_df[mask]
592
+ if len(cleaned_ref) < original_len:
593
+ push_district_reference(cleaned_ref, token, repo)
594
+ except Exception as e:
595
+ print(f"Failed to cleanup district reference on state delete: {e}")
596
 
597
 
598
  def pull_state_file(state_name: str, token: str, repo: str) -> pd.DataFrame: