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
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
<?php

/**
* A one-time migration, to be deleted once it has been run across the groups network.
*
* Check-ins used to live in a `wporg_groups_checked_in` comment meta on the RSVP. They are now GatherPress's
* own `Check_In` flag, so that the wp-admin RSVPs screen and the event page see the same thing (#2213). This
* moves the check-ins recorded before that onto the flag, and removes the old meta.
*
* The time of each check-in is not carried over: the flag has no time, and nothing ever read it.
*
* Self-contained on purpose: everything here goes when the file does, apart from the two lines that load it
* in `bootstrap.php`.
*/

namespace WordCamp\Groups\Check_Ins\Backfill;

use GatherPress\Core\Rsvp\Flag\Check_In;
use WP_CLI, WP_CLI_Command;

use function WP_CLI\Utils\format_items;

defined( 'WPINC' ) || die();

// phpcs:disable Universal.Files.SeparateFunctionsFromOO -- the command and the work it does are one unit here, so that removing the migration is removing one file.

const LEGACY_META_KEY = 'wporg_groups_checked_in';

/**
* The RSVPs on one site that still carry the old check-in meta.
*
* Takes a blog ID and builds the table names itself rather than switching, so that `scan()` can ask this of
* every site on the network from a single process.
*
* @param int|null $blog_id Defaults to the current site.
*
* @return int[]
*/
function get_legacy_check_in_ids( $blog_id = null ) {
global $wpdb;

$prefix = $wpdb->get_blog_prefix( $blog_id );

// The prefix comes from `get_blog_prefix()`.
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$comment_ids = $wpdb->get_col( $wpdb->prepare(
"SELECT DISTINCT comment_id FROM {$prefix}commentmeta WHERE meta_key = %s ORDER BY comment_id",
LEGACY_META_KEY
) );
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

return array_map( 'absint', $comment_ids );
}

if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
return;
}

/**
* WordCamp.org: move Groups check-ins onto GatherPress's check-in flag.
*/
class Command extends WP_CLI_Command {
/**
* Report which sites have check-ins left to migrate.
*
* Reads each site's `commentmeta` table directly, so it never loads a site or calls `switch_to_blog()`.
* Prints one `<url>` per site with work outstanding, so the output can be piped straight into a loop.
*
* ## OPTIONS
*
* [--verbose]
* : Also report the number of check-ins found on each site, on STDERR.
*
* ## EXAMPLES
*
* # See which sites need it.
* wp --url=events.wordpress.org wc-groups-check-ins scan --verbose
*
* # Migrate only those sites, one process each.
* wp --url=events.wordpress.org wc-groups-check-ins scan | while read -r site; do
* wp --url="$site" wc-groups-check-ins migrate
* done
*
* @param array $args
* @param array $assoc_args
*/
public function scan( $args, $assoc_args ) {
global $wpdb;

$verbose = isset( $assoc_args['verbose'] );
$sites = $wpdb->get_results( $wpdb->prepare(
"SELECT blog_id, domain, path FROM {$wpdb->blogs} WHERE site_id = %d ORDER BY blog_id",
get_current_network_id()
) );
$found = 0;
$unusable = array();

// A site whose tables have gone is a broken row, not a reason to stop part-way through a network.
$was_suppressing = $wpdb->suppress_errors( true );

foreach ( $sites as $site ) {
$comment_ids = get_legacy_check_in_ids( $site->blog_id );

// `get_col()` answers an empty array whether the site has nothing or the query failed.
if ( $wpdb->last_error ) {
$unusable[] = $site->domain . $site->path;
$wpdb->last_error = '';

continue;
}

if ( ! $comment_ids ) {
continue;
}

++$found;

WP_CLI::line( $site->domain . $site->path );

if ( $verbose ) {
WP_CLI::warning( sprintf( '%s%s (blog %d): %d check-ins.', $site->domain, $site->path, $site->blog_id, count( $comment_ids ) ) );
}
}

$wpdb->suppress_errors( $was_suppressing );

if ( $unusable ) {
WP_CLI::warning( sprintf(
'%d sites could not be read, and are not in the list above: %s.',
count( $unusable ),
implode( ', ', array_slice( $unusable, 0, 10 ) ) . ( count( $unusable ) > 10 ? ', ...' : '' )
) );
}

if ( $verbose ) {
WP_CLI::warning( sprintf( '%d of %d sites have check-ins to migrate.', $found, count( $sites ) ) );
}
}

/**
* Migrate the check-ins on one site.
*
* Flags each RSVP as checked in, then removes the old meta. Adding the flag fires
* `gatherpress_rsvp_flag_added`, which keeps the event's record of which dates use check-in in step.
* An RSVP whose flag can't be stored keeps its meta, so a re-run picks it up again.
*
* Safe to re-run: a site with nothing outstanding does one query and stops.
*
* ## OPTIONS
*
* [--dry-run]
* : Report what would change, without changing it.
*
* ## EXAMPLES
*
* wp --url=events.wordpress.org/group/narnia/ wc-groups-check-ins migrate --dry-run
* wp --url=events.wordpress.org/group/narnia/ wc-groups-check-ins migrate
*
* @param array $args
* @param array $assoc_args
*/
public function migrate( $args, $assoc_args ) {
if ( ! class_exists( Check_In::class ) ) {
WP_CLI::error( sprintf( 'GatherPress 0.36.0 or later is not active on %s.', home_url() ) );
}

$dry_run = isset( $assoc_args['dry-run'] );
$comment_ids = get_legacy_check_in_ids();
$results = array();
$unfinished = 0;

if ( ! $comment_ids ) {
WP_CLI::success( sprintf( 'No check-ins left to migrate on %s.', home_url() ) );

return;
}

foreach ( $comment_ids as $comment_id ) {
$comment = get_comment( $comment_id );
$migrated = $dry_run || ( new Check_In( $comment_id ) )->add();

if ( $migrated && ! $dry_run ) {
delete_comment_meta( $comment_id, LEGACY_META_KEY );
} elseif ( ! $migrated ) {
++$unfinished;
}

$results[] = array(
'RSVP' => $comment_id,
'Event' => $comment ? (int) $comment->comment_post_ID : '',
'Migrated' => $migrated ? 'yes' : 'no',
);
}

format_items( 'table', $results, array( 'RSVP', 'Event', 'Migrated' ) );

if ( $dry_run ) {
WP_CLI::success( sprintf( '%d check-ins would be migrated on %s. Run without --dry-run to apply.', count( $results ), home_url() ) );
} elseif ( $unfinished ) {
// Usually a comment that is no longer an RSVP: `Check_In` refuses anything else.
WP_CLI::warning( sprintf( '%d of %d check-ins on %s could not be migrated, and keep the old meta.', $unfinished, count( $results ), home_url() ) );
} else {
WP_CLI::success( sprintf( '%d check-ins migrated on %s.', count( $results ), home_url() ) );
}
}
}

