fix: replace unpublish with withdraw (FB API doesn't support unpublishing)
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

Facebook Graph API returns error 100 when setting is_published=false on
already-published posts. Replaced "Zmień na debug" with "Usuń z Facebooka"
which deletes the post from FB and resets status to draft for re-publishing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-19 10:50:10 +01:00
parent adceaaec60
commit af745cfb60
3 changed files with 78 additions and 30 deletions

View File

@ -322,7 +322,7 @@ def social_publisher_delete(post_id):
@login_required
@role_required(SystemRole.MANAGER)
def social_publisher_toggle_visibility(post_id):
"""Przelacz widocznosc posta miedzy debug a live na Facebook."""
"""Opublikuj draft post publicznie na Facebook (debug -> live)."""
from services.social_publisher_service import social_publisher
success, message = social_publisher.toggle_visibility(post_id)
@ -330,6 +330,18 @@ def social_publisher_toggle_visibility(post_id):
return redirect(url_for('admin.social_publisher_edit', post_id=post_id))
@bp.route('/social-publisher/<int:post_id>/withdraw', methods=['POST'])
@login_required
@role_required(SystemRole.MANAGER)
def social_publisher_withdraw(post_id):
"""Usun post z Facebooka i przywroc do szkicu."""
from services.social_publisher_service import social_publisher
success, message = social_publisher.withdraw_from_fb(post_id)
flash(message, 'success' if success else 'danger')
return redirect(url_for('admin.social_publisher_edit', post_id=post_id))
@bp.route('/social-publisher/<int:post_id>/refresh-engagement', methods=['POST'])
@login_required
@role_required(SystemRole.MANAGER)

View File

@ -503,10 +503,10 @@ class SocialPublisherService:
db.close()
def toggle_visibility(self, post_id: int) -> Tuple[bool, str]:
"""Toggle post visibility between live (public) and debug (draft) on Facebook.
"""Publish a debug/draft post live on Facebook (debug -> live only).
If post.is_live=True -> unpublish (make draft)
If post.is_live=False -> publish live (make public)
Note: Facebook API does not support unpublishing already-published posts.
Use withdraw_from_fb() to delete and reset to draft instead.
"""
db = SessionLocal()
try:
@ -515,7 +515,10 @@ class SocialPublisherService:
return False, "Post nie znaleziony"
if post.status != 'published' or not post.meta_post_id:
return False, "Post musi być opublikowany na Facebook, aby zmienić widoczność."
return False, "Post musi być opublikowany na Facebook."
if post.is_live:
return False, "Post jest już publiczny. Facebook nie pozwala na cofnięcie publikacji — użyj opcji 'Usuń z Facebooka'."
pub_company_id = post.publishing_company_id
if not pub_company_id:
@ -528,26 +531,14 @@ class SocialPublisherService:
from facebook_graph_service import FacebookGraphService
fb = FacebookGraphService(access_token)
if post.is_live:
# Currently public -> make draft
result = fb.unpublish_post(post.meta_post_id)
if result:
post.is_live = False
post.updated_at = datetime.now()
db.commit()
logger.info(f"Post #{post_id} toggled to DRAFT (unpublished)")
return True, "Post zmieniony na tryb debug (widoczny tylko dla adminów strony)."
return False, "Nie udało się ukryć posta na Facebook."
else:
# Currently draft -> make public
result = fb.publish_draft(post.meta_post_id)
if result:
post.is_live = True
post.updated_at = datetime.now()
db.commit()
logger.info(f"Post #{post_id} toggled to LIVE (published)")
return True, "Post zmieniony na tryb publiczny (widoczny dla wszystkich)."
return False, "Nie udało się upublicznić posta na Facebook."
result = fb.publish_draft(post.meta_post_id)
if result:
post.is_live = True
post.updated_at = datetime.now()
db.commit()
logger.info(f"Post #{post_id} toggled to LIVE (published)")
return True, "Post opublikowany publicznie — widoczny dla wszystkich."
return False, "Nie udało się upublicznić posta na Facebook."
except Exception as e:
db.rollback()
@ -556,6 +547,51 @@ class SocialPublisherService:
finally:
db.close()
def withdraw_from_fb(self, post_id: int) -> Tuple[bool, str]:
"""Delete post from Facebook and reset status to draft.
Facebook API doesn't support unpublishing, so we delete and let
the user re-publish later if needed.
"""
db = SessionLocal()
try:
post = db.query(SocialPost).filter(SocialPost.id == post_id).first()
if not post:
return False, "Post nie znaleziony"
if post.status != 'published' or not post.meta_post_id:
return False, "Post nie jest opublikowany na Facebook."
pub_company_id = post.publishing_company_id
if not pub_company_id:
return False, "Brak firmy publikującej."
access_token, config = self._get_publish_token(db, pub_company_id)
if not access_token:
return False, "Brak tokena Facebook dla wybranej firmy."
from facebook_graph_service import FacebookGraphService
fb = FacebookGraphService(access_token)
if fb.delete_post(post.meta_post_id):
old_fb_id = post.meta_post_id
post.status = 'draft'
post.meta_post_id = None
post.is_live = False
post.published_at = None
post.updated_at = datetime.now()
db.commit()
logger.info(f"Post #{post_id} withdrawn from FB (deleted {old_fb_id}), reset to draft")
return True, "Post usunięty z Facebooka i przywrócony do szkicu. Możesz go ponownie opublikować."
return False, "Nie udało się usunąć posta z Facebooka."
except Exception as e:
db.rollback()
logger.error(f"Failed to withdraw post #{post_id} from FB: {e}")
return False, f"Błąd usuwania z Facebooka: {str(e)}"
finally:
db.close()
# ---- AI Content Generation ----
@staticmethod

View File

@ -486,15 +486,15 @@
</form>
{% if post and post.status == 'published' and post.meta_post_id %}
<!-- Toggle visibility (outside postForm to avoid nested forms) -->
<!-- Visibility controls (outside postForm to avoid nested forms) -->
<div class="btn-group" style="margin-top: var(--spacing-md);">
<strong style="align-self: center;">Widoczność na FB:</strong>
{% if post.is_live %}
<form method="POST" id="toggleVisibilityForm" action="{{ url_for('admin.social_publisher_toggle_visibility', post_id=post.id) }}">
<form method="POST" id="withdrawForm" action="{{ url_for('admin.social_publisher_withdraw', post_id=post.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="button" class="btn btn-secondary"
onclick="showConfirm({icon: '🔒', title: 'Ukryj post', message: 'Post zostanie przełączony w tryb debug — będzie widoczny tylko dla administratorów strony na Facebooku.', okText: 'Ukryj post', okClass: 'btn btn-secondary', form: 'toggleVisibilityForm'});">
Zmień na debug
<button type="button" class="btn btn-error"
onclick="showConfirm({icon: '🗑️', title: 'Usuń z Facebooka', message: 'Post zostanie usunięty z Facebooka i przywrócony do szkicu. Będziesz mógł go ponownie opublikować.', okText: 'Usuń z FB', okClass: 'btn btn-error', form: 'withdrawForm'});">
Usuń z Facebooka
</button>
</form>
{% else %}