How to write if statement in Rails 4 as_json method? - ruby-on-rails

I'm using Rails 4.I'm creating API databse where users can sign up from Facebook Graph API.
If user has no profile picture then the image_url is null.
After reading answers in SO I thought this is the correct way how to build custom json for my response.
I have created method as_json to render response when user is created with only parameters who should get returned.
This is the method how I'm creating json response:
def as_json(options={}){
id: self.id,
first_name: self.first_name,
last_name: self.last_name,
auth_token: self.auth_token,
image: {
thumb: "http://domain.com" + self.profile_image.thumb.url
}
}
end
This method above gives me an error: no implicit conversion of nil into String.
I need to give absolute image url path if the image exists in my db, but i don't need to give this parameter in response if image url is null in my database.
How can I write if statement inside this as_json method?
I've tried this, but it doesn't work.
def as_json(options={}){
id: self.id,
first_name: self.first_name,
last_name: self.last_name,
auth_token: self.auth_token,
if !self.profile_image.thumb.url == nil
image: {
thumb: "http://domain.com" + self.profile_image.thumb.url
}
end
}
end
With the help from Jorge de los Santos I've managed to make it pass no implicit conversion of nil into String error with this code:
def as_json(options={})
response = { id: self.id,
first_name: self.first_name,
last_name: self.last_name,
auth_token: self.auth_token }
if !self.profile_image.thumb.url == nil
image = "http://domain.com" + self.profile_image.thumb.url
response.merge(image: {thumb: image })
end
response
end
But now all the users are returned without image parameter even when he has a image url.

Your code seems fine except when you try to merge image key, merge function is not working as you expect, check the followingt to understand:
hash = {a: 1, b:2 }
hash.merge(b: 3)
puts hash #{a: 1, b:2 }
hash = hash.merge(b: 3)
puts hash #{a: 1, b:2, c: 3 }
so you will need to modify your code by changing this line:
response.merge(image: {thumb: image })
to
response = response.merge(image: {thumb: image })

Using Jbuilder
When you are building a complex json object, it's better to use jbuilder, I'll assume the model is called 'User'
Create a template called show.json.jbuilder
json.id #user.id
json.first_name #user.first_name
json.last_name #user.last_name
json.auth_token #user.auth_token
unless #user.profile_image.thumb.url.nil?
json.image do |image|
image.thumb "http://domain.com#{#user.profile_image.thumb.url}"
end
end
I would recommend creating a helper for the image url, so we could for example call something like
json.image #user.full_profile_image_url
Using as_json
As for your own method (using as_json) you could create a method that returns the full image hash
class User < ActiveRecord::Base
def image
{ thumb: "http://domain.com#{profile_image.thumb.url}" }
end
end
Then in the as json call the method
#user.to_json(
only: %i(id first_name last_name auth_key),
methods: :image
)
This will call the image method and set it's value inside a key called 'image'

You can't use logic inside a hash key, you can declare the variable before returning the hash, or you can use the full statement inside the value of the hash. But I think this is more readable.
def as_json(options={})
response = { id: self.id,
first_name: self.first_name,
last_name: self.last_name,
auth_token: self.auth_token }
image = "http://domain.com" + self.profile_image.thumb.url
response.merge!({image: {thumb: image }}) unless self.profile_image.thumb.url
response
end

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

How to build complex json to POST to a web service with Rails 5.2 and Faraday gem?

