Skip to content
Open
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
181 changes: 137 additions & 44 deletions SMTplugins/Calendar/calendarWidget.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
# calendarWidget.py
# Displays a monthly calendar with Google Calendar event integration (optional)
# Falls back to a static calendar view if no credentials are provided
# SMTplugins/Calendar/calendarWidget.py

from widget import Widget
from datetime import datetime, date
from datetime import datetime, date, timedelta
import calendar
import json
import os
import time

# Google API Imports
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build

# Calendar Read-Only Scope
SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']

class calendarWidget(Widget):

def __init__(self):
self._preferences = self.widgetDefaultPreferences
self._events = {} # date_str -> list of event names
self.file = "calendar_events.json"
self._events = {}
self.load_local_events()

def load_local_events(self):
if os.path.exists(self.file):
with open(self.file, "r") as f:
self._events = json.load(f)

@property
def widgetName(self):
Expand All @@ -23,17 +39,15 @@ def widgetID(self):

@property
def widgetHTML(self):
"""Returns the HTML template name; Flask renders it via Jinja."""
return "calendar_widget.html"

@property
def widgetData(self):
"""Returns current calendar data as a JSON-serialisable dict."""
today = date.today()
year = today.year
month = today.month

cal = calendar.Calendar(firstweekday=6) # week starts Sunday
cal = calendar.Calendar(firstweekday=6)
weeks = cal.monthdatescalendar(year, month)

weeks_data = []
Expand All @@ -58,66 +72,145 @@ def widgetData(self):

@property
def widgetPreferences(self):
return self._preferences

@widgetPreferences.setter
def widgetPreferences(self, value):
self._preferences = value
return {}


@property
def widgetDefaultPreferences(self):
return {
"show_week_numbers": False,
"use_google_cal": False, # Set True + provide creds to enable
"use_google_cal": True, # Enabled by default for sync
"google_cal_id": "primary"
}

@property
def updateTimer(self):
# Refresh every 10 minutes
return 600_000
def get_service(self):
"""Authenticated Google Calendar service helper."""
creds = None
# Paths consistent with SMT structure
token_path = os.path.join("SMTplugins", "Calendar", "token_cal.json")
creds_path = os.path.join("SMTplugins", "Calendar", "credentials.json")

if os.path.exists(token_path):
creds = Credentials.from_authorized_user_file(token_path, SCOPES)

if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(creds_path, SCOPES)
creds = flow.run_local_server(port=0)
with open(token_path, 'w') as token:
token.write(creds.to_json())

return build('calendar', 'v3', credentials=creds)

def update(self):
"""Called by the widget subsystem on a timer. Fetches events if enabled."""
"""Called by the subsystem to refresh data."""
if self._preferences.get("use_google_cal"):
self._fetch_google_events()
else:
# Placeholder: inject a couple of demo events so the UI isn't empty
today = date.today().isoformat()
self._events = {
today: ["Class 4PM – LC 22", "Gym 5PM"]
}
print(f"Calendar Widget updated – {date.today().strftime('%B %Y')}")
if not self._events:
# Default demo data
today = date.today().isoformat()
self._events[today] = [{"id": "demo1", "title": "Class 4PM – LC 22"}]

def _fetch_google_events(self):
"""
Stub for Google Calendar API integration.
To enable: install google-auth + google-api-python-client,
create OAuth credentials, and fill in the logic below.
"""
"""Fetches events for the current month and syncs to self._events."""
try:
# TODO: implement OAuth flow and fetch events for current month
# from googleapiclient.discovery import build
# service = build("calendar", "v3", credentials=creds)
# events_result = service.events().list(...).execute()
raise NotImplementedError("Google Calendar auth not yet configured.")
service = self.get_service()
now = datetime.utcnow()
# Range: Start of current month to end of month
start_date = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0).isoformat() + 'Z'

events_result = service.events().list(
calendarId=self._preferences.get("google_cal_id", "primary"),
timeMin=start_date,
maxResults=100,
singleEvents=True,
orderBy='startTime'
).execute()

google_events = events_result.get('items', [])

# Temporary dict to avoid mixing old local events with fresh Google data
new_events_map = {}

for event in google_events:
start = event['start'].get('dateTime', event['start'].get('date'))
# Extract just the YYYY-MM-DD part
date_key = start[:10]

if date_key not in new_events_map:
new_events_map[date_key] = []

new_events_map[date_key].append({
"id": event['id'],
"title": event.get('summary', '(No Title)'),
"google_event": True
})

