Skip to content

Commit

Permalink
Filter Events: Add block to filter and display events
Browse files Browse the repository at this point in the history
  • Loading branch information
iandunn committed Oct 20, 2023
1 parent b4ca520 commit a895fa0
Show file tree
Hide file tree
Showing 5 changed files with 323 additions and 0 deletions.
33 changes: 33 additions & 0 deletions mu-plugins/blocks/google-map-event-filters/README.md
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.
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 );
}
33 changes: 33 additions & 0 deletions mu-plugins/blocks/google-map-event-filters/src/block.json
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"
}
27 changes: 27 additions & 0 deletions mu-plugins/blocks/google-map-event-filters/src/index.js
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,
} );
1 change: 1 addition & 0 deletions mu-plugins/loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
require_once __DIR__ . '/blocks/local-navigation-bar/index.php';
require_once __DIR__ . '/blocks/latest-news/latest-news.php';
require_once __DIR__ . '/blocks/link-wrapper/index.php';
require_once __DIR__ . '/blocks/google-map-event-filters/google-map-event-filters.php';
require_once __DIR__ . '/blocks/navigation/index.php';
require_once __DIR__ . '/blocks/notice/index.php';
require_once __DIR__ . '/blocks/query-filter/index.php';
Expand Down

0 comments on commit a895fa0

Please sign in to comment.