Tag Archives: WordPress

New WordPress plugin: Boone’s Sortable Columns

Boone’s Sortable Columns is a new WordPress plugin to make it easier for developers of WordPress plugins and themes to create sortable data tables and lists. Like my recent Boone’s Pagination, this is not a plugin for end users but for developers. (And, by the way, Boone’s Sortable Columns goes with Boone’s Pagination like strawberries go with rhubarb. More on that in a minute.)

If you’re building a client site, you can activate the plugin directly in the WordPress Dashboard, and instantiate the class anywhere in your installation. Or, if you’re the developer of a theme or plugin that you’ll be distributing for wide use, you can simply copy the file boones-sortable-columns.php into your own plugin/theme (I recommend a directory called ‘lib’), and then require it manually when you want to do some sortin’.

The plugin is extensively documented inline. I highly recommend that you crack open the source if you have questions about the kinds of options that Boone’s Sortable Columns provides. But, as a quick introduction, here’s a simple example of how you might use the class in your own plugin. Let’s say you have a custom post type called ‘restaurant’, and you want to display a list of restaurants that is sortable by restaurant name, the date when the restaurant was added to the site, and the name of the person who submitted the restaurant (the post author). Here’s the code you might use for a simple table, with my comments and explanations inline.

[code language=”php”]
// Include Boone’s Sortable Columns. You only need to do this if you’re not running it as a
// standalone plugin. Obviously, you’ll need to put the proper path for your plugin.
require_once( WP_CONTENT_DIR . ‘/my-plugin-name/lib/boones-sortable-columns.php’ );

// Define an array of column data.
// For more details on these (and more!) arguments, see the plugin’s inline docs.
$cols = array(
array(
‘name’ => ‘title’,
‘title’ => ‘Restaurant Name’,
‘css_class’ => ‘restaurant-name’,
‘is_default’ => true
),
array(
‘name’ => ‘author’,
‘title’ => ‘Creator’,
‘css_class’ => ‘creator’
),
array(
‘name’ => ‘date’,
‘title’ => ‘Date Added’,
‘css_class’ => ‘date-added’,
‘default_order’ => ‘desc’
)
);

// Create the sorting object based on this column data.
$sortable = new BBG_CPT_Sort( $cols );

// Use some of the data from the $sortable object to help you build a posts query.
// In this example, I’ve intentionally chosen sortable options that can be passed directly to the
// ‘orderby’ param, because they are accepted directly by WP_Query (see http://codex.wordpress.org/Function_Reference/WP_Query#Order_.26_Orderby_Parameters for more details).
// In real life, your query might require something more complex, like the building of a meta_query.
$query_args = array(
‘post_type’ => ‘restaurant’,
‘orderby’ => $sortable->get_orderby,
‘order’ => $sortable->get_order
);

// Fire the query
$restaurants = new WP_Query( $query_args );

// Now let’s create the table markup. Here’s where the magic really happens.
?>

have_posts() ) : ?>

have_columns() ) : ?>
have_columns ) : $sortable->the_column() ?>

have_posts() ) : $restaurants->the_post() ?>

“>the_column_title() ?>

[/code]

Boone’s Sortable Columns handles everything else for you. It figures out what the current orderby/order parameters are. It figures out what the href on the column headers should be. It even creates CSS selectors for the <th> element that match what WP itself uses in the ‘widefat’ tables on the Dashboard, so that you can take advantage of all the pretty JS and CSS built into the WP admin interface. In fact, if this were a Dashboard page, you could simplify the <thead> described above as follows:

[code language=”php”]

have_columns() ) : ?>
have_columns ) : $sortable->the_column() ?>
the_column_th() ?>

[/code]

The method the_column_th() will build the markup for you. Ain’t that the bee’s knees?

Together with Boone’s Pagination

Boone’s Sortable Columns is made more scrumptious when combined with Boone’s Pagination. Here’s a compressed (uncommented) version of the example above, this time with some pagination code.

[code language=”php”]
require_once( WP_CONTENT_DIR . ‘/my-plugin-name/lib/boones-sortable-columns.php’ );
require_once( WP_CONTENT_DIR . ‘/my-plugin-name/lib/boones-pagination.php’ );

$cols = array(
array(
‘name’ => ‘title’,
‘title’ => ‘Restaurant Name’,
‘css_class’ => ‘restaurant-name’,
‘is_default’ => true
),
array(
‘name’ => ‘author’,
‘title’ => ‘Creator’,
‘css_class’ => ‘creator’
),
array(
‘name’ => ‘date’,
‘title’ => ‘Date Added’,
‘css_class’ => ‘date-added’,
‘default_order’ => ‘desc’
)
);

$sortable = new BBG_CPT_Sort( $cols );

$pagination = new BBG_CPT_Pagination();

$query_args = array(
‘post_type’ => ‘restaurant’,
‘orderby’ => $sortable->get_orderby,
‘order’ => $sortable->get_order,
‘paged’ => $pagination->get_paged,
‘per_page’ => $pagination->get_per_page
);

$restaurants = new WP_Query( $query_args );

$pagination->setup_query( $restaurants );
?>

have_posts() ) : ?>

have_columns() ) : ?>
have_columns ) : $sortable->the_column() ?>

have_posts() ) : $restaurants->the_post() ?>

“>the_column_title() ?>

[/code]

Sweet, huh? If you want to see some real-life, more complex examples of Boone’s Pagination and Boone’s Sortable Columns in use, check out Invite Anyone (where the data is a custom post type, as in the example above – see method invite_anyone_settings_mi_content()) or Unconfirmed (where the data actually comes from a custom query of the wp_signups table – look for the admin_panel_main() method).

You can get Boone’s Sortable Columns from the wordpress.org repo or follow its development on Github.

New WordPress plugin: Unconfirmed

If you’ve ever been responsible for supporting an installation of WordPress Multisite with open registration, you know that the activation process can be a significant source of headaches. Sometimes activation emails get caught by spam filters. Sometimes they are overlooked and deleted by unwitting users. And, to complicate matters, WP’s safeguards prevent folks from re-registering with the same username or email address. This can result in a lot of support requests that are not particularly easy to handle. Aside from reaching manually into the database for an activation key, there’s not much the admin can do to help the would-be member of the site.

The Unconfirmed Dashboard panel

The Unconfirmed Dashboard panel

My new WordPress plugin Unconfirmed eases this problem a bit, by providing WPMS admins with a new set of tools for managing unactivated registrations. (By naming it “Unconfirmed”, I fully expect that the plugin will join some great movies and books in the pantheon of Important Cultural Objects.) Unconfirmed adds a new panel to your Network Admin Dashboard (under the Users menu). When you visit the Unconfirmed panel, it gives you a list of all pending registrations on your system. The list is easily sortable by registration date, username, email address, and activation key. For each unactivated registration, there are two actions that the admin can perform. “Resend Activation Email” does exactly what it says: it sends an exact duplicate of the original activation email, as created by the WordPress core activation notification functions. “Activate” allows admins to activate a pending registration manually, which will trigger the activation success email to the user.

At the moment, Unconfirmed is compatible with WordPress Multisite (aka Network mode) only. In the future, I may expand the plugin to work with non-MS installations of WP. Unconfirmed works with BuddyPress, too. The plugin was developed for use on the CUNY Academic Commons.

Download Unconfirmed from the wordpress.org repo or follow its development on Github.

Invite Anyone 0.9: Admin management and stats

Stats

Stats

I just released version 0.9 of Invite Anyone. IA is a a BuddyPress plugin that lets users send email invitations to friends who may be interested in joining the site. IA has always kept track of sent invitations at the user level, but there’s never been a good way for get an overall administrator’s view of invitations – until now.

Invite Anyone 0.9 includes a revamp of the plugin’s Dashboard panel, including two brand new tabs. Manage Invitations lets administrators see all of the invitations that have been sent on the system, complete with sortable columns for easy lookups. And Stats lets you see, at a glance, how successful your invitation campaigns are going. Bonus: If you’ve got CloudSponge integration turned on – which you should really try, as it’s a great driver of traffic and membership – CS-specific invitations are broken out into separate Stats columns, allowing you to compare overall invitation acceptance rates with those coming from CloudSponge.

Download Invite Anyone.

New WordPress plugin: Boone’s Pagination

The more I use custom post types in WordPress, the more I find myself leaving until the last minute (and often forgetting) the issue of pagination. WordPress has paginate_links() and a couple other functions to help make pagination easier, but it’s still kind of a pain, and I ended up rewriting certain common functions (like functions to get the per_page parameter out of the $_GET global, etc) in multiple projects.

So I took a little time to write a reusable class for a lot of these common functions. It’s called – tada! – Boone’s Pagination. This is not so much a plugin in itself (though if you plan to use it a lot on a client site, you can activate it as a plugin) as it is a helper for people building themes and plugins. Here’s how you use it:

[code language=”php”]
require_once( dirname(__FILE__) . ‘/boones-pagination.php’ ); // You only need this if you don’t activate it as a plugin

$pagination = new BBG_CPT_Pag;

$my_query_args = array(
‘post_type’ => ‘my_post_type’,
‘paged’ => $pagination->get_paged,
‘posts_per_page’ => $pagination->get_per_page // This does all the work of fetching the pagination arguments out of the $_GET global
);

$my_query = new WP_Query( $my_query_args );
$pagination->setup_query( $my_query ); // Now that you’ve run the query, finish populating the object

if ( $my_query->have_posts() ) :
while ( $my_query->have_posts() ); $my_query->the_post();
the_title(); // Do whatever you’d normally do in The Loop
endif;

$pagination->currently_viewing_text(); // eg “Viewing 11-20 of 34”
$pagination->paginate_links(); // These are the links themselves
[/code]

I’ll keep adding handy functions to the class as I think of them, but you should feel free to extend it yourself. I’ve also added plenty of inline docs, so crack open the source to learn more about it. Follow Boone’s Pagination on Github.

Announcing BuddyDrop

//

Hopefully, readers of this blog will know that I have a keen interest in social software. For some time, I’ve been
a leader in the development of BuddyPress, a plugin that lets users take control of their social networking ex-
periences. Up to now, I’ve focused on BuddyPress http://buddypress.org, the free plugin for WordPress that
puts a number of social networking features on top of an existing WP installation. I love WordPress;
yet lately I’ve been wondering if there’s a way for BuddyPress to break free of some of the WP’s inherent limitations.

BuddyDrop

BuddyDrop

And so today, I’m making my move, and announcing an exciting new project: BuddyDrop. BuddyDrop is a
port of BuddyPress to Drupal, and represents a move away from WordPress-centric development and into the
realm of truly powerful content management. WordPress has been, without question, an outstanding
incubator for BuddyPress. But WP has also held BP back. The move to Drupal introduces true CMS features,
like global taxonomies and fine-grained user access control, that are the foundation of the best social software.

Fans of BuddyPress may greet this news with mixed emotions. BuddyPress development will probably carry
on, at least for a while. For now, I’m leading the BuddyDrop development team, but I won’t be alone: a number of
other prominent members of the BuddyPress and WordPress development communities will be announced in the weeks
leading up to our initial beta release. I’m confident that, as they experiment with Drupal and BuddyDrop, devs and
site owners will migrate their sites, and their development resources, from BP to BD.

Don’t want to wait for the first public beta of BuddyDrop, scheduled to be released in the first part of May? I’m accepting
a small number of applications for a private alpha testing phase that will begin in a week or so. If you’re interested,
you can sign up by visiting buddydrop.com, where you’ll also find screenshots and learn more about BuddyDrop.

New BuddyPress plugin: BuddyPress Docs

BuddyPress Docs edit screen

BuddyPress Docs edit screen

Today I am releasing the first public beta of a significant new BuddyPress plugin: BuddyPress Docs. BuddyPress Docs is a collaborative, front-end, rich-text, document editing tool for BuddyPress groups (individual user Docs are an upcoming feature). Read much more about the plugin’s features.

This plugin has been developed for the CUNY Academic Commons (though it won’t be live there for a few weeks). Today’s release is a pre-stable beta – it should run fine, but there are bound to be bugs, and you probably won’t want to run it on a production site quite yet.

You can follow the plugin’s development at http://github.com/boonebgorges/buddypress-docs.

Hardening BuddyPress Group Documents

The BuddyPress Group Documents plugin allows groups a handy way for users to share documents with fellow members of a BP group. It’s crucial to the work that is done on the CUNY Academic Commons. But, by default, the plugin stores documents in a subdirectory of your WP uploads folder (usually /wp-content/blogs.dir/ on multisite). That means that documents are available directly, to anyone who has the URL, regardless of the public/private/hidden status of groups. This isn’t a problem from within BuddyPress, since URLs for the documents only appear inside of the protected group interface. But if the URL is shared, then the document becomes publicly available. Worse, if someone posts the URL of a document in a public place, search engine bots will find it, and the contents of the document could end up in Google.

I wrote a few helper functions to change this behavior. The strategy is this: Move the files so that they are not accessible via URL, ie in a directory above the web root. (In my case, it’s a directory called bp-group-documents, just above my web root.) Then, catch requests of a certain type (I’ve chosen to go with a URL parameter get_group_doc=), and check them to see whether the current user has the adequate permission to access the document in question. Finally, make sure that all of the URLs and paths that BPGD uses to upload and display documents are filtered to the updated versions. I’ve provided my code below – use and modify at your pleasure. You should be able to place it in your plugins/bp-custom.php file, and then move your existing docs from their current location (probably something like wp-content/blogs.dir/1/files/group-documents) to the new directory.

