Archives

Month: June 2018

  • How to Leave Your Job


    So you have a job offer from another company and are ready to leave your existing role. You want to leave without burning the social capital you’ve built up over time. What do you do?

    First, relax. Having that first conversation with your manager is usually nerve wracking, but everything will be okay. Switching jobs is expected, and chances are that your manager has been on both sides of the table before. Follow these guidelines below and you’ll be on your way to your new gig in no time.

    http://discoverpraxis.com/how-to-leave-your-job/

  • Celebrating 10 Years


    I started this blog ten years ago today. I was in a room at the Doubletree in Tarrytown, NY. It was a Sunday and it was the weekend in-between my first FEE seminars. I ordered a pizza and some ziti from Capri Pizza and decided to tear down my old HTML site and give blogging a try. I figured out how to install WordPress 2.5.1 and went to town. Here is what the site looked like:

    original blog

    Here is the post that started it all.

    Since then, I went on to work at FEE and move to NY, passing that Doubletree regularly. I blogged for a year straight. I’ve lived in 5 different cities and posted from every single one. I’ve written 763 posts on this blog since then and I average about 20,000 unique visitors a month according to my server logs.

    The blogging has paid off. Here are things that have come from my blogging and showing my work:

    • Photography gigs
    • Selling my photos
    • Photos published in dozens of newspapers, magazines, and websites because they were licensed as Creative Commons
    • The Light Graffiti Guide Sean and I wrote has been used by teachers all across the world.
    • Photos being shown at the 2008 Nuit des musées at Le CompaArchived Link, Conservatoire de l’agriculture in Chartres, France.
    • More contract gigs than I can count
    • My second full-time job (eResources)
    • My third full-time job (Praxis). Here is the post that put Isaac over the edge.
    • Weekly emails from people who found my tutorials helpful.
    • Meeting authors whose books I wrote about
    • Invitations to meetups and discussion groups
    • People using my code for their own projects
    • I’ve served up appliance repair manuals to hundreds of thousands of people. I let my Dad upload the ones he had back in 2008 to so that people could find them for free. Just doing our part to keep the Right to Repair/Fix It movement strong.

    Also, not only is today my 10 year blogging anniversary, it is also my 5 year wedding anniversary! Amanda and I had just started dating when I put this blog up, so she’s been here since the beginning. Little did I know that we’d get married exactly five years later.

    It’s been a fun ten years and I’m not done yet. cagrimmett.com is here to stay. Check out my archives and stay tuned for more.

  • Automated Accountability Check-ins


    The Need

    During the Praxis bootcamp, participants are expected to make every single day a non-zero day. Most participants ask us to hold them accountable, so they email one of our staff each day and that person emails them the next day if they miss a check-in. This takes an enormous number of emails, staff overhead to keep track of, and is difficult to search. Isaac asked me, “What can we do to automate this?”

    It needs this functionality:

    1. The ability to submit what you’ve worked on that day.
    2. The ability to see what you’ve submitted.
    3. The ability for staff to see what you’ve submitted.
    4. The ability to send an email to people who didn’t submit a check-in the previous day.
    5. The ability for logging who missed a check-in.

    The Restrictions

    There are existing systems that do this sort of thing, so why make our own? During the bootcamp, our customers use what we call The Portal, which is a curriculum platform I built on top of WordPress with the help of Restrict Content Pro and wpcomplete. I didn’t want to add another destination into the mix. I wanted a reason for customers to go to the Portal every day and keep their work moving forward. Plus, they already have logins there. No need to add yet another login for them to keep track of.

    The Solution Sketch

    1. USP Pro for front-end post submission
    2. Custom page template to display the content box but hide the tedious stuff: User ID, today’s date, user email
    3. New WordPress user meta field for keeping track of accountability opt-ins
    4. Custom script to check if opt-ins have posted within the last 28 hours and fire off a webhook to Zapier to send the email and log missed check-ins to a spreadsheet
    5. Zapier time trigger with a GET hook to run the script at a specific time to kick off the accountability check
    6. Custom template to display all opted-in participants and their check-ins to make it easy for staff to see everything in one place
    7. Hide the check-ins from search results

    The Solution Details

    Front-end post submission and showing posts

    USP Pro handles all sorts of complexity when it comes to submitting posts from the front-end of a WordPress site: The form, the custom post type, assigning the current logged-in user to be the author, and displaying success and error messages. They also have hooks and filters to make using the posts in templates easier.

    I used a custom template to hide some of the things I’d need to make the whole process work: User ID to assign as the post author, name and today’s date for the title of the post.

    After the form, we also want to show all the posts someone has already submitted. I do that with a WP_Query_  Note: I added a Delete button here in case some makes an accidental post. If you want someone to be able to delete their check-in post, they need to have the role Author.

    Here is the code I used for the template and comments about what it does:

    // Check if user is logged in. Display the check-in form if so, login form if not. if ( is_user_logged_in() ) { //Get the logged in user, today's date, and the PHP session ID 	$current_user = wp_get_current_user(); 	$today = date("F j, Y"); 	$ses_id = session_id(); ?>    class="usp-pro-form">  	 	 id="usp-pro" class="usp-pro usp-form-4102"> 	 id="usp-form-4102" class="usp-form" method="post" enctype="multipart/form-data" action="" data-parsley-validate="" data-persist="garlic">  		 class="usp-fieldset usp-fieldset-default"> 			 for="usp-content" class="usp-label usp-label-content">

    What did you do to make today a non-zero day?

    name="usp-content" id="usp-content" rows="5" cols="30" maxlength="999999" data-required="true" required="required" placeholder="Today I..." class="usp-input usp-textarea usp-input-content"> type="hidden" name="usp-content-required" value="1"> class="usp-hidden"> // Get first and last name, today's date, set that as the title. echo '. $current_user->user_firstname . ' ' . $current_user->user_lastname . ' ' . $today . '" type="hidden" />'; // Get user ID for author echo '. $current_user->ID . '">'; // Get PHP session ID. Plugin thing. echo '. $ses_id . '">'; ?> type="text" name="usp-verify" id="verify" value="" style="display:none;" class="exclude"> type="hidden" name="usp-form-id" value="4102">
    echo do_shortcode('[usp_form id="submit"]'); ?>
    class="submitted-checkins">

    Your submitted check-ins:

    // Show posts from this user ID with the type usp_post $post_query = new WP_Query( array( 'post_type' => 'usp_post', 'author' => $current_user->ID, 'orderby' => 'date', 'order' => 'DESC', ) ); if ( $post_query->have_posts() ) : while( $post_query->have_posts() ) : $post_query->the_post(); ?> post_class(); ?>>

    the_time('F j, Y \a\t g:i a') ?>

    the_content(); ?> href=" echo get_delete_post_link(); ?>" style="font-size: .8em;">Delete this check-in
    endwhile; else: echo "

    You have no check-in posts. Submit one above!

    "
    ; endif; ?>
    } else { ?> style="padding:50px;"> echo do_shortcode('[login_form]'); ?>
} ?>

