Living on the Edge (of Rails) - 1st week of the year edition

Posted 10 months back at redemption in a blog

Yup, it’s time for your weekly dose of the changes on edge Rails, more or less covered in the forthcoming Rails Envy podcast. Using edge Rails is neither arcane nor terrifying, and hopefully weekly reports like these will allow you to take control of your own release schedule with your Rails apps.

This week’s report covers changes from 31 Dec 2007 to the day the podcast was recorded (6 Jan 2007).

Caching changes

Looks like most of the changes from the 2.1 caching branch have been
merged into the trunk. Some key points:

  1. memcache-client has been vendored (included in Rails directly). MemCacheStore works out of the box in Rails now, no need to install the memcache-client gem!
  2. The caching code has been refactored and moved into ActiveSupport (ActiveSupport::Cache::*).
  3. Added ActiveRecord::Base.cache_key to make it easier to cache Active Records in combination with the ActiveSupport cache libraries introduced in this changeset.
  4. Fragment cache keys are now by default prefixed with ‘views/’.
  5. Deprecation: ActionController::Base.fragment_cache_store is now ActionController::Base.cache_store

Fragment caching now works in RJS and Builder templates

Yup, you couldn’t do fragment caching in non-erb views before - now you can.

Freezing Rails now automatically updates your Rails app

If you’re using edge Rails and use the rails:freeze:edge rake task, you probably usually forget to run (or maybe you’re not even aware of) rake rails:update to update your Rails app with the latest config/, scripts/ and javascript files from the version of Rails you just froze to. On edge Rails, the rake rails:freeze:edge task runs the rails:update task for you. +1 for convenience!

I prefer to use Piston so I’m gonna have to keep remembering to run my rake rails:update now and then!

Check out the related changeset.

Optimizations

Only 1 optimization in the past week worth talking about: the ActiveRecord::Base#exists? method is faster. It now uses ActiveRecord::Base#select_all instead of a more expensive ActiveRecord::Base#find that unnecessarily instantiates AR objects. (Check out the related changeset.)

Bug fixes

Paginatin' Christmas

Posted 11 months back at Err the Blog

In a timely holiday manner, we present to you a short list of will_paginate resources. Please enjoy responsibly.

will paginate

Sightings

Since its inception, millions of people have paginated billions of records using will_paginate. A few notable examples:

Know a site that belongs on this list? Let us know in the comments.

New Features

Just in time for the holidays, Mislav has gifted us all with a bucket o' new features for will_paginate, mostly dealing with customizing the output. Take a look at his announcement and dive in.

Testing Your Views

For those of us constantly asking the view testing question, stern but fair will_paginate maintainer Mislav comes to the rescue with his aptly titled will_paginate and view testing article. It's in-depth, so be sure to check it out. Also: subscribe to his blog. Immediately.

Ajax Pagination

This Ajax thing is going to be huge! Get in on the action with Matt Aimonetti's Ajax Pagination in less than 5 minutes article. He'll teach you how to unobtrusively add Ajax behavior to will_paginate using Prototype, LowPro, and RJS.

If you're more of the jQuery type, check out the Ajax will_paginate, jq-style article over at ozmm.

The RailsCast

The prolific Ryan Bates has a screencast explaining the basics of WP. As always, it's to the point and very well done. And hey, you can even watch it on your iPod! Have a look.

will_paginate without ActiveRecord

There's only a few things I like more than websites with tildes in the URL. Like, say, twisting Rails plugins into non-Rails uses. Lucky for us, Erin Ptacek provides both in an entry titled Erin's Adventures with Rails: will_paginate without ActiveRecord. In it, you'll learn how to paginate a collection of OpenStruct objects. No ActiveRecord required. What a rush.

Ferret Integration

Brandon Keepers, that handsome devil, wrote a popular article detailing the steps necessary to paginate your Ferret search results using good ol' WP. If you're using Ferret, this is definitely the way to go.

Solr Integration

While I don't know what 'The Pug Automatic' means, and I certainly know nothing of the RoboPug in said blog's header, Henrik Nyh's article on paginating acts_as_solr with will_paginate is an undeniable must read for any Java lovin', Apache huggin' Solr user wanting to add a bit of style and flair into his app.

acts_as_taggable Integration

Perhaps as contentious as the comments and this thread suggest, Jim Morris' Paginating acts_as_taggable article offers a few ways to make both the on_steroids variant of acts_as_taggable plugin and will_paginate play nicely together.

Rails Plugins

A number of Rails plugins have been released with support for will_paginate lovingly baked in. Like a Santa shaped sugar cookie.

SpinBits' SimplySearchable helps you search in style while providing options to paginate using our friend WP.

UltraSphinx, Evan Weaver's preeminent Sphinx search engine plugin, longs to be installed alongside will_paginate.

While will_paginate, due to popular demand, plays nicely with scope_out, Nick Kallen's similar HasFinder works technically and conceptually well with everyone's favorite paginator.

And finally, how can we neglect to mention Will_Paginate_Search, which hooks into both WP and acts_as_indexed to the benefit of all involved parties.

The Bugtracker

Found a bug? Got an idea? Of course we'd love to hear it. Our Lighthouse tracker, which we love is the place for all of it.

The Google Group

Did you catch it above? Yep, you did. So observant: there's now a Google Group for will_paginate. Be sure to join up and chime in.

Nightly RDoc

Finally, RDoc is now generated nightly from the latest code in Subversion. Check it out at the Rock. It's like Christmas every day!

That's a wrap

Hey, where did 2007 go? I'm getting sucked into 2008 faster than Perl into obscurity.

Keep paginatin', Railers. See you next year.

jRails: jQuery On Rails

Posted 11 months back at Web 2.0 with Ruby on Rails

Using jRails, you can get all of the same default Rails helpers for javascript functionality using the lighter jQuery library.

jRails is a drop-in jQuery replacement for Prototype/script.aculo.us on Rails. It has the features and the visual effect.The visual effects in jRails are based on the new jquery-fx library. jRails currently uses a slightly modified version of jquery fx code to get some of the desired effects.

Features of jRails :

jRails provides drop-in functionality for these existing Rails methods.

    • Scriptaculous
    • draggable_element
    • drop_receiving_element
    • sortable_element
    • visual_effect
    • RJS
    • hide
    • insert_html
    • remove
    • replace
    • replace_html
    • show
    • toggle

How to use it?

Just install and go! Once installed, the previous Prototype/script.aculo.us helpers will be replaced by jQuery ones. In order for them to function correctly, just include the appropriate javascript files in the head of your page.

 

<script src="/javascripts/jquery.js" type="text/javascript"></script>
<script src="/javascripts/jquery-ui.js" type="text/javascript"></script>
<script src="/javascripts/jquery-fx.js" type="text/javascript"></script>
<script src="/javascripts/jrails.js" type="text/javascript"></script>

You can also use the Rails javascript_include_tag helper with :default to load them automagically.

 

<%= javascript_include_tag :defaults %>

Visit jRails home page to find out more!

jRails: jQuery On Rails

Posted 11 months back at Web 2.0 with Ruby on Rails

Using jRails, you can get all of the same default Rails helpers for javascript functionality using the lighter jQuery library.

jRails is a drop-in jQuery replacement for Prototype/script.aculo.us on Rails. It has the features and the visual effect.The visual effects in jRails are based on the new jquery-fx library. jRails currently uses a slightly modified version of jquery fx code to get some of the desired effects.

Features of jRails :

jRails provides drop-in functionality for these existing Rails methods.

    • Scriptaculous
    • draggable_element
    • drop_receiving_element
    • sortable_element
    • visual_effect
    • RJS
    • hide
    • insert_html
    • remove
    • replace
    • replace_html
    • show
    • toggle

How to use it?

Just install and go! Once installed, the previous Prototype/script.aculo.us helpers will be replaced by jQuery ones. In order for them to function correctly, just include the appropriate javascript files in the head of your page.

 

<script src="/javascripts/jquery.js" type="text/javascript"></script>
<script src="/javascripts/jquery-ui.js" type="text/javascript"></script>
<script src="/javascripts/jquery-fx.js" type="text/javascript"></script>
<script src="/javascripts/jrails.js" type="text/javascript"></script>

You can also use the Rails javascript_include_tag helper with :default to load them automagically.

 

<%= javascript_include_tag :defaults %>

Visit jRails home page to find out more!

Hacking In Article Voting in Mephisto

Posted 11 months back at RailsJitsu

In this article I am going to give you a primer on adding AJAX, custom routes and controllers to Mephisto to extend it’s functionality. For this example we will add digg/dzone type voting to articles, which means we will be hacking the Content model and forcing the CachedPage to expire.

In this article I am going to give you a primer on adding AJAX, custom routes and controllers to Mephisto to extend it’s functionality. For this example we will add digg/dzone type voting to articles, which means we will be hacking the Content model and forcing the CachedPage to expire.

To start off we need to add a custom controller to our Mephisto code to handle the AJAX based voting. We also need to create a model for the votes and a migration to hold the vote data.

1
2
3
./script/generate controller votes vote
./script/generate model ContentVote
./script/generate migration add_voting_record

We will start with our migration file by adding our table as well as the code for reverse migration. We also need columns to track the content_id, ip_address of the voter, and whether it was an up or down vote, which we handle with a boolean value.

1
2
3
4
5
6
7
8
9
10
11
12
13
class AddVotingRecord < ActiveRecord::Migration
  def self.up
    create_table :content_votes, :force => true do |t|
      t.integer         :content_id
      t.string          :ip_address
      t.boolean         :up
    end
  end

  def self.down
    drop_table :content_votes
  end
end

Next we to add the code to handle the vote record.

The code works like this; we add a one to many relationship to our ContentVote model, as every Content object can have many ContentVotes. The vote method takes a direction (up, or down) and an ip_address to prevent double voting. If the user has already voted we just short circuit. If not then we create a vote record, then set the boolean based direction and ip_address, then save our new vote record.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Content < ActiveRecord::Base
  filtered_column :body, :excerpt
  belongs_to :user, :with_deleted => true
  belongs_to :site
  [:year, :month, :day].each { |m| delegate m, :to => :published_at }
  
  # Content Voting
  has_many :content_votes
  def vote( direction, ip_address )
    return if self.content_votes.collect(&:ip_address).include?(ip_address)
    vote = self.content_votes.new
    vote.up = (direction == 'up') ? true : false
    vote.ip_address = ip_address
    vote.save
  end
  
