Add chase notifications and weekly digest emails
This commit is contained in:
parent
2504b15f29
commit
4c4f2b437e
5 changed files with 350 additions and 0 deletions
|
|
@ -265,6 +265,7 @@ def import_setlists(session, show_map, song_map):
|
|||
page = 1
|
||||
total_processed = 0
|
||||
performance_count = 0
|
||||
new_performance_ids = [] # Track new performances for chase notifications
|
||||
params = {}
|
||||
|
||||
while True:
|
||||
|
|
@ -321,12 +322,24 @@ def import_setlists(session, show_map, song_map):
|
|||
notes=perf_data.get('notes')
|
||||
)
|
||||
session.add(perf)
|
||||
session.flush() # Get the ID
|
||||
new_performance_ids.append(perf.id)
|
||||
count_in_page += 1
|
||||
performance_count += 1
|
||||
|
||||
session.commit()
|
||||
|
||||
# Send chase notifications for new performances
|
||||
if new_performance_ids:
|
||||
try:
|
||||
from services.chase_notifications import check_new_performances_for_chases
|
||||
check_new_performances_for_chases(session, new_performance_ids)
|
||||
except Exception as e:
|
||||
print(f" Warning: Chase notifications failed: {e}")
|
||||
|
||||
print(f" Validated/Added {count_in_page} items.")
|
||||
page += 1
|
||||
new_performance_ids = [] # Reset for next page
|
||||
|
||||
print(f"✓ Imported {performance_count} new performances")
|
||||
|
||||
|
|
|
|||
6
backend/scripts/cron_weekly_digest.sh
Normal file
6
backend/scripts/cron_weekly_digest.sh
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/sh
|
||||
# Cron job runner for Elmeg
|
||||
# Add to crontab: 0 9 * * 0 /app/scripts/cron_weekly_digest.sh
|
||||
|
||||
cd /app
|
||||
python services/weekly_digest.py
|
||||
87
backend/services/chase_notifications.py
Normal file
87
backend/services/chase_notifications.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"""
|
||||
Chase Notification Service
|
||||
Sends email notifications to users when songs they're chasing are played.
|
||||
"""
|
||||
from sqlmodel import Session, select
|
||||
from models import ChaseSong, User, Song, Show, Performance, Venue
|
||||
from helpers import create_notification
|
||||
|
||||
|
||||
def notify_chase_users(session: Session, song_id: int, show_id: int, performance_id: int):
|
||||
"""
|
||||
Send notifications to all users chasing this song.
|
||||
Called after a new performance is imported.
|
||||
"""
|
||||
from services.email_service import send_chase_notification_email
|
||||
|
||||
# Get song, show, and venue info for the notification
|
||||
song = session.get(Song, song_id)
|
||||
show = session.get(Show, show_id)
|
||||
|
||||
if not song or not show:
|
||||
return 0
|
||||
|
||||
venue = session.get(Venue, show.venue_id) if show.venue_id else None
|
||||
|
||||
# Find all users chasing this song who haven't caught it yet
|
||||
chase_songs = session.exec(
|
||||
select(ChaseSong)
|
||||
.where(ChaseSong.song_id == song_id)
|
||||
.where(ChaseSong.caught_at.is_(None)) # Not yet caught
|
||||
).all()
|
||||
|
||||
notified_count = 0
|
||||
|
||||
for chase in chase_songs:
|
||||
user = session.get(User, chase.user_id)
|
||||
if not user:
|
||||
continue
|
||||
|
||||
# Create in-app notification
|
||||
create_notification(
|
||||
session,
|
||||
user_id=user.id,
|
||||
title="Chase Alert!",
|
||||
message=f"{song.title} was played at {venue.name if venue else 'a show'}!",
|
||||
type="chase",
|
||||
link=f"/performances/{performance_id}" # Link to performance page
|
||||
)
|
||||
|
||||
# Send email if user has chase alerts enabled
|
||||
if user.preferences and user.preferences.email_on_chase:
|
||||
show_date = show.date.strftime("%Y-%m-%d") if show.date else "Unknown date"
|
||||
venue_name = venue.name if venue else "Unknown venue"
|
||||
|
||||
# Get performance slug for link
|
||||
perf = session.get(Performance, performance_id)
|
||||
perf_link = f"/performances/{perf.slug}" if perf and perf.slug else f"/shows/{show.slug}"
|
||||
|
||||
send_chase_notification_email(
|
||||
to_email=user.email,
|
||||
song_title=song.title,
|
||||
show_date=show_date,
|
||||
venue_name=venue_name,
|
||||
link=perf_link
|
||||
)
|
||||
notified_count += 1
|
||||
|
||||
return notified_count
|
||||
|
||||
|
||||
def check_new_performances_for_chases(session: Session, performance_ids: list):
|
||||
"""
|
||||
Check a list of newly imported performances and notify chase users.
|
||||
Call this after batch importing shows.
|
||||
"""
|
||||
total_notified = 0
|
||||
|
||||
for perf_id in performance_ids:
|
||||
perf = session.get(Performance, perf_id)
|
||||
if perf:
|
||||
count = notify_chase_users(session, perf.song_id, perf.show_id, perf.id)
|
||||
total_notified += count
|
||||
|
||||
if total_notified > 0:
|
||||
print(f"📧 Sent {total_notified} chase notification emails")
|
||||
|
||||
return total_notified
|
||||
220
backend/services/weekly_digest.py
Normal file
220
backend/services/weekly_digest.py
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
"""
|
||||
Weekly Digest Email Service
|
||||
Sends weekly digest emails to users who have email_digest enabled.
|
||||
Run via cron job: 0 9 * * 0 (Sunday at 9am)
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add backend to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from sqlmodel import Session, select, func
|
||||
from database import engine
|
||||
from models import User, Show, Performance, Review, Rating, ChaseSong, Attendance
|
||||
|
||||
|
||||
def get_weekly_stats(session: Session, user_id: int, start_date: datetime, end_date: datetime):
|
||||
"""Get user's activity stats for the week"""
|
||||
|
||||
# Shows attended this week
|
||||
shows_attended = session.exec(
|
||||
select(func.count(Attendance.id))
|
||||
.where(Attendance.user_id == user_id)
|
||||
.where(Attendance.created_at >= start_date)
|
||||
.where(Attendance.created_at < end_date)
|
||||
).one() or 0
|
||||
|
||||
# Reviews written this week
|
||||
reviews_written = session.exec(
|
||||
select(func.count(Review.id))
|
||||
.where(Review.user_id == user_id)
|
||||
.where(Review.created_at >= start_date)
|
||||
.where(Review.created_at < end_date)
|
||||
).one() or 0
|
||||
|
||||
# Ratings given this week
|
||||
ratings_given = session.exec(
|
||||
select(func.count(Rating.id))
|
||||
.where(Rating.user_id == user_id)
|
||||
.where(Rating.created_at >= start_date)
|
||||
.where(Rating.created_at < end_date)
|
||||
).one() or 0
|
||||
|
||||
# Check for caught chase songs
|
||||
chase_caught = session.exec(
|
||||
select(func.count(ChaseSong.id))
|
||||
.where(ChaseSong.user_id == user_id)
|
||||
.where(ChaseSong.caught_at >= start_date)
|
||||
.where(ChaseSong.caught_at < end_date)
|
||||
).one() or 0
|
||||
|
||||
return {
|
||||
"shows_attended": shows_attended,
|
||||
"reviews_written": reviews_written,
|
||||
"ratings_given": ratings_given,
|
||||
"chase_caught": chase_caught
|
||||
}
|
||||
|
||||
|
||||
def get_community_highlights(session: Session, start_date: datetime, end_date: datetime):
|
||||
"""Get community highlights for the week"""
|
||||
|
||||
# New shows added
|
||||
new_shows = session.exec(
|
||||
select(func.count(Show.id))
|
||||
.where(Show.date >= start_date.date())
|
||||
.where(Show.date < end_date.date())
|
||||
).one() or 0
|
||||
|
||||
# Total reviews this week
|
||||
total_reviews = session.exec(
|
||||
select(func.count(Review.id))
|
||||
.where(Review.created_at >= start_date)
|
||||
.where(Review.created_at < end_date)
|
||||
).one() or 0
|
||||
|
||||
# Most reviewed performance this week
|
||||
top_perf = session.exec(
|
||||
select(Performance.id, func.count(Review.id).label("count"))
|
||||
.join(Review, Review.performance_id == Performance.id)
|
||||
.where(Review.created_at >= start_date)
|
||||
.where(Review.created_at < end_date)
|
||||
.group_by(Performance.id)
|
||||
.order_by(func.count(Review.id).desc())
|
||||
.limit(1)
|
||||
).first()
|
||||
|
||||
return {
|
||||
"new_shows": new_shows,
|
||||
"total_reviews": total_reviews,
|
||||
"top_performance_id": top_perf[0] if top_perf else None
|
||||
}
|
||||
|
||||
|
||||
def send_digest_email(user, stats: dict, highlights: dict, frontend_url: str):
|
||||
"""Send the weekly digest email to a user"""
|
||||
from services.email_service import email_service
|
||||
|
||||
subject = "Your Weekly Elmeg Digest"
|
||||
|
||||
# Build activity section
|
||||
activity_items = []
|
||||
if stats["shows_attended"] > 0:
|
||||
activity_items.append(f"🎵 Attended {stats['shows_attended']} show{'s' if stats['shows_attended'] > 1 else ''}")
|
||||
if stats["reviews_written"] > 0:
|
||||
activity_items.append(f"✍️ Wrote {stats['reviews_written']} review{'s' if stats['reviews_written'] > 1 else ''}")
|
||||
if stats["ratings_given"] > 0:
|
||||
activity_items.append(f"⭐ Gave {stats['ratings_given']} rating{'s' if stats['ratings_given'] > 1 else ''}")
|
||||
if stats["chase_caught"] > 0:
|
||||
activity_items.append(f"🎯 Caught {stats['chase_caught']} chase song{'s' if stats['chase_caught'] > 1 else ''}!")
|
||||
|
||||
activity_html = ""
|
||||
if activity_items:
|
||||
activity_html = "<ul>" + "".join([f"<li>{item}</li>" for item in activity_items]) + "</ul>"
|
||||
else:
|
||||
activity_html = "<p style='color: #666;'>No activity this week. Get out there and catch some shows!</p>"
|
||||
|
||||
html_content = f"""
|
||||
<html>
|
||||
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; background-color: #f5f5f5; padding: 20px;">
|
||||
<div style="max-width: 600px; margin: 0 auto; background: white; border-radius: 12px; padding: 40px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||||
<h2 style="color: #2563eb; margin-top: 0;">Weekly Digest</h2>
|
||||
|
||||
<h3 style="color: #374151; border-bottom: 2px solid #e5e7eb; padding-bottom: 8px;">Your Activity</h3>
|
||||
{activity_html}
|
||||
|
||||
<h3 style="color: #374151; border-bottom: 2px solid #e5e7eb; padding-bottom: 8px; margin-top: 30px;">Community Highlights</h3>
|
||||
<ul>
|
||||
<li>🆕 {highlights['new_shows']} new show{'s' if highlights['new_shows'] != 1 else ''} added</li>
|
||||
<li>📝 {highlights['total_reviews']} review{'s' if highlights['total_reviews'] != 1 else ''} written by the community</li>
|
||||
</ul>
|
||||
|
||||
<div style="margin: 30px 0; text-align: center;">
|
||||
<a href="{frontend_url}" style="background: linear-gradient(135deg, #2563eb, #4f46e5); color: white; padding: 14px 32px; text-decoration: none; border-radius: 8px; font-weight: 600; display: inline-block;">Visit Elmeg</a>
|
||||
</div>
|
||||
|
||||
<hr style="border: 0; border-top: 1px solid #eee; margin: 30px 0;">
|
||||
<p style="font-size: 12px; color: #999; margin-bottom: 0;">You're receiving this because you enabled weekly digests. <a href="{frontend_url}/settings" style="color: #2563eb;">Manage preferences</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
text_content = f"""
|
||||
Weekly Elmeg Digest
|
||||
|
||||
YOUR ACTIVITY:
|
||||
- Attended {stats['shows_attended']} shows
|
||||
- Wrote {stats['reviews_written']} reviews
|
||||
- Gave {stats['ratings_given']} ratings
|
||||
- Caught {stats['chase_caught']} chase songs
|
||||
|
||||
COMMUNITY HIGHLIGHTS:
|
||||
- {highlights['new_shows']} new shows added
|
||||
- {highlights['total_reviews']} reviews written
|
||||
|
||||
Visit Elmeg: {frontend_url}
|
||||
|
||||
To disable weekly digests, visit {frontend_url}/settings
|
||||
"""
|
||||
|
||||
return email_service.send_email(user.email, subject, html_content, text_content)
|
||||
|
||||
|
||||
def send_weekly_digests():
|
||||
"""Main function to send all weekly digest emails"""
|
||||
print("=" * 60)
|
||||
print("WEEKLY DIGEST EMAIL SENDER")
|
||||
print("=" * 60)
|
||||
|
||||
# Calculate date range (last 7 days)
|
||||
end_date = datetime.utcnow()
|
||||
start_date = end_date - timedelta(days=7)
|
||||
|
||||
print(f"Period: {start_date.date()} to {end_date.date()}")
|
||||
|
||||
from services.email_service import email_service
|
||||
frontend_url = email_service.frontend_url
|
||||
|
||||
with Session(engine) as session:
|
||||
# Get community highlights (shared across all emails)
|
||||
highlights = get_community_highlights(session, start_date, end_date)
|
||||
print(f"Community stats: {highlights['new_shows']} shows, {highlights['total_reviews']} reviews")
|
||||
|
||||
# Find all users with email_digest enabled
|
||||
users_with_digest = session.exec(
|
||||
select(User)
|
||||
.where(User.is_active == True)
|
||||
.where(User.email_verified == True)
|
||||
).all()
|
||||
|
||||
sent_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for user in users_with_digest:
|
||||
# Check if user has digest enabled
|
||||
if not user.preferences or not user.preferences.email_digest:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Get user's stats
|
||||
stats = get_weekly_stats(session, user.id, start_date, end_date)
|
||||
|
||||
# Send the digest
|
||||
try:
|
||||
success = send_digest_email(user, stats, highlights, frontend_url)
|
||||
if success:
|
||||
sent_count += 1
|
||||
print(f" ✓ Sent to {user.email}")
|
||||
else:
|
||||
print(f" ✗ Failed for {user.email}")
|
||||
except Exception as e:
|
||||
print(f" ✗ Error for {user.email}: {e}")
|
||||
|
||||
print(f"\n✓ Sent {sent_count} digest emails, skipped {skipped_count}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
send_weekly_digests()
|
||||
|
|
@ -130,6 +130,30 @@ services:
|
|||
networks:
|
||||
- elmeg
|
||||
|
||||
# Weekly digest email cron job (runs Sunday 9am UTC)
|
||||
cron:
|
||||
build: ./backend
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
environment:
|
||||
- DATABASE_URL=${DATABASE_URL:-postgresql://elmeg:elmeg_password@db:5432/elmeg}
|
||||
- SMTP_HOST=${SMTP_HOST}
|
||||
- SMTP_PORT=${SMTP_PORT:-25}
|
||||
- SMTP_USERNAME=${SMTP_USERNAME}
|
||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||
- SMTP_USE_TLS=${SMTP_USE_TLS:-true}
|
||||
- EMAIL_FROM=${EMAIL_FROM:-noreply@elmeg.xyz}
|
||||
- FRONTEND_URL=${FRONTEND_URL:-https://elmeg.xyz}
|
||||
command: >
|
||||
sh -c "echo '0 9 * * 0 cd /app && python services/weekly_digest.py >> /var/log/cron.log 2>&1' | crontab - && crond -f -l 2"
|
||||
depends_on:
|
||||
- db
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- elmeg
|
||||
- postal-internal
|
||||
|
||||
umami:
|
||||
image: ghcr.io/umami-software/umami:postgresql-latest
|
||||
restart: unless-stopped
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue