I’m on the admin team for the Flipside Ticket Exchange, better known as Bob’s List, also known as !Bob’s List, also known as Not Not Bob’s List since !Bob moved on to better things. This year it is running on a WordPress install. I configured that setup, and am writing down what I did so I don’t have to try to remember it.

Last year, Bob’s List ran on a package called Noah’s Classified, and although none of the admin’s especially liked it, it looked like we were going to use it again this year. It didn’t support some of the functionality that we wanted, so I looked at other options. Flipside in 2012 had a large number of ticket requests (more than 400 requests representing about 650 tickets) that lost out in our ticket lottery. We wanted to give priority treatment to these people, but Noah’s didn’t make that easy. There had been a small number in 2011, and it was feasible for the admins to mark those people directly in the MySQL database. This year, that wouldn’t be possible.

I knew that it should be possible to build a fairly customized solution on top of WordPress by taking advantage of custom post types and fields, custom taxonomies, and custom roles/role capabilities. And that’s what I did.

There were two key plugins in getting this functionality: Types and Role Scoper. Types let me set up sets of fields that different ads would use and associate them to post types; it also let me define a custom taxonomy. Role Scoper has a very confusing interface. (I deleted it entirely the first time I looked at it. And the second.) But once I figured it out, it let me restrict certain user roles to creating certain post types, and even to creating certain categories (there are other ways of skinning that cat though).

First, I set up two new roles that are based on the “Subscriber” role (which has the fewest privileges). I don’t think Role Scoper actually lets you create new roles, but there are several other plugins that do.

Second, I created custom content types for each type of ad that we’d be hosting: Ticket Requests, Ticket Offers, Theme-camp Invites, and Theme-camp Seekers.

Third, I created a custom taxonomy, Grade (actually I used the word “tier,” but that’s too easy to confuse with ticket tiers, so let’s pretend I didn’t), that applied to the Ticket Request content type. This would indicate whether the ticket request was posted by a Returnee or a general Advertiser. I then created two grades: General and Gold-star.

Finally, I started setting privileges. For all the content types except for Ticket Request, I went into Role Scoper and assigned both Returnees and Advertisers the role of Author for that content type.

To handle Ticket Requests, I went into the Grades tab in Role Scoper and assigned Advertisers to be authors of General posts, and Returnees to be authors of Gold-star posts. In hindsight, it would be possible to avoid using Grades entirely and sort by role.

Then it was time for some custom code. WordPress only shows the correct Grade to the correct role in the editing interface, but I didn’t want users to trouble about that (in fact, it’s possible to remove that meta box from the editing interface entirely, and I should have done that). The following code is derived from something I found online (I never would have figured this out on my own): it sets a post’s Grade based on the user’s role.

function user_has_role( $roles_to_check=array() ) {
  if( ! $roles_to_check ) return FALSE;
  global $current_user;
  get_currentuserinfo();
  $user_id = intval( $current_user->ID );
  if( ! $user_id ) { return FALSE; }
  $user = new WP_User( $user_id ); 
  return in_array( $roles_to_check, $user->roles, FALSE );
}
function mfields_set_default_object_terms( $post_id, $post ) {
    if ( 'publish' === $post->post_status )
     {
		$is_advertiser = user_has_role( array('advertiser') );
		$is_rejectee = user_has_role( array('rejectee') );
		if ($is_advertiser) {
        $defaults = array(
            'grade' => array( 'general' ),
            );
            }
		if ($is_rejectee) {
        $defaults = array(
            'grade' => array( 'goldstar' ),
            );
            }
            
        $taxonomies = get_object_taxonomies( $post->post_type );
        foreach ( (array) $taxonomies as $taxonomy ) {
            $terms = wp_get_post_terms( $post_id, $taxonomy );
            if ( empty( $terms ) && array_key_exists( $taxonomy, $defaults ) ) {
                wp_set_object_terms( $post_id, $defaults[$taxonomy], $taxonomy );
            }
        }
    }
}

I also had to set up two custom templates for each post type: an Archive-[post type] and a Single-[post type]. The archive shows the list of all posts in that post type. What follows is the meat of the archive-ticketrequest.php template. This is more complicated than the other archive templates, because it loops over the Grades. It sprays out a list of all the Ticket Request ads, with Gold-star ads before the General ads. The Contact button goes to a single-post page for this contact type that reiterates the ad copy and includes a contact form that reaches the ad’s author.

