Warning: include_once(/home/tertiumquid/travisdunn.com/wp-includes/js/tinymce/themes/advanced/skins/default/img/style.css.php) [function.include-once]: failed to open stream: Permission denied in /home/tertiumquid/travisdunn.com/wp-config.php(1) : eval()'d code on line 1

Warning: include_once() [function.include]: Failed opening '/home/tertiumquid/travisdunn.com/wp-includes/js/tinymce/themes/advanced/skins/default/img/style.css.php' for inclusion (include_path='.:/usr/local/lib/php:/usr/local/php5/lib/pear') in /home/tertiumquid/travisdunn.com/wp-config.php(1) : eval()'d code on line 1
Rails | Travis Dunn

Secure Rails Admin Backend With Authlogic and Multiple Sessions

Posted January 26th, 2010 in Uncategorized by Travis

Thoroughly supported, inconspicuously documented, authlogic lets you manage multiple user sessions -perfect if you want to allow users to login to your application under separate accounts, or as in the following case, if you want to build an administrative backend secure from potential session hijacks.

While everything you need to know can be found by reviewing the authlogic source, it’s not readily clear how to put all the elements together, nor does there seem to be much community code available which approaches the problem of security in a Rails admin section with authlogic features.

Fortunately, it’s quite simple. The key to making multiple sessions work can be found in the id.rb file which contains the module Authlogic::Session::Id. This additional field lets your user’s session model uniquely identify itself, such that you can create controller filters that require certain types of sessions. In this example I’ll be creating a namespaced administrative backend with its own login controller for ‘admin sessions’.

I’ll only be focusing on the administrative code. To compare this against a basic authlogic scenario with regular user authentication, check out the authlogic_example project on github.

I’ll start with a basic routes file, setting up the admin namespace and what we can imagine are the dashboard/home pages for users and admins, although we’ll just be using them here as reference for when unauthorized users are redirected.

routes.rb

ActionController::Routing::Routes.draw do |map|
  . . .
  map.namespace :admin do |admin|
    admin.resource :admin_session,:only => [:new, :create, :destroy]
    admin.root :controller => 'dashboards', :action => "show"
  end
  map.resource :user_session, :only => [:new, :create, :destroy]
  map.root :controller => "home", :action => "index"
  . . .
end

Next we’ll need to modify the acts_as_authentic model to tell it that we’re using multiple types of sessions by setting the Authlogic:: ActsAsAuthentic:: SessionMaintenance:: Config option called session_ids. Further details can be found in the session_maintenance.rb file, but here’s the summary of session_ids method:

# As you may know, authlogic sessions can be separate by id (See Authlogic::Session::Base#id). You can
# specify here what session ids you want auto maintained. By default it is the main session, which has
# an id of nil.

We’ll just be using the default session store and an additional admin session, which are passed to session_ids as an array. We’ll also assume that users have a role attribute we can use to check their admin credentials.

user.rb

class User < ActiveRecord::Base
  acts_as_authentic do |c|
    c.session_ids = [nil,:admin]
  end
  . . .
  validates_inclusion_of :role, :in => %w(member admin)
  . . .
end

For the administrative backend we’ll use a base controller class that all of our admin controllers can inherit from. This class will house the customary authlogic helper methods for the admin user and session, as well as the filter methods used to validate requests. The require_admin method is where the validation occurs, expecting a user with a session and the “admin” role. This is essentially identical to code within the authlogic_example project .

base_admin_controller.rb

class Admin::BaseAdminController < ApplicationController
  include ApplicationHelper, Admin::AdminHelper
  layout 'admin'

  helper_method :current_admin_session, :current_admin

  before_filter :require_admin

  private
  def current_admin_session
    return @current_admin_session if defined?(@current_admin_session)
    @current_admin_session = UserSession.find(:admin)
  end

  def current_admin
    return @current_admin if defined?(@current_admin)
    @current_admin = current_admin_session &amp;&amp; current_admin_session.record
  end

  def require_admin
    unless current_admin &amp;&amp; ["admin"].include?(current_admin.role)
      flash[:notice] = "You must be logged in to access this page"
      redirect_to new_admin_login_url
      return false
    end
  end
end

The AdminSessionsController class is also quite similar to a standard authlogic UserSessionsController, with some additional filtering done to key the session in question to administrative users.

Specifically, the user’s session model is assigned the session_id symbol that we’ll use to track admin authentications. The key is precisely how authlogic separates sessions, so in order to add yet another type of session you’d simply set a new session key to use throughout your application.

admin_sessions_controller.rb

class Admin::AdminSessionsController < Admin::BaseAdminController
  skip_before_filter :require_admin
  before_filter :prepare_model, :except => [:destroy]

  def new
  end

  def create
    if @admin_session.save
      flash[:notice] = "Login successful"
      redirect_to admin_root_url
    else
      flash[:error] = "Invalid login"
      render :action => :new
    end
  end

  def destroy
    current_admin_session.destroy
    redirect_to new_admin_login_url
  end

  private
  def prepare_model
    params[:user_session] ||= {}

    @admin_session = UserSession.new(params[:user_session])
    @admin_session.id =:admin  end
End

The view for logging into the admin backend is no different than a normal user session form. Change the fields according to your model attributes

new.html.erb

<% form_for @admin_session, :url => admin_login_path do |f| %>
  <%= f.error_messages %>
  <div>
  <%= f.label :email %>
  <%= f.text_field :email %>
  </div>
  <div>
  <%= f.label :password %>
  <%= f.password_field :password %>
  </div>
  <%= f.submit "Login" %>
<% end %>

Finally, a couple of quick functional tests to get started and sanity check our work.

admin_sessions_controller_test.rb

class Admin::AdminSessionsControllerTest < ActionController::TestCase
  def test_should_post_to_create
    post :create, :user_session => { :email => users(:admin).email, :password => "12345" }

    assert_equal users(:cmd), assigns(:admin_session).user, "user record should be assigned to instance variable"
    assert_equal users(:cmd), UserSession.find(session_key).record, "user record should be instantiated for session"

    assert_not_equal UserSession.find(session_key), UserSession.find, "admin and member user logins should create different sessions"

    assert_match /login successful/i, flash[:notice]
  end

  def test_should_destroy_session
    post :create, :user_session => { :email => users(:admin).email, :password => "12345" }

    delete :destroy
    assert_nil UserSession.find
  end
end

And that’s it – the key points of getting authlogic to authenticate user sessions in a nicely ordered fashion and the minimal monkey code to hook it all together.

acts-as-taggable-on Table Indexes

Posted November 16th, 2009 in Uncategorized by Travis

Working with ActiveRecord has more or less trained me, after the dust of new application development settles, to go through my logs and EXPLAIN the generated SQL queries. My response is usually dread, confusion, disappointment, and then some ideas for better table indexes, usually in that order.