end

Now we add the relationship to our new ContentVote model.

1
2
3
class ContentVote < ActiveRecord::Base
  belongs_to :content
end

On to our the controller that will be handling the AJAX call. We need to know what article the vote is for, and then call our vote method on the Content model. Then we need to expire the article page so that other people will see the new vote. The expiry part gave me some trouble, so let me elaborate how the expire_cached_pages method works.

The first parameter of the expire_cached_pages method is the controller making the request, this is our controller so we reference self. Next we pass a message to be logged, then we find all pages referenced by the article (as an article can live in multiple places simultaneously).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class VotesController < ApplicationController
  session :off

  def vote
    @article = Content.find params[:id]
    @article.vote( params[:direction], request.remote_ip )
  
    #expire article page
    site.expire_cached_pages( 
      self, 
      "Expired pages referenced by #{@article.class} ##{@article.id}", 
      site.cached_pages.find_by_reference(@article)
    )
  end
  
end

In order for us to be able to call a custom controller in Mephisto we need to add a route. In a plugin this is handled by Mephisto::Plugin.add_route, but we aren’t making a plugin just yet, so we have to patch it into the /lib/mephisto/routing.rb file, inside the self.connect_with(map) method which handles the Mephisto routing scheme. Also, since I am a big fan of resources we will do it that way, which also means we will add a :member action to delegate the call to and designate the request a :post.

Note the position of our added route, if you put it in the wrong place it won’t work. I like to put my custom routes just below the Plugin custom routes code.

1
2
3
4
5
6
# Custom voting route
map.resources :votes, :member => { :vote => :post }

# Original Mephisto code below here
map.dispatch '*path', :controller => 'mephisto', :action => 'dispatch'
map.home '', :controller => 'mephisto', :action => 'dispatch'

Of course, if we actually want our Liquid template to have access to the value of the votes we need to add it to the drop. In your /app/drops/article_drop.rb file you need to add this just above the protected methods:

1
2
3
4
5
6
7
8
  # VOTING
  def up_vote_count
    @up_vote_count ||= liquify(*@source.content_votes.inject(0) {|sum,vote| sum + ((vote.up) ? 1 : 0) })
  end
  
  def down_vote_count
    @down_vote_count ||= liquify(*@source.content_votes.inject(0) {|sum,vote| sum + ((!vote.up) ? 1 : 0) })
  end

We are almost done! The last thing we need to do is implement this AJAX call in our Liquid template, and the rjs code to change the vote numbers. This part will vary based on what theme you are using. I will give my code and you will have to figure out where in your theme to put it.

Also, since these themes use Liquid, we cannot call our rails helpers, so what I did was create a temp rails app, insert the link_to_remote call, render it in the browser and then copy out the produced AJAX code.

1
2
3
4
<span id="votes_up">{{ article.up_vote_count }}</span>
<a onclick="new Ajax.Request('/votes/{{article.id}}/vote?direction=up', {asynchronous:true, evalScripts:true}); return false;" href="#"><img src="/images/vote_up.gif"></a><br/>
<span id="votes_down">{{ article.down_vote_count }}</span>
 <a onclick="new Ajax.Request('/votes/{{article.id}}/vote?direction=down', {asynchronous:true, evalScripts:true}); return false;" href="#"><img src="/images/vote_down.gif"></a>

And our vote.rjs file (placed into the /app/views/votes/vote.rjs):

1
2
page.replace_html 'votes_up', @article.content_votes.inject(0) {|sum,vote| vote.up ? sum+1 : sum}
page.replace_html 'votes_down', @article.content_votes.inject(0) {|sum,vote| !vote.up ? sum+1 : sum}

And that is it! Run the migration, copy the up and down images to your theme folder, restart your mongrel processes, and you have content voting in Mephisto (this can be applied to comments as well with only changes to the _comment.liquid as comments and articles are the same base model)! I hope you enjoyed the ride, if you have trouble post in the comments and I will try and help.

A final note on customizing Mephisto in this way; if you do an svn up your code will get blasted into oblivion. So what to do? The way I handle this is to use a local Git repository. Git is amazing at doing code merges.

So I have 2 folders, one where the Mephisto trunk lives, and stays the way the core team wants it, and then a blog folder where my customizations live. When the core team updates trunk I do an svn up there, and then a Git merge to my blog directory.

So far this has never made me make a hand-merge, and it has never eaten my customizations.

Hacking In Article Voting in Mephisto

Posted 11 months back at RailsJitsu

In this article I am going to give you a primer on adding AJAX, custom routes and controllers to Mephisto to extend it’s functionality. For this example we will add digg/dzone type voting to articles, which means we will be hacking the Content model and forcing the CachedPage to expire.

In this article I am going to give you a primer on adding AJAX, custom routes and controllers to Mephisto to extend it’s functionality. For this example we will add digg/dzone type voting to articles, which means we will be hacking the Content model and forcing the CachedPage to expire.

To start off we need to add a custom controller to our Mephisto code to handle the AJAX based voting. We also need to create a model for the votes and a migration to hold the vote data.

1
2
3
./script/generate controller votes vote
./script/generate model ContentVote
./script/generate migration add_voting_record

We will start with our migration file by adding our table as well as the code for reverse migration. We also need columns to track the content_id, ip_address of the voter, and whether it was an up or down vote, which we handle with a boolean value.

