I want to use a serializer that renders not null attributes
class PersonSerializer < ActiveModel::Serializer
attributes :id, :name, :phone, :address, :email
end
Is this possible.
Many thanks.
Solution:
class PersonSerializer < ActiveModel::Serializer
attributes :id, :name, :phone, :address, :email
def attributes
hash = super
hash.each {|key, value|
if value.nil?
hash.delete(key)
end
}
hash
end
end
From version 0.10.x of active_model_serializer gem, you have to override the method serializable_hash instead of attributes:
# place this method inside NullAttributesRemover or directly inside serializer class
def serializable_hash(adapter_options = nil, options = {}, adapter_instance = self.class.serialization_adapter_instance)
hash = super
hash.each { |key, value| hash.delete(key) if value.nil? }
hash
end
Thanks Nabila Hamdaoui for your solution.
I made it a little more reusable via modules.
null_attribute_remover.rb
module NullAttributesRemover
def attributes
hash = super
hash.each do |key, value|
if value.nil?
hash.delete(key)
end
end
hash
end
end
Usage:
swimlane_serializer.rb
class SwimlaneSerializer < ActiveModel::Serializer
include NullAttributesRemover
attributes :id, :name, :wipMaxLimit
end
class ActiveModel::Serializer
def attributes
filter(self.class._attributes.dup).each_with_object({}) do |name, hash|
val = send(name)
hash[name] = val unless val.nil?
end
end
end
Please add validation presence:true in your Person model for (:id, :name, :phone, :address, :email) attributes, so you will get not null JSON value while you render.
Related
In my Rails application I have this class:
class Plan
attr_reader :name, :id, :amount, :interval, :maximum, :features
def initialize(id, name, amount, interval, maximum, features)
#id = id
#name = name
#amount = amount
#interval = interval
#maximum = maximum
#features = features
end
...
end
Is there a way to DRY up this class?
You can do it in one line:
def initialize(*args)
#id, #name, #amount, #interval, #maximum, #features = args
end
If you don't mind replacing attr_reader with attr_accessor, then you can use this, which will catch up the invalid attributes that may be provided when creating a new object:
class Plan
attr_accessor :name, :id, :amount, :interval, :maximum, :features
def initialize params = {}
params.each { |key, value| send "#{key}=", value }
end
end
If you don't mind attr_accessor instead of attr_reader, then you can use Struct. That's exactly what it does: take care of all the boilerplate in the initializer.
Plan = Struct.new(:name, :id, :amount, :interval, :maximum, :features) do
# def my_other_methods
# ...
# end
end
plan = Plan.new('Joe', 1, 500)
plan.name # => "Joe"
plan.id # => 1
plan.amount # => 500
plan.interval # => nil
how about:
def initialize(params = {})
params.each{ |k,v| instance_variable_set("##{k}", v)
end
what I usually do when I have more than two classes with initializers like this:
# initialize.rb
module Initialize
def initialize(params={})
params.each do |attr, value|
public_send("#{attr}=", value)
end if params
end
end
# plan.rb
class Plan
include Initialize
attr_accessor :name, :id, :amount, :interval, :maximum, :features
def initialize(id:, name:, amount:, interval:, maximum:, features:)
# some additional initialization code could go there
super
end
end
This way individual setters could be made to additionally cleanup data, you can define defaults, or you can delegate some setters.
How do I render an attribute only if some condition is true?
For example, I want to render User's token attribute on create action.
In the latest version (0.10.x), you can also do it this way:
class EntitySerializer < ActiveModel::Serializer
attributes :id, :created_at, :updated_at
attribute :conditional_attr, if: :condition?
def condition?
#condition code goes here
end
end
For example:
class UserSerializer < ActiveModel::Serializer
attributes :id, :username, :name, :email, :created_at, :updated_at
attribute :auth_token, if: :auth_token?
def created_at
object.created_at.to_i
end
def updated_at
object.updated_at.to_i
end
def auth_token?
true if object.auth_token
end
end
EDIT (Suggested by Joe Essey) :
This method does not work with latest version (0.10)
With the version 0.8 it is even simpler. You don't have to use the if: :condition?. Instead you can use the following convention to achieve the same result.
class EntitySerializer < ActiveModel::Serializer
attributes :id, :created_at, :updated_at
attribute :conditional_attr
def include_conditional_attr?
#condition code goes here
end
end
The example above would look like this.
class UserSerializer < ActiveModel::Serializer
attributes :id, :username, :name, :email, :created_at, :updated_at
attribute :auth_token
def created_at
object.created_at.to_i
end
def updated_at
object.updated_at.to_i
end
def include_auth_token?
true if object.auth_token
end
end
See 0.8 documentation for more details.
you can override the attributes method, here is a simple example:
class Foo < ActiveModel::Serializer
attributes :id
def attributes(*args)
hash = super
hash[:last_name] = 'Bob' unless object.persisted?
hash
end
end
You could start by setting a condition on the serializers 'initialize' method. This condition can be passed from wherever else in your code, included in the options hash that 'initialize' accepts as second argument:
class SomeCustomSerializer < ActiveModel::Serializer
attributes :id, :attr1, :conditional_attr2, :conditional_attr2
def initialize(object, options={})
#condition = options[:condition].present? && options[:condition]
super(object, options)
end
def attributes(*args)
return super unless #condition #get all the attributes
attributes_to_remove = [:conditional_attr2, :conditional_attr2]
filtered = super.except(*attributes_to_remove)
filtered
end
end
In this case attr1 would always be passed, while the conditional attributes would be hidden if the condition is true.
You would get the result of this custom serialization wherever else in your code as follows:
custom_serialized_object = SomeCustomSerializer.new(object_to_serialize, {:condition => true})
I hope this was useful!
Serializer options were merged into ActiveModel Serializers and now are available (since 0.10).
Override is a good idea, but if you use the super the attributes will be calculated before you remove what you want. If it does not make difference to you, ok, but when it does, you can use it:
def attributes(options={})
attributes =
if options[:fields]
self.class._attributes & options[:fields]
else
self.class._attributes.dup
end
attributes.delete_if {|attr| attr == :attribute_name } if condition
attributes.each_with_object({}) do |name, hash|
unless self.class._fragmented
hash[name] = send(name)
else
hash[name] = self.class._fragmented.public_send(name)
end
end
end
ps: v0.10.0.rc3
Here is how you can pass parameters directly to the serializer instance and show or hide attributes based on these parameters in the serializer declaration.
It also works with parent-child serializers.
Controller or parent serializer:
ActiveModelSerializers::SerializableResource.new(object.locations, {
each_serializer: PublicLocationSerializer,
params: {
show_title: true
},
})
Serializer with conditions:
class PublicLocationSerializer < ActiveModel::Serializer
attributes :id, :latitude, :longitude, :title
def title
object.title if #instance_options[:params][:show_title]
end
end
There are several questions for strong params, but I couldn't find any answer for achieving my goal. Please excuse any duplicates (and maybe point me in the right direction).
I'm using strong params in a model that has several 'has_one' associations and nested attributes with 'accepts_attributes_for'.
In my routes I have: (updated for better understanding)
resources :organisations do
resources :contact_details
end
So, i.e. for one associated model I have to use
def organisation_params
params.require(:organisation).permit(:org_reference, :supplier_reference, :org_type, :name, :org_members, :business, :contact_person, contact_detail_attributes: [:id, :contactable_id, :contactable_type, :phone, :fax, :mail, :state, :province, :zip_code, :street, :po_box, :salutation, :title, :last_name, :first_name, :description])
end
This works, but I have to retype all my permitted params for each associated model. When I modify my permitted attributes for contact_details , I have to change it in several locations (every model that has the polymorphic association).
Is there a way to get the parameter whitelist of contact_details and include it into the parent whitelist?
Something like:
def organisation_params
my_params = [:org_reference, :supplier_reference, :org_type, :name, :org_members, :business, :contact_person]
contact_params = #get permitted params, that are defined in contact_details_controller
params.require(:organisation).permit(my_params, contact_params)
end
I don't want to workaround security, but I had already defined the permitted attributes for the contact_details and don't want to repeat it in every associated "parent" model (because it's exhausting and very prone to stupid mistakes like omitting one attribute in one of several parent models).
Use a method defined inside ApplicationController, or a shared module:
ApplicationController:
class ApplicationController
def contact_details_permitted_attributes
[:id, :contactable_id, :contactable_type, ...]
end
end
class ContactDetailsController < ApplicationController
def contact_details_params
params
.require(contact_details)
.permit(*contact_details_permitted_attributes)
end
end
class OrganisationsController < ApplicationController
def organisation_params
params
.require(:organisation)
.permit(:org_reference, ...,
contact_detail_attributes: contact_details_permitted_attributes)
end
end
Shared module:
module ContactDetailsPermittedAttributes
def contact_details_permitted_attributes
[:id, :contactable_id, :contactable_type, ...]
end
end
class ContactDetailsController < ApplicationController
include ContactDetailsPermittedAttributes
def contact_details_params
params
.require(contact_details)
.permit(*contact_details_permitted_attributes)
end
end
class OrganisationsController < ApplicationController
include ContactDetailsPermittedAttributes
def organisation_params
params
.require(:organisation)
.permit(:org_reference, ...,
contact_detail_attributes: contact_details_permitted_attributes)
end
end
Rails has even dedicated directories for shared modules, concerns inside app/controllers and app/models; indeed, in your case you should use app/controllers/concerns
I don't see why not. In your ApplicationController you could have
def contact_attributes
[:id, :contactable_id, :contactable_type, :phone, :fax,
:mail, :state, :province, :zip_code, :street, :po_box,
:salutation, :title, :last_name, :first_name, :description]
end
Then in your organisation_params
def organisation_params
my_params = [:org_reference, :supplier_reference, :org_type, :name, :org_members, :business, :contact_person]
params.require(:organisation).permit(*my_params, contact_detail_attributes: contact_attributes)
end
In some other location you might do...
def contact_params
params.require(:contact).permit(*contact_attributes)
end
How do I get ActiveRecord attributes method functionality? I have this class:
class PurchaseForm
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :name,
:surname,
:email
validates_presence_of :name
validates_format_of :email, :with => /^[-a-z0-9_+\.]+\#([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
def initialize(attributes = {}, shop_name)
if not attributes.nil?
attributes.each do |name, value|
send("#{name}=", value)
end
end
def persisted?
false
end
end
What I need to do, to have an attributes method to list all names and values from PurchaseForm object?
Here is the refactored variant:
class PurchaseForm
include ActiveModel::Model
def self.attributes
[:name, :surname, :email]
end
attr_accessor *self.attributes
# your validations
def to_hash
self.class.attributes.inject({}) do |hash, key|
hash.merge({ key => self.send(key) })
end
end
end
Now you can easily work with this class:
irb(main):001:0> a = PurchaseForm.new({ name: 'Name' })
=> #<PurchaseForm:0x00000002606b50 #name="Name">
irb(main):002:0> a.to_hash
=> {:name=>"Name", :surname=>nil, :email=>nil}
irb(main):003:0> a.email = 'user#example.com'
=> "user#example.com"
irb(main):004:0> a
=> #<PurchaseForm:0x00000002606b50 #name="Name", #email="user#example.com">
irb(main):005:0> a.to_hash
=> {:name=>"Name", :surname=>nil, :email=>"user#example.com"}
Even more, if you want to make this behaviour reusable, consider extraction of .attributes and #to_hash methods into separate module:
module AttributesHash
extend ActiveSupport::Concern
class_methods do
def attr_accessor(*args)
#attributes = args
super(*args)
end
def attributes
#attributes
end
end
included do
def to_hash
self.class.attributes.inject({}) do |hash, key|
hash.merge({ key => self.send(key) })
end
end
end
end
Now, just include it to your model and you're done:
class PurchaseForm
include ActiveModel::Model
include AttributesHash
attr_accessor :name, :surname, :email
# your validations
end
#instance_values could do the job:
class PurchaseForm
attr_accessor :name, :email
def attributes
instance_values
end
end
Output sample:
purchase.attributes #=> {"name"=>"John", "email"=>"john#example.com"}
I've managed to solve problem with this code:
class PurchaseForm
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :attributes,
:name,
:surname,
:email
validates_presence_of :name
validates_format_of :email, :with => /^[-a-z0-9_+\.]+\#([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
def initialize(attributes = {})
#attributes = attributes
end
def persisted?
false
end
end
Let's try this
self.as_json
=> {:name=>"Name", :surname=>nil, :email=>"user#example.com"}
would it not be better to use
include ActiveModel::Serialization
def attributes
JSON.parse(self.to_json)
end
After searching for a tableless model example I came across this code which seems to be the general consensus on how to create one.
class Item < ActiveRecord::Base
class_inheritable_accessor :columns
self.columns = []
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
end
def all
return []
end
column :recommendable_type, :string
#Other columns, validations and relations etc...
end
However I would also like it to function, as a model does, representing a collection of object, so that I can do Item.all.
The plan is to populate Items with files and each Item's properties will be extracted from the files.
However currently if I do Item.all I get a
Mysql2::Error Table 'test_dev.items' doesn't exist...
error.
I found an example at http://railscasts.com/episodes/219-active-model where I can use model features and then override static methods like all (should have thought of this before).
class Item
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :name, :email, :content
validates_presence_of :name
validates_format_of :email, :with => /^[-a-z0-9_+\.]+\#([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
validates_length_of :content, :maximum => 500
class << self
def all
return []
end
end
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
def persisted?
false
end
end
Or you could do it like this (Edge Rails only):
class Item
include ActiveModel::Model
attr_accessor :name, :email, :content
validates_presence_of :name
validates_format_of :email, :with => /^[-a-z0-9_+\.]+\#([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
validates_length_of :content, :maximum => 500
end
By simply including ActiveModel::Model you get all the other modules included for you. It makes for a cleaner and more explicit representation (as in this is an ActiveModel)