Tuesday, December 15, 2009

Authenticate your Authlogic users in Rails Metal

Update 2010-03-17: Don't fail in test mode if we can't find a user

I haven't been able to find any information on authenticating users in Rails Metal controllers. So I took a look at what was available in the session and worked with that. I'm not an expert on Authlogic (I did write an Authlogic Add-On a while ago) but this seems like a good solution. Let me know if you know of any problems with this approach.

I added a method to UserSession (or add it to whatever class you use for your sessions) that takes the Metal env and checks the authentication parameters and returns the user record. I don't bother with updating the user's last_request_at attributes or anything. I had to bypass the Authlogic session entirely because Authlogic needs to be activated with a controller before you can use the session stuff. I considered dummying the controller by including authlogic/test_case but I don't know enough about what the dummy controller does regarding authentication to go this route.

And the code...

Here's the Metal "controller":

A couple neat things in the Metal controller are the use of ActionController::Request.new(env) so we have access to request.remote_ip and other helper methods this class provides.

The part where I validate the user is:

Tuesday, December 8, 2009

Silence the Rails logger as well as the SQL logs

It's one thing to turn off logging in Rails, it's another to prevent SQL statements from appearing in your logs.

I've written a Rake helper to silence both. You can adapt it for other uses, but I think it's mostly relevant in Rake tasks.

# lib/rake_helpers.rb
module Rake
  module Helpers
    # Silence the Rails logger as well as the SQL logger
    # Call with a block e.g
    # silent { my block code }
    def silent
      ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do
        alias_method :original_log_info, :log_info
        def silent_log_info(*args); end
        alias_method :log_info, :silent_log_info
      end
      @@old_logger = ActiveRecord::Base.logger
      ActiveRecord::Base.logger = Class.new { def method_missing(*args); end; }.new
      yield
      ActiveRecord::Base.logger = @@old_logger
      ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do
        alias_method :log_info, :original_log_info
      end 
    end
  end
end
And sample usage:
# lib/tasks/db.rake
require 'rake_helpers'
include Rake::Helpers

namespace :db do
  namespace :update do

    desc "Update counter caches"
    task :counters => :environment do
      silent do
        ActiveRecord::Base.transaction do
          [Artist, Album].each do |model| 
            model.find_in_batches(:batch_size => 5000) do |batch|
              batch.each { |record| record.update_counts }
            end
          end
        end
      end
    end
  end
end

Friday, December 4, 2009

Rails Metal real-world example with RSpec integration test

There's not that much information out there about Rails Metal, and even less about how to test Metal classes using RSpec. If I had time I would create a new RSpec ExampleGroup with a lot of the goodness of the ControllerExampleGroup, but alas I don't.

So integration tests will have to do. I had problems with using response.should be_success. The docs say it exists, but it doesn't for me. YMMV.

Anyways, here is my Metal class along with the integration test:

Thursday, December 3, 2009

Bash script to easily check and switch between Rails environments. Usage: renv [p|d|t|s]

When I'm on our deployment server I'm constantly checking the RAILS_ENV environment variable to make sure that I don't accidentally do something on the production database when I meant to do it on the staging database. (Incidentally I’ve added some stuff to the Rakefile to guard against such a thing: Rakefile: Prevent destructive rake tasks in the production environment.). So I created this handy Bash function to check and switch between your Rails environments easily.

Sample usage:

$ renv
RAILS_ENV is unset
$ renv p
RAILS_ENV set to production
$ renv d
RAILS_ENV set to development
$ renv s
RAILS_ENV set to staging
$ renv t
RAILS_ENV set to test
$ renv
RAILS_ENV is test

And the code:

# Put this in your ~/.profile
function renv() {
  rails_env_p="production" 
  rails_env_d="development" 
  rails_env_s="staging" 
  rails_env_t="test" 

  if [ $1 ];then
    eval 'rails_env=${rails_env_'"${1}"'}' 
    export RAILS_ENV=$rails_env
    echo "RAILS_ENV set to $RAILS_ENV" 
  elif [ $RAILS_ENV ];then
    echo "RAILS_ENV is $RAILS_ENV" 
  else
    echo "RAILS_ENV is unset" 
  fi
}

Rakefile: Prevent destructive rake tasks in the production environment.

We are using the same server for our staging and production deploys. The problem is that the default RAILS_ENV is set to production so when I go to the staging app and start messing about with rake db:seed (or worse, rake db:reset) I am almost always a hair's breadth away from imminent death.