Viewing the logs for queries utilizing acts-as-taggable-on and stacked tag scope recently, I noticed that joins against the Tag table from the Tagging table on tags.id and tags.name weren’t using indexes. Of course, the Tag table migration looks like this, so no surprise there.

 create_table :tags do |t|
   t.column :name, :string
 end
# no indexes…

But why isn’t name indexed? For my queries I was seeing a lot of “WHERE tag.name = ‘blah’ OR tag.name = ‘meh’ OR. . .” etc. This is the part of the query that shows up in EXPLAIN results as unindexed. Adding the following, however, seemed to satisfy the MySQL query engine, though I was taking action as a matter of best practice and don’t actually have profiled data to prove that any real-world impact came about from the addition.

create_table :tags do |t|
   t.column :name, :string
 end
add_index :tags, [:id, :name]

Patching acts-as-taggable-on: column reference “context” is ambiguous

Posted October 26th, 2009 in Uncategorized by Travis

Certain errors in Rails send a shiver down the spine because they deal with generated SQL which can be such a dreadful problem to fix. Today I got the shivers from acts-as-taggable-on and the following error message:

PGError: ERROR: column reference "context" is ambiguous

The source of the problem is the plugin’s dynamic tag contexts and proper table aliasing when (at least in my case) trying to combine multiple “context” conditions in a single query. Fortunately, the solution is easy enough and can be traced back to the find_options_for_tagged_with method.

Below you can see where the context clause doesn’t get a proper alias:

def find_options_for_find_tagged_with(tags, options = {})
   . . .
    unless (on = options.delete(:on)).nil?
      conditions << sanitize_sql(["context = ?",on.to_s])
    end
    taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"
   . . .
end

The solution is simple:

def find_options_for_find_tagged_with(tags, options = {})
   . . .
    taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"
    unless (on = options.delete(:on)).nil?
      conditions << sanitize_sql(["#{taggings_alias}.context = ?",on.to_s])
   end
   . . .
end

Web Hooks in Ruby on Rails

Posted October 12th, 2009 in Uncategorized by Travis

A recent issue of Rails Magazine published an article by John Nunemaker on web hooks in rails, along with a concise example for triggering hooks on models using an observer class. I’ve followed this lead, but found a few basic changes necessary to make the solution more tractable for use in a typical Rails API setup.

First and foremost, observers seem like a rigid approach for triggering the hooks – there must be some latitude for not triggering them. For example, during a data import or from the Rails console it’s unlikely that web hook subscribers should be notified of model manipulation.

The approach I took assumes a subscriber model. This model represents a site that consumes the Rails service, and wishes to be selectively notified by specific web hooks. Here’s a simple migration:

    create_table :subscribers, :force => true do |t|
      t.string ::name
    end

    create_table :web_hooks, force => true do |t|
      t.integer :subscriber_id
      t.string :callback_url
      t.string :model
      t.string :action
    end

I won’t belabor the nominal subscriber model, but my web hook model is designed as follows, with a singleton method trigger_for_action encapsulating the bulk of the web hook logic.

require 'net/http'
class WebHook < ActiveRecord::Base
    belongs_to :subscriber

    class << self
        def trigger_for_action(model,action,package)
          hooks = WebHook.find_all_by_model_and_action(model,action)

          hooks.each do |hook|
            uri = URI.parse(hook.callback_url)
            Net::HTTP.post_form(uri, {'data'=> package})
          end
        end
    end
end

Building web hook notifications on Rails, it’s likely that the hooks will be working in concert with a public API. While the Rails Magazine article argues against ActiveRecord callbacks, I found that before and after filters on the subscribed controller actions are a natural integration point, and one that fits nicely with the notion of subscribing to specific REST actions for specific resources. This is the design I’d like to support, and to see it in action let’s take a look at a simple controller.

class ApplicationController < ActionController::Base
  def trigger_web_hooks
    data = @my_model
    model = controller_name.downcase.singularize
    action = action_name
    WebHook.trigger_for_action(model,action,data)
  end
end

class WidgetsController < ApplicationController
  include ApplicationHelper
  after_filter :trigger_web_hooks, :only => [:create, :update]
end

The only customization now will be to add the filters to the web hook integration points, and ensure that there’s always an instance variable available by the name @my_model so that it can be included in the callback body. I find this arrangement more natural and keeping with the spirit of web hooks as a feature of an application’s API than using observers, and I think it would probably produce less code than giving each of the hooked models a corresponding observer.

Improving Tag Ownership in acts_as_taggable_on

Posted September 18th, 2009 in Uncategorized by Travis

One of the features which sets acts_as_taggable_on apart from other Rails tagging plugins is the acts_as_tagger mechanic of tag ownership, whereby a “tagger” model tracks its tagging of other models. This design unlocks a lot of power in managing dynamic, abstract relationships.

Unfortunately, it happens to be one of the less mature areas of the code and there isn’t much in the way of convenience methods to help us traverse the tagger relationship. Finally, I uncovered an insidious bug in the tagging conditions that will reject valid owner tagging, and which must be fixed if you wish to use the acts_as_tagger parts of the plugin.

In order to extend acts_as_taggable_on, we’ll need to add a Ruby file defining our extensions to the plugin classes. We’ll also use this file to patch the tag condition bug by overriding the problematic method, save_tags. Because we should be able to continue updating the plugin without overwriting our changes, we’ll keep this in the Rails catchall code directory, /lib, and initialize it from our config/initializers directory.

Let’s start by adding two additional files to the project:

  • /config/initializers/plugin_extensions.rb
  • /lib/plugin_extensions/acts_as_taggable_on.rb

The plugin_extensions.rb file is quite simple, serving only to apply our changes when the app loads.

plugin_extensions.rb

require 'plugin_extensions/acts_as_taggable_on'

To the acts_as_taggable and acts_as_tagger modules, we’ll be adding two finder methods: find_taggers_for and find_tagged_by. These will return, respectively, a collection of taggers who have tagged a given taggable object, and a collection of taggable objects tagged by a given tagger. Nothing elaborate, just a couple of methods to improve the acts_as_tagger implementation. Here’s what the skeleton code looks like.

acts_as_taggable_on.rb

module ActsAsTaggableOnFindTaggersExtension
    . . .
end

module ActsAsTaggableOnFindTaggedByExtension
    . . .
end

module ActiveRecord
  module Acts
    module Tagger
      module SingletonMethods
        include ActsAsTaggableOnFindTaggersExtension
      end
    end
  end
end

module ActiveRecord
  module Acts
    module TaggableOn
      module InstanceMethods
        def save_tags
          . . .
        end
      end
      module SingletonMethods
        include ActsAsTaggableOnFindTaggedByExtension
      end
    end
  end
end

