How to use devise current_user in Active Model Serializers - ruby-on-rails

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

Related

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.

Strong parameters: extending a controller and want to add required params to existing ones

I have the following controller:
class BoilerplatesController < InheritedResources::Base
load_and_authorize_resource
private
def boilerplate_params
params.require(:boilerplate).permit(:title)
end
end
Then I extend it with this one:
class BoilerplateCopiesController < BoilerplatesController
defaults instance_name: 'boilerplate',
resource_class: BoilerplateCopy
private
def boilerplate_params
params.require(:boilerplate).permit(findings_attributes: [:id, :url])
super
end
end
It seems that the extending one doesn't add the new fields to the existing ones, but overrules them. How can this be implemented?
One possible solution (or workaround) would be:
BoilerplatesController:
def boilerplate_params
params.require(:boilerplate).permit(required_params)
end
def required_params
[:title,
:intro,
:lock_version,
:outro,
:resources,
:topic_id,
:tags,
:how_to_evaluate,
:status,
:priority]
end
BoilerplateCopiesController:
def required_params
super << {findings_attributes: [:id,
:url,
:description,
:screenshot,
:remove_screenshot]
}
end

Serialize JSON association for children of a model

I'm using active-model-serializers for my API.
I have a model (Task) that has many subtasks(always Task model), called children.
I do this recursive has_many association thanks to ancestry gem (https://github.com/stefankroes/ancestry)
It works all enough well, but I have this problem:
Task has an association with User, but while active-model-serializers, export user for the main object, it doesn't show user details for also all children.
This is my serializer:
class TaskSerializer < ActiveModel::Serializer
attributes :id, :name, :details, :user_id
belongs_to :user
has_many :children
end
This is my controller:
class Api::V1::TasksController < Api::V1::BaseController
respond_to :json
def index
#tasks = current_user.company.tasks
respond_with #tasks, location: nil
end
end
And this is my model:
class Task < ActiveRecord::Base
has_ancestry
belongs_to :user
end
I've also tried to do this in my model:
class Api::V1::TasksController < Api::V1::BaseController
respond_to :json
def index
#tasks = current_user.company.tasks
render json: #tasks,
each_serializer: TaskSerializer,
status: :ok
end
end
But doesn't work...I've the user details for the parent object, but not for the children(where he only show me user_id, without all User object)
Any suggestions ?
Have you tried adding a serializer for the Children model or querying them as a explicit attribute like so?
class TaskSerializer < ActiveModel::Serializer
attributes :id, :name, :details, :user_id, :children
def children
object.children
end
end

Conditional attributes in Active Model Serializers

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

How can I pass params from a controller to my serializers?

For a JSON API, I need to pass url params to my serializes:
http://mydomain.com/api/categories?name=news&counter=123
This is my API controller:
class Api::CategoriesController < ApplicationController
respond_to :json
def index
respond_with Category.where("name ==? AND content_counter >?", params[:name], params[:counter].to_i)
end
end
My serializer looks like this:
class CategorySerializer < ActiveModel::Serializer
attributes :id, :name, :content_counter
has_many :chapters
def chapters
object.chapters.active.with_counter(???)
end
end
In my chapter model I have a scope:
scope :with_counter, lambda { |counter| where("content_counter >?", counter.to_i) }
How can I pass the counter value 123 into (???)
Is that possible?
Any help would be greatly appreciated.
You can pass values to an activemodel serializer using the #options object like this:
class Api::CategoriesController < ApplicationController
respond_to :json
def index
respond_with Category.where("name ==? AND content_counter >?", params[:name], params[:counter].to_i),
counter_value: params[:counter]
end
end
class CategorySerializer < ActiveModel::Serializer
attributes :id, :name, :content_counter
has_many :chapters
def chapters
object.chapters.active.with_counter(#options[:counter_value])
end
end

Resources