Monthly Archives: May 2010

New BuddyPress plugin: BP External Activity

On the CUNY Academic Commons we have a MediaWiki installation running parallel to our WordPress/BuddyPress installation. In the past I had hacked together an inelegant and constantly breaking solution for importing wiki edit notifications into the BP activity stream. I’ve just written a small plugin called BP External Activity which solves the problem by using the BP activity API and RSS.

The plugin can be used to pull items from any RSS feed and add them to your BP activity stream, with customizable text. It’s feature-light right now (and requires some hand-coding to work) but it’s still pretty much the coolest thing ever. I will update it to be better when I get around to it.

Get BP External Activity here.

BuddyPress plugins running on the CUNY Academic Commons

Cross-posted on the CUNY Academic Commons dev blog

A few people have asked recently for a list of the plugins installed on the CUNY Academic Commons. In the spirit of Joe’s post, here I thought I’d make it public. I’m going to limit myself to the BuddyPress plugins here, for the sake of simplicity. (I’d like to write a series of posts on the anatomy of the CUNY Academic Commons; maybe this will be the first in that series.) Here they are, in no particular order other than the order in which they appear on my plugin list.

  • BP TinyMCE. This plugin is messed up, and I have part of it switched off, but I still use the filters that allow additional tags through, in case people want to write some raw HTML in their forum posts, etc.
  • BP Groupblog. Allows blogs to be associated with groups, displaying posts on that group’s activity feed and automatically credentialing group members on the blog. I did some custom modifications to the way the plugin works so that clicking on the Blog tab in a group leads you to subdomain address rather than the Groupblog custom address (thereby also ensuring that visitors see the intended blog theme rather than the BP-ish theme).
  • BP MPO Activity Filter. This plugin works along with More Privacy Options to ensure that the new privacy settings are understood by Buddypress and that blog-related activity items are displayed to the appropriate people.
  • BuddyPress Group Documents. This one is crucial to our members, who often use the plugin to share collaborative docs.
  • BP Include Non-Member Comments makes sure that blog comments from non-members are included on the sitewide activity feed.
  • BP External Activity – an as-yet unreleased plugin I wrote that brings in items from an external RSS feed and adds them to the sitewide activity feed. We’re using it for MediaWiki edits.
  • BP Group Management lets admins add people to groups. Very handy for putting together a group quickly, without having to wait for invites.
  • BP System Report. We’re using this one to keep track of some data in our system and report it back to members and administrators.
  • BuddyPress Group Email Subscription allows users to subscribe to immediate or digest email notification of group activity. Right now we’re running it on a trial basis with a handful of members, in order to test it. (Here’s how to run it with a whitelist of users, if you want)
  • BuddyPress Terms of Service Agreement, another as-yet-unreleased plugin (this one by CAC Dev Team member Chris Stein) that requires new members to check TOS acceptance box before being allowed to register.
  • Custom Profile Filters for BuddyPress allows users to customize the way that their profile interests become links
  • Enhanced BuddyPress Widgets. Lets the admin decide the default state of BP widgets on the front page.
  • Forum Attachments for BuddyPress. Another of our most important BP plugins, this one allows users to share files via the group forums.
  • Group Forum Subscription for BuddyPress. This is our legacy email notification system, which is going to be in place until I get back from my honeymoon and can replace it :)
  • Invite Anyone lets our users invite new members to the community and makes it easier to populate groups.

Questions about any of these plugins or how they work with BuddyPress? Ask in the comments.

A distributed, multi-client courseware

At yesterday’s THATCamp I attended a session, facilitated by Steve Ramsay, entitled “All Courseware Sucks”. You can read the blog post that served as the inspiration for the session at the THATCamp blog. Steve started the session by framing the issue in a way that ended up being quite helpful: he had us list the features of courseware that we’d used, and then to talk about whether they were all crucial. The problem is that almost all of them were crucial to at least someone in the room. Moreover, some of the items that certain individuals found the most useful (say, a gradebook where students could track their progress) seemed the most expendable to others. Listening to that discussion, I thought to myself: This must be what happens in Blackboard focus groups. Gödel’s Second Theorem of LMSes: Any learning management system with a vocabulary rich enough to do interesting work can be shown to be bloatware.