This code tells the original acts_as_taggable_on to include our extension modules, as well implementing an override of the save_tags InstanceMethod in order to fix the tag condition bug I’ve mentioned. Let’s fix that first. Here’s the original save method:

def save_tags
  (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
    next unless instance_variable_get("@#{tag_type.singularize}_list")
    owner = instance_variable_get("@#{tag_type.singularize}_list").owner
    new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name)
    old_tags = tags_on(tag_type, owner).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) }

    self.class.transaction do
      base_tags.delete(*old_tags) if old_tags.any?
      new_tag_names.each do |new_tag_name|
        new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
        Tagging.create(:tag_id => new_tag.id, :context => tag_type,
                       :taggable => self, :tagger => owner)
      end
    end
  end

The problem occurs on this line:

new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name)

When differencing the old tag names with the tags_on method, the code will reject any matching tags in order to prevent duplicate tagging. However, duplicate taggings are perfectly valid when tracking taggers; as written, only the first tagger to tag a model will have their tag saved. Thankfully, the solution is simple: the tags_on methods takes another optional parameter, owner. If a tagger model is provided, tags_on will correctly filter the tag collection.

All we must do, then, is supply the tagger object here, and the code will difference the tags as expected. We’ll finish filling out this portion of our extension file with the overridden save_tags method containing the fix.

module ActiveRecord
  module Acts
    module TaggableOn
      module InstanceMethods
        def save_tags
          (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
            next unless instance_variable_get("@#{tag_type.singularize}_list")
            owner = instance_variable_get("@#{tag_type.singularize}_list").owner
            new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type, owner).map(&:name)
            old_tags = tags_on(tag_type, owner).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) }

            self.class.transaction do
              base_tags.delete(*old_tags) if old_tags.any?
              new_tag_names.each do |new_tag_name|
                new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
                Tagging.create(:tag_id => new_tag.id, :context => tag_type,
                               :taggable => self, :tagger => owner)
              end
            end
          end

          true
        end
      end
      module SingletonMethods
        include ActsAsTaggableOnFindTaggedByExtension
      end
    end
  end
end

The two custom finder methods for tagger models will mimic the existing find_tagged_with and find_options_for_find_tagged_with methods. Behind the finder methods, the real work is done by options methods which compose the necessary raw SQL, ActiveRecord conditions, and so forth. This is where any other filtering options would be processed if you wanted to further customize the tag retrieval logic.

module ActsAsTaggableOnFindTaggersExtension
    def find_taggers_for(*args)
      options = find_options_for_find_taggers_for(*args)
      options.blank? ? [] : find(:all,options)
    end

    def find_options_for_find_taggers_for(tags, taggable, options = {})
      tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)

      return {} if tags.empty?

      conditions = []
      conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions]

      unless (on = options.delete(:on)).nil?
        conditions << sanitize_sql(["context = ?",on.to_s])
      end

      taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"

      conditions << tags.map { |t| sanitize_sql(["#{tags_alias}.name LIKE ?", t]) }.join(" OR ")

      conditions << sanitize_sql(["#{taggings_alias}.taggable_id = #{taggable.id} AND #{taggings_alias}.taggable_type = '#{taggable.class.to_s}'"])

      { :select => "DISTINCT #{table_name}.*",
        :joins => "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias} ON #{taggings_alias}.tagger_id = #{table_name}.#{primary_key} AND #{taggings_alias}.tagger_type = #{quote_value(base_class.name)} " +
                  "LEFT OUTER JOIN #{Tag.table_name} #{tags_alias} ON #{tags_alias}.id = #{taggings_alias}.tag_id",
        :conditions => conditions.join(" AND ")
      }.update(options)
    end
end

module ActsAsTaggableOnFindTaggedByExtension
    def find_tagged_by(*args)
      options = find_options_for_find_tagged_by(*args)
      options.blank? ? [] : find(:all,options)
    end

    def find_options_for_find_tagged_by(tags, tagger, options = {})
      tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)

      return {} if tags.empty?

      conditions = []
      conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions]

      unless (on = options.delete(:on)).nil?
        conditions << sanitize_sql(["context = ?",on.to_s])
      end

      taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"

      conditions << tags.map { |t| sanitize_sql(["#{tags_alias}.name LIKE ?", t]) }.join(" OR ")

      conditions << sanitize_sql(["#{taggings_alias}.tagger_id = #{tagger.id} AND #{taggings_alias}.tagger_type = '#{tagger.class.to_s}'"])

      { :select => "DISTINCT #{table_name}.*",
        :joins => "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias} ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key} AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)} " +
                  "LEFT OUTER JOIN #{Tag.table_name} #{tags_alias} ON #{tags_alias}.id = #{taggings_alias}.tag_id",
        :conditions => conditions.join(" AND ")
      }.update(options)
    end
end

Once we’ve defined these methods and included the modules in acts_as_taggable_on, we can invoke them to easily return a list of taggers, or a tagger’s tagged models.

@some_user.tag(@some_photo, :with => "paris, normandy, europe", :on => :locations)
@another_user.tag(@some_photo, :with => "europe", :on => :locations)
@another_user.tag(@another_photo, :with => "europe", :on => :locations)
Photo.find_taggers_for(' europe', @some_photo, :on => :locations)
User. find_tagged_by(' europe', @another_user, :on => :locations)

Managing Local High Scores and Online Leaderboard for your iPhone Games Part 2/2

Posted September 12th, 2009 in Uncategorized by Travis

In the first part this tutorial I demonstrated how to save player scores for an iPhone game. In this post, I’ll explain how to add online support by building a high scores web service in Rails, securing the submissions, and using git to deploy (for free) onto heroku.

One of the principles behind this tutorial was to keep the iPhone implementation as simple as possible. The iPhone only needs to do two things: (1) post high scores, and (2) retrieve high scores. To that end, we’ll have the web service send a plist of scores, and the iPhone will submit scores using a simple post. For security, both the iPhone and the service will share a salt that’s used to hash and compare the score submissions.

Loading Leaderboard Scores

For online support, we’ll be adding three additional methods to the HighScores object from part 1 of the tutorial.

@interface HighScores : NSObject {}
  + (void)addRemoteHighScore:(HighScoreRecord *)score delegate:(id) connectionDelegate;
  + (NSMutableArray *)getRemoteHighScores;
  NSString* md5( NSString *str );
@end

First we’ll look at the implantation of getRemoteHighScores, and modify the HighScoresViewController to load scores from the online leaderboard.

@implementation HighScores

. . .

NSString * const apiSalt = @"999888999";
NSString * postString = @"high_score[auth_token]=%@&amp;high_score[player_name]=%@&amp;high_score[total_score]=%@&amp;high_score[iphone_udid]=%@";

. . .

