Rails: Prevent JSON hash from being re-ordered - ruby-on-rails

I have the following method in my ContactsController:
def index
#contacts = {}
company = current_employer.company
groups = company.groups.pluck(:name)
contacts = company.contacts.order(last_name: :asc)
contacts.each do |contact|
#contacts[contact.id] = contact.as_json
#contacts[contact.id][:groups] = {}
contact_groups = contact.groups.pluck(:name)
groups.each do |name|
#contacts[contact.id][:groups][name] = contact_groups.include?(name)
end
end
binding.pry
respond_to do |format|
format.json { render json: { contacts: #contacts } }
end
end
This generates a #contacts hash where the keys are the contact IDs, but the hash is ordered by the contact's last name. If I put in a binding.pry I can see that the hash is in the right order that my client expects, however once it's rendered, the hash is reordered by contact IDs.
Here is a sample of what #contacts looks like:
{
contacts: {
24: {
id: 24,
first_name: "Foo",
last_name: "Bar",
email: "foo#foo.co",
groups: {
Test: true,
Random: false,
Example: false
}
},
31: {
id: 31,
first_name: "Jackie",
last_name: "Chan",
email: "chan#example.com",
groups: {
Test: false,
Random: true,
Example: false
}
},
28: {
id: 28,
first_name: "Jason",
last_name: "Rebourne",
email: "jason.rebourne#example.com",
groups: {
Test: false,
Random: false,
Example: true
}
}
}
}
Normally this is desirable behavior, however in this case I'd like to disable it so as to avoid additional iteration through the response on the client side. How can I prevent the hash from being reordered?
Note: I don't have a custom serializer for my Contact or Group classes.
Edit
This is not a duplicate question. I want to return a custom JSON structure, not a standard serialized structure, which is what the linked "duplicate" question is about.

Related

What is the best way to loop through a collection of records and pass back an object to the front end?

I have a controller that returns user reports, and one of the methods sums up the points of said reports, per user. I want to pass back an object of this data to the front end so it can be displayed. Ideally my object would be shaped like this:
data: {
users: {
$user_id: {
name: "Foo Bar",
points: 100
},
$user_id: {
name: "Foo Bar Two",
points: 10
}
}
}
However my current implementation is not building the object like this, and simply adding to one big object.
My code looks like this:
def user_points
hash = {}
User.all.each do |u|
user_points = Report.select("points").where("user_id = ?", u.id).sum("points")
hash.merge!(
user:
{
first_name: u.first_name,
last_name:u.last_name,
time_zone: u.time_zone
}
)
end
render json: { data: hash }
end
and the resulting object only included the last user in one big object
data:
user:
first_name: "Test"
last_name: "Test"
points: 200
time_zone: "Pacific Time (US & Canada)"
You can also achieve the same result by joining both the table and then performing aggregation on joined table.
select users.id, users.name, sum(reports.points) as points from users join reports on users.id = reports.user_id group by users.id;
sql-fiddle
Thank you max for the comment.
def user_points
result = User.join(:reports)
.select(
:first_name,
:last_name,
Report.arel_table[:points].sum.as(:points),
:time_zone
)
.group(:id)
render json: { data: result }
end
Output:
data:
first_name: "Test1"
last_name: "Test1"
points: 100
first_name: "Test2"
last_name: "Test2"
points: 200
first_name: "Test3"
last_name: "Test3"
points: 300
As mentioned by dbugger you need to provide a unique key for each hash entry otherwise merge will just replace an existing value.
For example:
{a: :foo}.merge(b: :bar)
=> {:a=>:foo, :b=>:bar}
and
{a: :foo}.merge(b: :bar).merge(a: :foo_bar)
{:a=>:foo_bar, :b=>:bar}
You might want to consider returning a json array rather than an object with unique property names.
maybe something like this?
def user_points
result = User.all.map do |u|
points = Report.select("points").where("user_id = ?", u.id).sum("points")
{
first_name: u.first_name,
last_name:u.last_name,
time_zone: u.time_zone
points: points
}
end
render json: { data: result }
end

ActiveModel::Errors when mass updating

On a single instance, validation would produce something like:
foo = Foo.new(price: -2)
foo.valid?
foo.errors
=> #<ActiveModel::Errors:0x00007fc66e670430
#base=#<Foo:0x00007fc6503f8658 id: nil, price: nil,
#details={:price=>[{:error=>:greater_than_or_equal_to, :value=>-0.2e1, :count=>0}]},
#messages={:price=>["must be greater than or equal to 0"]}>
Is there a rails way of obtaining the errors when using the update method?:
Foo.update([1, 2, 3], [{ price: 10 }, { price: -20 }, { price: 3 }])
Thank you!
Here is an example how to gather errors from the Model.update(...) method:
# first create a payload with ids and attributes
payload = { 1 => { price: 10 }, 2 => { price: -20 } }
# next update records
result = Foo.update(payload.keys, payload.values)
# the update method returns processed records
# in case of array it will return array of records
# iterate over all objects and find invalid
with_errors = result.map { |r| !r.errors.any? ? nil : r }.compact
# after compact the with_errors variable contains only invalid records.
What I did in the end:
class FooUpdateService
attr_reader :errors
def update(ids, values)
self.errors = ActiveModel::Errors.new(Foo.new)
Foo.update(ids, values).select(&:invalid?).each { |invalid_foo| self.errors.merge!(invalid_foo.errors) }
end
private
attr_writer :errors
end

How to chaange my as_json method?

Now i have used the as_json method like this in my model
def as_json(options = {})
{
id: id,
diary_id: diary_id,
title: title,
post_date_gmt: date,
post_content: strip_tags(content),
smiley_id: smiley_id,
author_id: user_id,
author_name: user.display_name,
attachments: filter_attachments(options[:version]),
root_comments: format_comments(nested_comments.arrange(:order => :created_at)),
post_readings: post_readings.size,
is_read: read_by(options[:current_user])
}
end
I need to change this structure a bit as follows, Actually i want group this array by the date.
{
date_01: {
[post1], [post2], [post3]
},
date_02: {
[post1], [post2], [post3]
}
}
What should I do ?
I fixed the issue as follows
post_dates = (no_of_days.days.ago.to_date..(date_as_string.to_date + no_of_days.days)).map{ |date| date.strftime("%Y-%m-%d") }
# Arrange posts details under each date
i = 0
post_dates.each do |post_date|
posts_grouped_by_date[i] = {:post_date => post_date, :posts_for_date => diary.posts_for_date(Date.parse(post_date) )}
i = i + 1
end
render json: posts_grouped_by_date.sort_by {|hash| hash['post_date']}.as_json(current_user: current_user)
replace the values of data keys to an array of arrays. like below.
{
date_01: [
[post1], [post2], [post3]
],
date_02: [
[post1], [post2], [post3]
]
}

Array of hashes where hash is returned by a function in jbuilder

I have a PORO TutorProfileHandler that has a function json that returns a hash.
class TutorProfileHandler
def initialize(opts)
#profile = opts[:tutor_profile]
end
def json
tutor = #profile.tutor
return {
id: tutor.id,
first_name: tutor.first_name,
last_name: tutor.last_name.first + '.',
school: #profile.school,
avatar: #profile.avatar.url,
bio: #profile.bio,
academic_level: #profile.academic_level,
headline: #profile.headline,
major: #profile.major,
rate: #profile.rate,
rating: #profile.rating,
courses: JSON.parse(#profile.courses),
video_url: #profile.video_url
}
end
end
In my index_tutor_profiles.json.jbuilder, I would like to generate
{
tutor_profile: [{id: 1, ...}, {id: 2, ...}, ...],
tutor_sum: 20
}
However when I do this
json.tutor_profiles (#tutor_profiles) do |profile|
TutorProfileHandler.new({tutor_profile: profile}).json
end
json.tutor_sum #tutor_sum
It gives me an empty array for tutor_profiles.
However if I move everything from TutorProfileHandler.json to the jbuilder file, it works. How do I explicitly include the hash returned by TutorProfileHandler.json in the jbuilder array?
Note: This returns an array, but it creates a new key-value pair array:
json.tutor_profiles json.array(#tutor_profiles) do |profile|
TutorProfileHandler.new({tutor_profile: profile}).json
end
Result:
{
array: [{id: 1, ...}, {id: 2, ...}, ...],
tutor_profile: [],
tutor_sum: 20
}
There is a ugly approach:
json.tutor_profiles #tutor_profiles do |profile|
tmp_json = TutorProfileHandler.new({tutor_profile: profile}).json
json.(tmp_json, *(tmp_json.keys))
end
I think the best practise is directly nesting inside model. You can get more information from the its github page.

Include specific columns in Rails API/JSON

This:
def index
render json: Slide.all.to_json(include: :user)
end
Is rendering this:
[
{
id: 1,
title: 'Hello',
user: {
first_name: 'Guilherme',
last_name: 'Oderdenge',
email: 'guilhermeoderdenge#gmail.com'
}
}
]
Ok. But I just want the first_name from user. There's a way to do this?
Yes, you can do this:
render json: Slide.all.to_json(include: { user: { only: :first_name} )
See the rails api for more information.
Change it like
render json: Slide.all.to_json(:include=>{:user=>{:only=>[:first_name]}})

Resources