Skip to content

Commit

Permalink
Additional fixes for edge cases and for correct timezones
Browse files Browse the repository at this point in the history
  • Loading branch information
sylviamclaughlin authored Apr 26, 2024
1 parent 0e1cbd0 commit f98f1be
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 58 deletions.
27 changes: 11 additions & 16 deletions app/integrations/google_workspace/google_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def get_freebusy(time_min, time_max, items, **kwargs):
"items": items,
}
body.update({convert_string_to_camel_case(k): v for k, v in kwargs.items()})

return execute_google_api_call(
"calendar",
"v3",
Expand Down Expand Up @@ -102,33 +103,26 @@ def find_first_available_slot(
busy_times = []
for calendar in freebusy_response["calendars"].values():
for busy_period in calendar["busy"]:
start = (
datetime.strptime(busy_period["start"], "%Y-%m-%dT%H:%M:%SZ")
.replace(tzinfo=pytz.UTC)
.astimezone(est)
)
end = (
datetime.strptime(busy_period["end"], "%Y-%m-%dT%H:%M:%SZ")
.replace(tzinfo=pytz.UTC)
.astimezone(est)
)
# convert from iso 8601 standard to datetime
start = datetime.fromisoformat(busy_period["start"][:-1])
end = datetime.fromisoformat(busy_period["end"][:-1])
busy_times.append((start, end))
busy_times.sort(key=lambda x: x[0])

for day_offset in range(days_in_future, days_in_future + search_days_limit):
# Calculate the start and end times of the search window for the current day
search_date = datetime.now(tz=est) + timedelta(days=day_offset)
search_date = datetime.utcnow() + timedelta(days=day_offset)

# Check if the day is Saturday (5) or Sunday (6) and skip it
if search_date.weekday() in [5, 6]:
continue

search_start = search_date.replace(
hour=13, minute=0, second=0, microsecond=0
) # 1 PM EST
hour=17, minute=0, second=0, microsecond=0
) # 1 PM EST, times are in UTC
search_end = search_date.replace(
hour=15, minute=0, second=0, microsecond=0
) # 3 PM EST
hour=19, minute=0, second=0, microsecond=0
) # 3 PM EST, times are in UTC

# Attempt to find an available slot within this day's search window
for current_time in (
Expand All @@ -139,6 +133,7 @@ def find_first_available_slot(
slot_end <= start or current_time >= end for start, end in busy_times
):
if slot_end <= search_end:
return current_time, slot_end
# return the time and convert them to EST timezone
return current_time.astimezone(est), slot_end.astimezone(est)

return None, None # No available slot found after searching the limit
104 changes: 62 additions & 42 deletions app/tests/integrations/google_workspace/test_google_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import json
from unittest.mock import patch, MagicMock
from datetime import datetime
from datetime import datetime, timedelta
import pytest
import pytz
from integrations.google_workspace import google_calendar
Expand All @@ -18,7 +18,6 @@ def event_details():
}
)


# Fixture to mock the calendar service object
@pytest.fixture
def calendar_service_mock():
Expand All @@ -38,6 +37,10 @@ def calendar_service_mock():
def est_timezone():
return pytz.timezone("US/Eastern")

@pytest.fixture
def fixed_utc_now():
# Return a fixed UTC datetime
return datetime(2023, 4, 10, 12, 0) # This is a Monday

# Fixture to mock the datetime.now() function
@pytest.fixture
Expand Down Expand Up @@ -247,73 +250,84 @@ def test_insert_event_api_call_error(
assert not mock_handle_errors.called


# Test out the find_first_available_slot function on the first available weekday
def test_available_slot_on_first_weekday(mock_datetime_now, est_timezone):
@patch('integrations.google_workspace.google_calendar.datetime')
def test_available_slot_on_first_weekday(mock_datetime, fixed_utc_now, est_timezone):
# Mock datetime to control the flow of time in the test
mock_datetime.utcnow.return_value = fixed_utc_now
mock_datetime.fromisoformat.side_effect = lambda d: datetime.fromisoformat(d[:-1])
mock_datetime.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs)

# Simulate a freebusy response with no busy times on the first available day
freebusy_response = {
"calendars": {
"primary": {
"busy": [
# Assuming the provided busy time is on April 10, 2023, from 1 PM to 1:30 PM UTC (9 AM to 9:30 AM EST)
{
"start": "2023-04-10T17:00:00Z",
"end": "2023-04-10T17:30:00Z",
} # Busy at 1 PM to 1:30 PM EST on April 10
"start": "2023-04-10T17:00:000Z",
"end": "2023-04-10T17:30:000Z",
}
]
}
}
}

