Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter Events: Add block to filter and display events #486

Merged
merged 1 commit into from
Oct 20, 2023
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
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
Loading