My application sends data to a metadata repository through a REST API. I choosed Faraday to handle the HTTP requests. I basically setup some headers, a json dataset, and POST to the webservice. The following code takes place in the skills_controller, and is triggered when the user decides to publish the definition of a variable:
### Create the variable for the BusinessArea, get the location header in return
connection = Faraday.new("https://sis-sms-r.application.opendataquality.ch", :ssl => {:verify => false})
request_body = {
definedVariableType: #skill.skill_type.property,
description: {
en: #skill.description_translations.where(language: :en).take!,
de: #skill.description_translations.where(language: :de_OFS).take!,
fr: #skill.description_translations.where(language: :fr_OFS).take!
},
identifier: "Variable TEST 10",
name: {
en: #skill.name_translations.where(language: :en).take!,
de: #skill.name_translations.where(language: :de_OFS).take!,
fr: #skill.name_translations.where(language: :fr_OFS).take!
},
pattern: nil,
pseudonymized: true,
validFrom: Time.now,
validTo: Time.now + 1.year,
version: "1",
responsibleDeputy: {
identifier: #skill.deputy.email,
name: #skill.deputy.external_directory_id
},
responsibleOrgUnit: {
identifier: #skill.organisation.code,
name: #skill.organisation.external_reference
},
responsiblePerson: {
identifier: #skill.responsible.email,
name: #skill.responsible.external_directory_id
}
}
puts "--- body"
puts request_body
response = connection.post("/api/InformationFields/#{business_area.uuid}/definedVariables") do |req|
req.body = request_body.to_json
req.headers['Content-Type'] = 'application/json'
req.headers['Accept'] = 'application/json'
req.headers['Authorization'] = "Bearer #{token}"
end
puts "--- response"
puts response.status # Status 201 => successful request
puts response.body # Message
puts response.headers["location"] # uuid of new object
The method then renders an updated partial of the Show view of the skill, with its updated status.
This works fine as long as the request body is quite simple. But I'd like to handle a variable number of translations, and in some cases also send child records to the web service: i.e. implement loops, nested json objects, and probably partials.
I read about Jbuilder features to create complex json for views. Is there something similar I could use in a controller? Or is there a way to create a json view (and partials) and render it into Faraday' request body? Which would be a good architecture to build this feature? Do you know any article that would describe this?
Thanks a lot for showing me the way.
Start by creating an object that touches your application boundry:
class JSONClient
attr_reader :connection
def initialize(base_uri, **opts, &block)
#connection = Faraday.new(
base_uri,
**opts
) do |f|
f.request :json # encode req bodies as JSON
f.response :json # decode response bodies as JSON
yield f if block_given?
end
end
end
class BusinessAreaClient < JSONClient
def initialize(**opts)
super(
"https://sis-sms-r.application.opendataquality.ch",
ssl: { verify: false},
**opts
)
end
def defined_variables(skill:, uiid: token:)
response = connection.post(
"/api/InformationFields/#{uuid}/definedVariables"
SkillSerializer.serialize(skill),
{
'Authorization' => "Bearer #{token}"
}
)
if response.success?
response
else
# handle errors
end
end
end
response = BusinessAreaClient.new
.defined_variables(
skill: skill,
uuid: business_area.uuid,
token: token
)
This gives you object that can be tested in isolation and stubbed out. Its also the only object that should know about the quirks and particularities of the API thus limiting the impact on your application if it should change.
While using a view sounds like a good idea intially you're basically using a very awkward DSL to generate basic data structures like arrays and hashes that map 1-1 to JSON. jBuilder is also very slow.
As a first step to refactoring you could just extract turning a Skill into JSON into its own PORO:
class SkillSerializer < SimpleDelegator
LANG_MAPPING = {
en: :en,
de: :de_OFS,
fr: :fr_OFS
}.freeze
def serialize
{
definedVariableType: skill_type.property,
description: translate(description_translations),
identifier: "Variable TEST 10",
name: translate(name_translations),
pattern: nil,
pseudonymized: true,
validFrom: Time.now,
validTo: Time.now + 1.year,
version: "1",
responsibleDeputy: {
identifier: deputy.email,
name: deputy.external_directory_id
},
responsibleOrgUnit: {
identifier: organisation.code,
name: organisation.external_reference
},
responsiblePerson: {
identifier: responsible.email,
name: responsible.external_directory_id
}
}
end
def self.serialize(object)
new(object).serialize
end
private
# should probally be refactored to not cause an excessive amount of queries
def translate(relation)
LANG_MAPPING.dup.transform_values do |lang|
relation.where(language: lang).take!
end
end
end
ActiveModel::Serializers is also an option.

Rails method on ActiveRecord::Associations::CollectionProxy

I have a dilema. There's a huge function in my controller to standardise loads of different types of data into one list for the view. So I have this kind of way of handling it at the moment:
customer.notes.each do |note|
to_push = {
id: note.id,
title: 'Contact Note',
type: 'note',
description: note.notes,
user: note.user,
date: note.date,
action: nil,
extras: note.customer_interests,
closed: false,
colour: '#9b59b6'
}
history.push to_push
end
I want to move that out of the controller into the model but I'm not too sure how. I ideally want a method like customer.notes.format_for_timeline but I can't figure out how to iterate over results like that in a self method within the class.
Thanks
I found out how. Using a self method then all:
def self.format
all.each do |item|
# Manipulate items here
end
end
However, I ended up having a method like this:
def format
{
id: id,
note: 'Contact Note',
# Etc
}
end
Then just used:
customer.notes.map {|i| i.format }

calling a method with signature

I have a method with a signautre in rails:
def my_function(some_variable)
end
I call the method from jquery get function like this:
$.get('/controller/my_function', {data: mydata}, function(){
});
But I get an error because I need to send the argument also.
How can I do that?
You need to define an action in your controller and call your function from that action
def my_action
my_function(params[:data])
end
and your jquery script will be calling my_action
$.get('/controller/my_action', {data: mydata}, function(){
});
as #Henry pointed out - in your javascript code data: xxxx is HTTP parameters being sent from the browser to your rails controller action on the server, rails puts all HTTP parameters into the params hash, so if you had
var data = {
first_name: "Joe",
last_name: "Smith"
}
$.get('/controller/some_action', data, function() {
// ...
access those in the params hash on the server
def some_action
logger.debug params.inspect
# => { :first_name => "Joe", :last_name => "Smith", :action => "some_action" }
user.first_name = params[:first_name]
# ...
end

Rails - how to instantly get id of inserted row

I use this logic in my app:
controller
#current_user = User.find_or_create_from_oauth(auth_hash)
user.rb
def self.find_or_create_from_oauth(auth_hash)
provider = auth_hash["provider"]
uid = auth_hash["uid"].to_s
case provider
when 'twitter'
if user = self.find_by_twitter_uid(uid)
return user
else
return self.create_user_from_twitter(auth_hash)
end
end
end
def self.create_user_from_twitter(auth_hash)
a = self.create({
:twitter_uid => auth_hash["uid"],
:name => auth_hash["info"]["name"]
})
puts a.inspect
user = User.find_by_twitter_uid(a.twitter_uid)
puts '---'
puts user.inspect
end
Immediately after self.create I would need to run this line:
Assignment.create(:user_id => a.id, :role_id => 2)
The problem is, that the line puts user.inspect return something like this:
#<User id: nil, name: "...name...", twitter_uid: "96580821", provider: "twitter", created_at: nil, updated_at: nil>
Why is in the hash returned id: nil?
Or, is there any other way, how to get the ID of last created record?
If the user has been correctly saved, you can use directly a:
a.assignments.create(:role_id => 2)
Otherwise (check using create! instead of create) there may be a validation error.

Resources