diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index c856a6d..a40c777 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -46,12 +46,12 @@ ICS downloads read the following entry fields when present: |---|---| | `LOCATION` | `address`, falling back to `location` — only when the value is a string | | `URL` | `online_url`, then deprecated `link`, then deprecated `location` when that string is a URL | -| `GEO` | `coordinates` (`latitude` / `longitude`) | +| `GEO` | `coordinates` (`latitude` / `longitude`) — declared on the event fieldset | | `DESCRIPTION` | `description` | A URL-valued `location` (with no separate `address` / `online_url` / `link`) currently emits **both** `LOCATION:` and `URL:`. Prefer `online_url` for join links; `link` and the URL-valued `location` fallback are deprecated and will be removed in 7.0. -The `coordinates` field must be a keyed array: +`coordinates` shape: ```php 'coordinates' => [ @@ -60,6 +60,8 @@ The `coordinates` field must be a keyed array: ], ``` +Partial or non-numeric coordinates are skipped (no `GEO:`) rather than failing the download. + If your field names differ from the defaults above, use a [Computed Value](https://statamic.dev/content-modeling/computed-values#defining-computed-values) to map them. --- @@ -84,6 +86,7 @@ Using the sample fieldset is the fastest way to get started. | Field | Description | |-------|-------------| | `online_url` | Join link for online or hybrid events (Zoom, livestream, etc.). Optional; can be combined with a physical place once `location` is declared. | +| `coordinates` | Optional `latitude` / `longitude` floats for ICS `GEO:`. Only meaningful alongside a physical location. | `link` is deprecated in favour of `online_url` and will be removed in 7.0. diff --git a/resources/fieldsets/event.yaml b/resources/fieldsets/event.yaml index 9ba674c..c0831d3 100644 --- a/resources/fieldsets/event.yaml +++ b/resources/fieldsets/event.yaml @@ -219,3 +219,24 @@ fields: - nullable - url instructions: 'Join link for online or hybrid events (Zoom, livestream, etc.).' + - + handle: coordinates + field: + type: group + display: Coordinates + fullscreen: false + border: false + instructions: 'Optional map coordinates for ICS GEO. Only meaningful alongside a physical location.' + fields: + - + handle: latitude + field: + type: float + display: Latitude + width: 50 + - + handle: longitude + field: + type: float + display: Longitude + width: 50 diff --git a/tests/Http/Contollers/IcsControllerTest.php b/tests/Http/Contollers/IcsControllerTest.php index 121c5ae..bcdc23f 100755 --- a/tests/Http/Contollers/IcsControllerTest.php +++ b/tests/Http/Contollers/IcsControllerTest.php @@ -581,3 +581,130 @@ $this->assertStringContainsString('URL:https://zoom.us/j/multiday', $multiDate); expect(substr_count($multiWhole, 'URL:https://zoom.us/j/multiday'))->toBe(2); }); + +test('declared coordinates emit GEO', function () { + Carbon::setTestNow(now()->setTimeFromTimeString('10:00')); + + Entry::make() + ->collection('events') + ->slug('coords-event') + ->id('coords-id') + ->data([ + 'title' => 'Coords Event', + 'start_date' => Carbon::now()->toDateString(), + 'start_time' => '11:00', + 'end_time' => '12:00', + 'address' => '123 Main St', + 'coordinates' => [ + 'latitude' => 49.28, + 'longitude' => -123.12, + ], + ])->save(); + + $content = $this->get(route('statamic.events.ics.show', [ + 'date' => now()->toDateString(), + 'event' => 'coords-id', + ]))->assertDownload('coords-event.ics')->streamedContent(); + + $this->assertStringContainsString('GEO:49.28;-123.12', $content); +}); + +test('coordinates field rejects non-numeric latitude or longitude', function () { + $fields = \Statamic\Facades\Fieldset::find('events::event')->fields(); + + expect(fn () => $fields->addValues([ + 'coordinates' => [ + 'latitude' => 'north', + 'longitude' => 50, + ], + ])->validator()->validate())->toThrow(\Illuminate\Validation\ValidationException::class); +}); + +test('coordinates are included on all four download routes', function () { + Carbon::setTestNow(now()); + + Entry::make() + ->collection('events') + ->slug('coords-single') + ->id('coords-single-id') + ->data([ + 'title' => 'Coords Single', + 'start_date' => now()->toDateString(), + 'start_time' => '11:00', + 'end_time' => '12:00', + 'coordinates' => [ + 'latitude' => 40, + 'longitude' => 50, + ], + ])->save(); + + Entry::make() + ->collection('events') + ->slug('coords-recurring') + ->id('coords-recurring-id') + ->data([ + 'title' => 'Coords Recurring', + 'start_date' => now()->toDateString(), + 'start_time' => '11:00', + 'end_time' => '12:00', + 'recurrence' => 'weekly', + 'coordinates' => [ + 'latitude' => 40, + 'longitude' => 50, + ], + ])->save(); + + Entry::make() + ->collection('events') + ->slug('coords-multi-day') + ->id('coords-multi-day-id') + ->data([ + 'title' => 'Coords Multi Day', + 'multi_day' => true, + 'coordinates' => [ + 'latitude' => 40, + 'longitude' => 50, + ], + 'days' => [ + [ + 'date' => now()->toDateString(), + 'start_time' => '19:00', + 'end_time' => '21:00', + ], + [ + 'date' => now()->addDay()->toDateString(), + 'start_time' => '11:00', + 'end_time' => '15:00', + ], + ], + ])->save(); + + $single = $this->get(route('statamic.events.ics.show', [ + 'date' => now()->toDateString(), + 'event' => 'coords-single-id', + ]))->assertDownload('coords-single.ics')->streamedContent(); + + $recurringDate = $this->get(route('statamic.events.ics.show', [ + 'date' => now()->toDateString(), + 'event' => 'coords-recurring-id', + ]))->assertDownload('coords-recurring.ics')->streamedContent(); + + $recurringWhole = $this->get(route('statamic.events.ics.show', [ + 'event' => 'coords-recurring-id', + ]))->assertDownload('coords-recurring.ics')->streamedContent(); + + $multiDate = $this->get(route('statamic.events.ics.show', [ + 'date' => now()->toDateString(), + 'event' => 'coords-multi-day-id', + ]))->assertDownload('coords-multi-day.ics')->streamedContent(); + + $multiWhole = $this->get(route('statamic.events.ics.show', [ + 'event' => 'coords-multi-day-id', + ]))->assertDownload('coords-multi-day.ics')->streamedContent(); + + $this->assertStringContainsString('GEO:40;50', $single); + $this->assertStringContainsString('GEO:40;50', $recurringDate); + $this->assertStringContainsString('GEO:40;50', $recurringWhole); + $this->assertStringContainsString('GEO:40;50', $multiDate); + expect(substr_count($multiWhole, 'GEO:40;50'))->toBe(2); +});