McRUN Points System¶
About¶
The McRUN Points System is a Google Apps Script codebase designed to automate, calculate, and manage member points and event logs for the McGill Students Running Club. This system integrates with Google Sheets to record and tally participation, interfaces with Strava to extract running data, and automates communications and formatting via Gmail and Google Drive.
The project's purpose is to streamline the tracking of club events, automate member points calculation, and enhance the club's engagement and reporting using cloud tools and APIs.
Files¶
- Github Repo: points-system
- Google Sheets: Points Ledger - 2024/2025
- Apps Script Project: Points Ledger Code (McRUN) (Accessible via Extensions > Apps Script in the Google Sheet)
Key Features¶
- Automated points calculation and event logging for club activities.
- Strava API integration to auto-import running data.
- Dynamic email generation for reporting and member notifications.
- Custom formatting of event and points ledger sheets.
- Time-based and web triggers for scheduled automation.
- Script property storage for secure handling of credentials.
- Tools for extracting and formatting HTML email templates.
- Map image generation for event routes.
- Robust logging and error handling.
Tools Used¶
- Google Apps Script (Sheets, Drive, Gmail, Calendar)
- Strava API (OAuth2)
- Google Cloud APIs (Maps, Storage)
- Make.com automations
- HTML template processing for emails
Function Docs¶
This section is divided by project file (in alphabetical order). Each file includes a list of all its functions and a detailed entry for each.
Note: Functions named with a trailing underscore (
_) are internal but documented here with the underscore removed.
# Formatting.gs ¶
- sortTimestampByAscending()→ Sorts event log by timestamp ascending
- formatSpecificColumns()→ Formats columns in Head Run Attendance sheet
- toTitleCase(inputString)→ Converts a string to title case
## sortTimestampByAscending() ¶
Sorts the event log sheet by event timestamp (ascending, skipping headers).
Pitfalls: Assumes event timestamp is in column 3 and headers are at row 1.
## formatSpecificColumns() ¶
Applies formatting (bold, wrap, checkbox, font size) to columns in the "Head Run Attendance" sheet.
Pitfalls: Sheet/range must exist; formatting is hardcoded.
## toTitleCase(inputString) ¶
Converts a string to title case.
| Name | Type | Description | 
|---|---|---|
| inputString | String | String to format | 
Output: String (title-cased)
# HTML-Extraction.gs ¶
- extractTagsFromProjectFile()→ Extracts placeholder tags from HTML template
- extractPlaceholders()→ Extracts double-curly placeholders from email text
- createInlineImage(fileUrl, blobKey)→ Creates inline image blob
- saveDraftAsHtml()→ Saves a Gmail draft as an HTML file in Drive
## extractTagsFromProjectFile() ¶
Extracts all placeholder tags (e.g., <?= TAG ?>) from a named HTML file in the project.
Output: In Logger.
## extractPlaceholders() ¶
Extracts all {{placeholder}} tags from the email template text.
Output: Logs placeholder list
Pitfalls: Requires STATS_EMAIL_OBJ.text to be defined.
## createInlineImage(fileUrl, blobKey) ¶
Creates a blob for an image file from its Google Drive URL and assigns a content ID.
| Name | Type | Description | 
|---|---|---|
| fileUrl | String | Drive file URL | 
| blobKey | String | Blob name/content ID | 
Output: Blob (image for inline email attachment)
Pitfalls: File must exist and be accessible.
## saveDraftAsHtml() ¶
Saves the body of a Gmail draft (by subject line) as an HTML file in Drive.
Output: Creates file in Drive
Pitfalls: Draft with the specified subject must exist.
# Ledger-Code.gs ¶
- newSubmission()→ Formats and sorts a new event submission
- getLatestSubmissionTimestamp()→ Gets latest event timestamp (Date)
- getSubmissionTimestamp(row)→ Gets event timestamp for a row
- getValidLastRow(sheet)→ Gets last non-empty row in a sheet
- getLatestLog()→ Gets the latest log row data
- getLogInRow(row)→ Gets data for a specific row
- getAttendeesInLog(row)→ Gets attendees for a log row
## newSubmission() ¶
Formats the columns and sorts timestamps for a new event submission.
Output: None
## getLatestSubmissionTimestamp() ¶
Returns the latest event timestamp as a Date object.
Output: Date
## getSubmissionTimestamp(row) ¶
Returns the timestamp of a submission for a given row.
| Name | Type | Description | 
|---|---|---|
| row | Integer | Row in sheet | 
Output: Date
## getValidLastRow(sheet) ¶
Finds the last non-empty row in a sheet.
| Name | Type | Description | 
|---|---|---|
| sheet | Sheet | Sheet to check | 
Output: Integer (row index)
## getLatestLog() ¶
Returns the data (array) from the latest log row.
## getLogInRow(row) ¶
Returns log row data as array for a specific row.
| Name | Type | Description | 
|---|---|---|
| row | Integer | Row in sheet | 
Output: Array
## getAttendeesInLog(row) ¶
Gets the list of attendees for a given log row.
| Name | Type | Description | 
|---|---|---|
| row | Integer | Row in sheet | 
# Ledger-Variables.gs ¶
- getLedger()→ Returns cached ledger data
- getLogSheet()→ Returns Event Log sheet object
- getLedgerSheet()→ Returns Member Points sheet object
## getLedger() ¶
Returns or initializes the cached ledger data.
Output: Array/object
## getLogSheet() ¶
Returns the Event Log sheet object.
Output: Sheet
## getLedgerSheet() ¶
Returns the Member Points sheet object.
Output: Sheet
# Map-Generation.gs ¶
- createAndAppendMap(timestamp, activity)→ Generates and appends map URL to activity
- createStravaMap(activity, name)→ Creates PNG map image from Strava activity
- saveMapForRun(polyline, name)→ Saves map for run to storage
## createAndAppendMap(timestamp, activity) ¶
Creates and stores a PNG map from Strava activity, appending the URL to the activity.
| Name | Type | Description | 
|---|---|---|
| timestamp | Date | Event timestamp | 
| activity | Object | Strava activity data | 
Output: Object (activity with mapUrl property)
## createStravaMap(activity, name) ¶
Creates a PNG map image from a Strava activity and returns the blob.
| Name | Type | Description | 
|---|---|---|
| activity | Object | Strava activity | 
| name | String | Map file name | 
Output: Blob
## saveMapForRun(polyline, name) ¶
Saves a polyline as a map image using Google Maps API and automation.
| Name | Type | Description | 
|---|---|---|
| polyline | String | Encoded polyline | 
| name | String | Map file name | 
Output: API response
# Send-Email.gs ¶
- logStatus(messageArr, logSheet, thisRow)→ Logs email sending status
## logStatus(messageArr, logSheet, thisRow) ¶
Logs the email sending/update status for a given row.
| Name | Type | Description | 
|---|---|---|
| messageArr | String[] | Array of status messages | 
| logSheet | Sheet | Log sheet object (default: LOG_SHEET) | 
| thisRow | Integer | Row number (default: last row) | 
Output: None (status appended in sheet)
# Strava-Code.gs ¶
- findAndStoreStravaActivity(row)→ Finds and stores Strava activity for a log row
- prettyLog(msg)→ Multi-line log utility
## findAndStoreStravaActivity(row) ¶
Finds Strava activity for a row (from log or API), stores it, and returns the activity.
| Name | Type | Description | 
|---|---|---|
| row | Integer | Row to process (default: last log) | 
Output: Object (Strava activity)
Pitfalls: User must be logged in as club; Strava API may rate limit.
## prettyLog(...msg) ¶
Logs multiple lines for better readability.
| Name | Type | Description | 
|---|---|---|
| msg | String[] | Messages to log | 
# Strava-Service.gs ¶
- reset()→ Resets Strava OAuth2 authorization state
- safeReset()→ Conditionally resets OAuth2 state
- getStravaActivity(fromTimestamp, toTimestamp)→ Fetches Strava activities in a time range
- getStravaService()→ Configures and returns the OAuth2 service
## reset() ¶
Resets the Strava OAuth2 authorization state.
Output: None
## safeReset() ¶
Resets Strava OAuth2 authorization if allowed in script properties.
Output: None
## getStravaActivity(fromTimestamp, toTimestamp) ¶
Fetches Strava activities in the given time range.
| Name | Type | Description | 
|---|---|---|
| fromTimestamp | Integer | Start Unix time | 
| toTimestamp | Integer | End Unix time | 
Output: Array of Strava activities
## getStravaService() ¶
Configures and returns the OAuth2 service for Strava.
Output: OAuth2 service object
# Triggers.gs ¶
- doGet(e)→ Handles GET requests for Strava triggers
- createNewStravaTrigger(row)→ Creates a time-based Strava trigger for a row
- runStravaChecker()→ Checks for Strava activity and cleans up triggers
## doGet(e) ¶
Handles GET requests to set up Strava triggers via web endpoint.
| Name | Type | Description | 
|---|---|---|
| e | Object | Event object from web request | 
Output: ContentService TextOutput
Pitfalls: Requires correct API key in e.parameter.key.
## createNewStravaTrigger(row) ¶
Creates a new time-based trigger for checking Strava for a row.
| Name | Type | Description | 
|---|---|---|
| row | Integer | Row to associate with the trigger | 
Output: None (side effect: creates trigger and script property)
## runStravaChecker() ¶
Checks all active Strava triggers, verifies activities, and cleans up when found.
Output: None
Triggers¶
The project uses several types of triggers:
- Time-based triggers: Scheduled (e.g., every 30 minutes) for checking Strava activities, sending emails, and formatting sheets.
- Web app triggers: The doGet(e)function can be deployed as a web app endpoint for remote automation.
- Manual triggers: Functions like newSubmission()orsafeReset()may be called manually for setup or maintenance.
Purpose:
Triggers automate the periodic checking for new Strava runs, update points, send notifications, and keep the points ledger up to date without human intervention.
Troubleshooting & FAQ¶
| Issue/Error | Cause | Solution | 
|---|---|---|
| "Unauthorized! Please verify key." | Wrong API key | Set correct key in script properties and request | 
| "No permission" | Missing OAuth scopes | Ensure all required Apps Script scopes are granted | 
| "Cannot read property 'getRange' of null" | Missing or renamed sheet/range | Double-check all sheet names and constants | 
| "Rate limit exceeded" | Strava API throttling | Wait and retry, ensure efficient API use | 
| "OAuth error" | Strava authorization failed | Use reset()orsafeReset()to reauthorize | 
FAQ
- 
How do I update Strava credentials? 
 UpdateCLIENT_IDandCLIENT_SECRETin Apps Script > Project Properties.
- 
How do I add a new event type? 
 Update constants inLedger-Variables.gsand related logic in points calculation.
- 
How do I test a function? 
 Use the Apps Script IDE's “Run” feature; check the logs/output in Execution Log.
See Also¶
Last updated: 2025-06-12