Gathering Calendar Download Feature
Last Updated: November 4, 2025
Status: Active
Service: ICalendarService
Controller: GatheringsController::downloadCalendar()
Overview
The calendar download feature generates RFC 5545 compliant iCalendar (.ics) files that users can import into any calendar application. This allows attendees to easily add events to their personal calendars (Google Calendar, Outlook, Apple Calendar, Android, etc.).
Features
Universal Compatibility
- Standards Compliant: Follows RFC 5545 (iCalendar) specification
- All Major Applications: Works with Google Calendar, Outlook, Apple Calendar, Thunderbird, etc.
- Proper Formatting: Correct MIME type (
text/calendar) and file extension (.ics) - Character Support: UTF-8 encoding for international characters
- Smart Event Handling: Different formats for single-day vs. multi-day events
Event Information Included
Each generated .ics file contains:
- Basic Details: Event name and full description
- Dates and Times:
- Single-day events: 9 AM - 5 PM (customizable in calendar app after import)
- Multi-day events: All-day format for better calendar display
- Location: Venue address and GPS coordinates (if available)
- Event Metadata: Type (tournament, practice, etc.) and hosting branch
- Activities List: All attached activities with descriptions
- Staff Information: Event stewards with contact details
- Link Back: URL to the event page in KMP for more details
Access Control
- Route:
/gatherings/download-calendar/{publicId} - Requires: Gathering must have
public_page_enabled = trueor the user must be authenticated. - Returns: iCalendar file as attachment
- No authentication needed
Security:
- All text properly sanitized per iCalendar specification
- No sensitive data exposed beyond what’s visible on the page
- Public gatherings only downloadable when explicitly enabled
Implementation Details
ICalendarService
Location: src/Services/ICalendarService.php
Main Method:
public function generateICalendar(Gathering $gathering, string $eventUrl): string
Features:
- Text escaping per RFC 5545 (newlines, commas, semicolons)
- Line folding at 75 characters (per specification)
- Proper VEVENT structure with all required fields
- UID generation for calendar app tracking
- DTSTAMP with current timestamp
- GEO property for GPS coordinates
Event Formats:
Single-Day Events:
DTSTART:20251215T090000Z
DTEND:20251215T170000Z
Multi-Day Events:
DTSTART;VALUE=DATE:20251215
DTEND;VALUE=DATE:20251218
(Note: End date is exclusive in iCalendar, so day after last event day)
Filename Generation
Format: event-name-YYYY-MM-DD.ics
Example: winter-festival-2025-12-15.ics
Method:
public function getFilename(Gathering $gathering): string
Sanitizes event name by:
- Converting to lowercase
- Replacing spaces with hyphens
- Removing special characters
- Appending start date
Controller Integration
GatheringsController::downloadCalendar() Action:
- Parse route parameters (id or publicId)
- Load gathering with associations (Branch, GatheringType, Activities, Staff)
- Check authorization:
- For authenticated: User has view permission
- For public:
public_page_enabled = true
- Generate event URL for calendar link
- Call
ICalendarService::generateICalendar() - Return response with:
- Content-Type:
text/calendar; charset=UTF-8 - Content-Disposition:
attachment; filename="..." - Body: iCalendar content
- Content-Type:
Routes Configuration
File: config/routes.php
// Authenticated route
$routes->connect(
'/gatherings/:id/download-calendar',
['controller' => 'Gatherings', 'action' => 'downloadCalendar'],
['pass' => ['id'], 'id' => '\d+']
);
// Public route
$routes->connect(
'/gatherings/download-calendar/:publicId',
['controller' => 'Gatherings', 'action' => 'downloadCalendar'],
['pass' => ['publicId']]
);
UI Integration
1. Gathering View Page
Location: templates/Gatherings/view.php
Button:
<?= $this->Html->link(
'<i class="bi bi-calendar-plus"></i> ' . __('Add to Calendar'),
['action' => 'downloadCalendar', $gathering->id],
[
'class' => 'btn btn-outline-success btn-sm',
'escape' => false,
'title' => __('Download calendar file (.ics) for Outlook, Google Calendar, iOS, etc.')
]
) ?>
Position: First action button, before “Share Event”
2. Calendar Quick View Modal
Location: templates/Gatherings/quick_view.php
Button: Similar to view page, but opens in new tab/window via Turbo frame
Position: Before “Full Details” button in modal footer
3. Public Landing Page
Location: templates/element/gatherings/public_content.php
Button:
<?= $this->Html->link(
'<i class="bi bi-calendar-plus"></i> ' . __('Add to Calendar'),
[
'controller' => 'Gatherings',
'action' => 'downloadCalendar',
$gathering->public_id
],
[
'class' => 'btn btn-outline-light btn-lg mb-4',
'escape' => false
]
) ?>
Position: Hero section, prominently displayed below event metadata
Styling: Large button with light outline to match public page aesthetic
Browser Behavior
Desktop Browsers:
- Chrome/Edge/Firefox: Downloads file to default downloads folder
- User can then open file to import or double-click to add to default calendar app
iOS Safari:
- Presents option to add directly to Calendar app
- Or save to Files for later import
Android:
- Presents option to select calendar app for import
- Or save to device storage
All Platforms:
- Downloaded .ics files can be manually imported via calendar app’s import function
- Files can be shared via email or messaging
Testing
Unit Tests
Location: tests/TestCase/Services/ICalendarServiceTest.php
Test Coverage:
- ✅ Single-day event generation
- ✅ Multi-day event generation (3 days)
- ✅ Filename generation and sanitization
- ✅ Text escaping (commas, semicolons, newlines)
- ✅ RFC 5545 compliance
Run Tests:
cd app
vendor/bin/phpunit tests/TestCase/Services/ICalendarServiceTest.php
Expected Output: OK (4 tests, 14 assertions)
Manual Testing
Test Single-Day Event:
- Navigate to a single-day gathering view page
- Click “Add to Calendar”
- Verify .ics file downloads
- Open file in text editor - check DTSTART/DTEND have times
- Import into calendar app - verify shows as 9 AM - 5 PM event
Test Multi-Day Event:
- Navigate to a multi-day gathering (3+ days)
- Click “Add to Calendar”
- Open file in text editor - check VALUE=DATE format
- Import into calendar - verify shows as all-day, multi-day event
Test Public Access:
- Enable public page for a gathering
- Visit
/gatherings/public-landing/{publicId}(not logged in) - Click “Add to Calendar”
- Verify download works without authentication
Test Security:
- Try accessing
/gatherings/download-calendar/{publicId}for gathering withpublic_page_enabled = false - Should redirect or show error (not found)
Example iCalendar Output
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//KMP//Gathering Calendar//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VEVENT
UID:gathering-123@kmp.example.com
DTSTAMP:20251104T120000Z
DTSTART;VALUE=DATE:20251215
DTEND;VALUE=DATE:20251218
SUMMARY:Winter Festival 2025
DESCRIPTION:Event Type: Festival\nHosted by: Kingdom Branch\n\nJoin us for
our annual winter celebration...\n\nActivities:\n- Heavy Combat\n- Archer
y\n- Arts & Sciences\n\nEvent Steward(s):\n- Lord John Smith (john@examp
le.com)\n\nMore information: https://kmp.example.com/gatherings/view/123
LOCATION:Event Center\, 123 Main St\, City\, State 12345
GEO:40.7128;-74.0060
URL:https://kmp.example.com/gatherings/view/123
STATUS:CONFIRMED
ORGANIZER;CN=Kingdom Branch:noreply@kmp.example.com
CATEGORIES:Festival
END:VEVENT
END:VCALENDAR
Troubleshooting
Button Not Appearing
Check:
- User has view permission for the gathering
- Template file includes the button code
- Browser cache (hard refresh: Ctrl+F5 or Cmd+Shift+R)
Download Not Working
Check:
- Browser download settings/permissions
- Server error logs:
logs/error.log - Network tab in browser dev tools for failed requests
- Check that route is properly configured in
routes.php
Calendar App Won’t Import
Check:
- File has
.icsextension - File is valid iCalendar format (open in text editor)
- Calendar app supports iCalendar (all modern apps do)
- Try different calendar app to isolate issue
Missing Event Details
Check:
- Gathering has all required fields populated
- Associated entities (branch, type) are properly loaded
- Server logs for warnings during generation
- File content in text editor - verify all expected fields present
Public Download Not Working
Check:
public_page_enabledistruefor the gathering- Using correct public ID (not database ID)
- Route matches
/gatherings/download-calendar/{publicId}format
Future Enhancements
Potential Improvements:
- Custom Event Times: Allow organizers to specify exact start/end times instead of defaults
- Calendar Subscriptions: Generate subscription feeds (.ics URLs) for automatic updates
- Reminder/Alarm Settings: Include pre-event reminders in calendar file
- Recurring Events: Support for recurring event patterns
- Bulk Download: Download multiple gatherings as single calendar file
- Time Zone Support: Include time zone information for multi-region kingdoms
- Attendee List: Include other confirmed attendees (with privacy controls)
Quick Reference
For End Users:
- Click “Add to Calendar” button anywhere you see it
- Download .ics file
- Open in your calendar application (or tap on mobile)
- Event is added to your calendar
For Developers:
- Service:
ICalendarService - Tests:
ICalendarServiceTest.php - Routes: authenticated (
/gatherings/:id/download-calendar) and public (/gatherings/download-calendar/:publicId) - RFC: 5545 (iCalendar specification)
Supported Apps:
✅ Google Calendar
✅ Microsoft Outlook
✅ Apple Calendar
✅ Android Calendar
✅ Mozilla Thunderbird
✅ Any RFC 5545 compliant app