Enhance ClickUp task creation, smart field setting, and Cora distribution comments
- Add set_custom_field_smart() to auto-resolve dropdown option UUIDs - Extend create_task with priority, assignees, time_estimate params - Expand clickup_create_task tool and CLI script with tags, due dates, custom fields - Add _comment_distributed_tasks to post clear ClickUp comments on Cora distribution (e.g. "Cora XLSX moved to cora-inbox" / "content-cora-inbox") - Remove unused _find_all_todo_tasks; simplify AutoCora sibling matching - Add tests for set_custom_field_smart dropdown and text fields Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>fix/customer-field-migration
parent
0d5450fb65
commit
fb4498b978
|
|
@ -93,6 +93,8 @@ uv add --group test <package>
|
||||||
| `identity/SOUL.md` | Agent personality |
|
| `identity/SOUL.md` | Agent personality |
|
||||||
| `identity/USER.md` | User profile |
|
| `identity/USER.md` | User profile |
|
||||||
| `skills/` | Markdown skill files with YAML frontmatter |
|
| `skills/` | Markdown skill files with YAML frontmatter |
|
||||||
|
| `scripts/create_clickup_task.py` | CLI script to create ClickUp tasks |
|
||||||
|
| `docs/clickup-task-creation.md` | Task creation conventions, per-type fields, and defaults |
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -456,6 +456,50 @@ class ClickUpClient:
|
||||||
log.error("Failed to set field '%s' on task %s: %s", field_name, task_id, e)
|
log.error("Failed to set field '%s' on task %s: %s", field_name, task_id, e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def set_custom_field_smart(
|
||||||
|
self, task_id: str, list_id: str, field_name: str, value: str
|
||||||
|
) -> bool:
|
||||||
|
"""Set a custom field by name, auto-resolving dropdown option UUIDs.
|
||||||
|
|
||||||
|
For dropdown fields, *value* is matched against option names
|
||||||
|
(case-insensitive). For all other field types, *value* is passed through.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
fields = self.get_custom_fields(list_id)
|
||||||
|
target = None
|
||||||
|
for f in fields:
|
||||||
|
if f.get("name") == field_name:
|
||||||
|
target = f
|
||||||
|
break
|
||||||
|
|
||||||
|
if not target:
|
||||||
|
log.warning("Field '%s' not found in list %s", field_name, list_id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
field_id = target["id"]
|
||||||
|
resolved = value
|
||||||
|
|
||||||
|
if target.get("type") == "drop_down":
|
||||||
|
options = target.get("type_config", {}).get("options", [])
|
||||||
|
for opt in options:
|
||||||
|
if opt.get("name", "").lower() == value.lower():
|
||||||
|
resolved = opt["id"]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
log.warning(
|
||||||
|
"Dropdown option '%s' not found for field '%s'",
|
||||||
|
value,
|
||||||
|
field_name,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.set_custom_field_value(task_id, field_id, resolved)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(
|
||||||
|
"Failed to set field '%s' on task %s: %s", field_name, task_id, e
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
def get_custom_field_by_name(self, task_id: str, field_name: str) -> Any:
|
def get_custom_field_by_name(self, task_id: str, field_name: str) -> Any:
|
||||||
"""Read a custom field value from a task by field name.
|
"""Read a custom field value from a task by field name.
|
||||||
|
|
||||||
|
|
@ -478,6 +522,9 @@ class ClickUpClient:
|
||||||
due_date: int | None = None,
|
due_date: int | None = None,
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
custom_fields: list[dict] | None = None,
|
custom_fields: list[dict] | None = None,
|
||||||
|
priority: int | None = None,
|
||||||
|
assignees: list[int] | None = None,
|
||||||
|
time_estimate: int | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Create a new task in a ClickUp list.
|
"""Create a new task in a ClickUp list.
|
||||||
|
|
||||||
|
|
@ -489,6 +536,9 @@ class ClickUpClient:
|
||||||
due_date: Due date as Unix timestamp in milliseconds.
|
due_date: Due date as Unix timestamp in milliseconds.
|
||||||
tags: List of tag names to apply.
|
tags: List of tag names to apply.
|
||||||
custom_fields: List of custom field dicts ({"id": ..., "value": ...}).
|
custom_fields: List of custom field dicts ({"id": ..., "value": ...}).
|
||||||
|
priority: 1=Urgent, 2=High, 3=Normal, 4=Low.
|
||||||
|
assignees: List of ClickUp user IDs.
|
||||||
|
time_estimate: Time estimate in milliseconds.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
API response dict containing task id, url, etc.
|
API response dict containing task id, url, etc.
|
||||||
|
|
@ -502,6 +552,12 @@ class ClickUpClient:
|
||||||
payload["tags"] = tags
|
payload["tags"] = tags
|
||||||
if custom_fields:
|
if custom_fields:
|
||||||
payload["custom_fields"] = custom_fields
|
payload["custom_fields"] = custom_fields
|
||||||
|
if priority is not None:
|
||||||
|
payload["priority"] = priority
|
||||||
|
if assignees:
|
||||||
|
payload["assignees"] = assignees
|
||||||
|
if time_estimate is not None:
|
||||||
|
payload["time_estimate"] = time_estimate
|
||||||
|
|
||||||
def _call():
|
def _call():
|
||||||
resp = self._client.post(f"/list/{list_id}/task", json=payload)
|
resp = self._client.post(f"/list/{list_id}/task", json=payload)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
|
@ -1146,6 +1147,72 @@ class Scheduler:
|
||||||
category="autocora",
|
category="autocora",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Post ClickUp comment on matched tasks
|
||||||
|
self._comment_distributed_tasks(client, xlsx_path.stem, copied_to, tasks, stem)
|
||||||
|
|
||||||
|
def _comment_distributed_tasks(
|
||||||
|
self, client, xlsx_stem: str, copied_to: list[str], tasks, normalized_stem: str
|
||||||
|
):
|
||||||
|
"""Post a ClickUp comment on tasks when a Cora report is distributed."""
|
||||||
|
from .tools.autocora import _slugify
|
||||||
|
from .tools.linkbuilding import _fuzzy_keyword_match, _normalize_for_match
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
for dest in copied_to:
|
||||||
|
if dest.startswith("link"):
|
||||||
|
parts.append("cora-inbox")
|
||||||
|
elif dest.startswith("content"):
|
||||||
|
parts.append("content-cora-inbox")
|
||||||
|
else:
|
||||||
|
parts.append(dest)
|
||||||
|
dest_label = " and ".join(parts)
|
||||||
|
comment = f"Cora XLSX moved to {dest_label}."
|
||||||
|
|
||||||
|
# Try to find task_ids from job JSON files
|
||||||
|
task_ids: list[str] = []
|
||||||
|
jobs_dir = Path(self.config.autocora.jobs_dir)
|
||||||
|
slug = _slugify(xlsx_stem)
|
||||||
|
|
||||||
|
if jobs_dir.is_dir():
|
||||||
|
# Check both jobs/ root and processed/ subfolder
|
||||||
|
search_dirs = [jobs_dir]
|
||||||
|
processed = jobs_dir / "processed"
|
||||||
|
if processed.is_dir():
|
||||||
|
search_dirs.append(processed)
|
||||||
|
|
||||||
|
for search_dir in search_dirs:
|
||||||
|
for job_file in search_dir.glob("job-*.json"):
|
||||||
|
# Strip "job-{timestamp}-" prefix to get the slug
|
||||||
|
parts = job_file.stem.split("-", 2)
|
||||||
|
if len(parts) >= 3:
|
||||||
|
job_slug = parts[2]
|
||||||
|
if job_slug == slug:
|
||||||
|
try:
|
||||||
|
data = json.loads(job_file.read_text(encoding="utf-8"))
|
||||||
|
task_ids = data.get("task_ids", [])
|
||||||
|
except (json.JSONDecodeError, OSError) as e:
|
||||||
|
log.warning("Could not read job file %s: %s", job_file, e)
|
||||||
|
break
|
||||||
|
if task_ids:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Fallback: match from the task list we already have
|
||||||
|
if not task_ids:
|
||||||
|
for task in tasks:
|
||||||
|
keyword = task.custom_fields.get("Keyword", "")
|
||||||
|
if not keyword:
|
||||||
|
continue
|
||||||
|
keyword_norm = _normalize_for_match(str(keyword))
|
||||||
|
if _fuzzy_keyword_match(normalized_stem, keyword_norm):
|
||||||
|
task_ids.append(task.id)
|
||||||
|
|
||||||
|
# Post comments
|
||||||
|
for tid in task_ids:
|
||||||
|
try:
|
||||||
|
client.add_comment(tid, comment)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning("Failed to comment on task %s: %s", tid, e)
|
||||||
|
|
||||||
# ── Morning Briefing ──
|
# ── Morning Briefing ──
|
||||||
|
|
||||||
_CENTRAL = ZoneInfo("America/Chicago")
|
_CENTRAL = ZoneInfo("America/Chicago")
|
||||||
|
|
|
||||||
|
|
@ -217,18 +217,6 @@ def _is_lookahead(task, today_end_ms, lookahead_end_ms) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _find_all_todo_tasks(client, config, categories: list[str]):
|
|
||||||
"""Find ALL 'to do' tasks in cora_categories (no date filter).
|
|
||||||
|
|
||||||
Used to find sibling tasks sharing the same keyword.
|
|
||||||
"""
|
|
||||||
space_id = config.clickup.space_id
|
|
||||||
if not space_id:
|
|
||||||
return []
|
|
||||||
|
|
||||||
tasks = client.get_tasks_from_space(space_id, statuses=["to do"])
|
|
||||||
return [t for t in tasks if t.task_type in categories]
|
|
||||||
|
|
||||||
|
|
||||||
def _group_by_keyword(tasks, all_tasks):
|
def _group_by_keyword(tasks, all_tasks):
|
||||||
"""Group tasks by normalized keyword, pulling in sibling tasks from all_tasks.
|
"""Group tasks by normalized keyword, pulling in sibling tasks from all_tasks.
|
||||||
|
|
@ -324,11 +312,8 @@ def submit_autocora_jobs(target_date: str = "", ctx: dict | None = None) -> str:
|
||||||
if not qualifying:
|
if not qualifying:
|
||||||
return f"No qualifying tasks found ({label})."
|
return f"No qualifying tasks found ({label})."
|
||||||
|
|
||||||
# Find ALL to-do tasks in cora categories for sibling keyword matching
|
# Group by keyword — only siblings that also passed the sweep qualify
|
||||||
all_todo = _find_all_todo_tasks(client, config, autocora.cora_categories)
|
groups, alerts = _group_by_keyword(qualifying, qualifying)
|
||||||
|
|
||||||
# Group by keyword
|
|
||||||
groups, alerts = _group_by_keyword(qualifying, all_todo)
|
|
||||||
|
|
||||||
if not groups and alerts:
|
if not groups and alerts:
|
||||||
return "No jobs submitted.\n\n" + "\n".join(f"- {a}" for a in alerts)
|
return "No jobs submitted.\n\n" + "\n".join(f"- {a}" for a in alerts)
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,9 @@ def clickup_task_status(task_id: str, ctx: dict | None = None) -> str:
|
||||||
@tool(
|
@tool(
|
||||||
"clickup_create_task",
|
"clickup_create_task",
|
||||||
"Create a new ClickUp task for a client. Requires task name and client name. "
|
"Create a new ClickUp task for a client. Requires task name and client name. "
|
||||||
"Optionally set work category, description, and status. "
|
"Optionally set work category, description, status, due_date (Unix ms), "
|
||||||
|
"tags (comma-separated), and arbitrary custom fields via custom_fields_json "
|
||||||
|
'(JSON object like {"Keyword":"value","CLIFlags":"--tier1-count 5"}). '
|
||||||
"The task is created in the 'Overall' list within the client's folder.",
|
"The task is created in the 'Overall' list within the client's folder.",
|
||||||
category="clickup",
|
category="clickup",
|
||||||
)
|
)
|
||||||
|
|
@ -173,9 +175,17 @@ def clickup_create_task(
|
||||||
work_category: str = "",
|
work_category: str = "",
|
||||||
description: str = "",
|
description: str = "",
|
||||||
status: str = "to do",
|
status: str = "to do",
|
||||||
|
due_date: str = "",
|
||||||
|
tags: str = "",
|
||||||
|
custom_fields_json: str = "",
|
||||||
|
priority: int = 2,
|
||||||
|
assignee: int = 10765627,
|
||||||
|
time_estimate_ms: int = 0,
|
||||||
ctx: dict | None = None,
|
ctx: dict | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Create a new ClickUp task in the client's Overall list."""
|
"""Create a new ClickUp task in the client's Overall list."""
|
||||||
|
import json as _json
|
||||||
|
|
||||||
client_obj = _get_clickup_client(ctx)
|
client_obj = _get_clickup_client(ctx)
|
||||||
if not client_obj:
|
if not client_obj:
|
||||||
return "Error: ClickUp API token not configured."
|
return "Error: ClickUp API token not configured."
|
||||||
|
|
@ -188,29 +198,48 @@ def clickup_create_task(
|
||||||
# Find the client's Overall list
|
# Find the client's Overall list
|
||||||
list_id = client_obj.find_list_in_folder(cfg.space_id, client)
|
list_id = client_obj.find_list_in_folder(cfg.space_id, client)
|
||||||
if not list_id:
|
if not list_id:
|
||||||
return f"Error: Could not find folder '{client}' with an 'Overall' list in space."
|
return (
|
||||||
|
f"Error: Could not find folder '{client}' "
|
||||||
|
f"with an 'Overall' list in space."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build create kwargs
|
||||||
|
create_kwargs: dict = {
|
||||||
|
"list_id": list_id,
|
||||||
|
"name": name,
|
||||||
|
"description": description,
|
||||||
|
"status": status,
|
||||||
|
"priority": priority,
|
||||||
|
"assignees": [assignee],
|
||||||
|
}
|
||||||
|
if due_date:
|
||||||
|
create_kwargs["due_date"] = int(due_date)
|
||||||
|
if tags:
|
||||||
|
create_kwargs["tags"] = [t.strip() for t in tags.split(",")]
|
||||||
|
if time_estimate_ms:
|
||||||
|
create_kwargs["time_estimate"] = time_estimate_ms
|
||||||
|
|
||||||
# Create the task
|
# Create the task
|
||||||
result = client_obj.create_task(
|
result = client_obj.create_task(**create_kwargs)
|
||||||
list_id=list_id,
|
|
||||||
name=name,
|
|
||||||
description=description,
|
|
||||||
status=status,
|
|
||||||
)
|
|
||||||
task_id = result.get("id", "")
|
task_id = result.get("id", "")
|
||||||
task_url = result.get("url", "")
|
task_url = result.get("url", "")
|
||||||
|
|
||||||
# Set Client dropdown field
|
# Set Client dropdown field
|
||||||
client_obj.set_custom_field_by_name(task_id, "Client", client)
|
client_obj.set_custom_field_smart(task_id, list_id, "Client", client)
|
||||||
|
|
||||||
# Set Work Category if provided
|
# Set Work Category if provided
|
||||||
if work_category:
|
if work_category:
|
||||||
field_info = client_obj.discover_field_filter(list_id, "Work Category")
|
client_obj.set_custom_field_smart(
|
||||||
if field_info and work_category in field_info["options"]:
|
task_id, list_id, "Work Category", work_category
|
||||||
option_id = field_info["options"][work_category]
|
)
|
||||||
client_obj.set_custom_field_value(task_id, field_info["field_id"], option_id)
|
|
||||||
else:
|
# Set any additional custom fields
|
||||||
log.warning("Work Category '%s' not found in dropdown options", work_category)
|
if custom_fields_json:
|
||||||
|
extra_fields = _json.loads(custom_fields_json)
|
||||||
|
for field_name, field_value in extra_fields.items():
|
||||||
|
client_obj.set_custom_field_smart(
|
||||||
|
task_id, list_id, field_name, str(field_value)
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
f"Task created successfully!\n"
|
f"Task created successfully!\n"
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
uv run python scripts/create_clickup_task.py --name "Task" --client "Client"
|
uv run python scripts/create_clickup_task.py --name "Task" --client "Client"
|
||||||
uv run python scripts/create_clickup_task.py --name "PR" --client "Acme" \\
|
uv run python scripts/create_clickup_task.py --name "LB" --client "Acme" \\
|
||||||
--category "Press Release"
|
--category "Link Building" --due-date 2026-03-11 --tag feb26 \\
|
||||||
|
--field "Keyword=some keyword" --field "CLIFlags=--tier1-count 5"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -12,6 +13,7 @@ import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Add project root to path so we can import cheddahbot
|
# Add project root to path so we can import cheddahbot
|
||||||
|
|
@ -21,16 +23,80 @@ from dotenv import load_dotenv
|
||||||
|
|
||||||
from cheddahbot.clickup import ClickUpClient
|
from cheddahbot.clickup import ClickUpClient
|
||||||
|
|
||||||
|
DEFAULT_ASSIGNEE = 10765627 # Bryan Bigari
|
||||||
|
|
||||||
|
|
||||||
|
def _date_to_unix_ms(date_str: str) -> int:
|
||||||
|
"""Convert YYYY-MM-DD to Unix milliseconds (noon UTC).
|
||||||
|
|
||||||
|
Noon UTC ensures the date displays correctly in US timezones.
|
||||||
|
"""
|
||||||
|
dt = datetime.strptime(date_str, "%Y-%m-%d").replace(
|
||||||
|
hour=12, tzinfo=UTC
|
||||||
|
)
|
||||||
|
return int(dt.timestamp() * 1000)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_time_estimate(s: str) -> int:
|
||||||
|
"""Parse a human time string like '2h', '30m', '1h30m' to ms."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
total_min = 0
|
||||||
|
match = re.match(r"(?:(\d+)h)?(?:(\d+)m)?$", s.strip())
|
||||||
|
if not match or not any(match.groups()):
|
||||||
|
raise ValueError(f"Invalid time estimate: '{s}' (use e.g. '2h', '30m', '1h30m')")
|
||||||
|
if match.group(1):
|
||||||
|
total_min += int(match.group(1)) * 60
|
||||||
|
if match.group(2):
|
||||||
|
total_min += int(match.group(2))
|
||||||
|
return total_min * 60 * 1000
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Create a ClickUp task")
|
parser = argparse.ArgumentParser(description="Create a ClickUp task")
|
||||||
parser.add_argument("--name", required=True, help="Task name")
|
parser.add_argument("--name", required=True, help="Task name")
|
||||||
parser.add_argument("--client", required=True, help="Client folder name in ClickUp")
|
parser.add_argument(
|
||||||
parser.add_argument("--category", default="", help="Work Category (e.g. 'Press Release')")
|
"--client", required=True, help="Client folder name"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--category", default="", help="Work Category dropdown value"
|
||||||
|
)
|
||||||
parser.add_argument("--description", default="", help="Task description")
|
parser.add_argument("--description", default="", help="Task description")
|
||||||
parser.add_argument("--status", default="to do", help="Initial status (default: 'to do')")
|
parser.add_argument(
|
||||||
|
"--status", default="to do", help="Initial status (default: 'to do')"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--due-date", default="", help="Due date as YYYY-MM-DD"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--tag", action="append", default=[], help="Tag (mmmYY, repeatable)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--field",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="Custom field as Name=Value (repeatable)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--priority",
|
||||||
|
type=int,
|
||||||
|
default=2,
|
||||||
|
help="Priority: 1=Urgent, 2=High, 3=Normal, 4=Low (default: 2)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--assignee",
|
||||||
|
type=int,
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="ClickUp user ID (default: Bryan 10765627)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--time-estimate",
|
||||||
|
default="",
|
||||||
|
help="Time estimate (e.g. '2h', '30m', '1h30m')",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
api_token = os.environ.get("CLICKUP_API_TOKEN", "")
|
api_token = os.environ.get("CLICKUP_API_TOKEN", "")
|
||||||
|
|
@ -43,6 +109,15 @@ def main():
|
||||||
print("Error: CLICKUP_SPACE_ID not set", file=sys.stderr)
|
print("Error: CLICKUP_SPACE_ID not set", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Parse custom fields
|
||||||
|
custom_fields: dict[str, str] = {}
|
||||||
|
for f in args.field:
|
||||||
|
if "=" not in f:
|
||||||
|
print(f"Error: --field must be Name=Value, got: {f}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
name, value = f.split("=", 1)
|
||||||
|
custom_fields[name] = value
|
||||||
|
|
||||||
client = ClickUpClient(api_token=api_token)
|
client = ClickUpClient(api_token=api_token)
|
||||||
try:
|
try:
|
||||||
# Find the client's Overall list
|
# Find the client's Overall list
|
||||||
|
|
@ -52,27 +127,47 @@ def main():
|
||||||
print(msg, file=sys.stderr)
|
print(msg, file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Create the task
|
# Build create_task kwargs
|
||||||
result = client.create_task(
|
create_kwargs: dict = {
|
||||||
list_id=list_id,
|
"list_id": list_id,
|
||||||
name=args.name,
|
"name": args.name,
|
||||||
description=args.description,
|
"description": args.description,
|
||||||
status=args.status,
|
"status": args.status,
|
||||||
|
}
|
||||||
|
if args.due_date:
|
||||||
|
create_kwargs["due_date"] = _date_to_unix_ms(args.due_date)
|
||||||
|
if args.tag:
|
||||||
|
create_kwargs["tags"] = args.tag
|
||||||
|
create_kwargs["priority"] = args.priority
|
||||||
|
create_kwargs["assignees"] = args.assignee or [DEFAULT_ASSIGNEE]
|
||||||
|
if args.time_estimate:
|
||||||
|
create_kwargs["time_estimate"] = _parse_time_estimate(
|
||||||
|
args.time_estimate
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Create the task
|
||||||
|
result = client.create_task(**create_kwargs)
|
||||||
task_id = result.get("id", "")
|
task_id = result.get("id", "")
|
||||||
|
|
||||||
# Set Client dropdown field
|
# Set Client dropdown field
|
||||||
client.set_custom_field_by_name(task_id, "Client", args.client)
|
client.set_custom_field_smart(task_id, list_id, "Client", args.client)
|
||||||
|
|
||||||
# Set Work Category if provided
|
# Set Work Category if provided
|
||||||
if args.category:
|
if args.category:
|
||||||
field_info = client.discover_field_filter(list_id, "Work Category")
|
client.set_custom_field_smart(
|
||||||
if field_info and args.category in field_info["options"]:
|
task_id, list_id, "Work Category", args.category
|
||||||
option_id = field_info["options"][args.category]
|
)
|
||||||
client.set_custom_field_value(task_id, field_info["field_id"], option_id)
|
|
||||||
else:
|
# Set any additional custom fields
|
||||||
msg = f"Warning: Work Category '{args.category}' not found"
|
for field_name, field_value in custom_fields.items():
|
||||||
print(msg, file=sys.stderr)
|
ok = client.set_custom_field_smart(
|
||||||
|
task_id, list_id, field_name, field_value
|
||||||
|
)
|
||||||
|
if not ok:
|
||||||
|
print(
|
||||||
|
f"Warning: Failed to set '{field_name}'",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
print(json.dumps({
|
print(json.dumps({
|
||||||
"id": task_id,
|
"id": task_id,
|
||||||
|
|
|
||||||
|
|
@ -204,9 +204,7 @@ class TestSubmitAutocoraJobs:
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"cheddahbot.tools.autocora._find_qualifying_tasks", lambda *a, **kw: [task]
|
"cheddahbot.tools.autocora._find_qualifying_tasks", lambda *a, **kw: [task]
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(
|
|
||||||
"cheddahbot.tools.autocora._find_all_todo_tasks", lambda *a, **kw: [task]
|
|
||||||
)
|
|
||||||
|
|
||||||
result = submit_autocora_jobs(target_date="2025-01-01", ctx=ctx)
|
result = submit_autocora_jobs(target_date="2025-01-01", ctx=ctx)
|
||||||
assert "Submitted 1 job" in result
|
assert "Submitted 1 job" in result
|
||||||
|
|
@ -233,9 +231,7 @@ class TestSubmitAutocoraJobs:
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"cheddahbot.tools.autocora._find_qualifying_tasks", lambda *a, **kw: [task]
|
"cheddahbot.tools.autocora._find_qualifying_tasks", lambda *a, **kw: [task]
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(
|
|
||||||
"cheddahbot.tools.autocora._find_all_todo_tasks", lambda *a, **kw: [task]
|
|
||||||
)
|
|
||||||
|
|
||||||
submit_autocora_jobs(target_date="2025-01-01", ctx=ctx)
|
submit_autocora_jobs(target_date="2025-01-01", ctx=ctx)
|
||||||
|
|
||||||
|
|
@ -256,9 +252,7 @@ class TestSubmitAutocoraJobs:
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"cheddahbot.tools.autocora._find_qualifying_tasks", lambda *a, **kw: [task]
|
"cheddahbot.tools.autocora._find_qualifying_tasks", lambda *a, **kw: [task]
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(
|
|
||||||
"cheddahbot.tools.autocora._find_all_todo_tasks", lambda *a, **kw: [task]
|
|
||||||
)
|
|
||||||
|
|
||||||
# First submit
|
# First submit
|
||||||
submit_autocora_jobs(target_date="2025-01-01", ctx=ctx)
|
submit_autocora_jobs(target_date="2025-01-01", ctx=ctx)
|
||||||
|
|
@ -278,9 +272,7 @@ class TestSubmitAutocoraJobs:
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"cheddahbot.tools.autocora._find_qualifying_tasks", lambda *a, **kw: [task]
|
"cheddahbot.tools.autocora._find_qualifying_tasks", lambda *a, **kw: [task]
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(
|
|
||||||
"cheddahbot.tools.autocora._find_all_todo_tasks", lambda *a, **kw: [task]
|
|
||||||
)
|
|
||||||
|
|
||||||
result = submit_autocora_jobs(target_date="2025-01-01", ctx=ctx)
|
result = submit_autocora_jobs(target_date="2025-01-01", ctx=ctx)
|
||||||
assert "missing Keyword" in result
|
assert "missing Keyword" in result
|
||||||
|
|
@ -296,9 +288,7 @@ class TestSubmitAutocoraJobs:
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"cheddahbot.tools.autocora._find_qualifying_tasks", lambda *a, **kw: [task]
|
"cheddahbot.tools.autocora._find_qualifying_tasks", lambda *a, **kw: [task]
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(
|
|
||||||
"cheddahbot.tools.autocora._find_all_todo_tasks", lambda *a, **kw: [task]
|
|
||||||
)
|
|
||||||
|
|
||||||
result = submit_autocora_jobs(target_date="2025-01-01", ctx=ctx)
|
result = submit_autocora_jobs(target_date="2025-01-01", ctx=ctx)
|
||||||
assert "Submitted 1 job" in result
|
assert "Submitted 1 job" in result
|
||||||
|
|
|
||||||
|
|
@ -569,3 +569,73 @@ class TestClickUpClient:
|
||||||
result = client.find_list_in_folder("space_1", "NonExistent Client")
|
result = client.find_list_in_folder("space_1", "NonExistent Client")
|
||||||
assert result is None
|
assert result is None
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
def test_set_custom_field_smart_dropdown(self):
|
||||||
|
"""Resolves dropdown option name to UUID automatically."""
|
||||||
|
respx.get(f"{BASE_URL}/list/list_1/field").mock(
|
||||||
|
return_value=httpx.Response(
|
||||||
|
200,
|
||||||
|
json={
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"id": "cf_lb",
|
||||||
|
"name": "LB Method",
|
||||||
|
"type": "drop_down",
|
||||||
|
"type_config": {
|
||||||
|
"options": [
|
||||||
|
{"id": "opt_cora", "name": "Cora Backlinks"},
|
||||||
|
{"id": "opt_manual", "name": "Manual"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
respx.post(f"{BASE_URL}/task/t1/field/cf_lb").mock(
|
||||||
|
return_value=httpx.Response(200, json={})
|
||||||
|
)
|
||||||
|
|
||||||
|
client = ClickUpClient(api_token="pk_test")
|
||||||
|
result = client.set_custom_field_smart(
|
||||||
|
"t1", "list_1", "LB Method", "Cora Backlinks"
|
||||||
|
)
|
||||||
|
assert result is True
|
||||||
|
import json
|
||||||
|
|
||||||
|
body = json.loads(respx.calls.last.request.content)
|
||||||
|
assert body["value"] == "opt_cora"
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
def test_set_custom_field_smart_text(self):
|
||||||
|
"""Passes text field values through without resolution."""
|
||||||
|
respx.get(f"{BASE_URL}/list/list_1/field").mock(
|
||||||
|
return_value=httpx.Response(
|
||||||
|
200,
|
||||||
|
json={
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"id": "cf_kw",
|
||||||
|
"name": "Keyword",
|
||||||
|
"type": "short_text",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
respx.post(f"{BASE_URL}/task/t1/field/cf_kw").mock(
|
||||||
|
return_value=httpx.Response(200, json={})
|
||||||
|
)
|
||||||
|
|
||||||
|
client = ClickUpClient(api_token="pk_test")
|
||||||
|
result = client.set_custom_field_smart(
|
||||||
|
"t1", "list_1", "Keyword", "shaft manufacturing"
|
||||||
|
)
|
||||||
|
assert result is True
|
||||||
|
import json
|
||||||
|
|
||||||
|
body = json.loads(respx.calls.last.request.content)
|
||||||
|
assert body["value"] == "shaft manufacturing"
|
||||||
|
client.close()
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ from cheddahbot.config import AutoCoraConfig, Config, ContentConfig, LinkBuildin
|
||||||
class FakeTask:
|
class FakeTask:
|
||||||
"""Minimal ClickUp task stub for distribution tests."""
|
"""Minimal ClickUp task stub for distribution tests."""
|
||||||
|
|
||||||
|
id: str = "fake_id"
|
||||||
name: str = ""
|
name: str = ""
|
||||||
task_type: str = ""
|
task_type: str = ""
|
||||||
custom_fields: dict = field(default_factory=dict)
|
custom_fields: dict = field(default_factory=dict)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue