Confusion over generating custom JSON response in Rails - ruby-on-rails

Model description:
User, Widget, Purchase
User
has_many :purchases
has_many :widgets, :through => :purchases
Widget
has_many :purchases
has_many :users, :through => :purchases
Purchase
belongs_to :user
belongs_to :widget
Hope that makes sense and is correct for the below.
Right, in my controller, I have current_user.
I wish to render a nested JSON response for use eventually with jquery templates.
Like this would be ideal:
{
"purchases": [
{
"quantity": 200,
"price": "1.0",
"widget": {
"name": "Fantastic Widget",
"size": "Large"
}
},
{
"quantity": 300,
"price": "3.0",
"widget": {
"name": "Awesome Widget",
"size": "Medium"
}
}
]
}
This:
render :json => current_user.to_json(:include => [:purchases, :widgets])
Will render some current_user details (not really relevant) and then purchases and widgets at the same level.
This works (outputs some current user details but thats not my main gripe at the moment):
render :json => current_user.to_json({:include => :purchases })
But obviously only outputs purchase data. I cannot get a nested include to work (should it work?) even after looking at this sample:
konata.to_json(:include => { :posts => {
:include => { :comments => {
:only => :body } },
:only => :title } })
From here and this existing stackoverflow question.
So I've just got myself thoroughly confused. Help appreciated.

I would look into utilizing the as_json method in your models to get the desired output. Adding the following to your models should get you going in the right direction.
#users model
def as_json(options={})
{:purchases => self.purchases}
end
#purchases model
def as_json(options={})
{:quantity: self.quantity,
:price: self.price,
:widget: self.widget}
end
#widgets model
def as_json(options={})
{:name:self.name,
:size: self.size}
end
Once you've added these you'd simply user to_json on your user instance and it should output correctly.

RABL lets you craft JSON views similar to the HTML ERB views.
This blog post, "If you’re using to_json, you’re doing it wrong", explains a bit of background and the shortcomings of using to_json.

Related

Rails nested attributes creating has many through association, but want to set more data on it

