Localized Javascript countdown

These days many websites use a simple Javascript countdown clock to help promote the launch of a new service. Because many web applications are international, it’s important to ensure that your countdown is localized for all visitors.

Let me give you an example. Say you’re in Portland, OR and you’re launching a new web app on April 27, 2011 at 5:00 p.m. PST. Not only are you launching a new web service, but you’re also coordinating an iOS and an Android app launch. So it’s important to be precise. You want to ensure your Hong Kong visitors don’t expect your product to be launching at 5:00 p.m. HKT. Instead, you want them to know that your product is actually launching at April 28, 2011 at 8 a.m. local time.

One good way to do this is using the Date.UTC Javascript method.

// Get the current local time
var now = new Date();
 
// Get the localized end date for your countdown
var end = new Date(Date.UTC(
    2037, // Year
    3-1,  // Month (0 is January, so 3 minus 1 is 2, which is March)
    31,   // Day
    19,   // Hour
    0,    // Minutes
    0,    // Seconds
    0     // Milliseconds
));
 
// Toggle countdown
if (now < end) {
    // Show countdown
    alert(now + " < " + end);
} else {
    // Yay! We launched!
    alert('Launch day!');
}

If you run the script above, you’ll notice that while now is always increasing, end should stay constant. The only reason the value of end should change is if you change your computer’s local timezone.

You can now take these variables and pass them into a nice Javascript countdown script. I’ve used Keith Wood’s jquery.countdown.js jQuery plugin in the past, but I leave that decision up to you.

Multiple Base Templates in Django

I’ve been working on a pretty big project at work that’s heavily oriented towards mobile devices. We have a Django site running with a lot of custom ecommerce code, but wanted to serve up an optimized experience for mobile devices.

There are a lot of ways to do this, but ultimately we wanted to serve up a different base template for visitors on small-screen mobile devices. We wrote a piece custom view middleware that sets request['is_mobile'] = True. Unfortunately, Django currently requires any {% extends %} template tag to be the first tag in your template. This means you can’t do something like this:

{% if request.is_mobile %}
    {% extends 'mobile_base.html' %}
{% else %}
    {% extends 'default_base.html' %}
{% endif %}

After racking my brain all day my coworker suggested trying the yesno template filter. It worked like a charm!

{% extends request.is_mobile|yesno:"mobile_base.html,default_base.html" %}

So in our view templates we now do {% extends 'base.html' %} which then loads the proper base template depending on the status of is_mobile.

Detecting mobile devices with Javascript

Today at work we were struggling with a way to detect the iPad and similar devices without relying on the browser user agent string.

We ended up checking the value of window.onorientationchange like so:

function is_mobile_device() {
    if (typeof window.onorientationchange != "undefined") {
        return true;
    }
    else {
        return false;
    }
}

This function will return false in all current desktop browsers because typeof window.onorientationchange is “undefined.” On the iPad and other mobile devices it will return a type of “object.”

What other ways are you using to detect mobile devices?

JSON-RPC 2.0 Implementation

Here’s another quick braindump post. A custom JSON-RPC library I wrote for a project at my last job. It should follow the JSON-RPC 2.0 Spec pretty closely. It does require the jQuery library as well.

You can pretty much ignore the MIKU references. Basically it’s just a way of namespacing objects to make them globally available. Try reading up on the YUI library for more information.

