Drupal
The Drupal Achievements module offers the ability to create achievements and 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. The recently released 7.x-1.4 update focuses on changes that make theming easier.
In my first post about #theme_wrappers, we fixed up bad HTML in #prefix and #suffix to give themers an easier time. This post, about render arrays, is no different. In fact, most of the tweaks in Achievements 7.x-1.4 are not very obvious, or even useful, to module developers. It's only when you sit down with a themer who wants to change the entire look and feel that you realize your carefully-coded "looks good in Garland!" output is not as useful as you think.
Why render arrays are better
You've probably read about render arrays in the Drupal Developer's Handbook already, but the documentation doesn't really give a developer a decent reason why they should use them. It also doesn't talk about $content or splitting your variables into "data" and "presentation". I'll cover both below.
First, let's look at the wrong approach:
$build['image'] = theme('image', array('path' => array('misc/druplicon.png')));
If this was passed to a theme file, the themer would simply print $image; and be done with it. The themer could even link the image if they wanted, but they wouldn't be able to add any classes or attributes to the image, since calling theme() directly always returns rendered HTML. Here's what the above looks like as a render array:
$build['image'] = array(
'#theme' => 'image',
'#path' => 'misc/druplicon.png',
);
To get this to display in the theme, we'd use print render($image); instead. In this case, the theme is telling Drupal to render the HTML, not your module. Why is this important? Ignoring the nebulous "I CAN'T ADD CSS CLASSES, SNIFF!" themer lament, consider the following small tweak to our original bad example:
$image = theme('image', array('path' => array('misc/druplicon.png')));
$build['image'] = l($image, 'node', array('html' => TRUE));
Here, we've simply added a link to our dummy image and the themer gets the fully rendered HTML to print out. Everything is toasty... unless the client or theme doesn't want or need the image to be linked. The themer now has no choice: they either have to use a regular expression to strip out the unwanted link or they have to recreate the $image variable in his own, well, image. Not only is that fragile (it's essentially copying module code to the theme and that code might change in a future version), but it also mixes too much logic with too little presentation. If the image's path isn't available to the theme as its own variable (it isn't, in the above example), the themer will still have to parse your rendered HTML to find the image's path first. Yuck.
Let's convert the above to a render array:
$build['image'] = array(
'#theme' => 'image_formatter',
'#item' => array(
'uri' => 'misc/druplicon.png',
),
'#path' => array(
'path' => 'node',
'options' => array('html' => TRUE),
),
);
Now, if the themer wants to remove the link, they can listen in a theme override, a preprocess, or even the dreaded hook_page_alter() and simply unset($variables['image']['#path'])'. No more link, without duplicating any upstream code or recreating things themselves. This is a real-life example, by the way - I just finished an optional submodule of Achievements which removes all links to the default leaderboards. I couldn't have easily done that if I used either of the link approaches below:
$build['achievement_title'] = l($title, 'path/to/achievement');
$build['achievement_title']['#markup'] = l($title, 'path/to/achievement');
But with the following render array:
$build['achievement_title'] = array(
'#type' => 'link',
'#title' => $title,
'#href' => 'path/to/achievemnt',
);
I can make those links linkless:
unset($variables['achievement_title']['#type']); $variables['achievement_title']['#markup'] = $variables['achievement_title']['#title'];
Data, presentation, and $content['a'] not $a
This doesn't mean that every single variable you pass to the theme should be a render array: there's clearly a difference between variables that represent data (an image path, the $node or $user object, a Unix timestamp, etc.) and variables that are meant for presentation (the linked image, the node's filtered teaser, a human-readable date, etc.).
There's a move afoot to more clearly indicate these two types by using a $content variable as a container for all the supplied render arrays. Drupal core does this in some places (node.tpl.php, for one), but not all, and it's slowly becoming a preferred practice in the theming world. Another benefit is the ability to use print render($content);, which says "render everything I haven't already rendered", allowing themes to display things that they, or the parent module, might not know about (new data added by a third party module in a preprocess, etc.). I've yet to implement this in Achievements, but I'll likely get there for the next release.
- Add new comment
- 3744 reads
The Drupal Achievements module offers the ability to create achievements and 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. The recently released 7.x-1.4 update focuses on changes that make theming easier.
One of those changes was implementing #theme_wrappers. Like many other modules, Achievements has various HTML displays that are essentially containers of things: a group of unlocked achievements, a group of categorized achievements, etc. For stronger theming, one usually wraps those in an extra <div> so that CSS folks can control the container's display. In the past, I've usually accomplished this with:
$build['achievements'] = array(
'#prefix' => '<div class="achievement-groups">',
'#suffix' => '</div>',
);
Simliar code to the above exists in Drupal core (book.module, field.module, etc.) so I never gave it a second thought. However, for a themer, the above is problematic because there's no way to tweak that HTML without dipping into a hook_page_alter() and rewriting or munging the #prefix entirely. Since I've been away from Drupal theming for so long, I never knew there was a stronger alternative by using #theme_wrappers. Not surprisingly, Drupal core uses this pattern alongside the "bad" #prefix version, so a good contender for a "Novice" patch would likely be "Replace all uses of #prefix divs with #theme_wrappers".
Like other theme functions and files, #theme_wrappers requires a hook_theme() definition:
function achievements_theme() {
return array(
'achievement_groups_wrapper' => array(
'render element' => 'element',
),
);
}
In this case, we'll use a regular ol' theme function for the callback:
/**
* Default theme for the wrapper around a user's achievements page.
*
* @param $variables
* An associative array containing:
* - element: A render containing the user's achievements page.
*/
function theme_achievement_groups_wrapper($variables) {
return '<div id="achievement-groups">' . $variables['element']['#children'] . '';
}
And finally, we can tweak the above $build render array to use it:
$build['achievements'] = array(
'#theme_wrappers' => array('achievement_groups_wrapper'),
);
In this case, the resultant HTML will be exactly the same, but now a themer can more easily override it just by defining THEMENAME_achievement_groups_wrapper(). This is a far cheaper method (mentally and performant-wise) than having to futz with hook_page_alter().
- 3 comments
- 3076 reads
The Achievements module offers the ability to create achievements and 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.
Here's what's new in version 7.x-1.3, released today.
Relative leaderboards and latest achievements
The biggest changes in 7.x-1.3 happened to the leaderboards. You can now provide an (optional) "relative leaderboard" which will show a logged-in user their current rank as well as (optionally) a number of ranks before and after their position. For folks who like achievements or mapping their rise to greatness, this will give them a little something more to strive for, a continual heartbeat of their movement through the site-wide rankings. It can also encourage rivalries as users flip-flop back and forth in their point gains and positions. Some users won't give a crap at all, certainly, but for those that do, relative leaderboards can increase a site's stickiness.

Another addition to the leaderboard is "latest achievement" which is exactly as it sounds: it'll show the latest unlock a user has earned. This is important because it increases discoverability of your site's achievements. In previous versions of the module, the only mention of individual achievements was located on a tab in the user's profile -- not exactly the easiest place to find. By including recent achievements on the leaderboard, we're increasing awareness of what you're offering, as well as providing subtle and potential todos for your users and curious visitors.
Hooks for developers
Besides the required hook_achievements_info(), I didn't add any other achievement hooks in the earlier releases because I didn't trust I had a firm grasp of how Achievements module would "end up" based upon my initial roadmap. With said roadmap nearly complete, the latest release of Achievements now includes some additional hooks for more integration. As is best practice, I'm starting off "small and obvious", but will add more as needs arise. hook_achievements_info_alter(), hook_achievements_unlocked(), and hook_achievements_locked() all do what they sound like and are further documented in achievements.api.php.
Small footprint
At its heart, Achievements is a monitoring and statistical module: it listens for events, logs statistics, and does analysis on incoming data. When you have hundreds of achievements, all this intervention could slow your site down. Thankfully, the module has been designed under the oppressive gaze of better performance experts than I and has Examiner.com, one of the largest Drupal 7 sites, as an in-progress client. The new release of Achievements has yet another performance improvement that ensures heavy traffic sustainability, and the new features above cost just one additional database query (per page, but only if you're using the block with relative leaderboards enabled). Future releases of Achievements will continue this dedication to a svelte codebase.
Take a look, bub
You can read about the full feature-set at the Achievements module project page which also includes screenshots of the default interface. Don't hesitate to contact me or create an issue with your pie-in-the-sky feature requests and questions.
- Add new comment
- 4723 reads
Achievements 7.x-1.2 has been released:
- Achievement unlock notifications are now JS fadeins and offline-able.
- We no longer use drupal_set_message() to inform of an unlock.
- A new achievement-notification.tpl.php controls the appearance.
- Unlock notifications now fade in and out at the window's bottom right.
- We now track whether a user has seen an achievement unlock notification.
- If they haven't, they will the next time they login or access the site.
- Leaderboard top rank counts can now be tweaked in the relevant configs.
- This is in preparation for "relative leaderboards" planned for 7.x-1.3.
- Unlocked achievements on user/#/achievements can now be unsorted.
- That is, instead of "move to top" they can remain "as defined in code".
- Defaults to "move to top". Config at admin/config/people/achievements.
- Block 'achievements-leaderboard' renamed with underscores not dashes.
- If you're using this block, you'll need to place it again upon upgrade.
- Fixed a few PHP warnings when a user has yet to unlock any achievements.
- Achievement unlocks are now logged in watchdog.
- Variables are now properly deleted on uninstall.
- Add new comment
- 5258 reads
Achievements 7.x-1.1 has been released (with screenshots for installer trepidation):
- Achievements can now be categorized into groups.
- Grouped achievements will be displayed within jQuery UI tabs.
- If groups exist, ungrouped achievements "upgrade" automatically.
- See achievements.api.php for more on how to define achievement groups.
- Achievements can now have images.
- Three possible display states: locked, unlocked, and hidden.
- Admins may set the default images at admin/config/people/achievements.
- An 'images' array has been added to hook_achievements_info() definitions.
- Per-achievement images can override the default on a per-state basis.
- CSS tweaks were made for a more flexible achievement display.
- Default images have been provided for each of the three states.
- Administrators can now manually give and take achievements from users.
- It's problematic on progression-based achievements and internal statistics.
- hook_achievements_info() gets 'storage' to define where statistics are kept.
- If 'storage' is not specified, assume it exists under the achievement ID.
- See admin/config/people/achievements for the disclaimer text on usage.
- See achievements.api.php for more on 'storage' and a revised HOWTO.
- A new permission, "Earn achievements", has been added.
- It is REQUIRED and NECESSARY for all roles that can unlock achievements.
- Core functions check for it so you shouldn't need it in your own code.
- If you think you need to check, use achievements_user_is_achiever().
- Removing this permission from a role does NOT delete data or ranks.
- It does stop, however, the collection of new data, points, or unlocks.
- achievements/leaderboard/NONEXISTENT now returns a 404.
- Locked achievements are now displayed on a user's achievements tab.
- No more warnings when viewing a user with no achievement unlocks.
- 'id' is no longer duplicated in hook_achievements_info() definitions.
- achievements.tpl.php has had its PHP moved to template_preprocess_hook().
- New variables have been added to streamline the display code.
- Add new comment
- 6612 reads
The first "official" version of my Achievements module is now available for Drupal 7.x. Achievements originated as a Drupal 6 module from 2008 which I just never got around to properly supporting with a Drupal project - it has lived the past three years as a tarball attached to an issue. Due to some recent interest from others, however, I've since finalized a new 1.0 with lots of incremental improvements, with a particular eye toward supporting very large installations. These same interested parties will be satisfying a number of new features and improvements over the coming weeks, including:
- Per-achievement images and the ability to categorize your milestones.
- Manual/administrative granting/removing of achievements.
- A JavaScript popup, ala Xbox 360 or World of Warcraft, instead of
drupal_set_message(). - Progress meters for grindish achievements.
...and a healthy bit more.
- Add new comment
- 7638 reads
My Drupal IRC bot.module received a new release today, bringing it to 7.x-1.3:
- #273116: bot_auth.module added (thanks snufkin, RobLoach).
- scripts/bot_start.php: the new location of bot_start.php.
- scripts/bot_check.sh added: restarts the bot if it's not running.
- #524218: Drush integration added (thanks jonhattan, q0rban, sirkitree).
- bot_log.channel is now a text column (required for large installations).
- All incoming messages are forced to UTF-8 (thanks bellHead, nick_vh).
- #918966: New Net_SmartIRC wrapper; hook_irc_access added (thanks Bevan).
- All forgotten strlens()s have been changed to drupal_strlen().
- All forgotten substr()s have been changed to drupal_substr().
- First release for Drupal 7.x; based on 6.x-1.2 release.
- Add new comment
- 13718 reads
My Drupal IRC bot.module received a new release today, bringing it to 6.x-1.2:
- #937836 and #937820: PostgreSQL and E_STRICT fixes (thanks Shiny).
- #477596: Fixed regexp error in bot_seen.module (thanks Gurpartap).
- bot_tell.module can now remind you of things: "BOTNAME: help Reminders?"
- bot_potpourri.module added (thanks ae1): "BOTNAME: help Timezones?"
- bot_aggregator.module added (thanks cwgordon7/snufkin). Additions:
- Configuration has been moved to the feed config form, not our own.
- Different feeds can now be sent to different channels, based on config.
- If we receive an error containing "flood", we slow down our sends.
- If the server or client library receives an error, we'll watchdog it.
- If a channel bans us, it is now permanently removed from the join list.
- #648606: Chinese "ni hao" is mis-spelled in greetings (thanks JohnAlbin).
- Advanced/debugging option added: whether to use real sockets or not.
- bot_project: fixed broken Trac metadata from ticket/revision exports.
- bot_project: function lookups now allow dashes in the branch name.
- irc_bot_cron_faster (one minute) and _fastest (15 seconds) are available.
- #564524: bot_project now reports comment counts on d.o URLs (thanks killes).
- bot_tell now accepts :;, after a told nick (thanks webchick).
- Channels are now joined every 15 seconds instead of at initial connection.
- Logging checks for channels are now case-insensitive (thanks mozillamonks).
- #362661: Attempt to GHOST and IDENTIFY on nick clashes (thanks seutje).
- #380330: Added perms for factoids, karma, and project (thanks lut4rp).
- #391916: bot_log needed another index for speedier table SELECTs.
- Add new comment
- 11664 reads
Every so often I'll ask Druplicon to remind me about something in the future, and then curse that I haven't yet written such functionality. Then everyone points and laughs at little ol' Morbus. Welp, NO MORE LAUGHING as that code has hit the CVS and Druplicon just now. From the integrated help: Reminders can be set with "BOTNAME: remind NICK (at|by|in|on) DURATION (about|how|that|to) MESSAGE". For example: "BOTNAME: remind Morbus in 1 hour and 6 minutes that his bot is awesome", "BOTNAME: remind me in 23 minutes to check my pot roast.", or even "BOTNAME: remind Monty on Wed, 12 May 2010 13:10:21 -0400 that this was when this code debuted."
Some examples:
<Morbus> Druplicon: remind me in 5 minutes to celebrate.
<Druplicon> Morbus: I'll remind you about that on Wed, 12 May 2010 13:32:55 -0400.
(time passes)
<Druplicon> Morbus: You asked me to remind you to celebrate.<EvanDonovan> Druplicon: remind me in 7 hours that I should still be working :(
<Druplicon> EvanDonovan: I'll remind you about that on Wed, 12 May 2010 20:17:34 -0400.<Morbus> Druplicon: remind me by next week that this feature debuted!
<Druplicon> Morbus: I'll remind you about that on Wed, 19 May 2010 13:34:41 -0400.
- 5 comments
- 11334 reads
I've added some new code to my Drupal IRC bot.module and Druplicon, the official Drupal IRC bot, is now running with these features. First up is feed aggregation: bot.module now integrates with aggregator.module to provide IRC announcements of new feed items. Feeds can be configured per channel or the items can be sent to multiple channels at once. If you run a channel on Freenode that currently has Druplicon and you'd like it to announce relevant news as it happens, don't hesitate to let me know.
A new bot_potpourri.module has been added, and its first feature is timezone display and conversion. From the integrated help: Display timezones with "BOTNAME: timezone BST". Convert timezones with "tz 10AM MST to EST" or "tz 14:27 UTC in Europe/London". Timestamps are allowed if combined and with no spaces: "tz 2010-10-23T10:00 EST to UTC". All returned dates are DST-aware.
Some examples:
<Morbus> find out what time it is somewhere:
<Morbus> Druplicon: tz EST
<Druplicon> 2010-05-11 12:05 EDT.<Morbus> or convert from one timezone to another:
<Morbus> timezone 14:27 EST to Europe/London
<Druplicon> 2010-05-11 19:27 BST.
<Morbus> tz 10 A.M. America/New_York to MST
<Druplicon> 2010-05-11 08:00 MDT.
<Morbus> tz 6 pm EST in EST
<Druplicon> 2010-05-11 18:00 EDT.<Morbus> or dates in the future. note that DST is always considered:
<Morbus> tz 2010-10-23T10:00 EST in UTC
<Druplicon> 2010-10-23 14:00 UTC.
<Morbus> timezone 2010-01-23T10:00 EST in UTC
<Druplicon> 2010-01-23 15:00 UTC.
I hope to squeeze in some more features later this week too.
- 4 comments
- 11268 reads