Tag Archives: CUNY Academic Commons

Selectively deprecating WordPress plugins from Dashboard > Plugins

On large WordPress MS installations where site admins are allowed to manage their own plugins, the list of plugins tends to get crowded over time. Sometimes you introduce a plugin to the network and admins start using it, but some time down the road – a year or two later, even – you decide that you want to deprecate that plugin (maybe to replace it with another one, etc). However, migrating users of one plugin to another plugin is a logistical and technical tangle, and sometimes the best medium-term strategy is to allow existing users of the plugin to keep using it, but to prevent admins from activating it in the future.

Here’s how we’re doing it on the CUNY Academic Commons. In the gist below, $disabled_plugins are the plugins that we don’t want people to activate in the future. In most cases, however, we do want people to be able to deactivate the plugins, so by default, we don’t filter plugins if they’re active. However, we also have an array of $undeactivatable_plugins, which cannot be activated or deactivated.

Props to dev team member Dominic Giglio for writing part of this.

Three talks in Vancouver

For those Bo(o)neheads who follow me to every event in VW vans, I’ll be giving three talks in Vancouver next month:

  1. BuddyPress: Beyond Facebook Clones, Oct 13, WordCamp Vancouver. I’ll highlight some uses of BP that are not straightforward social networks. (BTW, if you know of any really cool ones, please let me know in the comments!)
  2. Free Software and the University: The Story of the CUNY Academic Commons, Oct 14, BuddyCamp Vancouver. I’ll be using the story of the Commons as an excuse to rant about an allegory about the importance of free software in public schools.
  3. Getting Started with BuddyPress Plugins, Oct 14, BuddyCamp Vancouver. I’ll be giving an overview of what WordPress plugin developers need to know about getting their feet wet with BP plugins.

TimThumb problems and solutions for WordPress theme developers

TimThumb is popular PHP script for dymanic image resizing, and is often included in WordPress themes and plugins. It caches its processed images for performance reasons; its default cache directory is called cache, one directory level up from the script itself. TimThumb itself helpfully allows users to define a custom cache directory with a constant (FILE_CACHE_DIRECTORY) as well as a mechanism for loading this custom configuration (an optional timthumb-config.php file).

This setup provides plenty of flexibility when you’re using TimThumb on a one-off site for a client. But when TimThumb is packaged with a theme or plugin, this method of configuring TT not very helpful. Take the case of WooThemes, which uses TimThumb in all of its themes. We use a number of these themes on the CUNY Academic Commons, where I keep tight reins on file permissions. Our Apache user can’t write to the subdirectories of wp-content/themes/, which means that the versions of TimThumb in this directory can’t use the default cache location.

The problem is that there’s no easy way to fix this in a global way. Creating a /cache/ subdirectory in each of these theme directories and chmodding them to something more lax is out of the question in my scenario. After all, I already have a directory where Apache can write files: the WP uploads directories (wp-content/uploads and wp-content/blogs.dir).

But configuring TimThumb to put its cache in one of these places is difficult to accomplish globally. The obvious solution is to create appropriate timthumb-config.php file in each of the theme directories in question. This stinks, though, because it means that my modifications will have to be reapplied each time I upgrade the themes. In fact, what I’ve actually done for the moment is a variation on this: I created a centralized timthumb-config.php file, in a centralized location (wp-content, in my case), containing a single line:

<?php define( 'FILE_CACHE_DIRECTORY', dirname( __FILE__ ) . '/blogs.dir/1/timthumbcache/' ); ?>

I wrote a short script that crawled over every one of my WP themes, and if it found an offender (in my case, any theme with ‘WooThemes’ in the Author tag), create a symlink to the global config. I’ll be including this script as part of my more general deployment scripts.

