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:
- 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!
- The caching code has been refactored and moved into ActiveSupport (
ActiveSupport::Cache::*).
- Added
ActiveRecord::Base.cache_key to make it easier to cache Active Records in combination with the ActiveSupport cache libraries introduced in this changeset.
- Fragment cache keys are now by default prefixed with ‘views/’.
- 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
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.
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.
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!
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!

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.
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.
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.
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.
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.
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