Model relation: Article belongs_to Author
Sample jbuilder view:
json.extract! article,
:id,
:created_at,
:updated_at
json.author article.author, partial: 'author', as: :author
What happens when Article has no Author:
{
"id": 1,
"created_at": "01-01-1970",
"updated_at": "01-01-1970",
"author": []
}
Question:
Is there a clean way to force jbuilder to display null or {} when variable passed to associated template is empty? This problem is prevalent across quite big application and adding code like that article.author.empty? ? json.author(nil) : json.author(article.author, partial: 'author', as: :author) everywhere is not something I'd want to do. Perhaps some form of helper that wouldn't require too much refactoring?
I don't want to override core jbuilder functionality as I don't want to break it (partials accepting multiple variables for example).
Related jbuilder issue: https://github.com/rails/jbuilder/issues/350
This will accomplish what you want
json.author do
if article.author.blank?
json.null!
else
json.partial! 'authors/author', author: article.author
end
end
I would suggest a helper though to avoid all the duplication:
module ApplicationHelper
def json_partial_or_null(json, name:, local:, object:, partial:)
json.set! name do
object.blank? ? json.null! : json.partial!(partial, local => object)
end
end
end
Then you would call it like this:
json_partial_or_null(json, name: 'author', local: :author, object: article.author, partial: 'authors/author')
Related
I'm attempting to structure a json response to mimic an existing structure we have elsewhere in our application (using jbuilder templates). In this specific use case, we are unable to use the jbuilder template because we are preparing the json data for a live update job being fired from a model method, not responding to a server call in the controller.
Desired structure:
{"notes": {
"1": {
"id": "1",
"name": "Jane",
...
},
"2": {
"id": "2",
"name": "Joe",
...
}
}
}
Jbuilder Template (for reference):
json.notes do
for note in notes
json.set! note.id.to_s do
json.id note.id.to_s
json.name note.name
...
end
end
end
I've tried defining a to_builder method in the model class (below), per the jbuilder docs, and active model serializers but can't seem to get the hashes nested under the id attribute. Any direction would be appreciated!
to_builder method
def to_builder
Jbuilder.new do |note|
note.set! self.id do
note.(self, :id, :name, ...)
end
end
end
How about just plain Ruby?
notes.each_with_object({"notes": {}}) do |note, hash|
hash["notes"][note.id.to_s] = note.as_json
end
Or AMS:
adapter = ActiveModelSerializers::Adapter
notes.each_with_object({"notes": {}}) do |note, hash|
hash["notes"][note.id.to_s] = adapter.create(NoteSerializer.new(note))
end
I have the following hash in my controller.
#order = {
:id => "somestringid",
:user_id => "someotherstringid",
:amount => 19.99,
:metadata => [
{
:type => :shipping_data,
:address => "line 1 of address, line 2 of address, city, state, pincode"
},
{
:type => :payment,
:stripe_customer_id => "somestringid",
:stripe_card_id => "someotherstringid"
},
{
:type => :contact,
:email => "someone#example.com",
:phone => "1231231231"
}
]
}
Notes:
The "metadata" is a list of objects.
There can be 0, 1 or more metadata objects.
Each metadata object has a different structure.
The only common key for all metadata objects is the "type" key.
I want to use rabl to generate the following json, but cannot figure out what I should put into my template.
The JSON output that I want should look like the following.
{
"id": "somestringid",
"user_id": "someotherstringid",
"amount": 19.99,
"metadata": [
{
"type": "shipping_data",
"address": "line 1 of address, line 2 of address, city, state, pincode"
},
{
"type": "payment",
"stripe_customer_id": "somestringid",
"stripe_card_id": "someotherstringid"
},
{
"type": "contact",
"email": "someone#example.com",
"phone": "1231231231"
}
]
}
What should I put into my template, so that I get the desired output?
Note
I am not sure whether by rabl you mean you are using standard rabl gem OR rabl-rails because you haven't provided any details about the nature of your application and it is built using what all libraries.
But my solution below is in context of a Rails application using rabl-rails gem. And the rabl-rails gem's README says following:
rabl-rails is faster and uses less memory than the standard rabl gem while letting you access the same features. There are some slight changes to do on your templates to get this gem to work but it should't take you more than 5 minutes.
So I guess it should not be a problem to adapt this solution in context of standard rabl gem whether the application is built Rails or Non-Rails based. My aim is to provide a guidance on the approach which can be used to achieve your desired output.
Now coming to the solution approach:
Using some abstractions you can design a flexible and maintainable solution. Let me elaborate:
Assuming you have a plain ruby-class Order like following or if you don't have any such class you can define it easily using virtus gem which provides some handy out-of-the-box features for a class:
app/models/order.rb
class Order
attr_accessor :id, :user_id, :amount, :order_metadata_obj_arr
....
..
end
app/models/order_metadata.rb
class OrderMetadata
attr_accessor :metadata_type, :metadata_data_obj
...
...
end
app/models/shipping_data.rb
class ShippingData
attr_accessor :address
end
app/models/payment_data.rb
class PaymentData
attr_accessor :stripe_customer_id, :stripe_card_id
end
app/models/contact_data.rb
class ContactData
attr_accessor :email, :phone
end
/app/json_views/orders/metadata/shipping.rabl
attribute :address
/app/json_views/orders/metadata/payment.rabl
attribute :stripe_customer_id, :stripe_card_id
/app/json_views/orders/metadata/contact.rabl
attribute :email, :phone
/app/json_views/orders/order_metadata.rabl
attribute :type
# Anonymous node.
node do |order_metadata_obj|
template_path = "./orders/metadata/#{order_metadata_obj.type}"
metadata_data_obj = order_metadata_obj.metadata_data_obj
partial(template_path, object: metadata_data_obj)
end
/app/json_views/order.rabl
attributes :id, :user_id, :amount
node(:metadata) do |order_obj|
partial('./orders/order_metadata', object: order_obj.order_metadata_obj_arr)
end
Then in your controller do this
/app/controllers/my_controller.rb
class MyController < MyBaseController
def my_action
order_obj = create_order_obj_from_hash(order_hash)
order_json = order_json_representation(order_obj)
status = 200
render json: order_json, status: status
end
private
def create_order_obj_from_hash(order_hash)
order_metadata_arr = order_hash[:metadata]
order_metadata_obj_arr = []
order_metadata_arr.each do |item_hash|
md_type = item_hash[:type]
md_obj = case md_type
when :shipping_data
ShippingData.new(address: item_hash[:address])
when :payment
PaymentData.new(stripe_customer_id: item_hash[:stripe_customer_id], stripe_card_id: item_hash[:stripe_card_id])
when :contact
ContactData.new(email: item_hash[:email], phone: item_hash[:phone])
else
// unsupported md_type
end
unless md_obj.nil?
omd_obj = OrderMetadata.new(metadata_type: md_type, metadata_data_obj: md_obj)
order_metadata_obj_arr << omd_obj
end
end
order_attrs = order_hash.slice(:id, :user_id, :amount)
order = Order.new(order_attrs)
order.order_metadata_obj_arr = order_metadata_obj_arr
order
end
def order_json_representation(order_obj)
template_name = "order.rabl"
template_path = Rails.root.join("app", "json_views")
RablRails.render(order_obj, template_name, view_path: template_path, format: :json)
end
end
Hope this helps.
Thanks.
I am getting info from my database
#teams = Team.select(:id,:name)
Then, I am rendering this info with an assosiation
render json: #teams, include: :players
This produce me the following input:
[
{
"id": 1,
"name": "Lyon Gaming",
"players": [
{
"name": "Jirall",
"position": "Superior",
},
{
"name": "Oddie",
"position": "Jungla",
}
]
}
]
How ever I need to sort the players in an specific order (not alphabetical)
I already have the sql statement to do it; how ever I dont know how to apply this method to each association.
I am looking something like:
render json: #teams, (include: :players, method: custom_order)
But this raise me an error.
Any ideas?
This should work:
render json: #teams, include: :players, methods: :custom_order
I'd like to add that if you will always want to return the object this way, you can add this to your model instead:
def as_json(options = nil)
super(include: :players, methods: :custom_order)
end
As Adam has mentioned, active_model_serializers is good to have if your render json's start getting ugly. If the complexity doesn't grow too much, I always recommend keeping the dependencies down.
https://github.com/rails-api/active_model_serializers
You need to read more about serializers in rails(This is good practice)
Here you have a nice article about this https://www.sitepoint.com/active-model-serializers-rails-and-json-oh-my/
Then you can make something like this
app/serializers/team_serializer.rb
class TeamSerializer < ActiveModel::Serializer
attributes :id, :name, :ordered_players
end
app/controllers/teams_controller.rb
def index
respond_with(Team.all.map(&TeamSerializer.method(:new)))
end
How can I expand associations more than one level deep? Right now I can expand reviews but am not sure how to also expand the patient_profile_id?
class Review
belongs_to :patient_profile
end
render json: doctors.to_json(
:include => {:reviews => {:include => :patient_profile_id }}
)
I'd highly suggest you check out the jbuilder gem. There's a great railscast that explains it's usage.
Basically, you will have to add a jbuilder file into your views, that gives you allot more control over your json.
For your specific use case you'd use something like this:
doctors/index.json.jbuilder
json.doctors #doctors do |json, doctor|
json.(doctor, :id, :name)
json.reviews doctor.reviews do |json, review|
json.(review, :id, :rating, :patient_profile_id)
json.patient_profile review.patient_profile do |json, profile|
json.(profile, :id, :name, ... ) # attributes of the profile
end
end
end
Try to use something like this:
render json: doctors.to_json(
:include => {:reviews => {:include => :patient_profile }}
)
Here you can find detail information how to serialize nested objects.
Check for overriding as_json method
Given a model:
class Person
validates_lenght_of :name, :maximum => 50
end
I have some view code that shows a countdown and enforces this maximum. However I hard coded the number 50 into that view code. Is there a way to extract this number from the model?
Something like:
Person.maximum_length_of_name
I tried this:
Person.validators_on(:name)
=> [#<ActiveRecord::Validations::UniquenessValidator:0x000001067a9840 #attributes=[:name], #options={:case_sensitive=>true}, #klass=Trigger(id: integer, name: string, created_at: datetime, updated_at: datetime, user_id: integer, slug: string, last_update_by: integer)>, #<ActiveModel::Validations::PresenceValidator:0x000001067a6c30 #attributes=[:name], #options={}>, #<ActiveModel::Validations::LengthValidator:0x000001067a3f08 #attributes=[:name], #options={:tokenizer=>#<Proc:0x00000101343f08#/Users/sjors/.rvm/gems/ruby-1.9.2-p0/gems/activemodel-3.0.6/lib/active_model/validations/length.rb:9 (lambda)>, :maximum=>50}>]
The information is in there, but I don't know how to extract it:
Use validators_on method
irb(main):028:0> p Person.validators_on(:name)[0].options[:maximum]
50
=> 50
As #Max Williams mentioned it works only on Rails 3
The problem with #nash answer is that validators do not own a certain order. I figured out how to do the same thing with just some more code but in some kind of safer mode ('cause you can add more validators later and break the order you get it):
(Person.validators_on(:name).select { |v| v.class == ActiveModel::Validations::LengthValidator }).first.options[:maximum]
I think it does only work for Rails 3 too.
[Edit 2017-01-17] Carefull my answer is old (2012) and was for Rails 3. It may not work / be ideal for newer Rails versions.
Just to bring a little more DRY spirit, you could create a generic class method to get maximum "length_validator" value on any attribute, like so:
Create a module in your lib directory and make it extend ActiveSupport::Concern:
module ActiveRecord::CustomMethods
extend ActiveSupport::Concern
end
# include the extension
ActiveRecord::Base.send(:include, ActiveRecord::CustomMethods)
Add the "module ClassMethods" in it and create the "get_maximum" method:
module ActiveRecord::CustomMethods
extend ActiveSupport::Concern
module ClassMethods
def get_maximum(attribute)
validators_on(attribute).select{|v| v.class == ActiveModel::Validations::LengthValidator}.first.options[:maximum]
end
end
end
EDIT 1: Configuration
You'll also have to add a require in one of your initializers.
For instance, here are my configurations:
I've put my file in lib/modules/active_record/extensions.
I've added this in my autoload_paths: config.autoload_paths +=
%W(#{config.root}/lib/modules) Note: this is not required, but best practice if you want to put there some of your custom classes and modules that you share between your apps.
In one of my initializers (config/initializers/custom.rb) I've added this line: require "active_record/extensions"
And that should do it! Restart your server and then...
END EDIT 1
And then you should be able to do something like this:
<%= form_for #comment do |f| %>
<%= f.text_area(:body, :maxlength => f.object.class.get_maximum(:body)) #Or just use Comment.get_maximum(:body) %>
<% end %>
I hope it will help others! :) Of course you can customize the method the way you want and add options and do fancy stuff. ;-)
More concise:
Person.validators_on(:name).detect { |v| v.is_a?(ActiveModel::Validations::LengthValidator) }.options[:maximum]
Uses detect{} instead of select{}.first and is_a? instead of class ==.
That works with Rails 4.1 as well.
Even more dynamic than Kulgar's answer would be to use something like this:
Person.validators_on(attribute)
.select { |v| v.class == ActiveRecord::Validations::LengthValidator }
.select { |v| v.options[:maximum].present? }.first.options[:maximum]
That way you can order the validators inside the model the way you want.
Use it as a Rails helper
You then could use this code to write a helper:
# Returns the maximum length for a given attribute of a model
def get_maxlength(model, attribute)
model.validators_on(attribute)
.select { |v| v.class == ActiveRecord::Validations::LengthValidator }
.select { |v| v.options[:maximum].present? }.first.options[:maximum]
end
And utilize the helper like this:
= f.text_field :name, maxlength: get_maxlength(f.object.class, :name) # Or use get_maxlength(Person, :name)
This is an indirect answer, but is an alternative solution, just in case it may prove useful to anyone.
Alternative Solution 1
class Person
MAX_LENGTHS = {
name: 50,
# email: 30, etc...
}.freeze
validates :name, length: { maximum: MAX_LENGTHS.fetch(:name) }
end
# usage example in view file
<%= Person.MAX_LENGTHS.fetch(:name) %>
Alternative Solution 2
... or if you prefer one-liners, or to not use a Hash constant
class Person
validates :name, length: { maximum: (MAX_NAME_LENGTH = 50) }
# validates :password, length: {
# minimum: (MIN_PASSWORD_LENGTH = 8),
# maximum: (MAX_PASSWORD_LENGTH = 70)
# }
# ... etc
end
# usage example in view file
<%= Person.MAX_NAME_LENGTH %>
If you're in rails 2, it's not easy. I remember trying this before and not getting far. You can get at the validation objects but they don't list which field they are for which seemed very odd to me. People have done plugins for this (ie to inspect or reflect on an AR object's validations), such as https://github.com/redinger/validation_reflection
Kulgar's answer is good and possibly ideal. Here is an easier way to do this for Rails 4 that does not require modifying any configuration, with the disadvantage that you have to add an include line to every model that wants to use it.
models/concerns/introspection.rb:
module Introspection
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def max_length(property)
Resource.validators_on(property.to_sym).
select{ |v| v.kind_of?(ActiveModel::Validations::LengthValidator) }.
first.options[:maximum]
end
end
end
Then in your model:
class Comment < ActiveRecord::Base
include Introspection
..
end
Then you can do something like this:
<%= form_for #comment do |f| %>
<%= f.text_area(:body, :maxlength => f.object.class.max_length(:body)) %> # Or just use Comment.max_length(:body) %>
<% end %>