Half-Elf on Tech

Thoughts From a Professional Lesbian

Category: How To

  • Multi Faceted Connections

    Multi Faceted Connections

    That’s a pun because I’m using FacetWP y’all.

    As you probably know by now, I have a site that has a lot of weird data. And one of the problems I ran into with FacetWP and my site was that I saved data in a serialized manner.

    Simple Arrays

    Since characters can have multiple actors, the data is saved in a serialized array like this: a:1:{i:0;s:13:"Lucy Lawless";}

    I want to be able to search for all the characters Lucy Lawless plays, even if someone else also played the role, so to do that I need to tell FacetWP to save the data twice, an entry for each actor. To do that, I use this code:

    add_filter( 'facetwp_index_row', 'filter_facetwp_index_row', 10, 2 );
    function filter_facetwp_index_row( $params, $class ) {	
    	// Actors
    	// Saves one value for each actor
    	if ( 'char_actors' == $params['facet_name'] ) {
    		$values = (array) $params['facet_value'];
    		foreach ( $values as $val ) {
    			$params['facet_value'] = $val;
    			$params['facet_display_value'] = $val;
    			$class->insert( $params );
    		}
    		return false; // skip default indexing
    	}
    	return $params;
    }
    

    That saves two entries in the FacetWP table like this:

    An example of two actors for one character

    Complex Arrays

    Buuuuut I also have a complex array where I list a show’s airdates: a:2:{s:5:"start";s:4:"1994";s:6:"finish";s:4:"2009";}

    Now that doesn’t look too weird, I know, but the problem is I wanted to be able to compare the start and end dates, so you could get a list of, say, all shows that were on air between 1950 and 1960 (two, by the way). In order to do that, I had to break the array apart into not only two values, but two separate sources!

    In order to make that work, I do this:

    // Airdates
    // Splits array value into two sources
    if ( 'show_airdates' == $params['facet_name'] ) {
    	$values = (array) $params['facet_value'];
    
    	$start = ( isset( $values['start'] ) )? $values['start'] : '';
    	$end   = ( isset( $values['finish'] ) )? $values['finish'] : date( 'Y' );
    
    	$params['facet_value']         = $start;
    	$params['facet_display_value'] = $start;
    	$class->insert( $params );
    
    	$params['facet_source']        = 'cf/lezshows_airdates_end';
    	$params['facet_name']          = 'show_airdates_end';
    	$params['facet_value']         = $end;
    	$params['facet_display_value'] = $end;
    	$class->insert( $params );
    	
    	return false; // skip default indexing
    }
    

    That gives me two database entries like so:

    An example of two values in two separate sources

    The reason this is done is because I have a facet that compares the datasets for lezshow_airdates_end with lezshow_airdates and if the numbers are between them, that’s what it shows.

    And this works because of this filter:

    // Filter Facet sources
    add_filter( 'facetwp_facet_sources', function( $sources ) {
        $sources['custom_fields']['choices']['cf/lezshows_airdates_end'] = 'Airdates End';
        return $sources;
    });
    

    That creates a new custom field based on the values in cf/lezshows_airdates_end so I can compare between the two. And with a snazzy slider, I can do this:

    Aired Between ... as a slider

  • Sharing WordPress Content with any PHP App

    Sharing WordPress Content with any PHP App

    Last week I explained how I shared my WordPress content with Hugo. Now, that was all well and good, but there is an obvious way I can do this when I don’t have a tool that understands dynamic content?

    I’ve got PHP so the answer is “You bet your britches, baby!”

    The First Version

    If you just want to grab the URL and output the post content, you can do this:

    $curl = curl_init();
    curl_setopt_array( $curl, array(
    	CURLOPT_FAILONERROR    => true,
    	CURLOPT_CONNECTTIMEOUT => 30,
    	CURLOPT_TIMEOUT        => 60,
    	CURLOPT_FOLLOWLOCATION => false,
    	CURLOPT_MAXREDIRS      => 3,
    	CURLOPT_SSL_VERIFYPEER => false,
    	CURLOPT_RETURNTRANSFER => true,
    	CURLOPT_URL            => 'https://example.com/wp-json/wp/v2/pages/12345'
    ));
    
    $result = curl_exec( $curln);
    curl_close( $curl );
    
    $obj = json_decode( $result );
    
    echo $obj->content->rendered;
    

    Now this does work, and it works well, but it’s a little basic and it doesn’t really sanitize things. Plus I would be placing this code in multiple files per site (not everyone themes as nicely as WordPress, ladies and gentlemen). So I wanted to write something that was more easily repeatable.

    The Advanced Code

    With that in mind, I whipped up a PHP file that checks and validates the URL, makes sure it’s a wp-json URL, makes sure it’s JSON, and then spits out the post content.

    <?php
    
    /* This code shows the content of a WP post or page.
     *
     * To use, pass the variable ?url=FOO
     *
     */
    
    if (!$_GET || !$_GET["url"]) return;
    
    include_once( "StrictUrlValidator.php" );
    
    $this_url = (string) filter_var( $_GET['url'], FILTER_SANITIZE_URL );
    
    if (strpos( $this_url, 'wp-json') == FALSE ) return;
    
    function do_curl ( $url ) {
    	$curl = curl_init();
    
    	curl_setopt_array( $curl, array(
    		CURLOPT_FAILONERROR    => true,
    		CURLOPT_CONNECTTIMEOUT => 30,
    		CURLOPT_TIMEOUT        => 60,
    		CURLOPT_FOLLOWLOCATION => false,
    		CURLOPT_MAXREDIRS      => 3,
    		CURLOPT_SSL_VERIFYPEER => false,
    		CURLOPT_RETURNTRANSFER => true,
    		CURLOPT_URL            => $url
    	) );
    
    	return curl_exec( $curl );
    	curl_close( $curl );
    }
    
    if ( StrictUrlValidator::validate( $this_url, true, true ) === false ) {
    	$return = "ERROR: Bad URL";
    } else {
    	$obj = json_decode( do_curl ( $this_url ) );
    
    	if ( json_last_error() === JSON_ERROR_NONE ) {
    		$return = $obj->content->rendered;
    	} else {
    		$return = "ERROR: Bad JSON";
    	}
    }
    
    echo $return;
    

    You can see I have some complex and some basic checks in there. The URL validation is done via a PHP Library called StrictUrlValidator. If I was using WordPress, I’d have access to esc_url() and other nifty things, but since I’m running this out in the wild, I make do with what I have.

  • Sharing WordPress Content with Hugo

    Sharing WordPress Content with Hugo

    When your life is just WordPress, there’s not a lot of headaches involved in making some posts and updating widgets and keeping your whole site in sync.

    When your life isn’t just WordPress, it gets a little weird.

    When you want to use WordPress to run your life a little more, you un-weird it by making it weirder.

    Rethinking Where the Content Lives

    Normally we think about content living on its own site. Well, I have a ‘message’ that needs to be the same on five different domains. Just work with me here. The point is, if I want to update the header message on the five sites, I have to update five sites. Yuck.

    Now, there are a lot of solutions to this. I decided I wanted one, and only one, place to update the header message. The most obvious is making a static text file that I could update when needed and import/include it in everywhere. But as I started looking into how I do that, my eyes drifted to WordPress.

    What if I used JSON? What if instead of a file, I made a page on WordPress, grabbed the content from https://example.com/wp-json/wp/v2/pages/12345 and parsed that so I didn’t have to do a whole mess of editing anywhere but on WordPress?

    Hugo

    Guess what. That works. And it works extra well for Hugo (a static site generator I’m fond of) because Hugo understands dynamic content. The one drawback is that it can’t live refresh remote data, so I will always have to push a change to the site to trigger this rebuild.

    However if you ever wondered how to include WordPress’ JSON data into a Hugo theme, here’s what I have in my template for utility-bar.html:

    {{ $wordpressURL  := "https://example.com/wp-json/wp/v2/pages/12345" }}
    {{ $wordpressJSON := getJSON $wordpressURL }}
    
    <div class="utility-bar">
    	<div class="wrap">
    		<section id="text-16" class="widget widget_text">
    			<div class="widget-wrap">
    				<div class="textwidget">
    					{{ $wordpressJSON.content.rendered | safeHTML}}
    				</div>
    			</div>
    		</section>
    	</div>
    </div>
    

    The reason safeHTML is there is that otherwise Hugo wants to escape my HTML. Which is a wise choice! Default to not trusting.

    This outputs the post content and I have a happy (enough) day. The more I look at it, the more I realize how much I can do with WordPress and Hugo, since regenerating the site just takes a push of Hugo content.

  • 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.

  • CMB2: Repeatable Groups

    CMB2: Repeatable Groups

    This is something that the plugin does out of the box, but my reason for doing it was a little odd.

    Background

    Originally, I had a set of TV characters as a custom post type and each one had their own TV show. Since the TV shows are a second post type, the data was saved as a number and that number was used to generate data on the show pages. Look for everyone who has a TV show value of the same ID as the post ID. Yay!

    The problem with it was spinoffs and crossovers. As time went on, certain characters began to appear on other shows. And it only got worse, until at length there were 30 characters on more than one show, and the number was only growing.

    The quick fix was to make the shows value a repeatable field in CMB2, where I could add multiple shows. Done and done. But then we reached critical mass with how we were handling character roles. Was the character a main, a recurring, or a guest?

    Shows and Roles

    Breaking down the problem to it’s most simple, we have one data set:

    • Show (stored as an ID in an array)
    • Role Type (stored as plain text)

    Instead of saving it as a data set together, the shows were one field (an array, as I mentioned) and the role types were another (a text field).

    In order to make this work, I would have to:

    1. Create a field ‘group’ in CMB2 that stored both show and role as related to that show
    2. Make that group repeatable for characters on multiple shows
    3. Migrate the data

    Data Migration

    There are a lot of ways around this. I ended up with going for the super simple route. I exported two CSVs from my database: one of the shows and one of the role types. Each one had the Post ID associated with it, so I opened those up in a spreadsheet app and combined them, for all cases where the Post ID was the same.

    This gave me a new table that looked like this: 123, 456, regular

    More or less. The ones where shows were arrays looked like, obviously, arrays. I then converted that into a file with 1500 lines that looked like this:

    wp post meta add 6957 character_tvshow_group '[{"show":"6951","type":"regular"},{"show":"7009","type":"regular"}]' --format=json
    

    I could have done it differently, grabbing a file with the data and parsing it on the fly, but I like to look at my 1500 lines and make sure I don’t have weird extra quotes lying around.

    Once that was done, I ran the file, having it execute every line one at a time. It took about one episode of House Hunters: International.

    The CMB2 Code

    In case you’re wondering the code to do this in CMB2 looks like this:

    		// Field Group: Character Show information
    		// Made repeatable since each show might have a separate role. Yikes...
    		$group_shows = $cmb2->add_field( array(
    			'id'          => $prefix . 'show_group',
    			'type'        => 'group',
    			'repeatable'  => true,
    			'options'     => array(
    				'group_title'   => 'Show #{#}',
    				'add_button'    => 'Add Another Show',
    				'remove_button' => 'Remove Show',
    				'sortable' => true,
    			),
    		) );
    		// Field: Show Name
    		$cmb2->add_group_field( $group_shows, array(
    			'name'             => 'TV Show',
    			'id'               => 'show',
    			'type'             => 'select',
    			'show_option_none' => true,
    			'default'          => 'custom',
    			'options_cb'       => array( $this, 'cmb2_get_shows_options'),
    		) );
    		// Field: Character Type
    		$cmb2->add_group_field( $group_shows, array(
    			'name'             => 'Character Type',
    			'id'               => 'type',
    			'type'             => 'select',
    			'show_option_none' => true,
    			'default'          => 'custom',
    			'options'          => $this->character_roles,
    		) );
    

    You’ll notice the options are a bit extra custom.

    Get Shows

    This is done in two parts:

    	public function Sitename_get_post_options( $query_args ) {
    	    $args = wp_parse_args( $query_args, array(
    	        'post_type'   => 'post',
    	        'numberposts' => wp_count_posts( 'post' )->publish,
    	        'post_status' => array('publish'),
    	    ) );
    
    	    $posts = get_posts( $args );
    
    	    $post_options = array();
    	    if ( $posts ) {
    	        foreach ( $posts as $post ) {
    	          $post_options[ $post->ID ] = $post->post_title;
    	        }
    	    }
    
    	    asort($post_options);
    	    return $post_options;
    	}
    
    	public function cmb2_get_shows_options() {
    		return SiteName_get_post_options( array(
    				'post_type'   => 'post_type_shows',
    				'numberposts' => wp_count_posts( 'post_type_shows' )->publish,
    				'post_status' => array('publish', 'pending', 'draft', 'future'),
    			) );
    	}
    

    The reason we search for all shows, from draft to future, is that sometimes we like to schedule updates.

    Character Roles

    		$this->character_roles = array(
    			'regular'   => 'Regular/Main Character',
    			'recurring'	=> 'Recurring Character',
    			'guest'	 	=> 'Guest Character',
    		);
    
  • Linear Regressions in PHP

    Linear Regressions in PHP

    Sometimes math exists to give me a headache.

    In calculating the deaths of queer females per year, my wife wondered what the trend was, other than “Holy sweat socks, it’s going up!” That’s called a ‘trendline’ which is really just a linear regression. I knew I needed a simple linear regression model and I knew what the formula was. Multiple the slope by the X axis value, and add the intercept (which is often a negative number), and you will calculate the points needed.

    Using Google Docs to generate a trend line is easy. Enter the data and tell it to make a trend line. Using PHP to do this is a bit messier. I use Chart.js to generate my stats into pretty graphs, and while it gives me a lot of flexibility, it does not make the math easy.

    I have an array of data for the years and the number of death per year. That’s the easy stuff. As of version 2.0 of Chart.js, you can stack charts, which lets me run two lines on top of each other like this:

    var myChart = new Chart(ctx, {
        type: 'bar',
        data: {
            labels: ['Item 1', 'Item 2', 'Item 3'],
            datasets: [
                {
                    type: 'line',
                    label: 'Line Number One',
                    data: [10, 20, 30],
                },
                {
                    type: 'line',
                    label: 'Line Number Two',
                    data: [30, 20, 10],
                }
            ]
        }
    });
    

    But. Having the data doesn’t mean I know how to properly generate the trend. What I needed was the most basic formula solved: y = x(slope) + intercept and little more. Generating the slope an intercept are the annoying part.

    For example, slope is (NΣXY - (ΣX)(ΣY)) / (NΣX2 - (ΣX)2) where,

    • x and y are the variables.
    • b = The slope of the regression line
    • a = The intercept point of the regression line and the y axis.
    • N = Number of values or elements
    • X = First Score
    • Y = Second Score
    • ΣXY = Sum of the product of first and Second Scores
    • ΣX = Sum of First Scores
    • ΣY = Sum of Second Scores
    • ΣX2 = Sum of square First Scores

    If that made your head hurt, here’s the PHP to calculate it (thanks to Richard Thome ):

    	function linear_regression( $x, $y ) {
    
    		$n     = count($x);     // number of items in the array
    		$x_sum = array_sum($x); // sum of all X values
    		$y_sum = array_sum($y); // sum of all Y values
    
    		$xx_sum = 0;
    		$xy_sum = 0;
    
    		for($i = 0; $i < $n; $i++) {
    			$xy_sum += ( $x[$i]*$y[$i] );
    			$xx_sum += ( $x[$i]*$x[$i] );
    		}
    
    		// Slope
    		$slope = ( ( $n * $xy_sum ) - ( $x_sum * $y_sum ) ) / ( ( $n * $xx_sum ) - ( $x_sum * $x_sum ) );
    
    		// calculate intercept
    		$intercept = ( $y_sum - ( $slope * $x_sum ) ) / $n;
    
    		return array( 
    			'slope'     => $slope,
    			'intercept' => $intercept,
    		);
    	}
    

    That spits out an array with two numbers, which I can plunk into my much more simple equation and, in this case, echo out the data point for each item:

    foreach ( $array as $item ) {
         $number = ( $trendarray['slope'] * $item['name'] ) + $trendarray['intercept'];
         $number = ( $number <= 0 )? 0 : $number;
         echo '"'.$number.'", ';
    }
    

    And yes. This works.

    Trendlines and Death