WP_CLI::add_command( 'wc-groups-check-ins', __NAMESPACE__ . '\Command' );
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
// A one-time migration; remove this line and the file once it has been run across the network.
require_once __DIR__ . '/backfill-sponsor-agreements.php';

// A one-time migration; remove this line and the file once it has been run across the groups network.
require_once __DIR__ . '/backfill-groups-check-ins.php';

WP_CLI::add_command( 'wc-dangling', 'WordCamp_CLI_Dangling_Hosts' );
WP_CLI::add_command( 'wc-misc', 'WordCamp_CLI_Miscellaneous' );
WP_CLI::add_command( 'wc-rewrite', 'WordCamp_CLI_Rewrite_Rules' );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@
*
* Storage:
*
* - **Check-ins** live in one `wporg_groups_checked_in` comment meta on the
* GatherPress RSVP comment, holding the GMT time of the check-in. Like the
* registration answers in `rsvp-questions.php`, keeping it on the comment
* means it goes away with the RSVP it belongs to.
* - **Check-ins** are GatherPress's own `Check_In` flag on the RSVP
* comment, so the wp-admin RSVPs screen and this page show and change
* the same thing (#2213). GatherPress drops the flag with the RSVP.
* - **Which dates use check-in** lives in one `wporg_groups_check_in_scopes`
* post meta on the event: the recurrence ids (or `''` for a plain event)
* with at least one check-in. Consumers deciding what "attended" means
Expand Down Expand Up @@ -54,6 +53,8 @@
defined( 'WPINC' ) || die();

use GatherPress\Core\Event\Event;
use GatherPress\Core\Rsvp\Flag\Check_In as Check_In_Flag;
use GatherPress\Core\Rsvp\Query;
use GatherPress\Core\Rsvp\Rsvp;
use WP_Comment;
use WP_Error;
Expand All @@ -67,9 +68,8 @@
use function WordCamp\Groups\Frontend\My_Events\get_comment_recurrence_ids;
use function WordCamp\Groups\Frontend\REST\current_user_can_edit_event;

const CHECKED_IN_META = 'wporg_groups_checked_in';
const SCOPES_META = 'wporg_groups_check_in_scopes';
const WALK_INS_META = 'wporg_groups_walk_ins_without_account';
const SCOPES_META = 'wporg_groups_check_in_scopes';
const WALK_INS_META = 'wporg_groups_walk_ins_without_account';

/**
* Upper bound for walk-ins without an account on one date. Far above any
Expand All @@ -89,6 +89,27 @@
*/
function bootstrap(): void {
add_action( 'rest_api_init', __NAMESPACE__ . '\register_routes' );
add_action( 'gatherpress_rsvp_flag_added', __NAMESPACE__ . '\on_flag_changed', 10, 2 );
add_action( 'gatherpress_rsvp_flag_removed', __NAMESPACE__ . '\on_flag_changed', 10, 2 );
}

/**
* Keep the event's record of which dates use check-in in step with every
* check-in, including the ones made from the wp-admin RSVPs screen.
*
* @param int $comment_id RSVP comment ID.
* @param string $flag Flag slug.
*/
function on_flag_changed( int $comment_id, string $flag ): void {
$comment = get_comment( $comment_id );

if ( Check_In_Flag::SLUG !== $flag || ! $comment instanceof WP_Comment ) {
return;
}

$scope = get_comment_recurrence_ids( array( $comment_id ) )[ $comment_id ] ?? '';

sync_scope( (int) $comment->comment_post_ID, $scope );
}

/**
Expand Down Expand Up @@ -212,31 +233,38 @@ function is_open( Event $event ): bool {
* @param int $comment_id RSVP comment ID.
*/
function is_checked_in( int $comment_id ): bool {
return '' !== (string) get_comment_meta( $comment_id, CHECKED_IN_META, true );
return ( new Check_In_Flag( $comment_id ) )->has();
}

/**
* Check an RSVP in, or undo it, and keep the event's record of which dates
* use check-in in step.
* Load the check-ins of many RSVPs in one query, ahead of calling
* `is_checked_in()` on each.
*
* @param int[] $comment_ids RSVP comment IDs.
*/
function prime_check_ins( array $comment_ids ): void {
$comment_ids = array_filter( array_map( 'intval', $comment_ids ) );

if ( $comment_ids ) {
update_object_term_cache( $comment_ids, 'comment' );
}
}

/**
* Check an RSVP in, or undo it. `on_flag_changed()` keeps the event's record
* of which dates use check-in in step.
*
* @param WP_Comment $comment RSVP comment.
* @param bool $checked_in Whether they came.
*/
function set_checked_in( WP_Comment $comment, bool $checked_in ): void {
$comment_id = (int) $comment->comment_ID;
$flag = new Check_In_Flag( (int) $comment->comment_ID );

if ( $checked_in ) {
// Keep the original time when someone is checked in twice.
if ( ! is_checked_in( $comment_id ) ) {
update_comment_meta( $comment_id, CHECKED_IN_META, current_time( 'mysql', true ) );
}
$flag->add();
} else {
delete_comment_meta( $comment_id, CHECKED_IN_META );
$flag->remove();
}

$scope = get_comment_recurrence_ids( array( $comment_id ) )[ $comment_id ] ?? '';

sync_scope( (int) $comment->comment_post_ID, $scope );
}

/**
Expand All @@ -249,17 +277,28 @@ function set_checked_in( WP_Comment $comment, bool $checked_in ): void {
* @param string $scope Recurrence ID, or `''` for a plain event.
*/
function sync_scope( int $event_id, string $scope ): void {
$checked_in_ids = get_comments(
array(
'post_id' => $event_id,
'type' => Rsvp::COMMENT_TYPE,
'status' => 'approve',
'meta_key' => CHECKED_IN_META, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- One event's RSVPs.
'fields' => 'ids',
/*
* Filtered here rather than with a `tax_query`: `WP_Comment_Query` keys
* its cache on its own query vars only, so a `tax_query` would share a
* cache entry with the same query without it, and GatherPress's list of
* the event's RSVPs would come back holding only the checked-in ones.
*/
$rsvp_ids = array_map(
'intval',
Query::get_instance()->get_rsvps(
array(
'post_id' => $event_id,
'status' => 'approve',
'fields' => 'ids',
)
)
);

$recurrence_ids = get_comment_recurrence_ids( array_map( 'intval', $checked_in_ids ) );
prime_check_ins( $rsvp_ids );

$checked_in_ids = array_values( array_filter( $rsvp_ids, __NAMESPACE__ . '\is_checked_in' ) );

$recurrence_ids = get_comment_recurrence_ids( $checked_in_ids );
$has_check_ins = false;

foreach ( $checked_in_ids as $comment_id ) {
Expand Down Expand Up @@ -464,10 +503,7 @@ function build_list( Event $event, int $event_id ): array {
$responses['waiting_list']['records'] ?? array()
);

$comment_ids = array_filter( array_map( 'intval', wp_list_pluck( $records, 'commentId' ) ) );
if ( $comment_ids ) {
update_meta_cache( 'comment', $comment_ids );
}
prime_check_ins( wp_list_pluck( $records, 'commentId' ) );

$user_ids = array_filter( array_map( 'intval', wp_list_pluck( $records, 'userId' ) ) );
if ( $user_ids ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,7 @@ function get_event_rsvps( array $event_ids ) {

$comment_ids = array_map( 'intval', wp_list_pluck( $comments, 'comment_ID' ) );
update_meta_cache( 'comment', $comment_ids );
Check_In\prime_check_ins( $comment_ids );

$status_terms = wp_get_object_terms(
$comment_ids,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ function filter_to_attended( array $candidates ): array {
return array();
}

update_meta_cache( 'comment', array_column( $candidates, 'comment_id' ) );
Check_In\prime_check_ins( array_column( $candidates, 'comment_id' ) );

return array_values(
array_filter(
Expand Down
Loading
Loading