If it’s not the functionality of courseware that we dislike, then, what is it? Well, the basic complaint seems to be that the interface is terrible. And not just terrible in an aesthetic way, though certainly most courseware is absolutely devoid of whimsy. The deeper problem is that software design decisions about how courseware conceives of a course – its hierarchy, its ontology, the vocabulary used to describe its objects, its workflow, its presentation – constrain the instructional design decisions that the professor can make about how the course will be taught.

How do you design a courseware interface that jibes with the aesthetic and instructional predilictions of instructors from math, biology, French Lit, and everywhere between? Answer: You don’t. There are dozens and dozens of well-developed, general purpose content management systems out there. Each has an interface that its users are comfortable with. Why not take advantage of this fact? If students sometimes prefer Blackboard because at least with Bb they know what they’re getting into, why not just let them use whatever they’re already comfortable with?

The idea in a nutshell: Abstract the content from the platform, so that individual students and professors can use whatever platform they’d like as an interface. Existing CMSes like Blackboard, Moodle, WordPress, Drupal, Joomla, and so forth become clients that sync with a central data repository hosted by the school or by a third party.

There are about a trillion details that need to be filled in to make this viable. But here’s a very rough-n-ready explanation of how it might work in a particular case. Let’s imagine that Jack is the professor, using Drupal as client software, and Sally is the student, using WordPress as her preferred client. Jack is going to assign a blog post, and Sally’s going to complete it.

  1. Jack logs into his Drupal installation. This could be on his own server or on a centralized installation hosted by his school. On this installation there will be a module that creates a number of new content types in addition to the default Story and Page. Let’s imagine the content type is called Assignment, which contains fields for content, subject, due date, and whatever other metadata you might like.
  2. Jack writes the assignment and saves it. It saves to the Drupal database as a native Drupal item.
  3. After the save, a hook is triggered by a Bridge module. Bridges are client-specific translators that send and fetch information to and from the central repository. The Bridge module translates the Drupal content type into an XML or JSON document that is formatted according to the agreed-upon standard data format for Assignments.
  4. The Bridge module sends the document to the central Server. The Server can be connected to the student information and registrar systems in much the way that Blackboard is. The Server recognizes Jack’s signature on the document, and furthermore knows that the file is associated with Jack’s particular course. The document is translated into a Server database object and saved, keyed by Jack’s user id, the course id, etc. The Server could be set up to send out email notifications to students at this time, alerting them of a new assignment.
  5. The next time Sally logs into her WordPress installation, her WP Bridge plugin (another piece of translating software, this one WP-specific) pings the Server. The Server knows that Sally is in Jack’s class, so it looks through the database to see if any new assignments have been posted in that class since Sally last logged in. It finds the blog assignment that Jack posted, translates it to a JSON or XML doc, and sends it to Sally’s WordPress installation. The Bridge then parses the document and saves it as a WordPress-native item in the database of Sally’s WordPress installation, perhaps as an instance of a custom post type or something like that.
  6. Sally visits the course page in her WP installation. This could be an admin panel on the Dashboard, or a front-end page. It shows the new assignment, to write a new blog post.
  7. Sally writes a new blog post using the native blog functionality of WP (different content types that aren’t so WP-friendly would need a bit of interface or theme work. Discussion forums, for instance, could easily be stored as custom post types and skinned to look like a traditional forum thread). She might indicate, using a certain tag or category or postmeta checkbox, that this blog post belongs to a particular class, or is a response to a particular assignment. The post is saved to the WP database, and, as above, the Bridge plugin then kicks in, translates the post, and sends it back to the Server, where the process will repeat the next time that Jack logs into his installation.

As I’m writing this, I realize that it seems really simple and obvious, and I’m sure that there are a hundred reasons why something like this would be hard to actually implement. But it’s worth exploration, as this model enjoys the huge advantage of letting the user choose whichever of a number of interfaces suits their fancy.

A few complications to mull over, off the top of my head:

  • Certain content is likely not to translate very well between platforms, especially content whose visual presentation is central to its effect. In those instances, the content could be rendered using the client software of the author and linked to in addition to/instead of being transcoded.
  • File storage. Where do you store images, zip files, etc? On the author’s client installation, on the Server, or both?
  • Security. How do you ensure that only Jack is able to download assignments for his course? What prot