1
2
3
4
5
6
7
8
9
10
11
12
13
class AddVotingRecord < ActiveRecord::Migration
  def self.up
    create_table :content_votes, :force => true do |t|
      t.integer         :content_id
      t.string          :ip_address
      t.boolean         :up
    end
  end

  def self.down
    drop_table :content_votes
  end
end

Next we to add the code to handle the vote record.

The code works like this; we add a one to many relationship to our ContentVote model, as every Content object can have many ContentVotes. The vote method takes a direction (up, or down) and an ip_address to prevent double voting. If the user has already voted we just short circuit. If not then we create a vote record, then set the boolean based direction and ip_address, then save our new vote record.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Content < ActiveRecord::Base
  filtered_column :body, :excerpt
  belongs_to :user, :with_deleted => true
  belongs_to :site
  [:year, :month, :day].each { |m| delegate m, :to => :published_at }
  
  # Content Voting
  has_many :content_votes
  def vote( direction, ip_address )
    return if self.content_votes.collect(&:ip_address).include?(ip_address)
    vote = self.content_votes.new
    vote.up = (direction == 'up') ? true : false
    vote.ip_address = ip_address
    vote.save
  end
  
end

Now we add the relationship to our new ContentVote model.

1
2
3
class ContentVote < ActiveRecord::Base
  belongs_to :content
end

On to our the controller that will be handling the AJAX call. We need to know what article the vote is for, and then call our vote method on the Content model. Then we need to expire the article page so that other people will see the new vote. The expiry part gave me some trouble, so let me elaborate how the expire_cached_pages method works.

The first parameter of the expire_cached_pages method is the controller making the request, this is our controller so we reference self. Next we pass a message to be logged, then we find all pages referenced by the article (as an article can live in multiple places simultaneously).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class VotesController < ApplicationController
  session :off

  def vote
    @article = Content.find params[:id]
    @article.vote( params[:direction], request.remote_ip )
  
    #expire article page
    site.expire_cached_pages( 
      self, 
      "Expired pages referenced by #{@article.class} ##{@article.id}", 
      site.cached_pages.find_by_reference(@article)
    )
  end
  
end

In order for us to be able to call a custom controller in Mephisto we need to add a route. In a plugin this is handled by Mephisto::Plugin.add_route, but we aren’t making a plugin just yet, so we have to patch it into the /lib/mephisto/routing.rb file, inside the self.connect_with(map) method which handles the Mephisto routing scheme. Also, since I am a big fan of resources we will do it that way, which also means we will add a :member action to delegate the call to and designate the request a :post.

Note the position of our added route, if you put it in the wrong place it won’t work. I like to put my custom routes just below the Plugin custom routes code.

1
2
3
4
5
6
# Custom voting route
map.resources :votes, :member => { :vote => :post }

# Original Mephisto code below here
map.dispatch '*path', :controller => 'mephisto', :action => 'dispatch'
map.home '', :controller => 'mephisto', :action => 'dispatch'

Of course, if we actually want our Liquid template to have access to the value of the votes we need to add it to the drop. In your /app/drops/article_drop.rb file you need to add this just above the protected methods:

1
2
3
4
5
6
7
8
  # VOTING
  def up_vote_count
    @up_vote_count ||= liquify(*@source.content_votes.inject(0) {|sum,vote| sum + ((vote.up) ? 1 : 0) })
  end
  
  def down_vote_count
    @down_vote_count ||= liquify(*@source.content_votes.inject(0) {|sum,vote| sum + ((!vote.up) ? 1 : 0) })
  end

We are almost done! The last thing we need to do is implement this AJAX call in our Liquid template, and the rjs code to change the vote numbers. This part will vary based on what theme you are using. I will give my code and you will have to figure out where in your theme to put it.

Also, since these themes use Liquid, we cannot call our rails helpers, so what I did was create a temp rails app, insert the link_to_remote call, render it in the browser and then copy out the produced AJAX code.

1
2
3
4
<span id="votes_up">{{ article.up_vote_count }}</span>
<a onclick="new Ajax.Request('/votes/{{article.id}}/vote?direction=up', {asynchronous:true, evalScripts:true}); return false;" href="#"><img src="/images/vote_up.gif"></a><br/>
<span id="votes_down">{{ article.down_vote_count }}</span>
 <a onclick="new Ajax.Request('/votes/{{article.id}}/vote?direction=down', {asynchronous:true, evalScripts:true}); return false;" href="#"><img src="/images/vote_down.gif"></a>

And our vote.rjs file (placed into the /app/views/votes/vote.rjs):

1
2
page.replace_html 'votes_up', @article.content_votes.inject(0) {|sum,vote| vote.up ? sum+1 : sum}
page.replace_html 'votes_down', @article.content_votes.inject(0) {|sum,vote| !vote.up ? sum+1 : sum}

And that is it! Run the migration, copy the up and down images to your theme folder, restart your mongrel processes, and you have content voting in Mephisto (this can be applied to comments as well with only changes to the _comment.liquid as comments and articles are the same base model)! I hope you enjoyed the ride, if you have trouble post in the comments and I will try and help.

A final note on customizing Mephisto in this way; if you do an svn up your code will get blasted into oblivion. So what to do? The way I handle this is to use a local Git repository. Git is amazing at doing code merges.

