Half-Elf on Tech

Thoughts From a Professional Lesbian

Author: Ipstenu (Mika Epstein)

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

  • Will You Help Me Sell My Plugin?

    Will You Help Me Sell My Plugin?

    I get asked this a lot. It comes with the territory, but people ask me to help them monetize their plugins all the time. And my answer is always the same.

    No

    As much as I am a strong advocate of people making money off of WordPress, and as much as I support plugin and theme devs in their work, I’m not out here to help you run your business. While I do spend time thinking of ways to get people to pay for services and software, I don’t prioritize it, and most of my ideas are just that. Ideas.

    Really what people are asking for is my ideas and my free work. And to that, I say no.

    Business Help Isn’t Free

    If you wanted to hire me to help, to look at your code and to assist you in coming up with business strategies, based on my experience in the WordPress world, that’s a different matter. That gets a ‘no’ because I don’t have the time to dedicate to that work. I have a full time job that I do like, and I have some volunteer work I enjoy, and I have a very addictive side project. Since I enjoy being married, I don’t take on extra work right now. I don’t need the money.

    But the point here, if you can’t tell, is that yes, I would expect you to pay me for my work.

    I’m No Good At Sales

    Of course, keep in mind the fact that I’m a terrible salesman. I don’t like exaggerating what a product can do, I don’t like even suggesting a lie. I downplay. And that’s because I don’t like it when people promise the moon and only deliver low Earth orbit. I want realistic goals and possibilities. Can you do anything with this plugin? Sure. But it comes at a cost and I feel people should know that cost.

    I’m Hard to be Bought

    Everyone may have a price, but my price is rarely money. I know this sounds weird, since I said I expect people to pay me for my work. You see, asking me to do you a favor for free doesn’t really happen. But also, asking me to do you a favor for pay won’t happen.

    And by this I mean reviews.

    I’ve been asked, many times, to review people’s themes and plugins and post about it here. And in general, I say no. I review the things I use and like because I use and like them. I’m driven by usability. If I like your ‘thing’ and I think people should hear about it, I’ll talk it up. If your thing is free or for sale, I don’t care. What I care is if your thing was what I needed and wanted, and I liked it.

    I Won’t Help You Sell

    That’s not my deal. It’s not my deal on this blog. I’ve never been bought off for a review, I’ve never been asked “Would you review this product of mine?” unless I’ve already been known to use it. And even then, I’ve told people “You don’t want me to review it. I like it, but you have some bad bugs.”

    I’m honest. I’m direct. I’m incurably truthful.

    You probably don’t want me to help you sell your stuff, but if I really like it, I may anyway.

  • CMB2, Select2, and Taxonomies

    CMB2, Select2, and Taxonomies

    I’m going to start with “This is not my best work.”

    In using CMB2, I have created situations where it’s smarter to have the ‘normal’ WordPress taxonomy fields changed. Oh sure, they work most of the time for most things, but most is not all. In my situation, I had some custom taxonomies that I did not want people adding to from the post-edit screen.

    To get around the issue, most of the taxonomies were drop-downs using the taxonomy_select field type. That let me control the display and have the drop-down be the terms they could add. Anyone with admin access could add more, of course, but they’d see special notes about that. It gave me control.

    The problem really arose when I had a multicheck list of terms to add. Yes, I had 1 to 20 terms that might be added. And while I could use taxonomy_multicheck to do that, it wasn’t perfect. It made the screen very large.

    Select2 is Better

    Select2 is a jQuery replacement for select boxes. Using it, you can make a simple dropdown where you can have a single (or even multiple) selections, but also it has a nice interface for multiple selections:

    Select2 example: a single and a multicheck

    That looks much nicer than a list or grid of 20 options. You click on the box and you get a dropdown:

    Select2 - Showing the dropdown

    Select2 and CMB2

    Thankfully there’s already a plugin/add-on for this with CMB2. Phil Wylie made cmb-field-select2 which I pulled into my site and it works quite well. Except… You can’t use it to save Taxonomy data properly!

    This is due to a lot of complicated things, and while my first instinct was to complain to myself that core CMB2 could do it, and thus so could everyone, I know it’s not that simple. All the effort CMB2 put into making that work is little short of phenomenal. It was hard and it’s complex and it’s outright weird. I looked at the code and backed away slowly.

    But that doesn’t mean it’s impossible. It’s just a little weird and it’s not my best work. But it does work.

    Show the Taxonomies

    The first step is that you have to make a function to convert the taxonomy to something that can be used in a selection box. Thankfully Phil already did this and his example code works:

    /**
     * Get a list of terms
     *
     * Generic function to return an array of taxonomy terms formatted for CMB2.
     * Simply pass in your get_terms arguments and get back a beautifully formatted
     * CMB2 options array.
     *
     * @param string|array $taxonomies Taxonomy name or list of Taxonomy names
     * @param  array|string $query_args Optional. Array or string of arguments to get terms
     * @return array CMB2 options array
     */
    function iweb_get_cmb_options_array_tax( $taxonomies, $query_args = '' ) {
    	$defaults = array(
    		'hide_empty' => false
    	);
    	$args = wp_parse_args( $query_args, $defaults );
    	$terms = get_terms( $taxonomies, $args );
    	$terms_array = array();
    	if ( ! empty( $terms ) ) {
    		foreach ( $terms as $term ) {
    			$terms_array[$term->term_id] = $term->name;
    		}
    	}
    	return $terms_array;
    }
    

    Next you call that in your CMB2 code:

    // Field: Genre
    $field_genre = $cmb_notes->add_field( array(
    	'name'              => 'Genre',
    	'desc'              => 'Subject matter.',
    	'id'                => 'theshows_genre',
    	'taxonomy'          => 'my_genres',
    	'type'              => 'pw_multiselect',
    	'select_all_button' => false,
    	'remove_default'    => 'true',
    	'options'           => iweb_get_cmb_options_array_tax( 'my_genres' ),
    	'attributes'        => array(
    		'placeholder' => 'What kind of show...'
    ),
    

    And now you can add taxonomy items via Select2. But … It doesn’t save the taxonomy data.

    Saving The Taxonomy Data

    This is the part of code I’m not thrilled about. You see, the code in the previous section adds a new postmeta field for theshows_genre with an array of the IDs added. And that’s it. That isn’t what I wanted. I certainly could use the postmeta data to generate the output, but I used Taxonomies for a reason. They’re incredibly useful.

    In order to save the data, I needed to take the content from the post meta and copy it into the values for saved taxonomies, but only sometimes. After kicking around the options, I decided that I would give priority to the postmeta, not the taxonomies. That would allow me to have them save the taxonomies all the time unless the post meta was empty.

    function select2_taxonomy_process( $post_id, $postmeta, $taxonomy ) {
    
    	$get_post_meta = get_post_meta( $post_id, $postmeta, true );
    	$get_the_terms = get_the_terms( $post_id, $taxonomy );
    
    	if ( is_array( $get_post_meta ) ) {
    		// If we already have the post meta, then we should set the terms
    		$get_post_meta   = array_map( 'intval', $get_post_meta );
    		$get_post_meta   = array_unique( $get_post_meta );
    		$set_the_terms = array();
    
    		foreach( $get_post_meta as $term_id ) {
    			$term = get_term_by( 'id' , $term_id, $taxonomy );
    			array_push( $set_the_terms, $term->slug );
    		}
    
    		wp_set_object_terms( $post_id, $set_the_terms , $taxonomy );
    
    	} elseif ( $get_the_terms && ! is_wp_error( $get_the_terms ) ) {
    		// If there's no post meta, we force the terms to be the default
    		$get_post_meta = array();
    		foreach( $get_the_terms as $term ) {
    			$term_id = $term->term_id;
    			array_push( $get_post_meta, $term_id );
    		}
    		update_post_meta( $post_id, $postmeta, $get_post_meta );
    	}
    
    }
    

    This is not perfect code. It’s not even very good code, I don’t think. I’m not happy that I had to break the terms out instead of just using the the content from $get_post_meta but for some reason, wp_set_object_terms() wasn’t happy with an array of terms. It was fine with the slugs, so that’s the way I went.

    The logic is basic. If there’s postmeta and it’s an array, it ‘wins.’ If it’s not, take the taxonomy data and push it into the term.

    Triggering The Save

    But how to trigger that code? And where and when?

    I wrote code for my custom post type that triggered a check every time the page was loaded. Which is why I don’t like it.

    add_action( 'init', 'select2_taxonomy_save' );
    function select2_taxonomy_save() {
    	// Force saving data to convert select2 saved data to a taxonomy
    	$post_id   = ( isset( $_GET['post'] ) )? $_GET['post'] : 0 ;
    	
    	if ( $post_id !== 0 && is_admin() ) {
    		$post_type = ( isset( $_GET['post_type'] ) )? $_GET['post_type'] : 0 ;
    		switch ( $post_type ) {
    			case 'post_type_shows':
    				LP_CMB2_Addons::select2_taxonomy_save( $post_id, 'theshows_tropes', 'my_tropes' );
    				LP_CMB2_Addons::select2_taxonomy_save( $post_id, 'theshows_tvgenre', 'my_genres' );	
    				break;
    		}
    	}
    }
    

    Obviously it’s not the best code out there. It runs too often, though at least it’s only on page loads.. It would be better if it only ran on save, however that had a problem with race conditions. I would end up with a case where the postmeta might still be blank. So having it run before the page loaded appeared to be my only hope. I also don’t like having the data stored twice, but there was a limit to how far I wanted to run with this.

    Pull requests welcome!

  • Front Facing Alerts on Posts

    Front Facing Alerts on Posts

    Once I wrote the back-end code to show admin alerts on posts, I decided to add in some front end magic.

    Since the original code was written to be called by hooking into the admin_notices action, the obvious solution was to hook into something on the front end. But where?

    Genesis

    I’m a fan of the Genesis themes. There’s a handy hook for placement called genesis_before_content_sidebar_wrap() and the call (based on last week’s code) is this:

    add_action( 'genesis_before_content_sidebar_wrap', 'MYSITE_admin_notices' );
    

    And immediately that outputs this below the menu:

    Output message above title

    Obviously there’s no CSS going on there, but that’s okay.

    Not Genesis

    That’s lovely but what if I don’t use Genesis?

    While a filter like add_filter( 'the_title', 'MYSITE_admin_notices', 10, 2 ); would work, it also calls it for every output of the title on a page. If you happen to be using a sidebar that links to your 5 most recent posts, it can get pretty messy.

    And the problem here is that WordPress doesn’t have standard template hooks like that. You can use get_header() but that puts it at the top of every page. There just isn’t a way to say “I want this above the title” and have it work universally on all themes. Bummer.

    That said, you can totally make the get_header() call work with a little CSS magic and a little extra if-checks.

    The CSS Code

    Remember our goal in life is to load as little as possible on pages, so the best thing is to check if this is even the right post type:

    add_action( 'wp_enqueue_scripts', 'MYSITE_wp_enqueue_scripts' );
    public function MYSITE_wp_enqueue_scripts( ) {
    	wp_register_style( 'cpt-shows-styles', plugins_url('shows.css', __FILE__ ) );
    
    	if( is_single() && get_post_type() == 'post_type_shows' ){
    		wp_enqueue_style( 'cpt-shows-styles' );
    	}
    }
    

    The actual CSS is up to you, but mine looks like this and is cribbed from WordPress core:

    .wrap .notice {
    	background: #fff;
    	border-left: 4px solid #fff;
    	-webkit-box-shadow: 0 1px 1px 0 rgba(0, 0, 0, .1);
    	box-shadow: 0 1px 1px 0 rgba(0, 0, 0, .1);
    	margin: 5px;
    	padding: 1px 12px;
    }
    
    .wrap .notice p {
    	margin: .5em 0;
    	padding: 2px;
    }
    
    .wrap .notice p:before {
    	margin-right: 6px;
    	vertical-align: bottom;
    }
    
    .wrap .notice-error {
    	border-left-color: #dc3232;
    }
    
    .wrap .notice-info {
    	border-left-color: #00a0d2;
    }
    
    .wrap .notice-warning {
    	border-left-color: #ffb900;
    }
    
    .wrap .notice-updated,
    .wrap .notice-success {
    	border-left-color: #46b450;
    }
    

    This basically reproduces the same CSS as you’d see on the Admin Dashboard.

    The PHP Code

    While we covered the brunt of the PHP code before, we need to make a few alterations to make sure that the output only shows on the right pages and to the right people.

    The first changes will be in this section:

    if ( $message ) {
    	printf( '<div class="notice %1$s"><p><span class="dashicons dashicons-%2$s"></span> %3$s</p></div>', esc_attr( $type ), esc_attr( $dashicon ), esc_html( $message ) );
    }
    

    While it’s possible to use is_admin() to check if a visitor is on the dashboard or not, this is going to show on both front and backend. That means a better choice is is_user_logged_in() so our code will look like if ( $message && is_user_logged_in() ) and now it only shows if you’re logged in.

    But. The code shows on every page. That meant the code showed up on the static front page because it had no code and just widgets. In order to make it only show up on the right pages, I put in a check for what was called when.

    Since I have multiple CPTs, I keep the code that is post type specific in a file that matches the name, and I have an extra file called all-cpts.php that calls common code that they all use. This helps me keep my code dry (Don’t Repeat Yourself). That makes it easy for me to use a switch based the post type and output the right code on the right pages:

    function MYSITE_CPT_admin_notices() {
    
    	if ( !get_post() ) return;
    
    	$message    = '';
    	$type       = 'updated';
    	$post       = get_post();
    
    	$content    = get_post_field( 'post_content', $post->ID );
    	$word_count = str_word_count( strip_tags( $content ) );
    
    	switch ( $post->post_type ) {
    		case 'post_type_shows':
    			$worthit = get_post_meta( $post->ID, 'shows_worthit_details', true );
    
    			if ( $worthit < '1' ) {
    				$type     = 'notice-info';
    				$message  = 'Is this show worth watching? We don\'t know. Halp!';
    				$dashicon = 'heart';
    
    				if ( $word_count < '100' ) {
    					$type     = 'notice-error';
    					$message  = 'We clearly know nothing about this show. Help!';
    					$dashicon = 'warning';
    				} elseif ( $word_count < '200' ) {
    					$type     = 'notice-warning';
    					$message  = 'This post is a stub. Please edit it and make it more awesome.';
    					$dashicon = 'info';
    				}
    			}
    			break;
    	}
    
    	if ( $message && is_user_logged_in() && ( is_single() || is_admin() ) ) {
    		printf( '<div class="wrap"><div class="notice %1$s"><p><span class="dashicons dashicons-%2$s"></span> %3$s</p></div></div>', esc_attr( $type ), esc_attr( $dashicon ), esc_html( $message ) );
    	}
    
    }
    

    Now the message shows up for logged in users only, and only on the intended pages.

  • Admin Alerts on Posts

    Admin Alerts on Posts

    As the new WordPress.org plugin directory rolls out, I know a lot of people are fussing over the front end. I’m not for a very practical reason. I’m working on making the backend functional and educational.

    Information Is Key

    A lot of the information about what a various post status means is something I understand from experience. My goal is to expand the team as broadly as possible and in order to do that, I must document. But documentation is more than just writing up a doc and telling people to follow it.

    People need contextual reminders of what the status of a ‘thing’ is to understand what they must do next. With that in mind, I leveraged some code by Obenland to make reviewer alerts better. Originally, you see, our plan was to let plugin developers edit things from the admin panel. Everything’s moving to the front end except for the reviewers you see.

    On Beyond Plugins

    That’s all well and good, but what if I want to do it on my own sites? What if I wanted to have a site message for the editors on my site, so they’d know what posts are ‘done’ and which need some love? What if I wanted to encourage my fellow editors to edit a post I’d worked on?

    I made my plan, based off the old stub template from MediaWiki, because that was always a good way to know if a post did or didn’t need some love.

    1. I needed a way to ‘flag’ a post as a stub automatically
    2. I needed it to display on the page being edited
    3. I would like it to display on the front end but only to logged in users (for now)

    Stub Qualifications

    In order to flag a post automatically, I needed to come up with criteria or qualifications that would mark a post as a stub. Obviously content was part of it, but measuring the quality of the content was going to be difficult.

    My initial criteria for a stub was the following:

    • Under 100 words in post content
    • Empty meta value for “Worth it details”

    Obviously that second item is unique to my situation, but having it meant I could measure engagement by how much someone knew about a show to enter the data. That criteria seemed too strict though. So I tweaked them to be an OR and not an AND. No matter what, if there was under 100 words it was getting flagged. And no matter what, if there was no meta for worthiness, it would be flagged.

    • Info: No meta value for Worth It
    • Warning: No meta value for Worth It and post content under 200 words
    • Alert: No meta value for Worth It and post content under 100 words

    WP Admin Code

    The original code that all this is based on is from WordPress.org (not the WordPress code but the stuff that runs the domain).

    add_action( 'edit_form_after_title', 'MYSITE_admin_notices' );
    function MYSITE_admin_notices() {
    	$message    = '';
    	$type       = 'updated';
    	$post       = get_post();
    
    	switch ( $post->post_type ) {
    		case 'post_type_shows':
    
    			$content    = get_post_field( 'post_content', $post->ID );
    			$word_count = str_word_count( strip_tags( $content ) );
    			$worthit    = get_post_meta( $post->ID, 'worthit_details', true );
    
    			if ( $worthit < '1' ) {
    				$type     = 'notice-info';
    				$message  = 'Is this show worth watching? We don\'t know. Halp!';
    				$dashicon = 'heart';
    
    				if ( $word_count < '100' ) {
    					$type     = 'notice-error';
    					$message  = 'We clearly know nothing about this show. Help!';
    					$dashicon = 'warning';
    				} elseif ( $word_count < '200' ) {
    					$type     = 'notice-warning';
    					$message  = 'This post is a stub. Please edit it and make it more awesome.';
    					$dashicon = 'info';
    				}
    			}
    			break;
    	}
    
    	if ( $message ) {
    		printf( '<div class="notice %1$s"><p><span class="dashicons dashicons-%2$s"></span> %3$s</p></div>', esc_attr( $type ), esc_attr( $dashicon ), esc_html( $message ) );
    	}
    
    }
    

    The reason it uses edit_form_after_title and prints the error instead of add_settings_error is that I don’t actually want these alerts to be dismissible and I only want it to show on post editor pages.

    The Output

    This post needs some attention

    There’s some improvements to be made, but this is a start.