As always, feel free to post with any questions.

  1. var MIKU;
  2.  
  3. /*
  4.  * Method to allow namespacing of new objects within MIKU;
  5.  *
  6.  * Example:
  7.  * MIKU.namespace('objectname');
  8.  * MIKU.objectname = function() { return {'...'} }
  9.  *
  10.  */
  11. MIKU = function() {
  12.   return {
  13.     namespace: function(name) {
  14.       try {
  15.         if (MIKU[name]) {
  16.           throw 'Namespace exists.';
  17.         }
  18.         return {}
  19.       }
  20.       catch (e) {
  21.         this.throwError(e);
  22.       }
  23.     }
  24.   }
  25. }();
  26.  
  27. MIKU.namespace('throwError');
  28.  
  29. /*
  30.  * Method to catch MIKU errors
  31.  */
  32. MIKU.throwError = function(e) {
  33.   if (console) {
  34.     console.error((e.message || 'MikuError:'), e);
  35.   }
  36. };
  37.  
  38. /*
  39.  * Init the JsonRpc namespace into the MIKU object.
  40.  */
  41. MIKU.namespace('JsonRpc');
  42.  
  43. /*
  44.  * Miku Json-RPC Implementation
  45.  */
  46.  
  47. MIKU.JsonRpc = function() {
  48.   var _url = 'json-rpc/call';
  49.   var _timeout = false;
  50.   var _requests = [];
  51.   var _responses = [];
  52.   var _callbacks = {};
  53.   var _requestId = 0;
  54.   var _failures = 0;
  55.  
  56.   function _send() {
  57.     _request();
  58.     _t = false;
  59.     _requests = [];
  60.   }
  61.  
  62.   function _request() {
  63.     try {
  64.       if (!_url) {
  65.         throw('undefined post url.');
  66.       }
  67.       $.ajax({
  68.         type: 'POST',
  69.         url: _url,
  70.         data: ({
  71.           request: JSON.stringify(_requests)
  72.         }),
  73.         dataFilter: function(data, type) {
  74.           //check for php errors
  75.           try {
  76.             return JSON.parse(data);
  77.           }
  78.           catch (e) {
  79.             var phpError = /^.*?(Error|Warning|Notice).*?\:\s*(.*?) in .*?(\/[0-9A-Za-z\/\.\-\_\ ]+).* on line .*?([0-9]+).*$/i;
  80.             var lines = data.split(/\n/g);
  81.             $.each(lines, function() {
  82.               var error = this.match(phpError);
  83.               if (error) {
  84.                 MIKU.throwError({
  85.                   message: error[1] + ': ' + error[2],
  86.                   file: error[3],
  87.                   line: error[4]
  88.                 });
  89.               }
  90.             });
  91.           }
  92.         },
  93.         success: function(data) {
  94.           _success(data);
  95.         },
  96.         error: function(XMLHttpRequest, textStatus, errorThrown) {
  97.           // retry after connection failures
  98.           if (XMLHttpRequest.status != '200') {
  99.             _failures ++;
  100.             if (_failures < 3) {
  101.               _request();
  102.               return;
  103.             }
  104.           }
  105.  
  106.           // give up and throw errors...
  107.           MIKU.throwError([XMLHttpRequest, textStatus, errorThrown]);
  108.         }
  109.       });
  110.     }
  111.     catch(e) {
  112.       MIKU.throwError(e);
  113.     }
  114.   }
  115.  
  116.   function _success(response) {
  117.     $.each(jQuery.makeArray(response), function() {
  118.       try {
  119.         if (this.error) {
  120.           // check for error
  121.           throw(this.error);
  122.         }
  123.         else if(this.result) {
  124.           // trigger callback
  125.           var callback = _callbacks[this.id];
  126.           callback(this.result);
  127.         }
  128.       }
  129.       catch(e) {
  130.         MIKU.throwError(e);
  131.       }
  132.     });
  133.  
  134.     // reset callbacks
  135.     _callbacks = {};
  136.   }
  137.  
  138.   function _genId() {
  139.     return ++_requestId;
  140.   }
  141.  
  142.   return {
  143.     version: '2.0',
  144.     delay: 10,
  145.     setUrl: function(url) {
  146.       _url = url;
  147.     },
  148.     call: function(args) {
  149.       var id = _genId();
  150.  
  151.       var request = {
  152.         jsonrpc: this.version,
  153.         method: args.method,
  154.         params: args.params,
  155.         id: id
  156.       }
  157.  
  158.       _requests.push(request);
  159.       _callbacks[id] = args.onSuccess;
  160.  
  161.       if (_timeout) {
  162.         clearTimeout(_timeout);
  163.       }
  164.       _timeout = setTimeout(_send, this.delay);
  165.  
  166.       return request;
  167.     }
  168.   }
  169. }();
  170.  
  171. /*
  172.  * Testing Below
  173.  */
  174.  
  175. $(document).ready(function() {
  176.   var rpc = MIKU.JsonRpc;
  177.  
  178.   rpc.call({
  179.     method: 'System.getTitle',
  180.     params: [
  181.       'id_13'
  182.     ],
  183.     onSuccess: function(result) {
  184.       console.debug(result);
  185.     }
  186.   });
  187. });