I have the three models set up like the below (incomplete for brevity):
class Client < ActiveRecord::Base
has_many :roles
has_many :cases, through: :roles
end
class Role < ActiveRecord::Base
belongs_to :client
belongs_to :case
end
class Case < ActiveRecord::Base
has_many :roles
has_many :clients, through: :roles
accepts_nested_attributes_for :roles, :clients
end
It's a simple has_many through association. I've set up the models to accept nested attributes for the appropriate associations, and permitted the parameters properly in my controller. I'm sending the below request body JSON to POST /cases which hits this method:
def create
#case = Case.new(case_params)
if #case.save
render json: #case, root: 'case', status: :created, location: #case, serializer: CaseShowSerializer
else
render json: #case.errors, status: :unprocessable_entity
end
end
And the JSON:
{
"case": {
"state": "CA",
"clients_attributes": [
{
"first_name": "John",
"last_name": "Doe",
"email": "johndoe#gmail.com"
}
]
}
}
The case is created and the nested clients array are also created. Rails automatically creates Role records for each Client in the array of JSON. However, it only sets case_id and client_id (which associates the two). I ALSO want to set other fields on the Role model that it creates.
How can this be done?
Edit:
Both answers below (Oct 1 2015, 12:50PM PST) will not work. Using those answer's logic, a client can not have multiple roles (which is needed and explicitly defined in the model).
Example:
The roles CAN NOT nested inside of the case --
{
"case": {
"name": "test",
"roles_attributes": [
{
"type": "Type 1",
"client_attributes": {
"name": "Client 1"
}
},
{
"type": "Type 2",
"client_attributes": {
"name": "Client 1"
}
}
]
}
}
The JSON above will create the SAME client twice. The Client with a name of "Client 1" has two roles, one of the roles type is "Type 1" the other is "Type 2". The snippet above as suggested by the answer would create two clients and two roles. It should create one client (since it's the same client but that client has multiple roles).
Instead of posting the clients_attributes, post the roles_attributes with the desired data for each role.
Each role in the roles_attributes can contain the nested client object.
When you do this, you might have trouble with saving a new role that references an existing client. Check the answer to this question for the solution.
The only way I've been able to do this before is to define the various models' attributes as they are required. I wrote an answer about it a while back.
Your way is more concise (and better) than mine. However, you should be able to pass the data you need through it like I did:
{
"case": {
"state": "CA",
"roles_attributes": [
"name": "Admin",
"client_attributes": [
{
"first_name": "John",
"last_name": "Doe",
"email": "johndoe#gmail.com"
}
]
]
}
}
If you didn't want to use JSON, and had to populate your data from an HTML form etc, you'd use the following:
#app/controllers/cases_controller.rb
class CasesController < ApplicationController
def new
#case = Case.new
#case.roles.build.build_client
end
def create
#case = Case.new case_params
#case.save
end
private
def cases_params
params.require(:case).permit(:state, roles_attributes: [:name, clients_attributes:[:first_name, :last_name, :email])
end
end
You'd have to back this up with the appropriate model nested attributes:
#app/models/role.rb
class Role < ActiveRecord::Base
belongs_to :client
belongs_to :case
accepts_nested_attributes_for :client
end
class Case < ActiveRecord::Base
has_many :roles
has_many :clients, through: :roles
accepts_nested_attributes_for :roles
end
This would allow you to use the fields_for helper:
#app/views/cases/new.html.erb
<%= form_for #case do |f| %>
<%= f.text_field :state %>
<%= f.fields_for :roles do |role| %>
<%= role.text_field :name %>
<%= role.fields_for :client do |client| %>
<%= client.text_field :first_name %>
<%= client.text_field :last_name %>
<%= client.email_field :email %>
<% end %>
<% end %>
<%= f.submit %>
<% end %>

How to DRY up Rails/ActiveRecord includes when using as_json?

Assume the following relations:
Blog has_many Posts has_many Comments has_one Author
If I want to get all Blogs with all Posts, I could write:
Blog.all.as_json(:include => :posts)
However this will result in an N+1 query.
So instead I need to write:
Blog.includes(:posts).all.as_json(:include => :posts)
Which works as expected, but is not very DRY, especially when you have nested includes.
For example:
Blog.includes(
:posts => {
:comments => :author
}
).all.as_json(
:include => {
:posts => {
:include => {
:comments => {
:include => :author
}
}
}
}
)
This problem becomes even worse when I need to query for this same JSON format in multiple locations.
I've thought about putting the as_json relations format in a class method like so:
class Blog < ActiveRecord::base
...
def self.include_all_json_format
:include => {
:posts => {
:include => {
:comments => {
:include => :author
}
}
}
}
end
...
end
Which solves the problem of querying for this JSON format in multiple locations, because I can then just use:
Blog.includes(
:posts => {
:comments => :author
}
).all.as_json(
Blog.include_all_json_format
)
But of course the Blog.includes() takes a different format for its relations hash, so this:
Blog.includes(
Blog.include_all_json_format
).all.as_json(
Blog.include_all_json_format
)
Won't work.
I could put the Blog.includes() relations hash in a second class method, but having two method declaring the same includes structure isn't DRY.
The only idea I can think of right now is using the Blog.include_all_json_format method mentioned above, and then writing a converter method that can turn that relations hash into the format expected by Blog.includes() (essentially just stripping out the :include keys) so it could be called as:
Blog.includes(
MyConverter(Blog.include_all_json_format)
).all.as_json(
Blog.include_all_json_format
)
But then it gets complicated when I want to use :only or :except in my as_json format.
How can I DRY these includes up, preferably only declaring the relations format once?
Perhaps there's some way to utilize named scopes or some gems?
Any help is greatly appreciated.
You can override the as_json method in your model
For example
class Blog < ActiveRecord::Base
def as_json
super(:include => :posts)
end
end
Then your controller should look like
render json: #blogs
Suggestion
You could use JBuilder, Rabl, AMS or other template library that would allow you to separate the response building out of your controller/model logic. Doing this would definitely DRY up your code.

How can I store nested json data in Rails?

I have 3 models:
class Depot < ActiveRecord::Base
has_many :car_amounts
has_many :cars, :through => :car_amounts
end
class CarAmount < ActiveRecord::Base
belongs_to :depot
belongs_to :car
end
class Car < ActiveRecord::Base
has_many :car_amounts
has_many :depots, :through => :car_amounts
end
What is the best way to store json paramers, which contains depot, amounts and cars data. Something like this:
{
"Depots": {
"title": "Dealer1"
},
"Amounts": [
{
"amount": "1"
"car": [
{
"Type": "supercar1"
}
]
},
{
"amount": "5"
"car": [
{
"Type": "supercar2"
}
]
},
]
}
I am a little unclear what your question is, but I think you might be looking for accepts_nested_attributes_for. The documentation can be found here http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html. From the documentation:
Nested attributes allow you to save attributes on associated records through the parent.
As an example, you could add accepts_nested_attributes_for :cars in you Depots model. While it wouldn't quite work with the JSON you've shown above, it works well with the general idea.
If this isn't quite your question then please comment and I will clarify.
If this data needs to be in you your database then I would 'seed' the data and import it into your database. You can create a file db/seed.rb and lets say I want to import students into my database. I have a student model already created/migrated. In my seed.rb I would do something like...
students = ActiveSupport::JSON.decode(File.read('db/students.json'))
students.each do |a|
Student.find_or_create_by_id_number(:id_number => a['id_number'], :username => a['username'], :first_name => a['first_name'], :last_name => a['last_name'])
end
...and my students.json looks like...
[
{"id_number": "00xx280","username": "jesxxne","first_name": "Jessie","last_name": "Lane","email": "jesxxane#xx.edu","phone_number": "602-321-6693","is_active": true ,"last_four": "1944" },
{"id_number": "11xx2","username": "jamxxacqua","first_name": "James","last_name": "Bevixxua","email": "jamesbxxacqua#xx.edu","phone_number": "828-400-5393","is_active": true ,"last_four": "5422" }
]
Now you can run rake db:seed to import this data. Good luck!

testing rails' nested attributes with rspec

I'm pretty new to testing and rails and tried to figure it out myself but without any luck.
I have the following models
class Picture < ActiveRecord::Base
belongs_to :product
has_attached_file :image
end
class Product < ActiveRecord::Base
has_many :pictures, :dependent => :destroy
accepts_nested_attributes_for :pictures, :reject_if => lambda { |p| p[:image].blank? }, :allow_destroy => true
end
and a controller which is pretty standard, I guess…
def create
#product = Product.new(params[:product])
if #product.save
redirect_to products_path, :notice => "blah."
else
render :action => "new"
end
end
how would I go about and test this? i tried something like this but I can't get it to work:
describe ProductsController do
it "adds given pictures to the product" do
product = Factory.build(:product)
product.pictures.build(Factory.attributes_for(:picture))
post :create, :product => product.attributes
Product.where(:name => product[:name]).first.pictures.count.should == 1 # or something
end
end
It probably has something to do with the way the attributes are passed to the create action but how can I get this to work? Im using rails 3.1.rc5 but i doubt that that hast anything to do with why its not working…
or would you not test this at all since it is basic rails functionality and most likely well tested to begin with?
Try:
post :create, :product => Factory.attributes_for(:product, :pictures => [ Factory.build(:picture) ])
As you say you don't really need to test this, necessarily, because it will be covered by the basic rails functionality, and stuff like this should be thoroughly covered by your integration tests.
However if you DO want to test this the best way is tail your development logs and see what is being posted to the action, copy and paste that into your test and then modify it to suite your needs.
Using attributes or the factory_girl attributes isn't going to cut it unfortunately.

Rails belongs_to and single table inheritance not behaving

I have a Bike model and a Component model. Several models inherit from Component: Frame, Chain, Crankset etc.
When I submit my form, my params look like this:
"bike" => { "frame" => { "id" => "4" }, "chain" => { "id" => "19" }, ... }
In my controller, the following code breaks:
#bike = Bike.new(params[:bike])
> Frame(#90986230) expected, got HashWithIndifferentAccess(#81888970)
If I hack my form to generate the following params, it works:
"bike" => { "frame_id" => "4", "chain_id" => "19" ... }
Here's my models:
class Bike < ActiveRecord::Base
belongs_to :frame
belongs_to :chain
...
end
class Component < ActiveRecord::Base
has_many :bikes
end
class Frame < Component
end
Single table inheritance is working - I can call Frame.first and Component.all without issue.
Am I going insane? Isn't the nested params the usual convention? That's what gets generated by:
- f.fields_for #bike.frame do |frame|
= frame.hidden_field :id
What am I doing wrong??
You are using nested forms, so nested params should work if you use the accepts_nested_attributes_for tag (see railscast 196/197).
belongs_to :frame
accepts_nested_attributes_for :frame

Resources