I am trying to include a virtual attribute/method within a respond_to JSON hash.
The Model (employee.rb)
attr_reader :my_method
def my_method
return "foobar"
end
The Controller (employees_controller.rb)
respond_to :json
def index
#employees = Employee.all
respond_with(:data => #employees, :total => Employee.all.count)
end
It is important that I have "data" as the json root for the collection of "employees" and also to include the "total" within the hash. This works well and returns a nice JSON result of all the employees and the total value.
My qustion is: How do I include the virtual attribute "my_method" for each employee within the employees hash in the JSON response?
Thanks for your time!
This is what worked for me.
Employee.rb
def as_json(options={})
super.as_json(options).merge({:my_method => my_method})
end
Thanks for cmason for pointing me in the right direction. Any other solutions are welcome.
In Rails 3 one can use following
#yourmodel.to_json(methods: ['virtual_attr1', 'virtual_attr2']
Overwriting as_json in your model should do the trick:
def as_json(options={})
{ :methods=>[:my_method] }.merge(options)
end
Related
I have a Rails Controller who responds with JSON objects. Let's take this theoretical example :
respond_to :json
def index
respond_with Comment.all
end
This would respond with something like
[{"id":1,"comment_text":"Random text ", "user_id":1 ,"created_at":"2013-07-26T15:08:01.271Z","updated_at":"2013-07-26T15:08:01.271Z"}]
What i'm looking for is a "best practice" method to interfere with the formating of the json object and return something like this :
[{"id":1,"comment_text":"Random text ", "username": "John Doe", "user_id":1 ,"created_at":"3 hours ago"}]
As you can see, i'm adding a column that doesn't exist in the database model "username" , i'm taking out "updated_at" , and i'm formatting "created_at" to contain human readable text rather than a date.
Any thoughts anyone ?
Overwriting as_json or working with JSON ERB views can be cumbersome, that's why I prefer using ActiveModel Serializers (or RABL):
class CommentSerializer < ActiveModel::Serializer
include ActionView::Helpers::DateHelper
attributes :id, :created_at
def created_at
time_ago_in_words(object.created_at)
end
end
Look here for more information:
https://github.com/rails-api/active_model_serializers
https://github.com/nesquena/rabl
2 ways:
first: define a view, where you build and return an hash that you'll convert to json.
controller:
YourController < ApplicationController
respond_to :json
def index
#comments = Comment.all
end
end
view: index.json.erb
res = {
:comments => #comments.map do |x|
item_attrs = x.attributes
item_attrs["username"] = calculate_username
end
}
res.to_json.html_safe
second: use gem active_model_serializers
I'd redefine the as_json method of your model.
In your Comment model,
def username
"John Doe"
end
def time_ago
"3 hours ago"
end
def as_json(options={})
super(:methods => [:username, :time_ago], except: [:created_at, :updated_at])
end
You don't have to change your controller
Take a look at the documentation for as_json
I'm trying to get some virtual (non-persisted) attributes to show up in the JSON representation of some Mongoid models, but can't seem to get it to work:
class MyModel
include Mongoid::Document
def virtual_attribute
#my_attribute || false
end
def virtual_attribute=(value)
#my_attribute=value
end
end
class MyController
def myaction
false_values=MyModel.where( whatever )
true_values=MyModel.where( something_else ).map{ |model| model.virtual_attribute=true }
#val['my_models']=false_values+true_values
render json: #val.to_json( :include => {:my_models => {:methods => %w(virtual_attribute)}} )
end
end
virtual_attribute doesn't appear in the json. What am I doing wrong?
Edit - ok, so I guess my actual problem is that I can't figure out how to invoke the virtual_attribute method on each of an array of objects that is nested in the root object.
to_json passes the options directly to the array and the objects. :include is only a Mongoid thing:
render json: #val.to_json(methods: :virtual_attribute)
I have a post model that has a virtual attribute that I would like to set and then include in a response to a JSON call to my post#index action. I can't seem to get the virtual attribute to be included in the response.
class Post < ActiveRecord::Base
attr_accessible :height
attr_accessor :m_height
end
class PostsController < ApplicationController
respond_to :html, :json, :js
def index
story = Story.find(params[:story_id])
#posts = story.posts.where("posts.id >= ?", 100)
#posts.each do |post|
post.m_width = post.height * 200
end
results = { :total_views => story.total_views,
:new_posts => #posts }
respond_with(results)
end
end
I think that I must need something similar to #post.to_json(:methods => %w(m_width)), but I don't see how to use :methods in a respond_with
This seems to provide the answer. Implement a to_json and to_xml in your models, as appropriate, with definitions like:
There's a better answer implied here.
Following code stolen from the post:
def as_json(options={})
super(options.merge(:methods => [...], :only => [...], :include => [...])
end
to_json won't be called on your model in this case, from what I can tell in the source, but as_json will be, in the process of serialization.
So, here's what happens, in overview form:
You call respond_with with the results hash you've constructed.
Rails (ActionController) calls to_json on that.
to_json sends you over to JSON::Encoding which keeps calling as_json all the way down until everything is JSONified.
That's why there was the confusion about to_json and as_json in an earlier version of this answer.
Given two models and a controller:
Apples
class Apples < ActiveRecord::Base
belongs_to :not_oranges
...
def as_json(options={})
opts = {:include => [:not_oranges]}
super(options.reverse_merge! opts)
end
end
Oranges
class Oranges < ActiveRecord::Base
belongs_to :not_apples
...
def as_json(options={})
opts = {:include => [:not_apples]}
super(options.reverse_merge! opts)
end
end
Search Controller
class SearchController < ApplicationController
a = Apples.search params[:q]
o - Oranges.search params[:q]
#results = {
:apples => a,
:oranges => o
}
respond_to do |format|
format.json { render :json => #results }
end
As you can see, the two models are completely unrelated and both have different :include options in their as_json definitions.
All works as expected if the search query only hits apples or only hits oranges, but once both objects aren't empty I get:
undefined method `not_apples' for #<Oranges:0x00000004af8cd8>
Seems either the two as_json definitions are being merged, or Oranges.as_json is being overriden by Apples.as_json.
Is this expected behaviour? Is there any clean way around it without using something like RABL? I feel it would be overkill for my needs.
In pseudo code the code for hash as_json method looks like
def as_json(options={})
Hash[collect {|key,element| [key.to_s,element.as_json(options)]}]
end
But your element is modifying the options argument you pass to it. Hash is unaware of this and so passes the modified options hash to as json.
It's usually a good idea not to modify in place the arguments passed to you, except when it is very clear this is ok. I'd rewrite your method as
def as_json(options={})
defaults = {:include => :not_apples}
super(defaults.merge(options))
end
When user's create a post I'd like to set the user_id attribute first. I'm trying to do this using alias_method_chain on the arrtibutes method. But I'm not sure if this is right as the problem I thought this would fix is still occurring. Is this correct?
Edit:
When my users create a post they assign 'artist(s)' to belong to each post, using a virtual attribute called 'artist_tokens'. I store the relationships in an artist model and a joined table of artist_ids and post_ids called artisanships.
I'd like to to also store the user_id of whomever created the artist that belongs to their post (and I want it inside the artist model itself), so I have a user_id column on the artist model.
The problem is when I create the artist for each post and try to insert the user_id of the post creator, the user_id keeps showing as NULL. Which is highly likely because the post's user_id attribute hasn't been set yet.
I figured to get around this I needed to set the user_id attribute of the post first, then let the rest of the attributes be set as they normally are. This is where I found alias_method_chain.
post.rb
attr_reader :artist_tokens
def artist_tokens=(ids)
ids.gsub!(/CREATE_(.+?)_END/) do
Artist.create!(:name => $1, :user_id => self.user_id).id
end
self.artist_ids = ids.split(",")
end
def attributes_with_user_id_first=(attributes = {})
if attributes.include?(:user_id)
self.user_id = attributes.delete(:user_id)
end
self.attributes_without_user_id_first = attributes
end
alias_method_chain :attributes=, :user_id_first
EDIT:
class ArtistsController < ApplicationController
def index
#artists = Artist.where("name like ?", "%#{params[:q]}%")
results = #artists.map(&:attributes)
results << {:name => "Add: #{params[:q]}", :id => "CREATE_#{params[:q]}_END"}
respond_to do |format|
format.html
format.json { render :json => results }
end
end
In your controller, why not just do this:
def create
#post = Post.new :user_id => params[:post][:user_id]
#post.update_attributes params[:post]
...
end
But it seems to me that it would be much better to create the artist records after you've done validation on the post rather than when you first assign the attribute.
EDIT
I would change this to a callback like this:
class Post < ActiveRecord::Base
attr_accessor :author_tokens
def artist_tokens=(tokens)
#artist_tokens = tokens.split(',')
end
after_save :create_artists
def create_artists
#artist_tokens.each do |token|
...
end
end
end