I would like to do the same trick as in this post - https://thoughtbot.com/blog/fast-json-apis-in-rails-with-key-based-caches-and, but it is impossible to do in AMS 0.10.0.rc because AMS doesn't have a #serializable_hash method.
What would you suggest doing instead?
Updated to the new version of active_model_serializers :
class BusinessDistanceSerializer < ActiveModel::Serializer
attributes :distance
def attributes options = {}, reload = false
data = super
data.reverse_merge! ActiveModel::Serializer.adapter.new(BusinessSerializer.new(object)).serializable_hash
data
end
end
Notice the reload = false argument.
This is how i'm doing it.
class BusinessDistanceSerializer < ActiveModel::Serializer
attributes :distance
def attributes options = {}
data = super
data.reverse_merge! ActiveModel::Serializer.adapter.new(BusinessSerializer.new(object)).serializable_hash
data
end
end
Related
Using Active Model Serializer, is there an easy and integrated way to return a JSON "object" (that would then be converted in a javascript object by the client framework) instead of a JSON "array" when serializing a collection? (I am quoting object and array, since the returned JSON is by essence a string).
Let's say I have the following ArticleSerializer:
class ArticleSerializer < ActiveModel::Serializer
attributes :id, :body, :posted_at, :status, :teaser, :title
end
I call it from ArticlesController:
class ArticlesController < ApplicationController
def index
#feed = Feed.new(articles: Article.all)
render json: #feed.articles, each_serializer: ArticleSerializer
end
end
Is there a way to pass an option to the serializer to make it return something like:
{"articles":
{
"1":{
...
},
"2":{
...
}
}
}
instead of
{"articles":
[
{
"id":"1",
...
},
{
"id":"2"
...
}
]
}
Edit: I guess that the approach proposed in this post (subclassing AMS ArraySerializer) might be helpful (Active Model Serializer and Custom JSON Structure)
You'd have to write a custom adapter to suit your format.
Alternatively, you could modify the hash before passing it to render.
If you do not mind iterating over the resulting hash, you could do:
ams_hash = ActiveModel::SerializableResource.new(#articles)
.serializable_hash
result_hash = ams_hash['articles'].map { |article| { article['id'] => article.except(:id) } }
.reduce({}, :merge)
Or, if you'd like this to be the default behavior, I'd suggest switching to the Attributes adapter (which is exactly the same as the Json adapter, except there is no document root), and override the serializable_hash method as follows:
def format_resource(res)
{ res['id'] => res.except(:id) }
end
def serializable_hash(*args)
hash = super(*args)
if hash.is_a?(Array)
hash.map(&:format_resource).reduce({}, :merge)
else
format_resource(hash)
end
end
No, semantically you are returning an array of Articles. Hashes are simply objects in Javascript, so you essentially want an object with a 1..n method that returns each Article, but that would not make much sense.
I have a model with the following method:
class Book < ActiveRecord:Base
def as_json(options = nil)
if options.nil?
super(only: [:id, :title, :status, :description])
else
super(options)
end
end
end
I also have a controller that looks like:
class BookController < ApplicationController
def search
books = Book.where(title: params[:keyword])
render json: books.as_json(only: [:id, :title])
end
end
Is it possible to override the :title symbol with another name that is needed by a 3rd party application? I'd like to change the :title to :value just for pushing out via this JSON call.
I've tried doing several different things to override (without writing special rules in as_json, as this is called from serveral different locations in the application).
Thanks in advance!
So, this the solution I ended up using:
JSON before .tap call:
[{"id"=>4, "book_code"=>"11-292454", "title"=>"How the world turns."}]
For single JSON object:
json = json.tap { |hash| hash["value"] = hash.delete "title" }
For JSON array:
json = json.each do |j|
j.tap { |hash| hash["value"] = hash.delete "title" }
end
JSON after .tap call:
[{"id"=>4, "book_code"=>"11-292454", "value"=>"How the world turns."}]
Don't know if it's the right way, but it seems to be working for me.
I want to be able to create attributes on a ActiveRecord:Base Model that are nested.
For example -
class Book < ActiveRecord::Base
attr_accessor :operator, :who
I would like :who to have further attributes like :family, :me
So finally I can access these as follows
book = Book.new
book.who.family = [1,2,3]
book.who.me = 1
I also want to know how can I define the kind of values that attributes can take so I do not have to do that at runtime.
Currently am using something like this
after_initialize do
#who = {family: [], me: nil}
end
I tried it in my console. So if you add the your attributes as attribute accessors and after_initialize set them like this.
attr_accessor :who, :operator
after_initialize do
self.who = {family: [], me: nil}
self.operator = "Minus"
end
then you can access them like this
self.who[:family] = [1,2,3,4]
or
self.operator = "Minus"
and you can simply access them like this
self.who[:family] ==> [1,2,3]
Will this help?
I have some methods in my model which I use to access stored text hashes in my views:
class CarSpec < ActiveRecord::Base
##fuel_type_select_data = Hash[(1..5).to_a.zip(['Petrol', 'Diesel', 'Gas/petrol', 'Hybrid', 'Electric'])]
##mileage_type_select_data = Hash[(1..2).to_a.zip(['km', 'miles'])]
##transmission_select_data = Hash[(1..3).to_a.zip(['Manual', 'Automatic', 'Tiptronic'])]
##wheel_drive_data = Hash[(1..3).to_a.zip(['Front', 'Rear', 'All'])]
##color_data = Hash[(1..8).to_a.zip(['black', 'white', 'beige',
'blue', 'yellow', 'green', 'red', 'silver'])]
def text_for_fuel_type
##fuel_type_select_data[fuel_type]
end
def text_for_mileage_type
##mileage_type_select_data[mileage_type]
end
def text_for_transmission
##transmission_select_data[transmission]
end
def text_for_wheel_drive
##wheel_drive_data[wheel_drive]
end
def text_for_color
##color_data[color]
end
def text_for_interior_color
##color_data[interior_color]
end
Currently, I need to write a new method for every field. How can I refactor these methods, so that I do not need to write a new method for each field? Please include how the new method/s would be called in the view.
Using constants instead of class variables would be much better in your case.
An example:
class CarSpec < ActiveRecord::Base
WHEEL_DRIVE_DATA = {'Front' => 1}
# remaining class code
end
Sample code for view:
<%= CarSpec::WHEEL_DRIVE_DATA['Front'] %>
Agree with #Humza: constants are better than class variables here.
The current set of methods can be defined dynamically - with some metaprogramming magic - as follows:
fields = %w{ fuel_type mileage_type transmission wheel_drive interior_color }
fields.each do |field|
define_method("text_for_#{field}") do
CarSpec.class_variable_get("###{field}_select_data").send(:[], eval(field))
end
end
Note:
For the above to work, the class variables for all fields will have to be consistent with the ###{field}_select_data name; so ##wheel_drive_data should be changed to ##wheel_drive_select_data, etc.
For example in my Car model i have such fields:
color, price, year
and in form partial i generate form with all this fields. But how to code such logic:
user could enter color and year and i must find with this conditions, user could enter just year or all fields in same time...
And how to write where condition? I could write something like:
if params[:color].present?
car = Car.where(color: params[:color])
end
if params[:color].present? && params[:year].present?
car = Car.where(color: params[:color], year: params[:year])
end
and so over....
But this is very ugly solution, i'm new to rails, and want to know: how is better to solve my problem?
Check out the has_scope gem: https://github.com/plataformatec/has_scope
It really simplifies a lot of this:
class Graduation < ActiveRecord::Base
scope :featured, -> { where(:featured => true) }
scope :by_degree, -> degree { where(:degree => degree) }
scope :by_period, -> started_at, ended_at { where("started_at = ? AND ended_at = ?", started_at, ended_at) }
end
class GraduationsController < ApplicationController
has_scope :featured, :type => :boolean
has_scope :by_degree
has_scope :by_period, :using => [:started_at, :ended_at], :type => :hash
def index
#graduations = apply_scopes(Graduation).all
end
end
Thats it from the controller side
I would turn those into scopes on your Car model:
scope :by_color, lambda { |color| where(:color => color)}
scope :by_year, lambda { |year| where(:year => year)}
and in your controller you would just conditionally chain them like this:
def index
#cars = Car.all
#cars = #cars.by_color(params[:color]) if params[:color].present?
#cars = #cars.by_year(params[:year]) if params[:year].present?
end
user_params = [:color, :year, :price]
cars = self
user_params.each do |p|
cars = cars.where(p: params[p]) if params[p].present?
end
The typical (naive, but simple) way I would do this is with a generic search method in my model, eg.
class Car < ActiveRecord::Base
# Just pass params directly in
def self.search(params)
# By default we return all cars
cars = all
if params[:color].present?
cars = cars.where(color: params[:color])
end
if params[:price1].present? && params[:price2].present?
cars = cars.where('price between ? and ?', params[:price1], params[:price2])
end
# insert more fields here
cars
end
end
You can easily keep chaining wheres onto the query like this, and Rails will just AND them all together in the SQL. Then you can just call it with Car.search(params).
I think you could use params.permit
my_where_params = params.permit(:color, :price, :year).select {|k,v| v.present?}
car = Car.where(my_where_params)
EDIT: I think this only works in rails 4, not sure what version you're using.
EDIT #2 excerpt from site I linked to:
Using permit won't mind if the permitted attribute is missing
params = ActionController::Parameters.new(username: "john", password: "secret")
params.permit(:username, :password, :foobar)
# => { "username"=>"john", "password"=>"secret"}
as you can see, foobar isn't inside the new hash.
EDIT #3 added select block to where_params as it was pointed out in the comments that empty form fields would trigger an empty element to be created in the params hash.