Making CRUD less "Cruddy", one step at a time »
Created at: 28.04.2010 03:42, source: OnRails.org, tagged: Ruby on Rails Rails Tips
One of the great “new” features of Rails (as of 2.3) is accepts_nested_attributes_for, allowing you to build cross-model CRUD forms without “cruddying” your controller. There are some great examples out there about how to do this, but I’d like to walk thorough a particular use case — managing the “join” records in a has_many :through relationship.
Consider the following database schema:
class Villain < ActiveRecord::Base
has_many :gifts
has_many :super_powers, :through => :gifts
end
class Gift < ActiveRecord::Base
belongs_to :villain
belongs_to :super_power
validates_uniqueness_of :super_power, :scope => :villain_id
end
class SuperPower < ActiveRecord::Base
has_many :gifts
has_many :villains, :through => :gifts
endIn our dataset, there are a relatively small number of super powers which we wish to present as a list of checkboxes on the villain management form. Checking/unchecking the boxes will manage the gift records for that villain, effectively managing the list of super powers available to the baddy.
To get started, we need to add accepts_nested_attributes_for :gifts to the Villain class — piece of cake. To complete the implementation, we need to change the params hash that our form generates. Let’s review the cases that we need to support and the associated params hash format needed to implement the correct functionality.
The first case is a super power record that is not currently associated with the villain. Here, the UI should display an unchecked checkbox. If we check it and submit the form, a gift record should be created linking the villain with the super power, making this bad guy that much badder. Here is an example of the params hash we should be sending to accomplish this:
{
'villain' => {
'name' => 'Lex Luthor',
...
'gifts_attributes' => {
1 => { 'super_power_id' => 5 },
2 => { 'super_power_id' => 7 },
...
}
}
}The alternate case is a super power this villain already possesses. In this instance, the UI should display a checked checkbox, and if we uncheck it, the existing gift record should be deleted, diminishing the villain’s capacity for evil. And our params hash needs to look like:
{
'villain' => {
'name' => 'Two-Face',
...
'gifts_attributes' => {
1 => { 'id' => 101, '_delete' => true },
...
}
}
}Note that the keys for the gifts_attributes hash are arbitrary; we can use any scheme to generate unique keys for the hash.
So how can we craft a form that sends the params hash that Rails wants to see? Here’s my implementation:
<%- SuperPower.all.each_with_index do |super_power, index| -%>
<label>
<%- if gift = @villain.gifts.find_by_super_power_id(super_power.id) -%>
<%= hidden_field_tag "villain[gifts_attributes][#{ index }][id]", gift.id %>
<%= check_box_tag "villain[gifts_attributes][#{ index }][_delete]", false, true %>
<%= hidden_field_tag "villain[gifts_attributes][#{ index }][_delete]", true %>
<%- else -%>
<%= check_box_tag "villain[gifts_attributes][#{ index }][super_power_id]", super_power.id %>
<%- end -%>
<%= super_power.name %>
</label><br />
<%- end -%>If the gift is detected, the villain has the super power, and we handle our second case from above, using the checkbox / hidden field hack Rails employs in the check_box helper method to make sure a value is sent whether or not the checkbox is checked. The else block handles the other case, setting up our params hash to create the gift if the checkbox is checked.
This works, but we probably don’t want to copy and paste that code everywhere we use this pattern. How can we reuse this in a DRY fashion? Here’s a helper method that encapsulates the logic:
def has_join_relationship(model, join_collection_name, related_item, collection_index, options={})
returning "" do |output|
relationship_name = options[:relationship_name] || related_item.class.table_name.singularize + "_id"
tag_prefix = "#{ model.class.class_name.underscore }[#{ join_collection_name }_attributes][#{ collection_index }]"
if join_item = model.send(join_collection_name).find(:first, :conditions => { relationship_name => related_item.id })
output << hidden_field_tag("#{ tag_prefix }[id]", related_item.id)
output << check_box_tag("#{ tag_prefix }[_delete]", false, true)
output << hidden_field_tag("#{ tag_prefix }[_delete]", true)
else
output << check_box_tag("#{ tag_prefix }[#{ relationship_name }]", related_item.id, false)
end
end
endDrop that in a helper, and then your form code becomes:
<%- SuperPower.all.each_with_index do |super_power, index| -%>
<label>
<%= has_join_relationship(@villain, :gifts, super_power, index) %>
<%= super_power.name %>
</label><br />
<%- end -%>Much nicer … although I’m not sold on the name has_join_relationship. Any suggestions?
more »
Making CRUD less "Cruddy", one step at a time »
Created at: 27.04.2010 20:42, source: OnRails.org, tagged: Rails Tips Ruby on Rails
One of the great “new” features of Rails (as of 2.3) is accepts_nested_attributes_for, allowing you to build cross-model CRUD forms without “cruddying” your controller. There are some great examples out there about how to do this, but I’d like to walk thorough a particular use case—managing the “join” records in a has_many :through relationship.
Consider the following database schema:
class Villain < ActiveRecord::Base
has_many :gifts
has_many :super_powers, :through => :gifts
end
class Gift < ActiveRecord::Base
belongs_to :villain
belongs_to :super_power
validates_uniqueness_of :super_power, :scope => :villain_id
end
class SuperPower < ActiveRecord::Base
has_many :gifts
has_many :villains, :through => :gifts
endIn our dataset, there are a relatively small number of super powers which we wish to present as a list of checkboxes on the villain management form. Checking/unchecking the boxes will manage the gift records for that villain, effectively managing the list of super powers available to the baddy.
To get started, we need to add accepts_nested_attributes_for :gifts to the Villain class—piece of cake. To complete the implementation, we need to change the params hash that our form generates. Let’s review the cases that we need to support and the associated params hash format needed to implement the correct functionality.
The first case is a super power record that is not currently associated with the villain. Here, the UI should display an unchecked checkbox. If we check it and submit the form, a gift record should be created linking the villain with the super power, making this bad guy that much badder. Here is an example of the params hash we should be sending to accomplish this:
{
'villain' => {
'name' => 'Lex Luthor',
...
'gifts_attributes' => {
1 => { 'super_power_id' => 5 },
2 => { 'super_power_id' => 7 },
...
}
}
}The alternate case is a super power this villain already possesses. In this instance, the UI should display a checked checkbox, and if we uncheck it, the existing gift record should be deleted, diminishing the villain’s capacity for evil. And our params hash needs to look like:
{
'villain' => {
'name' => 'Two-Face',
...
'gifts_attributes' => {
1 => { 'id' => 101, '_delete' => true },
...
}
}
}Note that the keys for the gifts_attributes hash are arbitrary; we can use any scheme to generate unique keys for the hash.
So how can we craft a form that sends the params hash that Rails wants to see? Here’s my implementation:
<%- SuperPower.all.each_with_index do |super_power, index| -%>
<label>
<%- if gift = @villain.gifts.find_by_super_power_id(super_power.id) -%>
<%= hidden_field_tag "villain[gifts_attributes][#{ index }][id]", gift.id %>
<%= check_box_tag "villain[gifts_attributes][#{ index }][_delete]", false, true %>
<%= hidden_field_tag "villain[gifts_attributes][#{ index }][_delete]", true %>
<%- else -%>
<%= check_box_tag "villain[gifts_attributes][#{ index }][super_power_id]", super_power.id %>
<%- end -%>
<%= super_power.name %>
</label><br />
<%- end -%>If the gift is detected, the villain has the super power, and we handle our second case from above, using the checkbox / hidden field hack Rails employs in the check_box helper method to make sure a value is sent whether or not the checkbox is checked. The else block handles the other case, setting up our params hash to create the gift if the checkbox is checked.
This works, but we probably don’t want to copy and paste that code everywhere we use this pattern. How can we reuse this in a DRY fashion? Here’s a helper method that encapsulates the logic:
def has_join_relationship(model, join_collection_name, related_item, collection_index, options={})
returning "" do |output|
relationship_name = options[:relationship_name] || related_item.class.table_name.singularize + "_id"
tag_prefix = "#{ model.class.class_name.underscore }[#{ join_collection_name }_attributes][#{ collection_index }]"
if join_item = model.send(join_collection_name).find(:first, :conditions => { relationship_name => related_item.id })
output << hidden_field_tag("#{ tag_prefix }[id]", related_item.id)
output << check_box_tag("#{ tag_prefix }[_delete]", false, true)
output << hidden_field_tag("#{ tag_prefix }[_delete]", true)
else
output << check_box_tag("#{ tag_prefix }[#{ relationship_name }]", related_item.id, false)
end
end
endDrop that in a helper, and then your form code becomes:
<%- SuperPower.all.each_with_index do |super_power, index| -%>
<label>
<%= has_join_relationship(@villain, :gifts, super_power, index) %>
<%= super_power.name %>
</label><br />
<%- end -%>Much nicer … although I’m not sold on the name has_join_relationship. Any suggestions?
more »
Cucumber, meet Routes »
Created at: 07.04.2010 07:13, source: OnRails.org, tagged: Ruby on Rails Rails Tips cucumber
I’ve been loving Rails BDD with Cucumber for the past year or so — it helps me focus on the next required step to build a feature in my application, and better focus equals better development velocity. However, one thing I found tedious in starting with Cucumber was defining route matchers in paths.rb.
The stock path_to method is implemented as a case statement, allowing you to add a case for each path you want to recognize. This works, but leaves you feeling a bit un-DRY since you’re basically duplicating information in your routes.rb file.
Here’s a quick hack to paths.rb that lets you leverage your existing routes:
change
else
raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
"Now, go and add a mapping in #{__FILE__}"
endto
else
begin
page_name =~ /the (.*) page/
path_components = $1.split(/\s+/)
self.send(path_components.push('path').join('_').to_sym)
rescue Object => e
raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
"Now, go and add a mapping in #{__FILE__}"
end
endIn your Cucumber steps, you can now use any named route that does not require a parameter. For example, users_path would become “the users page”, and new_product_path would become “the new product page”. As you add new resources, at least the index and new options should work out of the box — no further edits to paths.rb required!
UPDATE:
w00t!
This is now baked into cucumber-rails!
more »
RMagick (from source) on Snow Leopard »
Created at: 04.09.2009 03:31, source: OnRails.org, tagged: ruby Rails Tips Ruby on Rails Source leopard snow rmagick
After the release of 10.5, I published an article about building RMagick from source on Leopard. I won’t rehash the why, you can read the original article for that. My clean install necessitated updating the RMagick script, so here’s what worked for me to install from source on Snow Leopard! For the impatient, here’s the download link: rmagick-build.sh
First, we start with installing wget, as it seems to be a bit more clever than curl about dealing with mirrors, etc. Then, we compile and install each prerequisite package. Finally, we install the gem.
All the links in the script worked for me, but, depending on your location, network, conditions, etc, your mileage may vary. Enjoy!
#!/bin/sh
# install wget, which is cleverer than curl
curl -O http://ftp.gnu.org/gnu/wget/wget-1.11.tar.gz
tar zxvf wget-1.11.tar.gz
cd wget-1.11
./configure --prefix=/usr/local
make
sudo make install
cd /usr/local/src
# prerequisite packages
wget http://nongnu.askapache.com/freetype/freetype-2.3.9.tar.gz
tar zxvf freetype-2.3.9.tar.gz
cd freetype-2.3.9
./configure --prefix=/usr/local
make
sudo make install
cd /usr/local/src
wget http://superb-west.dl.sourceforge.net/sourceforge/libpng/libpng-1.2.39.tar.gz
tar zxvf libpng-1.2.39.tar.gz
cd libpng-1.2.39
./configure --prefix=/usr/local
make
sudo make install
cd /usr/local/src
wget ftp://ftp.uu.net/graphics/jpeg/jpegsrc.v6b.tar.gz
tar xzvf jpegsrc.v6b.tar.gz
cd jpeg-6b
ln -s `which glibtool` ./libtool
export MACOSX_DEPLOYMENT_TARGET=10.6
./configure --enable-shared --prefix=/usr/local
make
sudo make install
cd /usr/local/src
wget ftp://ftp.remotesensing.org/libtiff/tiff-3.9.1.tar.gz
tar xzvf tiff-3.9.1.tar.gz
cd tiff-3.9.1
./configure --prefix=/usr/local
make
sudo make install
cd /usr/local/src
wget http://superb-west.dl.sourceforge.net/sourceforge/wvware/libwmf-0.2.8.4.tar.gz
tar xzvf libwmf-0.2.8.4.tar.gz
cd libwmf-0.2.8.4
make clean
./configure
make
sudo make install
cd /usr/local/src
wget http://www.littlecms.com/lcms-1.17.tar.gz
tar xzvf lcms-1.17.tar.gz
cd lcms-1.17
make clean
./configure
make
sudo make install
cd /usr/local/src
wget ftp://mirror.cs.wisc.edu/pub/mirrors/ghost/GPL/gs870/ghostscript-8.70.tar.gz
tar zxvf ghostscript-8.70.tar.gz
cd ghostscript-8.70
./configure --prefix=/usr/local
make
sudo make install
cd /usr/local/src
wget ftp://mirror.cs.wisc.edu/pub/mirrors/ghost/GPL/gs860/ghostscript-fonts-std-8.11.tar.gz
tar zxvf ghostscript-fonts-std-8.11.tar.gz
sudo mv fonts /usr/local/share/ghostscript
# Image Magick
wget ftp://ftp.fifi.org/pub/ImageMagick/ImageMagick.tar.gz
tar xzvf ImageMagick.tar.gz
cd `ls | grep ImageMagick-`
export CPPFLAGS=-I/usr/local/include
export LDFLAGS=-L/usr/local/lib
./configure --prefix=/usr/local --disable-static --with-modules --without-perl --without-magick-plus-plus --with-quantum-depth=8 --with-gs-font-dir=/usr/local/share/ghostscript/fonts --disable-openmp
make
sudo make install
cd /usr/local/src
# RMagick
sudo gem install rmagickUPDATE There is a bug with libgomp that breaks the convert utility (See comments below). the --disable-openmp configure option has been added to the script to fix this.
UPDATE 2 A new patchlevel of ImageMagick has been released that supersedes the original one referenced in this script, and the original has been removed from the server. Thanks to Sebastian for this update that will grab the latest release.
more »
