Sunday, 6 November 2011

Bits of Code #2 - Ordering records through index page with AJAX (powered by jQuery) in a Rails 3.1 App

In one of the projects that I worked in I had to make it possible for users to easily change the order in which the records of a table would appear in their index page. I knew that there were a few gems out there that could handle this for me, but I thought that it was such a simple functionality that it was probably not a strong enough reason to add another dependency to the project; so I went about building it.

In this example I've added this functionality to a Product model. I started by creating a migration to add the column that would keep the order between products, I decided to call this column sort_index.

#PATH: db/migrate/
class AddSortIndexToProducts < ActiveRecord::Migration
def self.up
add_column :products, :sort_index, :integer
end
def self.down
remove_column :products, :sort_index
end
end

Then I added a few methods to the Product model. Some methods to check the position of a product, others to move the product up and down in the 'ranking' of products, and a before_create callback to set the initial sort_index of each new product. Note that the upper position in my implementation is the position with sort_index equal to 0, so moving a product up is actually reducing its sort_index by 1.

#PATH: app/models/
class Product < ActiveRecord::Base
attr_accessible :sort_index, :name, :price
before_create :set_sort_index
def last?
self.sort_index == Product.maximum('sort_index')
end
def first?
self.sort_index == 0
end
def next
Product.where(:sort_index => (self.sort_index+1)).first
end
def previous
Product.where(:sort_index => (self.sort_index-1)).first
end
def move_up
switched = Product.where(:sort_index => (self.sort_index - 1)).first
return false if !switched
switched.sort_index = self.sort_index
self.sort_index -= 1
switched.save
self.save
end
def move_down
switched = Product.where(:sort_index => (self.sort_index + 1)).first
return false if !switched
switched.sort_index = self.sort_index
self.sort_index += 1
switched.save
self.save
end
private
def set_sort_index
current_max_index = Product.maximum('sort_index') || -1
self.sort_index = current_max_index+1
end
end
view raw product.rb hosted with ❤ by GitHub

These methods were tested with RSpec (might not be 100% coverage, let me know if that is the case). I've moved the RSpec file to the end of the post because is quite a long file.

Having these methods working I implemented the functionality and the interface to allow the users to actually change the ordering of the products.

Created the necessary routes.

#PATH: config/
YourApplication::Application.routes.draw do
resources :products do
member do
put :move_up
put :move_down
end
end
end
view raw routes.rb hosted with ❤ by GitHub

Created the controller methods.

#PATH: app/controllers/
class ProductsController < ApplicationController
respond_to :html, :js
def index
@products = Product.order("sort_index ASC")
end
def move_up
@product = Product.find(params[:id])
@success = !@product.first? && @product.move_up
end
def move_down
@product = Product.find(params[:id])
@success = !@product.last? && @product.move_down
end
end

Followed by the views. The index page.

<%#PATH: app/views/products/ %>
<h1>Products</h1>
<p><%= link_to "New Product", new_product_path %></p>
<table>
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<% @products.each do |product| %>
<tr id="product_#{product.id}">
<td id="product_#{product.id}_sort">
<%= render :partial => "ordering", :locals => {:product => product} %>
</td>
<td><%= product.name %></td>
<td><%= product.price %></td>
</tr>
<% end %>
</tbody>
</thead>
view raw index.html.erb hosted with ❤ by GitHub

Which calls the _ordering partial

<%#PATH: app/views/products/ %>
<% if !product.first? %>
<%= link_to image_tag("ordering_icons/up.gif", :alt => "up", :title => "Move up"), move_up_product_path(product), :method => :put, :remote => true %>
<% end %>
<% if !product.last? %>
<%= link_to image_tag("ordering_icons/down.gif", :alt => "down", :title => "Move down"), move_down_product_path(product), :method => :put, :remote => true %>
<% end %>

And then the js views to process the result of the requests. To move down.

<%#PATH: app/views/products/ %>
$(function(){
<% if @success %>
$("#product_<%=@product.id%>").next().after($("#product_<%=@product.id%>"));
$("#product_<%=@product.id%>_sort").html("<%= escape_javascript(render :partial => "ordering", :locals => {:product => @product} ) %>");
$("#product_<%=@product.previous.id%>_sort").html("<%= escape_javascript(render :partial => "ordering", :locals => {:product => @product.previous} ) %>");
<% else %>
alert("It wasn't possible to move the product. The last product can't be moved further down...")
<% end %>
});

And to move up.

<%#PATH: app/views/products/ %>
$(function(){
<% if @success %>
$("#product_<%=@product.id%>").next().after($("#product_<%=@product.id%>"));
$("#product_<%=@product.id%>_sort").html("<%= escape_javascript(render :partial => "ordering", :locals => {:product => @product} ) %>");
$("#product_<%=@product.previous.id%>_sort").html("<%= escape_javascript(render :partial => "ordering", :locals => {:product => @product.previous} ) %>");
<% else %>
alert("It wasn't possible to move the product. The last product can't be moved further down...")
<% end %>
});

If you need to use this in more than one model you can move some bits into a Module and then include the Module where necessary. I haven't had the need to do it. But if I do, I'll probably do a post about it.

Hope these bits of code are useful for you some day, I've already used in two projects. If you find any issues or have any suggestions to improve this code please let me know.

And now for the RSpec file:

