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,184 @@
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from google.auth.transport.requests import Request
|
||||
from google.oauth2.credentials import Credentials
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
from scripts.edoxid_calendar_config import GoogleConfig
|
||||
|
||||
JAKARTA_TZ = ZoneInfo("Asia/Jakarta")
|
||||
|
||||
TYPE_LABELS = {
|
||||
1: "Seminar Proposal",
|
||||
2: "Seminar Hasil",
|
||||
3: "Sidang Akhir",
|
||||
}
|
||||
|
||||
DAY_NAMES = {
|
||||
"Monday": "Senin", "Tuesday": "Selasa", "Wednesday": "Rabu", "Thursday": "Kamis",
|
||||
"Friday": "Jumat", "Saturday": "Sabtu", "Sunday": "Minggu",
|
||||
}
|
||||
|
||||
MONTH_NAMES = {
|
||||
"January": "Januari", "February": "Februari", "March": "Maret", "April": "April",
|
||||
"May": "Mei", "June": "Juni", "July": "Juli", "August": "Agustus",
|
||||
"September": "September", "October": "Oktober", "November": "November", "December": "Desember",
|
||||
}
|
||||
|
||||
|
||||
# --- Fungsi format, dipakai bareng oleh event Calendar & email reminder (edoxid_calendar_gmail_reminder.py) ---
|
||||
|
||||
def get_schedule_datetime(submission: dict) -> datetime:
|
||||
schedule = submission["schedule"]
|
||||
if isinstance(schedule, str):
|
||||
schedule = datetime.fromisoformat(schedule)
|
||||
return schedule.replace(tzinfo=JAKARTA_TZ)
|
||||
|
||||
|
||||
def get_type_label(submission: dict) -> str:
|
||||
return TYPE_LABELS.get(submission["type_id"], "Sidang")
|
||||
|
||||
|
||||
def format_indonesian_date(date: datetime) -> str:
|
||||
day = DAY_NAMES.get(date.strftime("%A"), date.strftime("%A"))
|
||||
month = MONTH_NAMES.get(date.strftime("%B"), date.strftime("%B"))
|
||||
return f"{day}, {date.strftime('%d')} {month} {date.strftime('%Y')}"
|
||||
|
||||
|
||||
def build_description(submission: dict) -> str:
|
||||
schedule = get_schedule_datetime(submission)
|
||||
type_label = get_type_label(submission)
|
||||
pembimbing = [v for v in submission["verificators"] if v["type"] == "Pembimbing"]
|
||||
penguji = [v for v in submission["verificators"] if v["type"] == "Penguji"]
|
||||
|
||||
lines = [
|
||||
f"UNDANGAN {type_label.upper()} SECARA LURING",
|
||||
"JURUSAN INFORMATIKA",
|
||||
"",
|
||||
"Mengundang Bapak dan Ibu dosen tim pembimbing dan penguji untuk hadir pada "
|
||||
f"{type_label.lower()} mahasiswa yang dilaksanakan secara Luring.",
|
||||
"",
|
||||
f"👨🎓 {submission['student_name']} - {submission['student_nim']}",
|
||||
"",
|
||||
f"📖 {submission['title']}",
|
||||
"",
|
||||
f"📅 {format_indonesian_date(schedule)}",
|
||||
f"🕑 {schedule.strftime('%H:%M')} WIB",
|
||||
"",
|
||||
]
|
||||
|
||||
for i, v in enumerate(pembimbing, start=1):
|
||||
lines.append(f"📕 Pembimbing {i} : {v['name']}")
|
||||
for i, v in enumerate(penguji, start=1):
|
||||
lines.append(f"📒 Penguji {i} : {v['name']}")
|
||||
|
||||
lines += [
|
||||
"",
|
||||
"Tempat:",
|
||||
submission.get("room") or "-",
|
||||
"",
|
||||
"Terimakasih.",
|
||||
"ttd",
|
||||
"Kajur Informatika",
|
||||
]
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def format_submission_summary(submission: dict) -> str:
|
||||
"""Ringkasan 1-baris untuk log Airflow — supaya kelihatan jelas di task log
|
||||
siapa yang dapat undangan/reminder tanpa perlu buka Calendar/email manual."""
|
||||
schedule = get_schedule_datetime(submission)
|
||||
pembimbing = [v["name"] for v in submission["verificators"] if v["type"] == "Pembimbing"]
|
||||
penguji = [v["name"] for v in submission["verificators"] if v["type"] == "Penguji"]
|
||||
|
||||
return (
|
||||
f"{submission['student_name']} ({submission['student_nim']}) — "
|
||||
f"{get_type_label(submission)} — \"{submission['title']}\" — "
|
||||
f"{format_indonesian_date(schedule)} {schedule.strftime('%H:%M')} WIB "
|
||||
f"@ {submission.get('room') or '-'} | "
|
||||
f"Pembimbing: {', '.join(pembimbing) or '-'} | "
|
||||
f"Penguji: {', '.join(penguji) or '-'}"
|
||||
)
|
||||
|
||||
|
||||
def build_attendee_emails(submission: dict) -> list[str]:
|
||||
emails = [submission["student_email"]] + [v["email"] for v in submission["verificators"]]
|
||||
seen = set()
|
||||
deduped = []
|
||||
for email in emails:
|
||||
if email and email not in seen:
|
||||
seen.add(email)
|
||||
deduped.append(email)
|
||||
return deduped
|
||||
|
||||
|
||||
class GoogleCalendarService:
|
||||
"""Auth via OAuth refresh token sebagai tu@informatika.untan.ac.id LANGSUNG
|
||||
(bukan service account) — service account tidak bisa invite attendee tanpa
|
||||
Domain-Wide Delegation of Authority (dicoba & ditolak Google 2026-07-27)."""
|
||||
|
||||
def __init__(self, config: GoogleConfig):
|
||||
self._calendar_id = config.calendar_id
|
||||
|
||||
creds = Credentials(
|
||||
token=None,
|
||||
refresh_token=config.refresh_token,
|
||||
token_uri="https://oauth2.googleapis.com/token",
|
||||
client_id=config.client_id,
|
||||
client_secret=config.client_secret,
|
||||
scopes=["https://www.googleapis.com/auth/calendar"],
|
||||
)
|
||||
creds.refresh(Request())
|
||||
|
||||
self._service = build("calendar", "v3", credentials=creds, cache_discovery=False)
|
||||
|
||||
def create_event(self, submission: dict) -> str:
|
||||
event = self._build_event(submission)
|
||||
result = (
|
||||
self._service.events()
|
||||
.insert(calendarId=self._calendar_id, body=event, sendUpdates="all")
|
||||
.execute()
|
||||
)
|
||||
return result["id"]
|
||||
|
||||
def update_event(self, event_id: str, submission: dict) -> None:
|
||||
event = self._build_event(submission)
|
||||
self._service.events().update(
|
||||
calendarId=self._calendar_id, eventId=event_id, body=event, sendUpdates="all"
|
||||
).execute()
|
||||
|
||||
def delete_event(self, event_id: str) -> None:
|
||||
try:
|
||||
self._service.events().delete(
|
||||
calendarId=self._calendar_id, eventId=event_id, sendUpdates="all"
|
||||
).execute()
|
||||
except HttpError as e:
|
||||
# 404/410 berarti event sudah tidak ada (mis. dihapus manual) — aman diabaikan
|
||||
if e.resp.status not in (404, 410):
|
||||
raise
|
||||
|
||||
def _build_event(self, submission: dict) -> dict:
|
||||
schedule = get_schedule_datetime(submission)
|
||||
end = schedule + timedelta(hours=1)
|
||||
|
||||
return {
|
||||
"summary": f"{get_type_label(submission)} — {submission['student_name']}",
|
||||
"description": build_description(submission),
|
||||
"location": submission.get("room") or "",
|
||||
"start": {"dateTime": schedule.isoformat(), "timeZone": "Asia/Jakarta"},
|
||||
"end": {"dateTime": end.isoformat(), "timeZone": "Asia/Jakarta"},
|
||||
"attendees": [{"email": email} for email in build_attendee_emails(submission)],
|
||||
"reminders": {"useDefault": False, "overrides": self._build_reminders(schedule)},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _build_reminders(schedule: datetime) -> list[dict]:
|
||||
h1 = schedule.replace(hour=7, minute=0, second=0, microsecond=0) - timedelta(days=1)
|
||||
minutes_h1 = max(0, int((schedule - h1).total_seconds() // 60))
|
||||
return [
|
||||
{"method": "popup", "minutes": minutes_h1},
|
||||
{"method": "popup", "minutes": 60},
|
||||
]
|
||||
Reference in New Issue
Block a user