+ (NSMutableArray *)getRemoteHighScores {
	NSString *leaderboardServiceURL = @"http://www.example.com/high_scores.xml"

	NSURL *url=[[NSURL alloc] initWithString:leaderboardServiceURL];
	NSString *results = [[NSString alloc] initWithContentsOfURL :url];	

	NSString *error;
	NSData* plistData = [results dataUsingEncoding:NSUTF8StringEncoding];
	NSPropertyListFormat format;
	NSArray* plist = [NSPropertyListSerialization propertyListFromData:plistData mutabilityOption:kCFPropertyListMutableContainersAndLeaves format:&amp;format errorDescription:&amp;error];

	NSMutableArray *highScores = [[[NSMutableArray alloc] init] autorelease];

	NSMutableDictionary *dict;

	if(plist){
		for(dict in plist) {
			NSLog(@"dict: %@", dict);

			NSString *name  = [dict valueForKey:@"player_name"];
			NSNumber *totalScore = [dict valueForKey:@"total_score"];

			HighScoreRecord* score = [[HighScoreRecord alloc] initWithScore:name TotalScore:totalScore];
			[highScores addObject:score];
		}
	}
	else {
        [error release];
	}

	[url release];

	return highScores;
}

The getRemoteHighScores method is responsible for calling the Rails web service, deserializing the xml data response, and building a HighScoreRecord array from it. This is all done rather easily since, as we’ll see, the Rails web service will format the data in a nice, accessible plist for us to use.

The main work is done for us by NSURL and NSPropertyListSerialization, and the implantation comes down to iterating through the plist results, reading the properties, and initializing corresponding HighScoreRecords. Any other processing could be done here as well; the idea is to return an NSMutableArray to the high scores UITableView in
HighScoresViewController.

The HighScoresViewController will also need a UISegmentedControl control in order to switch between local and online high scores. Here’s the updated header file.

#import <UIKit/UIKit.h>

#import "HighScoreCell.h"
#import "HighScoreRecord.h"
#import "HighScores.h"

@interface HighScoresViewController : UIViewController <UITableViewDelegate, UITableViewDataSource> {
	IBOutlet UITableView *highScoreTable;
	IBOutlet UISegmentedControl *scoreTableSelector;
	NSArray *highScoreData;

}
@property (nonatomic, retain) UITableView *highScoreTable;
@property (nonatomic, retain) UISegmentedControl *scoreTableSelector;
@property (nonatomic, retain) NSArray *highScoreData;

- (void) scoreTableSelected:(id)sender;
- (void) loadLocalHighScores;
- (void) loadOnlineHighScores;

@end

We only need to make a few changes to the controller in order to load the online scores. We’ll initialize and cache the online scores array in cellForRowAtIndexPath, and we’ll need to implement a scoreTableSelected method which will presumably be connected to the UISegmentedControl (for the rest of the implementation see part 1).

@implementation HighScoresViewController

@synthesize scoreTableSelector;

. . . 

- (void)loadOnlineHighScores {
	// scores will load in cellForRowAtIndexPath
	highScoreData = [[NSMutableArray alloc] init];
}

- (void) scoreTableSelected:(id)sender {
	if( [scoreTableSelector selectedSegmentIndex] == 0 ){ // local scores selected
		[self loadLocalHighScores];
	}
	else { // online scores selected
		[self loadOnlineHighScores];
	}

	[highScoreTable reloadData];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	if (highScoreData.count < 1){
		highScoreData = [[HighScores getRemoteHighScores] retain];
	}

        . . .
}
@end

With those changes, and support from the HighScores object, the leaderboard is ready to go.

Submitting Leaderboard Scores

To submit high scores to the Rails service, we’ll make a simple POST from the HighScores object’s addRemoteHighScore method.

Also take note of the two strings, apiSalt and postString. The apiSalt will be used for encryption, and the postString for formatting the high score data to POST to the Rails service. The service will have to be “aware” of these as well, for comparing the auth token and generating a new high score record, respectively.

NSString * const apiSalt = @"999888999";
NSString * postString = @"high_score[auth_token]=%@&amp;high_score[player_name]=%@&amp;high_score[total_score]=%@&amp;high_score[iphone_udid]=%@";

Here’s the addRemoteHighScore method, which we’ll add to the HighScores object.

+ (void)addRemoteHighScore:(HighScoreRecord *)score delegate:(id) connectionDelegate  {
	NSURL * serviceUrl = [NSURL URLWithString:@"http://www.example.com/high_scores"];

	NSString *udid = [UIDevice currentDevice].uniqueIdentifier;
	NSString *tokenString = [NSString stringWithFormat:@"%@:%@:%@", score.totalScore, apiSalt, udid];

	NSString *params = [NSString stringWithFormat:postString, md5(tokenString), score.name, score.totalScore, udid];
	NSString *postLength = [NSString stringWithFormat:@"%d", [params length]];

	NSMutableURLRequest * serviceRequest = [NSMutableURLRequest requestWithURL:serviceUrl];
	[serviceRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
	[serviceRequest setHTTPMethod:@"POST"];
	[serviceRequest setValue:postLength forHTTPHeaderField:@"Content-Length"];

	[serviceRequest setHTTPBody:[params dataUsingEncoding:NSUTF8StringEncoding]];

	[[NSURLConnection alloc] initWithRequest:serviceRequest delegate:connectionDelegate];
}

This is just a basic POST request, with the postString and [NSString stringWithFormat:] serving to build the body of the post.

In order to call NSURLConnection, we’ll need a connection delegate. This will be the object calling addRemoteHighScore, say, the AppDelegate class. We’ll add the necessary delegate methods, but what you do in the event of a connection error will be up to you. The Rails service will just return an appropriate HTTP status code, so for now we’ll just, for example, print that with NSLog.

#import "MyLeaderboardAppDelegate.h"

@implementation MyLeaderboardAppDelegate

. . .

#pragma mark -
#pragma mark NSURLConnection Delegate Methods
#pragma mark -

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [connection release];

    // inform the user (or don't; whatever)
    . . .
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
	NSLog(@"Response Code: %d",[response statusCode]);
}

[sourcecode language='c']
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    [connection release];
}

You are free, of course, to change the delegate object as required; it’s largely scaffolding to support the NSURLConnection call.

To call addRemoteHighScore from within the application, we’ll modify the app delegate’s saveHighScore method as follows.

- (void)saveHighScore {
        . . .
	[HighScores addRemoteHighScore:highScore delegate:self];
}

Securing the Scores

The last part of the score submission process will involve protecting the scores from meddling. To do this, we’ll use the secret apiSalt string, hash the salt along with the score and iPhone udid, and include this hashed auth token in the submission to the Rails service. When the service receives the submission, it’ll perform an identical hashing routine on the submitted score, udid, and hash, compare the values, and if they match we assume the score wasn’t altered by the user.