So I have 2 folders, one where the Mephisto trunk lives, and stays the way the core team wants it, and then a blog folder where my customizations live. When the core team updates trunk I do an svn up there, and then a Git merge to my blog directory.

So far this has never made me make a hand-merge, and it has never eaten my customizations.

Hacking In Article Voting in Mephisto

Posted 11 months back at RailsJitsu

In this article I am going to give you a primer on adding AJAX, custom routes and controllers to Mephisto to extend it’s functionality. For this example we will add digg/dzone type voting to articles, which means we will be hacking the Content model and forcing the CachedPage to expire.

In this article I am going to give you a primer on adding AJAX, custom routes and controllers to Mephisto to extend it’s functionality. For this example we will add digg/dzone type voting to articles, which means we will be hacking the Content model and forcing the CachedPage to expire.

To start off we need to add a custom controller to our Mephisto code to handle the AJAX based voting. We also need to create a model for the votes and a migration to hold the vote data.

1
2
3
./script/generate controller votes vote
./script/generate model ContentVote
./script/generate migration add_voting_record

We will start with our migration file by adding our table as well as the code for reverse migration. We also need columns to track the content_id, ip_address of the voter, and whether it was an up or down vote, which we handle with a boolean value.

1
2
3
4
5
6
7
8
9
10
11
12
13
class AddVotingRecord < ActiveRecord::Migration
  def self.up
    create_table :content_votes, :force => true do |t|
      t.integer         :content_id
      t.string          :ip_address
      t.boolean         :up
    end
  end

  def self.down
    drop_table :content_votes
  end
end

Next we to add the code to handle the vote record.

The code works like this; we add a one to many relationship to our ContentVote model, as every Content object can have many ContentVotes. The vote method takes a direction (up, or down) and an ip_address to prevent double voting. If the user has already voted we just short circuit. If not then we create a vote record, then set the boolean based direction and ip_address, then save our new vote record.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Content < ActiveRecord::Base
  filtered_column :body, :excerpt
  belongs_to :user, :with_deleted => true
  belongs_to :site
  [:year, :month, :day].each { |m| delegate m, :to => :published_at }
  
  # Content Voting
  has_many :content_votes
  def vote( direction, ip_address )
    return if self.content_votes.collect(&:ip_address).include?(ip_address)
    vote = self.content_votes.new
    vote.up = (direction == 'up') ? true : false
    vote.ip_address = ip_address
    vote.save
  end
  
end

Now we add the relationship to our new ContentVote model.

1
2
3
class ContentVote < ActiveRecord::Base
  belongs_to :content
end

On to our the controller that will be handling the AJAX call. We need to know what article the vote is for, and then call our vote method on the Content model. Then we need to expire the article page so that other people will see the new vote. The expiry part gave me some trouble, so let me elaborate how the expire_cached_pages method works.

The first parameter of the expire_cached_pages method is the controller making the request, this is our controller so we reference self. Next we pass a message to be logged, then we find all pages referenced by the article (as an article can live in multiple places simultaneously).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class VotesController < ApplicationController
  session :off

  def vote
    @article = Content.find params[:id]
    @article.vote( params[:direction], request.remote_ip )
  
    #expire article page
    site.expire_cached_pages( 
      self, 
      "Expired pages referenced by #{@article.class} ##{@article.id}", 
      site.cached_pages.find_by_reference(@article)
    )
  end
  
end

In order for us to be able to call a custom controller in Mephisto we need to add a route. In a plugin this is handled by Mephisto::Plugin.add_route, but we aren’t making a plugin just yet, so we have to patch it into the /lib/mephisto/routing.rb file, inside the self.connect_with(map) method which handles the Mephisto routing scheme. Also, since I am a big fan of resources we will do it that way, which also means we will add a :member action to delegate the call to and designate the request a :post.

Note the position of our added route, if you put it in the wrong place it won’t work. I like to put my custom routes just below the Plugin custom routes code.

1
2
3
4
5
6
# Custom voting route
map.resources :votes, :member => { :vote => :post }

# Original Mephisto code below here
map.dispatch '*path', :controller => 'mephisto', :action => 'dispatch'
map.home '', :controller => 'mephisto', :action => 'dispatch'

Of course, if we actually want our Liquid template to have access to the value of the votes we need to add it to the drop. In your /app/drops/article_drop.rb file you need to add this just above the protected methods:

1
2
3
4
5
6
7
8
  # VOTING
  def up_vote_count
    @up_vote_count ||= liquify(*@source.content_votes.inject(0) {|sum,vote| sum + ((vote.up) ? 1 : 0) })
  end
  
  def down_vote_count
    @down_vote_count ||= liquify(*@source.content_votes.inject(0) {|sum,vote| sum + ((!vote.up) ? 1 : 0) })
  end

We are almost done! The last thing we need to do is implement this AJAX call in our Liquid template, and the rjs code to change the vote numbers. This part will vary based on what theme you are using. I will give my code and you will have to figure out where in your theme to put it.

Also, since these themes use Liquid, we cannot call our rails helpers, so what I did was create a temp rails app, insert the link_to_remote call, render it in the browser and then copy out the produced AJAX code.

1
2
3
4
<span id="votes_up">{{ article.up_vote_count }}</span>
<a onclick="new Ajax.Request('/votes/{{article.id}}/vote?direction=up', {asynchronous:true, evalScripts:true}); return false;" href="#"><img src="/images/vote_up.gif"></a><br/>
<span id="votes_down">{{ article.down_vote_count }}</span>
 <a onclick="new Ajax.Request('/votes/{{article.id}}/vote?direction=down', {asynchronous:true, evalScripts:true}); return false;" href="#"><img src="/images/vote_down.gif"></a>

And our vote.rjs file (placed into the /app/views/votes/vote.rjs):

1
2
page.replace_html 'votes_up', @article.content_votes.inject(0) {|sum,vote| vote.up ? sum+1 : sum}
page.replace_html 'votes_down', @article.content_votes.inject(0) {|sum,vote| !vote.up ? sum+1 : sum}

And that is it! Run the migration, copy the up and down images to your theme folder, restart your mongrel processes, and you have content voting in Mephisto (this can be applied to comments as well with only changes to the _comment.liquid as comments and articles are the same base model)! I hope you enjoyed the ride, if you have trouble post in the comments and I will try and help.

A final note on customizing Mephisto in this way; if you do an svn up your code will get blasted into oblivion. So what to do? The way I handle this is to use a local Git repository. Git is amazing at doing code merges.

So I have 2 folders, one where the Mephisto trunk lives, and stays the way the core team wants it, and then a blog folder where my customizations live. When the core team updates trunk I do an svn up there, and then a Git merge to my blog directory.

So far this has never made me make a hand-merge, and it has never eaten my customizations.

Hacking In Article Voting in Mephisto

Posted 11 months back at RailsJitsu

In this article I am going to give you a primer on adding AJAX, custom routes and controllers to Mephisto to extend it’s functionality. For this example we will add digg/dzone type voting to articles, which means we will be hacking the Content model and forcing the CachedPage to expire.

To start off we need to add a custom controller to our Mephisto code to handle the AJAX based voting. We also need to create a model for the votes and a migration to hold the vote data.

1
2
3
./script/generate controller votes vote
./script/generate model ContentVote
./script/generate migration add_voting_record

We will start with our migration file by adding our table as well as the code for reverse migration. We also need columns to track the content_id, ip_address of the voter, and whether it was an up or down vote, which we handle with a boolean value.

1
2
3
4
5
6
7
8
9
10
11
12
13
class AddVotingRecord < ActiveRecord::Migration
  def self.up
    create_table :content_votes, :force => true do |t|
      t.integer         :content_id
      t.string          :ip_address
      t.boolean         :up
    end
  end

  def self.down
    drop_table :content_votes
  end
end

Next we to add the code to handle the vote record.

The code works like this; we add a one to many relationship to our ContentVote model, as every Content object can have many ContentVotes. The vote method takes a direction (up, or down) and an ip_address to prevent double voting. If the user has already voted we just short circuit. If not then we create a vote record, then set the boolean based direction and ip_address, then save our new vote record.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Content < ActiveRecord::Base
  filtered_column :body, :excerpt
  belongs_to :user, :with_deleted => true
  belongs_to :site
  [:year, :month, :day].each { |m| delegate m, :to => :published_at }
  
  # Content Voting
  has_many :content_votes
  def vote( direction, ip_address )
    return if self.content_votes.collect(&:ip_address).include?(ip_address)
    vote = self.content_votes.new
    vote.up = (direction == 'up') ? true : false
    vote.ip_address = ip_address
    vote.save
  end
  
end

Now we add the relationship to our new ContentVote model.

1
2
3
class ContentVote < ActiveRecord::Base
  belongs_to :content
end

On to our the controller that will be handling the AJAX call. We need to know what article the vote is for, and then call our vote method on the Content model. Then we need to expire the article page so that other people will see the new vote. The expiry part gave me some trouble, so let me elaborate how the expire_cached_pages method works.

The first parameter of the expire_cached_pages method is the controller making the request, this is our controller so we reference self. Next we pass a message to be logged, then we find all pages referenced by the article (as an article can live in multiple places simultaneously).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class VotesController < ApplicationController
  session :off

  def vote
    @article = Content.find params[:id]
    @article.vote( params[:direction], request.remote_ip )
  
    #expire article page
    site.expire_cached_pages( 
      self, 
      "Expired pages referenced by #{@article.class} ##{@article.id}", 
      site.cached_pages.find_by_reference(@article)
    )
  end
  
end

In order for us to be able to call a custom controller in Mephisto we need to add a route. In a plugin this is handled by Mephisto::Plugin.add_route, but we aren’t making a plugin just yet, so we have to patch it into the /lib/mephisto/routing.rb file, inside the self.connect_with(map) method which handles the Mephisto routing scheme. Also, since I am a big fan of resources we will do it that way, which also means we will add a :member action to delegate the call to and designate the request a :post.

Note the position of our added route, if you put it in the wrong place it won’t work. I like to put my custom routes just below the Plugin custom routes code.

1
2
3
4
5
6
# Custom voting route
map.resources :votes, :member => { :vote => :post }

# Original Mephisto code below here
map.dispatch '*path', :controller => 'mephisto', :action => 'dispatch'
map.home '', :controller => 'mephisto', :action => 'dispatch'

Of course, if we actually want our Liquid template to have access to the value of the votes we need to add it to the drop. In your /app/drops/article_drop.rb file you need to add this just above the protected methods:

1
2
3
4
5
6
7
8
  # VOTING
  def up_vote_count
    @up_vote_count ||= liquify(*@source.content_votes.inject(0) {|sum,vote| sum + ((vote.up) ? 1 : 0) })
  end
  
  def down_vote_count
    @down_vote_count ||= liquify(*@source.content_votes.inject(0) {|sum,vote| sum + ((!vote.up) ? 1 : 0) })
  end

We are almost done! The last thing we need to do is implement this AJAX call in our Liquid template, and the rjs code to change the vote numbers. This part will vary based on what theme you are using. I will give my code and you will have to figure out where in your theme to put it.

Also, since these themes use Liquid, we cannot call our rails helpers, so what I did was create a temp rails app, insert the link_to_remote call, render it in the browser and then copy out the produced AJAX code.

1
2
3
4
<span id="votes_up">{{ article.up_vote_count }}</span>
<a onclick="new Ajax.Request('/votes/{{article.id}}/vote?direction=up', {asynchronous:true, evalScripts:true}); return false;" href="#"><img src="/images/vote_up.gif"></a><br/>
<span id="votes_down">{{ article.down_vote_count }}</span>
 <a onclick="new Ajax.Request('/votes/{{article.id}}/vote?direction=down', {asynchronous:true, evalScripts:true}); return false;" href="#"><img src="/images/vote_down.gif"></a>

And our vote.rjs file (placed into the /app/views/votes/vote.rjs):

1
2
page.replace_html 'votes_up', @article.content_votes.inject(0) {|sum,vote| vote.up ? sum+1 : sum}
page.replace_html 'votes_down', @article.content_votes.inject(0) {|sum,vote| !vote.up ? sum+1 : sum}

And that is it! Run the migration, copy the up and down images to your theme folder, restart your mongrel processes, and you have content voting in Mephisto (this can be applied to comments as well with only changes to the _comment.liquid as comments and articles are the same base model)! I hope you enjoyed the ride, if you have trouble post in the comments and I will try and help.

A final note on customizing Mephisto in this way; if you do an svn up your code will get blasted into oblivion. So what to do? The way I handle this is to use a local Git repository. Git is amazing at doing code merges.

So I have 2 folders, one where the Mephisto trunk lives, and stays the way the core team wants it, and then a blog folder where my customizations live. When the core team updates trunk I do an svn up there, and then a Git merge to my blog directory.

So far this has never made me make a hand-merge, and it has never eaten my customizations.

Hacking In Article Voting in Mephisto

Posted 11 months back at RailsJitsu

In this article I am going to give you a primer on adding AJAX, custom routes and controllers to Mephisto to extend it’s functionality. For this example we will add digg/dzone type voting to articles, which means we will be hacking the Content model and forcing the CachedPage to expire.

In this article I am going to give you a primer on adding AJAX, custom routes and controllers to Mephisto to extend it’s functionality. For this example we will add digg/dzone type voting to articles, which means we will be hacking the Content model and forcing the CachedPage to expire.

To start off we need to add a custom controller to our Mephisto code to handle the AJAX based voting. We also need to create a model for the votes and a migration to hold the vote data.

1
2
3
./script/generate controller votes vote
./script/generate model ContentVote
./script/generate migration add_voting_record

We will start with our migration file by adding our table as well as the code for reverse migration. We also need columns to track the content_id, ip_address of the voter, and whether it was an up or down vote, which we handle with a boolean value.

1
2
3
4
5
6
7
8
9
10
11
12
13
class AddVotingRecord < ActiveRecord::Migration
  def self.up
    create_table :content_votes, :force => true do |t|
      t.integer         :content_id
      t.string          :ip_address
      t.boolean         :up
    end
  end

  def self.down
    drop_table :content_votes
  end
end

Next we to add the code to handle the vote record.

The code works like this; we add a one to many relationship to our ContentVote model, as every Content object can have many ContentVotes. The vote method takes a direction (up, or down) and an ip_address to prevent double voting. If the user has already voted we just short circuit. If not then we create a vote record, then set the boolean based direction and ip_address, then save our new vote record.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Content < ActiveRecord::Base
  filtered_column :body, :excerpt
  belongs_to :user, :with_deleted => true
  belongs_to :site
  [:year, :month, :day].each { |m| delegate m, :to => :published_at }
  
  # Content Voting
  has_many :content_votes
  def vote( direction, ip_address )
    return if self.content_votes.collect(&:ip_address).include?(ip_address)
    vote = self.content_votes.new
    vote.up = (direction == 'up') ? true : false
    vote.ip_address = ip_address
    vote.save
  end
  
end

Now we add the relationship to our new ContentVote model.

1
2
3
class ContentVote < ActiveRecord::Base
  belongs_to :content
end

On to our the controller that will be handling the AJAX call. We need to know what article the vote is for, and then call our vote method on the Content model. Then we need to expire the article page so that other people will see the new vote. The expiry part gave me some trouble, so let me elaborate how the expire_cached_pages method works.

The first parameter of the expire_cached_pages method is the controller making the request, this is our controller so we reference self. Next we pass a message to be logged, then we find all pages referenced by the article (as an article can live in multiple places simultaneously).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class VotesController < ApplicationController
  session :off

  def vote
    @article = Content.find params[:id]
    @article.vote( params[:direction], request.remote_ip )
  
    #expire article page
    site.expire_cached_pages( 
      self, 
      "Expired pages referenced by #{@article.class} ##{@article.id}", 
      site.cached_pages.find_by_reference(@article)
    )
  end
  
end

In order for us to be able to call a custom controller in Mephisto we need to add a route. In a plugin this is handled by Mephisto::Plugin.add_route, but we aren’t making a plugin just yet, so we have to patch it into the /lib/mephisto/routing.rb file, inside the self.connect_with(map) method which handles the Mephisto routing scheme. Also, since I am a big fan of resources we will do it that way, which also means we will add a :member action to delegate the call to and designate the request a :post.

Note the position of our added route, if you put it in the wrong place it won’t work. I like to put my custom routes just below the Plugin custom routes code.

1
2
3
4
5
6
# Custom voting route
map.resources :votes, :member => { :vote => :post }

# Original Mephisto code below here
map.dispatch '*path', :controller => 'mephisto', :action => 'dispatch'
map.home '', :controller => 'mephisto', :action => 'dispatch'

Of course, if we actually want our Liquid template to have access to the value of the votes we need to add it to the drop. In your /app/drops/article_drop.rb file you need to add this just above the protected methods:

1
2
3
4
5
6
7
8
  # VOTING
  def up_vote_count
    @up_vote_count ||= liquify(*@source.content_votes.inject(0) {|sum,vote| sum + ((vote.up) ? 1 : 0) })
  end
  
  def down_vote_count
    @down_vote_count ||= liquify(*@source.content_votes.inject(0) {|sum,vote| sum + ((!vote.up) ? 1 : 0) })
  end

We are almost done! The last thing we need to do is implement this AJAX call in our Liquid template, and the rjs code to change the vote numbers. This part will vary based on what theme you are using. I will give my code and you will have to figure out where in your theme to put it.

Also, since these themes use Liquid, we cannot call our rails helpers, so what I did was create a temp rails app, insert the link_to_remote call, render it in the browser and then copy out the produced AJAX code.

1
2
3
4
<span id="votes_up">{{ article.up_vote_count }}</span>
<a onclick="new Ajax.Request('/votes/{{article.id}}/vote?direction=up', {asynchronous:true, evalScripts:true}); return false;" href="#"><img src="/images/vote_up.gif"></a><br/>
<span id="votes_down">{{ article.down_vote_count }}</span>
 <a onclick="new Ajax.Request('/votes/{{article.id}}/vote?direction=down', {asynchronous:true, evalScripts:true}); return false;" href="#"><img src="/images/vote_down.gif"></a>

And our vote.rjs file (placed into the /app/views/votes/vote.rjs):

1
2
page.replace_html 'votes_up', @article.content_votes.inject(0) {|sum,vote| vote.up ? sum+1 : sum}
page.replace_html 'votes_down', @article.content_votes.inject(0) {|sum,vote| !vote.up ? sum+1 : sum}

And that is it! Run the migration, copy the up and down images to your theme folder, restart your mongrel processes, and you have content voting in Mephisto (this can be applied to comments as well with only changes to the _comment.liquid as comments and articles are the same base model)! I hope you enjoyed the ride, if you have trouble post in the comments and I will try and help.

A final note on customizing Mephisto in this way; if you do an svn up your code will get blasted into oblivion. So what to do? The way I handle this is to use a local Git repository. Git is amazing at doing code merges.

So I have 2 folders, one where the Mephisto trunk lives, and stays the way the core team wants it, and then a blog folder where my customizations live. When the core team updates trunk I do an svn up there, and then a Git merge to my blog directory.

So far this has never made me make a hand-merge, and it has never eaten my customizations.

RSpec and Inline RJS

Posted 12 months back at Jonathan.inspect

Rails give us the ability to write inline RJS via render :update syntax, as in :

render :update do |page|
  page['addressPreviewStatus'].update 'Address Not Found'
end

Previous code will update the content of tag with “addressPreviewStatus” id with ‘Address Not Found’.

But how can we spec that out ? I needed to search a little as there seems to be very little examples.

First in rails there is a special assertion assert_select_rjs, merged from the assert_select plugin, it let you test your RJS with a syntax similar to RJS itself. RSpecOnRails has a special matcher wrapping assert_select_rjs : has_rjs.

You can use it on response to specify what should be generated, for exemple you can :

# Specify response should contains an update or insert of some kind
response.should have_rjs

# Specify response should contains an update or insert for the tag with given id
response.should have_rjs('id')

# Specify response should contains a specific update, insert, etc. for the given tag
response.should have_rjs(:replace, 'id')

You get the point. Now, a nice syntax allows you to write RJS this way :

render :update do |page|
  page['id'].update('replacement text')
end

So my first try was to use :

response.should have_rjs(:update, 'id', 'replacement text')

but this fail miserably with an error Unknown RJS statement type update. I tried different syntax but none worked.

Finally browsing through source of assert_select_rjs I found what I was looking for. When using this syntax you should use one of :chained_replace or :chained_replace_html depending what you want to test :replace or :update.

Now here is the solution :

response.should have_rjs(:chained_replace_html, 'id', 'replacement text')


1 2 3 4 5 ... 7