The meat of Facebook

danah boyd wrote a blog post arguing that Facebook ought to be regulated like a utility. What exactly it means to be a utility, and why utilities ought to be regulated in general, is not the main focus of her piece, and she adds in an addendum that the issue is not so much that FB is a utility as that it is trying to be one. But, in any case, I want to push against the utility analogy with one of my own.

I take it that the reason why utilities ought to be regulated is that they are monopolies, and in a single-provider market, people can’t realistically use the threat of leaving for a competitor as leverage for bargaining with the monopolistic company. To claim this is the case for Facebook is surely an overstatement: people can and do opt out of using Facebook, and certainly there are enough other social options out there to block the analogy between them and “people who are building cabins in the woods” (an analogy suggested by boyd). Even if Facebook is dominant, it’s not a monopoly in the way that utility companies are, so the same arguments for regulation don’t really work.

The government sees fit to regulate in other sorts of cases, though. Take the meat industry. The government regulates certain aspects of the meat industry (however lax or ill-conceived USDA oversight might be). The justification here is not that the meat industry is monopolistic (though I’m sure it is mostly controlled by a couple conglomerates, and insofar as this is true it should be additionally regulated as a monopoly). Instead, the justification seems to be: 1) this kind of industry has the potential to do great harm if left to its own devices (E. coli and stuff like that), and 2) it is unlikely that many (or any) consumers of this industry’s goods are in a position to verify independently the claims of the industry (not many have access to labs where they can test for bacteria, etc). The government is justified in protecting its citizens at their most vulnerable (you might even say this is the primary reason for government). So they’re justified in regulating the meat industry.

The case of Facebook is parallel. 1) Because people keep a lot of their most important stuff in Facebook, a large amount of harm could be done if Zuck decided to start selling it to advertisers or something more nefarious still. 2) It’s difficult, if not impossible, for most people to verify the claims of FB with respect to how FB claims to store and use data. For one thing, the “privacy” settings are arcane to the point of incomprehensibility. And even if you figure out the settings, without access to FB’s software and servers, you can’t really know whether they’re living up to their word. Thus, government regulation might be justified.

Someone needs to write The Jungle, Part II: Zuckerpunched.

New BuddyPress plugin: BuddyPress Group Email Subscription

Email Options on settings page

Email Options on settings page

I’m quite happy to announce the release of a more-or-less stable (we hope!) version of BuddyPress Group Email Subscription, a BuddyPress plugin that allows for fine-grained, user-controllable email subscription to group content in BuddyPress.

This plugin is different from some of my others in that it was truly a group endeavor. The base of the plugin was written by David Cartwright, with a little bit of code from me. A nearly complete rewrite of the front-end and most of the guts of the plugin was undertaken by Deryk Wenaus. I wrote the daily and weekly digest functionality, along with some of the settings pages and various bugfixes throughout. The current codebase of the plugin is probably 60% Deryk, 30% me, and 10% David.

It was my first time working on a truly collaborative software development project like this, and it was a real pleasure working with both of these gentlemen. Thanks, guys.

Get BuddyPress Group Email Subscription here.

Setting up a WordPress/BuddyPress development environment on OS X

A local development environment is a collection of software and files on your local computer that replicates a server environment. There’s a number of reasons why doing web development in a local environment and then pushing it to a remote server is a good idea:

  • Convenience: You don’t need an internet connection
  • Speed: Because you’re not transferring files remotely, there’s no save or reload lag
  • Power: You have total control over the environment, in a way you don’t on, eg, shared hosting
  • Safety: You can set up as many parallel environments as you’d like on your local machine, and if you destroy one of them, you can wipe it out and replace it in just a few clicks