Of course, this is all extremely inelegant. There are a few things that the theme developers could have done to account for my sort of situation:

  • Stop putting a copy of your shared utilities (the “framework”) in every theme directory. I understand why the theme shop might make this architectural decision – I’m sure that the vast majority of their theme sales are to people running the theme on a non-networked, single site. But it causes a lot of problems for Multisite networks that offer many themes to their users: a single change in the framework means updating all themes, which becomes a real pain if the theme dev’s autoupdater also isn’t working properly. Allow your framework to be installed as a network-activated plugin, or at least dropped into the wp-content directory. Better still: You can continue to ship the framework files with the themes, but only load them if the pluginified version is not found. That gives you the best of both worlds.

    Now in the case of TimThumb, this schema wouldn’t have solved the problem. But it would have meant hacking a single file, rather than 50 different files spread out through my installation.

  • Put a wrapper around TimThumb, to allow more flexible placement of the config file. For instance, create a file called mytheme-thumb.php, which looks like this:

    	<?php
    	if ( file_exists( dirname(__FILE__) . '../../global-timthumb-config.php' ) ) {
    		require( dirname(__FILE__) . '../../global-timthumb-config.php' );
    	}
    
    // Load the out-of-the-box TimThumb utility
    	require( dirname(__FILE__) . '/thumb.php' );
    	?>
    	

    Then point all of your TimThumb-generated src attributes to mytheme-thumb.php, instead of directly at TimThumb. This way, if a global config is found, it’s loaded first, otherwise it falls back on a local timthumb-config.php file, as specified by TT itself.

  • Pluginify TimThumb before using it In a perfect world, you would have the flexibility of WordPress behind TimThumb: the ability to store your cache location to the database, for example, or to filter the location using apply_filters(). I did a bit of testing to see just how much overhead this would add. In my tests, TimThumb used about 700KB to load a simple picture. To load WordPress using the SHORTINIT constant (which loads just enough of WP to get things like get_option() and apply_filters()) takes about 2800KB. So loading WP SHORTINIT + TimThumb would take about 3500KB, or about 5x what it takes to load the same image using TimThumb alone. This wouldn’t be so bad with aggressive caching, but it’s probably too much sacrifice for a script that may get called several times on every single page load.

    So here’s another idea: Just drop in wp-includes/plugins.php. I once heard someone say that a great way to improve any PHP package is to include this file, and I think they’re right. plugins.php is self-contained (it doesn’t require the rest of WP), and it only adds another 50-100KB of overhead. Let’s take our mytheme-thumb.php file again, but add some plugins.php goodness:

    	<?php
    
    // Load the plugin tools, conditionally just in case
    	if ( !function_exists( 'do_action' ) ) {
    		require( dirname(__FILE__) . '/plugins.php' );
    	}
    
    if ( file_exists( dirname(__FILE__) . '../../global-timthumb-config.php' ) ) {
    		require( dirname(__FILE__) . '../../global-timthumb-config.php' );
    	}
    
    // Load TimThumb
    	require( dirname(__FILE__) . '/thumb.php' );
    	?>
    	

    Now in this example, thumb.php wouldn’t have to be an out-of-the-box TimThumb instance. It could be filled with wonderful do_action() and apply_filters() goodness. And your global-timthumb-config.php or timthumb-config.php file wouldn’t just define constants – you could have full-fledged PHP functions, using WP’s hook infrastructure.

    This means maintaining a fork of TimThumb, but it could be a great resource for WP theme developers.

2011

A bunch of stuff happened in 2011.

Like 2010, 2011 was a year of transitions for me: in my relationship with academia, in the way I earn a living, in the way I present myself as a citizen-builder of the internet. Being a parent is the biggest transition of all, forcing me to put into perspective the ways I spend my energy and the ways in which I define myself and what has value to me. (This transition has been overwhelmingly a Good Thing.) Continuing to strive for the right balance in these areas will, I’m sure, be a hallmark of my 2012. (Thankfully, I have no plans to have a child or get married in 2012. A man needs a year off from major life events!)

Happy new year!

New WordPress plugin: Add User Autocomplete

Add User Autocomplete

Add User Autocomplete

Site admins on a WordPress Network can add existing network members to their site on the Dashboard > Users > Add New panel. But the interface requires that one know either the email address or the username of the user in question. My new plugin, Add User Autocomplete, makes the Add Existing User workflow a bit easier, by adding autocomplete/autosuggest to the Email Address/Username field. Just start typing, and the plugin will return matching users; arrow down or click on the intended user to add her to the Add User list.

