reservations: Refactor sync_google_calendar to use class

This commit is contained in:
Adam Goldsmith 2024-08-19 18:47:44 -04:00
parent 06fd819acf
commit deb1165afc

View File

@ -1,6 +1,7 @@
import logging import logging
from datetime import date, datetime from datetime import date, datetime
from http import HTTPStatus from http import HTTPStatus
from typing import TYPE_CHECKING
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
@ -9,6 +10,9 @@ from google.oauth2 import service_account
from googleapiclient.discovery import build from googleapiclient.discovery import build
from googleapiclient.errors import HttpError from googleapiclient.errors import HttpError
if TYPE_CHECKING:
from googleapiclient._apis.calendar.v3 import CalendarResource, Event
from cmsmanage.django_q2_helper import q_task_group from cmsmanage.django_q2_helper import q_task_group
from reservations.models import ExternalReservation, Reservation, Resource from reservations.models import ExternalReservation, Reservation, Resource
@ -26,196 +30,195 @@ def parse_google_calendar_datetime(dt) -> date | datetime:
raise Exception("Google Calendar event with out a start/end date/dateTime") raise Exception("Google Calendar event with out a start/end date/dateTime")
def update_calendar_event( class GoogleCalendarSynchronizer:
service, resource: Resource, existing_event, reservation: Reservation service: "CalendarResource"
):
changes = reservation.make_google_calendar_event() def __init__(self) -> None:
# skip update if no changes are needed self.service = build(
if ( "calendar",
parse_google_calendar_datetime(existing_event["start"]) != reservation.start "v3",
or parse_google_calendar_datetime(existing_event["end"]) != reservation.end credentials=service_account.Credentials.from_service_account_file(
or any( settings.GOOGLE_SERVICE_ACCOUNT_FILE,
existing_event[k] != v scopes=SCOPES,
for k, v in changes.items() ),
if k not in ("start", "end")
) )
def update_calendar_event(
self, resource: Resource, existing_event: "Event", reservation: Reservation
): ):
logger.debug("Updating event") changes = reservation.make_google_calendar_event()
new_event = existing_event | changes # skip update if no changes are needed
service.events().update(
calendarId=resource.google_calendar,
eventId=reservation.google_calendar_event_id,
body=new_event,
).execute()
def insert_calendar_event(service, resource: Resource, reservation: Reservation):
new_gcal_event = reservation.make_google_calendar_event()
created_event = (
service.events()
.insert(
calendarId=resource.google_calendar,
body=new_gcal_event,
)
.execute()
)
reservation.google_calendar_event_id = created_event["id"]
reservation.save()
def insert_or_update_calendar_event(
service, resource: Resource, reservation: Reservation
):
if not reservation.google_calendar_event_id:
logger.info(
"Event in database has no Google Calendar event ID: inserting | %s",
reservation.google_calendar_event_id,
)
insert_calendar_event(service, resource, reservation)
else:
# this event was in Google Calendar at some point (possibly for a different
# resource/calendar), but did not appear in list(). Try to update it, then
# fall back to insert
logger.info(
"Reservation with event id not in Google Calendar: trying update | %s",
reservation.google_calendar_event_id,
)
try:
event = (
service.events()
.get(
calendarId=resource.google_calendar,
eventId=reservation.google_calendar_event_id,
)
.execute()
)
update_calendar_event(service, resource, event, reservation)
except HttpError as error:
if error.status_code == HTTPStatus.NOT_FOUND:
logger.info(
"Event in database not in Google Calendar: inserting | %s",
reservation.google_calendar_event_id,
)
insert_calendar_event(service, resource, reservation)
else:
raise
def sync_resource_from_google_calendar(
service, resource: Resource, now: datetime
) -> set[str]:
request = (
service.events()
.list(
calendarId=resource.google_calendar,
timeMin=now.isoformat(timespec="seconds"),
maxResults=2500,
)
.execute()
)
if "nextPageToken" in request:
# TODO: implement pagination
raise Exception(
"More events than fit on a page, and pagination not implemented"
)
events = request["items"]
for event in events:
if ( if (
"extendedProperties" in event parse_google_calendar_datetime(existing_event["start"]) != reservation.start
and "private" in event["extendedProperties"] or parse_google_calendar_datetime(existing_event["end"]) != reservation.end
and event["extendedProperties"]["private"].get("cmsmanage") == "1" or any(
existing_event[k] != v
for k, v in changes.items()
if k not in ("start", "end")
)
): ):
try: logger.debug("Updating event")
reservation = resource.reservation_set.get_subclass( new_event = existing_event | changes
google_calendar_event_id=event["id"] self.service.events().update(
) calendarId=resource.google_calendar,
# event exists in both Google Calendar and database, check for update eventId=reservation.google_calendar_event_id,
logger.debug( body=new_event,
"Event in Google Calendar found in database: checking for update | %s", ).execute()
event["id"],
) def insert_calendar_event(self, resource: Resource, reservation: Reservation):
update_calendar_event(service, resource, event, reservation) new_gcal_event = reservation.make_google_calendar_event()
except Reservation.DoesNotExist: created_event = (
# reservation deleted in database, so remove from Google Calendar self.service.events()
logger.info( .insert(
"Event in Google Calendar not found in database: deleting | %s", calendarId=resource.google_calendar,
event["id"], body=new_gcal_event,
)
service.events().delete(
calendarId=resource.google_calendar,
eventId=event["id"],
sendUpdates="none",
).execute()
else:
logger.debug(
"Event in Google Calendar not originated by CMSManage: adding/updating as external reservation | %s",
event["id"],
) )
# TODO: this might cause issues if something external .execute()
# creates events with matching IDs in different calendars )
reservation, created = ExternalReservation.objects.update_or_create( reservation.google_calendar_event_id = created_event["id"]
google_calendar_event_id=event["id"], reservation.save()
defaults={
"title": event["summary"],
"start": parse_google_calendar_datetime(event["start"]),
"end": parse_google_calendar_datetime(event["end"]),
},
)
reservation.resources.add(resource)
return {event["id"] for event in events} def insert_or_update_calendar_event(
self, resource: Resource, reservation: Reservation
):
def sync_reservation_from_database(service, reservation: Reservation): if not reservation.google_calendar_event_id:
for resource in reservation.resources.all():
insert_or_update_calendar_event(service, resource, reservation)
def sync_resource_from_database(
service, resource: Resource, now: datetime, existing_event_ids: set[str]
):
reservations = (
resource.reservation_set.filter(end__gt=now)
# skip events we already pulled from Google Calendar during this sync
.exclude(google_calendar_event_id__in=existing_event_ids)
.select_subclasses()
)
# TODO: this could probably be more efficient?
for reservation in reservations:
if isinstance(reservation, ExternalReservation):
logger.info( logger.info(
"External event in database did not exist in future of Google Calendar: deleting locally | %s", "Event in database has no Google Calendar event ID: inserting | %s",
reservation.google_calendar_event_id, reservation.google_calendar_event_id,
) )
reservation.delete() self.insert_calendar_event(resource, reservation)
else: else:
insert_or_update_calendar_event(service, resource, reservation) # this event was in Google Calendar at some point (possibly for a different
# resource/calendar), but did not appear in list(). Try to update it, then
# fall back to insert
logger.info(
"Reservation with event id not in Google Calendar: trying update | %s",
reservation.google_calendar_event_id,
)
try:
event = (
self.service.events()
.get(
calendarId=resource.google_calendar,
eventId=reservation.google_calendar_event_id,
)
.execute()
)
self.update_calendar_event(resource, event, reservation)
except HttpError as error:
if error.status_code == HTTPStatus.NOT_FOUND:
logger.info(
"Event in database not in Google Calendar: inserting | %s",
reservation.google_calendar_event_id,
)
self.insert_calendar_event(resource, reservation)
else:
raise
def sync_resource_from_google_calendar(
self, resource: Resource, now: datetime
) -> set[str]:
request = (
self.service.events()
.list(
calendarId=resource.google_calendar,
timeMin=now.isoformat(timespec="seconds"),
maxResults=2500,
)
.execute()
)
if "nextPageToken" in request:
# TODO: implement pagination
raise Exception(
"More events than fit on a page, and pagination not implemented"
)
events = request["items"]
def sync_resource(service, resource: Resource, now: datetime): for event in events:
logger.info( if (
"Checking calendar %s for resource %s", resource.google_calendar, resource "extendedProperties" in event
) and "private" in event["extendedProperties"]
and event["extendedProperties"]["private"].get("cmsmanage") == "1"
):
try:
reservation = resource.reservation_set.get_subclass(
google_calendar_event_id=event["id"]
)
# event exists in both Google Calendar and database, check for update
logger.debug(
"Event in Google Calendar found in database: checking for update | %s",
event["id"],
)
self.update_calendar_event(resource, event, reservation)
except Reservation.DoesNotExist:
# reservation deleted in database, so remove from Google Calendar
logger.info(
"Event in Google Calendar not found in database: deleting | %s",
event["id"],
)
self.service.events().delete(
calendarId=resource.google_calendar,
eventId=event["id"],
sendUpdates="none",
).execute()
else:
logger.debug(
"Event in Google Calendar not originated by CMSManage: adding/updating as external reservation | %s",
event["id"],
)
# TODO: this might cause issues if something external
# creates events with matching IDs in different calendars
reservation, created = ExternalReservation.objects.update_or_create(
google_calendar_event_id=event["id"],
defaults={
"title": event["summary"],
"start": parse_google_calendar_datetime(event["start"]),
"end": parse_google_calendar_datetime(event["end"]),
},
)
reservation.resources.add(resource)
existing_event_ids = sync_resource_from_google_calendar(service, resource, now) return {event["id"] for event in events}
sync_resource_from_database(service, resource, now, existing_event_ids)
def sync_reservation_from_database(self, reservation: Reservation):
for resource in reservation.resources.all():
self.insert_or_update_calendar_event(resource, reservation)
def sync_resource_from_database(
self, resource: Resource, now: datetime, existing_event_ids: set[str]
):
reservations = (
resource.reservation_set.filter(end__gt=now)
# skip events we already pulled from Google Calendar during this sync
.exclude(google_calendar_event_id__in=existing_event_ids)
.select_subclasses()
)
# TODO: this could probably be more efficient?
for reservation in reservations:
if isinstance(reservation, ExternalReservation):
logger.info(
"External event in database did not exist in future of Google Calendar: deleting locally | %s",
reservation.google_calendar_event_id,
)
reservation.delete()
else:
self.insert_or_update_calendar_event(resource, reservation)
def sync_resource(self, resource: Resource, now: datetime):
logger.info(
"Checking calendar %s for resource %s", resource.google_calendar, resource
)
existing_event_ids = self.sync_resource_from_google_calendar(resource, now)
self.sync_resource_from_database(resource, now, existing_event_ids)
@q_task_group("Sync Reservations with Google Calendar") @q_task_group("Sync Reservations with Google Calendar")
def sync_reservations_with_google_calendar(): def sync_reservations_with_google_calendar():
service = build( synchronizer = GoogleCalendarSynchronizer()
"calendar",
"v3",
credentials=service_account.Credentials.from_service_account_file(
settings.GOOGLE_SERVICE_ACCOUNT_FILE,
scopes=SCOPES,
),
)
now = timezone.now() now = timezone.now()
for resource in Resource.objects.all(): for resource in Resource.objects.all():
sync_resource(service, resource, now) synchronizer.sync_resource(resource, now)