Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions data-machine-events.php
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,11 @@ private function load_data_machine_components() {
new \DataMachineEvents\Abilities\UpcomingCountAbilities();
}

if ( file_exists( DATA_MACHINE_EVENTS_PLUGIN_DIR . 'inc/Abilities/EventDateQueryAbilities.php' ) ) {
require_once DATA_MACHINE_EVENTS_PLUGIN_DIR . 'inc/Abilities/EventDateQueryAbilities.php';
new \DataMachineEvents\Abilities\EventDateQueryAbilities();
}

$this->registerSystemHealthChecks();
}

Expand Down
293 changes: 283 additions & 10 deletions inc/Abilities/CalendarAbilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@

namespace DataMachineEvents\Abilities;

use WP_Query;
use DataMachineEvents\Blocks\Calendar\Query\EventQueryBuilder;
use DateTime;
use DataMachineEvents\Blocks\Calendar\Query\ScopeResolver;
use DataMachineEvents\Blocks\Calendar\Data\EventHydrator;
use DataMachineEvents\Blocks\Calendar\Grouping\DateGrouper;
use DataMachineEvents\Blocks\Calendar\Display\EventRenderer;
use DataMachineEvents\Blocks\Calendar\Pagination;
use DataMachineEvents\Blocks\Calendar\Pagination\PageBoundary;
use DataMachineEvents\Blocks\Calendar\Cache\CalendarCache;
use DataMachineEvents\Blocks\Calendar\Template_Loader;
use DataMachineEvents\Core\EventDatesTable;

if ( ! defined( 'ABSPATH' ) ) {
exit;
Expand Down Expand Up @@ -207,7 +208,7 @@ public function executeGetCalendarPage( array $input ): array {
'geo_radius_unit' => $input['geo_radius_unit'] ?? 'mi',
);

$date_data = PageBoundary::get_unique_event_dates( $base_params );
$date_data = self::get_unique_event_dates( $base_params );
$unique_dates = $date_data['dates'];
$total_event_count = $date_data['total_events'];
$events_per_date = $date_data['events_per_date'];
Expand Down Expand Up @@ -268,13 +269,67 @@ function ( $d ) use ( $range_start, $range_end ) {
}
}

$built = EventQueryBuilder::build_query_args( $query_params );
$events_query = new WP_Query( $built['args'] );
$built['cleanup']();
// Build ability input from query_params.
$ability_input = array(
'scope' => $query_params['show_past'] ? 'past' : 'upcoming',
'tax_filters' => $query_params['tax_filters'],
'search' => $query_params['search_query'],
'order' => $query_params['show_past'] ? 'DESC' : 'ASC',
);

// Date range overrides scope.
if ( ! empty( $query_params['date_start'] ) || ! empty( $query_params['date_end'] ) ) {
$ability_input['date_start'] = $query_params['date_start'];
$ability_input['date_end'] = $query_params['date_end'];
$ability_input['time_start'] = $query_params['time_start'] ?? '';
$ability_input['time_end'] = $query_params['time_end'] ?? '';
// When user provides explicit dates, don't add scope filter.
if ( $query_params['user_date_range'] ) {
$ability_input['scope'] = 'all';
}
}

// Taxonomy archive constraint.
if ( ! empty( $query_params['archive_taxonomy'] ) && ! empty( $query_params['archive_term_id'] ) ) {
$ability_input['tax_filters'][ $query_params['archive_taxonomy'] ] = array( (int) $query_params['archive_term_id'] );
}

// Apply calendar_base_query filter.
$tax_query_override = apply_filters(
'data_machine_events_calendar_base_query',
null,
array(
'archive_taxonomy' => $query_params['archive_taxonomy'],
'archive_term_id' => $query_params['archive_term_id'],
'source' => 'ability',
)
);
if ( $tax_query_override ) {
foreach ( $tax_query_override as $clause ) {
if ( isset( $clause['taxonomy'] ) && isset( $clause['terms'] ) ) {
$ability_input['tax_filters'][ $clause['taxonomy'] ] = (array) $clause['terms'];
}
}
}

// Geo.
if ( ! empty( $query_params['geo_lat'] ) && ! empty( $query_params['geo_lng'] ) ) {
$ability_input['geo'] = array(
'lat' => (float) $query_params['geo_lat'],
'lng' => (float) $query_params['geo_lng'],
'radius' => (float) ( $query_params['geo_radius'] ?? 25 ),
'unit' => $query_params['geo_radius_unit'] ?? 'mi',
);
}

