nordabiz/scripts/event_reminders_cron.py
Maciej Pienczyn c9985ba51a
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
feat(notifications): D.2+D.3 — forum, broadcasty Izby, wydarzenia, cron 24h
Rozszerzenie powiadomień o kolejne typy zdarzeń, z symetrycznymi togglami
e-mail i push w /konto/prywatnosc.

Migracje 103 + 104 — 6 nowych kolumn preferencji e-mail + NordaEvent.reminder_24h_sent_at.

Triggery:
- Forum odpowiedź → push do autora wątku (notify_push_forum_reply)
- Forum cytat (> **Imię** napisał(a):) → push + email do cytowanego
  (notify_push/email_forum_quote)
- Admin publikuje aktualność → broadcast push (ON) + email (OFF)
  do aktywnych członków (notify_push/email_announcements)
- Board: utworzenie / publikacja programu / publikacja protokołu
  → broadcast push + opt-in email (notify_push/email_board_meetings)
- Nowe wydarzenie w kalendarzu → broadcast push + email (oba ON)
  (notify_push/email_event_invites)
- Cron scripts/event_reminders_cron.py co godzinę — wydarzenia za 23-25h,
  dla zapisanych (EventAttendee.status != 'declined') push + email,
  znacznik NordaEvent.reminder_24h_sent_at żeby nie dublować.

Email defaults dobrane, by nie zalać inbox: broadcast OFF (announcements,
board, forum_reply), personalne/actionable ON (forum_quote, event_invites,
event_reminders).

Wszystkie nowe e-maile mają jednym-kliknięciem unsubscribe (RFC 8058
+ link w stopce) — unsubscribe_tokens.py rozszerzony o nowe typy.

Cron entry do dodania na prod (osobny krok, bo to edycja crontaba):
  0 * * * * cd /var/www/nordabiznes && venv/bin/python3 scripts/event_reminders_cron.py

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:20:38 +02:00

136 lines
5.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Event reminders cron — wysyła przypomnienia push/email 24h przed wydarzeniem.
Uruchomienie (cron co godzinę):
0 * * * * cd /var/www/nordabiznes && venv/bin/python3 scripts/event_reminders_cron.py
Logika:
- Szuka wydarzeń, których event_date+time_start mieści się między now()+23h a now()+25h
- Które jeszcze nie mają reminder_24h_sent_at ustawionego
- Dla każdego: pobiera zapisanych (EventAttendee.status != 'declined')
- Dla każdego zapisanego: sprawdza notify_push_event_reminders i notify_email_event_reminders
- Wysyła push i/lub email
- Oznacza event.reminder_24h_sent_at = now()
"""
import os
import sys
import logging
from datetime import datetime, timedelta, time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from dotenv import load_dotenv
load_dotenv()
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger(__name__)
from database import SessionLocal, NordaEvent, EventAttendee, User
def _combine_event_datetime(event):
"""Zwraca datetime startu wydarzenia (data + time_start lub 09:00 jeśli brak)."""
t = event.time_start if event.time_start else time(9, 0)
return datetime.combine(event.event_date, t)
def main():
from email_service import init_email_service
init_email_service()
from blueprints.push.push_service import send_push
from email_service import send_email, _email_v3_wrap
now = datetime.now()
window_start = now + timedelta(hours=23)
window_end = now + timedelta(hours=25)
db = SessionLocal()
try:
candidates = db.query(NordaEvent).filter(
NordaEvent.event_date >= window_start.date(),
NordaEvent.event_date <= window_end.date(),
NordaEvent.reminder_24h_sent_at.is_(None),
).all()
fired = 0
for event in candidates:
event_dt = _combine_event_datetime(event)
if not (window_start <= event_dt <= window_end):
continue
attendees = db.query(EventAttendee).filter(
EventAttendee.event_id == event.id,
EventAttendee.status != 'declined',
).all()
if not attendees:
event.reminder_24h_sent_at = now
continue
time_str = event.time_start.strftime('%H:%M') if event.time_start else ''
date_str = event.event_date.strftime('%d.%m.%Y')
url_path = f'/kalendarz/{event.id}'
title = f'Jutro wydarzenie: {event.title[:60]}'
body = f'{date_str} {time_str} · {event.location or "Miejsce w szczegółach"}'
push_count = 0
email_count = 0
for a in attendees:
user = db.query(User).filter(User.id == a.user_id).first()
if not user or not user.is_active:
continue
try:
if getattr(user, 'notify_push_event_reminders', True) is not False:
send_push(
user_id=user.id,
title=title,
body=body,
url=url_path,
tag=f'event-reminder-{event.id}',
)
push_count += 1
except Exception as e:
logger.warning(f"push err user={user.id}: {e}")
try:
if user.email and getattr(user, 'notify_email_event_reminders', True) is not False:
subject = f"Przypomnienie: {event.title[:60]} jutro o {time_str}"
body_text = f"Przypominamy o wydarzeniu na które jesteś zapisany:\n\n{event.title}\nTermin: {date_str} {time_str}\nMiejsce: {event.location or '-'}\n\nSzczegóły: https://nordabiznes.pl{url_path}"
content = (
f'<p style="margin:0 0 16px;color:#1e293b;font-size:16px;">Cześć <strong>{user.name or user.email}</strong>!</p>'
f'<p style="margin:0 0 20px;color:#475569;font-size:15px;">Przypominamy o wydarzeniu na które jesteś zapisany:</p>'
f'<h3 style="color:#1e3a8a;">{event.title}</h3>'
f'<p style="color:#475569;line-height:1.6">Termin: <b>{date_str} {time_str}</b><br>'
f'Miejsce: {event.location or "-"}</p>'
f'<p style="margin:24px 0;"><a href="https://nordabiznes.pl{url_path}" '
f'style="background:#1e3a8a;color:#fff;padding:12px 24px;border-radius:6px;text-decoration:none">Zobacz szczegóły</a></p>'
)
body_html = _email_v3_wrap('Przypomnienie o wydarzeniu', 'Norda Biznes Partner', content)
send_email(
to=[user.email],
subject=subject,
body_text=body_text,
body_html=body_html,
email_type='event_reminder',
user_id=user.id,
recipient_name=user.name,
notification_type='event_reminders',
)
email_count += 1
except Exception as e:
logger.warning(f"email err user={user.id}: {e}")
event.reminder_24h_sent_at = now
fired += 1
logger.info(f"event={event.id} '{event.title}' — push={push_count}, email={email_count}")
db.commit()
logger.info(f"Done. Fired reminders for {fired} event(s).")
finally:
db.close()
if __name__ == '__main__':
main()