A few additional bonuses provided by the plugin, aside from autocomplete:

  • In addition to return email address and username matches, the plugin also checks against the display_name and user_url fields. So if my username is ‘admin’, and my email address is ‘bgorges@boonebgorges.com’, but my display name around the site is ‘Boone Gorges’, you’ll be able to find me by searching on ‘Boone’.
  • You can add many users to a blog at once. Search for one user, select and hit Return, and then search for another.
  • Prettier success messages. When you submit the Add New User page, your success message will give you a list of the users invited, instead of a generic “Invitations have been sent” type message.

Add User Autocomplete requires WP 3.1 and JavaScript. The plugin was developed for the CUNY Academic Commons. Check out the plugin at wordpress.org or follow its development at Github.

New BuddyPress plugin: BP Better Directories

BP Better Directories

BP Better Directories

BP Better Directories is a new BuddyPress plugin that will turn your (kinda boring) member directories into something a lot slicker. Site admins select which fields they’d like to be filterable in member directories. Site visitors can then use a nice AJAX interface for narrowing search results.

This plugin is being developed for the CUNY Academic Commons, and is in early beta. Don’t use on a live site. There’s also a pretty good chance that the technique I’m using in the guts of the plugin won’t scale all that well without proper caching. You have been warned! (Also, it requires at least BP 1.5.1.)

Download the plugin or follow its development on Github.

I develop free software because of CUNY and Blackboard

For two reasons, Blackboard is the key to why I develop free software.

The first reason is historical. I first got into free software development because of my work with the CUNY Academic Commons project. As spearheaded by Matt Gold, George Otte and others, the Commons is intended to create a space, using free software like WordPress and MediaWiki for members of the huge community of the City University of New York to discover each other and work together. The project is not pitched as a Blackboard alternative, for a number of reasons (primary among which is that the Commons’s Terms of Service prohibit undergraduate courses from being held on the site). Still, the Commons was conceived, at least in part, out of frustration about the near lack of collaborative tools and spaces in CUNY. And more than anything else, Blackboard (by which I mean Blackboard Learn, the proprietary learning management software that has been CUNY’s official courseware for quite a few years) is the embodiment of what can be so frustrating about academic technology at CUNY: central management, inflexibility, clunkiness, anti-openness. In this way, Blackboard begat the CUNY Academic Commons, and the CUNY Academic Commons begat Boone the developer.

There is another reason why Blackboard is integral to my free software development. It is ideological.

Short version: I love CUNY and I love public education. Blackboard is a parasite on both. Writing free software is the best way I know to disrupt the awful relationship between companies like Blackboard and vulnerable populations like CUNY undergraduates.

Here’s the longer version. I’ve been affiliated with CUNY in a number of capacities over the last decade: PhD student, adjunct lecturer, graduate fellow, full-time instructional technologist, external contractor. I’ve seen many parts of CUNY from many different points of view. Like so many others who have philandered their way through CUNY’s incestuous HR departments, my experience has rendered a decidedly love/hate attitude toward the institution. You can get a taste of the what CUNY hate looks like by glancing at something like @CUNYfail. The love runs deeper. Those fortunate enough to have “gotten around” at CUNY can attest to the richness of its varied campus cultures. In every office and every department on every campus, you’ll meet people who are innovating and striving to get their work done, in spite of a bureaucracy that sometimes feels designed to thwart.

And the students. CUNY is the City University of New York, the City University. It belongs to New York, and its history is tied up with the ideals of free education for New York’s residents. While the last few decades have seen the institution (as a whole, as well as a collection of campuses) evolve away from these ideals in various official and unofficial ways, it’s impossible to step into a CUNY classroom without getting a sense that CUNY still serves as a steward for New York’s future. CUNY is too huge and its population too varied to make general statements about the student body, but I’ll say anecdotally that, of all the universities I’ve been associated with, none even approach the level of racial, economic, and academic diversity that you find on a single campus, to say nothing of the system as a whole. CUNY is (to use a lame but apt cliché) a cross-section of New York: her first-generation Americans, her first-generation college students, her rich and her poor, her advantaged and her vulnerable. (See also Jim Groom’s I Bleed CUNY, which makes a similar point with a lot less abandon.)

