reservations: Refactor sync_google_calendar to use class
This commit is contained in:
parent
06fd819acf
commit
deb1165afc
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user