add edoxid calendar sync dag
Sync jadwal sidang eDOXID ke Google Calendar "Jadwal Sidang" tiap 5 menit (read-only ke doxid2022), plus reminder email H-1/1-jam via Gmail API karena reminder Calendar bawaan tidak ikut ke kalender pribadi attendee.
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from scripts.edoxid_calendar_config import (
|
||||
get_db_config,
|
||||
get_google_config,
|
||||
get_state_sqlite_path,
|
||||
get_sync_min_date,
|
||||
)
|
||||
from scripts.edoxid_calendar_database import Database
|
||||
from scripts.edoxid_calendar_gmail import GmailService
|
||||
from scripts.edoxid_calendar_google import (
|
||||
GoogleCalendarService,
|
||||
build_attendee_emails,
|
||||
build_description,
|
||||
format_submission_summary,
|
||||
get_schedule_datetime,
|
||||
get_type_label,
|
||||
)
|
||||
from scripts.edoxid_calendar_state_store import StateStore
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
JAKARTA_TZ = ZoneInfo("Asia/Jakarta")
|
||||
|
||||
# Toleransi jendela waktu untuk cek reminder — harus >= interval polling DAG (5 menit)
|
||||
# supaya tidak ada window yang "kelewat" di antara 2 run.
|
||||
REMINDER_WINDOW = timedelta(minutes=10)
|
||||
|
||||
|
||||
def _check_and_send_reminders(submissions: list[dict], state: StateStore, gmail: GmailService) -> int:
|
||||
"""Kirim email reminder H-1 (07:00) & 1 jam sebelum ke semua attendee —
|
||||
terpisah dari reminders.overrides Calendar (yang cuma berlaku untuk
|
||||
calendar TU sendiri, tidak ikut ke kalender pribadi tiap attendee)."""
|
||||
now = datetime.now(JAKARTA_TZ)
|
||||
sent = 0
|
||||
|
||||
for submission in submissions:
|
||||
schedule = get_schedule_datetime(submission)
|
||||
h1_time = schedule.replace(hour=7, minute=0, second=0, microsecond=0) - timedelta(days=1)
|
||||
one_hour_time = schedule - timedelta(hours=1)
|
||||
emails = build_attendee_emails(submission)
|
||||
if not emails:
|
||||
continue
|
||||
|
||||
if h1_time <= now < h1_time + REMINDER_WINDOW and not state.is_notified(submission["id"], "h1"):
|
||||
subject = f"[PENGINGAT SEBELUM H-1] {get_type_label(submission)} — {submission['student_name']}"
|
||||
gmail.send(emails, subject, build_description(submission))
|
||||
state.mark_notified(submission["id"], "h1")
|
||||
sent += 1
|
||||
logger.info(
|
||||
"REMINDER H-1 terkirim | submission #%s | %s | penerima: %s",
|
||||
submission["id"], format_submission_summary(submission), ", ".join(emails),
|
||||
)
|
||||
|
||||
if one_hour_time <= now < one_hour_time + REMINDER_WINDOW and not state.is_notified(
|
||||
submission["id"], "1h"
|
||||
):
|
||||
subject = f"[PENGINGAT 1 JAM SEBELUM] {get_type_label(submission)} — {submission['student_name']}"
|
||||
gmail.send(emails, subject, build_description(submission))
|
||||
state.mark_notified(submission["id"], "1h")
|
||||
sent += 1
|
||||
logger.info(
|
||||
"REMINDER 1-JAM terkirim | submission #%s | %s | penerima: %s",
|
||||
submission["id"], format_submission_summary(submission), ", ".join(emails),
|
||||
)
|
||||
|
||||
return sent
|
||||
|
||||
|
||||
def run_sync() -> dict:
|
||||
db = Database(get_db_config())
|
||||
state = StateStore(get_state_sqlite_path())
|
||||
google_config = get_google_config()
|
||||
calendar = GoogleCalendarService(google_config)
|
||||
gmail = GmailService(google_config)
|
||||
|
||||
created = updated = deleted = skipped = 0
|
||||
|
||||
try:
|
||||
submissions = db.get_scheduled_submissions(get_sync_min_date())
|
||||
|
||||
for submission in submissions:
|
||||
content_hash = state.compute_hash(submission)
|
||||
existing = state.find(submission["id"])
|
||||
|
||||
if existing is None:
|
||||
event_id = calendar.create_event(submission)
|
||||
state.save(submission["id"], event_id, content_hash)
|
||||
created += 1
|
||||
logger.info(
|
||||
"CREATE event %s | submission #%s | %s",
|
||||
event_id, submission["id"], format_submission_summary(submission),
|
||||
)
|
||||
elif existing["content_hash"] != content_hash:
|
||||
calendar.update_event(existing["event_id"], submission)
|
||||
state.update_hash(submission["id"], content_hash)
|
||||
updated += 1
|
||||
logger.info(
|
||||
"UPDATE event %s | submission #%s | %s",
|
||||
existing["event_id"], submission["id"], format_submission_summary(submission),
|
||||
)
|
||||
else:
|
||||
skipped += 1
|
||||
|
||||
active_ids = db.get_ids_with_active_schedule()
|
||||
for submission_id, event_id in state.find_orphaned(active_ids).items():
|
||||
calendar.delete_event(event_id)
|
||||
state.remove(submission_id)
|
||||
deleted += 1
|
||||
logger.info("DELETE submission #%s -> event %s", submission_id, event_id)
|
||||
|
||||
reminders_sent = _check_and_send_reminders(submissions, state, gmail)
|
||||
finally:
|
||||
db.close()
|
||||
state.close()
|
||||
|
||||
summary = {
|
||||
"created": created,
|
||||
"updated": updated,
|
||||
"deleted": deleted,
|
||||
"skipped": skipped,
|
||||
"reminders_sent": reminders_sent,
|
||||
}
|
||||
logger.info("Sync selesai: %s", summary)
|
||||
return summary
|
||||
Reference in New Issue
Block a user