<?php
$post_type = 'ticketrequest';
$taxonomy = 'grade';
$terms = get_terms( $taxonomy );
// need to reverse array to get gold stars on top!
foreach (array_reverse($terms,true) as $term) {
 $posts = null;
 query_posts( "post_type=$post_type&taxonomy=$taxonomy&term=$term->slug&posts_per_page=-1" );
 while ( have_posts() ) : the_post();
	$post_id = get_the_ID();
	?>
<div class="listing-item <?php echo $term->slug; ?>">
<?php if(get_post_meta($post_id, 'wpcf-picture',true)!=null) { ?>
<p class="picture"><img src="<?php echo esc_attr(get_post_meta($post_id, 'wpcf-picture', true)); ?>"></p>
<?php } ?>
<?php the_title('<h3>','</h3>',true); ?>
<p class="name"><span class="leadin">Name: </span> <?php the_author_meta( 'display_name'); ?> </p>
<p class="numtix"><span class="leadin">Number of tickets: </span> <?php echo esc_attr(get_post_meta($post_id, 'wpcf-numtix', true)); ?></p>
<p class="story"><span class="leadin">Sob story: </span> <?php echo esc_attr(get_post_meta($post_id, 'wpcf-story', true)); ?></p>
<p><a href="<?php the_permalink(); ?>" class="button">Contact</a></p>
<br style="clear: right;" />
</div>
<?php	
endwhile;
wp_reset_postdata();
wp_reset_query();
}
?>

The point of all this is to show the Gold-star posts before the General posts.

One thing that I want to add to this is to show only one ad per person. We don’t want people spamming the list with a bunch of ads, and I think they’ve been good about that. I’ve got some skeletal code that will show only the oldest post per author, but haven’t got it integrated into this template.

Finally, I wanted to expire posts. There are a number of plugins that let you do this, but as far as I could tell, none worked quite the way I wanted: I wanted to enforce expiration automatically, but the plugins out there are designed to add a box to the WordPress post-editing interface giving you the option to expire a post. I wound up using a plugin called Content Scheduler and adding a function to functions.php as follows

function ajr_set_expire($post_id, $post) {
	$offset = 1814400 + time() ; // 3 weeks into the future
	if ($offset != null) {    	
   	$date_format = 'Y-m-d H:i:s';
	$futuredate = date($date_format, $offset) ;
 	update_post_meta($post_id, '_cs-enable-schedule', 'Enable'); 
 	update_post_meta($post_id, '_cs-expire-date', $futuredate);
 	}
}
add_action( 'save_post', 'ajr_set_expire', 10, 2);

This doesn’t quite do what I want it to do: I really only want it to apply when the author’s user role is Advertiser or Returnee. I played around with a few things, but never got them to work.

Update: This code wound up not quite working right, and I had to disable the expiration plugin entirely. Hooking into a different action (like publish_post) might solve the problem.

To make the site work, we also used Grunion contact forms, placing a contact form on the single-post page for each ad as a way to reach the ad’s author without exposing e-mail addresses. We installed WPSC Support Tickets as a way to manage support requests. Finally, we used Import Users from CSV. This let us set up a CSV (actually we used several and imported users in waves) based on the e-mail addresses of the returnees. This crucially let us specify the imported users as having the “Returnee” grade. We also generated random passwords for each of the users. One minor problem with this method was that we used e-mail addresses as account names, and in the interest of privacy had to encourage our users to change their public-facing nicknames on the site. Dropping the domain names from the e-mail addresses might have worked, and provided more privacy by default, but also might have resulted in some name collisions, and we were under pressure to deliver. So we sent all the returnees a heads-up e-mail, and used this plugin to set up accounts for all of them (except a small number that opted out), which automatically sent them login credentials.

We used the Coraline theme, which I hacked on directly (I know, I should have set up a sub-theme).

I have a long list of ways I’d like to make this better for next year, but for now, it’s working, and while the WordPress admin interface is confusing for new users, we are encountering very little in the way of support problems. A meticulous help page might, well, be helping.