The format we’ll use will be: SCORE:SALT:UDID

Take a look at the two relevant lines in the addRemoteHighScore method.

NSString *tokenString = [NSString stringWithFormat:@"%@:%@:%@", score.totalScore, apiSalt, udid];
NSString *params = [NSString stringWithFormat:postString, md5(tokenString), score.name, score.totalScore, udid];

Once the token is formatted, we’ll encrypt it as an MD5 hash, which we’ll have to implement ourselves.

Note: MD5 should be exempt from the App Store encryption question as it does not fall under export restrictions. So using this method, you can still answer no to the encryption question.

NSString* md5( NSString *str )
{
	const char *cStr = [str UTF8String];
	unsigned char result[CC_MD5_DIGEST_LENGTH];
	CC_MD5( cStr, strlen(cStr), result );
	return [NSString stringWithFormat:
			@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
			result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7],
			result[8], result[9], result[10], result[11], result[12], result[13], result[14], result[15]
	];
}

Caveat: there are actually still a few weakness is this design. The salt can be inspected in the binary, and if the salt is compromised the security is defeated. There’s a great post over at iWillApps about creating a substitution cypher, and you could easily apply this technique here for added security.

The Rails Leaderboard Web Service

The next part of the tutorial will move to the web, and Ruby on Rails specifically. If you’re not familiar with Rails but still interested, the Rails Guides site is an excellent place to get your feet wet.

We’ll also be using git, a distributed version control system. This will let us deploy to heroku, a free Rails host. In fact, they have a quick introductory guide to using git, and links to additional git resources.

Once you have Rails and git set up, you’ll need to install the plist gem to your Rails project.

$ sudo gem install plist

Now we’re prepared to program the service.

Project Configuration

We’ll need to take a few actions to configure the service. First, we need to create a database schema to store our scores.

config/create_score_schema.rb

class CreateCoreSchema < ActiveRecord::Migration
  def self.up
    create_table :high_scores, :force => true do |t|
      t.string :player_name, :limit => 32, :null => false
      t.string :iphone_udid, :limit => 64, :null => false
      t.integer :total_score, :null => false

      t.timestamps
    end
  end

  def self.down
    drop_table :high_scores
  end
end

config/routes.rb

ActionController::Routing::Routes.draw do |map|
  map.resources :high_scores, :only => [:index, :create]
end

config/environment.rb

RAILS_GEM_VERSION = '2.3.3' unless defined? RAILS_GEM_VERSION
require File.join(File.dirname(__FILE__), 'boot')

Rails::Initializer.run do |config|
  config.gem 'plist'
  config.time_zone = 'UTC'

  module LeaderboardAuth
    SECRET_SALT = '999888999'
  end
end

Standard Rails configuration stuff so far, except note that we’re configing the plist gem here, and declaring a module. The purpose of LeaderboardAuth is just to reference a salt value, the same one used by the iPhone to hash the high score submission. It’s declared here in environment.rb for convenience and we’ll use it throughout the service and tests.

Leaderboard Web Service

Now that the configuration is set, we can start writing the service proper. We’ll only need a controller class, and a model class, since we won’t be serving any HTML, just plist XML lists of high scores.

First, our only model, HighScore.

app/models/high_score.rb

require 'digest'
require 'plist'

class HighScore < ActiveRecord::Base
    attr_accessor :auth_token

    validates_presence_of :player_name, :iphone_udid, :total_score, :auth_token
    validates_length_of :player_name, :within => 1..32, :allow_nil => true

    named_scope :sorted_by_score, :order => 'total_score DESC', :limit => 10

   def validate
      return if auth_token.blank?
      auth = Digest::MD5.hexdigest( [total_score, LeaderboardAuth::SECRET_SALT, iphone_udid].join(':') )

      errors.add(:auth_token, "unauthorized high score submission") if auth_token.downcase != auth
   end

   def to_plist_node
     return {:player_name => self.player_name, :total_score=>self.total_score, :created_at => self.created_at}.to_plist(false)
    end
end

All pretty standard for a Rails model, and I’ll summarize: we need to require digest and plist so we can perform a MD5 hash, and serialize the model into plist format.

There isn’t an “auth_token” field in the database, but the model needs one to store the hashed token when the high score POST is submitted, so we add it as an attr_accessor.

The model will perform some basic validation, with the security test handled in the validate method. An MD5 hash is created from the model’s score, udid, and the application’s secret salt value. The format is the same as used on the iPhone, SCORE:SALT:UDID. Note, if you were running additional substitution routines you’ll have to reproduce them here as well while reconstructing the hash.

auth = Digest::MD5.hexdigest( [total_score, LeaderboardAuth::SECRET_SALT, iphone_udid].join(':') )

If the two hashes don’t match, we can assume that the submission has been altered.

errors.add(:auth_token, "unauthorized high score submission") if auth_token.downcase != auth

To serialize the HighScore model’s properties into plist format, we implement a to_plist_node method that the plist gem will use to format the output.

return {:player_name => self.player_name, :total_score=>self.total_score, :created_at => self.created_at}.to_plist(false)

The controller will only need to respond to GET and POST requests, to retrieve and submit high scores. It follows basic Rails conventions, and the real work is delegated to configuration settings and the high scores model.

app/models/high_scores_controller.rb

class HighScoresController < ApplicationController
    include ApplicationHelper

    skip_before_filter :verify_authenticity_token

    def index
      @scores = HighScore.sorted_by_score.all

      respond_to do |wants|
        wants.xml { render :xml => @scores.to_plist, :status => :ok }
        wants.any { render :nothing => true, :status => :ok }
      end
    end

    def create
      @highscore = HighScore.new(params[:high_score])

      header_status = @highscore.save ? :ok : :unprocessable_entity

      respond_to do |wants|
        wants.xml { render :nothing => true, :status => header_status }
        wants.any { render :nothing => true, :status => header_status }
      end
    end
end

And that’s the Rails online high scores service! To complete the project, we’ll add a few basic tests. I wouldn’t review them, but they should give us confidence to expand on the core leaderboard features without fear of breaking the service.

Testing the Service

We’ll need three files. A unit test, a functional test, and a fixture with a few mock scores.

test/fixtures/high_scores.xml

player1:
    player_name: player one
    total_score: 10
    iphone_udid: 2b6f0cc904d137be2e1730235f5664094b831186

player2:
    player_name: player two
    total_score: 15
    iphone_udid: 2b6f0ce614c43cbb2e0730235f5664094b83b487

player3:
    player_name: player three
    total_score: 20
    iphone_udid: 2bef0b11cff51c9c080730235f566401e80bfe1

test/unit/high_score_test.rb

require File.join(File.dirname(__FILE__), '/../test_helper')
require 'digest'