I just got a new computer and so have been going through the process of rebuildling my local dev environment. For the benefit of those just getting into web development, here’s how I set it up, with a bit of explanation. Keep in mind that I’m working with OS X 10.6, developing for WordPress; if you’re running a different operating system, or developing for a non-PHP based framework, your setup will differ from mine.

  1. Create a /sites directory: To make navigation from the command line a bit easier, I like to keep all my development environments in first-level directory called sites. Open a Terminal window and type:
    mkdir /sites
  2. Download and install MAMP: Strictly speaking, MAMP isn’t required on OSX, since the OS comes with Apache, MySQL, and PHP installed (enabled through System Preferences > Sharing > Web Sharing). But MAMP has a nice preferences interface, and comes with useful tools like PHPMyAdmin, so I use it. Get MAMP and install it.
  3. Configure MAMP: Open MAMP and click the Preferences button.
    • Configure Start/Stop. I like to uncheck the “Stop servers when quitting MAMP” box, so that I don’t have to keep MAMP open all the time.
    • Switch the ports. You can use the port settings that MAMP comes preconfigured with, but I like to change it because it can make managing domain names a bit easier. Click “Set to default Apache and MySQL ports”. The downside of changing this setting is that each time you start up MAMP (for me, that’s every time I start the computer, which is once every week or so), you’ll need to type in your OSX administrator password. That’ll happen when you save your settings at the end of this step, too – don’t be alarmed.
    • Switch the Apache root directory. On the Apache tab, change the root directory to /sites.

    When you click on OK to save your preferences, you will probably be asked for your admin password. Your local environment is now up and running, and it’s time to configure it to handle WordPress.

  4. Configure your hosts file: By default, you can reach your local installation in a browser by visiting http://localhost or The first option doesn’t work very well with WordPress (WPMU, at least), and the second one isn’t very attractive. We can set up a more attractive host name by editing the /etc/hosts. Open /etc/hosts, ideally at the command line with
    sudo nano /etc/hosts

    ‘sudo’ is important here, as you’ll need admin rights to change this file. Modify the line that says		localhost

    so that it says		localhost

    Now clearly you don’t have to use ‘’ (though you probably should, because I am awesome). You can use any combination of words you want, separated by periods, like a URL – ‘’, perhaps. Don’t use a real URL. Save the file (if you’re at the command line, hit Ctrl-X, and then Y when you’re prompted to save) and test your new hosts file by visiting (or whatever) in a browser window.

  5. Create a database: In MAMP, click the “Open Start Page” button, which will open the MAMP start page in the browser. Click on the PHPMyAdmin link on the start page. PHPMyAdmin is a graphical interface for your MySQL database that you might find handy as you do development. Click the Databases tab, and create a new database – I’m calling mine ‘wp-trunk’. You may also want to choose a default text encoding: ‘utf8_general_ci’ works pretty well if you think you might be doing development in different character sets (Cyrillic, Arabic, etc).
  6. Download WordPress: I like to get WP via SVN, which makes it easy to keep track of any core hacks I might make. Here are the Terminal commands to create an installation called ‘wp-trunk’:
    cd /sites
    mkdir wp-trunk
    svn co wp-trunk/

    You’ll see a bunch of files being downloaded. In this example I’m downloading the trunk, or development, version of WP, rather than the stable version. If you’d like to get a specific version, say 2.9.2, use svn co wp-trunk/ instead. (More on using svn with WordPress, from Mark Jaquith)

  7. Install WordPress: In your browser, go to (or the corresponding path on your machine). This should load the WordPress installer. If you’ve followed along with my instructions, the settings in this image ought to work for you. You’ll notice that I’m using the root MySQL account, with the default password, because it automatically has all privileges on all databases. You should obviously never do this on a database that is connected to the internet. I should also note here that I’m installing the beta of WP 3.0, but the same process will work for any version of WP, even WPMU. With MU, though, you may have some problems if you choose the subdomains option for secondary blogs.
  8. That’s it! You should now be able to log into your installation at When I plan to use an installation of WP to develop for BuddyPress, I check out the trunk version of BP in a similar fashion to step 6:
    cd /sites/wp-trunk/wp-content/plugins
    mkdir buddypress
    svn co buddypress/

Adding an “email to members” checkbox to the BuddyPress group activity stream

During the recent upgrade from BuddyPress 1.1.x to BuddyPress 1.2.x, and the subsequent move away from group wires to interactive group activity streams, one thing that some users on the CUNY Academic Commons missed was the “Notify members by email” checkbox of the old wire.

This morning I wrote a bit of code to add that kind of functionality to group activity streams. There are three functions, each of which goes in your plugins/bp-custom.php file.

