"""HTML email builders for scheduled admin notifications. Each builder is registered in ``BUILDERS`` keyed by ``NotificationSchedule.notification_type`` and returns ``(subject, html_body)``. Add new types by appending to the registry and extending ``NotificationSchedule.TYPE_CHOICES``. Week bounds for ``events_expiring_this_week`` are computed in Asia/Kolkata so the "this week" semantics match the operations team's wall-clock week regardless of ``settings.TIME_ZONE`` (currently UTC). """ from datetime import date, datetime, timedelta from html import escape try: from zoneinfo import ZoneInfo except ImportError: # pragma: no cover — fallback for py<3.9 from backports.zoneinfo import ZoneInfo # type: ignore from django.conf import settings from django.core.mail import EmailMessage from django.db.models import Q from eventify_logger.services import log IST = ZoneInfo('Asia/Kolkata') def _today_in_ist() -> date: return datetime.now(IST).date() def _upcoming_week_bounds(today: date) -> tuple[date, date]: """Return (next Monday, next Sunday) inclusive. If today is Monday the result is *this week* (today..Sunday). If today is any other weekday the result is *next week* (next Monday..next Sunday). Mon=0 per Python ``weekday()``. """ days_until_monday = (7 - today.weekday()) % 7 monday = today + timedelta(days=days_until_monday) sunday = monday + timedelta(days=6) return monday, sunday def _build_events_expiring_this_week(schedule) -> tuple[str, str]: from events.models import Event today = _today_in_ist() monday, sunday = _upcoming_week_bounds(today) qs = ( Event.objects .select_related('partner', 'event_type') .filter(event_status='published') .filter( Q(end_date__isnull=False, end_date__gte=monday, end_date__lte=sunday) | Q(end_date__isnull=True, start_date__gte=monday, start_date__lte=sunday) ) .order_by('end_date', 'start_date', 'name') ) events = list(qs) rows_html = '' for e in events: end = e.end_date or e.start_date title = e.title or e.name or '(untitled)' partner_name = '' if e.partner_id: try: partner_name = e.partner.name or '' except Exception: partner_name = '' category = '' if e.event_type_id and e.event_type: category = getattr(e.event_type, 'event_type', '') or '' rows_html += ( '' f'{escape(title)}' f'{escape(partner_name or "—")}' f'{escape(category or "—")}' f'' f'{end.strftime("%a %d %b %Y") if end else "—"}' '' ) if not events: rows_html = ( '' 'No published events are expiring next week.' '' ) subject = ( f'[Eventify] {len(events)} event(s) expiring ' f'{monday.strftime("%d %b")}–{sunday.strftime("%d %b")}' ) html = f"""

Events expiring next week

{monday.strftime("%A %d %b %Y")} → {sunday.strftime("%A %d %b %Y")} · {len(events)} event(s)

Scheduled notification: {escape(schedule.name)}

{rows_html}
Title Partner Category End date
Sent automatically by Eventify Command Center. To change recipients or the schedule, open admin.eventifyplus.com › Settings › Notifications.
""" return subject, html BUILDERS: dict = { 'events_expiring_this_week': _build_events_expiring_this_week, } def render_and_send(schedule) -> int: """Render the email for ``schedule`` and deliver it to active recipients. Returns the number of recipients the message was sent to. Raises on SMTP failure so the management command can mark the schedule as errored. """ builder = BUILDERS.get(schedule.notification_type) if builder is None: raise ValueError(f'No builder for notification type: {schedule.notification_type}') subject, html = builder(schedule) recipients = list( schedule.recipients.filter(is_active=True).values_list('email', flat=True) ) if not recipients: log('warning', 'notification schedule has no active recipients', logger_data={ 'schedule_id': schedule.id, 'schedule_name': schedule.name, }) return 0 msg = EmailMessage( subject=subject, body=html, from_email=settings.DEFAULT_FROM_EMAIL, to=recipients, ) msg.content_subtype = 'html' msg.send(fail_silently=False) log('info', 'notification email sent', logger_data={ 'schedule_id': schedule.id, 'schedule_name': schedule.name, 'type': schedule.notification_type, 'recipient_count': len(recipients), }) return len(recipients)