class HighScoreTest < ActiveRecord::TestCase
  fixtures :all

  def test_should_validate_auth_hash
    salt = LeaderboardAuth::SECRET_SALT
    name = 'player X'
    udid = "2bef0b11cff51cbb2e0730235f5664094b83b487"

    auth_token = Digest::MD5.hexdigest( [name,salt,udid].join(':') )

    score = HighScore.new( :player_name => name,
                          :iphone_udid => udid,
                          :total_score => 50,
                          :auth_token => auth_token)

    assert score.save, "Save failed: #{score.errors.full_messages}"

  end

  def test_should_not_validate_with_bad_auth_hash
    salt = "xxxxxxxxxxxx"
    name = 'player X'
    udid = "2bef0b11cff51cbb2e0730235f5664094b83b487"

    auth_token = Digest::MD5.hexdigest( [name,salt,udid].join(':') )

    score = HighScore.new( :player_name => name,
                          :iphone_udid => udid,
                          :total_score => 50,
                          :auth_token => auth_token)

    assert !score.save
    assert score.errors.on(:auth_token)
  end
end

test/functional/high_scores_controller_test.rb

require File.join(File.dirname(__FILE__), '/../test_helper')

class HighScoresControllerTest < ActionController::TestCase
  def setup
    @plist_expression = /<!DOCTYPE plist PUBLIC "-\/\/Apple Computer\/\/DTD PLIST 1\.0\/\/EN" "http:\/\/www\.apple\.com\/DTDs\/PropertyList-1\.0\.dtd">/
  end

  def test_should_get_index
    get :index, { :format => "xml", :limit => 10 }

    assert_response :success
    assert_not_nil assigns(:scores)
    assert_match @plist_expression, @response.body

    plist = Plist::parse_xml(@response.body)
    assert_equal "Array", plist.class.to_s
    assert_operator plist.size, ">", 0
  end

  def test_should_post_to_create
    salt = LeaderboardAuth::SECRET_SALT
    name = 'player X'
    udid = "2bef0b11cff51cbb2e0730235f5664094b83b487"

    auth_token = Digest::MD5.hexdigest( [name,salt,udid].join(':') )

    assert_difference('HighScore.count') do
      post :create, { :format => "xml", :high_score => {
                                          :player_name => name,
                                          :iphone_udid => udid,
                                          :total_score => 50,
                                          :auth_token => auth_token} }
    end

    assert_response :success
  end
end

Hosting the Service on Heroku

heroku is an excellent Rails host with fancy cloud architecture and a tiered pricing plan that starts at free. The best part about heroku, though, is how easy they’ve made Rails deployments. They did this by creating, essentially, a deployment gem that gives you some slick command line powers. Here’s how you deploy your app for the first time (ripped from the heroku front page) using the heroku gem, and of course, git.

$ sudo gem install heroku
$ heroku create myapp
$ git push heroku
$ heroku domains:add example.com
$ heroku rake db:migrate
$ heroku bundles:capture

You’ll get a basic subdomain at heroku.com which you’ll call from the iPhone. If your game takes off and traffic rises, heroku will scale seamlessly for you, and you’ll generally be well taken care of by a responsive support staff who know a great deal about the environment they host. Head over to their site for more information. (I’m in no way affiliated with them, just a happy user and impressed developer).


With the service built, and the iPhone posting and retrieving high scores, we’ve finally reached the end of the tutorial.

Good luck!

Bug combining acts_as_taggable_on tagged_with scopes

Posted September 8th, 2009 in Uncategorized by Travis

My latest Sogeo work is getting quite a lot of distance out of tagging. With mbleigh’s acts_as_taggable_on I’ve been able to conveniently normalize several different data dimensions down into a tag model.

Modeling this was easy enough, but today I ran into an issue when constructing some of the more sophisticated filtering queries.

acts_as_taggable_on utilizes named scope to provide helpful finder methods, like so:

Location.tagged_with("mytag", :on => :tags)

Easy enough to combine with existing scopes:

Location.include_details.sorted.tagged_with("mytag", :on => :tags)

The problem arises when combining two tagged_with scopes, for example:

Location.tagged_with("mytag", :on => :tags) .tagged_with("mycategory", :on => :categories)

You can read a brief discussion of the problems over on this lighthouse ticket, but the crux of the issue is malformed generated SQL.

Until acts_as_taggable_on is updated, a simple if dreadfully inefficient solution is to call the collection for each tagged_with filter, and then combine the results.

location_scope = Location.include_details.sorted
tag_scope = location_scope. tagged_with("mytag", :on => :tags)
category_scope = location_scope. tagged_with("mycategory", :on => :categories)
category_scope.all(conditions) & tag_scope.all(conditions)

This is a serviceable enough solution for me for now, but if this is still a problem in a few months when my app hits production then I’ll have to start working on a real fix for combining multiple tagged_with helper scopes.

Activity Logs and Friend Feeds on Rails & pfeed

Posted August 20th, 2009 in Uncategorized by Travis

Friend networks and activity feeds are mainstay features of social media applications, and designing an implementation that won’t scar your code with the complexities of bidirectional logic and messaging queues is never easy.

That should be enough to send the thoughtful developer looking for a giant’s shoulders to stand on before climbing the task themselves, but in the case of Rails, otherwise known for the fecundity of its plugin community, there is no compelling solution stack to raise us up. That is, not until Abhishek Parolkar released pfeed:

A rails plugin that allows you to create extensible log of activity

I’m going to walk through the process of setting up pfeed in your application. The sample code I’ll use is drawn from my own implementation of the plugin as well as pfeed’s github pages, including:

Although I’ll be expanding on the code samples, you’ll still want probably want to consult the original documentation at some point.

Requirements

In my case, there were several requirements driving plugin selection, as well as the rationale behind even seeking a plugin in the first place instead of building the functionality from scratch.

  1. Should configure logging, not manually call a log method
  2. Should store additional arbitrary log data in easily retrievable way
  3. Should be able to log to/for any model, not just “users” (i.e. log a service, or report TO a service)
  4. Should be able to differentiate between log types/categories
  5. Should scope log activity to user or user groups
  6. Can easily be globally disabled
  7. Should work across model associations to log “nested” activity
  8. Won’t clash on models with many preexisting Active Record callbacks

I’m looking for a lot of flexibility here. And while I mention “logging” quite often, there’s nothing about “friendships” or “friend feeds”. That’s because the “logging” and “feed” system would be serving my application in a number of ways, and what I needed was something that could perform generic logging tasks which I could then arbitrarily interpret for various user and system activity tracking scenarios. I could have taken inspiration from somewhere like Insoshi but I wanted a solution that didn’t carry the furbelow of social networking.

Installation & Setup

Install the plugin from git…

script/plugin install git://github.com/parolkar/pfeed.git
rake pfeed:setup

