ROR: Serializing output for multiple objects - ruby-on-rails

I am learning rails, and using active_model_serializers gem.
I need to get the customized output in JSON from two different tables. Here is an example
car: id, name, color, size, shape, date...
battery: id, car_id, price, weight, name, date...
class battery < ApplicationRecord
belongs_to :car
end
query I ran is
## This is sample query will not work
Car.includes(:battery).where(car: {color: red}).where(date >= 'blah')
I have three hashes in calculated in my controller
#cars_controller.rb
available_cars ={
available_cars:[{id:id,
name:name,
color:color,
size:size,
date:date..
}]
}
available_battries = {
available_batteries:[{id:id,
card_id:car_id,
price:price,
weight:weight,
name:name,
date:date..
}]
}
cars_with_battires = {
cars_with_battry: [{id:id,
name:name,
color:color,
size:size,
date:date..},
battries:[{id: 1,
name: name1
},
{id: 2,
name: name2
}
]
]}
render json: {
available_cars: available_cars,
available_batteries: available_batteries,
cars_with_battry: cars_with_battry }
serializer: CarIndexListSerializer }
This is my serializer file looks like and I need to fill those functions
#car_index_list_serializer.rb
class CarIndexListSerializer < ActiveModel::Serializer
attributes :available_cars,
:available_batteries,
:cars_with_battry
def available_cars
#todo?
end
def available_batteries
#todo?
end
def cars_with_battry
#todo?
end
I can just remove the fields I dont want and return the result in JSON but I want to use active_model_serializers to return it in following format.
{
available_cars:[{id: 1, name:name1, size: abc}],
available_batteries:[{id: 1, name: name1}],
cars_with_battry:[{id: 1, name:name1, size: abc,
battries:[{id: 1, name: name1}]
}]
}
if any of the hash is empty, I do not want to show it in the results

result = {}
available_cars = Car.where xxx
if available_cars.count.positive?
result[:available_cars] = available_cars.map {|car| CarSerializer.new(car).as_json }
end
# do the same thing for available battery and car with battery
render json: result
just a rough idea
Update
Usually for #index action on controller, it render list of resources
so CarIndexListSerializer is not required
What you have to create is two serializers:
car serializer
battery serializer
even you can add a relationship data to your car serializer
class CarSerializer < ActiveModel::Serializer
has_many :batteries
end
class Car < ApplicationRecord
has_many :batteries
end
so when you call this
car = Car.last
CarSerializer.new(car).as_json
it would return the car and batteries data
don't forget to create BatterySerializer too

Related

Rails Controller Hash into Array

trying to change the structure of a JSON response using a serializer. I'm having problems though as I need to group the data alphabetically (|one_record| one_record.name[0].to_s) which is returning a hash at the moment. How could I go about converting this to an array that I can pass into the serializer?
#WIP - order should return an array of contacts ordered alphabetically according to their name.
def order
# returns the first letter of the name
#ordered_contacts = #user.all_contacts.group_by { |one_record| one_record.name[0].to_s }
render json: #ordered_contacts, serializer: ContactshipsSerializer, :status => :ok
end
Serializer:
class ContactshipsSerializer < ActiveModel::Serializer
# method override
def serializable_object(options={})
#ordered_contacts.map do | *title*, ordered_contacts |
[ *title* , serialized_ordered_contacts(ordered_contacts) ]
end.to_h
end
private
def serialized_contactships contactships
ordered_contact.map{ |contactship| ContactshipsSerializer.new(ordered_contact, root: false) }
end
end

How to use Fabricate-gem to generate objects?

