/** * The leaderboard template loop. * * Doesn't use WP_Query, but the template loop and its data are structured in a vaguely similar * way to the dpa_has_achievements() and dpa_has_progress() loops (which do use WP_Query). * * @param array $args Optional. Associative array of optional arguments. See function for details. * @return bool Returns true if the query had any results to loop over * @since Achievements (3.4) */ function dpa_has_leaderboard($args = array()) { // If multisite and running network-wide, switch_to_blog to the data store site if (is_multisite() && dpa_is_running_networkwide()) { switch_to_blog(DPA_DATA_STORE); } $defaults = array('paged' => dpa_get_leaderboard_paged(), 'posts_per_page' => dpa_get_leaderboard_items_per_page(), 'user_ids' => array()); $args = dpa_parse_args($args, $defaults, 'has_leaderboard'); // Run the query achievements()->leaderboard_query = dpa_get_leaderboard($args); // Only add pagination if query returned results if ((count(achievements()->leaderboard_query['results']) || achievements()->leaderboard_query['total']) && $args['posts_per_page']) { // If a top-level /leaderboard/ rewrite is ever added, we can make this use pretty pagination. Also see dpa_get_leaderboard_paged(). $base = add_query_arg('leaderboard-page', '%#%'); // Pagination settings with filter $leaderboard_pagination = apply_filters('dpa_leaderboard_pagination', array('base' => $base, 'current' => $args['paged'], 'format' => '', 'mid_size' => 1, 'next_text' => is_rtl() ? '←' : '→', 'prev_text' => is_rtl() ? '→' : '←', 'total' => (int) $args['posts_per_page'] === achievements()->leaderboard_query['total'] ? 1 : ceil(achievements()->leaderboard_query['total'] / (int) $args['posts_per_page']))); achievements()->leaderboard_query['paged'] = (int) $args['paged']; achievements()->leaderboard_query['pagination_links'] = paginate_links($leaderboard_pagination); achievements()->leaderboard_query['posts_per_page'] = (int) $args['posts_per_page']; } // If multisite and running network-wide, undo the switch_to_blog if (is_multisite() && dpa_is_running_networkwide()) { restore_current_blog(); } return apply_filters('dpa_has_leaderboard', !empty(achievements()->leaderboard_query['results'])); }
/** * Get the current state of the leaderboard, sorted by users' karma points. * * If you try to use this function, you will need to implement your own switch_to_blog() and wp_reset_postdata() handling if running in a multisite * and in a dpa_is_running_networkwide() configuration, otherwise the data won't be fetched from the appropriate site. * * This function accept a 'user_ids' parameter in the $argument, which accepts an array of user IDs. * It is only useful if you want to create a leaderboard that only contains the specified users; for example, * you and your friends could have your own mini-league, or in BuddyPress, each Group could have its own leaderboard. * * It is totally useless if you're trying to find the position for one or more specific users in the *overall* leaderboard. * * @param array $args Optional. Associative array of optional arguments. See function for details. * @return array|bool If no results, false. Otherwise, an associative array: array('results' => array([0] => array('rank' => int, 'user_id' => int, 'karma' => int, 'display_name' => string), ...), 'total' => int). * @since Achievements (3.4) */ function dpa_get_leaderboard(array $args = array()) { global $wpdb; $defaults = array('paged' => dpa_get_leaderboard_paged(), 'populate_extras' => true, 'posts_per_page' => dpa_get_leaderboard_items_per_page(), 'user_ids' => array()); $args = dpa_parse_args($args, $defaults, 'get_leaderboard'); $points_key = "{$wpdb->prefix}_dpa_points"; $num_users = empty($args['user_ids']) ? 0 : count((array) $args['user_ids']); // No, we're not allowing infinite results. This is always a bad idea. if ((int) $args['posts_per_page'] < 1) { $args['posts_per_page'] = dpa_get_leaderboard_items_per_page(); } // We use this later to help get/set the object cache $last_changed = wp_cache_get('last_changed', 'achievements_leaderboard'); if ($last_changed === false) { $last_changed = microtime(); wp_cache_add('last_changed', $last_changed, 'achievements_leaderboard'); } /** * 1) Get all the distinct values of the _dpa_points keys from the usermeta table. * * We do the SELECT DISTINCT and sorting in PHP because meta_value is not indexed; this would cause use MySQL to use a temp table. */ $points_query = $wpdb->prepare("SELECT meta_value\n\t\tFROM {$wpdb->usermeta}\n\t\tWHERE meta_key = %s", $points_key); if ($num_users > 0) { $points_query .= $wpdb->prepare(' AND user_id IN (' . implode(',', wp_parse_id_list((array) $args['user_ids'])) . ') LIMIT %d', $num_users); } // Only query if not in cache $points_cache_key = 'get_leaderboard_points' . md5(serialize($points_query)) . ":{$last_changed}"; $points = wp_cache_get($points_cache_key, 'achievements_leaderboard_ids'); if ($points === false) { $points = $wpdb->get_col($points_query); wp_cache_add($points_cache_key, $points, 'achievements_leaderboard_ids'); } if (empty($points)) { // If points is empty, no-one has any karma, so bail out. return array('results' => array(), 'total' => 0); } /** * Can't use wp_parse_id_list() here because that casts the values to unsigned ints. * The leaderboard might contain users with negative karma point totals. */ $points = array_unique(array_map('intval', $points)); rsort($points, SORT_NUMERIC); // Sort descending for FIND_IN_SET $points = implode(',', $points); // Format for FIND_IN_SET /** * 2a) Start building the SQL to get each user's rank, user ID, and points total. */ $query = $wpdb->prepare("SELECT SQL_CALC_FOUND_ROWS FIND_IN_SET( karma.meta_value, %s ) as rank, ID as user_id, karma.meta_value as karma\n\t\tFROM {$wpdb->users} AS person\n\t\tINNER JOIN {$wpdb->usermeta} as karma ON person.ID = karma.user_id AND karma.meta_key = %s", $points, $points_key); /** * 2b) Sort users correctly even if some of them don't have any karma points. * * `ORDER BY... rank` causes a filesort because usermeta has no index on meta_value :( */ $query .= ' ORDER BY CASE WHEN rank IS NULL THEN 1 ELSE 0 END, rank'; /** * 2c) Handle pagination */ $offset = ((int) $args['paged'] - 1) * (int) $args['posts_per_page']; $query .= $wpdb->prepare(' LIMIT %d, %d', $offset, $args['posts_per_page']); /** * 3) Run the query and cache results */ $cache_key = 'get_leaderboard:' . md5(serialize($query)) . ":{$last_changed}"; $results = wp_cache_get($cache_key, 'achievements_leaderboard'); // Only query if not in cache if ($results === false) { $results = $wpdb->get_results($query); $results_found = $wpdb->get_var('SELECT FOUND_ROWS()'); // All the returned values should be ints, not strings, so cast them here. foreach ($results as $result) { foreach ($result as &$value) { $value = (int) $value; } } $results = array('results' => $results, 'total' => (int) $results_found); wp_cache_add($cache_key, $results, 'achievements_leaderboard'); } /** * 4) Maybe get users' display names */ if ($args['populate_extras']) { $users = get_users(array('fields' => array('ID', 'display_name'), 'include' => wp_list_pluck($results['results'], 'user_id'))); // For now, handle any cached user IDs for spammers or deleted users by setting a blank display name. foreach ($results['results'] as &$leaderboard_user) { $leaderboard_user->display_name = ''; } foreach ($users as $user) { foreach ($results['results'] as &$leaderboard_user) { if ((int) $user->ID === $leaderboard_user->user_id) { $leaderboard_user->display_name = $user->display_name; break; } } } } // Why an ArrayObject? See http://stackoverflow.com/questions/10454779/php-indirect-modification-of-overloaded-property return apply_filters('dpa_get_leaderboard', new ArrayObject($results), $defaults, $args, $points_cache_key, $cache_key); }