Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,6 @@ dist
pyrightconfig.json

opt/

# google api
server/api/booking/google_calendar/google_calendar_service.json
8 changes: 6 additions & 2 deletions server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ DJANGO_SUPERUSER_PASSWORD=Password123
DJANGO_SUPERUSER_EMAIL=admin@test.com
DJANGO_SUPERUSER_USERNAME=admin

AWS_ACCESS_KEY_ID=xxxx
AWS_SECRET_ACCESS_KEY=xxxx
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_STORAGE_BUCKET_NAME=bucket_name
AWS_REGION_NAME=ap-southeast-2

GOOGLE_CREDENTIALS_FILE=api/booking/google_calendar/google_calendar_service.json
GOOGLE_CALENDAR_ID=


Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary blank line added at the end of the Google API section.

Suggested change

Copilot uses AI. Check for mistakes.
FRONTEND_URL="http://localhost:3000"
7 changes: 6 additions & 1 deletion server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ A template Django server.
1. Install dependencies: `poetry install`
2. Run: `python manage.py runserver` or `.\dev.sh`

Note this file needs to be here otherwise poetry won't recognise this as a valid project.
Note this README file needs to be here otherwise poetry won't recognise this as a valid project.

# Python version

- 3.12

# Google calendar integration

- Place your Google service account JSON key file in `server/api/booking/google_calendar/google_calendar_service.json`
- An example JSON key file is `server/api/booking/google_calendar/google_calendar_service.example.json`
Empty file added server/api/booking/__init__.py
Empty file.
Empty file.
27 changes: 27 additions & 0 deletions server/api/booking/google_calendar/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import os
from pathlib import Path
from google.oauth2 import service_account
from googleapiclient.discovery import build
from dotenv import load_dotenv


# Resolve BASE_DIR and load .env
# adjust to server root
BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent
load_dotenv(os.path.join(BASE_DIR, ".env"))


def get_calendar_service():

cred_path = os.getenv("GOOGLE_CREDENTIALS_FILE")
if not cred_path:
raise ValueError(
"GOOGLE_CREDENTIALS_FILE is missing or invalid. "
f"Checked: {os.path.join(BASE_DIR, '.env')}"
)

creds = service_account.Credentials.from_service_account_file(
cred_path,
scopes=["https://www.googleapis.com/auth/calendar"]
)
return build("calendar", "v3", credentials=creds)
88 changes: 88 additions & 0 deletions server/api/booking/google_calendar/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import os
from .client import get_calendar_service


def requires_calendar_id(func):
def wrapper(*args, **kwargs):
CALENDAR_ID = os.getenv("GOOGLE_CALENDAR_ID")
if not CALENDAR_ID:
raise ValueError(
"GOOGLE_CALENDAR_ID is missing. Set it in your environment."
)
return func(CALENDAR_ID, *args, **kwargs)
return wrapper


@requires_calendar_id
def create_event(CALENDAR_ID, event_data: dict):
"""
Create a new Google Calendar event.

**Expected event_data keys (Google Calendar API format):**
- summary (str): Title of the event.
- description (str, optional): Details about the event.
- location (str, optional): Physical or virtual location.
- start (dict): Start date/time.
Example:
{
"dateTime": "2025-02-01T10:00:00+08:00",
"timeZone": "Australia/Perth"
}
- end (dict): End date/time.
Example:
{
"dateTime": "2025-02-01T11:00:00+08:00",
"timeZone": "Australia/Perth"
}
- 'recurrence': ['RRULE:FREQ=WEEKLY;COUNT=10'].
Returns:
dict: The created event object from Google Calendar.
"""
service = get_calendar_service()
return service.events().insert(calendarId=CALENDAR_ID, body=event_data).execute()


@requires_calendar_id
def get_event(CALENDAR_ID, event_id: str):
"""
Retrieve a specific event.

Args:
event_id (str): Google Calendar event ID.

Returns:
dict: Event details.
"""
service = get_calendar_service()
return service.events().get(calendarId=CALENDAR_ID, eventId=event_id).execute()


@requires_calendar_id
def update_event(CALENDAR_ID, event_id: str, updated_data: dict):
"""
Update an existing event.

Args:
event_id (str): Google Calendar event ID.
updated_data (dict): Same structure as event_data.

Returns:
dict: Updated event object.
"""
service = get_calendar_service()
return service.events().update(calendarId=CALENDAR_ID, eventId=event_id, body=updated_data).execute()


@requires_calendar_id
def delete_event(CALENDAR_ID, event_id: str):
"""
Delete an event.

Args:
event_id (str): Google Calendar event ID.

Returns:
dict: Response from Google Calendar API (usually empty).
"""
service = get_calendar_service()
return service.events().delete(calendarId=CALENDAR_ID, eventId=event_id).execute()
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "xxx",
"private_key_id": "xxx",
"private_key": "-----BEGIN PRIVATE KEY-----\nxxx\n-----END PRIVATE KEY-----\n",
"client_email": "xxx@xxx.iam.gserviceaccount.com",
"client_id": "xxx",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "xxx",
"universe_domain": "googleapis.com"
}
45 changes: 45 additions & 0 deletions server/api/booking/google_calendar/test_google_calendar.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test fails when you don't set the environment variables so need to fix it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import pytest
import os
from api.booking.google_calendar.events import (
create_event,
get_event,
update_event,
delete_event,
)
# skip if no Google Calendar env variables are set
pytestmark = pytest.mark.skipif(
not os.getenv("GOOGLE_CREDENTIALS_FILE") or not os.getenv(
"GOOGLE_CALENDAR_ID"),
reason="Google Calendar integration env vars missing. Set GOOGLE_CREDENTIALS_FILE and GOOGLE_CALENDAR_ID to run this test."
)


def test_google_calendar_crud():

event_data = {
"summary": "Test Event",
"description": "CRUD test event",
"start": {
"dateTime": "2025-12-02T15:00:00",
"timeZone": "Australia/Perth",
},
"end": {
"dateTime": "2025-12-02T16:00:00",
"timeZone": "Australia/Perth",
},
'recurrence': ['RRULE:FREQ=WEEKLY;COUNT=10'],
}

created = create_event(event_data)
event_id = created["id"]

fetched = get_event(event_id)
assert fetched["summary"] == "Test Event"

updated_data = dict(event_data)
updated_data["summary"] = "Updated Test Event"

updated = update_event(event_id, updated_data)
assert updated["summary"] == "Updated Test Event"

delete_event(event_id)
Loading