Conditional attributes in Active Model Serializers - ruby-on-rails

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

Related

How to use devise current_user in Active Model Serializers

I'm using Active Model Serializer 0.10.7 in rails 5
and I wanna know how to access devise current_user in serializer.
current_user is supposed to be set for scope by default.
according to doc
https://github.com/rails-api/active_model_serializers/blob/0-10-stable/docs/general/serializers.md#controller-authorization-context
but my code doesn't work well...
anybody knows about this?
class BookSerializer < ActiveModel::Serializer
attributes :id, :title, :url, :image, :is_reviewed
def is_reviewed
object.reviews.pluck(:user_id).include?(current_user.id)
end
end
and Book controller look like this.
class BooksController < ApplicationController
def index
#books = Book.order(created_at: :desc).page(params[:page])
respond_to do |format|
format.html
format.json {render json: #books, each_serializer: BookSerializer}
end
end
end
It is possible to pass a scope into your serializer when instantiating it in the controller (or elsewhere for that matter). I appreciate that this is for individual objects and not arrays of objects:
BookSerializer.new(book, scope: current_user)
Then in your Book Serializer you can do:
class BookSerializer < ActiveModel::Serializer
attributes :id, :title, :url, :image, :is_reviewed
private
def is_reviewed
object.reviews.pluck(:user_id).include?(current_user.id)
end
def current_user
scope
end
end
Devise doesn't expose the current_user helper to models or serializers - you can pass the value to the model from the controller, or set it in a storage somewhere.
Some examples from other answers:
https://stackoverflow.com/a/3742981/385532
https://stackoverflow.com/a/5545264/385532
in application controller:
class ApplicationController < ActionController::Base
...
serialization_scope :view_context
end
in serializer:
class BookSerializer < ActiveModel::Serializer
attributes :id, :title, :url, :image, :is_reviewed
def is_reviewed
user = scope.current_user
...
end
end
If you are using active_model_serializers gem, then it is straight forward.
In your serializer just use the keyword scope.
Eg:-
class EventSerializer < ApplicationSerializer
attributes(
:id,
:last_date,
:total_participant,
:participated
)
def participated
object.participants.pluck(:user_id).include?(scope.id)
end
end
You can also pass view context into serializer from controller when you want to intialize serializer your self instead of render json: book.
# controller
BookSerializer.new(book, scope: view_context)
# serializer
class BookSerializer < ActiveModel::Serializer
attributes :id, :title, :url, :image, :is_reviewed
private
def is_reviewed
object.reviews.pluck(:user_id).include?(scope.current_user.id)
end
end
seems there is a typo, you have is_reviewed and you defined method has_reviewed
so it should be like this
class BookSerializer < ActiveModel::Serializer
attributes :id, :title, :url, :image, :is_reviewed
def is_reviewed
object.reviews.pluck(:user_id).include?(current_user.id)
end
end

How to DRY up Ruby initialize method?

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.

Rails - validation inside decorator

I'm struggling with some kind of issue. I have a rails model (mongoid).
class User
include Mongoid::Document
include ActiveModel::SecurePassword
validate :password_presence,
:password_confirmation_match,
:email_presence,
field :email
field :password_digest
def password_presence
end
def email_presence
end
def password_confirmation_match
end
end
My goal is to call validations depends on which decorator I will use. Let's say I've got two decorators:
class PasswordDecorator < Draper::Decorator
def initialize(user)
#user = user
end
end
def RegistraionDecorator < Draper::Decorator
def initialize(user)
#user = user
end
end
So now when I create/save/update my user object inside RegistraionDecorator I would like to perform all validation methods.
RegistraionDecorator.new(User.new(attrbiutes))
But when I will do it inside PasswordDecorator I want to call for example only password_presence method.
PasswordDecorator.new(User.first)
When I move validations to decorator it won't work cuz its different class than my model.
How can I achieve that?
Try to use a Form Object pattern instead.
Here is an example (from a real project) of how it could be done with reform.
class PromocodesController < ApplicationController
def new
#form = PromocodeForm.new(Promocode.new)
end
def create
#form = PromocodeForm.new(Promocode.new)
if #form.validate(promo_params)
Promocode.create!(promo_params)
redirect_to promocodes_path
else
render :edit
end
end
private
def promo_params
params.require(:promocode).
permit(:token, :promo_type, :expires_at, :usage_limit, :reusable)
end
end
class PromocodeForm < Reform::Form
model :promocode
property :token
property :promo_type
property :expires_at
property :usage_limit
property :reusable
validates_presence_of :token, :promo_type, :expires_at, :usage_limit, :reusable
validates_uniqueness_of :token
validates :usage_limit, numericality: { greater_or_equal_to: -1 }
validates :promo_type, inclusion: { in: Promocode::TYPES }
end
Bonus: The model does not trigger validations and much easy to use in tests.

Rails ActiveModel Serializers render not null attributes

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.

Rails 4 strong params get permit from nested model

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

Resources