Spaces:
Sleeping
Sleeping
Update media_plan_templater.py
Browse files- media_plan_templater.py +104 -91
media_plan_templater.py
CHANGED
|
@@ -610,107 +610,120 @@ def render(get_conn):
|
|
| 610 |
|
| 611 |
updated_inputs = edited_inputs.copy()
|
| 612 |
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
|
|
|
| 624 |
for idx, row in updated_inputs.iterrows():
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 638 |
)
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
choices = build_row_placement_choices(row)
|
| 645 |
-
if choices:
|
| 646 |
-
current = (row.get("Placement") or "").strip()
|
| 647 |
-
default_ix = choices.index(current) if current in choices else 0
|
| 648 |
-
sel = st.selectbox(
|
| 649 |
-
label,
|
| 650 |
-
choices,
|
| 651 |
-
index=default_ix,
|
| 652 |
-
key=f"placement_row_{idx}",
|
| 653 |
-
)
|
| 654 |
-
updated_inputs.at[idx, "Placement"] = sel
|
| 655 |
-
# When package doesn't exist, user must select a placement; Details = selection
|
| 656 |
-
updated_inputs.at[idx, "Details"] = sel
|
| 657 |
-
else:
|
| 658 |
-
updated_inputs.at[idx, "Placement"] = ""
|
| 659 |
-
updated_inputs.at[idx, "Details"] = ""
|
| 660 |
st.selectbox(
|
| 661 |
-
|
| 662 |
-
options=["(
|
| 663 |
index=0,
|
| 664 |
-
key=f"
|
| 665 |
disabled=True,
|
| 666 |
)
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 670 |
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
|
|
|
|
|
|
|
|
|
| 676 |
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
|
|
|
| 684 |
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
if "Rate" in updated_inputs.columns:
|
| 692 |
-
canonical_inputs["Rate"] = updated_inputs["Rate"].fillna("").astype(str)
|
| 693 |
-
if "Placement" in updated_inputs.columns:
|
| 694 |
-
canonical_inputs["Placement"] = updated_inputs["Placement"].fillna("").astype(str)
|
| 695 |
-
|
| 696 |
-
st.session_state["media_plan_inputs"] = canonical_inputs
|
| 697 |
-
|
| 698 |
-
# Downloadable, cleaned CSV of inputs
|
| 699 |
-
download_df = st.session_state["media_plan_inputs"].copy()
|
| 700 |
-
if "Targeting" in download_df.columns:
|
| 701 |
-
download_df["Targeting"] = download_df["Targeting"].apply(_normalize_targeting_value)
|
| 702 |
-
for numeric_col in ("Budget", "Rate"):
|
| 703 |
-
if numeric_col in download_df.columns:
|
| 704 |
-
download_df[numeric_col] = download_df[numeric_col].apply(
|
| 705 |
-
lambda val: "" if pd.isna(val) else val
|
| 706 |
)
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
|
| 715 |
# (The remainder of your app continues here: Word upload/parsing, Excel template
|
| 716 |
# mapping, fill + export logic, etc. The code above includes the PACKAGE_EXIST
|
|
|
|
| 610 |
|
| 611 |
updated_inputs = edited_inputs.copy()
|
| 612 |
|
| 613 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 614 |
+
# Row-by-row controls (all selectors in one visual row per table row)
|
| 615 |
+
# - Package β Placement dependency (bundle disables placement)
|
| 616 |
+
# - Targeting multiselect, Notes
|
| 617 |
+
# - No Details preview UI; Details is set silently
|
| 618 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 619 |
+
|
| 620 |
+
st.markdown("#### Row controls")
|
| 621 |
+
|
| 622 |
+
# Fallbacks: allow placement selection even if Package is blank
|
| 623 |
+
ALL_PLACEMENTS = sorted({plc for lst in opts["placementMap"].values() for plc in lst})
|
| 624 |
+
|
| 625 |
for idx, row in updated_inputs.iterrows():
|
| 626 |
+
pkg_current = (row.get("Product") or "").strip()
|
| 627 |
+
rate_current = (row.get("Rate") or "").strip()
|
| 628 |
+
plc_current = (row.get("Placement") or "").strip()
|
| 629 |
+
notes_current = str(row.get("Notes") or "")
|
| 630 |
+
tgt_raw = str(row.get("Targeting") or "")
|
| 631 |
+
|
| 632 |
+
# Preselect Targeting choices from existing string ("A OR B")
|
| 633 |
+
tgt_tokens = [p.strip() for p in re.split(r"\s+OR\s+", tgt_raw) if p.strip()]
|
| 634 |
+
tgt_default = [t for t in tgt_tokens if t in TARGETING_OPTIONS]
|
| 635 |
+
|
| 636 |
+
# One visual row: Package | Placement | Rate | Targeting | Notes
|
| 637 |
+
c_pkg, c_plc, c_rate, c_tgt, c_notes = st.columns([1.2, 1.4, 1.0, 2.0, 1.8])
|
| 638 |
+
|
| 639 |
+
with c_pkg:
|
| 640 |
+
pkg_options = [""] + package_opts
|
| 641 |
+
pkg_index = pkg_options.index(pkg_current) if pkg_current in pkg_options else 0
|
| 642 |
+
sel_pkg = st.selectbox(
|
| 643 |
+
f"Row {idx+1} β Package",
|
| 644 |
+
options=pkg_options,
|
| 645 |
+
index=pkg_index,
|
| 646 |
+
key=f"row{idx}_pkg",
|
| 647 |
+
help="Choose a Package. If left blank, you can still pick any Placement."
|
| 648 |
)
|
| 649 |
+
|
| 650 |
+
with c_plc:
|
| 651 |
+
# Determine placement choices based on Package
|
| 652 |
+
pkg_exists = opts["packageExistsMap"].get(sel_pkg.strip(), False) if sel_pkg else False
|
| 653 |
+
if pkg_exists:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
st.selectbox(
|
| 655 |
+
"Placement",
|
| 656 |
+
options=["(package-defined elements)"],
|
| 657 |
index=0,
|
| 658 |
+
key=f"row{idx}_plc_disabled",
|
| 659 |
disabled=True,
|
| 660 |
)
|
| 661 |
+
sel_plc = ""
|
| 662 |
+
else:
|
| 663 |
+
choices = opts["placementMap"].get(sel_pkg.strip(), []) if sel_pkg else ALL_PLACEMENTS
|
| 664 |
+
if choices:
|
| 665 |
+
plc_index = choices.index(plc_current) if plc_current in choices else 0
|
| 666 |
+
sel_plc = st.selectbox(
|
| 667 |
+
"Placement",
|
| 668 |
+
options=choices,
|
| 669 |
+
index=plc_index,
|
| 670 |
+
key=f"row{idx}_plc",
|
| 671 |
+
help="Placement options depend on the selected Package."
|
| 672 |
+
)
|
| 673 |
+
else:
|
| 674 |
+
st.selectbox(
|
| 675 |
+
"Placement",
|
| 676 |
+
options=["(no placements available)"],
|
| 677 |
+
index=0,
|
| 678 |
+
key=f"row{idx}_plc_empty",
|
| 679 |
+
disabled=True,
|
| 680 |
+
)
|
| 681 |
+
sel_plc = ""
|
| 682 |
|
| 683 |
+
with c_rate:
|
| 684 |
+
rate_index = rate_opts.index(rate_current) if rate_current in rate_opts else 0
|
| 685 |
+
sel_rate = st.selectbox(
|
| 686 |
+
"Rate",
|
| 687 |
+
options=rate_opts,
|
| 688 |
+
index=rate_index,
|
| 689 |
+
key=f"row{idx}_rate",
|
| 690 |
+
)
|
| 691 |
|
| 692 |
+
with c_tgt:
|
| 693 |
+
sel_tgts = st.multiselect(
|
| 694 |
+
"Targeting",
|
| 695 |
+
options=TARGETING_OPTIONS,
|
| 696 |
+
default=tgt_default,
|
| 697 |
+
key=f"row{idx}_tgt",
|
| 698 |
+
help="Pick one or more presets; saved as 'A OR B'."
|
| 699 |
+
)
|
| 700 |
|
| 701 |
+
with c_notes:
|
| 702 |
+
sel_notes = st.text_input(
|
| 703 |
+
"Notes",
|
| 704 |
+
value=notes_current,
|
| 705 |
+
key=f"row{idx}_notes",
|
| 706 |
+
placeholder="Optional",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 707 |
)
|
| 708 |
+
|
| 709 |
+
# Write back to the table
|
| 710 |
+
updated_inputs.at[idx, "Product"] = sel_pkg
|
| 711 |
+
updated_inputs.at[idx, "Rate"] = sel_rate
|
| 712 |
+
updated_inputs.at[idx, "Notes"] = sel_notes
|
| 713 |
+
|
| 714 |
+
if sel_tgts:
|
| 715 |
+
updated_inputs.at[idx, "Targeting"] = " OR ".join(sel_tgts)
|
| 716 |
+
|
| 717 |
+
# Details logic (silent)
|
| 718 |
+
if sel_pkg and opts["packageExistsMap"].get(sel_pkg.strip(), False):
|
| 719 |
+
# Bundle: Details = package elements; no placement
|
| 720 |
+
updated_inputs.at[idx, "Placement"] = ""
|
| 721 |
+
bullets = opts["extractionMap"].get(sel_pkg.strip(), [])
|
| 722 |
+
updated_inputs.at[idx, "Details"] = "\n".join(f"β’ {b}" for b in bullets) if bullets else ""
|
| 723 |
+
else:
|
| 724 |
+
# Non-bundle or no package: Details = chosen placement
|
| 725 |
+
updated_inputs.at[idx, "Placement"] = sel_plc
|
| 726 |
+
updated_inputs.at[idx, "Details"] = sel_plc
|
| 727 |
|
| 728 |
# (The remainder of your app continues here: Word upload/parsing, Excel template
|
| 729 |
# mapping, fill + export logic, etc. The code above includes the PACKAGE_EXIST
|