First, adding the checkbox to the activity box. Notice that it only shows up when you’re on a group page.

function cac_email_activity_checkbox() {
	if ( !bp_is_groups_component() )

<label for="cac_activity_mail">
		Email this update to all group members? 
		<input type="checkbox" name="cac_activity_mail" id="cac_activity_mail" value="mailme" />
add_action( 'bp_activity_post_form_options', 'cac_email_activity_checkbox' );

Second, handling the data when it gets to the server and sending the emails. Obviously, you’ll want to change the text of the email to match your own site and your own preferences. The line “remove_action( ‘bp_activity_after_save’ , ‘ass_group_notification_activity’ , 50 );” is there to prevent an email notification from being sent if you’re using the Group Activity Notification plugin, a big official release of which is coming soon :)

function cac_email_activity_handler( $activity ) {
	global $bp;

if ( $_POST['mailme'] == 'mailme' ) {

$subject = sprintf('[CUNY Academic Commons] New update in the group "%s"',  $bp->groups->current_group->name );

$message = strip_tags($activity->action);
		$message .= '

		$message .= strip_tags($activity->content);

$message .= '


$message .= sprintf('You recieved this message because you are a member of the group "%s" on the CUNY Academic Commons. Visit the group: %s', $bp->groups->current_group->name, $bp->root_domain . '/' . $bp->groups->current_group->slug . '/' . $bp->groups->current_group->slug . '/' );


if ( bp_group_has_members( 'exclude_admins_mods=0&amp;per_page=10000' ) ) {
			global $members_template;
			foreach( $members_template->members as $m ) {
				wp_mail( $m->user_email, $subject, $message );

remove_action( 'bp_activity_after_save' , 'ass_group_notification_activity' , 50 );
add_action( 'bp_activity_after_save', 'cac_email_activity_handler', 1 );

Finally, you’ll need some Javascript to make the AJAX activity submission work correctly. This is really just a copy of what’s in the bp-default JS file, with a few added lines to make it work.

function cac_email_activity_js() {
	if ( !bp_is_groups_component() )
	<script type="text/javascript">

var jq = jQuery;
	jq(document).ready( function() {
			/* New posts */
	jq("input#aw-whats-new-submit").click( function() {
		var button = jq(this);
		var form = button.parent().parent().parent().parent();

form.children().each( function() {
			if ( jq.nodeName(this, "textarea") || jq.nodeName(this, "input") )
				jq(this).attr( 'disabled', 'disabled' );

jq( 'form#' + form.attr('id') + ' span.ajax-loader' ).show();

/* Remove any errors */

/* Default POST values */
		var object = '';
		var item_id = jq("#whats-new-post-in").val();
		var content = jq("textarea#whats-new").val();
		var mailme = jq("#cac_activity_mail:checked").val();

/* Set object for non-profile posts */
		if ( item_id > 0 ) {
			object = jq("#whats-new-post-object").val();
		} ajaxurl, {
			action: 'post_update',
			'cookie': encodeURIComponent(document.cookie),
			'_wpnonce_post_update': jq("input#_wpnonce_post_update").val(),
			'content': content,
			'object': object,
			'mailme': mailme,
			'item_id': item_id
			jq( 'form#' + form.attr('id') + ' span.ajax-loader' ).hide();

form.children().each( function() {
				if ( jq.nodeName(this, "textarea") || jq.nodeName(this, "input") )
					jq(this).attr( 'disabled', '' );

/* Check for errors and append if found. */
			if ( response[0] + response[1] == '-1' ) {
				form.prepend( response.substr( 2, response.length ) );
				jq( 'form#' + form.attr('id') + ' div.error').hide().fadeIn( 200 );
				button.attr("disabled", '');
			} else {
				if ( 0 == jq("ul.activity-list").length ) {
					jq("div.activity").append( '<ul id="activity-stream" class="activity-list item-list">' );

				jq("ul.activity-list li:first").addClass('new-update');
				jq("").hide().slideDown( 300 );
				jq("").removeClass( 'new-update' );

/* Re-enable the submit button after 8 seconds. */
				setTimeout( function() { button.attr("disabled", ''); }, 8000 );

return false;

add_action( 'bp_activity_post_form_options', 'cac_email_activity_js', 999 );