Skip to content

Commit a8ca912

Browse files
import to google calendar
1 parent 1a5bddc commit a8ca912

5 files changed

Lines changed: 101 additions & 5 deletions

File tree

my_studysmart.xlsx

57 Bytes
Binary file not shown.

src/study_smart/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
build_schedule,
33
generate_tips
44
)
5-
from .google_calendar import import_commitments_from_google_calendar
5+
from .google_calendar import (import_commitments_from_google_calendar, export_schedule_to_google_calendar)

src/study_smart/app.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
dbc.Row([
4444
dbc.Col(dbc.Button("💾 Save", id="save-button", color="secondary", size="sm")),
4545
dbc.Col(dbc.Button("📂 Load", id="load-button", color="secondary", size="sm")),
46-
dbc.Col(html.Div(id="save-status"), width="auto"),
46+
dbc.Col(html.Div(id="save-status"), width="auto"),
4747
], className="mb-3"),
4848
html.Div(id="exam-table")
4949
], width=6)
@@ -87,6 +87,8 @@
8787
className="mt-2"
8888
),
8989
dbc.Button("Generate / Regenerate schedule", id="generate-button", color="success", className="mt-3 mb-3"),
90+
dbc.Button("📅 Export to Google Calendar", id="export-calendar-button", color="info", className="mt-3 mb-3"),
91+
html.Div(id="export-calendar-status"),
9092
html.Div(id="warnings-div"),
9193
dcc.Graph(id="schedule-chart"),
9294
html.Div(id="legend-div"),
@@ -544,8 +546,31 @@ def import_from_google_calendar(n_clicks, start_date):
544546

545547
except Exception as e:
546548
return dbc.Alert(f"Could not import: {str(e)}", color="danger"), "📅 Import from Google Calendar"
549+
550+
# Callback — export to Google Calendar
551+
@app.callback(
552+
Output("export-calendar-status", "children"),
553+
Output("export-calendar-button", "children"),
554+
Input("export-calendar-button", "n_clicks"),
555+
State("schedule-store", "data"),
556+
prevent_initial_call=True
557+
)
558+
def export_to_google_calendar(n_clicks, stored_schedule):
559+
try:
560+
from study_smart.google_calendar import export_schedule_to_google_calendar
561+
562+
if not stored_schedule:
563+
return dbc.Alert("Please generate a schedule first!", color="warning"), "📅 Export to Google Calendar"
564+
565+
schedule = pd.DataFrame(stored_schedule)
566+
schedule["date"] = pd.to_datetime(schedule["date"]).dt.date
547567

568+
count = export_schedule_to_google_calendar(schedule)
569+
return dbc.Alert(f"✅ {count} events added to Google Calendar!", color="success"), "📅 Export to Google Calendar"
548570

571+
except Exception as e:
572+
return dbc.Alert(f"Could not export: {str(e)}", color="danger"), "📅 Export to Google Calendar"
573+
549574
# Callback — navigate weeks
550575
@app.callback(
551576
Output("current-week-store", "data"),

src/study_smart/google_calendar.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Google Calendar integration for StudySmart.
33
4-
Provides functions to import calendar events as study commitments
4+
Provides functions to import and export calendar events as study commitments
55
using the Google Calendar API with OAuth 2.0 authentication.
66
77
Note: Requires credentials.json in the repository root. See README
@@ -15,7 +15,7 @@
1515
from google_auth_oauthlib.flow import InstalledAppFlow
1616
from googleapiclient.discovery import build
1717

18-
SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"]
18+
SCOPES = ["https://www.googleapis.com/auth/calendar"]
1919

2020
# paths to credentials files — relative to repo root
2121
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -115,3 +115,45 @@ def import_commitments_from_google_calendar(start_date, end_date):
115115
return (commitments, event_name)
116116

117117

118+
def export_schedule_to_google_calendar(schedule):
119+
"""
120+
Export study schedule as events to Google Calendar.
121+
122+
Creates one all-day event per study session in the user's primary calendar.
123+
124+
Args:
125+
schedule (pd.DataFrame): Schedule with columns 'date', 'subject', 'hours', 'type'.
126+
127+
Returns:
128+
int: Number of events created.
129+
130+
Example:
131+
>>> from datetime import date
132+
>>> import pandas as pd
133+
>>> schedule = pd.DataFrame({
134+
... "date": [date(2026, 6, 1)],
135+
... "subject": ["Statistics"],
136+
... "hours": [3.0],
137+
... "type": ["initial"]
138+
... })
139+
>>> count = export_schedule_to_google_calendar(schedule)
140+
"""
141+
service = _get_calendar_service()
142+
count = 0
143+
144+
for _, row in schedule.iterrows():
145+
label = f"📚 {row['subject']}{row['hours']}hrs"
146+
if row["type"] == "review":
147+
label += " (review)"
148+
149+
event = {
150+
"summary": label,
151+
"start": {"date": str(row["date"])},
152+
"end": {"date": str(row["date"])},
153+
"description": f"StudySmart study session — {row['type']}"
154+
}
155+
service.events().insert(calendarId="primary", body=event).execute()
156+
count += 1
157+
158+
return count
159+

tests/test_google_calendar.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
- Multiple events on the same day have their hours summed
88
- Events without a summary fall back to 'Unnamed event'
99
10+
- export_schedule_to_google_calendar: event export logic
11+
12+
1013
Note: _get_calendar_service is not tested as it requires live OAuth credentials.
1114
All tests mock the calendar service to avoid network calls.
1215
"""
@@ -75,4 +78,30 @@ def test_import_unnamed_event_fallback():
7578
start_date=date(2026, 5, 13),
7679
end_date=date(2026, 5, 14)
7780
)
78-
assert event_names[date(2026, 5, 13)] == ["Unnamed event"]
81+
assert event_names[date(2026, 5, 13)] == ["Unnamed event"]
82+
83+
def test_export_to_google_calendar():
84+
"""Test that export creates one event per row in schedule."""
85+
import pandas as pd
86+
from datetime import date
87+
from study_smart.google_calendar import export_schedule_to_google_calendar
88+
89+
with patch("study_smart.google_calendar._get_calendar_service") as mock_service:
90+
mock_insert = mock_service.return_value.events.return_value.insert
91+
mock_insert.return_value.execute.return_value = {}
92+
93+
schedule = pd.DataFrame({
94+
"date": [date(2026, 6, 1), date(2026, 6, 2)],
95+
"subject": ["Stats", "Stats"],
96+
"hours": [3.0, 2.0],
97+
"type": ["initial", "review"]
98+
})
99+
100+
count = export_schedule_to_google_calendar(schedule)
101+
assert count == 2
102+
assert mock_insert.call_count == 2
103+
# check first event label
104+
event_body = mock_insert.call_args_list[0][1]["body"]
105+
assert event_body["summary"] == "📚 Stats — 3.0hrs"
106+
107+

0 commit comments

Comments
 (0)