My setup: Rails 2.3.10, Ruby 1.8.7
I have experimented, without success, with trying to access a virtual attribute in a model from a JSON call. Let's say I have the following models and controller code
class Product
name,
description,
price,
attr_accessor :discounted_price
end
class Price
discount
end
class ProductsController
def show
#product = Product.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.json { render :json => #product }
end
end
end
What I like is to have the JSON output also include Product.discounted_price which is calculated in real-time for each call, ie discounted_price = Price.discount * Product.price. Is there a way to accomplish this?
SOLUTION:
With the initial help from dmarkow, I figured it out, my actual scenario is more complex than the above example. I can do something like this, in the Product model, add a getter method
def discounted_price
...# do the calculation here
end
In the JSON call do this
store = Store.find(1)
store.as_json(:include => :products, :methods => :discounted_price)
You can run to_json with a :methods parameter to include the result of those method(s).
render :json => #product.to_json(:methods => :discounted_price)
Have a look at the gem RABL, as shown in this railscast:
http://railscasts.com/episodes/322-rabl?view=asciicast
RABL gives you fine grained control of the json you produce, including collections and children.
Related
I have a controller action (favorites) in my Rails app that returns a JSON object with two keys (companies and jobs). Each key represents a collection of Company or JobDescription objects. What I want to know is if there is a clean way I can serialize both #companies and #jobs. Here is my code:
def favorites
#companies = current_user.companies
#jobs = current_user.job_descriptions
respond_to do |format|
format.html
format.json { render json: {companies: #companies, jobs: #jobs}, root: false }
end
end
I could always refactor my code into two separate JSON calls (one for jobs, one for companies), but I'd prefer to stick with a single call to favorites.
You can use Rails Presenters here!
So, you can have two presenters: CompaniesPresenter and JobsPresenter which will be responsible for building the #companies and jobs objects respectively.
So, in your controller, you would have something like:
#companies = CompaniesPresenter.new(current_user).companies
#jobs = JobsPresenter.new(current_user).job_descriptions
For example, your CompaniesPresenter would look like this:
class CompaniesPresenter
attr_reader :current_user
def initialize(current_user)
#current_user = current_user
end
def companies
# build the companies JSON here
end
end
Here is a tutorial with Rails Presenter Pattern that might be useful.
And, here is an useful video. Hope this helps.
This example works, are you just trying to change the json format? If so...
In the company or job model, you can add an as_json method and format the output as you want.
def as_json(options = {})
{ :name => name }
end
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.
I'm using acts_as_taggable_on in my rails app. I'd like these tags to show up in the to_json representation of my model.
For example, to_json of one instance of my model looks like this:
{"created_at":"2012-02-19T03:28:26Z",
"description":"Please!",
"id":7,
"points":50,
"title":"Retweet this message to your 500+ followers",
"updated_at":"2012-02-19T03:28:26Z"}
...and I'd like it to look something like this:
{"created_at":"2012-02-19T03:28:26Z",
"description":"Please!",
"id":7,
"points":50,
"title":"Retweet this message to your 500+ followers",
"updated_at":"2012-02-19T03:28:26Z"
"tags" :
{"id":1,
"name":"retweet"},
{"id":2,
"name":"twitter"},
{"id":3,
"name":"social"}
}
My controller code is just the default that scaffolding gives me:
def show
#favor = Favor.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.json { render json: #favor }
end
end
Note that I can already access #favor.tags in the template, and #favor.tags.to_json works as expected, I just needed that data to be included when outputting #favor.to_json.
You can pass options to the json call by calling to_json. Or by redefining as_json in your favor model.
render json: #favor.to_json(include: :tags)
In your favor model override as_json method.
def as_json(options={})
super(:include => :tags)
end
I would like to create entries in a Track (as in music) database in a Rails application by sending the track data information from a client-side Ruby script. I only need to create and destroy tracks from the script, I don't need to have any web interface, and I'm not worrying about authentication/authorization at the moment. Could someone please walk me through (a) how to properly set up the Rails app (using Rails 2.3.8) and (b) how to send the data from a Ruby script?
Here's the approach I have taken so far:
I have created a Track model and Tracks controller. Here is the Track controller code:
class TracksController < ApplicationController
def create
#track = Track.new(params[:track])
respond_to do |format|
if track.save
format.any(:xml, :json) { head :ok }
else
format.xml { render :xml => #track.errors, :status => :unprocessable_entity}
format.json { render :json => #track.errors, :status => :unprocessable_entity}
end
end
end
def destroy
#track = Track.find(params[:id])
#track.destroy
respond_to do |format|
format.any(:xml, :json) { head :ok }
end
end
end
I have set up the routes as follows:
map.resources :tracks, :only => [:create, :destroy]
To send the information from the Ruby script, I have tried (1) using ActiveResource and (2) using net/http with the track information in xml format. For the latter, I'm not sure how to make the post request with net/http and also I'm unclear on how to properly format the xml. For example, can I just use to_xml on a track object?
Thank you in advance for your help.
I don't see any particular problems with your API, or how you are going about scripting it with an HTTP client. However, to get it to fit to the RESTful standard, your create call should return the object as XML or JSON. You can, indeed, simply call to_xml or to_json on the #track object. These functions accept options to further control the output. For instance, if you wish to exclude some piece of data from your API, you can pass the :except option. See the docs linked for more info.
As for your script, I personally prefer HTTParty over ActiveResource - very simple, easy to understand, and doesn't require that you fit your API exactly to the ActiveResource way of doing things. The examples are a good place to start, or have a look at the Chargify gem to see a longer example. HTTParty simply takes a Hash and converts it to XML or JSON. You don't need to have a Track object in your script (unless you really want to). Your script would be something like this:
require 'httparty'
class TrackPoster
include HTTParty
base_uri 'http://hostname.com'
def self.create_track(artist, song)
post('/tracks', :body => {
:track => { :artist => artist, :song => song }})
end
end
TrackPoster.create_track('The Beatles', 'Let It Be')
This call will return the parsed XML/JSON as a hash.
I'm trying to get my head around nested associations in Rails using ActiveResource.
My example is as follows:
What I have is an airport with many runways.
My show action in airports controller contains:
#airport = Airport.find(params[:id])
When I call http://localhost/airports/2.xml I get that piece of XML:
<airport>
<code>DUS</code>
<created-at type="datetime">2009-02-12T09:39:22Z</created-at>
<id type="integer">2</id>
<name>Duesseldorf</name>
<updated-at type="datetime">2009-02-12T09:39:22Z</updated-at>
</airport>
Now, I changed the action to
#airport = Airport.find(params[:id], :include => :runways)
How can I achieve that above loading above URL is giving me something like:
<airport>
<code>FRA</code>
<created-at type="datetime">2009-02-12T09:39:22Z</created-at>
<id type="integer">2</id>
<name>Frankfurt</name>
<updated-at type="datetime">2009-02-12T09:39:22Z</updated-at>
<runways>
<runway>
<id>1</id>
<name>bumpy runway</name>
</runway>
</runways>
</airport>
And on top of that: If I have a client with
class Airport < ActiveResource::Base
..
end
and
class Runway < ActiveResource::Base
..
end
How can I get it to automatically load associations like:
a = Airport.find(1)
puts a.runways.length
=> 1
And (last but not least): Is there a way to store data from the client like:
a = Airport.find(1)
a.runways << Runway.find(1)
a.save
Maybe I'm really too blind, but I'm stuck...
Any idea is warmly welcome.
Thanks
Matt
Resolved it myself finally.
Wasn't aware to put the include into the render statememt:
def show
#airport = Airport.find(params[:id], :include => :runways)
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => #airport.to_xml(:include => :runways) }
end
end
The :include option for the finder specifies that it should eagerly fetch the related items from the database. The :include option for to_xml specifies that it should be included in the XML rendering.
If the canonical XML representation includes the related objects, you can override the to_xml method to make your life a little simpler:
class Airport
def to_xml(options={})
super(options.merge(:include => :runways))
end
end
and then since render will call to_xml if you don't, your controller code can simply be
format.xml { render :xml => #airport }