update
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user