33f4666bad
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.
185 lines
6.8 KiB
Python
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},
|
|
]
|