Public education is a public trust, maybe the most important equalizer a state can provide for its citizens. CUNY, with the population of New York City as its public, could demonstrate the full potential of public education in a more complete and visible way than perhaps any other public university. It’s for this reason that it breaks my heart and boils my blood to see CUNY money – which is to say, student tuition and fees – poured into a piece of software like Blackboard.

In virtue of their age, undergraduates are inherently a vulnerable population, and CUNY undergraduates – reflecting as they do the full demographic spectrum of New York City itself – are doubly vulnerable. Many CUNY undergraduates go to CUNY because if they didn’t, they wouldn’t go to college at all. This imposes certain moral strictures on those responsible for managing and spending the money paid by CUNY students in tuition and fees. Wasting CUNY money is a far worse crime than wasting, say, shareholder money in a private company. Shareholders have freedom; if they don’t like your management, they vote with their feet/wallets/brokers. CUNY students, by and large, do not have the same freedom; it’s safe to say that, for most CUNY students most students, big-ticket NYU and Ivy Columbia are not reasonable alternatives. CUNY students are, in this sense, captive, which means that their hard-earned tuition money is captive as well. Thus it is a very bad thing to spend that money on things that aren’t worth it.

And Blackboard is not worth it. Vats of digital ink have been spilled expounding Blackboard’s turdiness, and this is no place to rehash all the arguments in depth. A short list, off the top of my head:

  • The software is expensive [EDIT 9-21-2011: See this post for more details on cost]
  • It’s extremely unpleasant to use.
  • It forces, and reinforces, an entirely teacher-centric pedagogical model.
  • It attempts to do the work of dozens of applications, and as a result does all of them poorly.
  • Blackboard data is stored in proprietary formats, with no easy export features built in, which creates a sort of Hotel California of educational materials
  • The very concept of a “learning management system” may itself be wrongheaded.
  • As recently reported, the software may be insecure, a fact that the company may have willingly ignored.
  • Blackboard’s business practices are monopolistic, litigious, and borgish

In short, Blackboard sucks. Blackboard supporters might claim that some, or even most, of the criticisms leveled above are false, or that they apply equally to other web software. Maybe. And I certainly don’t mean to downplay the difficulty of creating or assembling a suite of software that does well what Blackboard does poorly. But the argument against spending student money on something like Blackboard goes beyond a simple tally of weaknesses and strengths. As Jim Groom and others have argued for years, shelling out for Blackboard means sending money to a big company with no vested interest in the purposes of the institution, which in the case of CUNY is nothing less than the stewardship of New York City’s future, while the alternative is to divert money away from software licenses and into people who will actually support an environment of learning on our campuses. Frankly, even if Blackboard were a perfect piece of software, and even if its licensing and hosting fees were half of what it costs to hire full-time instructional technologists, programmers, and the like to support local instances of free software; even if these things were true, Blackboard would still be the wrong choice, because it perverts the goals of the university by putting tools and corporations before people. The fact that Blackboard is so expensive and so shitty just makes the case against it that much stronger.

As long as our IT departments are dominated by Microsoft-trained technicians and corporate-owned CIOs, perhaps the best way to advance the cause – the cause of justice in the way that student money is spent – is to create viable alternatives to Blackboard and its ilk, alternatives that are free (as in speech) and cheap (as in beer). This, more than anything else, is why I develop free software, the idea that I might play a role in creating the viable alternatives. In the end, it’s not just about Blackboard, of course. The case of Blackboard and CUNY is a particularly problematic example of a broader phenomenon, where vulnerable populations are controlled through proprietary software. Examples abound: Facebook, Apple, Google. (See also my Project Reclaim.) The case of Blackboard and its contracts with public institutions like CUNY is just one instance of these exploitative relationships, but it’s the instance that hits home the most for me, because CUNY is such a part of me, and because the exploitation is, in this case, so severe and so terrible.

