-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Filter Events: Add block to filter and display events
- Loading branch information
Showing
5 changed files
with
323 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# Google Map Event Filters | ||
|
||
This plugins creates the `wporg/google-map-event-filters` block, which displays a map and list of events that match a given filter during a given timeframe. Filters can be setup for anything, but some common examples are watch parties for WP anniversaries and the State of the Word. | ||
|
||
It uses the `wporg/google-map` block to display a searchable list and/or map of the selected events. | ||
|
||
|
||
## Usage | ||
|
||
1. Setup the API key needed for the `wporg/google-maps` block. See its README for details. | ||
1. Add a new filter to `filter_potential_events()` if you're not using an existing one. | ||
1. Add the following to a pattern in your theme. `googleMapBlockAttributes` are the attributes that will be passed to the `wporg/google-map` block, see it's README for details. | ||
|
||
```php | ||
$filter_options = array( | ||
'filterSlug' => 'wp20', | ||
'startDate' => 'April 21, 2023', | ||
'endDate' => 'May 30, 2023', | ||
|
||
// This has to be an object, see `WordPressdotorg\MU_Plugins\Google_Map_Event_Filters\render()`. | ||
'googleMapBlockAttributes' => (object) array( | ||
'id' => 'wp20', | ||
'apiKey' => 'WORDCAMP_DEV_GOOGLE_MAPS_API_KEY', | ||
), | ||
); | ||
|
||
?> | ||
|
||
<!-- wp:wporg/google-map-event-filters <?php echo wp_json_encode( $filter_options ); ?> /--> | ||
``` | ||
|
||
1. View the page where the block is used. That will create the cron job that updates the data automatically in the future. | ||
1. Run `wp cron event run prime_event_filters` to test the filtering. Look at each title, and add any false positives to `$false_positives` in `filter_potential_events()`. If any events that should be included were ignored, add a keyword from the title to `$keywords`. Run the command after those changes and make sure it's correct now. |
229 changes: 229 additions & 0 deletions
229
mu-plugins/blocks/google-map-event-filters/google-map-event-filters.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
<?php | ||
|
||
namespace WordPressdotorg\MU_Plugins\Google_Map_Event_Filters; | ||
use WP_Block; | ||
|
||
defined( 'WPINC' ) || die(); | ||
|
||
add_action( 'init', __NAMESPACE__ . '\init' ); | ||
add_action( 'prime_event_filters', __NAMESPACE__ . '\get_events', 10, 4 ); | ||
|
||
|
||
/** | ||
* Registers the block from `block.json`. | ||
*/ | ||
function init() { | ||
register_block_type( | ||
__DIR__ . '/build', | ||
array( | ||
'render_callback' => __NAMESPACE__ . '\render', | ||
) | ||
); | ||
} | ||
|
||
/** | ||
* Render the block content. | ||
*/ | ||
function render( array $attributes, string $content, WP_Block $block ): string { | ||
$attributes['startDate'] = strtotime( $attributes['startDate'] ); | ||
$attributes['endDate'] = strtotime( $attributes['endDate'] ); | ||
$wrapper_attributes = get_block_wrapper_attributes( array( 'id' => 'wp-block-wporg-google-map-event-filters-' . $attributes['filterSlug'] ) ); | ||
|
||
// The REST API doesn't support associative arrays, so this had to be defined as an object in this block. It | ||
// needs to be an array when passed to the Google Map block though. | ||
// See `rest_is_array()`. | ||
$map_options = (array) $attributes['googleMapBlockAttributes']; | ||
$map_options['markers'] = get_events( $attributes['filterSlug'], $attributes['startDate'], $attributes['endDate'] ); | ||
|
||
// This has to be called in `render()` to know which slug/dates to use. | ||
$cron_args = array( $attributes['filterSlug'], $attributes['startDate'], $attributes['endDate'], true ); | ||
|
||
if ( ! wp_next_scheduled( 'prime_event_filters', $cron_args ) ) { | ||
wp_schedule_event( | ||
time() + HOUR_IN_SECONDS, | ||
'hourly', | ||
'prime_event_filters', | ||
$cron_args | ||
); | ||
} | ||
|
||
ob_start(); | ||
|
||
?> | ||
|
||
<div <?php echo wp_kses_data( $wrapper_attributes ); ?>> | ||
<!-- wp:wporg/google-map <?php echo wp_json_encode( $map_options ); ?> /--> | ||
</div> | ||
|
||
<?php | ||
|
||
return do_blocks( ob_get_clean() ); | ||
} | ||
|
||
/** | ||
* Get events matching the provider filter during the given timeframe. | ||
*/ | ||
function get_events( string $filter_slug, int $start_timestamp, int $end_timestamp, bool $force_refresh = false ) : array { | ||
$events = array(); | ||
$cache_key = 'google-map-event-filters-' . md5( wp_json_encode( func_get_args() ) ); | ||
$cached_events = get_transient( $cache_key ); | ||
|
||
if ( $cached_events && ! $force_refresh ) { | ||
$events = $cached_events; | ||
} else { | ||
$potential_matches = get_events_between_dates( $start_timestamp, $end_timestamp ); | ||
$filtered_events = filter_potential_events( $filter_slug, $potential_matches ); | ||
$events = $filtered_events; | ||
|
||
// Store for a day to make sure it never expires before the priming cron job runs. | ||
set_transient( $cache_key, $filtered_events, DAY_IN_SECONDS ); | ||
} | ||
|
||
return $events; | ||
} | ||
|
||
/** | ||
* Get a list of all events during a given timeframe. | ||
*/ | ||
function get_events_between_dates( int $start_timestamp, int $end_timestamp ) : array { | ||
global $wpdb; | ||
|
||
$query = $wpdb->prepare( ' | ||
SELECT | ||
id, `type`, source_id, title, url, description, meetup, location, latitude, longitude, date_utc, | ||
date_utc_offset AS tz_offset | ||
FROM `wporg_events` | ||
WHERE | ||
status = "scheduled" AND | ||
date_utc BETWEEN FROM_UNIXTIME( %d ) AND FROM_UNIXTIME( %d ) | ||
ORDER BY date_utc ASC | ||
LIMIT 1000', | ||
$start_timestamp, | ||
$end_timestamp | ||
); | ||
|
||
if ( 'latin1' === DB_CHARSET ) { | ||
$events = $wpdb->get_results( $query ); | ||
} else { | ||
$events = get_latin1_results_with_prepared_query( $query ); | ||
} | ||
|
||
foreach ( $events as $event ) { | ||
// `capital_P_dangit()` won't work here because the current filter isn't `the_title` and there isn't a safelisted prefix before `$text`. | ||
$event->title = str_replace( 'Wordpress', 'WordPress', $event->title ); | ||
|
||
// `date_utc` is a misnomer, the value is actually in the local timezone of the event. So, convert to a true Unix timestamp (UTC). | ||
// Can't do this reliably in the query because MySQL converts it to the server timezone. | ||
$event->timestamp = strtotime( $event->date_utc ) - $event->tz_offset; | ||
|
||
unset( $event->date_utc ); | ||
} | ||
|
||
return $events; | ||
} | ||
|
||
/** | ||
* Query a table that's encoded with the `latin1` charset. | ||
* | ||
* wordpress.org uses a `DB_CHARSET` of `latin1` for legacy reasons, but wordcamp.org and others use `utf8mb4`. | ||
* `wporg_events` uses `latin1`, so querying it with `utf8mb4` will produce Mojibake. | ||
* | ||
* @param string $prepared_query ⚠️ This must have already be ran through `$wpdb->prepare()` if needed. | ||
* | ||
* @return object|null | ||
*/ | ||
function get_latin1_results_with_prepared_query( string $prepared_query ) { | ||
global $wpdb; | ||
|
||
// Local environments don't always use HyperDB, but production does. | ||
$db_handle = is_a( $wpdb, 'hyperdb' ) ? $wpdb->db_connect( $prepared_query ) : $wpdb->dbh; | ||
$wpdb->set_charset( $db_handle, 'latin1', 'latin1_swedish_ci' ); | ||
|
||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This function doesn't have the context to prepare it, the caller must. | ||
$results = $wpdb->get_results( $prepared_query ); | ||
|
||
// Revert to the default charset to avoid affecting other queries. | ||
$wpdb->set_charset( $db_handle, DB_CHARSET, DB_COLLATE ); | ||
|
||
return $results; | ||
} | ||
|
||
/** | ||
* Extract the desired events from an array of potential events. | ||
*/ | ||
function filter_potential_events( string $filter_slug, array $potential_events ) : array { | ||
$matched_events = array(); | ||
$other_events = array(); | ||
|
||
switch ( $filter_slug ) { | ||
case 'sotw': | ||
$false_positives = array(); | ||
$keywords = array( | ||
'sotw', 'state of the word', | ||
); | ||
break; | ||
|
||
case 'wp20': | ||
$false_positives = array( "292525625", "293437294" ); | ||
$keywords = array( | ||
'wp20', '20 year', '20 ano', '20 año', '20 candeline', '20 jaar', 'wordt 20', '20 yaşında', | ||
'anniversary', 'aniversário', 'aniversario', 'birthday', 'cumpleaños', 'celebrate', | ||
'Tanti auguri', | ||
); | ||
break; | ||
|
||
default: | ||
return array(); | ||
} | ||
|
||
foreach ( $potential_events as $event ) { | ||
$match = false; | ||
|
||
// Have to use `source_id` because `id` is rotated by `REPLACE INTO` when table is updated. | ||
if ( in_array( $event->source_id, $false_positives, true ) ) { | ||
$other_events[] = $event; | ||
continue; | ||
} | ||
|
||
foreach ( $keywords as $keyword ) { | ||
if ( false !== stripos( $event->description, $keyword ) || false !== stripos( $event->title, $keyword ) ) { | ||
// These are no longer needed, so remove it to save space in the database. | ||
unset( $event->description ); | ||
unset( $event->source_id ); | ||
$matched_events[] = $event; | ||
continue 2; | ||
} | ||
} | ||
|
||
if ( ! $match ) { | ||
$other_events[] = $event; | ||
} | ||
} | ||
|
||
print_results( $matched_events, $other_events ); | ||
|
||
return $matched_events; | ||
} | ||
|
||
/** | ||
* Print the matched/unmatched events for manual review. | ||
* | ||
* Run `wp cron event run prime_event_filters` to see this. | ||
*/ | ||
function print_results( array $matched_events, array $other_events ) : void { | ||
if ( 'cli' !== php_sapi_name() ) { | ||
return; | ||
} | ||
|
||
$matched_names = wp_list_pluck( $matched_events, 'title' ); | ||
$other_names = wp_list_pluck( $other_events, 'title' ); | ||
|
||
sort( $matched_names ); | ||
sort( $other_names ); | ||
|
||
echo "\nIgnored these events. Double check for false-negatives.\n\n"; | ||
print_r( $other_names ); | ||
|
||
echo "\Included these events. Double check for false-positives.\n\n"; | ||
print_r( $matched_names ); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{ | ||
"$schema": "https://schemas.wp.org/trunk/block.json", | ||
"apiVersion": 2, | ||
"name": "wporg/google-map-event-filters", | ||
"title": "Event Filters for the Google Map block", | ||
"icon": "nametag", | ||
"category": "widgets", | ||
"description": "Displays a map and/or list of a subset of events.", | ||
"textdomain": "wporg", | ||
"attributes": { | ||
"filterSlug": { | ||
"type": "string", | ||
"default": "" | ||
}, | ||
"startDate": { | ||
"type": "string", | ||
"default": "" | ||
}, | ||
"endDate": { | ||
"type": "string", | ||
"default": "" | ||
}, | ||
"googleMapBlockAttributes": { | ||
"type": "object", | ||
"default": [] | ||
} | ||
}, | ||
"supports": { | ||
"inserter": false | ||
}, | ||
"editorScript": "file:./index.js", | ||
"style": "file:./style.css" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { __ } from '@wordpress/i18n'; | ||
import { registerBlockType } from '@wordpress/blocks'; | ||
import { Placeholder } from '@wordpress/components'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import metadata from './block.json'; | ||
|
||
function Edit() { | ||
return ( | ||
<Placeholder | ||
instructions={ __( | ||
'This is a placeholder for the editor. Data is supplied to this block via the pattern that includes it.', | ||
'wporg' | ||
) } | ||
label={ __( 'Google Map Event Filters', 'wporg' ) } | ||
/> | ||
); | ||
} | ||
|
||
registerBlockType( metadata.name, { | ||
edit: Edit, | ||
} ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters