Sort it out - quick guide to ruby sort
Everyone knows how easy it is to sort an array in ruby right? Just use the sort method.
But what about when you need to sort an array of objects? Sort no longer works on the array as a whole - how would it know which attribute you want to sort by? Luckily, sorting arrays with objects and sorting on multiple attributes is still a breeze.
Let's say we have a collection of reports, where each report has a title, a price and a priority.
reports = Reports.find(:all)
See how simple it is sort by any one or more of the report attributes!
simple sort
cheapest_first = reports.sort { |a,b| a.price <=> b.price }
reverse sort
dearest_first = reports.sort { |a,b| b.price <=> a.price }
sort by multiple attributes
prioritised_reports = reports.sort { |a,b| [a.priority, b.price] <=> [b.priority, a.price] }
sort and overwrite the existing array
reports.sort! { |a,b| a.price <=> b.price }
sort, while ignoring capitalisation
reports.sort! { |a,b| a.title.downcase <=> b.title.downcase }
Of course ruby on rails also makes it easy to get your object collection pre-sorted by the database, that's probably faster in many cases but can't always be done (e.g. if you're working on an array of objects that's already had a bunch of filtering applied.)
rails db sort
cheapest_three_reports = Reports.find(:all, :order=>:price, :limit=>3)
I'm sure that much more is possible, but the simple examples above show just how easy it is to sort through arrays in ruby on rails.
Disappearing attributes... was I duped?
Showing a list of attributes that appear on either object A or it's parent object B should be really easy right?
I thought so, but then strange things started happening...
Here's the code:
def get_full_list
# start with attributes attached to this user
all_attributes = self.attributes
# build up an array of attribute codes
attribute_codes = Array.new
all_attributes.each do |a|
attribute_codes << a.code
end
# add attributes to be inherited from the parent
# (i.e. not attached at user level)
parent_attributes = self.parent.attributes
parent_attributes.each do |a|
if attribute_codes.index(a.code).nil?
attribute_codes << a.code
all_attributes << a
end
end
return all_attributes
end
Now what's wrong with this code?
(Ok, there could be be plenty of things wrong with the code, but I want to focus on one point in particular. Feel free to add a comment below if you spot any other deficiencies).
The problem is that when I create a new attribute on the parent (from a web form not shown here) then the attribute magically migrates to the child object once the above code is executed - all I wanted to do was to get a list of all attributes attached to the child or parent, not to change any of the attributes at either level.
So why were the parent attributes magically moving over to the child? Especially since there is no save involved...
The answer to the main point (why did they move?) lies in this line:
all_attributes = self.attributes
The solution is really, really simple, but it took me quite a while to work out what was really going on.
Rather than getting a copy of the array as I'd intended, I was actually getting the same array - i.e all_attributes became a reference to the real thing, so any changes to the elements in all_attributes were really changing elements in self.attributes.
So how can I get a copy of an array rather than a reference?
Just use the ruby core method dup. Simple as that.
.dup : "...produces a shallow copy of obj---the instance variables of obj are copied, but not the objects they reference..."
i.e. everything was solved once I changed the offending line to:
all_attributes = self.attributes.dup
Now another problem, how can I justify to by boss the way I spent so much time implementing a 4-character fix???
Beware of comments in ruby 1.8.7
I can across a strange bug when upgrading to ruby enterprise 1.8.7 - one of my views displayed incorrectly after the upgrade but it was fine beforehand.
Here's an example of the code I'm referring to:
<table border="1">
<% @services.each do |service| %>
<% next if service.status_id < 0 # skip inactive services %>
<tr>
<td><%= service.name %></td>
<td><%= service.summary %></td>
</tr>
<% end %>
</table>
Obviously I'm expecting a table that shows the name and summary of all active services.
i.e. something like this:
| Email Notification | User will receive email notification of events |
| Monthly Billing | User account has been enabled for monthly billing |
| Free Reports | User incurs no charge for viewing reports |
That's exactly what I got under ruby 1.8.6, but look at the results when using ruby 1.8.7:
Email NotificationMonthly BillingFree Reports| User will receive email notification of events |
| User account has been enabled for monthly billing |
| User incurs no charge for viewing reports |
Now why's that happening? The code is pretty basic so surely there's nothing that could go wrong.
Looking into the html source code I discovered that my code was generating this output:
<table border="1">
Email Notification</td>
<td>User will receive email notification of events</td>
</tr>
Monthly Billing</td>
<td>User account has been enabled for monthly billing</td>
</tr>
Free Reports</td>
<td>User incurs no charge for viewing reports</td>
</tr>
</table>
i.e. note the missing <tr><td>
Strange...
Well it turns out that this line was the problem:
<% next if service.status_id < 0 # skip inactive services %>
It's not the next or the if that causes things to go pear shaped - it's my comment! My comment has the effect of disabling the closing ruby tag so that nothing else is displayed until another closing tag is encountered, i.e. the one after displying the service name: <%= service.name %></td>
Strange that the same code worked fine using ruby 1.8.6, but at least the workaround is easy - just remove comments from the end of code lines, like so:
<table border="1">
<% @services.each do |service| %>
<% next if service.status_id < 0 %>
<tr>
<td><%= service.name %></td>
<td><%= service.summary %></td>
</tr>
<% end %>
</table>
Rails flash[] messages not clearing
I'm sure you're aware of flash[:notice] - that friendly way to pass messages on to your views, even when an HTTP redirect is involved.
I was happily using flash[:notice] to display a Changes Saved message to users when they successfully updated their profile but after making some additional changes I began encountering a perplexing problem - sometimes the flash messages would "stick around" for a while - i.e. they weren't always clearing themselves out automatically.
After some digging around it seems the answer is a simple one. In fact you probably already know it. (Well maybe not if you've read this far...)
The answer comes from the ruby on rails help pages themselves:
"When you need to pass an object to the next action, you use the standard flash assign ([]=). When you need to pass an object to the current action, you use now, and your object will vanish when the current action is done."
Ok, that explains it. My old code was using flash[...] in combination with redirect_to but my new code was using it with render. i.e. the messages weren't being cleared out properly because rails didn't consider my renders to be new pages views.
So to summarise, use:
- flash[:notice] with redirects (e.g. redirect_to :action => 'something')
- flash.now[:notice] with renders (i.e. render :action => 'something')
Result: No more flash clearing issues.
Creating nested objects with validation of parent id
Nested attributes in rails 2.3.3 can really simplify the updating of nested models, but creating nested models throws up an interesting problem when your model uses validates_presence_of... at least it does for me...
Lets say I have two models, a User and an Account. The User accepts nested attributes for the Account, while the Account requires the presence of a user_id.
E.g.
class User < ActiveRecord::Base has_many :accounts accepts_nested_attributes_for :accounts ... end
class Account < ActiveRecord::Base belongs_to :user validates_presence_of :user_id ... end
Now when I go to create a new user and account, up pops a validation error:
ActiveRecord::RecordInvalid: Validation failed: Accounts user can't be blank
It doesn't take much to work out that the error is due to the account object being saved before the user object - i.e. the user doesn't yet exist in the database and so doesn't have a user_id. This causes the validates_presence_of :user_id clause in the account model to fail.
I thought about simply removing the check for user_id but my user and account objects are part of a legacy system - i.e. they're used in other places and I don't want to risk removing validations that are probably required.
So what to do?
The solution I'm going with is to conditionally validate the presence of the user_id - i.e. require the validation only if the account object is being created as part of a dependent creation of a user object.
I've modified my code to set an attribute :new_user on the account when creating a new user with nested account, then check for this attribute as follows:
class Account < ActiveRecord::Base belongs_to :user validates_presence_of :user_id, :unless => :new_user attr_accessor :new_user ... end
That seems to do the trick - the validation message disappears and I'm able to create new users with embedded accounts.
Hopefully this issue will be fixed in a future version of rails. Until then... does anyone have a better solution I could use?
rspec route_for tests failing
Don't you hate it when your tests work on one server but not on another?
That's what happened to me today... rake spec on my development machine gives me a nice 0 failures, but run the same tests on the test server I get a frustrating 16 failures
So what's gone wrong? On the test server I'm getting error messags like these:
Name: UsersController route generation should map { :controller => 'users', :action => 'destroy', :id => 1} to /users/1
Type: Error
Message: Test::Unit::AssertionFailedError in 'UsersController route generation should map { :controller => 'users', :action => 'destroy', :id => 1} to /users/1'
The recognized options <{"action"=>"show", "id"=>"1", "controller"=>"users"}> did not match <{"action"=>"destroy", "id"=>"1", "controller"=>"users"}>, difference: <{"action"=>"destroy"}>
Why does that happen? The code is exactly the same. The configuration files are exactly the same. This problem is frustrating in the least!
After some digging around I found out that the problem was due to newer version of rspec on test server - version 1.2.8 compared with 1.1.12 on the development machine.
It would seem that the newer rspec is stricter (better) than the earlier version. The route_for mthod now requires the HTTP method to be specified if it isn't a GET.
So the solution? Fix up the test specifications to the stricter requirements.
e.g. change
route_for(:controller => "users", :action => "destroy", :id => "1").should == "/users/1"
to
route_for(:controller => "users", :action => "destroy", :id => "1").should == {:path => "/users/1", :method => :delete}
Run the tests again...
0 failures
That's better!
Problem using ActiveResource "find" with conditions
I can't get ActiveResource find to return the correct results when passing through conditions.
According to the documentation I've read, conditions should be specified with the :params symbol, like this:
Address.find(:all, :params => {:user_id => 2})
But when I do this I get returned all results rather than just those ones that satisfy the constraint.
My underlying models on the server are:
class User < ActiveRecord::Base has_many :addresses end
class Address < ActiveRecord::Base belongs_to :user end
and the test data contains 2 users and 2 addresses, with both of the addresses mapped to user 1 like this:
|
|
So,
Address.find(:all, :params => {:user_id => 2})
should return me nothing, but instead it returns me both address 1 and address 2, like what is returned with these queries:
Address.find(:all)
Address.find(:all, :params => {:user_id => 1})
i.e. it's like the :params attribute is being completely ignored.
Ok, so maybe :params is the wrong symbol? For ActiveRecord we should use :conditions to specfiy constraints - let's try that instead.
Address.find(:all, :conditions => {:user_id => 2})
But I just get the same result as before. i.e. all addresses, not just those with user_id = 2
So what can be wrong? Is ActiveRecord broken, or am I just using the wrong symbol to restrict my find query?
Passing error messages to ActiveResource get
How can I return an error message to an ActiveResource get request?
From the documentation I can find, the proper reponse to an invalid ActiveResource get request is to return :status => :unprocessable_entity
I tried passing an error message along with the response but the ActiveResource model doesn't seem to receive anything other than the http error code. This doesn't happen for http success codes - i.e. I can pass a valid @user object back with :status => :ok
My users_controller on the server application has code like this:
...
respond_to do |format|
if @user
if @user.active?
format.xml { render :json => @user, :status => :ok, :location => @user }
else
format.xml { render :json => '{"error":"disabled user"}', :status => :unprocessable_entity }
end
else
format.xml { render :json => '{"error":"incorrect login details"}', :status => :unprocessable_entity }
end
end
end
...
The ActiveResource user model on the client application has:
class User < ActiveResource::Base
self.site = "http://localhost:3000/"
self.format = :json
self.user = "user"
self.password = "pass"
def self.login(user_name, password)
res = get(:login, :user =>{:user_name => user_name, :password => password)
User.new(res['user'])
rescue
raise $!.inspect
end
end
The problem? If I enter invalid user details then all I get back is ActiveResource::ResourceInvalid: Failed with 422
My error string doesn't get passed along. The 422 code does seem to pass additional information along for other ActiveResource requests (e.g. a put request to save the model), but not for get requests.
So how can I make my code differentiate between say, invalid login details and a disabled user?
One possibility would be to return different http status codes depending on the error, and have the client model display switch on these to display different messages to the user. But this seems like a work-around rather than a proper solution.
Surely there must be a way to pass an error message back to the ActiveResource get request. Does anyone know how?