Spaces:
Sleeping
Sleeping
Update media_plan_templater.py
Browse files- media_plan_templater.py +213 -0
media_plan_templater.py
CHANGED
|
@@ -344,6 +344,219 @@ def render(get_conn):
|
|
| 344 |
|
| 345 |
package_opts = opts["packages"]
|
| 346 |
rate_opts = opts["rates"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
|
| 348 |
# Column config (Product column labeled as Package; Placement shown as text in grid)
|
| 349 |
column_config = {
|
|
|
|
| 344 |
|
| 345 |
package_opts = opts["packages"]
|
| 346 |
rate_opts = opts["rates"]
|
| 347 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 348 |
+
# Guided 5-row entry (independent controls, then write into the table)
|
| 349 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 350 |
+
st.subheader("Guided entry (5 rows)")
|
| 351 |
+
st.caption("Select Package → Placement (dependent), pick Targeting presets, add Budget/Rate, then click **Populate table**.")
|
| 352 |
+
|
| 353 |
+
# one-time init to hold the guided rows' state
|
| 354 |
+
if "guided_rows" not in st.session_state:
|
| 355 |
+
st.session_state["guided_rows"] = [
|
| 356 |
+
{
|
| 357 |
+
"Media plan": "",
|
| 358 |
+
"Budget": None,
|
| 359 |
+
"Rate": rate_opts[0] if rate_opts else "",
|
| 360 |
+
"Product": "", # Package
|
| 361 |
+
"Placement": "",
|
| 362 |
+
"Details": "",
|
| 363 |
+
"Targeting": [], # list for multiselect, will stringify as "A OR B"
|
| 364 |
+
"Notes": "",
|
| 365 |
+
}
|
| 366 |
+
for _ in range(DEFAULT_INPUT_ROWS)
|
| 367 |
+
]
|
| 368 |
+
|
| 369 |
+
def _placements_for(pkg: str) -> list[str]:
|
| 370 |
+
pkg = (pkg or "").strip()
|
| 371 |
+
return opts["placementMap"].get(pkg, [])
|
| 372 |
+
|
| 373 |
+
def _details_for(pkg: str, placement: str) -> str:
|
| 374 |
+
# PACKAGE_EXIST → bullets from extraction; else chosen placement
|
| 375 |
+
exists = opts["packageExistsMap"].get((pkg or "").strip(), False)
|
| 376 |
+
if exists:
|
| 377 |
+
bullets = opts["extractionMap"].get((pkg or "").strip(), [])
|
| 378 |
+
return "\n".join(f"• {b}" for b in bullets) if bullets else ""
|
| 379 |
+
return (placement or "").strip()
|
| 380 |
+
|
| 381 |
+
with st.form("guided_form_5rows", clear_on_submit=False):
|
| 382 |
+
for i in range(DEFAULT_INPUT_ROWS):
|
| 383 |
+
row_state = st.session_state["guided_rows"][i]
|
| 384 |
+
|
| 385 |
+
st.markdown(f"**Row {i+1}**")
|
| 386 |
+
c1, c2, c3, c4 = st.columns([1.1, 1.1, 1.2, 1.2])
|
| 387 |
+
with c1:
|
| 388 |
+
row_state["Media plan"] = st.text_input(
|
| 389 |
+
"Media plan",
|
| 390 |
+
key=f"g_media_{i}",
|
| 391 |
+
value=row_state["Media plan"],
|
| 392 |
+
placeholder="e.g., MP-01",
|
| 393 |
+
)
|
| 394 |
+
with c2:
|
| 395 |
+
row_state["Budget"] = st.number_input(
|
| 396 |
+
"Budget",
|
| 397 |
+
key=f"g_budget_{i}",
|
| 398 |
+
value=0.0 if row_state["Budget"] in (None, "") else float(row_state["Budget"]),
|
| 399 |
+
min_value=0.0,
|
| 400 |
+
step=100.0,
|
| 401 |
+
format="%0.2f",
|
| 402 |
+
)
|
| 403 |
+
with c3:
|
| 404 |
+
row_state["Rate"] = st.selectbox(
|
| 405 |
+
"Rate",
|
| 406 |
+
options=rate_opts,
|
| 407 |
+
key=f"g_rate_{i}",
|
| 408 |
+
index=max(0, rate_opts.index(row_state["Rate"])) if (row_state["Rate"] in rate_opts) else 0,
|
| 409 |
+
)
|
| 410 |
+
with c4:
|
| 411 |
+
row_state["Product"] = st.selectbox(
|
| 412 |
+
"Package",
|
| 413 |
+
options=[""] + package_opts,
|
| 414 |
+
key=f"g_pkg_{i}",
|
| 415 |
+
index=([""] + package_opts).index(row_state["Product"]) if row_state["Product"] in ([""] + package_opts) else 0,
|
| 416 |
+
help="Choose a Package first.",
|
| 417 |
+
)
|
| 418 |
+
|
| 419 |
+
# dependent Placement (disabled when PACKAGE_EXIST = yes)
|
| 420 |
+
pkg = row_state["Product"]
|
| 421 |
+
exists = opts["packageExistsMap"].get((pkg or "").strip(), False)
|
| 422 |
+
choices = _placements_for(pkg)
|
| 423 |
+
|
| 424 |
+
cc1, cc2 = st.columns([1.2, 2.0])
|
| 425 |
+
with cc1:
|
| 426 |
+
if exists:
|
| 427 |
+
# disable Placement when package is a bundle
|
| 428 |
+
st.selectbox(
|
| 429 |
+
"Placement",
|
| 430 |
+
options=["(package-defined elements; no placement selection)"],
|
| 431 |
+
index=0,
|
| 432 |
+
key=f"g_plc_disabled_{i}",
|
| 433 |
+
disabled=True,
|
| 434 |
+
help="This package defines its own elements.",
|
| 435 |
+
)
|
| 436 |
+
row_state["Placement"] = ""
|
| 437 |
+
else:
|
| 438 |
+
# allow selection when package is not a bundle
|
| 439 |
+
if choices:
|
| 440 |
+
default_ix = choices.index(row_state["Placement"]) if row_state["Placement"] in choices else 0
|
| 441 |
+
row_state["Placement"] = st.selectbox(
|
| 442 |
+
"Placement",
|
| 443 |
+
options=choices,
|
| 444 |
+
index=default_ix,
|
| 445 |
+
key=f"g_plc_{i}",
|
| 446 |
+
help="Placement options depend on the selected Package.",
|
| 447 |
+
)
|
| 448 |
+
else:
|
| 449 |
+
st.selectbox(
|
| 450 |
+
"Placement",
|
| 451 |
+
options=["(no placements for this package)"],
|
| 452 |
+
index=0,
|
| 453 |
+
key=f"g_plc_empty_{i}",
|
| 454 |
+
disabled=True,
|
| 455 |
+
)
|
| 456 |
+
row_state["Placement"] = ""
|
| 457 |
+
|
| 458 |
+
with cc2:
|
| 459 |
+
# Targeting multi-select (stores a list; will stringify later)
|
| 460 |
+
row_state["Targeting"] = st.multiselect(
|
| 461 |
+
"Targeting (presets)",
|
| 462 |
+
options=TARGETING_OPTIONS,
|
| 463 |
+
default=[t for t in (row_state["Targeting"] or []) if t in TARGETING_OPTIONS],
|
| 464 |
+
key=f"g_tgt_{i}",
|
| 465 |
+
help="Pick one or more. Will be saved as 'A OR B'.",
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
# Notes
|
| 469 |
+
row_state["Notes"] = st.text_input(
|
| 470 |
+
"Notes",
|
| 471 |
+
key=f"g_notes_{i}",
|
| 472 |
+
value=row_state["Notes"],
|
| 473 |
+
placeholder="Optional",
|
| 474 |
+
)
|
| 475 |
+
|
| 476 |
+
# Details preview (derived)
|
| 477 |
+
details_preview = _details_for(pkg, row_state["Placement"])
|
| 478 |
+
row_state["Details"] = details_preview
|
| 479 |
+
st.caption("**Details preview**")
|
| 480 |
+
st.text_area(
|
| 481 |
+
label=f"Details (auto)",
|
| 482 |
+
key=f"g_details_preview_{i}",
|
| 483 |
+
value=details_preview,
|
| 484 |
+
height=80,
|
| 485 |
+
disabled=True,
|
| 486 |
+
)
|
| 487 |
+
st.divider()
|
| 488 |
+
|
| 489 |
+
cleft, cright = st.columns([1, 1])
|
| 490 |
+
with cleft:
|
| 491 |
+
reset_guided = st.form_submit_button("Reset guided rows")
|
| 492 |
+
with cright:
|
| 493 |
+
populate = st.form_submit_button("Populate table")
|
| 494 |
+
|
| 495 |
+
if reset_guided:
|
| 496 |
+
st.session_state["guided_rows"] = [
|
| 497 |
+
{
|
| 498 |
+
"Media plan": "",
|
| 499 |
+
"Budget": None,
|
| 500 |
+
"Rate": rate_opts[0] if rate_opts else "",
|
| 501 |
+
"Product": "",
|
| 502 |
+
"Placement": "",
|
| 503 |
+
"Details": "",
|
| 504 |
+
"Targeting": [],
|
| 505 |
+
"Notes": "",
|
| 506 |
+
}
|
| 507 |
+
for _ in range(DEFAULT_INPUT_ROWS)
|
| 508 |
+
]
|
| 509 |
+
st.experimental_rerun()
|
| 510 |
+
|
| 511 |
+
if populate:
|
| 512 |
+
# Validate rows & build DataFrame
|
| 513 |
+
issues = []
|
| 514 |
+
out_rows = []
|
| 515 |
+
for i, r in enumerate(st.session_state["guided_rows"], start=1):
|
| 516 |
+
pkg = (r["Product"] or "").strip()
|
| 517 |
+
if not pkg:
|
| 518 |
+
# allow entirely blank rows to be skipped
|
| 519 |
+
if not any((r["Media plan"], r["Budget"], r["Rate"], r["Placement"], r["Notes"], r["Targeting"])):
|
| 520 |
+
continue
|
| 521 |
+
issues.append(f"Row {i}: Package is required.")
|
| 522 |
+
continue
|
| 523 |
+
|
| 524 |
+
exists = opts["packageExistsMap"].get(pkg, False)
|
| 525 |
+
placement = (r["Placement"] or "").strip()
|
| 526 |
+
if not exists:
|
| 527 |
+
if not placement:
|
| 528 |
+
issues.append(f"Row {i}: Placement required for non-bundle package '{pkg}'.")
|
| 529 |
+
continue
|
| 530 |
+
|
| 531 |
+
# stringify targeting
|
| 532 |
+
tgt_str = " OR ".join(r["Targeting"]) if r.get("Targeting") else ""
|
| 533 |
+
|
| 534 |
+
out_rows.append({
|
| 535 |
+
"Media plan": r["Media plan"],
|
| 536 |
+
"Budget": r["Budget"],
|
| 537 |
+
"Rate": r["Rate"],
|
| 538 |
+
"Product": pkg,
|
| 539 |
+
"Details": _details_for(pkg, placement),
|
| 540 |
+
"Targeting": tgt_str,
|
| 541 |
+
"Notes": r["Notes"],
|
| 542 |
+
"Placement": placement,
|
| 543 |
+
})
|
| 544 |
+
|
| 545 |
+
if issues:
|
| 546 |
+
st.error("Please fix the following before populating:\n- " + "\n- ".join(issues))
|
| 547 |
+
else:
|
| 548 |
+
if not out_rows:
|
| 549 |
+
st.warning("No rows to populate.")
|
| 550 |
+
else:
|
| 551 |
+
df_new = pd.DataFrame(out_rows, dtype="object")
|
| 552 |
+
# Push into the main input table (normalize + keep Placement column)
|
| 553 |
+
canonical = _normalize_input_table(df_new[DEFAULT_INPUT_COLUMNS])
|
| 554 |
+
canonical["Rate"] = df_new["Rate"].fillna("").astype(str)
|
| 555 |
+
if "Placement" in df_new.columns:
|
| 556 |
+
canonical["Placement"] = df_new["Placement"].fillna("").astype(str)
|
| 557 |
+
st.session_state["media_plan_inputs"] = canonical
|
| 558 |
+
st.success(f"Populated {len(canonical)} row(s) into the input table.")
|
| 559 |
+
st.dataframe(canonical, use_container_width=True)
|
| 560 |
|
| 561 |
# Column config (Product column labeled as Package; Placement shown as text in grid)
|
| 562 |
column_config = {
|