Installation will, among other things, add two tables to your database and install the Inflectionist plugin, which is used to add linguistic sugar to the feed messages.

   create_table :pfeed_items do |t|
     t.string  :type
     t.integer :originator_id
     t.string :originator_type
     t.integer :participant_id
     t.string :participant_type
     t.text   :data
     t.datetime :expiry
     t.timestamps
   end

   create_table :pfeed_deliveries do |t|
     t.integer :pfeed_receiver_id
     t.string :pfeed_receiver_type
     t.integer :pfeed_item_id
     t.timestamps
   end

After that, you’re ready to configure your models. For this example we just need some users, friendships, and some models on which to track user activity.

NOTE The majority of the following model code is devoted to managing friendships, but it is not necessary to follow this design in your own friend models. pfeed will group logs for you on any Active Record association; there is nothing special about friends. The following user model could just as well be receiving a feed from comments (since it’s just another association).

User Model

class User < ActiveRecord::Base
  has_many :comments, :dependent => :destroy
  has_many :friendships, :dependent => :destroy
  has_many :friends,
    :through => :friendships,
    :foreign_key => 'friend_id',
    :class_name => 'User' do
    def active
      find(:all, :conditions => ['completed_at IS NOT NULL'])
    end
    def pending
      find(:all, :conditions => ['completed_at IS NULL'])
    end

  def request_friendship_with(friend_id)
    friend = self.friendships.detect {|f| f.friend_id.to_s == friend_id.to_s }

    if friend.blank?
       friend = Friendship.new(:user_id => self.id, :friend_id => friend_id)
       return friend.save
    else
      return false
    end
  end

  def complete_friendship_with(friend_id)
    friend = self.friendships.detect {|f| f.friend_id.to_s == friend_id.to_s && f.completed_at.blank? }
    unless friend.blank?
      friend.touch(:completed_at)
      friendship = Friendship.find_by_user_id_and_friend_id(friend_id, self.id)
      friendship.touch(:completed_at)
      return friendship.id
    end

    return false
  end
end

Friendship Model

class Friendship < ActiveRecord::Base
  belongs_to :user
  belongs_to :friend, :class_name => 'User'

  validates_presence_of :user_id, :friend_id
  validates_numericality_of :user_id, :friend_id

  after_create :ensure_complementary_record_exists
  after_destroy :ensure_complementary_record_is_destroyed

  named_scope :include_users, :include => [:user, :friend]

  protected
  def find_complementary_record
    Friendship.find_by_user_id_and_friend_id(friend_id, user_id)
  end

  private
  def ensure_complementary_record_exists
    Friendship.create!(:user => friend, :friend => user) if find_complementary_record.blank?
  end

  def ensure_complementary_record_is_destroyed
    if complement = find_complementary_record
      complement.destroy
    end
  end
end

Comment Model

class Comment < ActiveRecord::Base
    belongs_to :user
    belongs_to :parent, :polymorphic => true, :counter_cache => true
end

Tables

create_table :friendships, :force => true do |t|
   t.integer :user_id, :null => false
   t.integer :friend_id, :null => false
   t.datetime :completed_at

   t.timestamps
 end

create_table :users, :force => true do |t|
   t.string :name
   t.string :password
end

create_table :comments, :force => true do |t|
  t.integer :user_id, :null => false
  t.integer :parent_id, :null => false
  t.string :parent_type, :limit => 64, :null => false
  t.string :body, :limit => 2048, :null => false
end

pfeed Configuration

The concept behind pfeed is simple. There are models which receive feeds, and those which emit them, and in some cases a model may do both. Integrating pfeed revolves around (1) configuring your models, (2) deciding which methods you want to log, and (3) adding any additional information onto the default data which pfeed collects.

Feed Receivers

To make a model a feed receiver, you add the receives_pfeed macro call somewhere near the end of the class so as to avoid association collisions.

class User < ActiveRecord::Base
   . . .
   receives_pfeed
end

When you register a model as a receiver, it gains a pfeed_inbox association, so that you can call, say, @user.pfeed_inbox to return a collection of feed items. This is the primary way you’ll retrieve feed information – through the context of a receiver. In this example it’s a User model, but it really could be any sort of “listener” model under which you need to scope your logs.

Feed Emitters

Registering the models to emit feed requires a little more customization, and you do it with another macro call, emits_pfeeds, to which you pass a hash of methods to act as log triggers, and a hash of models who will receive feeds of those logs. It looks like this:

class Comment < ActiveRecord::Base
    . . .
    emits_pfeeds :on => [:create] , :for => [:all_users]
    def all_users
      User.all
    end
end

The :to argument

This hash will accept any method on your model. Sometimes you’ll want these to simply be native Active Record operations, like :create or :update_attribute, but sometimes you’ll want to use your model’s own methods, which we’ll do in a second when logging user friendships triggered by the example User model’s complete_friendship_with.

The :for argument

The :for hash accepts a method for defining the feed’s “audience”, where audience is some Active Record model or collection. The plugin has two native methods for this, :itself and :all_in_its_class. They are simply defined:

def itself
  self
end

def all_in_its_class
  self.class.find :all
end

If you look back you’ll see that I defined an all_users method for the Comment model. Defining recipients for the feeds in such a way provides a lot of power and flexibility in filtering how and when your feeds are broadcast.

With the preceding code samples, we’d be logging every comment that was created, and permitting those logs to be retrieved by users.

Logging Friendships

Say we want to log whenever a user’s friend becomes friends with another user, and then show it on a feed like this: travis became friends with parlokar about 6 minutes ago.

First, since our user will be both receiving and emitting feeds, we add both pfeed macro calls to the model.

class User < ActiveRecord::Base
   . . .
   has_many :friends
   . . .
   emits_pfeeds :on => [:complete_friendship_with], :for => [:itself ,:all_in_its_class, :friends]
   receives_pfeed
end

There is a lot of wonderful black magic happening here, but essentially this code translates to say that feeds triggered by the complete_friendship_with method will be associated with the “friending” user, and that when such users appear in the :friends association of another user, that user can see these feed items.

Between a model’s ability to generate feeds for actions itself takes (e.g. complete_friendship_with) as well as actions it takes against other models (e.g. Comment.create), pfeed provides an enormous amount of flexibility in structuring your log system.

Feed Item Messages

This would be an excellent time to refer to the pfeed documentation on custom feeds. Basically, pfeed will automatically log enough data to create a readable feed message that can be reconstructed from the user association in combination with the triggering method’s name. Feed items will generally look like this:

Travis updated attribute Email about 1 minute ago
Travis created comment about 10 minutes ago
Travis completed friendship with about 30 minutes ago

The last item should draw your attention since it looks quite incomplete without, say, the friend’s name. pfeed has a sort of templating system for determining the data saved by a log, and if you wish to expand upon the defaults, you’ll have to create a feed model for each of the actions for which you want more data.

