Social composer: send now and rename messy account names (#6)
Deploy Beta / unit-tests (push) Successful in 16s
Deploy Beta / docker (push) Successful in 20s
Deploy Beta / deploy-beta (push) Successful in 3m11s

## Summary
Closes #5.
- Composer can **Send now** without a schedule time (Schedule and Save draft still available)
- Account names wrap and show the platform id so a bad OAuth label is still readable
- Social accounts page can rename the display name; re-auth keeps a custom name instead of overwriting it with a placeholder
- Worker polls `dispatch_due` every 15s so scheduled campaigns and social posts fire without a manual command; local `docker compose up` now starts the worker

## Test plan
- [ ] Open Social → Compose: confirm **Send now**, **Schedule**, and **Save draft** buttons (no combined Save / publish)
- [ ] Send now with a connected account and caption → post queues immediately, no datetime required
- [ ] Schedule with a datetime → post is scheduled; Schedule with a blank time → validation error
- [ ] Composer account list shows full name plus external id, wrapping if the name is long
- [ ] Social accounts → rename a messy label → new name shows in composer without reconnecting
- [ ] Re-authorize an account that was renamed → custom name is kept
- [ ] Rebuild and start compose (`docker compose up --build`) so the **worker** service is running
- [ ] Schedule a social post ~2 minutes ahead → after due time, worker logs `Enqueued N due item(s).` and the post leaves Scheduled (published or failed)
- [ ] `docker compose logs -f worker` shows `Polling due scheduled work every 15s.`

Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
2026-08-24 09:47:58 -07:00
parent 71de919c34
commit e8611ef926
14 changed files with 433 additions and 38 deletions
+35 -9
View File
@@ -162,6 +162,17 @@ def account_list(request):
if request.method == "POST":
action = (request.POST.get("action") or "connect").strip()
if action == "rename":
pk = request.POST.get("account_id")
account = get_object_or_404(SocialAccount, pk=pk)
label = (request.POST.get("label") or "").strip()[:120]
if not label:
messages.error(request, "Enter a display name.")
return redirect("social:account_list")
account.label = label
account.save(update_fields=["label", "updated_at"])
messages.success(request, f"Renamed account to {account.label}.")
return redirect("social:account_list")
if action == "disconnect":
pk = request.POST.get("account_id")
account = get_object_or_404(SocialAccount, pk=pk)
@@ -361,11 +372,14 @@ def linkedin_oauth_callback(request):
profile = linkedin_fetch_member_profile(token_payload["access_token"])
author_urn = profile["author_urn"]
blob = linkedin_token_blob_from_oauth(token_payload, author_urn=author_urn)
existing = SocialAccount.objects.filter(
platform=Platform.LINKEDIN, external_id=author_urn
).first()
account, created = SocialAccount.objects.update_or_create(
platform=Platform.LINKEDIN,
external_id=author_urn,
defaults={
"label": profile["label"],
"label": SocialAccount.resolve_label(existing, profile["label"]),
"encrypted_tokens": encrypt_tokens(json.dumps(blob)),
"is_active": True,
"owner": request.user,
@@ -473,11 +487,14 @@ def meta_oauth_complete(request):
)
for page in ig_pages:
blob = instagram_token_blob(page, user_token=user_token)
existing = SocialAccount.objects.filter(
platform=Platform.INSTAGRAM, external_id=blob["ig_user_id"]
).first()
account, created = SocialAccount.objects.update_or_create(
platform=Platform.INSTAGRAM,
external_id=blob["ig_user_id"],
defaults={
"label": blob["label"],
"label": SocialAccount.resolve_label(existing, blob["label"]),
"encrypted_tokens": encrypt_tokens(json.dumps(blob)),
"is_active": True,
"owner": request.user,
@@ -487,12 +504,15 @@ def meta_oauth_complete(request):
else:
for page in pages:
blob = facebook_token_blob(page, user_token=user_token)
label = (page.get("name") or "").strip() or f"Page {page['id']}"
incoming_label = (page.get("name") or "").strip() or f"Page {page['id']}"
existing = SocialAccount.objects.filter(
platform=Platform.FACEBOOK, external_id=str(page["id"])
).first()
account, created = SocialAccount.objects.update_or_create(
platform=Platform.FACEBOOK,
external_id=str(page["id"]),
defaults={
"label": label,
"label": SocialAccount.resolve_label(existing, incoming_label),
"encrypted_tokens": encrypt_tokens(json.dumps(blob)),
"is_active": True,
"owner": request.user,
@@ -525,7 +545,7 @@ def composer(request):
form = {
"body": "",
"prompt": "",
"publish_mode": "schedule",
"publish_mode": "now",
"scheduled_for": "",
"account_ids": [],
"media_json": "[]",
@@ -536,11 +556,17 @@ def composer(request):
if request.method == "POST":
form["body"] = (request.POST.get("body") or "").strip()
form["prompt"] = (request.POST.get("prompt") or "").strip()
form["publish_mode"] = (request.POST.get("publish_mode") or "schedule").strip()
form["publish_mode"] = (request.POST.get("publish_mode") or "now").strip()
form["scheduled_for"] = request.POST.get("scheduled_for") or ""
form["account_ids"] = request.POST.getlist("account_ids")
form["media_json"] = (request.POST.get("media_json") or "[]").strip() or "[]"
action = (request.POST.get("action") or "save").strip()
if action == "send_now":
form["publish_mode"] = "now"
elif action == "schedule":
form["publish_mode"] = "schedule"
elif action == "save":
form["publish_mode"] = "draft"
if action == "generate" and form["prompt"]:
try:
@@ -548,7 +574,7 @@ def composer(request):
form["body"] = draft
except OllamaError as exc:
error = str(exc)
elif action in {"save", "publish"}:
elif action in {"save", "publish", "send_now", "schedule"}:
media_items: list = []
try:
media_items = parse_media_json(form["media_json"])
@@ -571,7 +597,7 @@ def composer(request):
error = str(exc)
else:
if not scheduled_for:
error = "Pick a schedule date/time, or choose Publish now."
error = "Pick a schedule date/time, or choose Send now."
else:
status = SocialPost.Status.SCHEDULED
elif form["publish_mode"] == "now":
@@ -614,7 +640,7 @@ def composer(request):
publish_social_post.enqueue(post_id=str(post.pk))
messages.success(
request,
"Post queued for publishing to selected accounts.",
"Post sent — queued for publishing to selected accounts.",
)
elif status == SocialPost.Status.SCHEDULED:
messages.success(