I also added a line to my .htaccess file to ensure that requests to the old URLs are redirected to the new, hardened URL. That line is this:
[code]
RewriteRule ^wp-content/blogs.dir/1/files/group-documents/(.*) /?get_group_doc=$1 [R,L]
[/code]
Obviously, you may have to modify it for different file paths.

EDITED Feb 8, 2011 to include the code for creating directories when none exist

[code language=”php”]
define( ‘BP_GROUP_DOCUMENTS_SECURE_PATH’, substr( ABSPATH, 0, strrpos( rtrim( ABSPATH, ‘/’ ), ‘/’ ) ) . ‘/bp-group-documents/’ );

function cac_filter_doc_url( $doc_url, $group_id, $file ) {
$url = bp_get_root_domain() . ‘?get_group_doc=’ . $group_id . ‘/’ . $file;
return $url;
}
add_filter( ‘bp_group_documents_file_url’, ‘cac_filter_doc_url’, 10, 3 );

function cac_filter_doc_path( $doc_url, $group_id, $file ) {
$document_dir = BP_GROUP_DOCUMENTS_SECURE_PATH . $group_id . ‘/’;

if ( !is_dir( $document_dir ) )
mkdir( $document_dir, 0755, true );

$path = BP_GROUP_DOCUMENTS_SECURE_PATH . $group_id . ‘/’ . $file;
return $path;
}
add_filter( ‘bp_group_documents_file_path’, ‘cac_filter_doc_path’, 10, 3 );

