diff --git a/CHANGELOG.txt b/CHANGELOG.txt index fc634c6507a16a02579443bbba099d62cf514010..cbe0ba9cdc4076b8a35ab2d89514f52eef935b8f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,4 +1,15 @@ +Achievements 7.x-1.4, 2011-XX-XX +-------------------------------- + * (todo) Write up a "Getting started" part of the README? + * (todo) Add some links for good achievement design? + + * Achievement leaderboards can be altered via hook_achievements_leaderboard(). + * Lets you add new table columns or heavily modify leaderboard display. + * Context (type of leaderboard, block, etc.) and raw data is passed. + * See achievements.api.php for full details about the new hook. + + Achievements 7.x-1.3, 2011-09-29 -------------------------------- * The following hooks have been added. See achievements.api.php for details: diff --git a/README.txt b/README.txt index 660986d55f4465a769d693c33b2ca67cdd573984..39a2419d1b4c385c945018a065813e5cc7840660 100644 --- a/README.txt +++ b/README.txt @@ -12,7 +12,7 @@ INTRODUCTION Current Maintainer: Morbus Iff The Achievements module offers the ability to create achievements and -badges similar to systems seen on the Xbox 360, Playstation 3, Foursquare, +badges similar to systems seen on Xbox 360, Playstation 3, Foursquare, Gowalla, GetGlue, and more. For a Drupal site, this could mean commenting a certain number of times, starting a forum topic, visiting the site every day of the week, or anything else that can be tracked and coded. @@ -47,8 +47,10 @@ Current features and design: * Achievements can be grouped into categories and tabbed with jQuery UI. * An adminterface allows you to manually grant or remove achievements. If - the user is offline at the time, any unlocked achievements will display - the next time the user visits the site. + the user is offline, unlocked achievements will display the next time + the user visits the site. + + * Achievement unlocks fade-in and out at the window's bottom right corner. * Your code decides whether achievements are retroactively applied or not. diff --git a/achievements.api.php b/achievements.api.php index 973ea14e4a5729fa65b5a5b9ace64876cb38e359..ab6a548b21e059e6e8c86e82e3388b01274903c6 100644 --- a/achievements.api.php +++ b/achievements.api.php @@ -158,13 +158,13 @@ function example_node_insert($node) { * hesitate to create an issue asking for them. * * achievement_totals: - * Leaderboard: find the totals of all users in ranking order. + * Find the totals of all users in ranking order. * * achievement_totals_user: - * Leaderboard: find the totals of the passed user. + * Find the totals of the passed user. * * achievement_totals_user_nearby: - * Leaderboard: find users nearby the ranking of the passed user. + * Find users nearby the ranking of the passed user. */ function example_query_alter() { // futz with morbus' logic. insert explosions and singularities. @@ -214,3 +214,33 @@ function example_achievements_unlocked($achievement, $uid) { function example_achievements_locked($achievement, $uid) { // react to achievement removal. bad user, BaAaDdd UUserrRR! } + +/** + * Implements hook_achievements_leaderboard(). + * + * Allows you to tweak or even recreate the leaderboard as required. The + * default implementation creates leaderboards as HTML tables and this hook + * allows you to modify that table (new columns, tweaked values, etc.) or + * replace it entirely with a new render element. + * + * @param &$leaderboard + * An array of information about the leaderboard. Available keys are: + * - achievers: The database results from the leaderboard queries. + * Results are keyed by leaderboard type (top, relative, first, and + * recent) and then by user ID, sorted in proper ranking order. + * - block: A boolean indicating whether this is a block-based leaderboard. + * - type: The type of leaderboard being displayed. One of: top (the overall + * leaderboard displayed on achievements/leaderboard), relative (the + * current-user-centric version with nearby ranks), first (the first users + * who unlocked a particular achievement), and recent (the most recent + * users who unlocked a particular achievement). + * - render: A render array for use with drupal_render(). Default rendering + * is with #theme => table, and you'll receive all the keys necessary + * for that implementation. You're welcome to insert your own unique + * render, bypassing the default entirely. + */ +function example_achievements_leaderboard(&$leaderboard) { + if ($leaderboard['type'] == 'first') { + $leaderboard['render']['#caption'] = t('Congratulations to our first 10!'); + } +} \ No newline at end of file diff --git a/achievements.css b/achievements.css index eb6d019e82ad24448d1c17aa7c77e126a45f9328..3867283c9e842294d499a148dc9eac4ec4a67cb7 100644 --- a/achievements.css +++ b/achievements.css @@ -141,18 +141,18 @@ /** * Stats on the per-achievement leaderboards. */ -.achievement-stats-first { +.achievement-leaderboard-first { float: left; width: 49%; } -.achievement-stats-recent { +.achievement-leaderboard-recent { float: right; width: 49%; } -.achievement-stats-first caption, -.achievement-stats-recent caption { +.achievement-leaderboard-first caption, +.achievement-leaderboard-recent caption { font-size: 150%; margin-bottom: 0.6em; } diff --git a/achievements.pages.inc b/achievements.pages.inc index 46e8407aa684d19d6c0c28455815dfbeb3452608..8e7277e9824286414aa088177ec302254457f6de 100644 --- a/achievements.pages.inc +++ b/achievements.pages.inc @@ -46,11 +46,11 @@ function achievements_leaderboard_totals() { $variables = array('achievement' => achievements_load($achiever->achievement_id), 'unlock' => array()); achievements_template_shared_variables($variables); // generate image, munge for hidden, etc. - $rows[$type][] = array( // give a special class if its the current user. + $rows[$type][$achiever->uid] = array( // give a special class if its the current user. 'class' => ($achiever->uid == $GLOBALS['user']->uid) ? array('achievement-leaderboard-current-user') : array(), 'data' => array( - array( - 'data' => isset($achiever->rank) ? $achiever->rank : $rank++, + array( // increment our internal rank counter, and apply it to the dataset too. + 'data' => isset($achiever->rank) ? $achiever->rank : ($achiever->rank = $rank++), 'class' => array('achievement-leaderboard-rank') ), array( @@ -78,14 +78,22 @@ function achievements_leaderboard_totals() { } foreach (array('top', 'relative') as $type) { - $build['achievements']['leaderboard'][$type] = array( - '#attributes' => array('class' => array('achievement-leaderboard', 'achievement-leaderboard-' . $type)), - '#header' => $type == 'top' ? $header : NULL, // relative piggybacks off of top. - '#empty' => $type == 'top' ? t('No one has been ranked yet.') : NULL, - '#rows' => $rows[$type], - '#theme' => 'table', + $leaderboard = array( + 'achievers' => $achievers, + 'block' => FALSE, + 'type' => $type, + 'render' => array( + '#attributes' => array('class' => array('achievement-leaderboard', 'achievement-leaderboard-' . $type)), + '#header' => $type == 'top' ? $header : NULL, // relative piggybacks off of top. + '#empty' => $type == 'top' ? t('No one has been ranked yet.') : NULL, + '#rows' => $rows[$type], + '#theme' => 'table', + ), ); + drupal_alter('achievements_leaderboard', $leaderboard); + $build['achievements']['leaderboard'][$type] = $leaderboard['render']; } + $build['achievements']['leaderboard_pager'] = array( '#theme' => 'pager', ); @@ -121,8 +129,8 @@ function achievements_leaderboard_block() { $rows[$type][] = array( // give a special class if its the current user. 'class' => ($achiever->uid == $GLOBALS['user']->uid) ? array('achievement-leaderboard-current-user') : array(), 'data' => array( - array( - 'data' => isset($achiever->rank) ? $achiever->rank : $rank++, + array( // increment our internal rank counter, and apply it to the dataset too. + 'data' => isset($achiever->rank) ? $achiever->rank : ($achiever->rank = $rank++), 'class' => array('achievement-leaderboard-rank') ), array( @@ -139,18 +147,26 @@ function achievements_leaderboard_block() { } foreach (array('top', 'relative') as $type) { - $build['achievements']['leaderboard'][$type] = array( - '#attributes' => array('class' => array('achievement-leaderboard', 'achievement-leaderboard-' . $type)), - '#header' => $type == 'top' ? $header : NULL, // relative piggybacks off of top. - '#empty' => $type == 'top' ? t('No one has been ranked yet.') : NULL, - '#rows' => $rows[$type], - '#theme' => 'table', + $leaderboard = array( + 'achievers' => $achievers, + 'block' => TRUE, + 'type' => $type, + 'render' => array( + '#attributes' => array('class' => array('achievement-leaderboard', 'achievement-leaderboard-' . $type)), + '#header' => $type == 'top' ? $header : NULL, // relative piggybacks off of top. + '#empty' => $type == 'top' ? t('No one has been ranked yet.') : NULL, + '#rows' => $rows[$type], + '#theme' => 'table', + ), ); + drupal_alter('achievements_leaderboard', $leaderboard); + $build['achievements']['leaderboard'][$type] = $leaderboard['render']; } + $build['achievements']['see-more'] = array( - '#prefix' => '
', - '#markup' => l(t('View full leaderboard »'), 'achievements/leaderboard'), - '#suffix' => '
', + '#prefix' => '
', + '#markup' => l(t('View full leaderboard »'), 'achievements/leaderboard'), + '#suffix' => '
', ); return $build; @@ -175,41 +191,47 @@ function achievements_leaderboard_for($achievement) { $query->condition('au.achievement_id', $achievement['id']); // ... with a slight tweak. $query->fields('au', array('uid', 'rank', 'timestamp'))->fields('at', array('points', 'unlocks'))->fields('u', array('name')); $query2 = clone $query; // allows us to save a few lines of duplicate query building. never used clone before. awesome. - $stats['first'] = $query->orderBy('rank')->range(0, 10)->execute()->fetchAllAssoc('rank'); // FI... sigh. - $stats['recent'] = $query2->orderBy('timestamp', 'DESC')->range(0, 10)->execute()->fetchAllAssoc('rank'); + $achievers['first'] = $query->orderBy('rank')->range(0, 10)->execute()->fetchAllAssoc('uid'); // FI... sigh. + $achievers['recent'] = $query2->orderBy('timestamp', 'DESC')->range(0, 10)->execute()->fetchAllAssoc('uid'); - // both stat tables are displayed similarly. foreach (array('first', 'recent') as $type) { $rows = array(); // clear previous run. - foreach ($stats[$type] as $stat) { - $rows[] = array( + foreach ($achievers[$type] as $achiever) { + $rows[$achiever->uid] = array( array( - 'data' => $stat->rank, + 'data' => $achiever->rank, 'class' => array('achievement-leaderboard-rank') ), array( - 'data' => theme('username', array('account' => $stat)), + 'data' => theme('username', array('account' => $achiever)), 'class' => array('achievement-leaderboard-username') ), array( - 'data' => l($stat->points, 'user/' . $stat->uid . '/achievements'), + 'data' => l($achiever->points, 'user/' . $achiever->uid . '/achievements'), 'class' => array('achievement-leaderboard-points') ), array( - 'data' => format_date($stat->timestamp, 'short'), + 'data' => format_date($achiever->timestamp, 'short'), 'class' => array('achievement-leaderboard-when') ), ); } - $build['achievements']['stats'][$type] = array( - '#attributes' => array('class' => array('achievement-stats-' . $type)), - '#caption' => t('@type achievement unlocks', array('@type' => drupal_ucfirst($type))), - '#header' => array(t('#'), t('Who'), t('Points'), t('When')), - '#empty' => t('No one has unlocked this yet. Keep trying!'), - '#rows' => $rows, - '#theme' => 'table', + $leaderboard = array( + 'achievers' => $achievers, + 'block' => FALSE, + 'type' => $type, + 'render' => array( + '#attributes' => array('class' => array('achievement-leaderboard-' . $type)), + '#caption' => t('@type achievement unlocks', array('@type' => drupal_ucfirst($type))), + '#header' => array(t('#'), t('Who'), t('Points'), t('When')), + '#empty' => t('No one has unlocked this yet. Keep trying!'), + '#rows' => $rows, + '#theme' => 'table', + ), ); + drupal_alter('achievements_leaderboard', $leaderboard); + $build['achievements']['leaderboard'][$type] = $leaderboard['render']; } return $build;