This commit is contained in:
2025-11-12 22:33:56 +01:00
parent 95669fd211
commit 45df834d5b
12 changed files with 1172 additions and 240 deletions

View File

@@ -83,18 +83,18 @@
</td>
</tr>
<!-- Top Trending Section -->
{% if trending_articles %}
<!-- Category Sections -->
{% for section in category_sections %}
<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 style="margin: 0; font-size: 22px; font-weight: 700; color: #1a1a1a;">
{{ section.icon }} {{ section.name }}
</h2>
<p style="margin: 8px 0 0 0; font-size: 13px; color: #666666;">
The most talked-about stories today
Top stories in {{ section.name.lower() }}
</p>
</td>
</tr>
@@ -102,8 +102,8 @@
</td>
</tr>
<!-- Trending Articles -->
{% for article in trending_articles %}
<!-- Category Articles -->
{% for article in section.articles %}
<tr>
<td style="padding: 25px 40px;">
<!-- Article Number Badge -->
@@ -177,110 +177,16 @@
</tr>
{% endif %}
{% endfor %}
{% endif %}
<!-- Other Articles Section -->
{% if other_articles %}
<!-- Section Divider -->
<!-- Category Section Divider -->
{% if not loop.last %}
<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; text-align: justify;">
{{ 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 -->
<tr>
@@ -299,8 +205,8 @@
Here's everything in one sentence each:
</p>
{% set all_articles = (trending_articles or []) + (other_articles or []) %}
{% for article in all_articles %}
{% for section in category_sections %}
{% for article in section.articles %}
<div style="margin-bottom: 12px; padding-left: 20px; position: relative;">
<span style="position: absolute; left: 0; top: 0; color: #667eea; font-weight: 700;">{{ loop.index }}.</span>
<p style="margin: 0; font-size: 14px; line-height: 1.5; color: #333333;">
@@ -309,6 +215,7 @@
</p>
</div>
{% endfor %}
{% endfor %}
</td>
</tr>
@@ -355,6 +262,8 @@
<p style="margin: 0; font-size: 12px; color: #666666;">
<a href="{{ website_link }}" style="color: #999999; text-decoration: none;">Visit Website</a>
<span style="color: #444444;"></span>
<a href="{{ preferences_link }}" style="color: #999999; text-decoration: none;">Manage Preferences</a>
<span style="color: #444444;"></span>
<a href="{{ unsubscribe_link }}" style="color: #999999; text-decoration: none;">Unsubscribe</a>
</p>

View File

@@ -162,6 +162,7 @@ def get_latest_articles(max_articles=10, hours=24):
'link': doc.get('link', ''),
'summary': cluster.get('neutral_summary', doc.get('summary', '')),
'source': doc.get('source', ''),
'category': doc.get('category', 'general'),
'published_at': doc.get('published_at', ''),
'is_clustered': True,
'sources': sources,
@@ -177,6 +178,7 @@ def get_latest_articles(max_articles=10, hours=24):
'link': doc.get('link', ''),
'summary': doc.get('summary', ''),
'source': doc.get('source', ''),
'category': doc.get('category', 'general'),
'published_at': doc.get('published_at', ''),
'is_clustered': False
})
@@ -190,6 +192,7 @@ def get_latest_articles(max_articles=10, hours=24):
'link': doc.get('link', ''),
'summary': doc.get('summary', ''),
'source': doc.get('source', ''),
'category': doc.get('category', 'general'),
'published_at': doc.get('published_at', ''),
'is_clustered': False
})
@@ -206,22 +209,29 @@ def get_latest_articles(max_articles=10, hours=24):
def get_active_subscribers():
"""
Get all active subscribers from database
Get all active subscribers from database with their category preferences
Returns:
list: Email addresses of active subscribers
list: Subscriber dictionaries with email and categories
"""
cursor = subscribers_collection.find({'status': 'active'})
return [doc['email'] for doc in cursor]
subscribers = []
for doc in cursor:
subscribers.append({
'email': doc['email'],
'categories': doc.get('categories', None) # None means all categories
})
return subscribers
def render_newsletter_html(articles, tracking_enabled=False, pixel_tracking_id=None,
link_tracking_map=None, api_url=None):
def render_newsletter_html(articles, subscriber_categories=None, tracking_enabled=False,
pixel_tracking_id=None, link_tracking_map=None, api_url=None):
"""
Render newsletter HTML from template with optional tracking integration
Args:
articles: List of article dictionaries
subscriber_categories: List of categories the subscriber wants (None = all)
tracking_enabled: Whether to inject tracking pixel and replace links
pixel_tracking_id: Tracking ID for the email open pixel
link_tracking_map: Dictionary mapping original URLs to tracking IDs
@@ -237,10 +247,39 @@ def render_newsletter_html(articles, tracking_enabled=False, pixel_tracking_id=N
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 []
# Filter articles by subscriber's category preferences
if subscriber_categories:
filtered_articles = [a for a in articles if a.get('category', 'general') in subscriber_categories]
else:
filtered_articles = articles
# Group articles by category (max 3 per category)
from collections import defaultdict
articles_by_category = defaultdict(list)
for article in filtered_articles:
category = article.get('category', 'general')
if len(articles_by_category[category]) < 3:
articles_by_category[category].append(article)
# Convert to list of category sections
category_sections = []
category_names = {
'general': {'name': 'Top Trending', 'icon': '🔥'},
'local': {'name': 'Local Events', 'icon': '🏛️'},
'sports': {'name': 'Sports', 'icon': ''},
'science': {'name': 'Science & Tech', 'icon': '🔬'}
}
for category, category_articles in sorted(articles_by_category.items()):
if category_articles:
cat_info = category_names.get(category, {'name': category.title(), 'icon': '📄'})
category_sections.append({
'id': category,
'name': cat_info['name'],
'icon': cat_info['icon'],
'articles': category_articles
})
# Get weather data
from weather_service import get_munich_weather
@@ -248,13 +287,14 @@ def render_newsletter_html(articles, tracking_enabled=False, pixel_tracking_id=N
# Prepare template data
now = datetime.now()
total_articles = sum(len(section['articles']) for section in category_sections)
template_data = {
'date': now.strftime('%A, %B %d, %Y'),
'year': now.year,
'article_count': len(articles),
'trending_articles': trending_articles,
'other_articles': other_articles,
'article_count': total_articles,
'category_sections': category_sections,
'unsubscribe_link': f'{Config.WEBSITE_URL}/unsubscribe',
'preferences_link': f'{Config.WEBSITE_URL}/preferences.html',
'website_link': Config.WEBSITE_URL,
'tracking_enabled': tracking_enabled,
'weather': weather
@@ -358,7 +398,8 @@ def send_newsletter(max_articles=None, test_email=None):
# Get subscribers
if test_email:
subscribers = [test_email]
# For test mode, send with all categories
subscribers = [{'email': test_email, 'categories': None}]
print(f"\n🧪 Test mode: Sending to {test_email} only")
else:
print("\nFetching active subscribers...")
@@ -386,7 +427,10 @@ def send_newsletter(max_articles=None, test_email=None):
failed_count = 0
errors = []
for i, email in enumerate(subscribers, 1):
for i, subscriber in enumerate(subscribers, 1):
email = subscriber['email']
categories = subscriber['categories']
print(f"[{i}/{len(subscribers)}] Sending to {email}...", end=' ')
# Generate tracking data for this subscriber if tracking is enabled
@@ -399,9 +443,10 @@ def send_newsletter(max_articles=None, test_email=None):
tracking_service=tracking_service
)
# Render newsletter with tracking
# Render newsletter with tracking and subscriber's category preferences
html_content = render_newsletter_html(
articles=articles,
subscriber_categories=categories,
tracking_enabled=True,
pixel_tracking_id=tracking_data['pixel_tracking_id'],
link_tracking_map=tracking_data['link_tracking_map'],
@@ -410,10 +455,10 @@ def send_newsletter(max_articles=None, test_email=None):
except Exception as e:
print(f"⚠ Tracking error: {e}, sending without tracking...", end=' ')
# Fallback: send without tracking
html_content = render_newsletter_html(articles)
html_content = render_newsletter_html(articles, subscriber_categories=categories)
else:
# Render newsletter without tracking
html_content = render_newsletter_html(articles)
# Render newsletter without tracking but with subscriber's preferences
html_content = render_newsletter_html(articles, subscriber_categories=categories)
# Send email
success, error = send_email(email, subject, html_content)