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
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:
parent
5950a55416
commit
f26341d8cc
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
5
database/migrations/072_social_post_is_live.sql
Normal file
5
database/migrations/072_social_post_is_live.sql
Normal 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;
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user