I'm using Rails 4, Fabricate and Faker Gems. And I'm trying to seed my database with (100 or so) randomly created objects (Order that contains up to 3 Ice Creams). I followed This Answer that recommend using this approach.
models/order.rb
class Order < ActiveRecord::Base
...
has_many :ice_creams
...
end
models/ice_cream.rb
class IceCream < ActiveRecord::Base
...
has_and_belongs_to_many :flavors
has_many :added_extras
has_many :extras, :through => :added_extras
belongs_to :order
...
end
models/extra.rb
class Extra < ActiveRecord::Base
...
has_many :added_extras
has_many :extras, :through => :added_extras
...
end
test/fabricators/order_fabricator.rb
Fabricator(:order) do
user { User.offset(rand(User.count)).first } #fine
shift { Shift.offset(rand(Shift.count)).first } #fine
created_at { Faker::Date.backward(365) } #fine
ice_creams(rand: 3) { |attrs| Fabricate( :ice_cream, created_at: attrs[:created_at] ) } #fine
total { Faker::Number.between(5, 25) }
#add more logic depending of the total number of randomly created ice creams
discount { [0, 10, 15, 25].sample } #fine
total_after_discount { |order| order[:total] - ( (order[:total] * order[:discount]) / 100 ) }
paid { [50, 100, 200].sample } #fine
remaining { |order| order[:paid] - order[:total_after_discount] } #fine
end
test/fabricators/ice_cream_fabricator.rb
Fabricator(:ice_cream) do
size { Size.offset(rand(Size.count)).first } #fine
basis { Basis.offset(rand(Basis.count)).first } #fine
sauce { Sauce.offset(rand(Sauce.count)).first } #fine
topping { Topping.offset(rand(Topping.count)).first } #fine
flavors { [ Flavor.offset(rand(Flavor.count)).first ] }
#add additional ability to be one or two flavors randomly
extras { [ Extra.offset(rand(Extra.count)).first ] }
ice_cream_price { [15, 17, 18, 19, 20, 22].sample } #add logic
extras_price { [5, 10, 15, 20 ].sample } #add logic
total_price { |attrs| attrs[:ice_cream_price] + attrs[:extras_price] } #fine
created_at { Faker::Date.backward(365) }
end
It's working fine , I can now create fake Orders that contains upto 3 fake Ice Creams, But the thing is I'm struglling to figure out the logic to Fabricate more realistic Orders, As you may noticed in my fabricators code there are some attributes that I labeled fine -Which I'm fine with it's result- and some that I still not completely satisfied of, Like...
I wish that the Fabricated Ice Cream can -randomly- have one or two flavors.
I wish to do the same thing with Extras
I want to have the sum of the randomly Fabricated Ice Creams :total_price to be passed to the Order as :total
I've Tried to do so by creating a Flavor Fabricator but It didn't work..
test/fabricators/flavor_fabricator.rb
Fabricator(:flavor) do
Flavor.offset(rand(Flavor.count)).first
end
I also tried to sum the :total_price the activeRecord way, but it also didn't work
test/fabricators/order_fabricator.rb
Fabricator(:order) do
...
total { self.ice_creams.sum(:total_price) }
...
end
So my question is...
- Are the things that I wish for possible or it's just too much? And if so how to achieve that?
I hope I made myself clear, And you can help me,. Thanks
It looks like you're trying to use fabrication to set calculated values on your models, IceCream#total_price for example. You should be letting methods on your model do their thing, like calculate that total from the parts, instead of trying to force them with fabrication.
To answer your questions specifically:
1) I wish that the Fabricated Ice Cream can -randomly- have one or two flavors.
Fabricator(:ice_cream) do
flavors { Flavor.all.sample(rand(1..2)) }
end
2) Same as #1
3) You should have a method on Order that calculates the total when it is created.

How to select the count of joins as metadata for the main model using activerecord

I have two tables that are something like:
users
id
name
active
items
id
user_id
color
Using Rails, I want to select the active users along with the number of items that are red or blue.
Something like:
User.where(active: true).joins(:items).where(items: {color: ['red', 'blue']}).count(:items)
I want the result to be an array of Users where the Users have an annotated number of items.
So it could end up like users = activerecord query, users.first.name == 'John', users.first.items_count == 3
What would you do?
Considering the color filter, I'd just do the count in ruby.
class User
scope :active, -> { where(active: true) }
def items_of_color(colors)
items.select{ |i| i.color.in?(colors) }
end
end
in the controller
#users = User.active.preload(items)
and then in the view, count the red and blue
user.items_of_color(['red', 'blue']).size
But, if RED and BLUE are special and commonly referenced, you can do this...
class User
...
has_many :red_and_blue_items, where -> ({color: ["red", "blue"]}), class_name: "Item"
end
And then, preload like so
#users = User.active.preload(:red_and_blue_items)
In the view
#users.first.red_and_blue_items.size
I don't say it is the solution, but this following statement
Item
.joins(:user)
.group(:user_id)
.where(users: { active: true }, color: ['red', 'blue'])
.count
return a list of user_id with its associated item count:
{
user_id_1 => count_1,
user_id_2 => count_2,
user_id_3 => count_3,
...
}

combine keys in array of hashes

