import logging from celery.utils.log import get_task_logger from django.db.models import Q from apps.google import constants from apps.google.client import ( GoogleCalendarAPIClient, GoogleCalendarGenericHTTPError, GoogleCalendarRefreshError, GoogleCalendarUnauthorizedHTTPError, ) from apps.google.models import GoogleOAuth2User from apps.schedules.models import OnCallSchedule, ShiftSwapRequest from common.custom_celery_tasks import shared_dedicated_queue_retry_task logger = get_task_logger(__name__) logger.setLevel(logging.DEBUG) @shared_dedicated_queue_retry_task(autoretry_for=(Exception,), retry_backoff=True) def sync_out_of_office_calendar_events_for_user(google_oauth2_user_pk: int) -> None: google_oauth2_user = GoogleOAuth2User.objects.get(pk=google_oauth2_user_pk) google_api_client = GoogleCalendarAPIClient(google_oauth2_user.access_token, google_oauth2_user.refresh_token) user = google_oauth2_user.user user_id = user.pk logger.info(f"Syncing out of office Google Calendar events for user {user_id}") users_schedules = OnCallSchedule.objects.related_to_user(user) user_google_calendar_settings = user.google_calendar_settings oncall_schedules_to_consider_for_shift_swaps = user_google_calendar_settings[ "oncall_schedules_to_consider_for_shift_swaps" ] if oncall_schedules_to_consider_for_shift_swaps: users_schedules = users_schedules.filter(public_primary_key__in=oncall_schedules_to_consider_for_shift_swaps) try: out_of_office_events = google_api_client.fetch_out_of_office_events() except GoogleCalendarUnauthorizedHTTPError: # this means the user's current token is missing the required scopes, don't delete their token for now # we'll notify them via the plugin UI and ask them to reauth and grant us the missing scope logger.warning( f"Failed to fetch out of office events for user {user_id} due to missing required scopes. " "Safe to skip for now" ) return except GoogleCalendarRefreshError: # in this scenarios there's really not much we can do with the refresh/access token that we # have available. The user will need to re-connect with Google so lets delete their persisted token logger.exception( f"Failed to fetch out of office events for user {user_id} due to an invalid access and/or refresh token" ) user.reset_google_oauth2_settings() return except GoogleCalendarGenericHTTPError: logger.exception(f"Failed to fetch out of office events for user {user_id} due to a generic HTTP error") return for out_of_office_event in out_of_office_events: raw_event = out_of_office_event.raw_event event_title = raw_event["summary"] event_id = raw_event["id"] start_time_utc = out_of_office_event.start_time_utc end_time_utc = out_of_office_event.end_time_utc logger.info( f"Processing out of office event {event_id} starting at {start_time_utc} and ending at " f"{end_time_utc} for user {user_id}" ) if constants.EVENT_SUMMARY_IGNORE_KEYWORD in event_title.lower(): logger.info( f"Skipping out of office event {event_id} because it contains the ignore keyword " f"'{constants.EVENT_SUMMARY_IGNORE_KEYWORD}'" ) continue for schedule in users_schedules: _, current_shifts, upcoming_shifts = schedule.shifts_for_user( user, start_time_utc, datetime_end=end_time_utc, ) if current_shifts or upcoming_shifts: logger.info( f"Found {len(current_shifts)} current shift(s) and {len(upcoming_shifts)} upcoming shift(s) " f"for user {user_id} during the out of office event {event_id}" ) # also consider deleted shift swap requests.. this can be useful in the event # that we autogenerated a shift swap request for the user but the user decided to delete it # in this case, we shouldn't recreate a new one shift_swap_request_exists = ShiftSwapRequest.objects_with_deleted.filter( beneficiary=user, schedule=schedule, swap_start=start_time_utc, swap_end=end_time_utc, ).exists() if not shift_swap_request_exists: logger.info( f"Creating shift swap request for user {user_id} schedule {schedule.pk} " f"due to the out of office event {event_id}" ) ssr = ShiftSwapRequest.objects.create( beneficiary=user, schedule=schedule, swap_start=start_time_utc, swap_end=end_time_utc, description=f"{user.name or user.email} will be out of office during this time according to Google Calendar", ) logger.info(f"Created shift swap request {ssr.pk}") else: logger.info(f"Shift swap request already exists for user {user_id} schedule {schedule.pk}") else: logger.info( f"No current or upcoming shifts found for user {user_id} during the out of office event {event_id}" ) @shared_dedicated_queue_retry_task(autoretry_for=(Exception,), retry_backoff=True) def sync_out_of_office_calendar_events_for_all_users() -> None: # some existing tokens may not have all the required scopes, lets skip these tokens_containing_required_scopes = GoogleOAuth2User.objects.filter( *[Q(oauth_scope__contains=scope) for scope in constants.REQUIRED_OAUTH_SCOPES], user__organization__deleted_at__isnull=True, ) logger.info( f"Google OAuth2 tokens with the required scopes - " f"{tokens_containing_required_scopes.count()}/{GoogleOAuth2User.objects.count()}" ) for google_oauth2_user in tokens_containing_required_scopes: sync_out_of_office_calendar_events_for_user.apply_async(args=(google_oauth2_user.pk,))