Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: coding

  • Datepicker and a Widget

    Datepicker and a Widget

    Last week, I worked on making a plugin that would safely and smartly allow for a date selection, and not permit people to put in junk data. I had my code ‘clean’ and only accepted good data, there was one question remaining. How do I make it look GOOD?

    Let’s be honest, pretty data matters. If things look good, people use them. It’s that simple. This let me play with another aspect of coding that I don’t generally look at. Javascript.

    What Code Do I Need?

    There are a lot of ways to tackle this kind of problem. If you wanted to just list the months and days, and have those be drop-downs, you could do that. The option I went with was to have a calendar date-picker pop up, as I felt that would visually explain what the data should be.

    To do that I needed a date picker jQuery script (which is included in WordPress core) and a second script to format the output.

    My Script

    This part is really small:

    jQuery(function() {
        jQuery( ".datepicker" ).datepicker({
            dateFormat : "mm-dd"
        });
    });
    

    All it does is force the format to be “mm-dd” – so if you picked the date, that’s what it would be.

    Enqueuing the Scripts

    In order to make sure the scripts are only loaded on the widgets page, my enqueue function looks like this:

    	public function admin_enqueue_scripts($hook) {
    		if( $hook !== 'widgets.php' ) return;
    		wp_enqueue_script( 'byq-onthisday', plugins_url( 'js/otd-datepicker.js', __FILE__ ), array( 'jquery-ui-datepicker' ), $this->version, true );
    		wp_enqueue_style( 'jquery-ui', plugins_url( 'css/jquery-ui.css', __FILE__ ), array(), $this->version );
    	}
    

    The CSS is because, by default, WordPress doesn’t include the jquery UI CSS.

    Calling the Scripts

    In the widget class, I have a function for the form output. In there, I have an input field with a class defined as datepicker, which is used by the jquery I wrote above, to know “I’m the one for you!”

    	function form( $instance ) {
    		$instance = wp_parse_args( (array) $instance, $this->defaults );
    		?>
    		<p>
    			<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php _e( 'Title', 'bury-your-queers' ); ?>: </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>
    
    		<p>
    			<label for="<?php echo esc_attr( $this->get_field_id( 'date' ) ); ?>"><?php _e( 'Date (Optional)', 'bury-your-queers' ); ?>: </label>
    			<input type="text" id="<?php echo esc_attr( $this->get_field_id( 'date' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'date' ) ); ?>" class="datepicker" value="<?php echo esc_attr( $instance['date'] ); ?>" class="widefat" />
    			<br><em><?php _e( 'If blank, the date will be the current day.', 'bury-your-queers' ); ?></em>
    		</p>
    		<?php
    	}
    

    Making it Pretty

    To be honest, once I got the JS working, I left the default CSS alone. Why? Because I’m a monkey with a crayon when it comes to design. The default worked fine for me:

    The Default Date Picker

    It does make me think that it would be nice if WordPress included their own customize datepicker colors in the admin colors, but I understand why they don’t. Not everyone or even most people will ever need this.

  • Names, Short Names, and More Names

    Names, Short Names, and More Names

    I was working on a side project with Tracy Levesque of Yikes! and she lamented at my readme. I had totally half-assed it and I knew it, so she cleaned it up and asked me “What are the shortcodes?” I told her and about a second later she suggested two new names. Hers were better.

    Functionality Based Names

    I have a habit to name things what they are. I made a shorcode, for example, for a number of posts in a custom post type, and I called it [numposts] because that made sense. But when I added in a new one for number of posts in a taxonomy, I made a second shortcode named [numtax] which is kind of silly isn’t it?

    My problem is that I think about each shortcode as it’s own, stand-alone entity. It’s a functional thing. It does a thing. A function should be named uniquely to be clear what it’s for.

    Usage Based Names

    Perhaps without meaning to, Tracy jolted my brain into thinking about not the developer but the end user. Now, in my head, I thought “The user would know which shortcode to use and doesn’t have to think about more.” But. She suggested this: [plugin-name data="type"]

    I stared at that for a moment and felt the light slap me in the face. I’d named the shortcode for their function, but not for the plugin they were in, which is akin to all those terribly named functions and classes I’m always ranting about. In short order, the plugin was fixed and I turned back to my posts code.

    One Name to Bind Them

    [numposts data="posts" posttype="post-type" term="term-slug" taxonomy="taxonomy-slug"]
    

    The code defaults to a data set of posts and a post type of post since those are the most common usages. After that it’s a fast check “Is this a posts data set or a taxonomy one?” and runs the same code it used to run, passing the data along.

    What’s Really The Difference?

    “But Mika, now people have to type in more!” I hear you say.

    They do. [numposts] defaults to posts like it always did. [numposts posttype="page"] is four characters longer. But with the four extra characters (and really I could have left that out) comes something simpler: people only have to think of one shortcode.

    If they want to count the number of posts, then they just count the number of posts and call it a day. There’s no headache of realizing they meant to get the number of posts with a specific taxonomy. It all just works with one. Remember your terms, and those didn’t change except for needing [numposts data="taxonomy" term="wordpress"] which actually makes it more obvious what you’re doing.

    I have no idea if Tracy meant for me to get this deep into this, but she also knows I spent an hour contemplating the fact that the word ‘read’ exists in multiple tenses, and it’s only by context that I actually know which one anyone meant.

  • REST API On This Day

    REST API On This Day

    After making a simple REST API output that showed the date (and time since) the last death listed on a site, I had an idea. What if I could list everyone who died on a specific day? I had that data after all, so why not.

    A Word About Evolution of Code

    Over time, the code behind my “Bury Your Characters” plugin evolved a great deal. I went from unique function names to singletons to one aspect and now to two. The design changed a lot in the middle as well. I unpacked the parsing of data from being on the data driven site and moved that to the site running the plugin.

    The evolution of all this was for a simple reason. I wanted the JSON output to be generated as fast as possible, grabbed, and parsed locally. If the data takes less time to generate on my end, then the speed becomes more of a factor of how fast can the end-user’s site run. That would allow more people to use my plugin.

    A secondary bonus reason was that I hate repeating myself in code. If I only have one place to update my code that generates the list of dead characters, for example, then there are fewer odds I’ll screw up and only edit one of the six places it gets used.

    This means that the code I posted last week doesn’t look at all like that anymore.

    On My Site (aka The Service)

    In a file called bury-your-queers.php (yes, this is for that site) I have a class called LWTV_BYQ_JSON.

    In that class I have a constructor that calls the rest_api_init and that’s where I define my URLs. I already had the last-death set, so now I’ve added in two more for on-this-day:

    	public function rest_api_init() {
    		register_rest_route( 'lwtv/v1', '/last-death', array(
    			'methods' => 'GET',
    			'callback' => array( $this, 'last_death_rest_api_callback' ),
    		) );
    		register_rest_route( 'lwtv/v1', '/on-this-day/', array(
    			'methods' => 'GET',
    			'callback' => array( $this, 'on_this_day_rest_api_callback' ),
    		) );
    		register_rest_route( 'lwtv/v1', '/on-this-day/(?P<date>[\d]{2}-[\d]{2})', array(
    			'methods' => 'GET',
    			'callback' => array( $this, 'on_this_day_rest_api_callback' ),
    		) );
    	}
    

    The reason I have two for my new route is that I wanted both the main URL and the sub URL to work. That is /lwtv/v1/on-this-day/ and /lwtv/v1/on-this-day/03-03/ both work. They both call the same callback which checks what was passed to it. If the params for date are empty, it assumes today:

    	public function on_this_day_rest_api_callback( $data ) {
    		$params = $data->get_params();
    		$this_day = ( $params['date'] !== '' )? $params['date'] : 'today';
    		$response = $this->on_this_day( $this_day );
    		return $response;
    	}
    

    The callback grabs the function on_this_day(), passing the date through to it. Now this is the first BIG change. I pulled the code to generate the list of all the dead out of the function for last_death because it’s called twice. Now it calls LWTV_Loops::post_meta_query which is just a class for all my common loops that get reused a lot. They’re no faster than making the query the regular way, they’re just neater and tidier.

    	public static function on_this_day( $this_day = 'today' ) {
    
    		if ( $this_day == 'today' ) {
    			$this_day = date('m-d');
    		}
    
    		// Get all our dead queers
    		$dead_chars_loop  = LWTV_Loops::post_meta_query( 'post_type_characters', 'lezchars_death_year', '', 'EXISTS' );
    		$dead_chars_query = wp_list_pluck( $dead_chars_loop->posts, 'ID' );
    		$death_list_array = self::list_of_dead_characters( $dead_chars_query, $dead_chars_loop );
    
    		$died_today_array = array();
    
    		foreach ( $death_list_array as $the_dead ) {
    			if ( $this_day == date('m-d', $the_dead['died'] ) ) {
    				$died_today_array[ $the_dead['slug'] ] = array(
    					'slug' => $the_dead['slug'],
    					'name' => $the_dead['name'],
    					'url'  => $the_dead['url'],
    					'died' => date( 'Y', $the_dead['died'] ),
    				);
    			}
    		}
    
    		if ( empty( $died_today_array ) ) {
    			$died_today_array[ 'none' ] = array(
    				'slug' => 'none',
    				'name' => 'No One',
    				'url'  => site_url( '/cliche/dead/' ),
    				'died' => date('m-d'),
    			);
    		}
    
    		$return = $died_today_array;
    		return $return;
    	}
    

    Output

    And all that works to output this (if you pick 03-03):

    {"alisa-davies":{"slug":"alisa-davies","name":"Alisa Davies","url":"https:\/\/lezwatchtv.com\/character\/alisa-davies\/","died":"2010"},"lexa":{"slug":"lexa","name":"Lexa","url":"https:\/\/lezwatchtv.com\/character\/lexa\/","died":"2016"}}
    

    or this (if you pick a date where no one died):

    {"none":{"slug":"none","name":"No One","url":"https:\/\/lezwatchtv.com\/cliche\/dead\/","died":"02-19"}}
    

    Unlike what I did for ‘last death’, here I’m only reporting the year. The reason is that’s all I care about just now. I’m passing the date through, so I don’t need that information anymore.

    On Your Site (aka The Plugin)

    The plugin for this has three classes. The first is the master class to build out the widgets and shortcodes and also to parse the data. The other two are for the widgets.

    Parsing the data is what we’re going to talk about today.

    The function is straightforward. You pass the date to the function, it grabs the data for the date (defaulting to ‘today’ if none is provided) and spit out the resulting data:

    	public static function on_this_day( $this_day = 'today' ) {
    		$echo_day = ( $this_day == 'today' )? time() : strtotime( date('Y').'-'.$this_day );
    		$json_day = ( $this_day == 'today' )? '' : $this_day.'/' ;
    
    		$request  = wp_remote_get( 'https://lezwatchtv.com/wp-json/lwtv/v1/on-this-day/'.$json_day );
    		$response = wp_remote_retrieve_body( $request );
    		$response = json_decode($response, true);
    
    		$count = ( key($response) == 'none' )? 0 : count($response) ;
    		$how_many = __('no characters died!', 'bury-your-queers');
    		$the_dead = '';
    
    		if ( $count > 0 ) {
    			$how_many = sprintf( _n( '%s character died:', '%s queer female characters died:', $count, 'bury-your-queers' ), $count );
    
    			$the_dead = '<ul class="byq-otd">';
    			foreach ( $response as $dead_character ) {
    				$the_dead .= '<li><a href="'.$dead_character['url'].'">'.$dead_character['name'].'</a> - '.$dead_character['died'] .'</li>';
    			}
    			$the_dead .= '</ul>';
    		}
    
    		$onthisday = '<p>'. sprintf( __( 'On %s, %s', 'bury-your-queers'), date('F jS', $echo_day ), $how_many ).'</p>';
    		$return = $onthisday.$the_dead;
    
    		return $return;
    	}
    

    I picked paragraphs and un-ordered lists, since I feel those would be easily formatted by most people.

  • JSON Rest API (In Peace)

    JSON Rest API (In Peace)

    This week I’ve been talking about my list of dead characters, which I needed to order by year and wanted to display the most recent death in a widget.

    Part of my end goal with all this was a dream I had to let people have a widget on their own site where they could display the most recently dead character. There were other ideas I had, like a ‘this day in YEAR, Character X died.’ But for now, I wanted to start my delving into the JSON API with something more simple.

    Because this is my first serious go at it.

    Sketching Out The Concept

    To do this, I broke my concept down into the logical steps of what was needed.

    • Output the data
    • Create a JSON URL
    • Format the data there

    And no, I have no idea how to do any of that, except outputting the data.

    The Rest API

    To initialize the Rest API, you have to call a function on init() and that’s where it defines the URL that will be called. There are three (or four) parts to a URL:

    • Namespace
    • Version
    • Route
    • Arguments

    The version isn’t technically required, but in the interests of future proofing, it’s probably a good idea. And in this specific case, I don’t have any arguments I want to pass through. You’re going to get just the output of the last dead. In deciding that, I was able to determine the most sensible structure.

    My namespace should be for my site, not just this ‘show the dead’ feature. This is not always going to be the case, but since I’m adding in what I presume will be the first of some APIs, it’s wise to name in a way that is forward thinking. Similarly, I need my route to be logically named, and in this case I’m showing the last death, so I called it last-death.

    This makes my desired URL /wp-json/MYSITE/v1/last-death/ and the code is this:

    public function rest_api_init() {
    	register_rest_route( 'MYSITE/v1', '/last-death', array(
    		'methods' => 'GET',
    		'callback' => array( $this, 'rest_api_callback' ),
    	) );
    }
    

    The callback code is what gets my data, and based on my original widget resulted in the page displaying the following:

    "It has been <strong>2 months and 6 days<\/strong> since the last death: <a href=\"https:\/\/example.dev\/character\/gina\/\">Gina<\/a> - December 7, 2016"
    

    But I didn’t want it to be formatted like that. Instead, I wanted the code to spit out an array. To do that with my existing setup, I rewrote the dead_char() function, taking out all the parts that generated the days since the last death and instead put that in a separate plugin. Now this one gives an API output:

    {"name":"Gina","url":"https:\/\/example.dev\/character\/gina\/","died":1481068800,"since":"5792233"}
    

    That has the added bonus of letting anyone who wants to call it on their own for whatever they want isn’t stuck with my design. Yay, open source!

    The Code

    My JSON code went into a class like this:

    class MYSITE_Dead_Character_JSON {
    
    	/**
    	 * Constructor
    	 */
    	public function __construct() {
    		add_action( 'init', array( $this, 'init') );
    	}
    
    	/**
    	 * Init
    	 */
    	public function init() {
    		add_action( 'rest_api_init', array( $this, 'rest_api_init') );
    	}
    
    	/**
    	 * Rest API init
    	 *
    	 * Creates the callback - /MYSITE/v1/last-death/
    	 */
    	public function rest_api_init() {
    		register_rest_route( 'lwtv/v1', '/last-death', array(
    			'methods' => 'GET',
    			'callback' => array( $this, 'last_death_rest_api_callback' ),
    		) );
    	}
    
    	/**
    	 * Rest API Callback
    	 */
    	public function last_death_rest_api_callback( $data ) {
    		$response = $this->last_death();
    		return $response;
    	}
    
    	/**
    	 * Generate List of Dead
    	 *
    	 * @return array with last dead character data
    	 */
    	public static function last_death() {
    		// Get all our dead queers
    		$dead_chars_loop  = MYSITE_tax_query( 'post_type_characters' , 'cliches', 'slug', 'dead');
    		$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);
    
    		// Calculate the difference between then and now
    		$diff = abs( time() - $last_death['died'] );
    		$last_death['since'] = $diff;
    
    		$return = $last_death;
    
    		return $return;
    	}
    
    }
    new MYSITE_Dead_JSON();
    

    You may notice that I don’t seem to have a loop, and I call this instead:

    $dead_chars_loop  = MYSITE_tax_query( 'post_type_characters' , 'cliches', 'slug', 'dead');
    

    For various reasons, I reuse a lot of loop calls. To make my own theme and plugin more human readable, that function does the query. It looks like this:

    function MYSITE_tax_query( $post_type, $taxonomy, $field, $term, $operator = 'IN' ) {
    	$query = new WP_Query ( array(
    		'post_type'       => $post_type,
    		'posts_per_page'  => -1,
    		'no_found_rows'   => true,
    		'post_status'     => array( 'publish', 'draft' ),
    		'tax_query' => array( array(
    			'taxonomy' => $taxonomy,
    			'field'    => $field,
    			'terms'    => $term,
    			'operator' => $operator,
    		),),
    	) );
    	wp_reset_query();
    	return $query;
    }
    

    Since I have to do a lot of odd calls for the statistics on that site, it became smarter to do it that way.

    Calling the Data From YOUR Site!

    I made an endpoint! This is all well and good, but the new question is “How does someone else include this on their site?”

    The answer there is they need a widget. And it’s a widget I’ve mostly already made! All I had to do was create a plugin that made the widget and instead of calling the loops locally, just call the API.

    class Bury_Your_Dead {
    
    	public function __construct() {
    		add_action( 'widgets_init', array( $this, 'last_death_register_widget' ) );
    	}
    
    	public function last_death_register_widget() {
    		$this->widget = new BYD_Last_Death_Widget();
    		register_widget( $this->widget );
    	}
    
    	public static function last_death() {
    		$request  = wp_remote_get( 'https://example.dev/wp-json/MYSITE/v1/last-death/' );
    		$response = wp_remote_retrieve_body( $request );
    		$response = json_decode($response, true);
    
    		$diff = $response['since'];
    
    		$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));
    
    		$since = '';
    		if ( $years != 0 ) $since .= sprintf( _n( '%s year, ', '%s years, ', $years, 'MYSITE' ), $years );
    		if ( $months != 0 ) $since .= sprintf( _n( '%s month', '%s months', $months, 'MYSITE' ), $months );
    		$since .= ( $years != 0 )? ', ' : ' ';
    		$since .= ( $months != 0 )? __('and ', 'MYSITE') : '';
    		if ( $days != 0 ) $since .= sprintf( _n( '%s day', '%s days', $days, 'MYSITE' ), $days );
    
    		$response['since'] = $since;
    
    		return $response;
    	}
    }
    new Bury_Your_Dead();
    
    class BYD_Last_Death_Widget extends WP_Widget {
    
    	protected $defaults;
    
    	function __construct() {
    
    		$this->defaults = array(
    			'title'		=> __( 'The Most Recent Death', 'MYSITE' ),
    		);
    
    		$widget_ops = array(
    			'classname'   => 'dead-character deadwidget',
    			'description' => __( 'Displays time since the last WLW death', 'MYSITE' ),
    		);
    
    		$control_ops = array(
    			'id_base' => 'lezwatch-dead-char',
    		);
    
    		parent::__construct( 'lezwatch-dead-char', __( 'The Latest Dead', 'MYSITE' ), $widget_ops, $control_ops );
    	}
    
    	function widget( $args, $instance ) {
    
    		extract( $args );
    		$instance = wp_parse_args( (array) $instance, $this->defaults );
    
    		echo $args['before_widget'];
    
    		if ( ! empty( $instance['title'] ) ) {
    			echo $args['before_title'] . apply_filters( 'widget_title', $instance['title'] ) . $args['after_title'];
    		}
    
    		$dead_character = Bury_Your_Dead::last_death();
    
    		echo sprintf( __('It has been %s since the last death', 'MYSITE'), '<strong>'.$dead_character['since'].'</strong>' );
    		echo ': <a href="'.$dead_character['url'].'">'.$dead_character['name'].'</a> - '.date('F j, Y', $dead_character['died'] );
    
    		echo $args['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' ) ); ?>"><?php _e( 'Title', 'MYSITE' ); ?>: </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
    	}
    }
    

    Whew. That’s huge, I know! And you may have noticed I snuck some translation in there. Always look forward!

  • Display Posts, Ordered By Post Meta

    Display Posts, Ordered By Post Meta

    One of the things I’ve been working on over on my tv project was getting better lists of characters who died. Initially all that we recorded was the year. However we started getting the idea of a timeline and a calendar and a ‘this year in queer TV’ section, and I realized I needed more precise data.

    I’m going to skip the headache part here, but I changed the year dropdown to a calendar date field that stored the date as MM/DD/YYYY. While some calculations would have been easier with the time saved as a UNIX timestamp, having it be text let me search as text to collect all the death for a year. There were pros and cons to both sides of the coin of how to save the data. The other trick was I needed to save the data as an array. Just in case someone died twice.

    Suffice to say, here’s your scenario. I have meta data in the post containing the date of character death, and I want to display it in a specific way, not supported out of the box by the default queries.

    Display all the dead in a list ordered by date

    The simple part in WordPress is to get a loop of all posts in a type that have the meta for death. Using WP_Query I grabbed the array and instead of saving all the post’s data, I extracted the data I needed:

    • character name and URL
    • date of death
    • list of shows and their respective URLs

    This I turned into a very simple array that I can use in multiple situations:

    $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' );
    
    foreach( $dead_chars_query as $dead_char ) {
    	$show_info = get_post_meta( $dead_char, 'chars_show', true );
    	$died = get_post_meta( $dead_char, 'chars_dead', true );
    
    	$output_array[$post_slug] = array(
    		'name'  => get_the_title( $dead_char ),
    		'url'   => get_the_permalink( $dead_char ),
    		'shows' => $show_info,
    		'died'  => $died,
    	);
    }
    

    This array is made by looping through those WP_Query results and, for each character, grabbing the post meta data for shows and death. The simple array $output_array contains only the data I need for display. So no post content, no author, nada. Just the name, the URL, the shows, and the death. That said, I’m omitting my insane extra loop for generating $show_info with its links because it deserves its own post. Suffice to say, it’s weird. Oh and $thisyear is a page variable based on the URL you visit. If you go to /this-year/2015/ it will populate as ‘2015’ and so on.

    Side note. Characters actually have both a taxonomy for being dead and a lost meta for the date. That was a happy accident that allowed me to search for everyone who had died, and perhaps wasn’t currently dead. The time traveling Sara Lance is jokingly called Schroedinger’s Bisexual, since she is both alive and dead at any one given point in time.

    Fix The Date

    Okay! With my simple array of data, I can output the list however I want, depending on the way I ordered the query. The problem is that the query has a limited number of possible sorts, and they all have to do with the post subject, the title, not random post meta. Plus my post meta was saved in a way that wasn’t easily sortable.

    To solve this, I converted the date into a Unix timestamp:

    // Date(s) character died
    $died_date = get_post_meta( $dead_char, 'chars_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 ) {
    	$died_year = substr($date, -4);
    	if ( $died_year == $thisyear ) {
    		$date_parse = date_parse_from_format( 'm/d/Y' , $date);
    		$died = mktime( $date_parse['hour'], $date_parse['minute'], $date_parse['second'], $date_parse['month'], $date_parse['day'], $date_parse['year'] );
    	}
    }
    

    I put this in the foreach( $dead_chars_query as $dead_char ) {...} section and it transformed my date from 01/01/2017 to it’s commiserate Unix timestamp.

    Fix The Order

    The next step was to re-order my array based on the value of the death time. I wanted to go from smallest value to largest, as Unix timestamps increase over time.

    For this I used uasort to order the entire array by the date:

    // Reorder all the dead to sort by DoD
    uasort($death_list_array, function($a, $b) {
    	return $a['died'] <=> $b['died'];
    });
    

    This produced my desired results. Excellent. But now I have the date in a non-human readable format!

    Fix It On output

    Thankfully making the date human readable wasn’t all that messy. Once I have my finished array, I output it and clean it up:

    <ul>
    	<?php
    	foreach ( $death_list_array as $dead ) {
    		echo '<li><a href="'.$dead['url'].'">'.$dead['name'].'</a> / '.$dead['shows'].' / '.date( 'd F', $dead['died']).' </li>';
    	}
    	?>
    </ul>
    

    Voila!

    This Year example for 2014

    An ordered list of dead characters.

  • When a Page is an Endpoint

    When a Page is an Endpoint

    In making a site with non-standard WordPress pages, I ran into a common problem. How do I make a page that calls the data? To put it in a different way, I needed a URL (say, domain.com/stats/) to show site statistics. A ‘virtual page’ if you will.

    The Easy Way

    The first way I did this was the ‘easy’ way, though I’m loathe to really call it that. I made a page in WP Admin called ‘stats’ and I set that page to use a custom page template that, instead of calling post content, called my stats code. Custom page templates are pretty powerful, and having the page be a page meant I could allow editors to write and edit the page all they wanted.

    But the downside is that making ‘sub’ pages gets messy, and what happens if someone changes the slug or deletes the page or changes the template? No. There had to be a better way that let me force generate the page.

    The ‘Better’ Way

    This brings me to custom endpoints.

    Now there is a huge problem with this, and it’s that if you want to make your custom virtual pages look like the rest of your WordPress site, it’s … messy. This is why BuddyPress generates pages automatically for you. It needs the pages. No matter what, I was going to have to make pages. But there is a distinct difference between 19 pages and 5.

    My (unique) situation was that I wanted to have a series of sub pages. That would let me make one page, for example, for ‘roles’ and then automagically generate pages for /role/regular/ and /role/guest/ and so on. And yet, this brings up the other major problem.

    You see, it would also mean the slugs /about/regular/ could exist. Obviously there are ways around it but the point to hammer home here is that endpoints are not meant for this. They’re meant for if you want a custom endpoint (hah) on every page of a type.

    The (Really) Better Way

    Thankfully, there’s a better way!

    Rewrite rules and custom query vars will let me do everything I wanted, with only one page for each item.

    The Setup

    I have five types of pages with ‘extra’ sub pages: newest, role, star, stats, and thumbs. Each one has a few accepted types, but I don’t need to really worry about that just yet, because how I handle it differs on each page (more on that in a second). First I made a list of all my pages, the ‘kind’ of extra they had, and their custom template. Because yes, each page has a custom template.

    • page slug: newest, extra: newtype, template: newtype.php
    • page slug: role, extra: roleype, template: roletype.php
    • page slug: star, extra: starcolor, template: starcolor.php

    You’ll notice I went with a theme there. It makes it easier to remember what’s what. Each template is heavily customized for the data within, since each page is wildly different from each other. The one check I make on every page though is that the ‘extra’ value matches what I think it should. For example, here’s the stats header:

    $statstype = ( isset($wp_query->query['statistics'] ) )? $wp_query->query['statistics'] : 'main' ;
    $validstat = array('death', 'characters', 'shows', 'lists', 'main');
    
    if ( !in_array( $statstype, $validstat ) ){
    	wp_redirect( get_site_url().'/stats/' , '301' );
    	exit;
    }
    

    That means if you go to /stats/humbug/ it redirects you back to the main stats page.

    Cool, right? So the question next is how did I get /stats/characters/ to work if there’s no commensurate page?

    The WordPress Way

    The answer is add_rewrite_rule and query_vars. I made an array with all my page slugs and their extra, which you’ll remember was the same name as the template file. This let me use a series of loops and checks so my code is simpler.

    My query arguments are this:

    $query_args = array(
    	'newest'	=> 'newtype',
    	'role'		=> 'roletype',
    	'star'		=> 'starcolor',
    	'stats'		=> 'statistics',
    	'thumbs'	=> 'thumbscore',
    		);
    

    Notice how that matches exactly what my design was above? That’s why I take the time to plan all this out.

    The next thing I did with all this was to set up my query variables. These loop through the query array and set up a variable for each one.

    add_action ('query_vars', 'helf_query_vars');
    function helf_query_vars($vars){
    	foreach ( $query_args as $argument ) {
    		$vars[] = $argument;
    	}
    	return $vars;
    }
    

    What that does is makes a URL like this work: http://example.com/?pagename=stats&statistics=death

    It also means this works: http://example.com/stats/?statistics=death

    Neither of those are particularly ‘pretty’ permalinks, though, are they? That means it’s time for add_rewrite_rule!

    foreach( $query_args as $slug => $query ) {
        add_rewrite_rule(
            '^'.$slug.'/([^/]+)/?$',
            'index.php?pagename='.$slug.'&'.$query.'=$matches[1]',
            'top'
        );
    }
    

    This code is actually in an init function, but what it does is make a custom rewrite rule so we can call the URL like this: http://example.com/stats/death/

    Which is what I want.

    The Whole Code

    The following is actually the code I use. Feel free to fork! I put it into a class and did some extra work to call the right templates on the right pages.

    class LWTVG_Query_Vars {
    
    	// Constant for the query arguments we allow
    	public $query_args = array();
    
    	/**
    	 * Construct
    	 * Runs the Code
    	 *
    	 * @since 1.0
    	 */
    	function __construct() {
    		add_action( 'init', array( $this, 'init' ) );
    
    		$this->query_args = array(
    			'newest'	=> 'newtype',
    			'role'		=> 'roletype',
    			'star'		=> 'starcolor',
    			'stats'		=> 'statistics',
    			'thumbs'	=> 'thumbscore',
    		);
    	}
    
    	/**
    	 * Main Plugin setup
    	 *
    	 * Adds actions, filters, etc. to WP
    	 *
    	 * @access public
    	 * @return void
    	 * @since 1.0
    	 */
    	function init() {
    		// Plugin requires permalink usage - Only setup handling if permalinks enabled
    		if ( get_option('permalink_structure') != '' ) {
    
    			// tell WP not to override
    			add_action ('query_vars', array($this, 'query_vars'));
    
    			foreach( $this->query_args as $slug => $query ) {
    			    add_rewrite_rule(
    			        '^'.$slug.'/([^/]+)/?$',
    			        'index.php?pagename='.$slug.'&'.$query.'=$matches[1]',
    			        'top'
    			    );
    			}
    
    			// add filter for page
    			add_filter( 'page_template', array( $this, 'page_template' ) );
    
    		} else {
    			add_action( 'admin_notices', array( $this, 'admin_notice_permalinks' ) );
    		}
    	}
    
    	/**
    	 * No Permalinks Notice
    	 *
    	 * @since 1.0
    	 */
    	public function admin_notice_permalinks() {
    		echo '<div class="error"><p><strong>Custom Query Vars</strong> require you to use custom permalinks.</p></div>';
    	}
    
    	/**
    	 * Add the query variables so WordPress won't override it
    	 *
    	 * @return $vars
    	 */
    	function query_vars($vars){
    		foreach ( $this->query_args as $argument ) {
    			$vars[] = $argument;
    		}
    		return $vars;
    	}
    
    	/**
    	 * Adds a custom template to the query queue.
    	 *
    	 * @return $templates
    	 */
    	function page_template($templates = ""){
    		global $wp_query, $post;
    
    		if ( array_key_exists( $post->post_name, $this->lez_query_args ) )
    			$the_template = $this->lez_query_args[$post->post_name].'.php';
    
    		foreach ( $this->lez_query_args as $argument ) {
    			if( isset( $wp_query->query[$argument] ) ) {
    				$templates = dirname( dirname( __FILE__ ) ) . '/page-templates/' . $the_template;
    			}
    		}
    
    		return $templates;
    	}
    
    }
    
    new LWTVG_Query_Vars();
    

    PS: Yes, I totally built it all out in endpoints before I got smarter.