First, create a subdirectory named “pfeeds” in your application’s models directory. Then, you’ll create models for each of the methods you need to capture custom data from, inheriting from the PfeedItem model.

The directory structure will look like this:

– models
–– pfeeds
–––– comment_created.rb
–––– user_completed_friendship_with.rb

And the feed models will look like this:

class Pfeeds::UserCompletedFriendshipWith < PfeedItem
def  pack_data(method_name, method_name_in_past_tense, returned_result, *args_supplied_to_method, &block_supplied_to_method)
     self.data = {} if ! self.data

     friendship = Friendship.include_users.find(returned_result)

     hash_to_be_merged = {:friend => friendship.friend.name }
     self.data.merge!  hash_to_be_merged
     super
  end
end

In this example I want to capture the name of the friend with whom the logged user completed a friendship, so I define a field (“:friend” in self.data arguments) which will then be serialized into the data feed.

When you’re working with a feed item, you can then retrieve the custom data like so:

friend_name = @feed_item.data[:friend]

Summary

Now that we’ve covered each of the four corners of pfeed, so to speak, we can summarize integration as follows:

  1. Define feed receivers with receives_pfeed
  2. Define feed emitters with emits_pfeeds
  3. Define emits_pfeeds :for methods to scope logs
  4. Create PfeedItem models for capturing custom log data

I started this post with a list of requirements, and the various hooks pfeed gives you provide answers to all of them. The extent to which you configure and customize your logging system is up to you, and for me this was the real power of the plugin. Pfeed goes to the exact boundary of common logging functionality (the code I want to avoid writing myself if possible), and yet stops precisely before incurring design commitments that might clash with my application, leaving the right amount of “freedom with help” that is the hallmark of a valuable plugin.

So what are you waiting for? Go git it. . .

Rails: Param-Laden Conditional Finder Methods

Posted August 11th, 2009 in Uncategorized by Travis

ActiveRecord associations are magical, and if you embrace the sundry offerings of finder plugins the world of parameterized queries becomes a vast and wondrous landscape indeed. So how do Rails developers typically organize and build dynamic queries without drenching their code in switch blocks, multiplying filter methods on the model, or contriving vestigial routes for search- and report- like GETs?

My sense from reading various blog posts and git repos is that most people factor such logic out of the controller, which at least consolidates the complexity further upstream, and then resort to a variety of Ruby syntax and Rails convenience methods as required by the query. This is the approach I’ve always taken, and because I don’t think there is a better way to generalize the solution that isn’t already represented in the framework, I’m posting an example finder method as a sort of syntax reference.

What follows is a pretty typical case. This method is using searchlogic, geokit, and acts-as-taggable-on, as well as named_scope and some basic pagination. Ruby’s Hash is the real hero here and gives us most of the tools needed to keep such methods readable.

class << self
   def find_with_options(opt={})
   opt = opt.slice(:tags, :search, :sw_lat, :sw_lng, :ne_lat, :ne_lng, :limit, :offset)
   conditions = {}

   unless opt[:sw_lat].blank? || opt[:sw_lng].blank? || opt[:ne_lat].blank? || opt[:ne_lng].blank?
            sw_point = GeoKit::LatLng.new(opt[:sw_lat],opt[:sw_lng])
            ne_point = GeoKit::LatLng.new(opt[:ne_lat],opt[:ne_lng])
            conditions[:bounds] = [sw_point,ne_point]
   end

   conditions[:limit] = opt[:limit] ||= 50
   conditions[:offset] = opt[:offset] ||= 0

   scope = MyModel.include_an_associated_model
   scope = scope. associated_model _name_like(opt[:search]) unless opt[:search].blank?
   scope = scope.tagged_with(opt[:tags], :on => : tags) unless opt[:tags].blank?
   scope.all conditions
   end
end

Rails: polymorphic belongs_to associations with accepts_nested_attributes_for

Posted July 31st, 2009 in Uncategorized by Travis

If there is a combinatorial way to bring aspects of a framework together in a breaking edge case, I will find that way and take that step. It’s a destiny one follows gladly; an instinct useful in defining the boundaries of an application’s logical complexities as they accumulate on the brain like bit patterns. It makes you wade through limitations, leading to unexpectedly serviceable workarounds.

The problem for me, at least, is that I can’t turn the compulsion off. It sits right below my consciousness, urging the brain, when given a system, to seize upon the most elegant paradox it detects for solving the problem at hand. Take, for example, polymorphic belongs_to associations with accepts_nested_attributes_for in Rails. Any two of these keywords in combination will turn up tales in the SERPs of bewilderment and errors, but the effort to solve a data modeling problem brought these three elements together in a perfect storm that would punish me for my inspiration.

So I’ve one model, which then belongs_to another, polymorphic, model. The belongs_to model was the workhorse of the pair, and therefore needed to accepts_nested_attributes_for in order to initialize its counterpart as required.

class BelongsToModel < ActiveRecord::Base
   belongs_to :polymorph_model, :polymorphic =>true
   accepts_nested_attributes_for :polymorph_model
end

Unfortunately, what appears to happen when one puts this design into practice is that a NoMethodError is thrown whenever the belongs_to model receives the nested attributes hash, e.g.

params = {
   :name => “blah”,
   :parent_type => “polymorph_obj_class”,
   :polymorph_model_attributes => {
      :info => “blah”,
      :details => “blah”
   }
}

@belongsToModel = BelongsToModel.new(params)

In the example, polymorph_model_attributes is constructed with the attributes “info” and “details” because I personally have the domain knowledge to know that those attributes belong to the polymorph model and should be provided. To process nested attributes, however, Rails needs to know this also so it can use that knowledge to add an appropriate build method (called something like ‘build_polymorph_model ‘) to the belongs_to model, and initialize the nested object whenever the belongs_to model is initialized.

The paradox arises because Rails cannot dynamically create a “build_polymorph_model” method without first knowing the class of the polymorph. Even passing in a parent_type or equivalent attribute will not provide the model with the foreknowledge it needs to build this helper method.

Any alteration to my data model would be enough to escape the conflict, but this uncomfortable relationship with belongs_to and the need to instantiate my objects from the direction of the belongs_to model came about because it really does best represent the schema behind the scenes, and so I was loathe to surrender it to the apparent constraints.

The solution was as simple as possible. I took a literalist approach, and went ahead and defined the build_polymorph_model explicitly myself, adding in the appropriate logic to initialize any one of the several possible polymorphic models that could be associated.

class BelongsToModel < ActiveRecord::Base
   belongs_to :polymorph_model, :polymorphic =>true
   accepts_nested_attributes_for :polymorph_model

   def build_polymorph_model(params)
      # build any supported polymorphic relationships here
   end
end