On average, I spend about half of my working week doing unpaid work for the free software community. Every once in a while, I get discouraged: by unreasonable feedback, by systematic inertia, by community dramas, by my own limitations as a developer, and so on. In those moments, I think about CUNY, and I think about Blackboard, and I feel the fire burn again. For that, I say to CUNY (which I love) and Blackboard (which I hate): Thanks for making me into a free software developer.

Redirect BuddyPress activity reply links to forum’s “Leave a Reply”

Activity stream replies in BuddyPress are pretty cool, but they have the potential to be confusing. On the CUNY Academic Commons, we have disabled activity replies for activity entries related to blogs and forums, because allowing replies in these cases has the potential to confuse users and fracture conversation.

There are a number of ways that this could (and should, and will!) be improved in future versions of BuddyPress. But, for now, here’s a trick. The following code will change the behavior of the Reply buttons for forum-related activity entries (new forum topics, and forum topic replies), so that instead of sliding down the inline activity comment box, it goes to the Reply form on the forum topic itself.

Side note: This seems like it’d be an easy thing to do, but it turns out to be somewhat complex. As I explain in the inline documentation, the issue of pagination means that there’s no predictable way to easily concatenate a URL for a topic’s reply box (this is one of the things I want to fix in BP core) – you have to fetch the number of total replies and figure out the last page from there. Also, in the case of topic replies, you have to do an additional query to get the id of the topic that the post belongs to, because that info is not stored in the activity table. The function cac_insert_comment_reply_links() below tries to consolidate these lookups to add as few queries as possible to the pageload.

Second side note: This code is not particularly beautiful. It makes direct queries to the bbPress database tables. So sue me.

OK, so the code itself. First, put this chunk into your bp-custom.php file.

/**
 * Gets accurate reply URLs for the activity stream
 *
 * Getting accurate Reply links for forum topics is tricky because of pagination - you need to know
 * how many total posts are in the topic so that you can figure out what the last page should be.
 * Moreover, the forum reply activity items don't have the topic_id stored with them. This function
 * attempts to minimize DB queries by looking up all topic_ids at once, then looking up all post
 * counts at once - adding 2 queries for the activity loop is better than 20.
 *
 * Todo: Get a real redirecter into BuddyPress itself
 */
function cac_insert_comment_reply_links( $has_comments ) {
	global $activities_template, $wpdb, $bbdb;

do_action( 'bbpress_init' );

$topics_data = array();
	$posts_data = array();
	foreach( $activities_template->activities as $key => $activity ) {
		if ( $activity->type == 'new_forum_topic' ) {
			$topic_id = $activity->secondary_item_id;
			$topics_data[$topic_id]['url'] = $activity->primary_link;
			$topics_data[$topic_id]['activity_key'] = $key;	
		}

if ( $activity->type == 'new_forum_post' ) {
			$post_id = $activity->secondary_item_id;
			$posts_data[$post_id]['url'] = array_pop( array_reverse( explode( '#', $activity->primary_link ) ) );
			$posts_data[$post_id]['activity_key'] = $key; 
		}
	}

// In cases where we only have the post id, we must do an extra query to get topic ids
	if ( !empty( $posts_data ) ) {
		$post_ids 	= array_keys( $posts_data );
		$post_ids_sql 	= implode( ',', $post_ids );
		$sql 		= $wpdb->prepare( "SELECT topic_id, post_id FROM {$bbdb->posts} WHERE post_id IN ({$post_ids_sql})" );
		$post_topic_ids = $wpdb->get_results( $sql );

// Now that we have the topic IDs, we can add that info to $topics_data for the main query
		foreach( $post_topic_ids as $post_topic ) {
			$topics_data[$post_topic->topic_id] = $posts_data[$post_topic->post_id];
		}
	}

// Now for the main event
	// First, make a topic list and get all the associated posts
	$topic_ids 	= implode( ',', array_keys( $topics_data ) );
	$sql		= $wpdb->prepare( "SELECT topic_id, post_id FROM {$bbdb->posts} WHERE topic_id IN ({$topic_ids})" );
	$posts		= $wpdb->get_results( $sql );

// Now we get counts. BTW it sucks to do it this way
	$counter	= array();
	foreach( $posts as $post ) {
		if ( empty( $counter[$post->topic_id] ) )
			$counter[$post->topic_id] = 1;
		else
			$counter[$post->topic_id]++;
	}

// Finally, concatenate the reply url and put it in the activities_template
	foreach( $topics_data as $topic_id => $data ) {
		$total_pages = ceil( $counter[$topic_id] / 15 );	
		$reply_url = cac_forum_reply_url( $data['url'], $total_pages, 15 );
		$key = $data['activity_key'];
		$activities_template->activities[$key]->reply_url = $reply_url;
	}

return $has_comments;
}
add_action( 'bp_has_activities', 'cac_insert_comment_reply_links' );

