JonoRens/gr_test_app
0
1import gradio as gr2import psycopg23import pandas as pd4import random5import re6import io7from pathlib import Path8from PIL import Image, ImageDraw, ImageFont9import PIL10from math import sqrt11import pickle12from cryptography.fernet import Fernet, InvalidToken13 14from typing import Optional, Dict, Any15import time16import json17import os18 19 20# ===== Optional QR: try qrcode, else Pillow fallback (no crash on Spaces) =====21try:22 import qrcode # external; may not exist on Spaces23 def _make_qr_image(data: str, box_size: int = 20, border: int = 2) -> Image.Image:24 qr = qrcode.QRCode(version=1, box_size=box_size, border=border)25 qr.add_data(data)26 qr.make(fit=True)27 return qr.make_image(fill_color="#000000", back_color="#FFFFFF").convert("RGB")28except Exception:29 # Fallback: draw a simple placeholder (NOT a real QR) so app keeps working30 def _make_qr_image(data: str, box_size: int = 20, border: int = 2) -> Image.Image:31 w = h = 40032 img = Image.new("RGB", (w, h), "white")33 d = ImageDraw.Draw(img)34 d.rectangle([0, 0, w - 1, h - 1], outline="black")35 msg = "QR lib missing"36 d.text((12, 12), msg, fill="black")37 d.text((12, 40), str(data), fill="black")38 return img39 40 41# ===== Hugging Face / app.py glue =====42_CACHED_NHOST_PARAMS: Dict[str, Any] = {}43 44def _params_from_env() -> Optional[Dict[str, Any]]:45 j = os.environ.get("NHOST_JSON", "").strip()46 if j:47 try:48 obj = json.loads(j)49 return {50 "host": obj.get("host"),51 "port": int(obj.get("port", 5432)) if obj.get("port") else None,52 "user": obj.get("user"),53 "password": obj.get("password"),54 "dbname": obj.get("dbname"),55 "sslmode": obj.get("sslmode", "require"),56 }57 except Exception:58 pass59 60 host = os.environ.get("NHOST_HOST", "").strip()61 user = os.environ.get("NHOST_USER", "").strip()62 password = os.environ.get("NHOST_PASSWORD", "").strip()63 db = os.environ.get("NHOST_DB", "").strip() or os.environ.get("NHOST_DBNAME", "").strip()64 port = os.environ.get("NHOST_PORT", "").strip()65 sslmode = os.environ.get("NHOST_SSLMODE", "require").strip() or "require"66 67 if host and user and password and db:68 return {69 "host": host,70 "port": int(port) if port else 5432,71 "user": user,72 "password": password,73 "dbname": db,74 "sslmode": sslmode,75 }76 return None77 78 79def decrypt_nhost(enc_path: Path = None) -> Dict[str, Any]:80 global _CACHED_NHOST_PARAMS81 if _CACHED_NHOST_PARAMS:82 return _CACHED_NHOST_PARAMS83 84 env_params = _params_from_env()85 if env_params and env_params.get("host"):86 _CACHED_NHOST_PARAMS = env_params87 return _CACHED_NHOST_PARAMS88 89 if enc_path is None:90 here = Path(__file__).parent91 enc_path = (Path.cwd() / "nhost_params.enc")92 if not enc_path.exists():93 enc_path = here / "nhost_params.enc"94 95 secret = os.environ.get("SECRET_KEY", "").strip()96 try:97 if not secret:98 raise RuntimeError("SECRET_KEY env var is missing.")99 if not enc_path.exists():100 raise RuntimeError("nhost_params.enc not found.")101 key = secret.encode("utf-8")102 f = Fernet(key)103 blob = enc_path.read_bytes()104 raw = f.decrypt(blob)105 obj = json.loads(raw.decode("utf-8"))106 107 _CACHED_NHOST_PARAMS = {108 "host": obj.get("host"),109 "port": int(obj.get("port", 5432)) if obj.get("port") else 5432,110 "user": obj.get("user"),111 "password": obj.get("password"),112 "dbname": obj.get("dbname"),113 "sslmode": obj.get("sslmode", "require"),114 }115 return _CACHED_NHOST_PARAMS116 except Exception as e:117 print("[WARN] decrypt_nhost(): could not decrypt nhost_params.enc →", e)118 119 raise RuntimeError(120 "No NHOST connection params found. "121 "Set NHOST_JSON (or NHOST_* env vars) or provide nhost_params.enc with SECRET_KEY."122 )123 124 125def get_connection() -> psycopg2.extensions.connection:126 params = decrypt_nhost()127 if not params.get("host"):128 raise RuntimeError("NHOST params invalid (no host).")129 return psycopg2.connect(**params)130 131 132# ─────────────────────────────────────────────133# Active User: same approach as property/permissions modules134# ─────────────────────────────────────────────135 136NEW_DATA_FILE = Path(__file__).parent / "openqr_active_user.pkl.enc"137NEW_KEY_FILE = Path(__file__).parent / "openqr_secret.key"138LEGACY_DATA_FILE = Path(__file__).parent / "oqrdata.pkl.enc"139LEGACY_KEY_FILE = Path(__file__).parent / "oqrdata.key"140 141def _read_encrypted_pickle(data_path: Path, key_path: Path):142 try:143 if not data_path.exists() or not key_path.exists():144 return None145 key = key_path.read_bytes()146 f = Fernet(key)147 blob = data_path.read_bytes()148 raw = f.decrypt(blob)149# import pickle150 return pickle.loads(raw)151 except (InvalidToken, Exception):152 return None153 154 155ActiveUserID: Optional[int] = None156 157 158def _coerce_uid(val) -> Optional[int]:159 try:160 iv = int(str(val).strip())161 return iv if iv > 0 else None162 except Exception:163 return None164 165def resolve_and_cache_uid(get_user_id=None) -> Optional[int]:166 global ActiveUserID167 if _coerce_uid(ActiveUserID):168 return ActiveUserID169 170 # 1) callback first171 if callable(get_user_id):172 try:173 v = _coerce_uid(get_user_id())174 if v:175 ActiveUserID = v176 return ActiveUserID177 except Exception:178 pass179 180 # 2) env next181 env_val = _coerce_uid(os.getenv("OPENQR_ACTIVE_USER_ID") or os.getenv("OQR_ACTIVE_USER_ID"))182 if env_val:183 ActiveUserID = env_val184 return ActiveUserID185 186 # 3) encrypted pickle last187 info = _read_encrypted_pickle(NEW_DATA_FILE, NEW_KEY_FILE) or _read_encrypted_pickle(LEGACY_DATA_FILE, LEGACY_KEY_FILE)188 if info:189 for k in ("ActiveUserID", "UserID", "MUserID"):190 v = _coerce_uid(info.get(k))191 if v:192 ActiveUserID = v193 return ActiveUserID194 195 ActiveUserID = None196 return None197 198 199def launch_accesspoints_module(get_user_id=None):200 201 # ========== UTILS ==========202 def generate_id():203 return random.randint(111111111111111, 999999999999999)204 205 def _get_installers_flag(user_id: int | None) -> bool:206 if not user_id:207 return False208 try:209 conn = get_connection()210 cur = conn.cursor()211 cur.execute('SELECT "Installers" FROM managerusers WHERE "MUserID" = %s', (int(user_id),))212 row = cur.fetchone()213 cur.close()214 conn.close()215 return bool(row[0]) if row and row[0] is not None else False216 except Exception as e:217 print(f"⚠️ Installers check failed: {e}")218 return False219 220# IsInstaller = _get_installers_flag(_uid())221 222 def _get_property_name(prop_id: int | str, user_id: int | None) -> str:223 try:224 conn = get_connection()225 cur = conn.cursor()226 cur.execute(227 'SELECT "Name" FROM property WHERE "PropID" = %s AND "UserID" = %s AND "Active" = TRUE',228 (int(prop_id), int(user_id) if user_id is not None else None),229 )230 row = cur.fetchone()231 cur.close()232 conn.close()233 return row[0] if row and row[0] is not None else ""234 except Exception as e:235 print(f"⚠️ _get_property_name failed: {e}")236 return ""237 238 def _get_font(size: int, font_path: str | None = None) -> ImageFont.FreeTypeFont:239 candidates: list[str | Path] = []240 if font_path:241 candidates.append(font_path)242 pil_fonts = Path(PIL.__file__).parent / "fonts"243 candidates += [pil_fonts / "DejaVuSans.ttf", pil_fonts / "DejaVuSans-Bold.ttf"]244 candidates += [245 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",246 "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",247 "/Library/Fonts/Arial.ttf",248 "/System/Library/Fonts/Supplemental/Arial.ttf",249 r"C:\Windows\Fonts\arial.ttf",250 ]251 for p in candidates:252 try:253 p = Path(p)254 if p.exists():255 return ImageFont.truetype(str(p), size)256 except Exception:257 pass258 return ImageFont.load_default()259 260 # ---------- Geometry helper (flat-top regular hex that fits a bbox) ----------261 def hex_points_flat_top(width: int, height: int):262 A = height / 2.0 # apothem263 W_expected = (4.0 * A) / sqrt(3)264 scale_x = width / W_expected265 top_half = (A / sqrt(3)) * scale_x266 cx, cy = width / 2.0, height / 2.0267 pts = [268 (cx - top_half, 0), # top-left269 (cx + top_half, 0), # top-right270 (width, cy), # right-mid271 (cx + top_half, height), # bottom-right272 (cx - top_half, height), # bottom-left273 (0, cy), # left-mid274 ]275 return [(int(round(x)), int(round(y))) for (x, y) in pts]276 277 def create_save_png_qrcode(278 data: str,279 *,280 border_thickness: int = 60,281 border_color: str = "#EF5635",282 top_text: str = "RyGo QR",283 bottom_text: str = "Scan to open",284 top_font_size: int = 50,285 bottom_font_size: int = 50,286 text_color: str = "#000000",287 font_path: str | None = None,288 box_size: int = 20,289 qr_border_modules: int = 2290 ) -> bytes:291 if text_color is None:292 text_color = border_color293 294 # Use optional qrcode lib if available, else fallback placeholder295 qr_img = _make_qr_image(str(data), box_size=box_size, border=qr_border_modules)296 qr_w, qr_h = qr_img.size297 298 font_top = _get_font(top_font_size, font_path)299 font_bottom = _get_font(bottom_font_size, font_path)300 tmp = Image.new("RGB", (10, 10))301 dtmp = ImageDraw.Draw(tmp)302 303 def tsize(s, f):304 if not s:305 return (0, 0)306 l, t, r, b = dtmp.textbbox((0, 0), s, font=f)307 return (r - l, b - t)308 309 top_w, top_h = tsize(top_text, font_top)310 bot_w, bot_h = tsize(bottom_text, font_bottom)311 312 pad_top = max(8, int(top_font_size * 0.35))313 pad_bot = max(8, int(bottom_font_size * 0.35))314 top_band_h = (top_h + 2 * pad_top) if top_text else 0315 bottom_band_h = (bot_h + 2 * pad_bot) if bottom_text else 0316 317 inner_content_w = max(qr_w, top_w, bot_w)318 inner_content_h = top_band_h + qr_h + bottom_band_h319 320 need_top = (max(top_w, bot_w)) * (sqrt(3) / 2.0)321 need_center = inner_content_w * (sqrt(3) / 4.0)322 need_height = inner_content_h / 2.0323 A_in = int(round(max(need_top, need_center, need_height)))324 325 inner_w = int(round((4.0 * A_in) / sqrt(3)))326 inner_h = int(round(2.0 * A_in))327 328 A_out = A_in + border_thickness329 outer_w = int(round((4.0 * A_out) / sqrt(3)))330 outer_h = int(round(2.0 * A_out))331 332 canvas = Image.new("RGB", (outer_w, outer_h), "white")333 draw = ImageDraw.Draw(canvas)334 outer_hex = hex_points_flat_top(outer_w, outer_h)335 draw.polygon(outer_hex, fill=border_color)336 337 inner_offset = ((outer_w - inner_w) // 2, (outer_h - inner_h) // 2)338 inner_hex_abs = [(x + inner_offset[0], y + inner_offset[1]) for x, y in hex_points_flat_top(inner_w, inner_h)]339 draw.polygon(inner_hex_abs, fill="white")340 341 inner_img = Image.new("RGB", (inner_w, inner_h), "white")342 idraw = ImageDraw.Draw(inner_img)343 344 if top_text:345 tx = (inner_w - top_w) // 2346 ty = (top_band_h - top_h) // 2347 idraw.text((tx, ty), top_text, font=font_top, fill=text_color)348 349 qr_x = (inner_w - qr_w) // 2350 qr_y = top_band_h351 inner_img.paste(qr_img, (qr_x, qr_y))352 353 if bottom_text:354 bx = (inner_w - bot_w) // 2355 by = top_band_h + qr_h + (bottom_band_h - bot_h) // 2356 idraw.text((bx, by), bottom_text, font=font_bottom, fill=text_color)357 358 mask_local = Image.new("L", (inner_w, inner_h), 0)359 mdraw = ImageDraw.Draw(mask_local)360 mdraw.polygon(hex_points_flat_top(inner_w, inner_h), fill=255)361 canvas.paste(inner_img, inner_offset, mask_local)362 363 buf = io.BytesIO()364 canvas.save(buf, format="PNG")365 return buf.getvalue()366 367 # ========== MAIN MODULE ==========368 with gr.Blocks() as demo:369 370 with gr.Tab("Manage Access Points"):371 372 # ---- property options for current user ----373 def load_installer_options():374 conn = get_connection()375 cur = conn.cursor()376 cur.execute(377 'SELECT "UserCompanyID","UserCompany" '378 'FROM managerusers '379 'WHERE "Installers" = TRUE '380 'ORDER BY "UserCompany" ASC'381 )382 rows = cur.fetchall()383 cur.close()384 conn.close()385 opts: list[str] = ["<to be appointed>"]386 for cid, cname in rows:387 if cid is None or cname is None:388 continue389 opts.append(f"{int(cid)} | {str(cname)}")390 return opts391 392 def load_property_options():393 uid = _uid()394 print(f"DEBUG load_property(): ActiveUserID={uid}")395 396 if uid is None:397 return []398 conn = get_connection()399 cursor = conn.cursor()400 cursor.execute(401 'SELECT "PropID", "Name", "Owner" '402 'FROM property WHERE "UserID" = %s AND "Active" = TRUE',403 (uid,)404 )405 rows = cursor.fetchall()406 conn.close()407 seen = set()408 options = []409 for prop_id, name, owner in rows:410 key = (prop_id, name, owner)411 if key not in seen:412 seen.add(key)413 label = f"{name} ({owner}) [{prop_id}]"414 value = f"{prop_id}|{name}|{owner}"415 options.append((label, value))416 return options417 def _uid() -> Optional[int]:418 # convenience wrapper used throughout the module419 return resolve_and_cache_uid(get_user_id)420 421 def _prime_uid_then_fill_properties():422 # allow time for env/callback to be ready (HF cold start)423 deadline = time.time() + 10.0424 while resolve_and_cache_uid(get_user_id) is None and time.time() < deadline:425 time.sleep(0.1)426 427 uid = _uid()428 print("DEBUG (prime): starting with UID =", uid)429 430 if uid is None:431 return gr.update(choices=[], value=None, interactive=False)432 433 # Use options list [(label, value), ...]; build prop_map (label -> (PropID, Name, Owner))434 options = load_property_options()435 labels = [lbl for (lbl, _val) in options]436 437 prop_map.clear()438 for lbl, val in options:439 try:440 pid, pname, owner = val.split("|", 2)441 except ValueError:442 pid, pname, owner = "", "", ""443 prop_map[lbl] = (int(pid) if pid else None, pname, owner)444 445 return gr.update(choices=labels, value=None, interactive=bool(labels))446 447 def sync_accesspoints(prop_id):448 conn = get_connection()449 cursor = conn.cursor()450 cursor.execute(451 'SELECT "ID","PropID","NameOfAccessPoint","AccessPointID","UserID",'452 ' "RestrictedAP","APDeviceName","APInstaller","APInstallerID","ApInOut" '453 'FROM accesspoint '454 'WHERE "UserID" = %s AND "PropID" = %s '455 'ORDER BY lower("NameOfAccessPoint") ASC',456 (_uid(), int(prop_id))457 )458 rows = cursor.fetchall()459 conn.close()460 461 df = pd.DataFrame(462 rows,463 columns=[464 "ID","PropID","NameOfAccessPoint","AccessPointID","UserID",465 "RestrictedAP","APDeviceName","APInstaller","APInstallerID","ApInOut"466 ],467 )468 if not df.empty:469 df["AccessPointID"] = df["AccessPointID"].astype(str).str.replace(r"\.0$", "", regex=True)470 471 df_display = df.drop(columns=["ID", "PropID", "UserID"])472 if not df_display.empty:473 desired_front = [c for c in ["NameOfAccessPoint", "ApInOut", "AccessPointID"] if c in df_display.columns]474 others = [c for c in df_display.columns if c not in desired_front]475 df_display = df_display[desired_front + others]476 return df, df_display477 478 def get_row_options(df):479 if df is None or df.empty:480 return []481 return [482 (f"{row['NameOfAccessPoint']} - {row.get('ApInOut','')}", str(int(row["ID"])))483 for _, row in df.iterrows()484 ]485 486 def select_property(label):487 if not label or label not in prop_map:488 return (489 gr.update(visible=False), # accesspoint_table490 None, None,491 gr.update(choices=[], value=None, interactive=False, visible=True),492 gr.update(choices=[], value=None, interactive=False, visible=True),493 gr.update(visible=False), # apply_edit_btn494 gr.update(visible=False, interactive=False), # delete_btn495 gr.update(visible=False, interactive=False), # add_btn496 gr.update(visible=False), # edit_group497 gr.update(value="", visible=False), # error_msg498 gr.update(visible=False), # cancel_edit_btn499 gr.update(visible=False), # cancel_delete_btn500 )501 502 prop_id, prop_name, owner = prop_map[label]503 504 # Sync APProperty to property.Name505 try:506 conn_sync = get_connection()507 cur_sync = conn_sync.cursor()508 cur_sync.execute(509 'UPDATE accesspoint SET "APProperty" = %s '510 'WHERE "UserID" = %s AND "PropID" = %s '511 ' AND ( "APProperty" IS NULL OR btrim("APProperty") <> btrim(%s) )',512 (prop_name, _uid(), int(prop_id), prop_name)513 )514 if cur_sync.rowcount:515 print(f"[DEBUG] Synced accesspoint.APProperty rows: {cur_sync.rowcount} for PropID={prop_id}")516 conn_sync.commit()517 except Exception as e:518 try: conn_sync.rollback()519 except: pass520 print(f"❌ APProperty sync failed for PropID={prop_id}: {e}")521 finally:522 try:523 cur_sync.close(); conn_sync.close()524 except: pass525 526 df_full, df_display = sync_accesspoints(prop_id)527 options = get_row_options(df_full)528 529 return (530 gr.update(visible=True, value=df_display),531 df_full,532 prop_id,533 gr.update(choices=options, value=None, interactive=not df_full.empty, visible=True),534 gr.update(choices=options, value=None, interactive=not df_full.empty, visible=True),535 gr.update(visible=False),536 gr.update(visible=False, interactive=False),537 gr.update(visible=True, interactive=True),538 gr.update(visible=False),539 gr.update(value="", visible=False),540 gr.update(visible=False),541 gr.update(visible=False),542 )543 544 def select_row(row_id, df):545 if not row_id or df is None or df.empty:546 return (547 "", "", False, "", None,548 gr.update(choices=load_installer_options(), value=None),549 "",550 gr.update(visible=False),551 gr.update(visible=False, interactive=False),552 gr.update(interactive=True),553 gr.update(interactive=False),554 gr.update(visible=False),555 )556 try:557 selected_id = int(str(row_id))558 match = df[df["ID"] == selected_id]559 except Exception:560 match = pd.DataFrame()561 562 if match.empty:563 return (564 "", "", False, "", None,565 gr.update(choices=load_installer_options(), value=None),566 "",567 gr.update(visible=False),568 gr.update(visible=False, interactive=False),569 gr.update(interactive=True),570 gr.update(interactive=False),571 gr.update(visible=False),572 )573 574 row = match.iloc[0]575 opts = load_installer_options()576 ap_installer_id = row.get("APInstallerID")577 dd_value = "<to be appointed>"578 id_str = ""579 if pd.notna(ap_installer_id) and ap_installer_id is not None:580 target = str(int(ap_installer_id))581 for val in opts:582 if "|" in val:583 cid = val.split("|", 1)[0].strip()584 if cid == target:585 dd_value = val586 id_str = target587 break588 inout_val = row.get("ApInOut")589 inout_sel = str(inout_val) if isinstance(inout_val, str) and inout_val in ("In", "Out") else None590 591 return (592 row["NameOfAccessPoint"],593 gr.update(value=str(row["AccessPointID"]), interactive=False),594 row["RestrictedAP"],595 row["APDeviceName"],596 inout_sel,597 gr.update(choices=opts, value=dd_value),598 id_str,599 gr.update(visible=True),600 gr.update(visible=True, interactive=True),601 gr.update(interactive=False),602 gr.update(interactive=False),603 gr.update(visible=True),604 )605 606 def installer_changed(dd_value):607 if not dd_value:608 return ""609 try:610 cid, _ = str(dd_value).split("|", 1)611 return str(int(cid))612 except Exception:613 return ""614 615 def apply_edit(616 selected_row, df, new_name, new_phone, new_restrict, new_device,617 new_inout, dd_installer, ro_installer_id, prop_id618 ):619 if not selected_row or df is None or getattr(df, "empty", True):620 return (621 gr.update(), gr.update(), gr.update(),622 "", "", False, "", None,623 gr.update(choices=load_installer_options(), value=None), "",624 gr.update(visible=False),625 gr.update(visible=False),626 gr.update(visible=False, interactive=False),627 gr.update(interactive=True),628 gr.update(interactive=False),629 gr.update(visible=False),630 )631 row_id = None632 sval = str(selected_row).strip()633 try:634 row_id = int(sval)635 except Exception:636 m = re.search(r"\((\d+)\)$", sval)637 if m:638 row_id = int(m.group(1))639 if row_id is None:640 return (641 gr.update(), gr.update(), gr.update(),642 "", "", False, "", None,643 gr.update(choices=load_installer_options(), value=None), "",644 gr.update(visible=False),645 gr.update(visible=False),646 gr.update(visible=False, interactive=False),647 gr.update(interactive=True),648 gr.update(interactive=False),649 gr.update(visible=False),650 )651 652 sel_company_id = None653 sel_company = ""654 sentinel = "<to be appointed>"655 dd = (dd_installer or "").strip()656 if dd and dd != sentinel:657 try:658 cid, cname = dd.split("|", 1)659 sel_company_id = int(cid.strip())660 sel_company = cname.strip()661 except Exception:662 sel_company_id = None663 sel_company = ""664 else:665 sel_company_id = None666 sel_company = ""667 new_device = ""668 669 inout_value = new_inout if new_inout in ("In", "Out") else None670 671 conn = get_connection()672 cur = conn.cursor()673 try:674 cur.execute(675 'UPDATE accesspoint '676 'SET "NameOfAccessPoint"=%s, '677 ' "RestrictedAP"=%s, '678 ' "APDeviceName"=%s, '679 ' "ApInOut"=COALESCE(%s, "ApInOut"), '680 ' "APInstaller"=%s, '681 ' "APInstallerID"=%s '682 'WHERE "ID"=%s',683 (new_name, new_restrict, new_device, inout_value, sel_company, sel_company_id, row_id)684 )685 conn.commit()686 finally:687 cur.close()688 conn.close()689 690 df_updated, df_display = sync_accesspoints(prop_id)691 options = get_row_options(df_updated)692 return (693 gr.update(value=df_display),694 gr.update(choices=options, value=None),695 gr.update(choices=options, value=None, interactive=not df_updated.empty),696 "", "", False, "", None,697 gr.update(choices=load_installer_options(), value=None),698 "",699 gr.update(visible=False),700 gr.update(visible=False),701 gr.update(visible=False, interactive=False),702 gr.update(interactive=True),703 gr.update(interactive=False),704 gr.update(visible=False),705 )706 707 def cancel_edit():708 return (709 "", "", False, "", None,710 gr.update(choices=load_installer_options(), value=None),711 "",712 gr.update(visible=False),713 gr.update(visible=False, interactive=False),714 gr.update(interactive=True),715 gr.update(interactive=False),716 gr.update(visible=False),717 )718 719 def cancel_delete():720 return gr.update(value=None), gr.update(interactive=False, visible=False), gr.update(interactive=True), gr.update(visible=False)721 722 def add_row(prop_id):723 if not prop_id:724 return pd.DataFrame(), gr.update(), gr.update(), gr.update(), None725 new_id = generate_id()726 new_access_point_id = generate_id()727 name = "<enter access point name>"728 729 ap_property_name = _get_property_name(prop_id, _uid())730 ap_installer_value = ""731 ap_installer_id = None732 733 conn = get_connection()734 cursor = conn.cursor()735 cursor.execute(736 'INSERT INTO accesspoint '737 '("ID","PropID","NameOfAccessPoint","AccessPointID","UserID","RestrictedAP","APDeviceName","APInstaller","APInstallerID","APProperty") '738 'VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)',739 (new_id, int(prop_id), name, int(new_access_point_id), _uid(), False, "", ap_installer_value, ap_installer_id, ap_property_name)740 )741 conn.commit()742 conn.close()743 744 # Generate & save PNG (real QR if lib present; placeholder otherwise)745 bdata = create_save_png_qrcode(str(new_access_point_id))746 pngfile = str(new_access_point_id) + ".png"747 with open(pngfile, "wb") as f:748 f.write(bdata)749 print(f"QR image saved as {pngfile}")750 751 df_updated, df_display = sync_accesspoints(prop_id)752 options = get_row_options(df_updated)753 return (754 df_display,755 gr.update(choices=options, value=None),756 gr.update(choices=options, value=None),757 gr.update(choices=options, value=None),758 df_updated759 )760 761 def enable_delete_button(row_id):762 return gr.update(interactive=bool(row_id), visible=bool(row_id)), gr.update(visible=bool(row_id))763 764 def delete_row(row_id, df, prop_id):765 if not row_id or df is None or df.empty:766 return df, gr.update(), gr.update(), gr.update(interactive=False, visible=False), gr.update(visible=False), df767 try:768 selected_id = int(str(row_id))769 match = df[df["ID"] == selected_id]770 except Exception:771 match = pd.DataFrame()772 if match.empty:773 return df, gr.update(), gr.update(), gr.update(interactive=False, visible=False), gr.update(visible=False), df774 775 row_id_val = int(match.iloc[0]["ID"])776 conn = get_connection()777 cursor = conn.cursor()778 cursor.execute('DELETE FROM accesspoint WHERE "ID" = %s', (row_id_val,))779 conn.commit()780 conn.close()781 df_updated, df_display = sync_accesspoints(prop_id)782 options = get_row_options(df_updated)783 return df_display, gr.update(choices=options, value=None), gr.update(choices=options, value=None), gr.update(interactive=False, visible=False), gr.update(visible=False), df_updated784 785 # ---- small UI helpers for chaining ----786 def _activate_selects_if_options(df):787 has_rows = (df is not None) and (not getattr(df, "empty", True))788 return (789 gr.update(interactive=has_rows),790 gr.update(interactive=has_rows),791 )792 793 def _sync_delete_interactive_on_edit(selected_val):794 return gr.update(interactive=not bool(selected_val))795 796 def _enable_selectors():797 return gr.update(interactive=True), gr.update(interactive=True)798 799 def _enable_selectors_and_add():800 return gr.update(interactive=True), gr.update(interactive=True), gr.update(interactive=True)801 802 def _disable_both_selectors():803 return gr.update(interactive=False), gr.update(interactive=False)804 805 def _toggle_edit_and_add_on_delete(row_choice):806 active = bool(row_choice)807 if active:808 return gr.update(interactive=False), gr.update(interactive=False)809 else:810 return gr.update(), gr.update()811 812 def _refresh_table_and_both_selects(prop_id):813 if not prop_id:814 return gr.update(), None, gr.update(), gr.update()815 df_full, df_display = sync_accesspoints(prop_id)816 options = get_row_options(df_full)817 has_rows = (df_full is not None) and (not getattr(df_full, "empty", True))818 return (819 gr.update(value=df_display),820 df_full,821 gr.update(choices=options, value=None, interactive=has_rows),822 gr.update(choices=options, value=None, interactive=has_rows),823 )824 825 # ---- UI wiring ----826 prop_map: Dict[str, tuple[int | None, str, str]] = {}827 print("DEBUG (prime): starting with UID =", _uid())828 829 # Start empty; demo.load populates it830 prop_dropdown = gr.Dropdown(label="Select Property (Owner)", choices=[], value=None)831 832 btn_reload_props = gr.Button("Reload Properties", variant="secondary")833 btn_reload_props.click(_prime_uid_then_fill_properties, outputs=[prop_dropdown])834 835 accesspoint_table = gr.Dataframe(label="Tenant Users", visible=False, interactive=False)836 837 state_df = gr.State()838 state_prop_id = gr.State()839 840 demo.load(_prime_uid_then_fill_properties, inputs=[], outputs=[prop_dropdown])841 842 with gr.Row():843 add_btn = gr.Button("Add Row", interactive=False)844 845 with gr.Row():846 row_to_edit = gr.Dropdown(label="Select Row to Edit", choices=[], value=None, interactive=False)847 row_to_delete = gr.Dropdown(label="Select Row to Delete", choices=[], value=None, interactive=False)848 849 with gr.Row():850 delete_btn = gr.Button("Apply Delete", interactive=False, visible=False)851 cancel_delete_btn = gr.Button("Cancel Delete", visible=False)852 853 with gr.Group(visible=False) as edit_group:854 gr.Markdown("""<div style="background-color:#666666; color:#FFFFFF; padding:10px; border-radius:0px;">Access Point Details</div>""")855 with gr.Row():856 edit_phone = gr.Textbox(label="Access Point ID (auto)", interactive=False)857 edit_name = gr.Textbox(label="Name of Access Point")858 edit_inout = gr.Dropdown(label="Direction (In/Out)", choices=["In","Out"], value=None)859 edit_restrict = gr.Checkbox(label="Tick if Restricted AP")860 gr.Markdown("""<div style="background-color:#666666; color:#FFFFFF; padding:10px; border-radius:0px;">Access Point Installer details</div>""")861 with gr.Row():862 edit_installer_dd = gr.Dropdown(863 label="Installer (select company)",864 choices=load_installer_options(),865 value=None866 )867 edit_installer_id = gr.Textbox(label="Installer ID (auto)", interactive=False)868 edit_device = gr.Textbox(label="Installer's Device Name (auto))", interactive=False)869 with gr.Row():870 error_msg = gr.Markdown(visible=False)871 872 with gr.Row():873 apply_edit_btn = gr.Button("Apply Edit", interactive=False, visible=False)874 cancel_edit_btn = gr.Button("Cancel Edit", visible=False)875 876 # events877 prop_dropdown.change(878 select_property,879 inputs=prop_dropdown,880 outputs=[881 accesspoint_table, state_df, state_prop_id,882 row_to_edit, row_to_delete,883 apply_edit_btn, delete_btn, add_btn,884 edit_group, error_msg, cancel_edit_btn, cancel_delete_btn885 ],886 ).then(887 _activate_selects_if_options,888 inputs=[state_df],889 outputs=[row_to_edit, row_to_delete],890 )891 892 edit_installer_dd.change(installer_changed, inputs=edit_installer_dd, outputs=edit_installer_id)893 894 row_to_edit.change(895 select_row,896 inputs=[row_to_edit, state_df],897 outputs=[898 edit_name, edit_phone, edit_restrict, edit_device, edit_inout,899 edit_installer_dd, edit_installer_id,900 edit_group, apply_edit_btn, add_btn, delete_btn, cancel_edit_btn901 ]902 ).then(903 _sync_delete_interactive_on_edit,904 inputs=[row_to_edit],905 outputs=[row_to_delete]906 )907 908 cancel_edit_btn.click(909 cancel_edit,910 outputs=[edit_name, edit_phone, edit_restrict, edit_device, edit_inout,911 edit_installer_dd, edit_installer_id, edit_group, apply_edit_btn,912 add_btn, delete_btn, cancel_edit_btn]913 ).then(914 _refresh_table_and_both_selects,915 inputs=[state_prop_id],916 outputs=[accesspoint_table, state_df, row_to_edit, row_to_delete]917 ).then(918 _enable_selectors, inputs=[], outputs=[row_to_edit, row_to_delete]919 )920 921 row_to_delete.change(922 enable_delete_button, inputs=row_to_delete, outputs=[delete_btn, cancel_delete_btn]923 ).then(924 _toggle_edit_and_add_on_delete, inputs=[row_to_delete], outputs=[row_to_edit, add_btn]925 )926 927 cancel_delete_btn.click(928 cancel_delete,929 outputs=[row_to_delete, delete_btn, add_btn, cancel_delete_btn]930 ).then(931 _enable_selectors, inputs=[], outputs=[row_to_edit, row_to_delete]932 )933 934 apply_edit_btn.click(935 apply_edit,936 inputs=[row_to_edit, state_df, edit_name, edit_phone, edit_restrict, edit_device,937 edit_inout, edit_installer_dd, edit_installer_id, state_prop_id],938 outputs=[accesspoint_table, row_to_edit, row_to_delete,939 edit_name, edit_phone, edit_restrict, edit_device, edit_inout,940 edit_installer_dd, edit_installer_id, edit_group, error_msg,941 apply_edit_btn, add_btn, delete_btn, cancel_edit_btn]942 ).then(943 _refresh_table_and_both_selects,944 inputs=[state_prop_id],945 outputs=[accesspoint_table, state_df, row_to_edit, row_to_delete]946 ).then(947 _enable_selectors, inputs=[], outputs=[row_to_edit, row_to_delete]948 )949 950 add_btn.click(951 _disable_both_selectors, inputs=[], outputs=[row_to_edit, row_to_delete]952 ).then(953 add_row, inputs=state_prop_id,954 outputs=[accesspoint_table, row_to_edit, row_to_delete, row_to_edit, state_df]955 ).then(956 _enable_selectors, inputs=[], outputs=[row_to_edit, row_to_delete]957 )958 959 delete_btn.click(960 delete_row, inputs=[row_to_delete, state_df, state_prop_id],961 outputs=[accesspoint_table, row_to_edit, row_to_delete, delete_btn, cancel_delete_btn, state_df]962 ).then(963 _enable_selectors_and_add, inputs=[], outputs=[row_to_edit, row_to_delete, add_btn]964 )965 966 return demo967 968# app = launch_accesspoints_module()969# app.launch(share=True)970 971 972 