event = get_post( $post_id ); $this->max_attendance_limit = intval( get_post_meta( $post_id, 'gatherpress_max_attendance_limit', true ) ); } /** * Get RSVP information for a user and an event. * * This method retrieves RSVP information for a specific user and event, including the RSVP entry's ID, * associated post ID, user ID, timestamp, RSVP status ('attending', 'not_attending', or 'waiting_list'), * and the number of guests accompanying the user. If no RSVP information is found for the user and event, * default values are provided. * * @since 1.0.0 * * @param int|string $user_identifier The user ID or email address of the person whose RSVP information * is being retrieved. If an integer is provided, it's treated as a user ID. * If a string is provided, it's treated as an email address. * * @return array An array containing RSVP information. */ public function get( $user_identifier ): array { $post_id = $this->event->ID ?? 0; $rsvp_query = Rsvp_Query::get_instance(); $user_id = intval( $user_identifier ); $email = ''; if ( is_email( $user_identifier ) ) { $email = $user_identifier; } if ( 1 > $post_id || ( empty( $user_id ) && empty( $email ) ) ) { return array(); } $data = array( 'comment_id' => 0, 'post_id' => $post_id, 'user_id' => $user_id, 'timestamp' => null, 'status' => 'no_status', 'guests' => 0, 'anonymous' => 0, ); $args = array( 'post_id' => $post_id, 'status' => 'approve', ); if ( ! empty( $user_id ) ) { $args['user_id'] = $user_id; } elseif ( ! empty( $email ) ) { $args['author_email'] = $email; } $rsvp = $rsvp_query->get_rsvp( $args ); if ( ! empty( $rsvp ) ) { $data['comment_id'] = $rsvp->comment_ID; $data['user_id'] = $rsvp->user_id; $data['timestamp'] = $rsvp->comment_date; $data['anonymous'] = intval( get_comment_meta( intval( $rsvp->comment_ID ), 'gatherpress_rsvp_anonymous', true ) ); $data['guests'] = intval( get_comment_meta( intval( $rsvp->comment_ID ), 'gatherpress_rsvp_guests', true ) ); $terms = wp_get_object_terms( intval( $rsvp->comment_ID ), self::TAXONOMY ); if ( ! empty( $terms ) && is_array( $terms ) ) { $data['status'] = $terms[0]->slug; } } return $data; } /** * Saves a user's RSVP status for an event. * * Allows assigning one of the specified RSVP statuses to a user for an event. The user can be marked * as 'attending', 'not_attending', or placed on a 'waiting_list'. Additionally, users can specify * the number of guests they plan to bring along and whether their RSVP should be considered anonymous. * This method updates the database accordingly to reflect the new RSVP status. * * @since 1.0.0 * * @param int|string $user_identifier The user ID or email address of the person whose RSVP status is being updated. * If an integer is provided, it's treated as a user ID. If a string is provided, * it's treated as an email address. * @param string $status The new RSVP status for the user. Acceptable values are 'attending', * 'not_attending', or 'waiting_list'. * @param int $anonymous Optional. Whether the RSVP is to be marked as anonymous. * Accepts 1 for true (anonymous) and 0 for false (not anonymous). Default 0. * @param int $guests Optional. The number of guests the user plans to bring along. Default 0. * * @return array Associative array containing the event ID ('post_id'), user ID ('user_id'), * RSVP timestamp ('timestamp'), RSVP status ('status'), number of guests ('guests'), * and anonymity flag ('anonymous'). Returns a default array with 'post_id' and 'user_id' * set to 0, 'timestamp' to '0000-00-00 00:00:00', 'status' to 'no_status', 'guests' to 0, * and 'anonymous' to 0 if the post ID or user identifier is not valid, or if the status * is not one of the acceptable values. If the attending limit is reached, 'status' may be * automatically set to 'waiting_list', and 'guests' to 0, depending on the context. */ public function save( $user_identifier, string $status, int $anonymous = 0, int $guests = 0 ): array { $rsvp_query = Rsvp_Query::get_instance(); $max_guest_limit = intval( get_post_meta( $this->event->ID, 'gatherpress_max_guest_limit', true ) ); $user_id = intval( $user_identifier ); $email = ''; if ( is_email( $user_identifier ) ) { $email = $user_identifier; } if ( $max_guest_limit < $guests ) { $guests = $max_guest_limit; } // Check if anonymous RSVP is enabled for this event. $enable_anonymous_rsvp = get_post_meta( $this->event->ID, 'gatherpress_enable_anonymous_rsvp', true ); if ( ! $enable_anonymous_rsvp ) { $anonymous = 0; } $data = array( 'comment_id' => 0, 'post_id' => 0, 'user_id' => 0, 'timestamp' => '0000-00-00 00:00:00', 'status' => 'no_status', 'guests' => 0, 'anonymous' => 0, ); $post_id = $this->event->ID; if ( 1 > $post_id || ( empty( $user_id ) && empty( $email ) ) ) { return $data; } $args = array( 'post_id' => $post_id, ); if ( ! empty( $user_id ) ) { $args['user_id'] = $user_id; } elseif ( ! empty( $email ) ) { $args['author_email'] = $email; } $rsvp = $rsvp_query->get_rsvp( $args ); $current_response = $this->get( $user_identifier ); $limit_reached = $this->attending_limit_reached( $current_response, $guests ); if ( 'attending' === $status && $limit_reached ) { $guests = $current_response['guests']; } if ( in_array( $status, array( 'attending', 'waiting_list' ), true ) && 'attending' !== $current_response['status'] && $limit_reached ) { $status = 'waiting_list'; } if ( 'waiting_list' === $status ) { $guests = 0; } $args = array( 'comment_post_ID' => $post_id, 'comment_author_IP' => '127.0.0.1', 'comment_type' => self::COMMENT_TYPE, 'user_id' => $user_id, ); if ( intval( $user_id ) ) { $args['comment_author_url'] = get_author_posts_url( $user_id ); } if ( ! empty( $email ) ) { $args['comment_author_email'] = $email; } if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) { $remote_ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); if ( rest_is_ip_address( $remote_ip ) ) { $args['comment_author_IP'] = $remote_ip; } } if ( empty( $rsvp ) ) { $comment_id = wp_insert_comment( $args ); } else { $comment_id = $rsvp->comment_ID; $args['comment_ID'] = $comment_id; $args['comment_approved'] = 1; wp_update_comment( $args ); } if ( empty( $comment_id ) ) { return $data; } // If not attending and anonymous or status is 'no_status', remove the record. if ( ( 'not_attending' === $status && $anonymous ) || 'no_status' === $status ) { wp_delete_comment( $comment_id, true ); wp_cache_delete( sprintf( self::CACHE_KEY, $post_id ), GATHERPRESS_CACHE_GROUP ); return $data; } if ( ! in_array( $status, $this->statuses, true ) ) { return $data; } wp_set_object_terms( $comment_id, $status, self::TAXONOMY ); if ( ! empty( $guests ) ) { update_comment_meta( $comment_id, 'gatherpress_rsvp_guests', $guests ); } else { delete_comment_meta( $comment_id, 'gatherpress_rsvp_guests' ); } if ( ! empty( $anonymous ) ) { update_comment_meta( $comment_id, 'gatherpress_rsvp_anonymous', $anonymous ); } else { delete_comment_meta( $comment_id, 'gatherpress_rsvp_anonymous' ); } $data = array( 'comment_id' => intval( $comment_id ), 'post_id' => intval( $post_id ), 'user_id' => intval( $user_id ), 'timestamp' => gmdate( 'Y-m-d H:i:s' ), 'status' => sanitize_key( $status ), 'guests' => intval( $guests ), 'anonymous' => intval( $anonymous ), ); wp_cache_delete( sprintf( self::CACHE_KEY, $post_id ), GATHERPRESS_CACHE_GROUP ); if ( ! $limit_reached ) { $this->check_waiting_list(); } return $data; } /** * Check the waiting list and move response to attending if spots are available. * * This method checks if there are spots available in the attending list and moves response * from the waiting list to attending based on their timestamp. * * @since 1.0.0 * * @return int The number of responses from the waiting list that were moved to attending. */ public function check_waiting_list(): int { $responses = $this->responses(); $attending_count = intval( $responses['attending']['count'] ); $waiting_list_count = intval( $responses['waiting_list']['count'] ); $i = 0; if ( $waiting_list_count && ( empty( $this->max_attendance_limit ) || $attending_count < $this->max_attendance_limit ) ) { $waiting_list = $responses['waiting_list']['records']; // People who are longest on the waiting_list should be added first. usort( $waiting_list, array( $this, 'sort_by_timestamp' ) ); if ( ! empty( $this->max_attendance_limit ) ) { $total = $this->max_attendance_limit - intval( $responses['attending']['count'] ); } else { $total = $waiting_list_count; } while ( $i < $total ) { // Check that we have enough on the waiting_list to run this. if ( ( $i + 1 ) > intval( $responses['waiting_list']['count'] ) ) { break; } $response = $waiting_list[ $i ]; // @todo need to look into this since Open RSVP will not have a userId, // but email or commentId if we can use that. $this->save( $response['userId'], 'attending', $response['anonymous'] ); ++$i; } } return $i; } /** * Check if the attending limit has been reached for an event. * * This method determines whether the maximum response limit for the 'attending' status * has been reached for the event. It checks the current number of 'attending' responses * and compares it to the defined limit. It considers both the current response status * and the number of guests associated with that response. * * @since 1.0.0 * * @param array $current_response The current response data including status and number of guests. * Expected to have keys 'status' and 'guests', where 'status' is a * string indicating the current response status (e.g., 'attending'), * and 'guests' is an integer representing the number of guests. * @param int $guests The number of additional guests to consider in the limit calculation. * Defaults to 0. This is used to adjust the total count based on any new * guests being added as part of the current operation. * @return bool True if the 'attending' limit has been reached, false otherwise. */ public function attending_limit_reached( array $current_response, int $guests = 0 ): bool { $responses = $this->responses(); $user_count = 1; if ( empty( $this->max_attendance_limit ) ) { return false; } // If the user record was previously attending adjust numbers to figure out new limit. if ( 'attending' === $current_response['status'] ) { $guests = $guests - intval( $current_response['guests'] ); $user_count = 0; } return ( ! empty( $responses['attending'] ) && intval( $responses['attending']['count'] ) + $user_count + $guests > $this->max_attendance_limit ); } /** * Get all responses for an event. * * This method retrieves and organizes information about responses for the event. * It provides an array with response details grouped by RSVP status ('attending', 'not_attending', 'waiting_list'), * along with counts and additional response data. * * @since 1.0.0 * * @return array An array containing response information grouped by RSVP status. */ public function responses(): array { $post_id = $this->event->ID; $cache_key = sprintf( self::CACHE_KEY, $post_id ); $retval = wp_cache_get( $cache_key, GATHERPRESS_CACHE_GROUP ); $rsvp_query = Rsvp_Query::get_instance(); // @todo add testing with cache. // @codeCoverageIgnoreStart if ( ! empty( $retval ) && is_array( $retval ) ) { return $retval; } // @codeCoverageIgnoreEnd $retval = array( 'all' => array( 'records' => array(), 'count' => 0, ), ); if ( Event::POST_TYPE !== get_post_type( $post_id ) ) { return $retval; } $data = $rsvp_query->get_rsvps( array( 'post_id' => $post_id, 'status' => 'approve', ) ); $records = array(); $all_guests = 0; $statuses = $this->statuses; // `no_status` status is not relevant here. $status_key = array_search( 'no_status', $statuses, true ); unset( $statuses[ $status_key ] ); $statuses = array_values( $statuses ); foreach ( $statuses as $status ) { $retval[ $status ] = array( 'records' => array(), 'count' => 0, ); } foreach ( $data as $record ) { $comment_id = intval( $record->comment_ID ); $user_id = intval( $record->user_id ); $user_status = ''; $user_guests = intval( get_comment_meta( $record->comment_ID, 'gatherpress_rsvp_guests', true ) ); $all_guests += $user_guests; $user_info = false; $anonymous = intval( get_comment_meta( $record->comment_ID, 'gatherpress_rsvp_anonymous', true ) ); $terms = wp_get_object_terms( $record->comment_ID, self::TAXONOMY ); $display_name = $record->comment_author; $profile = ''; if ( ! empty( $terms ) && is_array( $terms ) ) { $user_status = $terms[0]->slug; } if ( ! empty( $user_id ) ) { $user_info = get_userdata( $user_id ); if ( ! empty( $user_info ) ) { // @todo make a filter so we can use this function if gatherpress-buddypress plugin is activated. // eg for BuddyPress bp_core_get_user_domain( $user_id ) $profile = get_author_posts_url( $user_id ); $display_name = $user_info->display_name; } } if ( ( $user_id && empty( $user_info ) ) || ! in_array( $user_status, $statuses, true ) ) { continue; } if ( ! current_user_can( 'edit_posts' ) && ! empty( $anonymous ) ) { $user_id = 0; $profile = ''; $display_name = __( 'Anonymous', 'gatherpress' ); } $records[] = array( 'userId' => $user_id, 'commentId' => $comment_id, 'name' => $display_name ? $display_name : __( 'Anonymous', 'gatherpress' ), 'photo' => get_avatar_url( $record ), 'profile' => $profile, 'role' => Leadership::get_instance()->get_user_role( $user_id ), 'timestamp' => sanitize_text_field( $record->comment_date ), 'status' => $user_status, 'guests' => $user_guests, 'anonymous' => $anonymous, ); } // Sort before breaking down statuses in return array. usort( $records, array( $this, 'sort_by_role' ) ); $retval['all']['records'] = $records; $retval['all']['count'] = count( $retval['all']['records'] ) + $all_guests; foreach ( $statuses as $status ) { $retval[ $status ]['records'] = array_filter( $records, static function ( $record ) use ( $status ) { return ( $status === $record['status'] ); } ); $guests = 0; foreach ( $retval[ $status ]['records'] as $record ) { $guests += intval( $record['guests'] ); } $retval[ $status ]['records'] = array_values( $retval[ $status ]['records'] ); $retval[ $status ]['count'] = count( $retval[ $status ]['records'] ) + $guests; } wp_cache_set( $cache_key, $retval, GATHERPRESS_CACHE_GROUP, 15 * MINUTE_IN_SECONDS ); return $retval; } /** * Sort responses by their role. * * This method compares two responses based on their user roles and returns * an integer (-1, 0, or 1) to determine their order in the sorted list. * * @since 1.0.0 * * @param array $first The first response to compare in the sort. * @param array $second The second response to compare in the sort. * @return int An integer indicating the sorting order: * -1 if $first should come before $second, * 0 if they have the same sorting order, * 1 if $first should come after $second. */ public function sort_by_role( array $first, array $second ): int { $roles = array_values( array_map( static function ( $role ) { return $role['labels']['singular_name']; }, Leadership::get_instance()->get_user_roles() ) ); $roles[] = __( 'Member', 'gatherpress' ); $first_role = array_search( $first['role'], $roles, true ); $second_role = array_search( $second['role'], $roles, true ); return ( $first_role > $second_role ) ? 1 : -1; } /** * Sort responses by their RSVP timestamp. * * This method compares two responses based on their RSVP timestamps and is used to sort responses * from the waiting list, with the earliest timestamp responses appearing first. * * @since 1.0.0 * * @param array $first First response to compare in the sort. * @param array $second Second response to compare in the sort. * @return int Returns a negative number if the first response's timestamp is earlier, * a positive number if the second response's timestamp is earlier, * or 0 if both are equal. */ public function sort_by_timestamp( array $first, array $second ): int { return strtotime( $first['timestamp'] ) <=> strtotime( $second['timestamp'] ); } }