/**
 * Filters the url of the activity reply link to use reply_url, if present
 */
function cac_filter_activity_reply_link( $link ) {
	global $activities_template;

if ( !empty( $activities_template->activity->reply_url ) )
		return $activities_template->activity->reply_url;
	else
		return $link;
}
add_action( 'bp_get_activity_comment_link', 'cac_filter_activity_reply_link' );

/**
 * Echoes the proper CSS class for the activity reply link. This is necessary to ensure that 
 * the JS slider does not appear when we have a custom reply_url.
 */
function cac_activity_reply_link_class() {
	global $activities_template;

if ( !empty( $activities_template->activity->reply_url ) )
		echo 'class="acomment-reply-nojs"';
	else
		echo 'class="acomment-reply"';
}

/**
 * A replacement for bp_activity_can_comment(). Todo: deprecate into a filter when BP 1.3 comes out
 */
function cac_activity_can_comment() {
	global $activities_template, $bp;

if ( false === $activities_template->disable_blogforum_replies || (int)$activities_template->disable_blogforum_replies ) {
		// If we've got a manually created reply_url (see cac_insert_comment_reply_links(), return true
		if ( !empty( $activities_template->activity->reply_url ) )
			return true;

if ( 'new_blog_post' == bp_get_activity_action_name() || 'new_blog_comment' == bp_get_activity_action_name() || 'new_forum_topic' == bp_get_activity_action_name() || 'new_forum_post' == bp_get_activity_action_name() )
			return false;
	}

return true;
}

You’ll note that there are a few places in that code where the number 15 is mentioned explicitly. I’m assuming that you’re using 15 posts-per-page for your single topic pagination. You can change this number accordingly if you want.

Next, you’ll have to make a few changes in your theme’s activity/entry.php to account for the changes. There are two relevant changes. First, you’ll be removing the activity reply button’s CSS class (hardcoded by default) and replacing it with the dynamically generated version in cac_activity_reply_link_class(). Second, you’ll be swapping out the checks for bp_activity_can_comment() with cac_activity_can_comment(), so that you can still block blog-activity comments. The code below is lines 27-29 of my activity/entry.php – you should be able to figure out which lines to replace with the following, as I haven’t changed much.

<?php if ( is_user_logged_in() && cac_activity_can_comment() ) : ?>
	<a href="<?php bp_activity_comment_link() ?>" <?php cac_activity_reply_link_class() ?> id="acomment-comment-<?php bp_activity_id() ?>"><?php _e( 'Reply', 'buddypress' ) ?> (<span><?php bp_activity_comment_count() ?></span>)</a>
<?php endif; ?>

Finally, because you’ve changed the CSS selector on some of the reply buttons, you’ll want to add some styles to your stylesheet. These are borrowed right from bp-default.

.activity-list div.activity-meta a.acomment-reply-nojs {
	background: #fff9db;
	border-bottom: 1px solid #ffe8c4;
	border-right: 1px solid #ffe8c4;
	color: #ffa200;
}

div.activity-meta a.acomment-reply-nojs:hover {
	background: #f7740a;
	color: #fff;
    border-color: #f7740a;
}

Good luck!