From f26341d8cc17125b0773b26e8148c0b9cb27e17d Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Thu, 19 Feb 2026 10:36:14 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20add=20toggle=20visibility=20for=20publi?= =?UTF-8?q?shed=20Facebook=20posts=20(debug=20=E2=86=94=20live)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- blueprints/admin/routes_social_publisher.py | 12 ++++ database.py | 3 + .../migrations/072_social_post_is_live.sql | 5 ++ facebook_graph_service.py | 11 ++++ services/social_publisher_service.py | 55 ++++++++++++++++++ templates/admin/social_publisher_form.html | 56 +++++++++++++++++-- 6 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 database/migrations/072_social_post_is_live.sql diff --git a/blueprints/admin/routes_social_publisher.py b/blueprints/admin/routes_social_publisher.py index b40efee..55e26da 100644 --- a/blueprints/admin/routes_social_publisher.py +++ b/blueprints/admin/routes_social_publisher.py @@ -318,6 +318,18 @@ def social_publisher_delete(post_id): return redirect(url_for('admin.social_publisher_list')) +@bp.route('/social-publisher//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//refresh-engagement', methods=['POST']) @login_required @role_required(SystemRole.MANAGER) diff --git a/database.py b/database.py index 75c22b2..bebe50e 100644 --- a/database.py +++ b/database.py @@ -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) diff --git a/database/migrations/072_social_post_is_live.sql b/database/migrations/072_social_post_is_live.sql new file mode 100644 index 0000000..3abf604 --- /dev/null +++ b/database/migrations/072_social_post_is_live.sql @@ -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; diff --git a/facebook_graph_service.py b/facebook_graph_service.py index 53eff0b..f1fc6b7 100644 --- a/facebook_graph_service.py +++ b/facebook_graph_service.py @@ -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. diff --git a/services/social_publisher_service.py b/services/social_publisher_service.py index 8e39e2a..0b484b8 100644 --- a/services/social_publisher_service.py +++ b/services/social_publisher_service.py @@ -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 diff --git a/templates/admin/social_publisher_form.html b/templates/admin/social_publisher_form.html index 17797b5..1ad0a0b 100644 --- a/templates/admin/social_publisher_form.html +++ b/templates/admin/social_publisher_form.html @@ -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; + } {% endblock %} @@ -229,6 +249,14 @@ {% if post.published_at %} | Opublikowano: {{ post.published_at.strftime('%Y-%m-%d %H:%M') }} {% endif %} + {% if post.status == 'published' and post.meta_post_id %} + | + {% if post.is_live %} + Publiczny + {% else %} + Debug (tylko admin) + {% endif %} + {% endif %} {% endif %} @@ -378,12 +406,28 @@ {% endif %} {% endif %} - {% if post.status == 'published' and is_debug %} - + {% if post.status == 'published' and post.meta_post_id %} + +
+ Widoczność na FB: + {% if post.is_live %} +
+ + +
+ {% else %} +
+ + +
+ {% endif %} {% endif %} {% if post.status != 'published' %}