Files
airflow-coolify/scripts/edoxid_calendar_google.py
T
Power BI Dev 33f4666bad 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.
2026-07-27 21:59:27 +07:00

185 lines
6.8 KiB
Python

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},
]