(Here's a handy bash script I wrote to easily switch between your Rails environments.)

So I decided to do something about it and protect destructive or database altering rake tasks from being run in the production environment. I really like this approach and I think I'll use it in future projects regardless of the setup. The fact is you only do a rake db:reset in production once, during the cold deploy of your app. It allows rake db:migrate and its variants, but not rake db:migrate:reset. Look at the code for the full list of restricted rake tasks. The tasks in PROTECT_SOME are matched explicitly, PROTECT_ALL restricts all variants of that task.

This shows the warning message and confirmation that appear. If you know what you are doing, or you want to automate things with Capistrano and need to run one of the protected tasks just pass FORCE=true to the task.

[deployer@rails1 ~/rails_apps/rails1.creagency.com.au/current] echo $RAILS_ENV
production
[deployer@rails1 ~/rails_apps/rails1.creagency.com.au/current] rake db:reset
(in /www/rails_apps/rails1.creagency.com.au/releases/20091204033227)
****************************************************************************
* WARNING! You are in the PRODUCTION environment and are running a Rake task 
* that will DESTROY your PRODUCTION database!
* 
* If you know what you are doing you can run this task with FORCE=true to
* prevent this message appearing.
****************************************************************************

Are you sure? (Yes|No) [No]

Quitting.
[deployer@rails1 ~/rails_apps/rails1.creagency.com.au/current] 
And here is the code. Just add it to the bottom of your application's Rakefile:

Solved: Capistro deploy fails with 'Permission denied (publickey).' but you know you have your permissions setup correctly

I spent a couple hours scratching me head one day when I couldn't deploy to the production server, but I could a little while ago.

The Capistrano deployment recipe had been changed to forward the ssh agent (which means that my local ssh key is the one that will be used by the deployment server to checkout the updated code from Git):

  ssh_options[:forward_agent] = true
Capistrano was complaining:
    servers: ["10.5.23.203"]
    [10.5.23.203] executing command
 ** [10.5.23.203 :: out] Permission denied (publickey).
 ** [10.5.23.203 :: out] fatal: The remote end hung up unexpectedly
The solution was just to add my SSH key to my local SSH agent:
$ ssh-add
Simple enough. Turns out you have to do that every time you restart your machine tho, which is a bit annoying. Any way to avoid that?

Cheers, Karl

Wednesday, December 2, 2009

Git: how to keep the master branch tidy, even if you've been working on it

In order to keep the master branch clean we want to avoid merges and "noisy" commits.

As you probably know, the best thing to do is to develop in a branch and rebase against master often (never rebase a remote branch). This ensures that your changes are always the latest changes applied to the index. And because they're applied on top of the index, when you merge them into the master branch and push, it's just a fast-forward update and you will never have to do a merge.

But what happens if you've been working on the master branch for a while? Is it too late? I just encountered this myself, and here's how you can keep the master merge-free in this situation.

On master

git fetch # have there been changes to master since you last fetched/pulled?  if not, you can just commit and push, otherwise...

git commit -a -m "awesome message here" # commit your changes
git branch wip           # create a new branch with your changes
git reset --hard HEAD~1  # reset the master branch to before your commit(s)
git checkout wip
continue working...
git fetch origin master
git rebase origin/master # rebase your changes on top of master often
all done...
git rebase -i origin/master # preferably squash your commits
git checkout master
git pull                 # make sure master has the latest code
git merge wip            # apply your fast-forward changes to the index
git push                 # quick before someone else does!

Can this process be shortened? Let me know if you have ideas.

Here are some excellent articles explaining some Git best-practices. There is some excellent info here, even from Linus himself!

Let me know if you have good Git articles. I'm still learning about Git, but now that I'm getting my head around it, it's getting better and better!

Karl

Sunday, November 15, 2009

Sass sprite mixin: making sprites super easy to use!

If you're like me, you love Haml and Sass!. And if you're a serious web developer trying to squeeze every last bit of performance out of your web application (let's pretend :), then you are already using image sprites, right?

I won't go into the benefits of using sprites, but apart from the obvious, I kinda like how they keep things organized. I don't need to worry about adding another small image to my application because it's not going to be yet another resource to bog things down, it goes in the sprite and probably only takes 1K. So that's nice. But they're just not as easy to use as a simple image...until now!

With my Sass mixin you can use a sprite image with just two lines of Sass.

The mixin:

// Add styles to an element to show the image in *image* at the given *x_pos* and *y_pos*.
// If the optional *x_size* or *y_size* arguments are passed in, the width and/or height 
// is set to that value.
=sprite(!image, !x_pos, !y_pos, !x_size=0, !y_size=0)
  :background-image = url(!image)
  :background-position = !x_pos !y_pos
  @if !x_size > 0
    :width = !x_size
  @if !y_size > 0
    :height = !y_size  
I'd recommend wrapping the mixin for your sprite image, because you're going to use it a lot. For example:
=player_sprite(!x_pos, !y_pos, !x_size=0, !y_size=0)
  +sprite("/images/player-sprite.png", !x_pos, !y_pos, !x_size, !y_size)
  
// Now define some styles using the player sprite
.playlists-icon
  +player_sprite(-600px, 0, 18px, 18px)
.recently-played-icon
  +player_sprite(-700px, 0, 18px, 18px)
.playing-now-icon
  +player_sprite(-800px, 0, 18px, 18px)
This will generate the following CSS
.playlists-icon {
  background-image: url('/images/player-sprite.png');
  background-position: -600px 0;
  width: 18px;
  height: 18px; }

.recently-played-icon {
  background-image: url('/images/player-sprite.png');
  background-position: -700px 0;
  width: 18px;
  height: 18px; }

.playing-now-icon {
  background-image: url('/images/player-sprite.png');
  background-position: -800px 0;
  width: 18px;
  height: 18px; }  
And your Haml to display the divs with the images is as simple as:
.playlists-icon
.recently-played-icon
.playing-now-icon
Awesome! Now that's easy!

If you found this useful, let me know!

Thursday, June 4, 2009

jQuery plugin to escape CSS selectors

Sometimes in jQuery you need to find an element(s) using a selector that contains special characters, for instance #, or .. I'm quite baffled that jQuery doesn't include a CSS selector escape function. Not that you should use special characters in your class/id attributes, but hey, sometimes you don't have control over the content.

My escape function uses a couple of neat JavaScript techniques like closures (very, very useful once you get the hang of 'em!) and regular expressions. I use Simon Willison's Escaping regular expression characters in JavaScript Regex extension a lot. For more on closures check out Mark Wubben's Getting Funky With Scopes and Closures.

As an extreme example, say we have something as crazy as:

<div id='#;&,.+*~":!^$[]()=>|/'> Hello World! </div>
We can match that with this jQuery:
$('#' + jQuery.escapeSelector('#;&,.+*~":!^$[]()=>|/'))

Try it out:

Here's the code

/**
 * jQuery static function extensions.
 */
 
jQuery.extend(jQuery, {
  /**
   * Escape all special jQuery CSS selector characters in *selector*.
   * Useful when you have a class or id which contains special characters
   * which you need to include in a selector.
   */
  escapeSelector: (function() {
    var specials = [
      '#', '&', '~', '=', '>', 
      "'", ':', '"', '!', ';', ','
    ];
    var regexSpecials = [
      '.', '*', '+', '|', '[', ']', '(', ')', '/', '^', '$'
    ];
    var sRE = new RegExp(
      '(' + specials.join('|') + '|\\' + regexSpecials.join('|\\') + ')', 'g'
    );

    return function(selector) {
      return selector.replace(sRE, '\\$1');
    }
  })()
});

Sunday, May 24, 2009

How to setup an ad-blocking proxy in 10 minutes and block ads in all your favorite browsers!

For a long time now I've been content using Opera's built-in ad blocker, an example of which you can see below. This approach has worked for me for a while, but has some major shortcomings: I can't use expressions to filter multiple URLs from the same server, and obviously all my hard work blocking ads in Opera doesn't do anything for me in Firefox or Safari.

Opera's Ad Blocker

The best solution to these problems lies in setting up a proxy to filter requests by URL. I had read about doing something like this a couple years ago, but couldn't be bothered to figure out all the details of setting it up. It turns out the be remarkably easy, as I'll show you! With my setup files and instructions you can be browsing ad-free in 10 minutes. And trust me, it's worth it!


The benefits of the proxy approach

  • Block ads in every browser with a single configuration file.
  • Filter URLs using Regular Expressions (I've added some methods so you don't have to).
  • Configuration files are in JavaScript for easy scripting and customization.
  • Blocks HTML ads, image ads, flash ads, you name it.
  • Faster browsing because you don't wait for ads to load (and ads are usually given first priority in the loading sequence when you visit commercial websites, so your pages render faster).

You're going to need to be running a webserver that will serve a blank page for the blocked ads. Without this your browser will report a 404 error because it can't access the blocked pages, and your webpages will have 404 errors rendered where the ads used to be - not so pretty to look at. If you're on Mac, you've already got Apache installed and ready to go. If you're on Windows, you're going to have to set that up and then come back here when you're done.

[ Aside: I've been thinking that you don't really need to run a local webserver at all, if you have access to one on the web. That would allow any number of people on any network to benefit from a single filter file. ]

Getting started

Webpage with Ads
Webpage without Ads
First off, here's a little taste of how your new browsing experience will look. On the left is a regular webpage followed by the same page minus the ads. Ad free goodness!

The setup is simple.

  1. You have an Apache virtual host listening on a non-standard port, say 61111. The virtual host does only one thing: it rewrites all requests to point to an empty HTML file, which I have named response.html.
  2. You also have a proxy configuration file, or PAC, which you configure your browser to use. All requests from the browser are sent to this PAC file. The PAC file examines the request and if it looks like it's an advertisement it sends the request to our apache virtual host (which will render a blank page...so no ad will display). Otherwise the request is served by the browser in the normal fashion.

Setup Apache

You're going to need to modify the paths according to where you put these files, and the setup of your machine. These instructions are particular to Mac OS X, but the Windows setup is analagous.

On Mac, I think the easiest place to put our PAC file and the response.html file is in your Public/ directory which is under your home directory. Extract this archive (ad-blocker.zip) which contains four files: favicon.ico, proxy.conf, response.html as well as a copy of ad-blocker.conf (the Apache virtual host configuration file).

Configure the virtual host
You should have a directory under /etc/apache2/ called other/. You can define virtual hosts in their own conf files here and they will be read by Apache on startup.

In /etc/apache2/other/ create a file called ad-blocker.conf with the following contents (this file is also contained in ad-blocker.zip):

  ## Use Listen directives and not Port directive if server will handle
  ## requests from multiple ports.
  Listen 127.0.0.1:61111

  <VirtualHost *:61111>
  DocumentRoot "/Users/Karl/Public/ad-blocker"
  <directory "/Users/Karl/Public/ad-blocker">
    Order allow,deny
    Allow from all
    Options +Indexes
  </directory>
  ErrorLog /var/log/apache2/error_log_ads
  SetEnvIf Request_URI .* no-access-log
  CustomLog /dev/null common env=!no-access-log
  RewriteEngine on
  RewriteRule ^(.*) /Users/Karl/Public/ad-blocker/response.html
  </VirtualHost>
Now modify the DocumentRoot and <directory ...> with the full path to the ad-blocker directory you added to Public, and modify the RewriteRule directive to point to the response.html file.

Start Apache
Mac OSX: Enable Web Sharing
On Mac, just enable Web Sharing under System Preferences -> Sharing. Apache will start, and if you visit http://127.0.0.1:61111 you should see a blank page (with no errors or anything else). If you get an error like You don't have permissions to access / on this server you may permissions problems on your ad-blocker directory, you may have messed up your <directory ...> directive path, or you may need to enable File Sharing on the directory and give permissions to Everyone to Read.

Apache should be up and running and ready to serve ads.

Now the easy part.

Configure your browsers

We need to configure the browser to use our proxy configuration file (PAC file). For each browser, open your Preferences and configure the path to the proxy.conf file for your machine using my screenshots as a guide.


Firefox
Open Preferences->Advanced->Network. Then under Connection, click 'Settings...'
Select 'Automatic proxy configuration URL' and enter the path to the proxy.conf file

Opera
Open Preferences->Advanced->Network and click on 'Proxy Servers'

Select 'Use automatic proxy configuration' and enter the path to the proxy.conf file

Safari
Open Preferences->Advanced and click 'Proxies: Change Settings...'

This should bring up your OS X Network->Proxies settings. Select 'Configure Proxies: Using a PAC file' and enter the path to the proxy.conf file

Alternatively you can get there from OS X System Preferences->Network click on 'Advanced' then 'Proxies'

The PAC file

Here's the proxy configuration (PAC) file if you just want to have a look at how it's setup. I've modified the original file that I reference in the comments by adding two really useful JavaScript functions, domainMatch() and subdomainMatch(). These perform regular expression matching on the URL and can filter URLs by domain e.g. domainMatch('doubleclick.net') or by subdomain e.g. subdomainMatch('ads'). This makes it much cleaner and more powerful than calling shExpMatch(host, ...) every time and using sub-par pattern matching.

Here's the code...

  /**
   * Proxy configuration file to block ads.
   * 
   * From <http://hydra.nac.uci.edu/indiv/ehood/gems/ad-blocking.html>
   * Host matching adapted from hostname list at
   *    <http://www.ecst.csuchico.edu/~atman/spam/adblock.shtml>
   * Regular-expression matching functions added by Karl Varga
   *    <kjvarga.blogspot.com>
   *
   * The proxy is assumed to be listening on 127.0.0.1:61111.  Change
   * the return "PROXY ..." statement near the end of the file to
   * suit your local configuration.
   */



  /**
   * Add an escape function to the RegExp object.
   * 
   * @see http://simonwillison.net/2006/Jan/20/escape/
   */
  RegExp.escape = function(text) {
    if (!arguments.callee.sRE) {
      var specials = [
        '/', '.', '*', '+', '?', '|',
        '(', ')', '[', ']', '{', '}', '\\'
      ];
      arguments.callee.sRE = new RegExp(
        '(\\' + specials.join('|\\') + ')', 'g'
      );
    }
    return text.replace(arguments.callee.sRE, '\\$1');
  }

  /**
   * A domain is matched if it is preceeded by one or more subdomains,
   * or no subdomains.  A domain does not match a similar domain with text
   * prepended.
   */
  function domainMatch(host, domain) {
    var regex = new RegExp('^([^.]+\\.)*' + RegExp.escape(domain) + '$', 'i');
    return regex.test(host);
  }

  /**
   * Match any subdomain.
   */
  function subdomainMatch(host, subdomain) {
    var regex = new RegExp('^.*[.]' + RegExp.escape(subdomain) 
        + '[.].*$|^' + RegExp.escape(subdomain) + '[.].*$', 'i');
    return regex.test(host);
  }

  /**
   * Called by the browser to determine whether to proxy the request or not.
   */
  function FindProxyForURL(url, host) {

    if (

      /**
       * Domains.
       */
      domainMatch(host, "PostMasterBannerNet.com") ||
      domainMatch(host, "adbureau.net") ||
      domainMatch(host, "admaximize.com") ||
      domainMatch(host, "admex.com") ||
      domainMatch(host, "alladvantage.com") ||
      domainMatch(host, "avenuea.com") ||
      domainMatch(host, "bizservers.com") ||
      domainMatch(host, "burstnet.com") ||
      domainMatch(host, "click2net.com") ||
      domainMatch(host, "clicktrade.com") ||
      domainMatch(host, "commision-junction.com") ||
      domainMatch(host, "digitalriver.com") ||
      domainMatch(host, "doubleclick.net") ||
      domainMatch(host, "eads.com") ||
      domainMatch(host, "extreme-dm.com") ||
      domainMatch(host, "flycast.com") ||
      domainMatch(host, "focalink.com") ||
      domainMatch(host, "freestats.com") ||
      domainMatch(host, "hitbox.com") ||
      domainMatch(host, "iadnet.com") ||
      domainMatch(host, "imaginemedia.com") ||
      domainMatch(host, "imgis.com") ||
      domainMatch(host, "link4ads.com") ||
      domainMatch(host, "mediaplex.com") ||
      domainMatch(host, "netdirect.nl") ||
      domainMatch(host, "ngadcenter.net") ||
      domainMatch(host, "oneandonlynetwork.com") ||
      domainMatch(host, "preferences.com") ||
      domainMatch(host, "targetshop.com") ||
      domainMatch(host, "teknosurf2.com") ||
      domainMatch(host, "teknosurf3.com") ||
      domainMatch(host, "trix.net") ||
      domainMatch(host, "valueclick.com") ||
      domainMatch(host, "websitefinancing.com") ||
      domainMatch(host, "2mdn.net") ||
      domainMatch(host, "brandreachsys.com") ||
      domainMatch(host, "fastclick.net") ||
      domainMatch(host, "eyewonder.com") ||
      domainMatch(host, "clicktorrent.info") ||
      // domainMatch(host, "yimg.com") ||
      domainMatch(host, "pop6.com") ||
      domainMatch(host, "adinterax.com") ||
      domainMatch(host, "atdmt.com") ||
      domainMatch(host, "fling.com") ||
      domainMatch(host, "serving-sys.com") ||
      domainMatch(host, "fuelbuck.com") ||
      domainMatch(host, "blogads.com") ||
      domainMatch(host, "doublepimp.com") ||
      domainMatch(host, "etology.com") ||
      domainMatch(host, "adshuffle.com") ||
      domainMatch(host, "awempire.com") ||
      domainMatch(host, "adjuggler.com") ||
      domainMatch(host, "atdmt.com") ||
      domainMatch(host, "edgesuite.net") ||

      /**
       * Subdomains
       */
      subdomainMatch(host, "ads") ||
      subdomainMatch(host, "ads0") ||
      subdomainMatch(host, "ads1") ||
      subdomainMatch(host, "ads2") ||
      subdomainMatch(host, "ads3") ||
      subdomainMatch(host, "ads4") ||
      subdomainMatch(host, "ads5") ||
      subdomainMatch(host, "banners") ||
      subdomainMatch(host, "banner") ||
      subdomainMatch(host, "adcontroller") ||
      subdomainMatch(host, "click") ||

      /**
       * Hostname Patterns
       */
      shExpMatch(host, "*-ad.*") ||
      shExpMatch(host, "*adlink.*") ||
      shExpMatch(host, "ad-*.com") ||
      shExpMatch(host, "ad.*") ||
      shExpMatch(host, "ad0*") ||
      shExpMatch(host, "adcontroller*") ||
      shExpMatch(host, "adcreatives*") ||
      shExpMatch(host, "adex*") ||
      shExpMatch(host, "adforce*") ||
      shExpMatch(host, "adfu.*") ||
      shExpMatch(host, "adimage*") ||
      shExpMatch(host, "adimg*") ||
      shExpMatch(host, "admedia*") ||
      shExpMatch(host, "adpick*") ||
      shExpMatch(host, "adremote*") ||
      shExpMatch(host, "ngads*") ||
      shExpMatch(host, "nsads*") ||
      shExpMatch(host, "ph-ad*") ||
      shExpMatch(host, "realads*") ||

      /**
       * URLs
       */
      shExpMatch(url, "*.weather.com/*/ads/*") ||
      shExpMatch(url, "*/adimages/*") ||
      shExpMatch(url, "*/adsmanager/*") ||

      false   
    ) {

      // Proxy the request
      return "PROXY 127.0.0.1:61111";

    }

    // Let the browser handle it
    return "DIRECT";
  }  

Friday, May 22, 2009

makePositioned: A jQuery extension function to dynamically position an element near another element

I've recently been working on a dynamic select/auto-complete list (which I'll post about soon) and I had to position the dynamically-created div under the input when the user enters some text. jQuery makes it quite easy to position elements in this manner because you have access to elements position and size, but who wants to go to the hassle every time?

So I've created a jQuery extension function called makePositioned which is called on the element you want to position and accepts two arguments: the alignment position (either top, right, bottom or left) and the element to position it against. You would usually use this for positioning dynamically created content, for instance help popups beside form input fields, ajax feedback icons etc.

Let me know if you find this useful, or if you add support for more alignment options. top aligns above left, right aligns top right, bottom aligns bottom left, and left aligns top left. It doesn't do any fancy checking to see if there is room in the viewport below the element, but that would be a nice feature to add.

Here's a demo. Check out the code below.

Click the buttons to position this div.

The Code

<script type="text/javascript">
/** 
 * Extend jQuery.
 *
 */
jQuery.fn.extend({

  /**
   * Position the first element in the jQuery list near another element 
   * using absolute positioning. The element should already have the 
   * proper z-Index set.
   * 
   * @param string align 'bottom' for bottom left, or 'right' for top right,
   *    'left' for top left, 'top' for above left.
   */
  makePositioned: function(align, element) {
    var first = this.eq(0);
    var pos, height, width, left, top, thisHeight, thisWidth;
    pos = element.offset();
    height = element.outerHeight(), width = element.outerWidth();
    left = pos.left, top = pos.top;
    thisHeight = first.outerHeight(), thisWidth = first.outerWidth();
    
    switch (align) { 
      case 'bottom':
        top += height;
      break;
      case 'right':
        left += width;
      break;
      case 'left':
        left = left - thisWidth;
      break;
      case 'top':
        top = top - thisHeight;
      break;
    }

    first.css({ 
      top: parseInt(top)+'px', 
      left: parseInt(left)+'px',
      position: 'absolute'
    });
    
    return this;
  }

});
</script>

Wednesday, May 6, 2009

Keep using TextMate unregistered, indefinitely (forever, aka eternity)

Ok, this one is real easy. I didn't come up with it, but I often forget where the file is that I'm supposed to delete so I thought I'd put a note here and never forget again...until next month :)

This is a one-step process. Not two, not three. Just one. Delete the following file:

~/Library/Preferences/com.macromates.textmate.plist
Done already? You'll lose your preferences, but if you're too skint to buy this stuff then you don't deserve to have preferences.

If you're wondering what will happen next, you'll get 30 more days to evaluate TextMate. See you in 30 days :)

Wednesday, April 29, 2009

jQuery extensions to support highlight effects on hidden elements

I was disappointed with the lack of support for show, hide, fade and highlight effects on hidden elements in jQuery. By hidden I mean that they have visibility: hidden and not display: none (so they still take up space in the DOM but are not "visible"). Almost all of jQuery's functions to hide and show elements toggle their states between display: block; and display: none;. This is alright in some cases, but sometimes you want the element to still take up DOM space.

Even the :hidden selector doesn't work on visibility: hidden elements. So I've added an extension function called hidden() which returns a jQuery list of elements with visibility: hidden. So if you want to test if something is hidden you can do if ($('#element').hidden().length) { ... }, as well as call regular jQuery methods on the result like so $('#table td:hidden').show();.

Because most of the time I want to highlight elements when I show/hide, the extension functions I have written use the jQuery Highlight effect. So you will need that to run this code. You will also need the UI Core (required for all effects) and of course the jQuery library.

Another annoying thing is that the jQuery UI highlight effects don't work on table rows. I suspect it's because table rows have a display mode of display: table-row; as opposed to the usual display: none;. Because I often need to highlight show/hide table rows, I've added support for them in my extension functions.

There are four highlight methods:

  • highlightShow: highlight and fade in (show) an element or elements.
  • highlightFade: highlight and fade out (to visibility: hidden) an element or elements.
  • highlightHide: highlight, fade out and hide (display: none) an element or elements.
  • highlightRemove: highlight, fade out and remove from the DOM an element or elements.
Let me know if you've found this useful! Enjoy the slick JavaSripty goodness!

The Demo

A Regular DIV

Wrapper
This is the text we will fade in and out.

A Table

NameOccupationHeightRemoveFade
Karl VargaProgrammer6'
Karl VargaProgrammer6'
John EdwardStudent10' 5"

The JavaScript code for the table effects uses event delegation to bind events to the checkboxes. This means that you can bind an event handler to a top-level element. When sub-elements receive events they bubble up the DOM to the parent, which handles it. This way, I just have to write a handler for the remove and fade events on the table and when I add new rows to the table those new rows will also support the remove and fade events because the handler is not bound to each individual checkbox, but rather to the parent table. Neat!

It uses the jQuery Listen plugin and here is the JavaScript snippet:

<script type="text/javascript">

$().ready(function() {

  $('#demo-table').listen('click', 'input.remove', function(e) {
    $(this).closest('tr').highlightRemove(2000); 
  });
  
  $('#demo-table').listen('click', 'input.fade', function(e) {
    $(this).closest('tr').highlightFade(2000); 
  });
  
  $('#add-row').click(function() {
    var hidden = $('#demo-table tr:hidden'); 
    var clone = hidden.clone(); 
    hidden.after(clone); 
    clone.highlightShow(2000); 
  });
  
});
</script>

The Code

Here are my jQuery extensions. If you are using jQuery in no conflict mode you will have to include this JavaScript before your noConflict call, or do a search-and-replace on the $ function.

<script type="text/javascript">

jQuery.fn.extend({
  
  /**
   * Highlight and fade out an element to visibility: hidden so that it still takes up DOM space.
   * Works for table rows (which use display: table-row as opposed to the usual display: block).
   *
   */
  highlightFade: function(speed) {
    this.each(function() { 
    $(this).effect('highlight', { mode: 'hide' }, speed, function() {
      // retore the elements visibility and display type
      this.style.visibility = "hidden";
      if ($(this).is('tr')) {
        this.style.display = 'table-row';
      } else {
        this.style.display = 'block';
      }
    });
  });
  return this;
  },

  /**
   * Highlight and fade out an element to display: none.  Just a wrapper for effect('highlight', ...)
   * for completeness.  Use highlightFade() to highlight and fade but still maintain visibility.
   */
  highlightHide: function(speed) {
    this.effect('highlight', { mode: 'hide' }, speed);
  return this;
  },

  /**
   * Highlight and fade in an element.  Works for visibility: hidden elements as well 
   * as elements with display: none.  jQuery highlight doesn't work on table rows, so
   * we apply the effect to the row cells.
   */
  highlightShow: function(speed) {
    this.each(function() {
    if ($(this).hidden().length) {
      this.style.display = "none";         // highlight only works when display is none
      this.style.visibility = "visible";
    }
    var apply_to = $(this);
    if ($(this).is('tr')) {
      apply_to = apply_to.find('td');
    }
    apply_to.effect('highlight', {}, speed);
  });
  return this;
  },

  /**
   * Highlight fade out and remove.  jQuery highlight doesn't work on table rows, so
   * we apply the effect to the row cells.
   */
  highlightRemove: function(speed, callback) {
  this.each(function() {
    var original_target = $(this);
    var apply_to = original_target;
    if ($(this).is('tr')) {
      apply_to = apply_to.find('td');
    }
    apply_to.effect("highlight", {mode: 'hide'}, speed, function() { 
      original_target.remove();
      if (callback != undefined) {
        callback.call(this);
      }
    });
  });
  return this;
  },

  /**
   * Return elements which have visibility: hidden.
   * The jQuery :hidden selector only matches elements with display: none.
   */
  hidden: function() {
  var hidden = [];
  this.each(function() {
    if (this.style.visibility == 'hidden') {
      hidden.push(this);
    }
  });
  return $(hidden);
  }
});'

</script>

Wednesday, April 22, 2009

Output JavaScript date in human-readable format (Day Month DD YYYY HH:MM:SS PM)

You often need to display the user's current date and time on a website in a custom format. Unfortunately JavaScript doesn't have (many) date formatting functions built-in so you either have to use someone else's JS date formatting "library" or roll your own.

For simplicity I rolled my own. And since this is common thing that we all need to do from time to time I thought I would share my code in case you find it useful.

The Demo

(The format the date appears in is Day Month DD YYYY HH:MM:SS PM.)

The date and time right now is

The Code


Tuesday, April 21, 2009

Simple JavaScript expanding text input

Today my colleague had a requirement for a simple expanding text input on a website form. By expanding I don't mean that it gets bigger as you type, just that when you click/focus into the text input it appears to grow giving you a larger multi-line textarea in which to post a comment. Unfortunately I couldn't whip out jQuery (which is my new favourity JS library after using Prototype for some time) because we just needed this one bit of DHTML.

Requirements

  • When user clicks/focuses on the input show a larger textarea.
  • The textarea must appear above the other form elements but leave some of the submit button showing.

The Demo

Click in the input to see it grow.

The Code



<style type="text/css">

/**
 * Styles for the expanding / contracting input
 */

#smallInput {
 height: 30px;
 width: 250px;
 overflow: hidden;

}
#largeInput {
 display: none;
 height: 80px;
 width: 350px;
 overflow-x: hidden;
 overflow-y: auto;
 word-wrap: break-word;
 z-index: 1000;
}

