From d9f1e59f7f0137109f3155bf9ae7ca782e2566b3 Mon Sep 17 00:00:00 2001 From: edalzell Date: Mon, 31 Mar 2025 17:16:50 -0700 Subject: [PATCH 1/8] wip --- src/Actions/GenerateICalendar.php | 21 +++++++++++++++++++++ src/Data/ICalEvent.php | 18 ++++++++++++++++++ src/Types/Event.php | 13 +++++-------- src/Types/MultiDayEvent.php | 7 +++++-- 4 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 src/Actions/GenerateICalendar.php create mode 100644 src/Data/ICalEvent.php diff --git a/src/Actions/GenerateICalendar.php b/src/Actions/GenerateICalendar.php new file mode 100644 index 0000000..1319159 --- /dev/null +++ b/src/Actions/GenerateICalendar.php @@ -0,0 +1,21 @@ +title) + ->withoutTimezone() + ->uniqueIdentifier($event->id) + ->startsAt($event->start) + ->endsAt($event->end) + ->address($event->address) + ->description($event->description) + ->url($event->url); + } +} diff --git a/src/Data/ICalEvent.php b/src/Data/ICalEvent.php new file mode 100644 index 0000000..e9e7d52 --- /dev/null +++ b/src/Data/ICalEvent.php @@ -0,0 +1,18 @@ +toCarbonImmutable($date); - $iCalEvent = ICalendarEvent::create($this->event->title) + return ICalendarEvent::create($this->event->title) ->withoutTimezone() ->uniqueIdentifier($this->event->id()) ->startsAt($immutableDate->setTimeFromTimeString($this->startTime())) - ->endsAt($immutableDate->setTimeFromTimeString($this->endTime())); - - if ($location = $this->location($this->event)) { - $iCalEvent->address($location); - } - - return $iCalEvent; + ->endsAt($immutableDate->setTimeFromTimeString($this->endTime())) + ->address($this->location($this->event)) + ->description($this->event->description) + ->url($this->event->link); } /** diff --git a/src/Types/MultiDayEvent.php b/src/Types/MultiDayEvent.php index 876ee8a..9dcef74 100644 --- a/src/Types/MultiDayEvent.php +++ b/src/Types/MultiDayEvent.php @@ -62,7 +62,10 @@ public function toICalendarEvent(string|CarbonInterface $date): ?ICalendarEvent return ICalendarEvent::create($this->event->title) ->uniqueIdentifier($this->event->id()) ->startsAt($immutableDate->setTimeFromTimeString($day->start())) - ->endsAt($immutableDate->setTimeFromTimeString($day->end())); + ->endsAt($immutableDate->setTimeFromTimeString($day->end())) + ->address($this->location($this->event)) + ->description($this->event->description) + ->url($this->event->link); } /** @@ -87,7 +90,7 @@ protected function rule(bool $collapseDays = false): RRuleInterface } return tap( - new RSet(), + new RSet, fn (RSet $rset) => $this->days->each(fn (Day $day) => $rset->addRRule([ 'count' => 1, 'dtstart' => $day->end()->subSecond(), From f54637ed95115018ec0c68e111e544a7a9fd8c92 Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 23 Apr 2025 14:14:28 -0700 Subject: [PATCH 2/8] tidy --- src/Types/RecurringEvent.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Types/RecurringEvent.php b/src/Types/RecurringEvent.php index 6af7fe8..f771726 100644 --- a/src/Types/RecurringEvent.php +++ b/src/Types/RecurringEvent.php @@ -27,8 +27,6 @@ public function interval(): int */ public function toICalendarEvents(): array { - $timezone = $this->timezone['timezone']; - return [ ICalendarEvent::create($this->event->title) ->uniqueIdentifier($this->event->id()) From 68a199bf57434d36380f6bb5fa15f0f706100362 Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 23 Apr 2025 14:22:20 -0700 Subject: [PATCH 3/8] not gonna do it this way --- src/Actions/GenerateICalendar.php | 21 --------------------- src/Data/ICalEvent.php | 18 ------------------ 2 files changed, 39 deletions(-) delete mode 100644 src/Actions/GenerateICalendar.php delete mode 100644 src/Data/ICalEvent.php diff --git a/src/Actions/GenerateICalendar.php b/src/Actions/GenerateICalendar.php deleted file mode 100644 index 1319159..0000000 --- a/src/Actions/GenerateICalendar.php +++ /dev/null @@ -1,21 +0,0 @@ -title) - ->withoutTimezone() - ->uniqueIdentifier($event->id) - ->startsAt($event->start) - ->endsAt($event->end) - ->address($event->address) - ->description($event->description) - ->url($event->url); - } -} diff --git a/src/Data/ICalEvent.php b/src/Data/ICalEvent.php deleted file mode 100644 index e9e7d52..0000000 --- a/src/Data/ICalEvent.php +++ /dev/null @@ -1,18 +0,0 @@ - Date: Wed, 23 Apr 2025 14:22:30 -0700 Subject: [PATCH 4/8] add to recurring events --- src/Types/RecurringEvent.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Types/RecurringEvent.php b/src/Types/RecurringEvent.php index f771726..003ad0d 100644 --- a/src/Types/RecurringEvent.php +++ b/src/Types/RecurringEvent.php @@ -32,6 +32,9 @@ public function toICalendarEvents(): array ->uniqueIdentifier($this->event->id()) ->startsAt($this->start()) ->endsAt($this->end()) + ->address($this->location($this->event)) + ->description($this->event->description) + ->url($this->event->link) ->rrule($this->spatieRule()), ]; } From f482354e5d75c44367d00c14f1446a3a929d6e3e Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 23 Apr 2025 15:08:41 -0700 Subject: [PATCH 5/8] tests --- src/Types/Event.php | 21 ++++++++++++++++----- src/Types/MultiDayEvent.php | 21 ++++++++++++++++----- tests/Feature/IcsControllerTest.php | 19 ++++++++++++++++++- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/Types/Event.php b/src/Types/Event.php index 37956c1..cb61c3c 100644 --- a/src/Types/Event.php +++ b/src/Types/Event.php @@ -103,14 +103,25 @@ public function toICalendarEvent(string|CarbonInterface $date): ?ICalendarEvent $immutableDate = $this->toCarbonImmutable($date); - return ICalendarEvent::create($this->event->title) + $iCalEvent = ICalendarEvent::create($this->event->title) ->withoutTimezone() ->uniqueIdentifier($this->event->id()) ->startsAt($immutableDate->setTimeFromTimeString($this->startTime())) - ->endsAt($immutableDate->setTimeFromTimeString($this->endTime())) - ->address($this->location($this->event)) - ->description($this->event->description) - ->url($this->event->link); + ->endsAt($immutableDate->setTimeFromTimeString($this->endTime())); + + if (!is_null($location = $this->location($this->event))) { + $iCalEvent->address($location); + } + + if (!is_null($description = $this->event->description)) { + $iCalEvent->description($description); + } + + if (!is_null($link = $this->event->link)) { + $iCalEvent->url($link); + } + + return $iCalEvent; } /** diff --git a/src/Types/MultiDayEvent.php b/src/Types/MultiDayEvent.php index 9dcef74..5eaa7e7 100644 --- a/src/Types/MultiDayEvent.php +++ b/src/Types/MultiDayEvent.php @@ -59,13 +59,24 @@ public function toICalendarEvent(string|CarbonInterface $date): ?ICalendarEvent $immutableDate = $this->toCarbonImmutable($date); $day = $this->getDayFromDate($immutableDate); - return ICalendarEvent::create($this->event->title) + $iCalEvent = ICalendarEvent::create($this->event->title) ->uniqueIdentifier($this->event->id()) ->startsAt($immutableDate->setTimeFromTimeString($day->start())) - ->endsAt($immutableDate->setTimeFromTimeString($day->end())) - ->address($this->location($this->event)) - ->description($this->event->description) - ->url($this->event->link); + ->endsAt($immutableDate->setTimeFromTimeString($day->end())); + + if (! is_null($location = $this->location($this->event))) { + $iCalEvent->address($location); + } + + if (! is_null($description = $this->event->description)) { + $iCalEvent->description($description); + } + + if (! is_null($link = $this->event->link)) { + $iCalEvent->url($link); + } + + return $iCalEvent; } /** diff --git a/tests/Feature/IcsControllerTest.php b/tests/Feature/IcsControllerTest.php index 8aa8aa4..43a3c45 100755 --- a/tests/Feature/IcsControllerTest.php +++ b/tests/Feature/IcsControllerTest.php @@ -23,6 +23,8 @@ protected function setUp(): void 'start_time' => '11:00', 'end_time' => '12:00', 'location' => 'The Location', + 'description' => 'The description', + 'link' => 'https://transformstudios.com' ])->save(); } @@ -38,6 +40,8 @@ public function can_create_single_day_event_ics_file() $this->assertStringContainsString('DTSTART:'.now()->setTimeFromTimeString('11:00')->format('Ymd\THis'), $response->streamedContent()); $this->assertStringContainsString('LOCATION:The Location', $response->streamedContent()); + $this->assertStringContainsString('DESCRIPTION:The description', $response->streamedContent()); + $this->assertStringContainsString('URL:https://transformstudios.com', $response->streamedContent()); } #[Test] @@ -55,6 +59,9 @@ public function can_create_single_day_recurring_event_ics_file() 'start_time' => '11:00', 'end_time' => '12:00', 'recurrence' => 'weekly', + 'location' => 'The Location', + 'description' => 'The description', + 'link' => 'https://transformstudios.com' ])->save(); $response = $this->get(route('statamic.events.ics.show', [ @@ -63,6 +70,9 @@ public function can_create_single_day_recurring_event_ics_file() ]))->assertDownload('recurring-event.ics'); $this->assertStringContainsString('DTSTART:'.now()->setTimeFromTimeString('11:00')->format('Ymd\THis'), $response->streamedContent()); + $this->assertStringContainsString('LOCATION:The Location', $response->streamedContent()); + $this->assertStringContainsString('DESCRIPTION:The description', $response->streamedContent()); + $this->assertStringContainsString('URL:https://transformstudios.com', $response->streamedContent()); $this->get(route('statamic.events.ics.show', [ 'date' => now()->addDay()->toDateString(), @@ -75,13 +85,16 @@ public function can_create_single_day_multiday_event_ics_file() { Carbon::setTestNow(now()); - $entry = Entry::make() + Entry::make() ->slug('multi-day-event') ->collection('events') ->id('the-multi-day-event') ->data([ 'title' => 'Multi-day Event', 'multi_day' => true, + 'location' => 'The Location', + 'description' => 'The description', + 'link' => 'https://transformstudios.com', 'days' => [ [ 'date' => now()->toDateString(), @@ -112,6 +125,10 @@ public function can_create_single_day_multiday_event_ics_file() ]))->assertDownload('multi-day-event.ics'); $this->assertStringContainsString('DTSTART:'.now()->addDay()->setTimeFromTimeString('11:00')->format('Ymd\THis'), $response->streamedContent()); + $this->assertStringContainsString('LOCATION:The Location', $response->streamedContent()); + $this->assertStringContainsString('DESCRIPTION:The description', $response->streamedContent()); + $this->assertStringContainsString('URL:https://transformstudios.com', $response->streamedContent()); + } #[Test] From 888d4d455b545b7dfffd3fcccd042ae4322cb2e2 Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 23 Apr 2025 15:24:18 -0700 Subject: [PATCH 6/8] more tests and fixes --- src/Http/Controllers/IcsController.php | 3 +- tests/Feature/IcsControllerTest.php | 64 ++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/Http/Controllers/IcsController.php b/src/Http/Controllers/IcsController.php index b2e9b3f..19e7230 100644 --- a/src/Http/Controllers/IcsController.php +++ b/src/Http/Controllers/IcsController.php @@ -51,7 +51,8 @@ public function __invoke(Request $request) if ($entry) { return $this->downloadIcs( - EventFactory::createFromEntry($entry)->toICalendarEvents() + EventFactory::createFromEntry($entry)->toICalendarEvents(), + $entry->title ); } } diff --git a/tests/Feature/IcsControllerTest.php b/tests/Feature/IcsControllerTest.php index 43a3c45..3e60eef 100755 --- a/tests/Feature/IcsControllerTest.php +++ b/tests/Feature/IcsControllerTest.php @@ -80,6 +80,70 @@ public function can_create_single_day_recurring_event_ics_file() ]))->assertStatus(404); } + #[Test] + public function can_create_ics_with_single_date_recurrence() + { + Carbon::setTestNow(now()->addDay()->setTimeFromTimeString('10:00')); + + Entry::make() + ->collection('events') + ->slug('recurring-event') + ->id('the-recurring-id') + ->data([ + 'title' => 'Recurring Event', + 'start_date' => Carbon::now()->toDateString(), + 'start_time' => '11:00', + 'end_time' => '12:00', + 'recurrence' => 'weekly', + 'location' => 'The Location', + 'description' => 'The description', + 'link' => 'https://transformstudios.com' + ])->save(); + + $response = $this->get(route('statamic.events.ics.show', [ + 'date' => now()->toDateString(), + 'event' => 'the-recurring-id', + ]))->assertDownload('recurring-event.ics'); + + + $this->assertStringContainsString('DTSTART:'.now()->setTimeFromTimeString('11:00')->format('Ymd\THis'), $response->streamedContent()); + $this->assertStringContainsString('LOCATION:The Location', $response->streamedContent()); + $this->assertStringContainsString('DESCRIPTION:The description', $response->streamedContent()); + $this->assertStringContainsString('URL:https://transformstudios.com', $response->streamedContent()); + + } + + #[Test] + public function can_create_ics_with_recurrence() + { + Carbon::setTestNow(now()->addDay()->setTimeFromTimeString('10:00')); + + Entry::make() + ->collection('events') + ->slug('recurring-event') + ->id('the-recurring-id') + ->data([ + 'title' => 'Recurring Event', + 'start_date' => Carbon::now()->toDateString(), + 'start_time' => '11:00', + 'end_time' => '12:00', + 'recurrence' => 'weekly', + 'location' => 'The Location', + 'description' => 'The description', + 'link' => 'https://transformstudios.com' + ])->save(); + + $response = $this->get(route('statamic.events.ics.show', [ + 'event' => 'the-recurring-id', + ]))->assertDownload('recurring-event.ics'); + + $this->assertStringContainsString('DTSTART:'.now()->setTimeFromTimeString('11:00')->format('Ymd\THis'), $response->streamedContent()); + $this->assertStringContainsString('LOCATION:The Location', $response->streamedContent()); + $this->assertStringContainsString('DESCRIPTION:The description', $response->streamedContent()); + $this->assertStringContainsString('URL:https://transformstudios.com', $response->streamedContent()); + + } + #[Test] public function can_create_single_day_multiday_event_ics_file() { From 9f01a5eecb3617542c6592b2c4aeae7068cd566e Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 23 Apr 2025 15:37:27 -0700 Subject: [PATCH 7/8] update docs --- DOCUMENTATION.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 8bdb90f..1e0f8b0 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -228,7 +228,10 @@ Tag pair that returns the next X event dates. ### Download Links -Single Tag returns a url to the event data and add it to your calendar. If there's a "location" field (see config above), it'll get added to the download. +Single Tag returns a url to the event data and add it to your calendar. The following fields will be added to the ICS if they exist: + * `location` (see config above) + * `description` + * `link` Parameters: From 55860d44cefb8d16b59c04d06b3888cc16f72b91 Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 23 Apr 2025 16:16:58 -0700 Subject: [PATCH 8/8] these are optional --- src/Types/RecurringEvent.php | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Types/RecurringEvent.php b/src/Types/RecurringEvent.php index 003ad0d..a59482a 100644 --- a/src/Types/RecurringEvent.php +++ b/src/Types/RecurringEvent.php @@ -27,16 +27,25 @@ public function interval(): int */ public function toICalendarEvents(): array { - return [ - ICalendarEvent::create($this->event->title) - ->uniqueIdentifier($this->event->id()) - ->startsAt($this->start()) - ->endsAt($this->end()) - ->address($this->location($this->event)) - ->description($this->event->description) - ->url($this->event->link) - ->rrule($this->spatieRule()), - ]; + $iCalEvent = ICalendarEvent::create($this->event->title) + ->uniqueIdentifier($this->event->id()) + ->startsAt($this->start()) + ->endsAt($this->end()) + ->rrule($this->spatieRule()); + + if (! is_null($location = $this->location($this->event))) { + $iCalEvent->address($location); + } + + if (! is_null($description = $this->event->description)) { + $iCalEvent->description($description); + } + + if (! is_null($link = $this->event->link)) { + $iCalEvent->url($link); + } + + return [$iCalEvent]; } protected function rule(): RRuleInterface