Corey Ehmke’s home on the web since 1996, IdolHands.com is an alpha-geek blog covering topics in Ruby on Rails development, Mac OS X, electronics, robotics, and other stuff important in the life of a technologist and tinkerer.
If you’re using Chris Bailey’s Better Nested Set (or better yet, Brandon Keeper’s Awesome Nested Set) for your Ruby on Rails models, you may one day want to implement a drag-and-drop interface to reorganize your sets. Here’s how I was able to implement it.
The Model
Let’s take a dead simple example: a Category model that supports n levels of subcategories using better_nested_set.
parent_id, lft, and rgt are all standard attributes required for better_nested_set. There’s one extra attribute in there, though: depth.
I’ve found that in real-world applications, getting the level of a node in a nested set can be a very expensive operation (to the tune of 100ms), so I typically cache this value in the database and update it on save. Note that when you use better_nested_sets move methods, e.g. move_to_child_of, the plugin is making direct SQL calls to avoid object instantiation, so you’ll need to manually call your update_depth method as well. This is annoying and easy to forget, but I think it’s worth it from a performance perspective; I’d rather optimize for fast read operations than write operations.
So our category.rb looks like this:
class Category < ActiveRecord::Base
# Behaviours
acts_as_nested_set
# Callbacks
after_save :update_depth
# Validations
validates_presence_of :name
validates_uniqueness_of :name
def update_depth
unless self.level == self.depth
self.update_attribute(:depth, self.level)
self.children.each{|child| child.update_depth }
end
end
end
Simple enough so far.
The View
In our view, we want to do a couple of things: display the nested categories with indentation, and allow the user to drag and drop between categories. This makes our view code a little complex.
Our index.html.erb file is pretty sparse:
<h1>Categories</h1>
<%- if @categories.empty? -%>
<p><em>No categories have been created yet.</em></p>
<%- else -%>
<br />
<table>
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody id="catlist">
<%= render :partial => 'categories_tree' -%>
</tbody>
</table>
<%- end -%>
<br />
<%= link_to 'New Category', new_category_path -%>
Note that we’re rendering a partial for the category tree itself. This will allow us to update the view using AJAX when the categories are restructured. So _categories_tree.html.erb looks like this:
<%- @categories.each do |category| -%>
<%= render :partial => 'category_row', :locals => {:category => category} -%>
<%- end -%>
One final partial to enable recursive display magick: _category_row.html.erb>
Notice the two special blocks here? We’re using the draggable_element helper method to define the DOM element tagged with category_# (in our case, the <tr>)as draggable, and the drop_receiving_element helper method to set the row as a receiver for other draggable elements. Since this partial is used to render each of our categories in rows, these definitions will apply to all of the categories in the table.
Each of these helper methods take several parameters, including snap-back behaviour (revert) and a CSS class to apply to an element when a dragged row hovers over it (hoverclass). I encourage you to explore these options to see the sort of nice effects that can be achieved.
When a row is dragged, it will ‘ghost’ and any receiving row will be highlighted. The result of the drop will be an AJAX call to the restructure method in our controller. Let’s add that now.
The Controller
We need to add a method for handling the restructuring of nested categories, and make a small change to the scoping of the index method:
This method is simply parsing the category_id and subcategory_id parameters, finding the appropriate category objects, and making the dragged category a child of the drop destination category.
def index
@categories = Category.roots
end
..
end
Rather than populating the @categories instance variable with all categories, the index method is only pulling root nodes (e.g. categories without parents) from the database. Because of our recursive view code, child nodes will be displayed under their parents, indented appropriately.
Drag and Drop in Action
Here’s a quick demo of what the drag and drop looks like. There are a number of improvements that could be made (indent/outdent? multiselect?) but I’m leaving those up to you.
I’m in the midst of refactoring an application, and I split one of my models into subclasses using single table inheritance. The model in question uses better_nested_set, and I was surprised to find my tests failing when I made the STI switch.
The issue came down to one of scoping, and there was no clear way out of the woods.
STI and Better Nested Set: Reproducing the Issue
I won’t go into the specifics of my models, but I’ll provide an example that demonstrates the problem.
Create a base class (let’s call it Category:
# category.rb
class Category < ActiveRecord::Base
acts_as_nested_set
end
Now define your subclasses:
# foo_category.rb
class FooCategory < Category
end
and
# bar_category.rb
class BarCategory < Category
end
Hop on over to script/console, and do the following:
Everything looks good so far. But now try invoking a Better Nested Set method from the context of one of the subclasses, and you get something very unexpected:
Note that the number remained the same regardless of the output of the named scope; even when none of the FooCategory objects had the published attribute set to true, I would get the same count from the chained roots method.
The Solution: Awesome Nested Set
I tried various options in the :scope arguments in the acts_as_nested_set declaration before I arrived at the perfect solution. Here’s what you need to do. In Terminal, navigate to the root of your Rails application. Then type the following:
Brandon Keeper’s awesome_nested_set plugin is a drop-in replacement for better_nested_set that just works. After installing it, I didn’t have to change any code at all to make things work as expected. All my tests passed the first time around. Thanks, Brandon!
I only recently discovered the counter_cache option for Rails relationships, and I’m finding it invaluable in speeding up performance (and tests) by reducing the number SQL queries and preventing the unnecessary instantiation of objects. It works by storing (caching) the number of objects in a relationship in the table for the associated class, incrementing or decrementing the number as needed.
There are a couple of things that you need to do, however, to ensure that your counts start off right and to make the whole thing work with STI-based models.
Methinks the Code Doth Query Too Much
First, let’s explore the core benefit of counter caching. Here’s a typical “before” case, supposing that User has_many :comments and Comment belongs_to :user:
>> User.last.comments.size
This results in the following three SQL queries:
User Load (0.4ms) SELECT * FROM `users` LIMIT 1
User Columns (2.9ms) SHOW FIELDS FROM `users`
SQL (0.3ms) SELECT count(*) AS count_all FROM `comments` WHERE (`pages`.user_id = 1)
We can cut that down to a single query by adding the following to the relationship in comment.rb:
belongs_to :user, :counter_cache => true
The User table also needs a new attribute:
add_column :users, :comments_count, :integer
Now, whenever a new Comment is created or destroyed, the comments_count for the appropriate User model will be automatically incremented. Now our previous query only requires one trip to the database, to fetch the value of this column:
User Load (0.4ms) SELECT * FROM `users` ORDER BY users.id DESC LIMIT 1
Nice! That resulted in a performance boost of .4ms over 3.6ms. These numbers are small, but that’s still a 9x speed improvement.
Making This Work with STI Models
What if you want to do the same thing with models that use single-table inheritance (STI)? This one stumped me for a while, so hopefully you will find this tip useful.
For a relationship involving an STI model, there’s a slight modification that you need to put in place when you’re defining the counter_cache.
Let’s say that you have a base class of Page, subclassed into Article and Review, and that Articles belong to Topics. Since in the database Articles are stored in the pages table, you’ll need to be more specific in your configuration of the counter_cache In articles.rb:
So Topic would get the column specified in the counter_cache argument:
add_column :topics, :articles_count, :integer
Now everything should work as expected.
Retrofitting Counter Cache
If you’re coming to counter_cache late in the game—in other words, you already have models in your database with relationships—you might be a little surprised by the result of calling @model.related_models.count. In short, you’ll find that the value only reflects related models that were created or destroyed after you put the counter_cache in place. For that reason, I recommend inserting the following to the migration you create for adding the counter column to your table.
Here’s a useful way to get a list of controllers that are in your Rails app. Unlike other approaches that I found on Google, this one does not require a trip to the file system to parse the app/controllers/ directory (although the possible_controllers method in Routing.rb that we’re calling actually does just that, but behind the scenes):
So what is all this good for? Well, consider the possibilities… You can go all kindsa crazy with introspection:
_c.instance_methods.include?('index') # => true
Or find those controllers that you’ve forgotten to secure behind an authentication_required method:
_unsecured = []
_controllers.each do |c|
_this = eval(c.camelize + "Controller")
_unsecured << c unless _this.before_filter.include?(:login_required)
end
puts "The following controllers are wide open to the public: #{_unsecured * ', '}"
I love writing RSpec tests, striving toward the goal of a field of green dots and 100% coverage (I know, it’s a disease). But I also hate to repeat myself, and I don’t really want to have to update my specs unless I’m adding significant new functionality.
A great example of this is data-driven behaviour; for example, controller methods that adapt based on a parameter (such as a filter or order_by parameter), the values of which aren’t necessarily nailed down at the time the spec is written. You’ll either end up not having tests for all possible parameter values, or you’ll end up writing a lot of tests for a little functionality.
So What Kind of Lazy Programmer Are You?
Larry Wall proved long ago that all good programmers are lazy. But there’s lazy, and then there’s lazy. Basically, you have three options:
Be lazy and do nothing.
Be lazy and do something.
Be lazy and do something that’s really smart.
Presumably, if you choose option 1, I can’t help you. If, however, you want to be lazy and smart, there’s a DRY way of writing very flexible tests.
Any Sufficiently Advanced Domain-Specific Language Is Indistinguishable from English
Remember that although it looks like magic and seems to work like magic, RSpec’s domain-specific language (DSL) is actually executable Ruby code. As such, you can do standard Ruby things with it and get expected results.
For example, there’s no reason that you can’t generate some specs in real-time, programmatically:
describe 'displays a listing for users' do
before do
Role.each{ |r| rand(10).times{ User.make(:role => r) } }
end
Role.each do |r|
it "of role #{r.name}" do
get :users, :show => r[:name]
assigns(:users).should == Users.find_all_by_role(r.id)
end
end
end
This generates specs for each defined role when the test is run. If any of them fail, you’ll see the specific expectation that failed:
'UsersController CRUD GET index displays a listing for users of role nobody' FAILED
Cool, huh?
Don’t Be Fooled by Passing Tests and 100% Coverage
That’s fine for iterating over models, but what about testing parameter-driven views? Suppose you have a filter-enabled view in a controller that you need to test. Each filter is defined by a named scope. You want to test each filter, but you don’t want to write n tests, one for each filter.
In your model, you have defined a constant with values that map human-readable labels with the names of a named scopes:
So your controller code looks something like this:
def index
@filters = Product::FILTERS
if params[:show] && @filters.collect{|f| f[:scope]}.include?(params[:show])
@products = Product.send(params[:show]).order_by(params[:by], params[:dir])
else
@products = Product.order_by(params[:by], params[:dir])
end
end
This method examines the :show parameter to determine which, if any, filter (defined in Product::FILTERS) to apply to the users that will be displayed in the view. If an invalid (or no) parameter is passed, it falls through to displaying all products.
A lazy way of testing this would be to have two tests: one in which the parameter is not set, and the other passing a hardcoded parameter through (like ‘active’). The tests would pass and the coverage would be reported as 100% as far as this method was concerned. But what if there’s a problem under the hood with the filter (like that ‘broken’ filter)? You’d never know it from your two tests.
That doesn’t mean that you have to hand-code specs in your products controller for each filter, though. Again, generate them programmatically:
describe 'displays products' do
before do
rand(50).times{ Product.make }
end
Product::FILTERS.each do |f|
it "filtered by #{f.label}" do
get :products, :show => f[:scope]
assigns(:products).should == Product.send(params[:show])
end
end
end
This test will detect that bad filter and is future-proof to boot.
Generating Dynamic Specs for Named Scopes
What about that bad named scope in the model, though?
Let’s use some reflection and generate tests for our named scopes in the model spec. It took some digging to figure out how to retrieve a list of named scopes for a given model, but there is a way: Product.read_inheritable_attribute(:scopes).keys gives us [:active, :inactive, :visible, :invisible, :high_rated, :low_rated, :also_broken, :scoped]. So we can iterate over that list to call each named scope in turn:
describe 'named scope' do
_exceptions = [:ordered, :something_else_requiring_a_parameter, :scoped]
(SitePage.read_inheritable_attribute(:scopes).keys - _exceptions).each do |ns|
it "#{ns.to_s} returns products" do
SitePage.send(ns).size.should_not be_nil
end
end
end
Note that :scoped is a special case and needs to be removed from our list of named scopes. Similarly, you’ll get a failure if any of your named scopes require parameters; in that case, you’ll want to write specs for those scopes, and keep them out of the generated test by adding them to the _exceptions array.
2 Comments »