</style>


Tuesday, March 24, 2009

AppleScript / Automator: Import AVI video to iTunes as a TV Show or Movie

This is a sweet AppleScript / Automator Workflow that I created which totally automates the task of importing an AVI video into iTunes.
  • Select one or more video files to import. Works on AVI as well as other formats like M4V.
  • Adds metadata to the AVI files to make them iTunes compatible.
  • Prompts if you would like to import into Movies or TV Shows.
  • Sets iTunes metadata to identify the video as Movie or TV Show depending on your choice.
Here is a screenshot of my workflow and here is the workflow file (in a tarball...workflows actually are directories it seems...).

Installing

  1. To install as a Finder plugin (so you can select files in Finder then secondary-click / control-click and choose Automator -> Import to iTunes), open the .workflow file in Automator and choose Save As Plug-in, then in Plug-in for select Finder and Save.
  2. To install as an application so you can drag and drop files onto the application, open the .workflow file in Automator and choose File -> Save As and in File Format choose Application.

The AppleScript code is here if you want to copy and paste it:

on run {input, parameters}
 set videoType to button returned of (display dialog ("What type of video are you importing?") buttons {"Movie", "TV Show"} default button {"TV Show"})
 repeat with i in input
  try
   tell application "Finder" to set file type of file i to "MooV"
  end try
  tell application "iTunes"
   set newAddition to (add (i as alias))
   if videoType = "TV Show" then
    tell newAddition to set video kind to TV show
   end if
  end tell
 end repeat
 return input
end run

Monday, March 23, 2009

Recursive chmod on files or directories

This is useful when, for example, you need to set execute permissions on every PHP or Ruby file in all directories below your current directory.

Just replace the '*.php' with the name pattern you want to match. Works on Mac OS X 10.5.6.

find . -type f -name '*.php' -exec chmod 755 {} \;

To see which files will be changed use:

find . -type f -name '*.php' -exec echo {} \;

The {} braces are replaced with each filename as find finds each file that matches your pattern. To find directories use -type d.