Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: wordpress

  • Stacked Charts Part 3: The Javascript

    Stacked Charts Part 3: The Javascript

    Finally!

    We have our data in a properly consumable array. It’s formatted the way we need. Now we just need to script the java.

    Take a deep breath.

    What We Want

    What we want is simple. A stacked bar chart that shows the values of all possible permutations. It looks like this:

    A stacked chart that shows how many characters per gender orientation there are per country
    A stacked chart

    That shows how many characters there are per gender orientation, and stacks it for a total count (which is why we needed that count you see).

    Send In The Clowns

    Since I’m already using Chart.js, I just need to have a function to output the javascript. But. Since I also have to loop through the arrays to get the collective data, I need a bit of PHP:

    /*
     * Statistics Display Barcharts
     *
     * Output the list of data usually from functions like self::meta_array
     * It loops through the arrays and outputs data as needed
     *
     * This relies on ChartJS existing
     *
     * @param string $subject The content subject (shows, characters)
     * @param string $data The data - used to generate the URLs
     * @param array $array The array of data
     *
     * @return Content
     */
    static function stacked_barcharts( $subject, $data, $array ) {
    
    	// Defaults
    	$data       = ( $data == 'nations' )? 'nations' : substr( $data, 8 );
    	$title      = ucfirst( substr($subject, 0, -1) ) . ' ' . ucfirst( $data );
    	$height     = '550';
    
    	// Define our settings
    	switch ( $data ) {
    		case 'gender':
    		case 'sexuality':
    		case 'romantic':
    			$title    = 'Character per Nation by ' . ucfirst( $data );
    			$datasets = array();
    			$terms    = get_terms( 'lez_' . $data, array( 'orderby' => 'count', 'order' => 'DESC', 'hide_empty' => 0 ) );
    			if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
    				foreach ( $terms as $term ) $datasets[] = $term->slug;
    			}
    			$counter  = 'characters';
    			$height   = '400';
    			break;
    	}
    	?>
    	<h3><?php echo $title; ?></h3>
    	<div id="container" style="width: 100%;">
    		<canvas id="barStacked<?php echo ucfirst( $subject ) . ucfirst( $data ); ?>" width="700" height="<?php echo $height; ?>"></canvas>
    	</div>
    
    	<script>
    	// Defaults
    	Chart.defaults.global.responsive = true;
    	Chart.defaults.global.legend.display = false;
    
    	// Bar Chart
    	var barStacked<?php echo ucfirst( $subject ) . ucfirst( $data ); ?>Data = {
    		labels : [
    		<?php
    			foreach ( $array as $item ) {
    				if ( $item[$counter] !== 0 ) {
    					$name = esc_html( $item['name'] );
    				}
    				echo '"'. $name .' ('.$item[$counter].')", ';
    			}
    		?>
    		],
    		datasets: [
    		<?php
    		foreach ( $datasets as $label ) {
    			$color = ( $label == 'undefined' )? 'nundefined' : str_replace( ["-", "–","-"], "", $label );
    			?>
    			{
    				borderWidth: 1,
    				backgroundColor: window.chartColors.<?php echo $color; ?>,
    				label: '<?php echo ucfirst( $label ); ?>',
    				stack: 'Stack',
    				data : [<?php
    					foreach ( $array as $item ) {
    						echo $item[ 'dataset' ][ $label ] . ',';
    					}
    				?>],
    			},
    			<?php
    		}
    		?>
    		]
    	};
    	var ctx = document.getElementById("barStacked<?php echo ucfirst( $subject ) . ucfirst( $data ); ?>").getContext("2d");
    	var barStacked<?php echo ucfirst( $subject ) . ucfirst( $data ); ?> = new Chart(ctx, {
    		type: 'horizontalBar',
    		data: barStacked<?php echo ucfirst( $subject ) . ucfirst( $data ); ?>Data,
    		options: {
    			scales: {
    				xAxes: [{ stacked: true }],
    				yAxes: [{ stacked: true }]
    			},
    			tooltips: {
    				mode: 'index',
    				intersect: false
    			},
    		}
    	});
    
    	</script>
    	<?php
    }
    

    The Color

    You may have noticed a strange variable:

    $color = ( $label == 'undefined' )? 'nundefined' : str_replace( ["-", "–","-"], "", $label );
    

    Which was then called in the javascript here:

    backgroundColor: window.chartColors.<?php echo $color; ?>,
    

    I have this in a javascript file that is loaded on that page:

    // Color Defines
    window.chartColors = {
    	
    	// Gender
    	agender: 'rgba(255, 99, 132, 0.6)', // 'red'
    	cisgender: 'rgba(75, 192, 192, 0.6)', // 'aqua'
    	demigender: 'rgba(255, 205, 86, 0.6)', // 'goldenrod'
    	genderfluid: 'rgba(54, 162, 235, 0.6)', // 'light blue'
    	genderqueer: 'rgba(255, 159, 64, 0.6)', // 'orange'
    	nonbinary: 'rgba(201, 203, 207, 0.6)', // 'grey'
    	transman: 'rgba(0, 169, 80, 0.6)', // 'green'
    	transwoman: 'rgba(153, 102, 255, 0.6)', // 'purple'
    
    	// Sexuality
    	asexual: 'rgba(255, 99, 132, 0.6)', // 'red'
    	bisexual: 'rgba(75, 192, 192, 0.6)', // 'aqua'
    	heterosexual: 'rgba(255, 205, 86, 0.6)', // 'goldenrod'
    	homosexual: 'rgba(54, 162, 235, 0.6)', // 'light blue'
    	pansexual: 'rgba(255, 159, 64, 0.6)', // 'orange'
    	nundefined: 'rgba(201, 203, 207, 0.6)', // 'grey'
    	queer: 'rgba(0, 169, 80, 0.6)', // 'green'
    	demisexual: 'rgba(153, 102, 255, 0.6)', // 'purple'
    }
    

    The reason it’s ‘undefined’ is that things got weird when I had a variable with a name of undefined.

  • Stacked Charts Part 2: Rebuilding the Array

    Stacked Charts Part 2: Rebuilding the Array

    I’ve talked about this before in category statistics, but in order to get the data from a simple array into a Chart.js consumable one, we have to rebuild the array.

    All Arrays are not Equal

    In order to save the data in a way I could use and reuse, I had to aim at the lowest common denominator. But also I had to save the arrays at a per show basis, which is not the same as what I was going to need to output.

    Instead of just outputting the averages for the show, I needed to combine all this into a ‘by nation’ statistic. That is, I needed to get a list of all shows that were associated with a taxonomy value for that country (easy) and combine all their arrays (not quite easy) and order the data in a way that would make sense (not easy).

    So again we start with understanding the array. Here’s a show that happens to air in Argentina:

    Array
    (
        [cisgender]    => 2
        [trans-woman]  => 0
        [trans-man]    => 0
        [non-binary]   => 0
        [gender-fluid] => 0
        [gender-queer] => 0
        [agender]      => 0
    )
    

    This is the data for one show. Argentina has 2, oddly both with the same stats breakdown by gender identity. What I need to do is loop through both those shows and add the arrays to be this:

    Array
    (
        [cisgender]    => 4
        [trans-woman]  => 0
        [trans-man]    => 0
        [non-binary]   => 0
        [gender-fluid] => 0
        [gender-queer] => 0
        [agender]      => 0
    )
    

    Get the Base Arrays

    Just like before, we make an array of the base data as we have it in the gender, sexuality, and romantic orientations. In this case, we’re adding in a query to change the order to be largest to smallest overall from the taxonomy. While this may not be true for all nations in the future, it is today:

    $taxonomy = get_terms( 'lez_nations' );
    foreach ( $taxonomy as $the_tax ) {
    }
    

    I need to pause here. Everything from here out goes in that foreach. We’re going to be looping for each nation in the list of nations. Now… I re-use this code for multiple taxonomies, so lez_nations is actually lez_' . $data and it dynamically changes based on how I call this function.

    On we go!

    	$characters = 0;
    	$shows      = 0;
    	
    	// Create a massive array of all the character terms we care about...
    	$valid_char_data = array( 
    		'gender'    => 'lez_gender',
    		'sexuality' => 'lez_sexuality',
    		'romantic'  => 'lez_romantic',
    	);
    
    	if ( isset( $subdata ) && !empty( $subdata ) ) {
    		$char_data = array();
    		$terms     = get_terms( $valid_char_data[ $subdata ], array( 'orderby' => 'count', 'order' => 'DESC' ) );
    
    		if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
    			foreach ( $terms as $term ) {
    				$char_data[ $term->slug ] = 0;
    			}
    		}
    	}
    

    Now that we have those base arrays, again set to zero,

    By the way, $subdata and $data are parameters sent to the function that runs this. $subdata is for the taxonomy we’re calculating (sexuality etc) and $data is for the overall taxonomy (Nations or perhaps Stations or genres – we use a lot of those).

    This gets us started.

    Queery the Posts

    Next we need a WP_Query of all the posts in the taxonomy.

    	$count = wp_count_posts( 'post_type_shows' )->publish;
    	$queery = new WP_Query ( array(
    		'post_type'              => 'post_type_shows',
    		'posts_per_page'         => $count,
    		'post_status'            => array( 'publish' ),
    		'tax_query'              => array( array(
    			'taxonomy' => 'lez_' . $data,
    			'field'    => 'slug',
    			'terms'    => $the_tax->slug,
    			'operator' => '=',
    		),),
    	) );
    	wp_reset_query();
    

    Remember, this is still within that foreach above. And once we have the posts, let’s query all the shows:

    	if ( $queery->have_posts() ) {
    		foreach( $queery->posts as $show ) {
    
    			$shows++;
    			// Get all the crazy arrays
    			$gender = get_post_meta( $show->ID, 'lezshows_char_gender' );
    			if ( isset( $subdata ) ) { 
    				$dataset = get_post_meta( $show->ID, 'lezshows_char_' . $subdata );
    			}
    
    			// Add the character counts
    			foreach( array_shift( $gender ) as $this_gender => $count ) {
    				$characters += $count;
    			}
    
    			if ( !empty( $dataset ) ) {
    				foreach( array_shift( $dataset ) as $this_data => $count ) {
    					$char_data[ $this_data ] += $count;
    				}
    			}
    
    		}
    	}
    

    The weird section you see, // Add the character counts is there because every character has a gender, but not everyone has a sexuality or romantic orientation. Because of that, I decided it was safest to use that as my baseline count.

    The second section that checks if ( !empty( $dataset ) ) {...} is what adds things up for the array.

    Speaking of…

    Output the New Array

    Once I have those counts, I generate different arrays depending on what I’m outputting. The basic barchart is different from a percentage, which is different from the stacked bar.

    	// Determine what kind of array we need to show...
    	switch( $format ) {
    		case 'barchart':
    			$array[] = array (
    				'name'  => $the_tax->name,
    				'count' => $shows,
    			);
    			break;
    		case 'percentage':
    			$array = self::taxonomy( 'post_type_shows', 'lez_' . $data );
    			break;
    		case 'count':
    			$array = count( $taxonomy );
    			break;
    		case 'stackedbar':
    			$array[$the_tax->slug] = array(
    				'name'       => $the_tax->name,
    				'count'      => $shows,
    				'characters' => $characters,
    				'dataset'    => $char_data,
    			);
    	}
    

    And all of this is so I could get that silly stacked bar, which will have the count of total characters, shows, and the data.

    Whew.

  • Stacked Charts Part 1: Understanding Your Data

    Stacked Charts Part 1: Understanding Your Data

    There are a few different type of charts. Actually there are a lot. I find a nice bar chart fairly easy to read and understand. So when Tracy said we should generate some nice stats about nations, like how many shows there were per nation, I was able to do that pretty easily:

    An excerpt of shows by nation - USA has the most. Yaaaay.
    An excerpt of shows by nation

    And as far as that goes, it’s pretty cool. It’s really just the same code I use to generate category statistics already. This is, by the way, why using WordPress to generate your data is useful. It’s easy to replicate code you’ve already got.

    But then Tracy, who I think derives some perverse joy out of doing this to me, says “Can we find out how many trans characters there are per nation?”

    Use WordPress First

    If you heard my talks about Sara Lance, you’ve heard me tout that data based sites should always use WordPress functions first. By which I mean they should use taxonomies and custom post types when possible, because accessing the data will be consistent, regular, and repeatable.

    Ironically, it’s because I chose to use WordPress than I was in a bit of a bind.

    You see, we have three post types on the site right now: shows, characters, and actors. The shows have the taxonomy of ‘nation’ so getting that simple data was straightforward. The characters store the taxonomies of gender identity and sexual preference. That sounds pretty logical, right?

    So how, you may wonder, do we get a list of characters on a show? A query. Basically we search wp_post_meta for all characters with the array of lezchars_show_group and, within that multidimensional, have a show of the post ID of the show saved. Which means the characters are dynamically generated every single time a page is loaded. And yes, that is why I use The L Word as my benchmark for page speed.

    However by doing all this dynamically, generating the stats for characters per nation would look like this:

    1. Use get_terms to get a list of all shows in a nation to …
    2. Loop through all those shows and …
    3. Loop through all the characters on each show to extract the data to …
    4. Store the data per nation

    Ouch. Talk about slow.

    Solution? Use WordPress!

    Thankfully there was a workaround. One of the other odd things we do with shows is generate a show ‘score’ – a value calculated by the shows relative awesomeness, our subjective enjoyment of it, and the number of characters, alive or dead, it has.

    In order to make that generation run faster, every time a show or character is saved, I trigger the following post_meta values to be saved:

    • lezshows_characters – An array of character counts alive and dead
    • lezshows_the_score – The insane math of the score

    So I added three more:

    • lezshows_sexuality
    • lezshows_gender
    • lezshows_romantic

    All of those are generated when the post is saved, as it loops through all the characters and extracts data.

    Generate The Base

    In order to get the basics, we start by generating an array of everything we’re going to care about. I do this by listing all the taxonomies I want to use and then loop through them, adding each slug to a new array with a value of 0:

    $valid_taxes = array( 
    	'gender'    => 'lez_gender',
    	'sexuality' => 'lez_sexuality',
    	'romantic'  => 'lez_romantic',
    );
    $tax_data = array();
    
    foreach ( $valid_taxes as $title => $taxonomy ) {
    	$terms = get_terms( $taxonomy );
    	if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
    		$tax_data[ $title ] = array();
    		foreach ( $terms as $term ) {
    			$tax_data[ $title ][ $term->slug ] = 0;
    		}
    	}
    }
    

    That gives me a multidimensional array which, I admit, is pretty epic and huge. But it lets move on to step two, of getting all the characters:

    $count          = wp_count_posts( 'post_type_characters' )->publish;
    $charactersloop = new WP_Query( array(
    	'post_type'              => 'post_type_characters',
    	'post_status'            => array( 'publish' ),
    	'orderby'                => 'title',
    	'order'                  => 'ASC',
    	'posts_per_page'         => $count,
    	'no_found_rows'          => true,
    	'meta_query'             => array( array(
    		'key'     => 'lezchars_show_group',
    		'value'   => $post_id,
    		'compare' => 'LIKE',
    	),),
    ) );
    

    Next I stop everything as a new array. Which is where we get into some serious fun. See, I have to actually double check the character is in the show, since the ‘like’ search has a few quirks when you’re searching arrays. The tl;dr explanation here is that if I look for shows with a post ID of “23” then I get “23” and “123” and “223” and so on.

    Yeah. It’s about as fun as you’d think. If I wasn’t doing arrays, this would be easier, but I have Sara Lance to worry about.

    if ($charactersloop->have_posts() ) {
    	while ( $charactersloop->have_posts() ) {
    		$charactersloop->the_post();
    		$char_id     = get_the_ID();
    		$shows_array = get_post_meta( $char_id, 'lezchars_show_group', true );
    
    		if ( $shows_array !== '' && get_post_status ( $char_id ) == 'publish' ) {
    			foreach( $shows_array as $char_show ) {
    				if ( $char_show['show'] == $post_id ) {
    					foreach ( $valid_taxes as $title => $taxonomy ) {
    						$this_term = get_the_terms( $char_id, $taxonomy, true );
    						if ( $this_term && ! is_wp_error( $this_term ) ) {
    							foreach( $this_term as $term ) {
    								$tax_data[ $title ][ $term->slug ]++;
    							}
    						}
    					}
    				}
    			}
    		}
    	}
    	wp_reset_query();
    }
    

    You’ll notice there’s a quick $tax_data[ $title ][ $term->slug ]++; in there to increment the count. That’s the magic that gets processed all over. It tells me things like “this show has 7 cisgender characters” which is the first half of everything I wanted.

    Because in the end I save this as an array for the show:

    foreach ( $valid_taxes as $title => $taxonomy ) { 
    	update_post_meta( $post_id, 'lezshows_char_' . $title , $tax_data[ $title ] );
    }
    

    How Well Does This Run?

    It’s okay. It’s not super awesome, since it has to loop so many times, this can get pretty chunky. See The L Word and it’s 60+ characters. However. It only updates when the show is saved, or a character is added to the show, which means the expensive process is limited. And by saving this data in an easily retrievable format, I’m able to do the next phase. Generate the stats.

  • Structured Data

    Structured Data

    Every month around the 15th and 30th, I check Google Webmaster to see if there are any 'errors' I need to address. Most of the time this is 404s on pages that were wrong for 30 minutes, and Google decided to crawl me right then. In January, I got a bunch of new alerts about 'structured data.'

    What is it?

    Google uses structured data that it finds on the web to understand the content of the page, as well as to gather information about the web and the world in general. Which is a fancy way of saying Google looks for specific classes and attributes in your page to determine what's what. 

    Most of the time, your theme handles this for you. It's why you see hentry and authorcard in your page source. This is part of telling Google what the content is, contextually.

    Bad Errors

    Sadly, Google's report just said I had "missing fn" errors and gave me this:

    itemtype: http://microformats.org/profile/hcard
    photo: https://secure.gravatar.com/avatar/CODE?s=118&d=retro&r=g

    That's not very helpful unless you already know what you're looking for. And even then… There was a link to test live data and I clicked on it, only to get a report back that I had no errors. Frustrating, right?

    Then I checked a different error:

    Missing: author
    Missing: entry-title
    Missing: updated

    That was a little better, I thought, and I found out that since we'd removed author and updated from those posts, for a valid reason mind you, that was showing an error. The 'easy' fix was to make this function:

    function microformats_fix( $post_id ) {
    	$valid_types = array( 'post_type_authors', 'post_type_characters', 'post_type_shows' );
    	if ( in_array( get_post_type( $post_id ), $valid_types ) ) {
    		echo '<div class="hatom-extra" style="display:none;visibility:hidden;">
    			<span class="entry-title">' . get_the_title( $post_id ) . '</span>
    			<span class="updated">' . get_the_modified_time( 'F jS, Y', $post_id ) . '</span>
    			<span class="author vcard"><span class="fn">' . get_option( 'blogname' ) . '</span></span>
    		</div>';
    	}
    }

    And then I just tossed it into my post content.

    Does This Matter?

    Kind of. It doesn't appear to actively hurt your SEO but it can help a little. In my case, I'm not actually using 'Author' so it's an inconvenience. And I don't want to make public when a page was last updated, since it's meant to be static content, but also it gets 'saved' a lot more than it gets updated, due to how I process some data. Basically it would lie to users.

    But. Apparently I get to lie to Google.

    Yaaay.

  • Customizing Jetpack Feedback

    Customizing Jetpack Feedback

    Fair bit of warning, this is a big code heavy post. I use Jetpack to handle a variety of things on my sites. Contact forms, stats, embeds, monitoring, backups, a bit of security (brute force prevention), and in some cases Photon and galleries. Oh and I especially use it for the tweeting and facebookieing of posts. Love it or hate it, it has it's uses and as a user, I find it easier to work with than its alternatives. However! Jetpack is not perfect. And my current drama llama was that I wanted to do two things:
    1. Show on my dashboard how many messages I had
    2. Mark a feedback message as 'answered' without deleting
    The second item was very important as on a shared-management type site, it's hard to know who did what and did everyone handle an email or what have you? Really a better tool would be something that you could reply to from within the admin dashboard, versus emailing all over, but that's another day. Instead, I decided to tackle a custom post status.

    This is NOT Fully Supported

    This is my caveat. My big warning. WordPress doesn't yet fully support Custom Post Status. That is, yes you can totally register them, but there's no easy way to put in the interface, as the trac ticket has been around since 2010 (you read that right) and it's still not done. All that said, if you're willing to wrangle a bit of Javascript and sanitize your outputs properly, you can do this.

    Big Block of Code

    <?php
    /*
    Description: Jetpack Customizations
    Version: 1.0
    */
    
    if ( ! defined('WPINC' ) ) die;
    
    /**
     * Customize_Jetpack_Feedback class.
     * Functions used by Jetpack to cutomize Feedback
     */
    class Customize_Jetpack_Feedback {
    
    	/**
    	 * Constructor
    	 * @since 1.0
    	 */
    	public function __construct() {
    		add_action( 'dashboard_glance_items', array( $this, 'dashboard_glance' ) );
    		add_action( 'admin_head', array( $this, 'dashboard_glance_css' ) );
    		
    		add_action( 'init', array( $this, 'custom_post_statuses' ), 0 );
    		add_filter( 'post_row_actions', array( $this, 'add_posts_rows' ), 10, 2);
    		add_action( 'plugins_loaded', array( $this, 'mark_as_answered' ) );
    		add_filter( 'display_post_states', array( $this, 'display_post_states' ) );
    		add_action( 'admin_footer-post.php', array( $this, 'add_archived_to_post_status_list' ) );
    		add_action( 'admin_footer-edit.php', array( $this, 'add_archived_to_bulk_edit' ) );
    	}
    
    	/**
    	 * Add custom post status for Answered
    	 * 
    	 * @access public
    	 * @return void
    	 * @since 1.0
    	 */
    	public function custom_post_statuses() {
    		register_post_status( 'answered', array(
    			'label'                     => 'Answered',
    			'public'                    => false,
    			'exclude_from_search'       => true,
    			'show_in_admin_all_list'    => true,
    			'show_in_admin_status_list' => true,
    			'label_count'               => _n_noop( 'Answered <span class="count">(%s)</span>', 'Answered <span class="count">(%s)</span>' ),
    		) );
    	}
    
    	/**
    	 * Add URL for replying to feedback.
    	 * 
    	 * @access public
    	 * @param mixed $actions
    	 * @param mixed $post
    	 * @return void
    	 * @since 1.0
    	 */
    	public function add_posts_rows( $actions, $post ) {
    		// Only for Feedback
    		if ( $post->post_type == 'feedback' ) {
    			$url = add_query_arg( 'answered_post_status-post_id', $post->ID );
    			$url = add_query_arg( 'answered_post_status-nonce', wp_create_nonce( 'answered_post_status-post_id' . $post->ID ), $url );
    	
    			// Edit URLs based on status
    			if ( $post->post_status !== 'answered' ) {
    				$url = add_query_arg( 'answered_post_status-status', 'answered', $url );
    				$actions['answered_link']  = '<a href="' . $url . '" title="Mark This Post as Answered">Answered</a>';
    			} elseif ( $post->post_status == 'answered' ){
    				$url = add_query_arg( 'answered_post_status-status', 'publish', $url );
    				$actions['answered']  = '<a class="untrash" href="' . $url . '" title="Mark This Post as Unanswered">Unanswered</a>';
    				unset( $actions['edit'] );
    				unset( $actions['trash'] );
    			}
    		}
    		return $actions;
    	}
    
    	/**
    	 * Add Answered to post statues
    	 * 
    	 * @access public
    	 * @param mixed $states
    	 * @return void
    	 * @since 1.0
    	 */
    	function display_post_states( $states ) {
    		global $post;
    
    		if ( $post->post_type == 'feedback' ) {
    			$arg = get_query_var( 'post_status' );
    			if( $arg != 'answered' ){
    				if( $post->post_status == 'answered' ){
    					return array( 'Answered' );
    				}
    			}
    		}
    
    		return $states;
    	}
    
    	/**
    	 * Process marking as answered
    	 * 
    	 * @access public
    	 * @return void
    	 * @since 1.0
    	 */
    	public function mark_as_answered() {
    
    		// If contact forms aren't active, we'll just pass
    		if ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'contact-form' ) ) {
    
    			// Check Nonce
    			if ( isset( $_GET['answered_post_status-nonce'] ) && wp_verify_nonce( $_GET['answered_post_status-nonce'], 'answered_post_status-post_id' . $_GET['answered_post_status-post_id'] ) ) { 
    				// Check Current user Can and then process
    				if( current_user_can('publish_posts') && isset( $_GET['answered_post_status-status'] ) ) {
    					$GLOBALS[ 'wp_rewrite' ] = new wp_rewrite;
    		
    					$status  = sanitized_text_field( $_GET['answered_post_status-status'] );
    					$post_id = (int) $_GET['answered_post_status-post_id'];
    		
    					// If it's not a valid status, we have a problem
    					if ( !in_array( $status, array( 'answered', 'publish' ) ) ) die( 'ERROR!!!' );
    		
    					$answered = array( 'ID' => $post_id, 'post_status' => $status );
    					wp_update_post( $answered );
    				}
    			}
    
    		}
    	}
    
    
    	/**
    	 * add_archived_to_post_status_list function.
    	 * 
    	 * @access public
    	 * @return void
    	 * @since 1.0
    	 */
    	function add_archived_to_post_status_list(){
    		global $post;
    		$complete = $label = '';
    
    		// Bail if not feedback
    		if ( $post->post_type !== 'feedback' ) return;
    
    		if( $post->post_status == 'answered' ) {
    			echo '
    				<script>
    					jQuery(document).ready(function($){
    						$("#post-status-display" ).text("Answered");
    						$("select#post_status").append("<option value=\"answered\" selected=\"selected\">Answered</option>");
    						$(".misc-pub-post-status label").append("<span id=\"post-status-display\">Answered</span>");
    					});
    				</script>
    			';
    		} elseif ( $post->post_status == 'publish' ){
    			echo '
    				<script>
    					jQuery(document).ready(function($){
    						$("select#post_status").append("<option value=\"answered\" >Answered</option>");
    					});
    				</script>
    			';
    		}
    	} 
    
    	public function add_archived_to_bulk_edit() {
    		global $post;
    		if ( $post->post_type !== 'feedback' ) return;	
    		?>
    			<script>
    			jQuery(document).ready(function($){
    				$(".inline-edit-status select ").append("<option value=\"answered\">Answered</option>");
    				$(".bulkactions select ").append("<option value=\"answered\">Mark As Answered</option>");
    			});
    			</script>
    		<?php
    	}
    
    	/*
    	 * Show Feedback in "Right Now"
    	 *
    	 * @since 1.0
    	 */
    	public function dashboard_glance() {
    		if ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'contact-form' ) ) {
    			foreach ( array( 'feedback' ) as $post_type ) {
    				$num_posts = wp_count_posts( $post_type );
    				$count_posts = ( isset( $num_posts->publish ) )? $num_posts->publish : '0';
    				if ( $count_posts !== '0' ) {
    					if ( 'feedback' == $post_type ) {
    						$text = _n( '%s Message', '%s Messages', $count_posts );
    					}
    					$text = sprintf( $text, number_format_i18n( $count_posts ) );
    					printf( '<li class="%1$s-count"><a href="edit.php?post_type=%1$s">%2$s</a></li>', $post_type, $text );
    				}
    			}
    		}
    	}
    
    	/*
    	 * Custom Icon for Feedback in "Right Now"
    	 *
    	 * @since 1.0
    	 */
    	public function dashboard_glance_css() {
    	if ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'contact-form' ) ) {
    		?>
    		<style type='text/css'>
    			#adminmenu #menu-posts-feedback div.wp-menu-image:before, #dashboard_right_now li.feedback-count a:before {
    				content: '\f466';
    				margin-left: -1px;
    			}
    		</style>
    		<?php
    		}
    	}
    
    }
    
    new Customize_Jetpack_Feedback();
    
    What this does is create a new status for your feedback of “Answered”. Ta Dah!
  • Restrict Site Access Filters

    Restrict Site Access Filters

    I have a demo site I use to for development. One of the things I want is to be able to lock the site to logged in users only and that I can do via Restricted Site Access by 10up.

    One of the things the plugin also allows is to open up access to an IP, so someone who doesn't have an account can check the site before you go live. The problem with this feature is caching.

    Caching Restricted Pages

    It doesn't really matter what kind of caching system you use, the point is all the same. People who aren't logged in should get a cached version of the content. People who are logged in, or whom you've determined need a unique experience, don't get cached content. That's the barebones of caching.

    The problem I ran into with restricted site access is that if I whitelisted an IP range, and someone from that range visited the site, they generated a page which my cache system … cached. That meant the next person got to see the cached content.

    Worf from Star Trek face-palming

    Now this may not actually be a problem in all cache systems, but I happened to be using Varnish, which is fairly straightforward about how it works. And, sadly, the plugin I'm using doesn't have a way around this. Yet.

    Filters and Hooks

    Like any enterprising plugin hoyden, I popped open the code and determined I needed to address the issue here:

    // check if the masked versions match
    if ( ( inet_pton( $ip ) & $mask ) == ( $remote_ip & $mask ) ) {
    	return;
    }

    This section of code is checking "If the IP matches the IP we have on our list, stop processing the block. It's okay to show them the content." What I needed was to add something just above the return to tell it "And if it's Varnish, don't cache!"

    At first my idea was to just toss a session_start() in there, which does work. For me. Adam Silverstein was leery of that having unintended consequences for others, and wouldn't it be better to make it hookable? After all, then any caching plugin could hook in! He was right, so I changed my pull request to this:

    do_action( 'restrict_site_access_ip_match', $remote_ip, $ip, $mask ); // allow users to hook ip match

    The next version of the release will have that code.

    In The Field

    Now, assuming you've slipped that code into your plugin, how do you actually use it?

    Since I need to have this only on my 'dev' site, and I'm incredibly lazy efficient, I decided to put this code into the MU plugins I use for the site:

    if ( DB_HOST == 'mysql.mydevsite.dream.press' ) {
    	add_action( 'restrict_site_access_ip_match', 'mydevsite_restrict_site_access_ip_match' );
    }
    
    function mydevsite_restrict_site_access_ip_match() {
    	session_start();
    }

    This is not the only way to do it. I also happen to have a define of define( 'MYSITE_DEV', true ); in my wp-config.php file, so I could have checked if that was true:

    if ( defined( 'MYSITE_DEV' ) && MYSITE_DEV ) { ... }

    Now, you'll notice I'm using sessions, even after Adam and I determined this could be bad for some people. It can. And in my case, in this specific situation, it's not dangerous. It's a quick and dirty way to tell Varnish not to cache (because PHP sessions indicate a unique experience is needed).

    The downside is that not caching means there's more load on my server for the non-logged in user who is legit supposed to be visiting the site. Since this is a development site, I'm okay with that. I would never run this in production on a live site.