self._events = new_events_map
# Persist to disk
with open(self.file, "w") as f:
json.dump(self._events, f)

except Exception as e:
print(f"[calendarWidget] Google Calendar fetch failed: {e}")
print(f"[calendarWidget] Google Calendar sync failed: {e}")

def updateTimer(self):
return 60000 # Refresh every 1 minute to stay within API limits

def handle_event(self, event, args):
"""
Supported events:
- "prev_month" : (future) navigate to previous month
- "next_month" : (future) navigate to next month
- "add_event" : args = {"date": "YYYY-MM-DD", "title": "..."}
"""

# -------- ADD TASK --------
if event == "add_event":
date_str = args.get("date")
title = args.get("title", "Event")

if date_str:
if date_str not in self._events:
self._events[date_str] = []
self._events[date_str].append(title)
print(f"[calendarWidget] Added event '{title}' on {date_str}")

self._events[date_str].append({
"id": int(time.time()*1000),
"title": title
})

self._save_events()

# -------- NEXT MONTH --------
elif event == "next_month":
print("NEXT MONTH TRIGGERED")

if self.current_date.month == 12:
self.current_date = self.current_date.replace(
year=self.current_date.year + 1,
month=1
)
else:
self.current_date = self.current_date.replace(
month=self.current_date.month + 1
)

self._save_state()
print("NEW MONTH:", self.current_date)

# -------- PREVIOUS MONTH --------
elif event == "prev_month":
print("PREV MONTH TRIGGERED")

if self.current_date.month == 1:
self.current_date = self.current_date.replace(
year=self.current_date.year - 1,
month=12
)
else:
self.current_date = self.current_date.replace(
month=self.current_date.month - 1
)

self._save_state()
print("NEW MONTH:", self.current_date)

else:
print(f"[calendarWidget] Unhandled event: {event} args={args}")
1 change: 1 addition & 0 deletions SMTplugins/Calendar/credentials.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"installed":{"client_id":"772172144197-ldo19j0f7s5m7s55jd6ndkadoj4c4ehd.apps.googleusercontent.com","project_id":"smt-tasks","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_secret":"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","redirect_uris":["http://localhost"]}}
1 change: 1 addition & 0 deletions SMTplugins/Calendar/token_cal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"token": "ya29.a0AQvPyINm3-Umt-poLbkfSpf3sLXjFOYawOQnHssKmcD6iWd6qI8Vxog2HqhWPjDc4-Gwib_qHJB0JgbnWlVCZeoMCrkxipNogHWpkAVsD4rjQxllKDdjlO7lcHQiFEODBc05EtXZHSdpdkqp3ah9cWzQeOHi2QksUPudqv8QNapkJpvElGqKDljOyvnuJMENZ8bTXnEaCgYKAaMSARQSFQHGX2Mi1bah18Yfs0QmPf9s0gUP6A0206", "refresh_token": "1//05NlvPY06LAVrCgYIARAAGAUSNwF-L9IrBh5LQKCmxDpM-lbVWh3B_Grots1N2s4FdVmAsdTZQ3AeBEwIUmtCI3uZRvfDXKTb4V4", "token_uri": "https://oauth2.googleapis.com/token", "client_id": "772172144197-ldo19j0f7s5m7s55jd6ndkadoj4c4ehd.apps.googleusercontent.com", "client_secret": "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", "scopes": ["https://www.googleapis.com/auth/calendar.readonly"], "universe_domain": "googleapis.com", "account": "", "expiry": "2026-04-30T16:24:43Z"}
1 change: 1 addition & 0 deletions calendar_events.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"2026-05-24": [{"id": "8ifd00fvn8hqjf73o7o24he5os", "title": "Photography Session (Easha Mashud)", "google_event": true}]}
2 changes: 1 addition & 1 deletion flaskServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

#allows for easier starting of flask from start file
def run_flask():
app.run(debug=True, port=5000, use_reloader=False)
app.run(debug=True, port=8000, use_reloader=False)



Expand Down
8 changes: 4 additions & 4 deletions layout_client.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@
"col": 3
},
{
"id": "bj-container",
"name": "Blackjack",
"class": "blackjack-widget",
"css_name": "blackjack_widget.css",
"id": "calendar",
"name": "Calendar",
"class": "calendar-widget",
"css_name": "calendar_widget.css",
"row": 2,
"col": 2
},
Expand Down
Loading