$event_date_query = new \DataMachineEvents\Abilities\EventDateQueryAbilities();
$query_result = $event_date_query->executeQueryEvents( $ability_input );

$event_counts = EventQueryBuilder::get_event_counts();
$event_counts = self::compute_event_counts_via_ability();

$paged_events = DateGrouper::build_paged_events( $events_query );
// Build paged_events from ability result posts (replaces DateGrouper::build_paged_events
// which requires a WP_Query object — we have raw WP_Post objects from the ability).
$paged_events = self::build_paged_events_from_posts( $query_result['posts'] );
$paged_date_groups = DateGrouper::group_events_by_date(
$paged_events,
$show_past,
Expand All @@ -293,7 +348,7 @@ function ( $d ) use ( $range_start, $range_end ) {
'current_page' => $current_page,
'max_pages' => $max_pages,
'total_event_count' => $total_event_count,
'event_count' => $events_query->post_count,
'event_count' => $query_result['post_count'],
'date_boundaries' => array(
'start_date' => $date_boundaries['start_date'],
'end_date' => $date_boundaries['end_date'],
Expand All @@ -315,7 +370,7 @@ function ( $d ) use ( $range_start, $range_end ) {
$max_pages,
$show_past,
$date_boundaries,
$events_query->post_count,
$query_result['post_count'],
$total_event_count,
$event_counts,
$deferred_dates,
Expand All @@ -328,6 +383,66 @@ function ( $d ) use ( $range_start, $range_end ) {
return $result;
}

/**
* Build paged events array from raw WP_Post objects.
*
* Mirrors DateGrouper::build_paged_events() but operates on a plain
* array of WP_Post objects instead of requiring a WP_Query instance.
*
* @param array $posts Array of WP_Post objects.
* @return array Array of event items with post, datetime, and event_data.
*/
private static function build_paged_events_from_posts( array $posts ): array {
$paged_events = array();

foreach ( $posts as $event_post ) {
$event_data = EventHydrator::parse_event_data( $event_post );

if ( $event_data ) {
$start_time = $event_data['startTime'] ?? '00:00:00';
$event_tz = DateGrouper::get_event_timezone( $event_data );
$event_datetime = new DateTime(
$event_data['startDate'] . ' ' . $start_time,
$event_tz
);

$paged_events[] = array(
'post' => $event_post,
'datetime' => $event_datetime,
'event_data' => $event_data,
);
}
}

return $paged_events;
}

/**
* Compute past/future event counts via the query-events ability (cached).
*
* @return array ['past' => int, 'future' => int]
*/
private static function compute_event_counts_via_ability(): array {
$cache_key = 'data-machine_cal_counts';
$cached = get_transient( $cache_key );
if ( false !== $cached ) {
return $cached;
}

$ability = new \DataMachineEvents\Abilities\EventDateQueryAbilities();

$future = $ability->executeQueryEvents( array( 'scope' => 'upcoming', 'fields' => 'count' ) );
$past = $ability->executeQueryEvents( array( 'scope' => 'past', 'fields' => 'count' ) );

$result = array(
'past' => $past['total'],
'future' => $future['total'],
);

set_transient( $cache_key, $result, 10 * MINUTE_IN_SECONDS );
return $result;
}

/**
* Serialize date groups for JSON output
*
Expand Down Expand Up @@ -422,4 +537,162 @@ private function renderHtml(
'navigation' => $navigation_html,
);
}

/**
* Get unique event dates for pagination calculations (cached).
*
* Multi-day events are expanded to count on each spanned date.
*
* @param array $params Query parameters.
* @return array {
* @type array $dates Ordered array of unique date strings (Y-m-d).
* @type int $total_events Total number of matching events.
* @type array $events_per_date Event counts keyed by date.
* }
*/
private static function get_unique_event_dates( array $params ): array {
$cache_key = CalendarCache::generate_key( $params, 'dates' );
$cached = CalendarCache::get( $cache_key );

if ( false !== $cached ) {
return $cached;
}

$result = self::compute_unique_event_dates( $params );

CalendarCache::set( $cache_key, $result, CalendarCache::TTL_DATES );

return $result;
}

/**
* Compute unique event dates (uncached).
*
* Fetches start/end dates (without post IDs) and uses DATE() in SQL
* to minimize data transfer. Multi-day events are properly expanded
* to count on each spanned date.
*
* @param array $params Query parameters.
* @return array Event dates data.
*/
private static function compute_unique_event_dates( array $params ): array {
global $wpdb;

$show_past_param = $params['show_past'] ?? false;
$current_date = current_time( 'Y-m-d' );
$ed_table = EventDatesTable::table_name();

// Build WHERE clauses from params for taxonomy/location filtering.
$where_clauses = array(
"p.post_type = 'data_machine_events'",
"p.post_status = 'publish'",
);
$join_clauses = array();
$query_values = array();

if ( ! $show_past_param ) {
$where_clauses[] = 'ed.start_datetime >= %s';
$query_values[] = $current_date . ' 00:00:00';
}

// Handle taxonomy archive filter (any taxonomy: artist, venue, location, etc.).
$archive_taxonomy = $params['archive_taxonomy'] ?? '';
$archive_term_id = $params['archive_term_id'] ?? 0;

if ( $archive_taxonomy && $archive_term_id ) {
$join_clauses[] = "INNER JOIN {$wpdb->term_relationships} tr_archive ON p.ID = tr_archive.object_id";
$join_clauses[] = "INNER JOIN {$wpdb->term_taxonomy} tt_archive ON tr_archive.term_taxonomy_id = tt_archive.term_taxonomy_id";
$where_clauses[] = 'tt_archive.taxonomy = %s';
$query_values[] = $archive_taxonomy;
$where_clauses[] = 'tt_archive.term_id = %d';
$query_values[] = (int) $archive_term_id;
}

// Handle additional taxonomy filters from the filter bar.
$tax_filters = $params['tax_filters'] ?? array();
$filter_index = 0;
foreach ( $tax_filters as $taxonomy_slug => $term_ids ) {
if ( empty( $term_ids ) || ! is_array( $term_ids ) ) {
continue;
}

$alias_tr = 'tr_filter_' . $filter_index;
$alias_tt = 'tt_filter_' . $filter_index;

$join_clauses[] = "INNER JOIN {$wpdb->term_relationships} {$alias_tr} ON p.ID = {$alias_tr}.object_id";
$join_clauses[] = "INNER JOIN {$wpdb->term_taxonomy} {$alias_tt} ON {$alias_tr}.term_taxonomy_id = {$alias_tt}.term_taxonomy_id";
$where_clauses[] = "{$alias_tt}.taxonomy = %s";
$query_values[] = sanitize_key( $taxonomy_slug );

$placeholders = implode( ', ', array_fill( 0, count( $term_ids ), '%d' ) );
$where_clauses[] = "{$alias_tt}.term_id IN ({$placeholders})";
foreach ( $term_ids as $term_id ) {
$query_values[] = (int) $term_id;
}

++$filter_index;
}

$joins = implode( ' ', $join_clauses );
$where = implode( ' AND ', $where_clauses );

// Fetch start/end dates without IDs — DATE() in SQL avoids gmdate() in PHP.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$rows = $wpdb->get_results(
empty( $query_values )
? "SELECT DATE(ed.start_datetime) AS start_date, DATE(ed.end_datetime) AS end_date
FROM {$wpdb->posts} p
INNER JOIN {$ed_table} ed ON p.ID = ed.post_id
{$joins}
WHERE {$where}
ORDER BY ed.start_datetime ASC"
: $wpdb->prepare(
"SELECT DATE(ed.start_datetime) AS start_date, DATE(ed.end_datetime) AS end_date
FROM {$wpdb->posts} p
INNER JOIN {$ed_table} ed ON p.ID = ed.post_id
{$joins}
WHERE {$where}
ORDER BY ed.start_datetime ASC",
...$query_values
)
);

$total_events = count( $rows );
$events_per_date = array();

foreach ( $rows as $row ) {
$events_per_date[ $row->start_date ] = ( $events_per_date[ $row->start_date ] ?? 0 ) + 1;

// Multi-day: expand to each spanned date after the start.
if ( $row->end_date && $row->end_date > $row->start_date ) {
$current = new \DateTime( $row->start_date );
$current->modify( '+1 day' );
$end_dt = new \DateTime( $row->end_date );

while ( $current <= $end_dt ) {
$date = $current->format( 'Y-m-d' );

if ( ! $show_past_param && $date < $current_date ) {
$current->modify( '+1 day' );
continue;
}

$events_per_date[ $date ] = ( $events_per_date[ $date ] ?? 0 ) + 1;
$current->modify( '+1 day' );
}
}
}

if ( $show_past_param ) {
krsort( $events_per_date );
} else {
ksort( $events_per_date );
}

return array(
'dates' => array_keys( $events_per_date ),
'total_events' => $total_events,
'events_per_date' => $events_per_date,
);
}
}
Loading
Loading