#PATH: spec/models/
require 'spec_helper'
describe 'setting and updating product sort_index value' do
it "should set the sort_index value to zero for the first product, and then increment the sort_index value for the following records" do
#no products
Product.all.count.should == 0
#create a product
product = Product.create
Product.all.count.should == 1
product.sort_index.should == 0
#create another product
product1 = Product.create
Product.all.count.should == 2
product1.sort_index.should == 1
#create another product
product2 = Product.create
Product.all.count.should == 3
product2.sort_index.should == 2
end
it "should respond true to first? and last? if there's only a product and it has sort_index value of 0" do
product = Product.create
product.first?.should == true
product.last?.should == true
product.sort_index.should == 0
end
it "responds true to the method first? and false to the method last? when called on a product with sort_index 0, and when there is a second product with sort_index value 1" do
product = Product.create
product1 = Product.create
product.first?.should == true
product.last?.should == false
product.sort_index.should == 0
product1.first?.should == false
product1.last?.should == true
product1.sort_index.should == 1
end
it "responds false to both first? and last? when called on a product with sort_index 1, and there are other designations of the same level with sort_index 0 and 2" do
product = Product.create
product1 = Product.create
product2 = Product.create
product.first?.should == true
product.last?.should == false
product.sort_index.should == 0
product1.first?.should == false
product1.last?.should == false
product1.sort_index.should == 1
product2.first?.should == false
product2.last?.should == true
product2.sort_index.should == 2
end
it "should return the product with sort_index+1 when calling next on a product. it should return nil if there isn't another product" do
product = Product.create
product.next.should == nil
product.previous.should == nil
product1 = Product.create
product.next.should == product1
product1.sort_index.should == product.sort_index+1
product1.previous.should == product
end
#Order hierarchy: 0, 1, 2, ...
it "should increase the sort_index of a product when moving it down, and increase it when moving up, updating the other products accordingly" do
product = Product.create
product1 = Product.create
product2 = Product.create
product.sort_index.should == 0
product1.sort_index.should == 1
product2.sort_index.should == 2
product.move_down
product.reload
product1.reload
product2.reload
product.sort_index.should == 1
product1.sort_index.should == 0
product2.sort_index.should == 2
product.move_down
product.reload
product1.reload
product2.reload
product.sort_index.should == 2
product1.sort_index.should == 0
product2.sort_index.should == 1
product.move_down
product.reload
product1.reload
product2.reload
product.sort_index.should == 2
product1.sort_index.should == 0
product2.sort_index.should == 1
product.move_up
product.reload
product1.reload
product2.reload
product.sort_index.should == 1
product1.sort_index.should == 0
product2.sort_index.should == 2
product.move_up
product.reload
product1.reload
product2.reload
product.sort_index.should == 0
product1.sort_index.should == 1
product2.sort_index.should == 2
product.move_up
product.reload
product1.reload
product2.reload
product.sort_index.should == 0
product1.sort_index.should == 1
product2.sort_index.should == 2
end
end

Friday, 4 November 2011

Bits of Code - Nested Attributes

Yesterday I had the need to implement a form with some nested attributes and so I did what I often do: I went back to a previous project and copy&pasted the necessary code and then changed it to match the new project's needs.

While I was doing this I thought that it would be useful to get this, and other bits of code in a place that I can easily refer to, instead of having to remember in which project I've used what. This is the reasoning behind this post.

Bare in mind that this is not a post about life changing bits of code, but rather bits of code that I need now and then and that are easier to just copy&paste&edit than to have to rewrite from scratch. Some of these bits can be easily found in different websites, but having them here will make my life easier than having to search again. I'll make sure I'll refer to the websites that I've used.

So these first bits of code are meant to build a form with nested attributes.

Imagine that you have a model named Content that has many Attachments and when you are adding or editing a content, you want to be able to manage that content's attachments. This allows you to easily do it.

This code uses Rails accepts_nested_attributes_for and then it manages the different attachments with javascript methods (using jQuery framework), allowing you to add, remove and edit them accordingly.

The Content model:

class Content < ActiveRecord::Base
has_many :attachments
accepts_nested_attributes_for :attachments, :allow_destroy => true
end
view raw content.rb hosted with ❤ by GitHub


The Attachment model (notice the accepts_nested_attributes_for method):

class Attachment < ActiveRecord::Base
belongs_to :content
end
view raw attachment.rb hosted with ❤ by GitHub


The ApplicationHelper, with the methods used in the views:

module ApplicationHelper
def link_to_remove_fields(name, f)
f.hidden_field(:_destroy) + link_to_function(name, "remove_fields(this)", :class => "btn remove danger")
end
def link_to_add_fields(name, f, association)
new_object = f.object.class.reflect_on_association(association).klass.new
fields = f.fields_for(association, new_object, :child_index => "new_#{association}") do |builder|
render(association.to_s.singularize + "_fields", :f => builder)
end
link_to_function(name, "add_fields(this, '#{association}', '#{escape_javascript(fields)}')", :class => "btn")
end
end

Part of the Content form view (notice the use of the method fields_for, and the helper method link_to_add_fields):

<!-- other form code -->
<div class="attachments">
<%= f.fields_for :attachments do |attachment_form| %>
<%= render 'attachment_fields', :f => attachment_form %>
<% end %>
</div>
<%= link_to_add_fields "Attach a file or an image", f, :attachments %><br />
<!-- other form code -->
view raw _form.html.erb hosted with ❤ by GitHub

The attachment_fields partial (notice the use of the helper method link_to_remove_field):

<div class="attachment fields">
<%= f.label :caption %>
<%= f.file_field :document %>
<%= link_to_remove_fields "remove", f %>
</div>

And the javascript methods (notice that if the position of the elements in the attachment_fields partial changes this code might need to be updated):

function remove_fields(link) {
$(link).prev("input[type=hidden]").val("1");
$(link).closest(".fields").hide();
}
function add_fields(link, association, content) {
var new_id = new Date().getTime();
var regexp = new RegExp("new_" + association, "g")
$("div.attachments").append(content.replace(regexp, new_id));
}
view raw application.js hosted with ❤ by GitHub

 These bits of code are based on (or were copied from):