Writing a custom WordPress multi-instance widget

While working on a soon-to-be finished WordPress theme for a client, I ran into a situation where it would be useful to create a custom widget that they could use to organize content on their site. Basically, they wanted to be able to select a random post or page and display some associated meta data. Essentially a custom image and content teaser.

They were going to be doing this several times throughout the site, but in slightly different configurations. A post here, a page there. It seemed silly to hard-code these features. Using a widget would allow them to swap them out for a Twitter stream, or an RSS news feed in the future.

Making multi-instance widgets in WordPress 2.8+ couldn’t be easier. Here is a good example to start with from the WordPress codex.

  1. <?php
  2. class My_Widget extends WP_Widget {
  3. 	function My_Widget() {
  4. 		// widget actual processes
  5. 	}
  6.  
  7. 	function widget($args, $instance) {
  8. 		// outputs the content of the widget
  9. 	}
  10.  
  11. 	function update($new_instance, $old_instance) {
  12. 		// processes widget options to be saved
  13. 	}
  14.  
  15. 	function form($instance) {
  16. 		// outputs the options form on admin
  17. 	}
  18. }
  19. register_widget('My_Widget');
  20. ?>

And here is my finished widget (evolved from the above example).

  1. <?php
  2. /*
  3.  * Custom mini-post widget
  4.  */
  5. class FGX_MiniPost_Widget extends WP_Widget {
  6. 	function FGX_MiniPost_Widget() {
  7. 		// widget actual processes
  8. 		parent::WP_Widget(false, $name = 'Floragenex MiniPost', array(
  9. 			'description' => 'Display a teaser for a post or a page.'
  10. 		));
  11. 	}
  12.  
  13. 	function widget($args, $instance) {
  14. 		// outputs the content of the widget
  15. 			global $post;
  16.  
  17. 			extract( $args );
  18. 			$type = $instance['type'];
  19. 			$include = (!empty($instance['include']) ? explode(',', $instance['include']) : '');
  20. 			$category = (is_numeric($instance['category']) ? (int)$instance['category'] : '');
  21.  
  22.                         // Set up query for posts with the provided filters
  23. 			query_posts(array(
  24. 				'post_type' => $type,
  25. 				'post__in' => $include,
  26. 				'post__not_in' => array($post->ID),
  27. 				'cat' => $category,
  28. 				'post_status' => 'publish',
  29. 				'meta_key' => 'teaser_value',
  30. 				'meta_value' => '',
  31. 				'meta_compare' => '!=',
  32. 				'orderby' => 'rand',
  33. 				'posts_per_page' => '1'
  34. 			));
  35.  
  36. 			echo $before_widget;
  37.  
  38.                         // Output widget, if a post exists that matches our query
  39. 			if ( have_posts() ) :
  40. 				while ( have_posts() ) : the_post();
  41. 					$post_meta = get_post_custom($post->ID);
  42. 					echo (!empty($post_meta['image_value'][0]) ? '<a href="' . get_page_link() . '">' .
  43. 							 '<img alt="image" src="' . get_bloginfo('template_url') . '/scripts/timthumb.php?src=' . htmlentities($post_meta['image_value'][0]) . '&h=62&w=180&zc=1" />' .
  44. 							 '</a>' : '') .
  45. 							 '<h3><a href="' . get_page_link() . '">' . get_the_title() . '</a></h3>' .
  46. 							 '<p>' . htmlentities($post_meta['teaser_value'][0]) . '</p>' .
  47. 							 '<p><a class="learn-more" href="' . get_page_link() . '">learn more</a></p>';
  48. 				endwhile;
  49. 			else:
  50. 				echo '<p>No posts found.</p>';
  51. 			endif;
  52.  
  53.                         // Very important to reset the query here.
  54. 			wp_reset_query();
  55.  
  56. 			echo $after_widget;
  57. 	}
  58.  
  59. 	function update($new_instance, $old_instance) {
  60. 		// processes widget options to be saved
  61. 		return $new_instance;
  62. 	}
  63.  
  64. 	function form($instance) {
  65. 		// outputs the options form on admin
  66. 		$type = esc_attr($instance['type']);
  67. 		$include = esc_attr($instance['include']);
  68. 		$category = esc_attr($instance['category']);
  69.  
  70.                 // Get the existing categories and build a simple select dropdown for the user.
  71. 		$categories = get_categories();
  72.  
  73. 		$cat_options = array();
  74. 		$cat_options[] = '<option value="BLANK">Select one...</option>';
  75.  
  76. 		foreach ($categories as $cat) {
  77. 			$selected = $category === $cat->cat_ID ? ' selected="selected"' : '';
  78. 			$cat_options[] = '<option value="' . $cat->cat_ID .'"' . $selected . '>' . $cat->name . '</option>';
  79. 		}
  80.  
  81. 		?>
  82. 			<p>
  83. 				<label for="<?php echo $this->get_field_id('type'); ?>">
  84. 					<?php _e('Content type:'); ?>
  85. 				</label>
  86. 				<select id="<?php echo $this->get_field_id('type'); ?>" class="widefat" name="<?php echo $this->get_field_name('type'); ?>">
  87. 					<option value="post"<?php echo ($type === 'post' ? ' selected="selected"' : ''); ?>>Post</option>
  88. 					<option value="page"<?php echo ($type === 'page' ? ' selected="selected"' : ''); ?>>Page</option>
  89. 				</select>
  90. 			</p>
  91. 			<p>
  92. 				<label for="<?php echo $this->get_field_id('include'); ?>">
  93. 					<?php _e('Include post IDs (optional):'); ?>
  94. 				</label>
  95. 				<input id="<?php echo $this->get_field_id('include'); ?>" class="widefat" type="text" name="<?php echo $this->get_field_name('include'); ?>" value="<?php echo $include; ?>" />
  96. 			</p>
  97. 			<p>
  98. 				<label for="<?php echo $this->get_field_id('category'); ?>">
  99. 					<?php _e('Include category (optional):'); ?>
  100. 				</label>
  101. 				<select id="<?php echo $this->get_field_id('category'); ?>" class="widefat" name="<?php echo $this->get_field_name('category'); ?>">
  102. 					<?php echo implode('', $cat_options); ?>
  103. 				</select>
  104. 			</p>
  105. 		<?php
  106. 	}
  107. }
  108.  
  109. // register widget
  110. register_widget('FGX_MiniPost_Widget');
  111. ?>

Just paste this code in your theme’s functions.php file and the widget should appear under your available widgets.

This was a quick braindump post, so please feel free to post with any specific questions.

Create and list new tickets through the Assembla API

A while back I created a PHP library for interacting with the Assembla Ticket API. I thought it might be useful to someone, so here it is.

You can use this library to create and list tickets that exist for one of your Assembla spaces. It’s useful for integrating bug reporting and other feedback features into an application that you may be developing. This code is very much a work in progress, so please let me know if you encounter any bugs or have any new feature requests.

You can view and download the code on GitHub:

github-logo