From 26032605ab283139de11c3036dbc661e73e98f68 Mon Sep 17 00:00:00 2001 From: Danny Gershman Date: Sun, 18 Jan 2026 23:58:48 -0500 Subject: [PATCH 1/2] fixing end time issues --- .../components/public/EventAnnouncement.js | 10 +- .../js/src/components/public/EventArchive.js | 9 +- assets/js/src/components/public/EventList.js | 2 + assets/js/src/components/public/EventModal.js | 4 +- .../src/components/public/cards/EventCard.js | 6 +- includes/Rest/AnnouncementsController.php | 99 ++++++- includes/Rest/EventsController.php | 61 +++- readme.txt | 1 + tests/Unit/Rest/EventsControllerTest.php | 263 ++++++++++++++++++ 9 files changed, 429 insertions(+), 26 deletions(-) diff --git a/assets/js/src/components/public/EventAnnouncement.js b/assets/js/src/components/public/EventAnnouncement.js index 35fb89a..737ad6b 100644 --- a/assets/js/src/components/public/EventAnnouncement.js +++ b/assets/js/src/components/public/EventAnnouncement.js @@ -3,6 +3,7 @@ import AnnouncementBanner from './AnnouncementBanner'; import AnnouncementModal from './AnnouncementModal'; import AnnouncementBellIcon from './AnnouncementBellIcon'; import { apiFetch } from '../../util'; +import { getUserTimezone } from '../../timezones'; const EventAnnouncement = ({ settings = {} }) => { const [announcements, setAnnouncements] = useState([]); @@ -40,6 +41,9 @@ const EventAnnouncement = ({ settings = {} }) => { return Date.now() - timestamp < twentyFourHours; }, [getDismissalKey]); + // Get user's timezone for accurate time filtering + const userTimezone = getUserTimezone(); + // Fetch announcements from the new announcements API useEffect(() => { const fetchAnnouncements = async () => { @@ -48,6 +52,10 @@ const EventAnnouncement = ({ settings = {} }) => { // Use the new announcements endpoint which handles date filtering server-side let endpoint = '/announcements?per_page=20'; + // Add timezone and current time for accurate end time filtering + endpoint += `&timezone=${encodeURIComponent(userTimezone)}`; + endpoint += `¤t_time=${encodeURIComponent(new Date().toISOString())}`; + if (categories) { endpoint += `&categories=${encodeURIComponent(categories)}`; } @@ -87,7 +95,7 @@ const EventAnnouncement = ({ settings = {} }) => { }; fetchAnnouncements(); - }, [categories, categoryRelation, tags, priority, orderBy, order, checkDismissed]); + }, [categories, categoryRelation, tags, priority, orderBy, order, userTimezone, checkDismissed]); // Handle dismiss const handleDismiss = useCallback(() => { diff --git a/assets/js/src/components/public/EventArchive.js b/assets/js/src/components/public/EventArchive.js index eba5397..6f53236 100644 --- a/assets/js/src/components/public/EventArchive.js +++ b/assets/js/src/components/public/EventArchive.js @@ -1,6 +1,7 @@ import { useState, useEffect } from '@wordpress/element'; import { formatTimezone, apiFetch } from '../../util'; // Import the helper function import { useEventProvider } from '../providers/EventProvider'; +import { getUserTimezone } from '../../timezones'; const EventArchive = () => { const [events, setEvents] = useState([]); @@ -8,10 +9,16 @@ const EventArchive = () => { const [error, setError] = useState(null); const { getServiceBodyName } = useEventProvider(); + // Get user's current timezone + const userTimezone = getUserTimezone(); + useEffect(() => { const fetchEvents = async () => { try { - const response = await apiFetch('/events?archive=true'); + const endpoint = `/events?archive=true` + + `&timezone=${encodeURIComponent(userTimezone)}` + + `¤t_time=${encodeURIComponent(new Date().toISOString())}`; + const response = await apiFetch(endpoint); // Ensure we have a valid response and it's an array if (response && Array.isArray(response)) { diff --git a/assets/js/src/components/public/EventList.js b/assets/js/src/components/public/EventList.js index d375d13..a2bf91f 100644 --- a/assets/js/src/components/public/EventList.js +++ b/assets/js/src/components/public/EventList.js @@ -288,6 +288,7 @@ const EventList = ({ widget = false, settings = {} }) => { + `&page=${page}` + `&per_page=${perPage}` + `&timezone=${encodeURIComponent(userTimezone)}` + + `¤t_time=${encodeURIComponent(new Date().toISOString())}` + `&archive=${archive}` + `&order=${order}`; @@ -368,6 +369,7 @@ const EventList = ({ widget = false, settings = {} }) => { + `&tags=${tags}` + `&source_ids=${sourceIds}` + `&timezone=${encodeURIComponent(userTimezone)}` + + `¤t_time=${encodeURIComponent(new Date().toISOString())}` + `&order=${order}` + `&start_date=${startDate}` + `&end_date=${endDate}` diff --git a/assets/js/src/components/public/EventModal.js b/assets/js/src/components/public/EventModal.js index cdb38c7..ab0c5b1 100644 --- a/assets/js/src/components/public/EventModal.js +++ b/assets/js/src/components/public/EventModal.js @@ -111,10 +111,10 @@ const EventModal = ({ event, timeFormat, onClose }) => { )} - {event.source_id && event.source_id !== 'local' && ( + {event.source && event.source.type === 'external' && (
- External Event + {event.source.name}
)} diff --git a/assets/js/src/components/public/cards/EventCard.js b/assets/js/src/components/public/cards/EventCard.js index b6a433a..1599098 100644 --- a/assets/js/src/components/public/cards/EventCard.js +++ b/assets/js/src/components/public/cards/EventCard.js @@ -108,9 +108,9 @@ const EventCard = ({ event, timeFormat, forceExpanded }) => { {formatDateTimeDisplay(event, timeFormat)} )} - {event.source_id && event.source_id !== 'local' && ( - - External Event + {event.source && event.source.type === 'external' && ( + + {event.source.name} )} {event.meta.service_body && ( diff --git a/includes/Rest/AnnouncementsController.php b/includes/Rest/AnnouncementsController.php index 621de9a..0995d6f 100644 --- a/includes/Rest/AnnouncementsController.php +++ b/includes/Rest/AnnouncementsController.php @@ -64,9 +64,19 @@ public static function get_announcements($request) { $active_only = !isset($params['active']) || $params['active'] !== 'false'; $orderby = isset($params['orderby']) ? sanitize_text_field($params['orderby']) : 'date'; $order = isset($params['order']) ? strtoupper(sanitize_text_field($params['order'])) : ''; + $timezone = isset($params['timezone']) ? urldecode(sanitize_text_field(wp_unslash($params['timezone']))) : wp_timezone_string(); + $current_time = isset($params['current_time']) ? sanitize_text_field(wp_unslash($params['current_time'])) : null; $today = current_time('Y-m-d'); + // Create DateTime object for current time with proper timezone + if ($current_time) { + $now = new \DateTime($current_time); + $now->setTimezone(new \DateTimeZone($timezone)); + } else { + $now = new \DateTime('now', new \DateTimeZone($timezone)); + } + $args = [ 'post_type' => 'mayo_announcement', 'post_status' => 'publish', @@ -144,9 +154,42 @@ public static function get_announcements($request) { $posts = get_posts($args); + // Post-filter based on end time if active_only is enabled + // The meta_query only checks dates, so we need to filter by time here + if ($active_only) { + $posts = array_filter($posts, function($post) use ($now, $timezone) { + $end_date_str = get_post_meta($post->ID, 'display_end_date', true); + + // If no end date, the announcement is considered active + if (empty($end_date_str)) { + return true; + } + + $end_time_str = get_post_meta($post->ID, 'display_end_time', true); + + // Create end DateTime with proper time and timezone + $tz = new \DateTimeZone($timezone); + $end_datetime = new \DateTime($end_date_str, $tz); + if (!empty($end_time_str)) { + $time_parts = explode(':', $end_time_str); + $end_datetime->setTime( + (int)($time_parts[0] ?? 23), + (int)($time_parts[1] ?? 59), + (int)($time_parts[2] ?? 59) + ); + } else { + // If no end time specified, use end of day + $end_datetime->setTime(23, 59, 59); + } + + // Announcement is active if end datetime is in the future + return $end_datetime >= $now; + }); + } + $announcements = []; foreach ($posts as $post) { - $announcements[] = self::format_announcement($post); + $announcements[] = self::format_announcement($post, $now); } // Sort announcements @@ -377,9 +420,10 @@ private static function send_submission_email($post_id, $params) { * Format announcement data for API response * * @param \WP_Post $post + * @param \DateTime|null $now Current DateTime for is_active calculation * @return array */ - public static function format_announcement($post) { + public static function format_announcement($post, $now = null) { $linked_refs = Announcement::get_linked_event_refs($post->ID); $linked_event_data = []; @@ -423,17 +467,56 @@ public static function format_announcement($post) { } } - // Calculate is_active based on display dates - $today = current_time('Y-m-d'); + // Calculate is_active based on display dates and times $display_start_date = get_post_meta($post->ID, 'display_start_date', true); + $display_start_time = get_post_meta($post->ID, 'display_start_time', true); $display_end_date = get_post_meta($post->ID, 'display_end_date', true); + $display_end_time = get_post_meta($post->ID, 'display_end_time', true); + + // Use provided $now or fall back to current time + if (!$now) { + $now = new \DateTime('now', new \DateTimeZone(wp_timezone_string())); + } + + // Get timezone from $now for consistent comparisons + $tz = $now->getTimezone(); $is_active = true; - if ($display_start_date && $display_start_date > $today) { - $is_active = false; + + // Check start date/time + if ($display_start_date) { + $start_datetime = new \DateTime($display_start_date, $tz); + if (!empty($display_start_time)) { + $time_parts = explode(':', $display_start_time); + $start_datetime->setTime( + (int)($time_parts[0] ?? 0), + (int)($time_parts[1] ?? 0), + (int)($time_parts[2] ?? 0) + ); + } else { + $start_datetime->setTime(0, 0, 0); + } + if ($start_datetime > $now) { + $is_active = false; + } } - if ($display_end_date && $display_end_date < $today) { - $is_active = false; + + // Check end date/time + if ($display_end_date && $is_active) { + $end_datetime = new \DateTime($display_end_date, $tz); + if (!empty($display_end_time)) { + $time_parts = explode(':', $display_end_time); + $end_datetime->setTime( + (int)($time_parts[0] ?? 23), + (int)($time_parts[1] ?? 59), + (int)($time_parts[2] ?? 59) + ); + } else { + $end_datetime->setTime(23, 59, 59); + } + if ($end_datetime < $now) { + $is_active = false; + } } $permalink = get_permalink($post->ID); diff --git a/includes/Rest/EventsController.php b/includes/Rest/EventsController.php index ebc503a..36a477b 100644 --- a/includes/Rest/EventsController.php +++ b/includes/Rest/EventsController.php @@ -196,6 +196,11 @@ public static function get_events() { $events = array_merge($events, array_map(function($event) { $event['source_id'] = 'local'; + $event['source'] = [ + 'type' => 'local', + 'id' => 'local', + 'name' => 'Local', + ]; return $event; }, $local_events)); } @@ -796,13 +801,20 @@ private static function get_local_events($params) { $categoryRelation = isset($params['category_relation']) ? strtoupper(sanitize_text_field(wp_unslash($params['category_relation']))) : 'OR'; $tags = isset($params['tags']) ? sanitize_text_field(wp_unslash($params['tags'])) : ''; $timezone = isset($params['timezone']) ? urldecode(sanitize_text_field(wp_unslash($params['timezone']))) : wp_timezone_string(); + $current_time = isset($params['current_time']) ? sanitize_text_field(wp_unslash($params['current_time'])) : null; $range_start = isset($params['start_date']) ? sanitize_text_field(wp_unslash($params['start_date'])) : null; $range_end = isset($params['end_date']) ? sanitize_text_field(wp_unslash($params['end_date'])) : null; $has_date_range = !empty($range_start) && !empty($range_end); - $today = new DateTime('now', new \DateTimeZone($timezone)); - $today->setTime(0, 0, 0); + // Use the browser's current time if provided, otherwise fall back to server time + if ($current_time) { + $today = new DateTime($current_time); + $today->setTimezone(new \DateTimeZone($timezone)); + } else { + $today = new DateTime('now', new \DateTimeZone($timezone)); + } + // Keep the actual current time - don't reset to midnight $events = []; @@ -870,21 +882,32 @@ private static function get_local_events($params) { if ($filter_range_start) $filter_range_start->setTime(0, 0, 0); if ($filter_range_end) $filter_range_end->setTime(23, 59, 59); - $filtered_recurring_events = array_filter($recurring_events, function($event) use ($today, $is_archive, $has_date_range, $filter_range_start, $filter_range_end) { + $filtered_recurring_events = array_filter($recurring_events, function($event) use ($today, $is_archive, $has_date_range, $filter_range_start, $filter_range_end, $timezone) { if (!isset($event['meta']['event_start_date']) || empty($event['meta']['event_start_date'])) { return false; } try { + $tz = new \DateTimeZone($timezone); $start_date_str = $event['meta']['event_start_date']; - $start_date = new DateTime($start_date_str); + $start_date = new DateTime($start_date_str, $tz); $start_date->setTime(0, 0, 0); $end_date_str = isset($event['meta']['event_end_date']) && !empty($event['meta']['event_end_date']) ? $event['meta']['event_end_date'] : $start_date_str; - $end_date = new DateTime($end_date_str); - $end_date->setTime(23, 59, 59); + $end_date = new DateTime($end_date_str, $tz); + + // Use actual event_end_time if available, otherwise default to end of day + $end_time_str = isset($event['meta']['event_end_time']) && !empty($event['meta']['event_end_time']) + ? $event['meta']['event_end_time'] + : '23:59:59'; + $end_time_parts = explode(':', $end_time_str); + $end_date->setTime( + (int)($end_time_parts[0] ?? 23), + (int)($end_time_parts[1] ?? 59), + (int)($end_time_parts[2] ?? 59) + ); if ($has_date_range) { return $start_date <= $filter_range_end && $end_date >= $filter_range_start; @@ -914,21 +937,32 @@ private static function get_local_events($params) { if ($range_start_dt) $range_start_dt->setTime(0, 0, 0); if ($range_end_dt) $range_end_dt->setTime(23, 59, 59); - $events = array_filter($events, function($event) use ($today, $is_archive, $has_date_range, $range_start_dt, $range_end_dt) { + $events = array_filter($events, function($event) use ($today, $is_archive, $has_date_range, $range_start_dt, $range_end_dt, $timezone) { if (!isset($event['meta']['event_start_date']) || empty($event['meta']['event_start_date'])) { return false; } try { + $tz = new \DateTimeZone($timezone); $start_date_str = $event['meta']['event_start_date']; - $start_date = new DateTime($start_date_str); + $start_date = new DateTime($start_date_str, $tz); $start_date->setTime(0, 0, 0); $end_date_str = isset($event['meta']['event_end_date']) && !empty($event['meta']['event_end_date']) ? $event['meta']['event_end_date'] : $start_date_str; - $end_date = new DateTime($end_date_str); - $end_date->setTime(23, 59, 59); + $end_date = new DateTime($end_date_str, $tz); + + // Use actual event_end_time if available, otherwise default to end of day + $end_time_str = isset($event['meta']['event_end_time']) && !empty($event['meta']['event_end_time']) + ? $event['meta']['event_end_time'] + : '23:59:59'; + $end_time_parts = explode(':', $end_time_str); + $end_date->setTime( + (int)($end_time_parts[0] ?? 23), + (int)($end_time_parts[1] ?? 59), + (int)($end_time_parts[2] ?? 59) + ); if ($has_date_range) { return $start_date <= $range_end_dt && $end_date >= $range_start_dt; @@ -1098,9 +1132,14 @@ private static function fetch_external_events($source) { 'service_bodies' => $service_bodies ]; - // Attach source_id to each event (service bodies are in the sources array) + // Attach source info to each event (service bodies are in the sources array) foreach ($events as &$event) { $event['source_id'] = $source['id']; + $event['source'] = [ + 'type' => 'external', + 'id' => $source['id'], + 'name' => $source['name'] ?? parse_url($source['url'], PHP_URL_HOST), + ]; } return [ diff --git a/readme.txt b/readme.txt index 4ae6767..6760dd5 100644 --- a/readme.txt +++ b/readme.txt @@ -189,6 +189,7 @@ This project is licensed under the GPL v2 or later. = 1.8.5 = * Fixed external feed events not displaying service body names correctly. [#234] +* Fixed events and announcements remaining visible after their scheduled end time passes. Previously only the end date was checked, ignoring the end time. [#237] = 1.8.4 = * Added custom URL/link support for announcements with selectable icons. [#227] diff --git a/tests/Unit/Rest/EventsControllerTest.php b/tests/Unit/Rest/EventsControllerTest.php index 780d051..b53c93e 100644 --- a/tests/Unit/Rest/EventsControllerTest.php +++ b/tests/Unit/Rest/EventsControllerTest.php @@ -1449,4 +1449,267 @@ public function testGetEventsWithMonthlyLastWeekdayOfMonth(): void { $this->assertInstanceOf(\WP_REST_Response::class, $response); } + /** + * Test get_events hides events after their end time passes + * + * This test verifies the fix for issue #237 where events remained visible + * after their scheduled end time. The end date worked correctly but the + * end time was ignored. + * + * Uses UTC timezone consistently to avoid server timezone dependencies. + */ + public function testGetEventsHidesEventsAfterEndTime(): void { + // Use fixed dates to avoid timezone ambiguity + // Event ends at 14:00 UTC, current time is 15:00 UTC + $eventDate = '2030-06-15'; + $eventStartTime = '13:00:00'; + $eventEndTime = '14:00:00'; + $currentTimeUtc = '2030-06-15T15:00:00Z'; // 1 hour after event ends + + $post = $this->createMockPost([ + 'ID' => 700, + 'post_title' => 'Past Event', + 'post_type' => 'mayo_event', + 'post_status' => 'publish' + ]); + + $this->setPostMeta(700, [ + 'event_start_date' => $eventDate, + 'event_end_date' => $eventDate, + 'event_start_time' => $eventStartTime, + 'event_end_time' => $eventEndTime + ]); + + Functions\when('get_posts')->justReturn([$post]); + Functions\when('get_post')->justReturn($post); + Functions\when('get_site_url')->justReturn('https://example.com'); + Functions\when('get_the_title')->justReturn('Past Event'); + Functions\when('has_post_thumbnail')->justReturn(false); + Functions\when('wp_get_post_terms')->justReturn([]); + + // Use UTC timezone consistently for both event and current time + $_GET = [ + 'timezone' => 'UTC', + 'current_time' => $currentTimeUtc + ]; + + $response = EventsController::get_events(); + + $this->assertInstanceOf(\WP_REST_Response::class, $response); + $data = $response->get_data(); + + // The event should not appear in the list since its end time has passed + $this->assertArrayHasKey('events', $data); + $this->assertCount(0, $data['events'], 'Event should be hidden after its end time passes'); + } + + /** + * Test get_events shows events that have not ended yet + * + * Uses UTC timezone consistently to avoid server timezone dependencies. + */ + public function testGetEventsShowsEventsBeforeEndTime(): void { + // Use fixed dates to avoid timezone ambiguity + // Event ends at 16:00 UTC, current time is 14:00 UTC (2 hours before) + $eventDate = '2030-06-15'; + $eventStartTime = '13:00:00'; + $eventEndTime = '16:00:00'; + $currentTimeUtc = '2030-06-15T14:00:00Z'; // 2 hours before event ends + + $post = $this->createMockPost([ + 'ID' => 701, + 'post_title' => 'Ongoing Event', + 'post_type' => 'mayo_event', + 'post_status' => 'publish' + ]); + + $this->setPostMeta(701, [ + 'event_start_date' => $eventDate, + 'event_end_date' => $eventDate, + 'event_start_time' => $eventStartTime, + 'event_end_time' => $eventEndTime + ]); + + Functions\when('get_posts')->justReturn([$post]); + Functions\when('get_post')->justReturn($post); + Functions\when('get_site_url')->justReturn('https://example.com'); + Functions\when('get_the_title')->justReturn('Ongoing Event'); + Functions\when('has_post_thumbnail')->justReturn(false); + Functions\when('wp_get_post_terms')->justReturn([]); + + // Use UTC timezone consistently for both event and current time + $_GET = [ + 'timezone' => 'UTC', + 'current_time' => $currentTimeUtc + ]; + + $response = EventsController::get_events(); + + $this->assertInstanceOf(\WP_REST_Response::class, $response); + $data = $response->get_data(); + + // The event should appear since its end time has not passed + $this->assertArrayHasKey('events', $data); + $this->assertCount(1, $data['events'], 'Event should be visible before its end time passes'); + } + + /** + * Test get_events with timezone offset shows event before end time + * + * This test reproduces the timezone mismatch bug where an event is incorrectly + * hidden when the server timezone (UTC) differs from the user's timezone. + * + * Scenario: + * - Event ends at 23:25 EST on Jan 18 + * - Current time is 23:12 EST (passed as 04:12 UTC on Jan 19) + * - Event should still be visible because 23:12 < 23:25 in EST + * + * The bug was that DateTime objects were created without timezone, causing + * the event's end time (23:25 UTC) to be compared against the user's current + * time (04:12 UTC), incorrectly filtering out the event. + */ + public function testGetEventsWithTimezoneOffsetShowsEventBeforeEndTime(): void { + $post = $this->createMockPost([ + 'ID' => 800, + 'post_title' => 'Test Expire Event', + 'post_type' => 'mayo_event', + 'post_status' => 'publish' + ]); + + // Event on Jan 18, 2026, ending at 23:25 EST + $this->setPostMeta(800, [ + 'event_start_date' => '2026-01-18', + 'event_end_date' => '2026-01-18', + 'event_start_time' => '23:00', + 'event_end_time' => '23:25' + ]); + + Functions\when('get_posts')->justReturn([$post]); + Functions\when('get_post')->justReturn($post); + Functions\when('get_site_url')->justReturn('https://example.com'); + Functions\when('get_the_title')->justReturn('Test Expire Event'); + Functions\when('has_post_thumbnail')->justReturn(false); + Functions\when('wp_get_post_terms')->justReturn([]); + + // Current time is 23:12 EST = 04:12 UTC on Jan 19 + // The event ends at 23:25 EST, so it should still be visible (13 minutes left) + $_GET = [ + 'timezone' => 'America/New_York', + 'current_time' => '2026-01-19T04:12:21Z' // 23:12 EST on Jan 18 + ]; + + $response = EventsController::get_events(); + + $this->assertInstanceOf(\WP_REST_Response::class, $response); + $data = $response->get_data(); + + $this->assertArrayHasKey('events', $data); + $this->assertCount(1, $data['events'], 'Event should be visible since current time (23:12 EST) is before event end time (23:25 EST)'); + } + + /** + * Test get_events with timezone offset hides event after end time + * + * Companion test to verify the event IS hidden when the time has passed. + */ + public function testGetEventsWithTimezoneOffsetHidesEventAfterEndTime(): void { + $post = $this->createMockPost([ + 'ID' => 801, + 'post_title' => 'Expired Event', + 'post_type' => 'mayo_event', + 'post_status' => 'publish' + ]); + + // Event on Jan 18, 2026, ending at 23:25 EST + $this->setPostMeta(801, [ + 'event_start_date' => '2026-01-18', + 'event_end_date' => '2026-01-18', + 'event_start_time' => '23:00', + 'event_end_time' => '23:25' + ]); + + Functions\when('get_posts')->justReturn([$post]); + Functions\when('get_post')->justReturn($post); + Functions\when('get_site_url')->justReturn('https://example.com'); + Functions\when('get_the_title')->justReturn('Expired Event'); + Functions\when('has_post_thumbnail')->justReturn(false); + Functions\when('wp_get_post_terms')->justReturn([]); + + // Current time is 23:30 EST = 04:30 UTC on Jan 19 + // The event ended at 23:25 EST, so it should be hidden + $_GET = [ + 'timezone' => 'America/New_York', + 'current_time' => '2026-01-19T04:30:00Z' // 23:30 EST on Jan 18 + ]; + + $response = EventsController::get_events(); + + $this->assertInstanceOf(\WP_REST_Response::class, $response); + $data = $response->get_data(); + + $this->assertArrayHasKey('events', $data); + $this->assertCount(0, $data['events'], 'Event should be hidden since current time (23:30 EST) is after event end time (23:25 EST)'); + } + + /** + * Test get_events uses current_time parameter correctly + * + * This test verifies the current_time parameter is properly parsed and used. + * We use far-future and far-past times to avoid timezone edge cases. + */ + public function testGetEventsUsesCurrentTimeParameter(): void { + // Create an event that ends at a specific time + $eventDate = '2030-06-15'; // Far future date to avoid any timezone issues + + $post = $this->createMockPost([ + 'ID' => 702, + 'post_title' => 'Future Event', + 'post_type' => 'mayo_event', + 'post_status' => 'publish' + ]); + + $this->setPostMeta(702, [ + 'event_start_date' => $eventDate, + 'event_end_date' => $eventDate, + 'event_start_time' => '10:00:00', + 'event_end_time' => '14:00:00' // 2 PM + ]); + + Functions\when('get_posts')->justReturn([$post]); + Functions\when('get_post')->justReturn($post); + Functions\when('get_site_url')->justReturn('https://example.com'); + Functions\when('get_the_title')->justReturn('Future Event'); + Functions\when('has_post_thumbnail')->justReturn(false); + Functions\when('wp_get_post_terms')->justReturn([]); + + // Test 1: When "now" is before the event starts, it should be visible + $_GET = [ + 'timezone' => 'UTC', + 'current_time' => '2030-06-15T08:00:00Z' // 8 AM, before event starts + ]; + + $response = EventsController::get_events(); + $data = $response->get_data(); + $this->assertCount(1, $data['events'], 'Event should be visible when current time is before event starts'); + + // Test 2: When "now" is during the event, it should still be visible + $_GET = [ + 'timezone' => 'UTC', + 'current_time' => '2030-06-15T12:00:00Z' // Noon, during the event + ]; + + $response = EventsController::get_events(); + $data = $response->get_data(); + $this->assertCount(1, $data['events'], 'Event should be visible when current time is during the event'); + + // Test 3: When "now" is after the event ends, it should be hidden + $_GET = [ + 'timezone' => 'UTC', + 'current_time' => '2030-06-15T15:00:00Z' // 3 PM, after event ends at 2 PM + ]; + + $response = EventsController::get_events(); + $data = $response->get_data(); + $this->assertCount(0, $data['events'], 'Event should be hidden when current time is after event ends'); + } } From c00ace94114c1e2e40ff22a5d7417440da3aff35 Mon Sep 17 00:00:00 2001 From: Danny Gershman Date: Mon, 19 Jan 2026 00:21:04 -0500 Subject: [PATCH 2/2] adding tz for announcements --- .../components/admin/AnnouncementEditor.js | 25 +++++++++++++++++++ .../admin/EventBlockEditorSidebar.js | 10 +++++++- includes/Announcement.php | 11 ++++++++ includes/Rest/AnnouncementsController.php | 12 ++++++--- tests/Unit/AnnouncementTest.php | 1 + 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/assets/js/src/components/admin/AnnouncementEditor.js b/assets/js/src/components/admin/AnnouncementEditor.js index 008f42b..5992d2a 100644 --- a/assets/js/src/components/admin/AnnouncementEditor.js +++ b/assets/js/src/components/admin/AnnouncementEditor.js @@ -14,6 +14,7 @@ import { __ } from '@wordpress/i18n'; import { useState, useEffect, useRef, useCallback } from '@wordpress/element'; import { apiFetch } from '../../util'; import { useEventProvider } from '../providers/EventProvider'; +import { getTimezoneOptions, getUserTimezone } from '../../timezones'; // Event Search Modal Component with infinite scroll const EventSearchModal = ({ isOpen, onClose, onSelectEvent, onRemoveEvent, linkedEventRefs, getRefKey }) => { @@ -575,6 +576,17 @@ const AnnouncementEditor = () => { fetchSubscriptionSettings(); }, [postType]); + // Determine if this is a new announcement + const isNewAnnouncement = postStatus === 'auto-draft'; + + // Auto-set timezone for new announcements based on browser timezone + useEffect(() => { + if (isNewAnnouncement && !meta.display_timezone) { + const detectedTimezone = getUserTimezone(); + editPost({ meta: { ...meta, display_timezone: detectedTimezone } }); + } + }, [isNewAnnouncement, meta.display_timezone]); + if (postType !== 'mayo_announcement') return null; // Filter service bodies by subscription settings @@ -898,6 +910,19 @@ const AnnouncementEditor = () => { Leave empty to show indefinitely

+
+ updateMetaValue('display_timezone', value)} + __nextHasNoMarginBottom={true} + __next40pxDefaultSize={true} + /> +
diff --git a/assets/js/src/components/admin/EventBlockEditorSidebar.js b/assets/js/src/components/admin/EventBlockEditorSidebar.js index 80189bd..ee92141 100644 --- a/assets/js/src/components/admin/EventBlockEditorSidebar.js +++ b/assets/js/src/components/admin/EventBlockEditorSidebar.js @@ -70,6 +70,14 @@ const EventBlockEditorSidebar = () => { editPost({ meta: { ...meta, [key]: value } }); }; + // Auto-set timezone for new events based on browser timezone + useEffect(() => { + if (isNewEvent && !meta.timezone) { + const detectedTimezone = getUserTimezone(); + updateMetaValue('timezone', detectedTimezone); + } + }, [isNewEvent, meta.timezone]); + const recurringPattern = meta.recurring_pattern || { type: 'none', interval: 1, @@ -219,7 +227,7 @@ const EventBlockEditorSidebar = () => { true, + 'single' => true, + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', + 'auth_callback' => function() { + return current_user_can('edit_posts'); + } + ]); + register_post_meta('mayo_announcement', 'priority', [ 'show_in_rest' => true, 'single' => true, diff --git a/includes/Rest/AnnouncementsController.php b/includes/Rest/AnnouncementsController.php index 0995d6f..0d8f989 100644 --- a/includes/Rest/AnnouncementsController.php +++ b/includes/Rest/AnnouncementsController.php @@ -167,8 +167,12 @@ public static function get_announcements($request) { $end_time_str = get_post_meta($post->ID, 'display_end_time', true); + // Use the announcement's own timezone if set, otherwise fall back to request timezone + $announcement_timezone = get_post_meta($post->ID, 'display_timezone', true); + $effective_timezone = !empty($announcement_timezone) ? $announcement_timezone : $timezone; + // Create end DateTime with proper time and timezone - $tz = new \DateTimeZone($timezone); + $tz = new \DateTimeZone($effective_timezone); $end_datetime = new \DateTime($end_date_str, $tz); if (!empty($end_time_str)) { $time_parts = explode(':', $end_time_str); @@ -472,14 +476,15 @@ public static function format_announcement($post, $now = null) { $display_start_time = get_post_meta($post->ID, 'display_start_time', true); $display_end_date = get_post_meta($post->ID, 'display_end_date', true); $display_end_time = get_post_meta($post->ID, 'display_end_time', true); + $display_timezone = get_post_meta($post->ID, 'display_timezone', true); // Use provided $now or fall back to current time if (!$now) { $now = new \DateTime('now', new \DateTimeZone(wp_timezone_string())); } - // Get timezone from $now for consistent comparisons - $tz = $now->getTimezone(); + // Use the announcement's own timezone if set, otherwise get timezone from $now + $tz = !empty($display_timezone) ? new \DateTimeZone($display_timezone) : $now->getTimezone(); $is_active = true; @@ -536,6 +541,7 @@ public static function format_announcement($post, $now = null) { 'display_start_time' => get_post_meta($post->ID, 'display_start_time', true), 'display_end_date' => $display_end_date, 'display_end_time' => get_post_meta($post->ID, 'display_end_time', true), + 'display_timezone' => $display_timezone, 'priority' => get_post_meta($post->ID, 'priority', true) ?: 'normal', 'service_body' => get_post_meta($post->ID, 'service_body', true) ?: '', 'is_active' => $is_active, diff --git a/tests/Unit/AnnouncementTest.php b/tests/Unit/AnnouncementTest.php index ec2e43d..d71b412 100644 --- a/tests/Unit/AnnouncementTest.php +++ b/tests/Unit/AnnouncementTest.php @@ -86,6 +86,7 @@ public function testRegisterMetaFieldsRegistersAllExpected(): void { $this->assertContains('display_end_date', $registeredMeta['mayo_announcement']); $this->assertContains('display_start_time', $registeredMeta['mayo_announcement']); $this->assertContains('display_end_time', $registeredMeta['mayo_announcement']); + $this->assertContains('display_timezone', $registeredMeta['mayo_announcement']); $this->assertContains('priority', $registeredMeta['mayo_announcement']); $this->assertContains('linked_events', $registeredMeta['mayo_announcement']); $this->assertContains('linked_event_refs', $registeredMeta['mayo_announcement']);