fix: correct content open rate calculation to use % of active members
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

Open rate now shows percentage of active members who read at least one
piece of content, capped at 100%. Previously showed inflated numbers
because it counted reads of older content against only recent publications.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-10 20:11:44 +01:00
parent b0d9758d4a
commit ad4acc7c62
2 changed files with 18 additions and 20 deletions

View File

@ -969,60 +969,58 @@ def _tab_pages(db, start_date, days):
'top_clicked': [{'name': r.name, 'clicks': r.clicks} for r in top_clicked_companies],
}
# Content open rates (30 days)
# Content engagement (30 days)
start_30d = datetime.combine(date.today() - timedelta(days=30), datetime.min.time())
# Announcements: total vs read
# Total active members for open rate denominator
total_active = db.query(func.count(User.id)).filter(
User.is_active == True, User.last_login.isnot(None)
).scalar() or 1
# Announcements: published in 30d + unique readers of those announcements
total_announcements = db.query(func.count(Announcement.id)).filter(
Announcement.created_at >= start_30d
).scalar() or 0
announcement_reads = db.query(func.count(func.distinct(AnnouncementRead.announcement_id))).filter(
AnnouncementRead.read_at >= start_30d
).scalar() or 0
# How many unique users read ANY announcement (regardless of when published)
total_announcement_readers = db.query(func.count(func.distinct(AnnouncementRead.user_id))).filter(
AnnouncementRead.read_at >= start_30d
).scalar() or 0
# Open rate = % of active members who read at least one announcement
ann_open_rate = min(100, round(total_announcement_readers / total_active * 100)) if total_active > 0 else 0
# Forum: topics published vs read
# Forum: topics published in 30d + unique readers
total_forum_topics = db.query(func.count(ForumTopic.id)).filter(
ForumTopic.created_at >= start_30d
).scalar() or 0
forum_reads_count = db.query(func.count(func.distinct(ForumTopicRead.topic_id))).filter(
ForumTopicRead.read_at >= start_30d
).scalar() or 0
total_forum_readers = db.query(func.count(func.distinct(ForumTopicRead.user_id))).filter(
ForumTopicRead.read_at >= start_30d
).scalar() or 0
forum_open_rate = min(100, round(total_forum_readers / total_active * 100)) if total_active > 0 else 0
# Classifieds: published vs read
# Classifieds: published in 30d + unique readers
total_classifieds = db.query(func.count(Classified.id)).filter(
Classified.created_at >= start_30d
).scalar() or 0
classified_reads_count = db.query(func.count(func.distinct(ClassifiedRead.classified_id))).filter(
ClassifiedRead.read_at >= start_30d
).scalar() or 0
total_classified_readers = db.query(func.count(func.distinct(ClassifiedRead.user_id))).filter(
ClassifiedRead.read_at >= start_30d
).scalar() or 0
classified_open_rate = min(100, round(total_classified_readers / total_active * 100)) if total_active > 0 else 0
content_engagement = {
'announcements': {
'published': total_announcements,
'read_by': total_announcement_readers,
'unique_read': announcement_reads,
'open_rate': round(announcement_reads / total_announcements * 100) if total_announcements > 0 else 0,
'open_rate': ann_open_rate,
},
'forum': {
'published': total_forum_topics,
'read_by': total_forum_readers,
'unique_read': forum_reads_count,
'open_rate': round(forum_reads_count / total_forum_topics * 100) if total_forum_topics > 0 else 0,
'open_rate': forum_open_rate,
},
'classifieds': {
'published': total_classifieds,
'read_by': total_classified_readers,
'unique_read': classified_reads_count,
'open_rate': round(classified_reads_count / total_classifieds * 100) if total_classifieds > 0 else 0,
'open_rate': classified_open_rate,
},
}

View File

@ -709,7 +709,7 @@
</div>
</div>
<div style="margin-bottom: 4px; display: flex; justify-content: space-between;">
<span style="font-size: var(--font-size-xs); color: var(--text-secondary);">Open rate</span>
<span style="font-size: var(--font-size-xs); color: var(--text-secondary);" title="% aktywnych członków, którzy czytali">Zasięg</span>
<span style="font-size: var(--font-size-sm); font-weight: 600; color: {{ '#16a34a' if stats.open_rate >= 50 else ('#f59e0b' if stats.open_rate >= 25 else '#ef4444') }};">{{ stats.open_rate }}%</span>
</div>
<div style="height: 8px; background: #e2e8f0; border-radius: 4px; overflow: hidden;">