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']);