From 6250918e5e4954748f7efe4a8898f4b91295d49c Mon Sep 17 00:00:00 2001 From: PeninsulaInd Date: Mon, 9 Mar 2026 12:23:14 -0500 Subject: [PATCH] Migrate ClickUp Customer field to space-level Client field Replaces all "Customer" field lookups with "Client" to match the new space-wide dropdown, eliminating the 20+ duplicate list-level fields. Includes migration script that populated 400 active tasks. Co-Authored-By: Claude Opus 4.6 --- cheddahbot/api.py | 8 +- cheddahbot/scheduler.py | 2 +- cheddahbot/tools/press_release.py | 2 +- config.yaml | 2 +- scripts/migrate_client_field.py | 161 ++++++++++++++++++++++++++++++ tests/test_scheduler.py | 4 +- 6 files changed, 170 insertions(+), 9 deletions(-) create mode 100644 scripts/migrate_client_field.py diff --git a/cheddahbot/api.py b/cheddahbot/api.py index 190206a..964df71 100644 --- a/cheddahbot/api.py +++ b/cheddahbot/api.py @@ -125,7 +125,7 @@ async def get_tasks_by_company(): data = await get_tasks() by_company: dict[str, list] = {} for task in data.get("tasks", []): - company = task["custom_fields"].get("Customer") or "Unassigned" + company = task["custom_fields"].get("Client") or "Unassigned" by_company.setdefault(company, []).append(task) # Sort companies by task count descending @@ -238,7 +238,7 @@ async def get_link_building_tasks(): in_progress_not_started.append(t) by_company: dict[str, list] = {} for task in active_lb: - company = task["custom_fields"].get("Customer") or "Unassigned" + company = task["custom_fields"].get("Client") or "Unassigned" by_company.setdefault(company, []).append(task) result = { @@ -320,7 +320,7 @@ async def get_need_cora_tasks(): if kw_lower not in by_keyword: by_keyword[kw_lower] = { "keyword": kw, - "company": t["custom_fields"].get("Customer") or "Unassigned", + "company": t["custom_fields"].get("Client") or "Unassigned", "due_date": t.get("due_date"), "tasks": [], } @@ -367,7 +367,7 @@ async def get_press_release_tasks(): by_company: dict[str, list] = {} for task in pr_tasks: - company = task["custom_fields"].get("Customer") or "Unassigned" + company = task["custom_fields"].get("Client") or "Unassigned" by_company.setdefault(company, []).append(task) return { diff --git a/cheddahbot/scheduler.py b/cheddahbot/scheduler.py index b472662..88c0df4 100644 --- a/cheddahbot/scheduler.py +++ b/cheddahbot/scheduler.py @@ -1282,7 +1282,7 @@ class Scheduler: """Group tasks by their Customer custom field.""" groups: dict[str, list] = {} for t in tasks: - customer = t.custom_fields.get("Customer", "") or "Unknown" + customer = t.custom_fields.get("Client", "") or "Unknown" groups.setdefault(str(customer), []).append(t) return groups diff --git a/cheddahbot/tools/press_release.py b/cheddahbot/tools/press_release.py index baf9256..8c4f4f9 100644 --- a/cheddahbot/tools/press_release.py +++ b/cheddahbot/tools/press_release.py @@ -88,7 +88,7 @@ def _find_clickup_task(ctx: dict, company_name: str) -> str: if task.task_type != "Press Release": continue - client_field = task.custom_fields.get("Customer", "") + client_field = task.custom_fields.get("Client", "") if not ( _fuzzy_company_match(company_name, task.name) or _fuzzy_company_match(company_name, client_field) diff --git a/config.yaml b/config.yaml index 8d36105..b9f2f64 100644 --- a/config.yaml +++ b/config.yaml @@ -58,7 +58,7 @@ clickup: required_fields: [topic, company_name, target_url] field_mapping: topic: "PR Topic" - company_name: "Customer" + company_name: "Client" target_url: "IMSURL" branded_url: "SocialURL" "On Page Optimization": diff --git a/scripts/migrate_client_field.py b/scripts/migrate_client_field.py new file mode 100644 index 0000000..55e89f2 --- /dev/null +++ b/scripts/migrate_client_field.py @@ -0,0 +1,161 @@ +"""Migrate ClickUp 'Customer' (list-level) → 'Client' (space-level) field. + +This script does NOT create the field — you must create the space-level "Client" +dropdown manually in ClickUp UI first, using the company names this script prints. + +Steps: + 1. Fetch all folders, filter to those with an 'Overall' list + 2. Print sorted company names for dropdown creation + 3. Pause for you to create the field in ClickUp UI + 4. Discover the new 'Client' field's UUID + option IDs + 5. Set 'Client' on every active task (folder name as value) + 6. Report results + +Usage: + DRY_RUN=1 uv run python scripts/migrate_client_field.py # preview only + uv run python scripts/migrate_client_field.py # live run +""" + +from __future__ import annotations + +import os +import sys +import time +from pathlib import Path + +# Allow running from repo root +_root = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(_root)) + +from dotenv import load_dotenv + +load_dotenv(_root / ".env") + +from cheddahbot.clickup import ClickUpClient + +# ── Config ────────────────────────────────────────────────────────────────── +DRY_RUN = os.environ.get("DRY_RUN", "0") not in ("0", "false", "") +NEW_FIELD_NAME = "Client" + +API_TOKEN = os.environ.get("CLICKUP_API_TOKEN", "") +SPACE_ID = os.environ.get("CLICKUP_SPACE_ID", "") + +if not API_TOKEN: + sys.exit("ERROR: CLICKUP_API_TOKEN env var is required") +if not SPACE_ID: + sys.exit("ERROR: CLICKUP_SPACE_ID env var is required") + + +def main() -> None: + client = ClickUpClient(api_token=API_TOKEN) + + # 1. Get folders, filter to those with an Overall list + print(f"\n{'=' * 60}") + print(f" Migrate to '{NEW_FIELD_NAME}' field -- Space {SPACE_ID}") + print(f" Mode: {'DRY RUN' if DRY_RUN else 'LIVE'}") + print(f"{'=' * 60}\n") + + folders = client.get_folders(SPACE_ID) + print(f"Found {len(folders)} folders:\n") + + client_folders = [] + for f in folders: + overall = next( + (lst for lst in f["lists"] if lst["name"].lower() == "overall"), None + ) + if overall: + print(f" {f['name']:35s} Overall list: {overall['id']}") + client_folders.append({"name": f["name"], "overall_id": overall["id"]}) + else: + print(f" {f['name']:35s} [SKIP - no Overall list]") + + if not client_folders: + sys.exit("\nNo client folders with Overall lists found.") + + # 2. Print company names for dropdown creation + option_names = sorted(cf["name"] for cf in client_folders) + print(f"\n--- Dropdown options for '{NEW_FIELD_NAME}' ({len(option_names)}) ---") + for name in option_names: + print(f" {name}") + + # 3. Build plan: fetch active tasks per folder + print("\nFetching active tasks from Overall lists ...") + plan: list[dict] = [] + for cf in client_folders: + tasks = client.get_tasks(cf["overall_id"], include_closed=False) + plan.append({ + "folder_name": cf["name"], + "list_id": cf["overall_id"], + "tasks": tasks, + }) + + total_tasks = sum(len(p["tasks"]) for p in plan) + print(f"\n--- Update Plan (active tasks only) ---") + for p in plan: + print(f" {p['folder_name']:35s} {len(p['tasks']):3d} tasks") + print(f" {'TOTAL':35s} {total_tasks:3d} tasks") + + if DRY_RUN: + print("\n** DRY RUN -- no changes made. Unset DRY_RUN to execute. **\n") + return + + # 4. Field should already be created in ClickUp UI + print(f"\nLooking for space-level '{NEW_FIELD_NAME}' field ...") + + # 5. Discover the field UUID + option IDs + first_list_id = client_folders[0]["overall_id"] + print(f"\nDiscovering '{NEW_FIELD_NAME}' field UUID and option IDs ...") + field_info = client.discover_field_filter(first_list_id, NEW_FIELD_NAME) + if field_info is None: + sys.exit( + f"\nERROR: Could not find '{NEW_FIELD_NAME}' field on list {first_list_id}.\n" + f"Make sure you created it as a SPACE-level field (visible to all lists)." + ) + + field_id = field_info["field_id"] + option_map = field_info["options"] # {name: uuid} + print(f" Field ID: {field_id}") + print(f" Options found: {len(option_map)}") + + # Verify all folder names have matching options + missing = [cf["name"] for cf in client_folders if cf["name"] not in option_map] + if missing: + print(f"\n WARNING: These folder names have no matching dropdown option:") + for name in missing: + print(f" - {name}") + print(" Tasks in these folders will be SKIPPED.") + + # 6. Set Client field on each task + updated = 0 + skipped = 0 + failed = 0 + for p in plan: + folder_name = p["folder_name"] + opt_id = option_map.get(folder_name) + if not opt_id: + skipped += len(p["tasks"]) + print(f"\n SKIP: '{folder_name}' -- no matching option") + continue + + print(f"\nUpdating {len(p['tasks'])} tasks in '{folder_name}' ...") + for task in p["tasks"]: + ok = client.set_custom_field_value(task.id, field_id, opt_id) + if ok: + updated += 1 + else: + failed += 1 + print(f" FAILED: task {task.id} ({task.name})") + time.sleep(0.15) + + print(f"\n{'=' * 60}") + print(f" Done! Updated: {updated} | Skipped: {skipped} | Failed: {failed}") + print(f"{'=' * 60}") + print(f"\n Next steps:") + print(f" 1. Verify tasks in ClickUp have the '{NEW_FIELD_NAME}' field set correctly") + print(f" 2. Update config.yaml: change 'Customer' → '{NEW_FIELD_NAME}' in field_mapping") + print(f" 3. Test CheddahBot with the new field") + print(f" 4. Delete the old list-level 'Customer' fields from ClickUp\n") + + +if __name__ == "__main__": + main() diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index a92bbe9..31e09a8 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -17,7 +17,7 @@ _PR_MAPPING = { "auto_execute": True, "field_mapping": { "topic": "task_name", - "company_name": "Customer", + "company_name": "Client", }, } @@ -69,7 +69,7 @@ def _now_ms(): return int(datetime.now(UTC).timestamp() * 1000) -_FIELDS = {"Customer": "Acme"} +_FIELDS = {"Client": "Acme"} # ── Tests ──