This commit is contained in:
2025-11-12 13:45:39 +01:00
parent ce6c2f88bd
commit b7d957f100
2 changed files with 230 additions and 15 deletions

View File

@@ -50,8 +50,27 @@
</td> </td>
</tr> </tr>
<!-- Articles --> <!-- Top Trending Section -->
{% for article in articles %} {% if trending_articles %}
<tr>
<td style="padding: 30px 40px 15px 40px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td>
<h2 style="margin: 0; font-size: 22px; font-weight: 700; color: #1a1a1a; display: flex; align-items: center;">
🔥 Top Trending in Munich
</h2>
<p style="margin: 8px 0 0 0; font-size: 13px; color: #666666;">
The most talked-about stories today
</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- Trending Articles -->
{% for article in trending_articles %}
<tr> <tr>
<td style="padding: 25px 40px;"> <td style="padding: 25px 40px;">
<!-- Article Number Badge --> <!-- Article Number Badge -->
@@ -79,10 +98,14 @@
<!-- Article Meta --> <!-- Article Meta -->
<p style="margin: 0 0 12px 0; font-size: 13px; color: #999999;"> <p style="margin: 0 0 12px 0; font-size: 13px; color: #999999;">
{% if article.is_clustered %}
<span style="color: #000000; font-weight: 600;">Multiple sources</span>
{% else %}
<span style="color: #000000; font-weight: 600;">{{ article.source }}</span> <span style="color: #000000; font-weight: 600;">{{ article.source }}</span>
{% if article.author %} {% if article.author %}
<span> • {{ article.author }}</span> <span> • {{ article.author }}</span>
{% endif %} {% endif %}
{% endif %}
</p> </p>
<!-- Article Summary --> <!-- Article Summary -->
@@ -90,10 +113,25 @@
{{ article.summary }} {{ article.summary }}
</p> </p>
<!-- Read More Link --> <!-- Read More Links -->
{% if article.is_clustered and article.sources %}
<!-- Multiple sources -->
<p style="margin: 0 0 8px 0; font-size: 13px; color: #666666;">
📰 Covered by {{ article.article_count }} sources:
</p>
<div style="margin: 0;">
{% for source in article.sources %}
<a href="{{ source.link }}" style="display: inline-block; color: #000000; text-decoration: none; font-size: 13px; font-weight: 600; border-bottom: 2px solid #000000; padding-bottom: 2px; margin-right: 15px; margin-bottom: 8px;">
{{ source.name }} →
</a>
{% endfor %}
</div>
{% else %}
<!-- Single source -->
<a href="{{ article.link }}" style="display: inline-block; color: #000000; text-decoration: none; font-size: 14px; font-weight: 600; border-bottom: 2px solid #000000; padding-bottom: 2px;"> <a href="{{ article.link }}" style="display: inline-block; color: #000000; text-decoration: none; font-size: 14px; font-weight: 600; border-bottom: 2px solid #000000; padding-bottom: 2px;">
Read more → Read more →
</a> </a>
{% endif %}
</td> </td>
</tr> </tr>
@@ -106,6 +144,110 @@
</tr> </tr>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endif %}
<!-- Other Articles Section -->
{% if other_articles %}
<!-- Section Divider -->
<tr>
<td style="padding: 25px 40px;">
<div style="height: 2px; background-color: #e0e0e0;"></div>
</td>
</tr>
<tr>
<td style="padding: 30px 40px 15px 40px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td>
<h2 style="margin: 0; font-size: 22px; font-weight: 700; color: #1a1a1a;">
📰 More Stories
</h2>
<p style="margin: 8px 0 0 0; font-size: 13px; color: #666666;">
Additional news from around Munich
</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- Other Articles -->
{% for article in other_articles %}
<tr>
<td style="padding: 25px 40px;">
<!-- Article Number Badge -->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td>
<span style="display: inline-block; background-color: #666666; color: #ffffff; width: 24px; height: 24px; line-height: 24px; text-align: center; border-radius: 50%; font-size: 12px; font-weight: 600;">
{{ loop.index + trending_articles|length }}
</span>
</td>
</tr>
</table>
<!-- Article Title -->
<h2 style="margin: 12px 0 8px 0; font-size: 19px; font-weight: 700; line-height: 1.3; color: #1a1a1a;">
{{ article.title_en if article.title_en else article.title }}
</h2>
<!-- Original German Title (subtitle) -->
{% if article.title_en and article.title_en != article.title %}
<p style="margin: 0 0 12px 0; font-size: 13px; color: #999999; font-style: italic;">
Original: {{ article.title }}
</p>
{% endif %}
<!-- Article Meta -->
<p style="margin: 0 0 12px 0; font-size: 13px; color: #999999;">
{% if article.is_clustered %}
<span style="color: #000000; font-weight: 600;">Multiple sources</span>
{% else %}
<span style="color: #000000; font-weight: 600;">{{ article.source }}</span>
{% if article.author %}
<span> • {{ article.author }}</span>
{% endif %}
{% endif %}
</p>
<!-- Article Summary -->
<p style="margin: 0 0 15px 0; font-size: 15px; line-height: 1.6; color: #333333;">
{{ article.summary }}
</p>
<!-- Read More Links -->
{% if article.is_clustered and article.sources %}
<!-- Multiple sources -->
<p style="margin: 0 0 8px 0; font-size: 13px; color: #666666;">
📰 Covered by {{ article.article_count }} sources:
</p>
<div style="margin: 0;">
{% for source in article.sources %}
<a href="{{ source.link }}" style="display: inline-block; color: #000000; text-decoration: none; font-size: 13px; font-weight: 600; border-bottom: 2px solid #000000; padding-bottom: 2px; margin-right: 15px; margin-bottom: 8px;">
{{ source.name }} →
</a>
{% endfor %}
</div>
{% else %}
<!-- Single source -->
<a href="{{ article.link }}" style="display: inline-block; color: #000000; text-decoration: none; font-size: 14px; font-weight: 600; border-bottom: 2px solid #000000; padding-bottom: 2px;">
Read more →
</a>
{% endif %}
</td>
</tr>
<!-- Article Divider -->
{% if not loop.last %}
<tr>
<td style="padding: 0 40px;">
<div style="height: 1px; background-color: #f0f0f0;"></div>
</td>
</tr>
{% endif %}
{% endfor %}
{% endif %}
<!-- Bottom Divider --> <!-- Bottom Divider -->
<tr> <tr>