The page (with a little bit of CSS added, which I’ll leave as an exercise for the reader) looks like this: Automated Accountability Check-in Home Page

Adding custom user meta field for accountability opt-in

This goes in functions.php:

add_action( 'show_user_profile', 'accountability_opt_in' ); add_action( 'edit_user_profile', 'accountability_opt_in' );  function accountability_opt_in( $user ) { ?>  	

Daily Accountability

class="form-table"> for="checkin">Opt in to daily check-in accountability? type="checkbox" name="checkin" id="checkin" if (get_the_author_meta( 'checkin', $user->ID) == 'True' ) { ?>checked="checked" }?> value="True" /> /> class="description">Yes, opt me in. } add_action( 'personal_options_update', 'save_accountability_opt_in' ); add_action( 'edit_user_profile_update', 'save_accountability_opt_in' ); function save_accountability_opt_in( $user_id ) { if ( !current_user_can( 'edit_user', $user_id ) ) return false; update_usermeta( $user_id, 'checkin', $_POST['checkin'] ); }

So, at the bottom of the user profile, it now shows this: Automated Accountability Check-in User Meta Field

Checking to see if opted in user has submitted a post in the last 28 hours

Outline:

  1. I trigger this by hitting the URL once a night. So no one else can trigger it, hit it with a long random key as a query string, which I first get and check.
  2. Load WordPress so I can use pre-built functions.
  3. Get list of opted-in users.
  4. Check to see if they have posts. If so, get the most recent one. If not, send a webhook to Zapier.
  5. If they have posts, check the date of the most recent one. If it is within the last 28 hours, do nothing. If not, send a webhook to Zapier. 28 hours gives a little wiggle room for early/late posts.

I like to leave the echos on for debugging if something goes wrong. Like, for example, if you use the_date instead of get_the_date and occasionally get NULL instead of a date and can’t figure out why. (Hint: the_date only fires once in a loop. Thanks for the help debugging, Eric Davis!)