start, end = google_calendar.find_first_available_slot(
freebusy_response, duration_minutes=30, days_in_future=3, search_days_limit=60
# Expected search date is three days in the future (which should be Thursday)
# Busy period is from 1 PM to 1:30 PM EST on the first day being checked (April 13th)
# The function should find an available slot after the busy period
expected_start_time = fixed_utc_now.replace(
day=fixed_utc_now.day + 3, hour=17, minute=0, second=0, microsecond=0
).astimezone(est_timezone)
expected_end_time = expected_start_time + timedelta(minutes=30)

# Run the function under test
actual_start, actual_end = google_calendar.find_first_available_slot(
freebusy_response, days_in_future=3, duration_minutes=30, search_days_limit=60
)

# Since April 10 is the "current" day and we start searching from 3 days in the future (April 13),
# we expect the function to find the first available slot on April 13 between 1 PM and 3 PM EST.
expected_start = datetime(
2023, 4, 13, 13, 0, tzinfo=est_timezone
) # Expected start time in EST
expected_end = datetime(
2023, 4, 13, 13, 30, tzinfo=est_timezone
) # Expected end time in EST

assert (
start == expected_start and end == expected_end
), "The function should find the first available slot on April 13, 2023, from 1 PM to 1:30 PM EST."

# Check if the times returned match the expected values
assert actual_start == expected_start_time
assert actual_end == expected_end_time


# Test out the find_first_available_slot function when multiple busy days
def test_opening_exists_after_busy_days(mock_datetime_now, est_timezone):
@patch('integrations.google_workspace.google_calendar.datetime')
def test_opening_exists_after_busy_days(mock_datetime, fixed_utc_now, est_timezone):
# Mock datetime to control the flow of time in the test
mock_datetime.utcnow.return_value = fixed_utc_now
mock_datetime.fromisoformat.side_effect = lambda d: datetime.fromisoformat(d[:-1])
mock_datetime.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs)
freebusy_response = {
"calendars": {
"primary": {
"busy": [
{"start": "2023-04-13T17:00:00Z", "end": "2023-04-13T19:00:00Z"},
{"start": "2023-04-14T17:00:00Z", "end": "2023-04-14T19:00:00Z"},
{"start": "2023-04-17T17:00:00Z", "end": "2023-04-17T19:00:00Z"},
{"start": "2023-04-18T17:00:00Z", "end": "2023-04-18T19:00:00Z"},
{"start": "2023-04-13T17:00:000Z", "end": "2023-04-13T19:00:000Z"},
{"start": "2023-04-14T17:00:000Z", "end": "2023-04-14T19:00:000Z"},
{"start": "2023-04-17T17:00:000Z", "end": "2023-04-17T19:00:000Z"},
{"start": "2023-04-18T17:00:000Z", "end": "2023-04-18T19:00:000Z"},
]
}
}
}

start, end = google_calendar.find_first_available_slot(
freebusy_response, duration_minutes=30, days_in_future=3, search_days_limit=60
freebusy_response, days_in_future=3, duration_minutes=30, search_days_limit=60
)

expected_start = datetime(
2023, 4, 13, 14, 30, tzinfo=est_timezone
) # Expected start time in EST
expected_end = datetime(
2023, 4, 13, 15, 0, tzinfo=est_timezone
) # Expected end time in EST

expected_start = fixed_utc_now.replace(
day=fixed_utc_now.day + 9, hour=17, minute=0, second=0, microsecond=0
).astimezone(est_timezone)
expected_end = expected_start + timedelta(minutes=30)

assert (
start == expected_start and end == expected_end
), "Expected to find an available slot correctly."


# Test that weekends are skipped when searching for available slots
def test_skipping_weekends(mock_datetime_now, est_timezone):
@patch('integrations.google_workspace.google_calendar.datetime')
def test_skipping_weekends(mock_datetime, fixed_utc_now, est_timezone):
mock_datetime.utcnow.return_value = fixed_utc_now
mock_datetime.fromisoformat.side_effect = lambda d: datetime.fromisoformat(d[:-1])
mock_datetime.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs)
freebusy_response = {
"calendars": {
"primary": {
Expand All @@ -330,23 +344,29 @@ def test_skipping_weekends(mock_datetime_now, est_timezone):
)

# Adjust these expected values based on the specific 'now' being mocked
expected_start = datetime(2023, 4, 11, 13, 0, tzinfo=est_timezone)
expected_end = datetime(2023, 4, 11, 13, 30, tzinfo=est_timezone)
expected_start = fixed_utc_now.replace(
day=fixed_utc_now.day + 1, hour=17, minute=0, second=0, microsecond=0
).astimezone(est_timezone)
expected_end = expected_start + timedelta(minutes=30)

assert (
start == expected_start and end == expected_end
), "Expected to find an available slot after skipping the weekend"


# Test that no available slots are found within the search limit
def test_no_available_slots_within_search_limit(mock_datetime_now):
@patch('integrations.google_workspace.google_calendar.datetime')
def test_no_available_slots_within_search_limit(mock_datetime, fixed_utc_now, est_timezone):
mock_datetime.utcnow.return_value = fixed_utc_now
mock_datetime.fromisoformat.side_effect = lambda d: datetime.fromisoformat(d[:-1])
mock_datetime.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs)
freebusy_response = {
"calendars": {
"primary": {
"busy": [
# Simulate a scenario where every eligible day within the search window is fully booked
# For simplicity, let's assume a pattern that covers the search hours for the next 60 days
{"start": "2023-04-10T17:00:00Z", "end": "2023-08-13T19:00:00Z"},
{"start": "2023-04-10T17:00:000Z", "end": "2023-08-13T19:00:000Z"},
]
}
}
Expand Down

0 comments on commit f98f1be

Please sign in to comment.