View File

@@ -81,13 +81,14 @@ subscribers_collection = db['subscribers']
def get_latest_articles(max_articles=10, hours=24): def get_latest_articles(max_articles=10, hours=24):
""" """
Get latest articles with AI summaries from database (from today only) Get latest articles with AI summaries from database (from today only)
Includes cluster information for articles with multiple sources
Args: Args:
max_articles: Maximum number of articles to return max_articles: Maximum number of articles to return
hours: Number of hours to look back (default 24) hours: Number of hours to look back (default 24)
Returns: Returns:
list: Articles with summaries published today list: Articles with summaries published today, including cluster info
""" """
from datetime import timedelta from datetime import timedelta
@@ -97,6 +98,9 @@ def get_latest_articles(max_articles=10, hours=24):
# Get start of today (00:00:00 UTC) # Get start of today (00:00:00 UTC)
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
# Get cluster summaries collection
cluster_summaries_collection = db['cluster_summaries']
# Query for articles with summaries published today OR created today # Query for articles with summaries published today OR created today
# This ensures we only get fresh articles from today # This ensures we only get fresh articles from today
cursor = articles_collection.find({ cursor = articles_collection.find({
@@ -110,6 +114,8 @@ def get_latest_articles(max_articles=10, hours=24):
}).sort('created_at', -1).limit(max_articles) }).sort('created_at', -1).limit(max_articles)
articles = [] articles = []
processed_clusters = set()
for doc in cursor: for doc in cursor:
# Double-check the date to ensure it's from today # Double-check the date to ensure it's from today
published_at = doc.get('published_at') published_at = doc.get('published_at')
@@ -123,6 +129,46 @@ def get_latest_articles(max_articles=10, hours=24):
if created_at < today_start: if created_at < today_start:
continue continue
cluster_id = doc.get('cluster_id')
# Check if this article is part of a cluster
if cluster_id and cluster_id not in processed_clusters:
# Get cluster summary
cluster = cluster_summaries_collection.find_one({'cluster_id': cluster_id})
if cluster and cluster.get('article_count', 0) > 1:
# This is a clustered article - get all source links
processed_clusters.add(cluster_id)
# Get all articles in this cluster
cluster_articles = list(articles_collection.find({
'cluster_id': cluster_id
}))
# Build sources list with links
sources = []
for art in cluster_articles:
sources.append({
'name': art.get('source', ''),
'link': art.get('link', ''),
'title': art.get('title', '')
})
articles.append({
'title': doc.get('title', ''),
'title_en': doc.get('title_en'),
'translated_at': doc.get('translated_at'),
'author': doc.get('author'),
'link': doc.get('link', ''),
'summary': cluster.get('neutral_summary', doc.get('summary', '')),
'source': doc.get('source', ''),
'published_at': doc.get('published_at', ''),
'is_clustered': True,
'sources': sources,
'article_count': len(sources)
})
else:
# Single article (no cluster or cluster with only 1 article)
articles.append({ articles.append({
'title': doc.get('title', ''), 'title': doc.get('title', ''),
'title_en': doc.get('title_en'), 'title_en': doc.get('title_en'),
@@ -131,8 +177,29 @@ def get_latest_articles(max_articles=10, hours=24):
'link': doc.get('link', ''), 'link': doc.get('link', ''),
'summary': doc.get('summary', ''), 'summary': doc.get('summary', ''),
'source': doc.get('source', ''), 'source': doc.get('source', ''),
'published_at': doc.get('published_at', '') 'published_at': doc.get('published_at', ''),
'is_clustered': False
}) })
elif not cluster_id or cluster_id not in processed_clusters:
# No cluster - single article
articles.append({
'title': doc.get('title', ''),
'title_en': doc.get('title_en'),
'translated_at': doc.get('translated_at'),
'author': doc.get('author'),
'link': doc.get('link', ''),
'summary': doc.get('summary', ''),
'source': doc.get('source', ''),
'published_at': doc.get('published_at', ''),
'is_clustered': False
})
# Sort articles: clustered articles first (by source count), then by recency
# This prioritizes stories covered by multiple sources
articles.sort(key=lambda x: (
-1 if x.get('is_clustered') else 0, # Clustered first
-x.get('article_count', 1), # More sources = higher priority
), reverse=True)
return articles return articles
@@ -170,13 +237,19 @@ def render_newsletter_html(articles, tracking_enabled=False, pixel_tracking_id=N
template = Template(template_content) template = Template(template_content)
# Split articles into sections
# Top 3 are "trending", rest are "other articles"
trending_articles = articles[:3] if len(articles) >= 3 else articles
other_articles = articles[3:] if len(articles) > 3 else []
# Prepare template data # Prepare template data
now = datetime.now() now = datetime.now()
template_data = { template_data = {
'date': now.strftime('%A, %B %d, %Y'), 'date': now.strftime('%A, %B %d, %Y'),
'year': now.year, 'year': now.year,
'article_count': len(articles), 'article_count': len(articles),
'articles': articles, 'trending_articles': trending_articles,
'other_articles': other_articles,
'unsubscribe_link': f'{Config.WEBSITE_URL}/unsubscribe', 'unsubscribe_link': f'{Config.WEBSITE_URL}/unsubscribe',
'website_link': Config.WEBSITE_URL, 'website_link': Config.WEBSITE_URL,
'tracking_enabled': tracking_enabled 'tracking_enabled': tracking_enabled