Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: plugins

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

  • 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

  • FacetWP: Making Sorting Suck Less

    FacetWP: Making Sorting Suck Less

    Sorting data in WordPress is generally done in the most basic of ways. You want to see all posts that are in a specific category, you go to example.com/category/drinks/ and there you are. But if you want to see everything in the category ‘drinks’ with the tag ‘bourbon’ and the custom taxonomy of ‘ingredients’ and a value of ‘mint’ AND ‘simple syrup’ to get the recipe for a mint julep, then you have a pretty crazy complex query.

    Enter FacetWP

    FacetWP is a premium plugin that, for $79 a year, handles all that crazy sorting for you. And yes, it’s worth it.

    FacetWP introduces advanced filtering to WordPress, which lets you do things like get that list of all drinks made with bourbon that include a simple syrup, in a dynamic way! It’s incredibly fast, since it’s using ajax and javascript, and as long as you have enough server memory to index all the data in the first place, it’s faster than reloading a new category page.

    Downsides

    In order to be that fast, you do not get pretty URLs. Let’s say you have your drinks category at `example.com/category/drinks’ and you want to list all those things. Your URL will look like this:

    example.com/category/drinks/?fwp_alcohol=bourbun&fwp_ingredients=simple+syrup%2Cmint

    The realistic reason they don’t try to make it ‘pretty’ is that it would create a lot more rewrite rules than would be sustainable, if you have a lot of facets. The number of checks would slow your site down, and that would kind of suck.

    Compatibility Notes

    If you use CMB2 you’ll need FacetWP + CMB2.

    If you use Genesis themes, there are two tricks. First, you’ll want to use the following function to add FacetWP’s CSS to your theme:

    function YOURTHEMENAME_facetwp_class( $atts ) {
        $atts['class'] .= ' facetwp-template';
        return $atts;
    }
    add_filter( 'genesis_attr_content', 'YOURTHEMENAME_facetwp_class' );
    

    Second, if you’re like me and you use a lot of custom loops, they may not behave as expected. If you call the loop multiple times on a page (which is bad behavior in the first place and I know it), FacetWP has a bit of trouble knowing what javascript to apply to what section. That should be expected, and once I cleaned it up, it worked great.

    Should you use it?

    If you have a lot of complex intersectional queries to sort through, yes.

    If you need dynamic result updates, yes.

    It works.

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

  • Show Featured Images on Post List

    Show Featured Images on Post List

    Let’s say you want to be sure every single post, in ever single post type, has a featured image. And you know someone might forget but you don’t want to have to open up every damn post to see.

    There are a lot of answers to this problem, and I love that there’s a plugin that will add in a Featured Image Column that shows a teeny featured image. But it wasn’t what I wanted. It was over kill.

    All I needed was a simple check mark for if there was an image and an X if there wasn’t, and that would suit me fine. And I wanted the column to be small, without a lot of fuss or folderol. I wanted something simple:

    My Featured Image Column

    That gives me a fast overview of if everything is what I wanted and where I wanted.

    The Code

    The code itself is the most basic column code, with a little bit of magic to put it as the first column on the list. Since I want to show this on all post types, I used the generic functions.

    /*
     * Show mark if featured image is set
     *
     * @since 1.1
     */
    
    add_filter('manage_posts_columns', 'helf_fi_manage_posts_columns');
    function helf_fi_manage_posts_columns( $columns ) {
    	if ( !is_array( $columns ) ) $columns = array();
    	$new_columns = array();
    
    	foreach( $columns as $key => $title ) {
    		if ( $key == 'title' ) $new_columns['featured_image'] = '<span class="dashicons dashicons-camera"></span>';
    		$new_columns[$key] = $title;
    	}
    
    	return $new_columns;
    }
    
    add_action('manage_posts_custom_column', 'helf_fi_manage_posts_custom_column', 10, 2);
    function helf_fi_manage_posts_custom_column( $column_name, $post_ID ) {
        if ($column_name == 'featured_image') {
            $post_featured_image = helf_fi_manage_column_check( $post_ID );
            $output = '<span class="dashicons dashicons-no"></span>';
            if ( $post_featured_image && $post_featured_image == true ) $output = '<span class="dashicons dashicons-yes"></span>';
            echo $output;
        }
    }
    
    function helf_fi_manage_column_check( $post_ID ) {
        $post_thumbnail_id = get_post_thumbnail_id( $post_ID );
        $post_thumbnail_img = false;
        if ( $post_thumbnail_id ) $post_thumbnail_img = true;
    	return $post_thumbnail_img;
    }
    
    add_action( 'admin_print_scripts', 'helf_fi_admin_print_styles' );
    function helf_fi_admin_print_styles(){
    	echo '
    	<style>
    		th#featured_image,
    		td.featured_image.column-featured_image {
    			max-height: 25px;
    			max-width: 25px;
    			width: 25px;
    			color: #444;
    		}
    		td.featured_image span.dashicons-no {
    			color: #dc3232;
    		}
    		td.featured_image span.dashicons-yes {
    			color: #46b450;
    		}
    		div#screen-options-wrap.hidden span.dashicons-camera {
    			padding-top: 5px;
    		}
    	</style>';
    }
    

    Fork and enjoy!