feat: add cursor-based pagination to Facebook posts API
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

Previously get_page_posts returned a flat list with no pagination support.
Now returns dict with posts and next_cursor, enabling infinite scrolling
through all Facebook page posts via the after query parameter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-19 16:51:31 +01:00
parent 9444c3484e
commit 779f0b0b73
3 changed files with 46 additions and 21 deletions

View File

@ -388,7 +388,8 @@ def social_publisher_fb_posts(company_id):
finally: finally:
db.close() db.close()
result = social_publisher.get_page_recent_posts(company_id) after = request.args.get('after')
result = social_publisher.get_page_recent_posts(company_id, after=after)
return jsonify(result) return jsonify(result)

View File

@ -273,22 +273,28 @@ class FacebookGraphService:
'reactions_total': result.get('reactions', {}).get('summary', {}).get('total_count', 0), 'reactions_total': result.get('reactions', {}).get('summary', {}).get('total_count', 0),
} }
def get_page_posts(self, page_id: str, limit: int = 10) -> Optional[List[Dict]]: def get_page_posts(self, page_id: str, limit: int = 10,
after: str = None) -> Optional[Dict]:
"""Get recent posts from a Facebook Page with engagement metrics. """Get recent posts from a Facebook Page with engagement metrics.
Args: Args:
page_id: Facebook Page ID page_id: Facebook Page ID
limit: Number of posts to fetch (max 100) limit: Number of posts to fetch (max 100)
after: Pagination cursor for next page
Returns: Returns:
List of post dicts or None on failure Dict with 'posts' list and 'next_cursor' (or None), or None on failure
""" """
fields = ( fields = (
'id,message,created_time,full_picture,permalink_url,status_type,' 'id,message,created_time,full_picture,permalink_url,status_type,'
'likes.summary(true).limit(0),comments.summary(true).limit(0),' 'likes.summary(true).limit(0),comments.summary(true).limit(0),'
'shares,reactions.summary(true).limit(0)' 'shares,reactions.summary(true).limit(0)'
) )
result = self._get(f'{page_id}/posts', {'fields': fields, 'limit': limit}) params = {'fields': fields, 'limit': limit}
if after:
params['after'] = after
result = self._get(f'{page_id}/posts', params)
if not result: if not result:
return None return None
@ -306,7 +312,14 @@ class FacebookGraphService:
'shares': item.get('shares', {}).get('count', 0) if item.get('shares') else 0, 'shares': item.get('shares', {}).get('count', 0) if item.get('shares') else 0,
'reactions_total': item.get('reactions', {}).get('summary', {}).get('total_count', 0), 'reactions_total': item.get('reactions', {}).get('summary', {}).get('total_count', 0),
}) })
return posts
# Extract next page cursor
next_cursor = None
paging = result.get('paging', {})
if paging.get('next'):
next_cursor = paging.get('cursors', {}).get('after')
return {'posts': posts, 'next_cursor': next_cursor}
def get_post_insights_metrics(self, post_id: str) -> Optional[Dict]: def get_post_insights_metrics(self, post_id: str) -> Optional[Dict]:
"""Get detailed insights metrics for a specific post. """Get detailed insights metrics for a specific post.

View File

@ -603,10 +603,11 @@ class SocialPublisherService:
# ---- Facebook Page Posts (read from API) ---- # ---- Facebook Page Posts (read from API) ----
def get_page_recent_posts(self, company_id: int, limit: int = 10) -> Dict: def get_page_recent_posts(self, company_id: int, limit: int = 10,
after: str = None) -> Dict:
"""Fetch recent posts from company's Facebook page with engagement metrics. """Fetch recent posts from company's Facebook page with engagement metrics.
Uses in-memory cache with 5-minute TTL. Uses in-memory cache with 5-minute TTL (first page only).
""" """
db = SessionLocal() db = SessionLocal()
try: try:
@ -615,31 +616,41 @@ class SocialPublisherService:
return {'success': False, 'error': 'Brak konfiguracji Facebook dla tej firmy.'} return {'success': False, 'error': 'Brak konfiguracji Facebook dla tej firmy.'}
page_id = config.page_id page_id = config.page_id
cache_key = (company_id, page_id)
# Check cache # Cache only first page (no cursor)
cached = _posts_cache.get(cache_key) if not after:
if cached and (time.time() - cached['ts']) < _CACHE_TTL: cache_key = (company_id, page_id)
return { cached = _posts_cache.get(cache_key)
'success': True, if cached and (time.time() - cached['ts']) < _CACHE_TTL:
'posts': cached['data'], return {
'page_name': config.page_name or '', 'success': True,
'cached': True, 'posts': cached['data'],
} 'next_cursor': cached.get('next_cursor'),
'page_name': config.page_name or '',
'cached': True,
}
from facebook_graph_service import FacebookGraphService from facebook_graph_service import FacebookGraphService
fb = FacebookGraphService(access_token) fb = FacebookGraphService(access_token)
posts = fb.get_page_posts(page_id, limit) result = fb.get_page_posts(page_id, limit, after=after)
if posts is None: if result is None:
return {'success': False, 'error': 'Nie udało się pobrać postów z Facebook API.'} return {'success': False, 'error': 'Nie udało się pobrać postów z Facebook API.'}
# Update cache posts = result['posts']
_posts_cache[cache_key] = {'data': posts, 'ts': time.time()} next_cursor = result.get('next_cursor')
# Update cache for first page only
if not after:
cache_key = (company_id, page_id)
_posts_cache[cache_key] = {
'data': posts, 'next_cursor': next_cursor, 'ts': time.time()
}
return { return {
'success': True, 'success': True,
'posts': posts, 'posts': posts,
'next_cursor': next_cursor,
'page_name': config.page_name or '', 'page_name': config.page_name or '',
'cached': False, 'cached': False,
} }