1+ import logging
12from datetime import timedelta
23
4+ from django .conf import settings
35from django .db import models
46from django .utils import timezone
57from django .utils .encoding import force_bytes
911from apps .user .models import EmailNotificationType , User
1012from main .permalinks import Permalink
1113from main .tokens import TokenManager
14+ from utils .common import logger_log_extra
1215from utils .emails import send_email
1316
17+ logger = logging .getLogger (__name__ )
18+
1419
1520def generate_unsubscribe_user_alert_subscription_url (subscription : UserAlertSubscription ) -> str :
1621 uid = urlsafe_base64_encode (force_bytes (subscription .pk ))
@@ -22,7 +27,7 @@ def generate_unsubscribe_user_alert_subscription_url(subscription: UserAlertSubs
2227def generate_user_alert_subscription_email_context (
2328 user : User ,
2429 email_frequency : UserAlertSubscription .EmailFrequency ,
25- ) -> tuple [dict , models .QuerySet [UserAlertSubscription ]]:
30+ ) -> tuple [bool , dict , models .QuerySet [UserAlertSubscription ]]:
2631 # NOTE: Number of subscription is static and less than UserAlertSubscription.LIMIT_PER_USER
2732 subscription_qs = UserAlertSubscription .objects .filter (user = user , email_frequency = email_frequency )
2833
@@ -31,62 +36,90 @@ def generate_user_alert_subscription_email_context(
3136 elif email_frequency == UserAlertSubscription .EmailFrequency .WEEKLY :
3237 from_datetime_threshold = timezone .now () - timedelta (days = 7 )
3338 elif email_frequency == UserAlertSubscription .EmailFrequency .MONTHLY :
34- # TODO: Calculate days instead of using 30 days
39+ # TODO: Calculate month days instead of using 30 days
3540 from_datetime_threshold = timezone .now () - timedelta (days = 30 )
3641
37- subscription_data = [
38- {
39- 'subscription' : subscription ,
40- 'unsubscribe_url' : generate_unsubscribe_user_alert_subscription_url (subscription ),
41- 'latest_alerts' : [
42- subscription_alert .alert
43- # NOTE: N+1 query, but N < 10 for now
44- # TODO: Index/partition alert__sent column?
45- for subscription_alert in (
46- SubscriptionAlert .objects .select_related ('alert' )
47- .filter (
48- subscription = subscription ,
49- alert__sent__gte = from_datetime_threshold ,
50- )
51- .order_by ('-alert__sent' )[:5 ]
52- )
53- ],
42+ def _alert_data (alert ):
43+ # TODO: Fix N+1 for alert.infos.first() and alert.admin1s
44+ info = alert .infos .first ()
45+ return {
46+ "url" : Permalink .alert_detail (alert .pk ),
47+ "name" : info and info .event or f"Alert #{ alert .pk } " ,
48+ "urgency" : info and info .urgency or '-' ,
49+ "severity" : info and info .severity or '-' ,
50+ "certainty" : info and info .certainty or '-' ,
51+ "admins" : "," .join (list (alert .admin1s .values_list ("name" , flat = True ))) or '-' ,
5452 }
55- for subscription in subscription_qs
56- ]
53+
54+ subscription_data = []
55+ for subscription in subscription_qs .iterator ():
56+ latest_alerts = [
57+ _alert_data (subscription_alert .alert )
58+ # NOTE: N+1 query, but N < 10 for now
59+ # TODO: Index/partition alert__sent column?
60+ for subscription_alert in (
61+ SubscriptionAlert .objects .select_related ('alert' )
62+ .filter (
63+ subscription = subscription ,
64+ alert__sent__gte = from_datetime_threshold ,
65+ )
66+ .order_by ('-alert__sent' )[:5 ]
67+ )
68+ ]
69+ if latest_alerts :
70+ subscription_data .append (
71+ {
72+ 'subscription' : subscription ,
73+ 'url' : Permalink .subscription_detail (subscription .pk ),
74+ 'unsubscribe_url' : generate_unsubscribe_user_alert_subscription_url (subscription ),
75+ 'latest_alerts' : latest_alerts ,
76+ }
77+ )
5778
5879 context = {
59- 'subscriptions ' : subscription_data ,
80+ 'subscriptions_data ' : subscription_data ,
6081 }
6182
62- return context , subscription_qs
83+ return len ( context [ "subscriptions_data" ]) > 0 , context , subscription_qs
6384
6485
6586def send_user_alert_subscription_email (user : User , email_frequency : UserAlertSubscription .EmailFrequency ):
66- context , subscription_qs = generate_user_alert_subscription_email_context (user , email_frequency )
87+ have_data , context , subscription_qs = generate_user_alert_subscription_email_context (user , email_frequency )
6788 sent_at = timezone .now ()
6889
69- send_email (
70- user = user ,
71- email_type = EmailNotificationType .ALERT_SUBSCRIPTIONS ,
72- subject = "Daily Alerts" , # TODO: Is this fine?
73- email_html_template = 'emails/subscription/body.html' ,
74- email_text_template = 'emails/subscription/body.txt' ,
75- context = context ,
76- )
90+ if have_data :
91+ send_email (
92+ user = user ,
93+ email_type = EmailNotificationType .ALERT_SUBSCRIPTIONS ,
94+ subject = f"{ settings .EMAIL_SUBJECT_PREFIX } { email_frequency .label } " ,
95+ email_html_template = 'emails/subscription/body.html' ,
96+ email_text_template = 'emails/subscription/body.txt' ,
97+ context = context ,
98+ )
7799
78100 # Post action
79101 subscription_qs .update (email_last_sent_at = sent_at )
80102
81103
82104def send_user_alert_subscriptions_email (email_frequency : UserAlertSubscription .EmailFrequency ):
83- # TODO: Send in parallel if email service supports it
105+ # TODO: Send in parallel if email service supports it?
84106 users_qs = User .objects .filter (
85107 id__in = UserAlertSubscription .objects .filter (email_frequency = email_frequency ).values ('user' ),
86108 )
87109
88- # TODO: Handle failure
89110 for user in users_qs .iterator ():
90- # TODO: Trigger this as cronjob
91- # TODO: Pass timezone.now for ref time
92- send_user_alert_subscription_email (user , email_frequency )
111+ # TODO: Trigger this as cronjob?
112+ # TODO: Pass timezone.now for ref time?
113+ try :
114+ send_user_alert_subscription_email (user , email_frequency )
115+ except Exception :
116+ logger .error (
117+ "Subscription: Failed to send email to user" ,
118+ exc_info = True ,
119+ extra = logger_log_extra (
120+ {
121+ 'user_id' : user .pk ,
122+ 'email_frequency' : email_frequency ,
123+ }
124+ ),
125+ )
0 commit comments