update
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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,16 +129,77 @@ def get_latest_articles(max_articles=10, hours=24):
|
|||||||
if created_at < today_start:
|
if created_at < today_start:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
articles.append({
|
cluster_id = doc.get('cluster_id')
|
||||||
'title': doc.get('title', ''),
|
|
||||||
'title_en': doc.get('title_en'),
|
# Check if this article is part of a cluster
|
||||||
'translated_at': doc.get('translated_at'),
|
if cluster_id and cluster_id not in processed_clusters:
|
||||||
'author': doc.get('author'),
|
# Get cluster summary
|
||||||
'link': doc.get('link', ''),
|
cluster = cluster_summaries_collection.find_one({'cluster_id': cluster_id})
|
||||||
'summary': doc.get('summary', ''),
|
|
||||||
'source': doc.get('source', ''),
|
if cluster and cluster.get('article_count', 0) > 1:
|
||||||
'published_at': doc.get('published_at', '')
|
# 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({
|
||||||
|
'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
|
||||||
|
})
|
||||||
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user