feat: add toggle visibility for published Facebook posts (debug ↔ live)
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

Adds bidirectional visibility control: published posts can be switched
between public (live) and draft (debug/admin-only) mode via Facebook
Graph API. Includes is_live column, status indicator, and toggle buttons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-19 10:36:14 +01:00
parent 5950a55416
commit f26341d8cc
6 changed files with 136 additions and 6 deletions

View File

@ -318,6 +318,18 @@ def social_publisher_delete(post_id):
return redirect(url_for('admin.social_publisher_list'))
@bp.route('/social-publisher/<int:post_id>/toggle-visibility', methods=['POST'])
@login_required
@role_required(SystemRole.MANAGER)
def social_publisher_toggle_visibility(post_id):
"""Przelacz widocznosc posta miedzy debug a live na Facebook."""
from services.social_publisher_service import social_publisher
success, message = social_publisher.toggle_visibility(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

@ -5375,6 +5375,9 @@ class SocialPost(Base):
scheduled_at = Column(DateTime, nullable=True)
published_at = Column(DateTime, nullable=True)
# Publish mode
is_live = Column(Boolean, default=False) # True = public, False = debug/draft on FB
# Facebook response
meta_post_id = Column(String(100))
meta_response = Column(JSONBType)

View File

@ -0,0 +1,5 @@
-- Add is_live column to social_media_posts
-- Tracks whether a published post is publicly visible (true) or debug/draft (false)
ALTER TABLE social_media_posts ADD COLUMN IF NOT EXISTS is_live BOOLEAN DEFAULT FALSE;
GRANT ALL ON TABLE social_media_posts TO nordabiz_app;

View File

@ -239,6 +239,17 @@ class FacebookGraphService:
"""
return self._post(post_id, data={'is_published': 'true'})
def unpublish_post(self, post_id: str) -> Optional[Dict]:
"""Unpublish a public post (make it draft/hidden).
Args:
post_id: Facebook post ID (format: PAGE_ID_POST_ID)
Returns:
API response or None on failure
"""
return self._post(post_id, data={'is_published': 'false'})
def get_post_engagement(self, post_id: str) -> Optional[Dict]:
"""Get engagement metrics for a published post.

View File

@ -481,6 +481,7 @@ class SocialPublisherService:
post.published_at = datetime.now()
post.meta_post_id = result['id']
post.meta_response = result
post.is_live = published
post.updated_at = datetime.now()
db.commit()
@ -501,6 +502,60 @@ class SocialPublisherService:
finally:
db.close()
def toggle_visibility(self, post_id: int) -> Tuple[bool, str]:
"""Toggle post visibility between live (public) and debug (draft) on Facebook.
If post.is_live=True -> unpublish (make draft)
If post.is_live=False -> publish live (make public)
"""
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 musi być opublikowany na Facebook, aby zmienić widoczność."
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 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."
except Exception as e:
db.rollback()
logger.error(f"Failed to toggle visibility for post #{post_id}: {e}")
return False, f"Błąd zmiany widoczności: {str(e)}"
finally:
db.close()
# ---- AI Content Generation ----
@staticmethod

View File

@ -201,6 +201,26 @@
.fb-link:hover {
text-decoration: underline;
}
.visibility-status {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius);
font-size: var(--font-size-sm);
font-weight: 600;
}
.visibility-status.live {
background: #dcfce7;
color: #166534;
}
.visibility-status.debug {
background: #fef9c3;
color: #854d0e;
}
</style>
{% endblock %}
@ -229,6 +249,14 @@
{% if post.published_at %}
| <strong>Opublikowano:</strong> {{ post.published_at.strftime('%Y-%m-%d %H:%M') }}
{% endif %}
{% if post.status == 'published' and post.meta_post_id %}
|
{% if post.is_live %}
<span class="visibility-status live">Publiczny</span>
{% else %}
<span class="visibility-status debug">Debug (tylko admin)</span>
{% endif %}
{% endif %}
</div>
{% endif %}
@ -378,12 +406,28 @@
</button>
{% endif %}
{% endif %}
{% if post.status == 'published' and is_debug %}
<button type="submit" name="action" value="publish_live" class="btn btn-success"
style="background: #dc2626; border: none;"
onclick="return confirm('Post zostanie ponownie opublikowany PUBLICZNIE. Kontynuowac?');">
Opublikuj publicznie
</button>
{% if post.status == 'published' and post.meta_post_id %}
</div>
<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" action="{{ url_for('admin.social_publisher_toggle_visibility', post_id=post.id) }}" style="display:inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-secondary"
onclick="return confirm('Post zostanie UKRYTY — widoczny tylko dla adminów strony FB. Kontynuować?');">
Zmień na debug
</button>
</form>
{% else %}
<form method="POST" action="{{ url_for('admin.social_publisher_toggle_visibility', post_id=post.id) }}" style="display:inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-success"
style="background: #dc2626; border: none;"
onclick="return confirm('Post zostanie UPUBLICZNIONY — widoczny dla wszystkich. Kontynuować?');">
Opublikuj publicznie
</button>
</form>
{% endif %}
{% endif %}
{% if post.status != 'published' %}
<button type="submit" name="action" value="delete" class="btn btn-error"