I map results of my query to create an array of hashes grouped by organisation_id like so:
results.map do |i|
{
i['organisation_id'] => {
name: capability.name,
tags: capability.tag_list,
organisation_id: i['organisation_id'],
scores: {i['location_id'] => i['score']}
}
}
a capability is defined outside the map.
The result looks like:
[{1=>{:name=>"cap1", :tags=>["tag A"], :scores=>{26=>4}}}, {1=>{:name=>"cap1", :tags=>["tag A"], :scores=>{12=>5}}}, {2 => {...}}...]
For every organisation_id there is a separate entry in the array. I would like to merge these hashes and combine the scores key as so:
[{1=>{:name=>"cap1", :tags=>["tag A"], :scores=>{26=>4, 12=>5}}}, {2=>{...}}... ]
EDIT
To create the results I use the following AR:
Valuation.joins(:membership)
.where(capability: capability)
.select("valuations.id, valuations.score, valuations.capability_id, valuations.membership_id, memberships.location_id, memberships.organisation_id")
.map(&:serializable_hash)
A Valuation model:
class Valuation < ApplicationRecord
belongs_to :membership
belongs_to :capability
end
A Membership model:
class Membership < ApplicationRecord
belongs_to :organisation
belongs_to :location
has_many :valuations
end
results snippet:
[{"id"=>1, "score"=>4, "capability_id"=>1, "membership_id"=>1, "location_id"=>26, "organisation_id"=>1}, {"id"=>16, "score"=>3, "capability_id"=>1, "membership_id"=>2, "location_id"=>36, "organisation_id"=>1}, {"id"=>31, "score"=>3, "capability_id"=>1, "membership_id"=>3, "location_id"=>26, "organisation_id"=>2}, {"id"=>46, "score"=>6, "capability_id"=>1, "membership_id"=>4, "location_id"=>16, "organisation_id"=>2}...
I'll assume for each organization: the name, taglist and organization_id remains the same.
your_hash = results.reduce({}) do |h, i|
org_id = i['organisation_id']
h[org_id] ||= {
name: capability.name,
tags: capability.taglist,
organisation_id: org_id,
scores: {}
}
h[org_id][:scores][i['location_id']] = i['score']
# If the location scores are not strictly exclusive, you can also just +=
h
end
I believe this works, but data is needed to test it.
results.each_with_object({}) do |i,h|
h.update(i['organisation_id'] => {
name: capability.name,
tags: capability.tag_list,
organisation_id: i['organisation_id'],
scores: {i['location_id'] => i['score']}) { |_,o,n|
o[:scores].update(n[:score]); o }
}
end.values
This uses the form of Hash#update (aka merge!) that uses a block to determine the values of keys that are present in both hashes being merged. Please consult the doc for the contents of each of the block variables _, o and n.
Assume, that result is your final array of hashes:
result.each_with_object({}) do |e, obj|
k, v = e.flatten
if obj[k]
obj[k][:scores] = obj[k][:scores].merge(v[:scores])
else
obj[k] = v
end
end

Any possible way to add parameters to ON clause on include left joins on rails?

I have a huge complex query like this:
#objects = Object.joins({ x: :y }).includes(
[:s, { x: { y: :z } }, { l: :m },:q, :w,
{ important_thing:
[:h, :v, :c,:l, :b, { :k [:u, :a] }]
}
]).where(conditions).order("x.foo, x.bar")
Then i want to show all Objects and only Important_things that were created at between two dates.
If i put this on there where clause i dont get all Objects, only Objects that has Important_things between informed dates.
A solution using raw sql was this:
select * from objects left join important_things on important_things.object_id = objets.id and important_things.created_at between 'x' and 'y'
Instead of:
select * from objects left join important_things on important_things.object_id = objets.id where important_things.created_at between 'x' and 'y'
I really need all those objects and i don't want to use a raw SQL, any workaround or a possibility to pass parameters to the ON clause on an association?
I do this,
class VendorsRatings < ActiveRecord::Base
def self.ratings(v_ids,sort = "DESC")
joins("RIGHT OUTER JOIN vendors_lists v
ON v.vendor_id = vendors_ratings.vendor_id").where(conditions)
end
end
I did a ugly workaround:
class Parent < ActiveRecord::Base
cattr_accessor :dt_begin, dt_end
has_many :children, conditions: Proc.new { { created_at: (##dt_begin..##dt_end) } }
end
class MetasController < ApplicationController
def index
Parent.dt_begin = Date.parse(param[:dt_begin])
Parent.dt_end = Date.parse(param[:dt_end])
#parents = Parent.includes(:children).where("children.age = ?", params[:age])
end
end
So this way i get all Parents even if i dont have Children created_at between those specified dates.
And the most important part of it i have all objects inside the ActiveRecord.
But be careful because i did messed with cattr_accessor so i have to set everytime before i search for Parents.

Resources