function cac_catch_group_doc_request() {
$error = false;

if ( empty( $_GET[‘get_group_doc’] ) )
return;

$doc_id = $_GET[‘get_group_doc’];

// Check to see whether the current user has access to the doc in question
$file_deets = explode( ‘/’, $doc_id );
$group_id = $file_deets[0];
$group = new BP_Groups_Group( $group_id );

if ( empty( $group->id ) ) {
$error = array(
‘message’ => ‘That group does not exist.’,
‘redirect’ => bp_get_root_domain()
);
} else {
if ( $group->status != ‘public’ ) {
// If the group is not public, then the user must be logged in and
// a member of the group to download the document
if ( !is_user_logged_in() || !groups_is_user_member( bp_loggedin_user_id(), $group_id ) ) {
$error = array(
‘message’ => sprintf( ‘You must be a logged-in member of the group %s to access this document. If you are a member of the group, please log into the site and try again.’, $group->name ),
‘redirect’ => bp_get_group_permalink( $group )
);
}
}

// If we have gotten this far without an error, then the download can go through
if ( !$error ) {

$doc_path = BP_GROUP_DOCUMENTS_SECURE_PATH . $doc_id;

if ( file_exists( $doc_path ) ) {
$mime_type = mime_content_type( $doc_path );
$doc_size = filesize( $doc_path );

header(“Cache-Control: public, must-revalidate, post-check=0, pre-check=0”);
header(“Pragma: hack”);

header(“Content-Type: $mime_type; name='” . $file_deets[1] . “‘”);
header(“Content-Length: ” . $doc_size );

header(‘Content-Disposition: attachment; filename=”‘ . $file_deets[1] . ‘”‘);
header(“Content-Transfer-Encoding: binary”);
ob_clean();
flush();
readfile( $doc_path );
die();

} else {
// File does not exist
$error = array(
‘message’ => ‘The file could not be found.’,
‘redirect’ => bp_get_group_permalink( $group ) . ‘/documents’
);
}
}
}

// If we have gotten this far, there was an error. Add a message and redirect
bp_core_add_message( $error[‘message’], ‘error’ );
bp_core_redirect( $error[‘redirect’] );
}
add_filter( ‘wp’, ‘cac_catch_group_doc_request’, 1 );

// http://www.php.net/manual/en/function.mime-content-type.php#87856
if(!function_exists(‘mime_content_type’)) {

function mime_content_type($filename) {

$mime_types = array(

‘txt’ => ‘text/plain’,
‘htm’ => ‘text/html’,
‘html’ => ‘text/html’,
‘php’ => ‘text/html’,
‘css’ => ‘text/css’,
‘js’ => ‘application/javascript’,
‘json’ => ‘application/json’,
‘xml’ => ‘application/xml’,
‘swf’ => ‘application/x-shockwave-flash’,
‘flv’ => ‘video/x-flv’,

// images
‘png’ => ‘image/png’,
‘jpe’ => ‘image/jpeg’,
‘jpeg’ => ‘image/jpeg’,
‘jpg’ => ‘image/jpeg’,
‘gif’ => ‘image/gif’,
‘bmp’ => ‘image/bmp’,
‘ico’ => ‘image/vnd.microsoft.icon’,
‘tiff’ => ‘image/tiff’,
‘tif’ => ‘image/tiff’,
‘svg’ => ‘image/svg+xml’,
‘svgz’ => ‘image/svg+xml’,

// archives
‘zip’ => ‘application/zip’,
‘rar’ => ‘application/x-rar-compressed’,
‘exe’ => ‘application/x-msdownload’,
‘msi’ => ‘application/x-msdownload’,
‘cab’ => ‘application/vnd.ms-cab-compressed’,

// audio/video
‘mp3’ => ‘audio/mpeg’,
‘qt’ => ‘video/quicktime’,
‘mov’ => ‘video/quicktime’,

// adobe
‘pdf’ => ‘application/pdf’,
‘psd’ => ‘image/vnd.adobe.photoshop’,
‘ai’ => ‘application/postscript’,
‘eps’ => ‘application/postscript’,
‘ps’ => ‘application/postscript’,

// ms office
‘doc’ => ‘application/msword’,
‘rtf’ => ‘application/rtf’,
‘xls’ => ‘application/vnd.ms-excel’,
‘ppt’ => ‘application/vnd.ms-powerpoint’,

// open office
‘odt’ => ‘application/vnd.oasis.opendocument.text’,
‘ods’ => ‘application/vnd.oasis.opendocument.spreadsheet’,
);

$ext = strtolower(array_pop(explode(‘.’,$filename)));
if (array_key_exists($ext, $mime_types)) {
return $mime_types[$ext];
}
elseif (function_exists(‘finfo_open’)) {
$finfo = finfo_open(FILEINFO_MIME);
$mimetype = finfo_file($finfo, $filename);
finfo_close($finfo);
return $mimetype;
}
else {
return ‘application/octet-stream’;
}
}
}

[/code]

New WordPress plugin: Prezi WP

I had a request to allow Prezis to be embedded on the CUNY Academic Commons, but the one plugin I tried for that purpose seemed to be broken and overengineered. So I took an hour and wrote my own: Prezi WP. In brief, it gives you a [prezi] shortcode for easy embedding of those Mind Blowing, Non-Linear bad boys.

It’ll be in the wordpress.org repository soon enough, but for now you can read more and download it here.

Group Announcements tab in BuddyPress

Cross-posted at the CUNY Academic Commons Development blog

I had a request or two to explain how I built the Group Announcements feature on the CUNY Academic Commons. Here goes.

Brief background: When the Commons was upgraded to BuddyPress 1.2, we got the benefit of interactive activity streams everywhere, including groups. This caused some confusion, however, as users were uncertain where conversation best fit into the Commons’s architecture: in the Forums (where it had been traditionally), or in the activity stream. In some communities this kind of fracturing might be okay or even welcome, but in ours it was confusing. At the same time, we wanted a way for group admins and mods to send important notices to the members of their groups. By taking the group activity updates and repurposing it as a Group Announcements section, I was able to kill two birds with one stone: providing an announcement space for mods, while focusing extended discussion in the forums.

You can download the CAC Group Announcements plugin here.

I’m not putting this in the repo at the moment because I don’t want to build a proper admin UI and support it 🙂 For that reason, here is a primer on how the plugin works – if you want to customize or maintain it, you’re on your own, buster.

  1. The CAC_Group_Announcements class is an instance of the BuddyPress Group Extension API. It is responsible for creating the Announcements tab markup and adding it to the nav. You’ll notice that the majority of the markup is created by including bp-default’s activity-loop.php and post-form.php templates. You could customize this more if you wanted.
  2. bp_is_group_announcements() is a little template tag that can be used to test whether you’re looking at a group announcements page. This is needed for the activity filter, in step 3.
  3. cac_set_announcement_filter() adds a filter to the bp_has_activities query string when you are looking at an announcements page, so that it only displays activity items of the type activity_update. In other words, when you are looking at the regular activity stream for the group, you see all of the associated group activity items (new members, forum posts, etc) but when you’re on the announcements page you only see activity updates (which, remember, have been repurposed as announcements).

As I look at the code, I see that there are things I would definitely change if I were going to make this into a distributed plugin. If you want to make those changes, be my guest. You’re welcome to help each other in the comment section, but I won’t be formally supporting this, as it is a very basic hack that should happen at the theme level anyway. Good luck!