Also, I know I’m using two different types for formatting for if statements. It helps me keep them separate.

 $key = $_GET['key'];  if ($key == 'RANDOM_KEY' ) {  	require_once($_SERVER['DOCUMENT_ROOT'] . '/wp-load.php');  	function get_user_by_meta_data( $meta_key, $meta_value ) {  		// Query for users based on the meta data 		$user_query = new WP_User_Query( 			array( 				'meta_key'	  =>	$meta_key, 				'meta_value'	=>	$meta_value 			) 		);  		// Get the results from the query, returning the users 		$users = $user_query->get_results();  		return $users; 	}  	$opted_in_users = get_user_by_meta_data( 'checkin', 'True');  	foreach ($opted_in_users as $user) { 		echo $user->ID . ' ' . $user->user_email . '
'
; $post_query = new WP_Query( array( 'post_type' => 'usp_post', 'author' => $user->ID, 'post_status' => 'publish', 'orderby' => 'date', 'order' => 'DESC', 'showposts' => '1', ) ); // The Loop if ( $post_query->have_posts() ) : while ( $post_query->have_posts() ) : $post_query->the_post(); $date = get_the_date('U'); echo 'last post date: ' . $date . '
'
; //echo 'now: ' . time() . '
';
if( $date > (time() - 100800)) { echo 'Within 28 hours

'
; continue; } else { echo 'Not within 28 hours

'
; // Initialize curl $curl = curl_init(); $data = array( 'user_id' => $user->ID, 'email' => $user->user_email, 'first_name' => $user->first_name, 'last_name' => $user->last_name, 'last_post' => $date, ); $jsonEncodedData = json_encode($data); $opts = array( CURLOPT_URL => 'https://hooks.zapier.com/hooks/catch/1503890/aqztv5/', CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_POST => 1, CURLOPT_POSTFIELDS => $jsonEncodedData, CURLOPT_HTTPHEADER => array('Content-Type: application/json','Content-Length: ' . strlen($jsonEncodedData)) ); // Set curl options curl_setopt_array($curl, $opts); // Get the results $result = curl_exec($curl); // Close resource curl_close($curl); echo $result; } endwhile; else : echo "No posts

"
; // Initialize curl $curl = curl_init(); $data = array( 'user_id' => $user->ID, 'email' => $user->user_email, 'first_name' => $user->first_name, 'last_name' => $user->last_name, ); $jsonEncodedData = json_encode($data); $opts = array( CURLOPT_URL => 'https://hooks.zapier.com/hooks/catch/1503890/aqztv5/', CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_POST => 1, CURLOPT_POSTFIELDS => $jsonEncodedData, CURLOPT_HTTPHEADER => array('Content-Type: application/json','Content-Length: ' . strlen($jsonEncodedData)) ); // Set curl options curl_setopt_array($curl, $opts); // Get the results $result = curl_exec($curl); // Close resource curl_close($curl); echo $result; endif; } } else { exit("You are not authorized. Go away."); } ?>

Template for viewing posts by user

Viewing posts in the wp-admin area is less than ideal. They are grouped by date instead of author and the full contents doesn’t show unless you click. No want to view updates at a glance. So I made a template for that.

// First, check if user is an admin. if ( !is_user_logged_in() || !current_user_can('administrator') ) { 	   wp_redirect( site_url() );  exit; } else { // User IDs are passed via query string. If no string, show all names 	$user = $_GET['id']; if ( $user == null ) {?> 	

Participants who have submitted check-ins:

$args1 = array( //'role' => 'author', 'orderby' => 'first_name', 'order' => 'ASC' ); $authors = get_users($args1); foreach ($authors as $user) { $user_post_count = count_user_posts( $user->ID , 'usp_post' ); if ( $user_post_count > 0) { echo '. $user->id . '">' . $user->first_name . ' ' . $user->last_name . '
'
; } } } else { $user = get_user_by('id', $user); $post_query = new WP_Query( array( 'post_type' => 'usp_post', 'author' => $user->ID, 'orderby' => 'date', 'order' => 'DESC', ) ); ?> />

href="/check-ins/"> Back to all participants with check-ins

echo $user->first_name . ' ' . $user->last_name .'\'s check-in posts';?>

if ( $post_query->have_posts() ) : while( $post_query->have_posts() ) : $post_query->the_post(); ?> post_class(); ?>>

the_time('F j, Y \a\t g:i a') ?>

the_content(); ?>
endwhile; else: echo "

You have no check-in posts. Submit one above!

"
; endif; } }

Here is what the template outputs:

check-in post user list

user posts

The best way to do this is to hook in to when the post type is initiated and toggle the ‘exclude_from_search option so ‘true’. I added this to functions.php:

add_action( 'init', 'usp_post_hide_search', 99 );  function usp_post_hide_search() { 	global $wp_post_types;  	if ( post_type_exists( 'usp_post' ) ) {  		// exclude from search results 		$wp_post_types['usp_post']->exclude_from_search = true; 	} } 

Using Zapier

I opted to use Zapier for two things:

  1. Triggering the nightly post checking script via GET. wp_cron isn’t super reliable if you need something to run at a given time, and since I’m hosting on WPengine, I don’t have access to the server cron. We already use Zapier for a ton of stuff, so I fire off a GET to my script with the query string random key.
  2. Sending the email and logging emails sent. I could have done this all in the script, but I wanted staff to be able to easily change the email and possibly add more post-email items like sending a message to Slack and Salesforce, or sending a text message to the user.

Going Forward

Here are a few ways I want to improve this when I have time:

  1. Extend the template that staff uses to see check-in posts to include filter criteria.
  2. Let users opt-in and opt-out of automated accountability instead of admins opting them in or out.
  3. I might want to expire or hide posts
  4. USP by default doesn’t preserve line breaks. I’ll need to update that.