Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: code

  • Adding Sort Options to Facet

    Adding Sort Options to Facet

    As I implement more and more aspects of FacetWP, I find more and more ways to manipulate the searches. At first I only added in the features that let people easily search for multiple aspects at once. But I hadn’t yet added in any features to sorting.

    Sorting and Ordering

    The way Facets generally work is that you can easily organize all ‘types’ together, so if you wanted to search for everything that crossed four separate categories, it was very easy. In addition, you can extend it to search meta data as well.

    Sorting, on the other hand, is changing the order of the results. For example, if you wanted to search for everyone with terms A, B, and D, and post meta foo, but order them based on post meta bar, you can!

    A Practical Example

    I always do better with examples I can wrap my hands around.

    Take television shows. Take a list of 500 TV shows, and have them include the following taxonomies:

    • Genres (drama, sitcom, etc)
    • Airdates (Year to Year)
    • Tropes (common tropes)
    • Number of characters
    • Number of dead characters

    That’s enough for now.

    With that list, and a couple facets, you can concoct a smaller list of all sitcoms that aired in between 2014 and 2016 (inclusive), with a trope of ‘sex workers.’ The answer is 4 by the way. By default, the list displays alphabetically.

    But. What if you wanted to order them by the ones with the most characters first?

    That’s sorting.

    The Code

    Okay so how do we add this in? Functions!

    Facet comes with quite a few defaults, but it lets you add your own sort options. The two things I’m going to show below are how to rename the display labels for some of the defaults, and how to add in one new option for the most number of characters:

    add_filter( 'facetwp_sort_options', 'DOMAIN_facetwp_sort_options', 10, 2 );
    
    function facetwp_sort_options( $options, $params ) {
    
    	$options['default']['label']    = 'Default (Alphabetical)';
    	$options['title_asc']['label']  = 'Name (A-Z)';
    	$options['title_desc']['label'] = 'Name (Z-A)';
    
    	if ( is_post_type_archive( 'DOMAIN_shows' ) ) {
    
    		$options['most_characters'] = array(
    			'label' => 'Number of Characters (Descending)',
    			'query_args' => array(
    				'orderby'  => 'meta_value_num', // sort by numerical
    				'meta_key' => 'DOMAIN_char_count',
    				'order'    => 'DESC', // descending order
    			)
    		);
    	}
    

    I have this wrapped in a check for is_post_type_archive because I don’t want the options to show on other pages. The meta key is the name of the meta key you’re going to use to sort by (I have key that updates every time a post is saved with a count of characters attached) and the orderby value is one of the ones WP Query can use.

    End result?

    A dropdown with the options

    Looks nice!

  • FacetWP and Genesis Pagination

    FacetWP and Genesis Pagination

    If you use StudioPress’ Genesis themes, you may be used to that pretty numerical pagination instead of ye olde previous and next buttons. And if you use FacetWP, you may have found out you need to use their pagination to make things work right.

    Thankfully this can be fixed thanks to two people’s previous work.

    Replace the Navigation

    First we look to Sridhar Katakam who came up with an elegant way to replace the normal Genesis post navigation. The code is nearly the same, I just single-lined the returns:

    // Replace Genesis' Pagination with FacetWP's.
    // Context: Posts page, all Archives and Search results page.
    // @author Sridhar Katakam - http://sridharkatakam.com/facetwp-genesis/
    add_action( 'loop_end', 'MYSITE_replace_genesis_pagination' );
    function MYSITE_replace_genesis_pagination() {
    	if ( ! ( is_home() || is_archive() || is_search() ) ) return;
    	remove_action( 'genesis_after_endwhile', 'genesis_posts_nav' );
    	add_action( 'genesis_after_endwhile', 'MYSITE_facet_posts_nav' );
    }
    function MYSITE_facet_posts_nav() {
    	echo facetwp_display( 'pager' );
    }
    

    The nice part about this is that if you’re not using FacetWP on that page, it reverts to the default pager. I’m not doing a check on if Facet exists because I have this in a giant block that does it for me. If you need to, though, the code would be this:

    if ( function_exists( 'facetwp_display' ) ) { 
        add_action( 'loop_end', 'MYSITE_replace_genesis_pagination' ); 
    }
    

    And yes, you should always check if the plugin is loaded before you break things.

    Style the Navigation

    The styling magic comes from a slight fork of Matt Gibbs’ code to remove pagination if there’s only one post:

    // Style pagination to look like Genesis
    // @author Matt Gibbs
    // https://gist.github.com/mgibbs189/69176ef41fa4e26d1419
    function MYSITE_facetwp_pager( $output, $params ) {
        $output = '<div class="archive-pagination pagination"><ul>';
        $page = (int) $params['page'];
        $total_pages = (int) $params['total_pages'];
    
        // Only show pagination when > 1 page
        if ( 1 < $total_pages ) {
    
            if ( 1 < $page ) {
                $output .= '<li><a class="facetwp-page" data-page="' . ( $page - 1 ) . '">&laquo; Previous Page</a></li>';
            }
            if ( 3 < $page ) {
                $output .= '<li><a class="facetwp-page first-page" data-page="1">1</a></li>';
                $output .= ' <span class="dots">…</span> ';
            }
            for ( $i = 2; $i > 0; $i-- ) {
                if ( 0 < ( $page - $i ) ) {
                    $output .= '<li><a class="facetwp-page" data-page="' . ($page - $i) . '">' . ($page - $i) . '</a></li>';
                }
            }
    
            // Current page
            $output .= '<li class="active" aria-label="Current page"><a class="facetwp-page active" data-page="' . $page . '">' . $page . '</a></li>';
    
            for ( $i = 1; $i <= 2; $i++ ) {
                if ( $total_pages >= ( $page + $i ) ) {
                    $output .= '<li><a class="facetwp-page" data-page="' . ($page + $i) . '">' . ($page + $i) . '</a></li>';
                }
            }
            if ( $total_pages > ( $page + 2 ) ) {
                $output .= ' <span class="dots">…</span> ';
                $output .= '<li><a class="facetwp-page last-page" data-page="' . $total_pages . '">' . $total_pages . '</a></li>';
            }
            if ( $page < $total_pages ) {
                $output .= '<li><a class="facetwp-page" data-page="' . ( $page + 1 ) . '">Next Page &raquo;</a></li>';
            }
        }
    
        $output .= '</ul></div>';
    
        return $output;
    }
    add_filter( 'facetwp_pager_html', 'MYSITE_facetwp_pager', 10, 2 );
    

    This is somewhat obvious. It adds in <li></li> around each page item, and also wraps the entire block in the default Genesis code of <div class="archive-pagination pagination"><ul></ul></div> to produce the default Genesis display.

  • Genesis Themes: Author Box Shortcode

    Genesis Themes: Author Box Shortcode

    In building out a network of sites, I was struck upon by a feature of multisite I love, a feature of a theme I adore, and an inconvenience of the combination.

    Author Box

    StudioPress’ Genesis themes include a feature called “Author Box” which allows authors to create bios from their profiles and show them at the bottom of posts. When you have multiple authors on a site, this is a great way to make sure everyone gets credit and that they can control it.

    The code to make this show up is included in most (if not all) StudioPress themes, but if you need to add it for your post archives and single posts, it looks like this:

    add_filter( 'get_the_author_genesis_author_box_single', '__return_true' );
    add_filter( 'get_the_author_genesis_author_box_archive', '__return_true' );
    

    Multisite Magic

    Once the code is enabled, and once someone’s written a bio, their author box shows up for all sites on the network. This is great for what I needed, as it meant everyone had control and I could just set it and forget it. The only ‘annoying’ part is it’s the same bio for all sites, so if you have wildly different sites on your network, this may not be right for you.

    This does harken back to my age old comment: WordPress Multisite is for a network of somewhat related sites.

    By this I mean if all the sites on your network are related, let’s say for a school, then it doesn’t matter that everyone’s bio talks about their school work. But if you combine the school with hobbies, then it gets weird to announce that the champion archer has a PhD in neuroscience. Although that is pretty cool.

    Display The Author Box Anywhere

    The other problem with the author box is you can only use it on pages or posts as context. Which is not what I wanted here. So I made it a shortcode.

    function author_box( $atts ) {
    
    	$user = username_exists( sanitize_user( $atts['user'] ) );
    
    	if ( !$user ) return;
    
    	wp_enqueue_style( 'author-box-shortcode', plugins_url( 'author-box.css', __FILE__ ) );
    
    	$authordata    = get_userdata( $user );
    	$gravatar_size = 'genesis_author_box_gravatar_size' ;
    	$gravatar      = get_avatar( get_the_author_meta( 'email', $user ), $gravatar_size );
    	$description   = wpautop( get_the_author_meta( 'description', $user ) );
    	$username      = get_the_author_meta( 'display_name' , $user );
    
    	$author_box    = '
    		<section class="author-box author-box-shortcode">'
    		. $gravatar
    		. '<h4 class="author-box-title"><span itemprop="name">' . $username . '</span></h4>
    		<div class="author-box-content" itemprop="description">'. $description .'</div>
    		</section>
    	';
    
    	return $author_box;
    }
    

    This is obviously skewed towards Genesis themes, but realistically other than the code in $gravatar_size you can use this for any theme anywhere. The benefit of Genesis here is that most, if not all, of the CSS is done for you. The shortcode is [author-box user="ipstenu"] and it dumps out a full width user box of your named author.

    Display Multiple Boxes Instead

    But… What if you wanted a grid? Or a group of IDs? Taking advantage of the fact that Genesis comes with columns, the code looks like this:

    function author_box( $atts ) {
    
    	if ( $atts['users'] == '' ) return;
    
    	wp_enqueue_style( 'author-box-shortcode', '/wp-content/mu-plugins/css/author-box.css' );
    
    	$users = explode(',', $atts['users'] );
    	$user_count = count( $users );
    
    	$columns = 'one-half';
    	if ( $user_count == 1 ) $columns = '';
    	if ( $user_count % 3 == 0 ) $columns = 'one-third';
    
    	$author_box = '<div class="author-box-shortcode">';
    
    	foreach( $users as $user ) {
    		$user = username_exists( sanitize_user( $user ) );
    		if ( $user ) {
    			$authordata    = get_userdata( $user );
    			$gravatar_size = 'genesis_author_box_gravatar_size' ;
    			$gravatar      = get_avatar( get_the_author_meta( 'email', $user ), $gravatar_size );
    			$description   = wpautop( get_the_author_meta( 'description', $user ) );
    			$username      = get_the_author_meta( 'display_name' , $user );
    
    			$author_box   .= '
    				<section class="author-box '. $columns .'">'
    				. $gravatar
    				. '<h4 class="author-box-title"><span itemprop="name">' . $username . '</span></h4>
    				<div class="author-box-content" itemprop="description">'. $description .'</div>
    				</section>
    			';
    		}
    	}
    
    	$author_box .= '</div>';
    
    	return $author_box;
    }
    

    The shortcode here is [author-box users="ipstenu, liljimmi"] and that puts out a column of either fullwidth, half, or a third. The default is a half, and if there’s only one item, it goes to full width, but I only put in a 1/3rd check because I didn’t feel the need to cover everything. If you want to nick the CSS, StudioPress put it up online, and you can extend it as you want.

  • It Has Been X Days Widget

    It Has Been X Days Widget

    Yesterday I talked about ordering posts by post meta in a weirdly unique situation. Today I’m going to tell you about an even weirder one.

    As I mentioned, what I’m working with is a list of all the dead characters recorded on a site. One of the aspects of this is to allow for us to show a widget that tells visitors how many days it’s been since the last death, and who it was. There are a lot of ways to do this, and one of them is manually updating, but no one likes this. If an event can be automated, everyone wins.

    Since I’d already done the lion’s share of the work, forking it over into a widget and grabbing the last (i.e. most recent) death was easier.

    The Code

    I broke this up into two classes because it’s habit. I have no other reason.

    class Dead {
    	public function __construct() {
    		add_action( 'widgets_init', array( $this, 'register_widget' ) );
    	}
    
    	public function register_widget() {
    		$this->widget = new Dead_Widget();
    		register_widget( $this->widget );
    	}
    
    	public static function dead_queers() {
    		// Get all our dead queers
    		$dead_chars_loop = new WP_Query( array(
    			'post_type'       => 'post_type_characters',
    			'posts_per_page'  => -1,
    			'meta_query'      => array(
    				array(
    					'key'     => 'chars_death_year',
    					'value'   => $thisyear,
    					'compare' => 'REGEXP',
    				),
    			),
    		) );
    		$dead_chars_query = wp_list_pluck( $dead_chars_loop->posts, 'ID' );
    
    		// List all queers and the year they died
    		if ( $dead_chars_loop->have_posts() ) {
    			$death_list_array = array();
    
    			// Loop through characters to build our list
    			foreach( $dead_chars_query as $dead_char ) {
    
    				// Date(s) character died
    				$died_date = get_post_meta( $dead_char, 'lezchars_death_year', true);
    				$died_date_array = array();
    
    				// For each death date, create an item in an array with the unix timestamp
    				foreach ( $died_date as $date ) {
    					$date_parse = date_parse_from_format( 'm/d/Y' , $date);
    					$died_date_array[] = mktime( $date_parse['hour'], $date_parse['minute'], $date_parse['second'], $date_parse['month'], $date_parse['day'], $date_parse['year'] );
    				}
    
    				// Grab the highest date (aka most recent)
    				$died = max( $died_date_array );
    
    				// Get the post slug
    				$post_slug = get_post_field( 'post_name', get_post( $dead_char ) );
    
    				// Add this character to the array
    				$death_list_array[$post_slug] = array(
    					'name' => get_the_title( $dead_char ),
    					'url' => get_the_permalink( $dead_char ),
    					'died' => $died,
    				);
    			}
    
    			// Reorder all the dead to sort by DoD
    			uasort($death_list_array, function($a, $b) {
    				return $a['died'] <=> $b['died'];
    			});
    		}
    
    		// Extract the last death
    		$last_death = array_slice($death_list_array, -1, 1, true);
    		$last_death = array_shift($last_death);
    
    		$diff = abs( time() - $last_death['died'] );
    		$years = floor($diff / (365*60*60*24));
    		$months = floor(($diff - $years * 365*60*60*24) / (30*60*60*24));
    		$days = floor(($diff - $years * 365*60*60*24 - $months*30*60*60*24)/ (60*60*24));
    
    		$days_since = '';
    		if ( $years != 0 ) $days_since  .= $years .' '. _n( 'year, ', 'years, ', $years );
    		if ( $months != 0 ) $days_since .= $months .' '. _n( 'month', 'months', $months );
    		if ( $years != 0 ) $days_since  .= ',';
    		if ( $months != 0 ) $days_since .= ' and ';
    		if ( $days != 0 ) $days_since   .= $days .' '. _n( 'day', 'days', $days );
    
    		$return = 'It has been <strong>'. $days_since .'</strong> since the last death: <a href="'.$last_death['url'].'">'.$last_death['name'].'</a> - '.date('F j, Y', $last_death['died'] );
    
    		echo $return;
    	}
    }
    new Dead();
    
    class Dead_Widget extends WP_Widget {
    
    	protected $defaults;
    
    	function __construct() {
    		$this->defaults = array(
    			'title'		=> 'Bury Your Dead',
    		);
    
    		$widget_ops = array(
    			'classname'   => 'dead-queer deadwidget',
    			'description' => 'Displays days since the last death',
    		);
    
    		$control_ops = array(
    			'id_base' => 'dead-person',
    		);
    
    		parent::__construct( 'dead-person', 'Days Since Last Dead', $widget_ops, $control_ops );
    	}
    
    	function widget( $args, $instance ) {
    		extract( $args );
    		$instance = wp_parse_args( (array) $instance, $this->defaults );
    
    		echo $before_widget;
    
    		if ( ! empty( $instance['title'] ) ) {
    			echo $before_title . apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base ) . $after_title;
    		}
    
    		echo Dead::dead_chars();
    
    		echo $after_widget;
    	}
    
    	function update( $new_instance, $old_instance ) {
    		$new_instance['title']      = strip_tags( $new_instance['title'] );
    		return $new_instance;
    	}
    
    	function form( $instance ) {
    		$instance = wp_parse_args( (array) $instance, $this->defaults );
    		?>
    		<p>
    			<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">Title:</label>
    			<input type="text" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" value="<?php echo esc_attr( $instance['title'] ); ?>" class="widefat" />
    		</p>
    		<?php
    	}
    }
    

    Which looks like this:

    Bury Your Queers

    An Explanation

    Some things to note.

    While I only allow for customizing the title, I’ve left my arguments as an array in order to let me extend it later. What if, for example, I want to display the featured image as well?

    I’ve also left in some ‘default’ calls to Genesis’ StudioPress theme with regards to the widget layout. That’s because I wrote this for a StudioPress theme.

    The whole reason the death year is an array is that, as I mentioned the other day, people may die multiple times. Insert Buffy singing “Hey I died twice” here. I have to get all the dates, pick the largest number (which is the most recent date, remember Unix timestamps are forever increasing), and save that one. Once I find the ultimate largest number, I know that’s my character to save. Problem is I have to go through them all to figure out who died last.

    I’m inordinately happy about getting to use array_slice() to grab the final item in an array, and ditto array_shift() to move the multidimensional array to a single dimension.

    Using array_shift() changed this:

    Array ( 
        [gina] => Array (
            [name] => Gina 
            [url] => https://example.dev/character/gina/ 
            [died] => 1481068800 
        )
    )
    

    to this:

    Array ( 
        [name] => Gina 
        [url] => https://example.dev/character/gina/ 
        [died] => 1481068800 
    ) 
    

    And getting to finally use _n() in a non-translation sense was fun.

    The reason the dead characters are in a function in the first class is that I plan to extend this code later on, maybe making a shortcode or a JSON API call. Who knows!

    Finally, I mentioned that there were two ways to list the dead characters. One was by a taxonomy of character clichés and the other was by the death date post meta. In this case, I opted to sort by the taxonomy because if someone has a death date but is not marked as dead, then they were dead and came back to life.