to_xml with :only and :methods - ruby-on-rails

I'm calling to_xml on an ActiveRecord object with both :only and :methods parameters.
The method that I'm including returns a collection for AR objects. This works fine without the :only param, but when that is added I just get the default to_s representation of my objects.
i.e
<author><books>#<Book:0x107753228></books>\n</author>
Any ideas?
Update, here is the code:
class Author < ActiveRecord::Base
def books
#this is a named scope
products.by_type(:book)
end
end
Author.to_xml(:methods => :books, :only => :id)

AFAIK, you have to handle the child objects by hand:
a = Author.find_by_whatever
xml_string = a.to_xml(:only => :id) { |xml|
a.books.to_xml(:builder => xml, :skip_instruct => true)
}
The :skip_instruct flag tells the builder to leave out the usual <?xml version="1.0" encoding="UTF-8"?> XML preamble on the inner blob of XML.
The XML serializer won't call to_xml recursively, it just assumes that everything from :methods is simple scalar data that should be slopped into the XML raw.

Off the top of my head, I think you can do:
Author.to_xml(:methods => {:books => {:only => :id}})
I've used something similar when including AR associations into the XML, not so sure about custom methods though.

Related

How to reuse RABL partial templates by keeping conventions?

I am using Ruby on Rails 4 and the RABL gem. I would like to reuse templates by rendering the show.json.rabl view from the index.json.rabl and keep convention stated at jsonapi.org (source: RABL documentation).
That is, I have the following files:
# index.json.rabl
collection #articles => :articles
attributes :id, :title, :...
# show.json.rabl
collection [#article] => :articles
attributes :id, :title, :...
Since for each article instance the index.json.rabl renders the same attributes as for the show.json.rabl I would like to reuse the latter as partial template, or (maybe) to extend the first.
What I would like to output is:
# index JSON output
{"articles":[{"id":1,"title":"Sample 1","...":...},{"id":2,"title":"Sample 2","...":...},{"id":3,"title":"Sample 3","...":...}]}
# show JSON output
{"articles":[{"id":1,"title":"Sample 1","...":...}]}
Here is a neat way:
things/base.rabl:
attributes :id, :title
child(:another_child, :object_root => false) { attributes :id, :title }
things/show.rabl:
object #thing
extends 'things/base'
things/index.rabl:
collection #things
extends 'things/base'
You can use extends - https://github.com/nesquena/rabl/wiki/Reusing-templates
object #post
child :categories do
extends "categories/show"
end
I was having trouble interpreting what I needed to do from https://github.com/nesquena/rabl/wiki/Reusing-templates but finally figured it out. For me, I had files structured like this: (names changed to protect the innocent ;) )
/app/controllers/api/my/boring_things_controller.rb
/app/views/api/my/boring_things/index.json.rabl
/app/controllers/api/my/interesting_things_controller.rb
/app/views/api/my/interesting_things/index.json.rabl
boring_things/index.json.rabl looked like this (except for real, it had a LOT more attributes):
collection #boring_things, :root => :things, :object_root => false
attributes :id,
:name,
:created_at,
:updated_at
And interesting_things/index.json.rabl looked ALMOST identical:
collection #interesting_things, :root => :things, :object_root => false
attributes :id,
:name,
:created_at,
:updated_at
I wanted to reuse just the attribute parts. The confusing part to me about https://github.com/nesquena/rabl/wiki/Reusing-templates was that I didn't really think I needed a 'node', and I didn't think I needed 'child' either because I wanted the attributes at the top level, not as a child object. But it turns out I did need 'child'. Here's what I ended up with:
/app/controllers/api/my/boring_things_controller.rb
/app/views/api/my/boring_things/index.json.rabl
/app/controllers/api/my/interesting_things_controller.rb
/app/views/api/my/interesting_things/index.json.rabl
/app/views/api/my/shared/_things.json.rabl
_things.json.rabl is this:
attributes :id,
:name,
:created_at,
:updated_at
boring_things/index.json.rabl is now this:
child #boring_things, :root => :things, :object_root => false do
extends "api/my/shared/_things"
end
and interesting_things/index.json.rabl is now this:
child #interesting_things, :root => :things, :object_root => false do
extends "api/my/shared/_things"
end
So for you, instead of rendering show from index, I'd try doing something where you extract article attributes out into a rabl file shared by index and show. In the same directory where your show.json.rabl and index.json.rabl are (I'm assuming it's views/articles/index.json.rabl and views/articles/show.json.rabl), create a "_article_attributes.json.rabl" file. Sorry I don't have your exact setup so I can't try it out myself syntactically, but it should be something like this in your index.json.rabl:
child #articles do
extends "articles/_article_attributes"
end
[A side note: Another thing that tripped me up when I was doing this was that since my shared file was in a sibling-directory to the different rabl files that used them and I was trying to use a relative path and that did NOT work. Instead I had to use the path starting with whatever's under "app/views" (i.e. "extends 'api/my/shared/_things'" not "extends '../shared/_things'"). That was a weird situation and I won't go into why we did it that way, but if you can it's better to have the shared file in the same directory as your index and show, like you're doing.]

Downgrading rails project to Ruby 1.9.3 | amusing .to_json trouble

Dear stackoverflow people,
I want to downgrade a rails project so it can run on rails 3 without any problems. It worked before on the newest version of rails, but the office does not want to use this one. I have trouble with rewriting this particular line:
#results = #sessions.to_json(:include => [:orientations, :subtopics, :data_files, :participants, :formats, :comments => {:include => [:user => {:only => [:id, :name]}]}])
#sessions are of course a list of results. Is there anyone who knows how I can write an alternative for this that will also run on older versions of rails?
Thanks in advance
Kind regards
Here you go:
Anytime to_json is called on an object, as_json is invoked to create the data structure, and then that hash is encoded as a JSON string using ActiveSupport::json.encode. This happens for all types: Object, Numeric, Date, String, etc (see active_support/json).
ActiveRecord objects behave the same way. There is a default as_json implementation that creates a Hash that includes all the model’s attributes. You should override as_json in your Model to create the JSON structure you want. as_json, just like the old to_json, takes an option hash where you can specify attributes and methods to include decoratively.
def as_json(options={})
super(:only => [:email, :avatar], :include =>[:addresses])
end
Your controller code to display one model should always look like this:
render :json => #user
And if you have to do anything out of the ordinary, call as_json passing your options.
render :json => { :success => true,
:user => #user.as_json(:only => [:email]) }
The moral of the story is: In controllers, do not call to_json directly, allow render to do that for you. If you need to tweak the JSON output, override as_json in your model, or call as_json directly.
Fix your code now to use as_json - it will be one less thing to worry about when you migrate to Rails 3 or Ruby 1.9.3.

Rails, including relationships in json render

I have the following code which returns a json with some data that includes #admin_user.companies:
#admin_user = User.find_by_email(email)
render :json=>{:status=>{:code=>200,:token=>#admin_user.authentication_token,
:user=> #admin_user,
:companies => #admin_user.companies }}
Each company also have many "locations". How do I include all the locations for every single company in #admin_user.companies in the json?
The conventional way is to use
render json: #admin_user.companies, include: :locations
(Please refer to #as_json for more options.)
You don't need to include the status code in your JSON, since it's already in the HTTP headers. Thus, the following might get you close to what you need.
render :json => #admin_user,
:include => {
:companies => { :include => :locations },
},
:methods => :authentication_token
Side Note
This is just an example. You will have to configure :include and :methods to get exactly what you want. For even more fine-grained control, look into JBuilder or RABL.
Another approach is to use jbuilder which allows you much finer-grained control over how your JSON is generated:
Add the jbuilder gem to your app
Use JBuilder.encode from your controllers to generate JSON
Alternatively, create .json.jbuilder templates in your views directories

as_json not calling as_json on associations

I have a model with data that should never be included when it is rendered as json. So I implemented the class' as_json method to behave appropriately. The problem is when other models with associations with this model render json, my custom as_json is not being called.
class Owner < ActiveRecord::Base
has_one :dog
def as_json(options={})
puts "Owner::as_json"
super(options)
end
end
class Dog < ActiveRecord::Base
belongs_to :owner
def as_json(options={})
puts "Dog::as_json"
options[:except] = :secret
super(options)
end
end
Loading development environment (Rails 3.0.3)
ruby-1.9.2-p136 :001 > d = Dog.first
=> #<Dog id: 1, owner_id: 1, name: "Scooby", secret: "I enjoy crapping everwhere">
ruby-1.9.2-p136 :002 > d.as_json
Dog::as_json
=> {"dog"=>{"id"=>1, "name"=>"Scooby", "owner_id"=>1}}
ruby-1.9.2-p136 :004 > d.owner.as_json(:include => :dog)
Owner::as_json
=> {"owner"=>{"id"=>1, "name"=>"Shaggy", :dog=>{"id"=>1, "name"=>"Scooby", "owner_id"=>1, "secret"=>"I enjoy crapping everwhere"}}}
Thanks for the help
This is a known bug in Rails. (The issue is marked closed due to the migration to Github issues from the previous bug tracker, but it's still a problem as of Rails 3.1.)
As acknowledged above, this is an issue with the Rails base. The rails patch here is not yet applied and seems at least slightly controversial, so I'm hesitant to apply it locally. Even if applied as a monkey patch it could potentially complicate future rails upgrades.
I'm still considering RABL suggested above, it looks useful. For the moment, I'd rather not add another view templating language into my app. My current needs are very small.
So here's a workaround which doesn't require a patch and work for most simple cases. This works where the association's as_json method you'd like to have called looks like
def as_json(options={})
super( <... custom options ...> )
end
In my case I've got Schedule model which has many Events
class Event < ActiveRecord::Base
# define json options as constant, or you could return them from a method
EVENT_JSON_OPTS = { :include => { :locations => { :only => [:id], :methods => [:name] } } }
def as_json(options={})
super(EVENT_JSON_OPTS)
end
end
class Schedule < ActiveRecord::Base
has_many :events
def as_json(options={})
super(:include => { :events => { Event::EVENT_JSON_OPTS } })
end
end
If you followed the guideline that anytime you :include an association in your as_json() methods, you define any options you need as a constant in the model to be referenced, this would work for arbitrary levels of associations. NOTE I only needed the first level of association customized in the above example.
I've found that serializable_hash works just as you'd expect as_json to work, and is always called:
def serializable_hash(options = {})
result = super(options)
result[:url] = "http://.."
result
end
I ran into the same issue. I wanted this to work:
render :json => #favorites.as_json(:include => :location)
But it didn't so I ended up adding an index.json.erb with the following:
<% favs = #favorites.as_json.each do |fav| %>
<% fav["location"] = Location.find(fav["location_id"]).as_json %>
<% end %>
<%= favs.to_json.html_safe %>
Not a fix - just a work around. I imagine you did the same thing.
Update #John pointed out this is a known bug in Rails. A patch to fix it appears to be: at https://github.com/rails/rails/pull/2200. Nevertheless, you might try RABL, because its sweet.
I've always been frustrated with passing a complex set of options to create the JSON views I want. Your problem, which I experienced with Mongoid in Rails 3.0.9, prompted me to write JSON templates. But actually, if you're dealing with relations or custom api properties, it turns out that templates are way nicer.
Besides, dealing with different outputs seems like the View layer to me, so I settled on using RABL, the API templating language. It makes it super easy to build valid JSON and include any associations or fields.
Not a solution to the problem, but a better solution for the use case.
This was reported as a bug: http://ternarylabs.com/2010/09/07/migrating-to-rails-3-0-gotchas-as_json-bug/

Ruby on Rails: Defining a method with options

I'm looking to define a method that lets me pass options; something like:
#user.tasks(:completed => true)
I thought something like this would work in my user model (but it's not):
User.rb model
def tasks(options)
tasks.find(:all, options)
end
How would I define the method correctly to let me use #user.tasks(:completed => true)?
This is basically how I'd do it:
def tasks(options={})
unless options[:something].blank?
# do stuff
end
end
There are some different ways to pass options, but you definitively want to pass a hash with a default value (so that you can call the method without options).
In your case the following should address what you want to do:
def tasks(options={})
Task.find(:all, options[:conditions])
end
Edit: and then call it #thing.tasks( {:conditions => "blah"} )
I haven't tested but it should be ok
Edit 2: But like EmFi said it's not optimal to do this. Consider using an association instead. You'll be able to go #thing.tasks.find(:all, :conditions => {blah})
Does User have a has_many :tasks association? That seems to be what you're after here. In that case Rails provides finders for you, which you can access like this:
#user.tasks.find :all, :conditions => { :completed => true }
Or even shorter:
#user.tasks.all :conditions => { :completed => true }
If that's not terse enough and you always want to use a particular condition, try a named scope:
# In your Task model:
named_scope :completed, :conditions => { :completed => true }
# Then you can just call...
#some_user.tasks.completed # => Only completed Tasks for #some_user
Why would you associate a find all on another model with an instance method? I could understand if it was a relation and the find required find options based on the calling record. But there's ActiveRecord Associations for that.
Then there's ActiveRecord::Base#all(options) which is an alias for Task.find(:all, options)
Together make things simpler:
class User < ActiveRecord::Base
has_many :tasks
end
#user.tasks.all(:conditions => {:completed => true})
what you need is:
options[:conditions] in your method
Activerecord provides a method called with_scope, so to pass any additional conditions
#user.tasks(:completed => true)
you can define the task method as
def tasks(options={})
with_scope :find => options
User.all :order => 'id desc'
end
end
and this will merge any hash passed as options parameter with the actual find
The only caveat is you need to modify your method call slightly
#user.tasks(:conditions => {:completed => true})
or to something like
#user.tasks(:select => 'username')
But if there is an association between user and tasks model then I would do what Jordan has in his post

Resources