How to make RESTful root keys in versioned Active model serializer? - ruby-on-rails

I have ruby on rails app with user_controller generated via scaffold.
# app/controllers/api/v1/users_controller.rb
class Api::V1::UsersController < Api::V1::ApiController
skip_before_action :verify_authenticity_token
serialization_scope :view_context
def show
render json: #user
end
end
The model
# app/models/api/v1/user.rb
class Api::V1::User < Api::V1::ApiRecord
has_one_time_password
validates_presence_of :phone
end
And serializer:
# app/serializers/api/v1/user_serializer.rb
class Api::V1::UserSerializer < ActiveModel::Serializer
attributes :id, :phone, :first_name, :email, :dob, :last_name, :gender, :otp_code
def otp_code
object.otp_code
end
end
Everything is fine but i got stuck in configuration. /api/v1/users/2 gives me below response.
{
"api/v1/user": {
"id": 2,
"phone": "999999999",
"first_name": "Rajan",
"email": "sample#h.com",
"dob": "2000-01-01",
"last_name": "Verma",
"gender": "male",
"otp_code": "503036"
}
}
Did you saw the root key? why it is coming with full namespace? it should be { "user": { ...data } } only.
I don't want to apply and patch or hacks for this trivial one. I think i am missing any configuration which i am not able to find in documentation.
Please help.

So it seems ActiveModel::Serializer uses the full model name including modules as the root key, see
https://github.com/rails-api/active_model_serializers/blob/0-10-stable/lib/active_model/serializer.rb#L384-L391
So either you set the root key in your controller.
class UsersController < ApplicationController
def index
render json: #users, root: "users"
end
end
Or if you never want to include the full name in your serializer you could create a base serializer
# app/serializers/api/base_serializer.rb
class BaseSerializer < ActiveModel::Serializer
def json_key
object.class.model_name.to_s.demodulize.underscore
end
end
# app/serializers/api/v1/user_serializer.rb
class Api::V1::UserSerializer < BaseSerializer
attributes :id, :phone, :first_name, :email, :dob, :last_name, :gender, :otp_code
def otp_code
object.otp_code
end
end

The reason this happens is that your model is Api::V1::User which most probably was autogenerated by the scaffold.
Are you sure you'll be needing versioning in your model?
Maybe having a model User and apply the namespacing versioning to solely to your controllers and routes will be enough for your application.
If you do want to have the Vx namespace to your models as well, then you can override the json_key as Christian Bruckmayer suggests, either to all serializers or explicitly to Api::V1::UserSerializer

What is https://github.com/bsm/serialization_scopes doing in there? The line serialization_scope :view_context could be removed no?

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 include token and other session information to send to front end?

I have the following method in an API controller:
module Api
module V1
class ArticlesController < Api::BaseController
def show
article = Article.find(params[:id])
render json: article
end
end end end
And while using the active_model_serializers gem, I have the following serializer:
class Api::V1::ArticleSerializer < ActiveModel::Serializer
attributes :id, :author_id, :title, :description
end
For all API requests the server should include information about the session, such as the api token and the current user. So in the above case the generated json shouldn't only include the attributes mentioned in the articles serializer but also for example the api token.
Where and how does one normally include this session information to send to the API front end? Is this perhaps in a separate serializer that is included in addition to in this case the articles serializer?
Since you want this information attached to all API responses, it makes sense to have a superclass serializer responsible for this, which all other serializers inherit from:
class SerializerWithSessionMetadata < ActiveModel::Serializer
attributes :token, :user
def token
# ...
end
def user
# ...
end
end
Then your serializers would inherit from this instead of ActiveModel::Serializer:
class ArticleSerializer < SerializerWithSessionMetadata
# ...
end
Alternatively, you could make it a module that you include in your serializers:
module SessionMetadataSerializer
def self.included(klass)
klass.attributes :token, :user
end
def token
# ...
end
# ...
end
Then:
class Api::V1::ArticleSerializer < ActiveModel::Serializer
include SessionMetadataSerializer
attributes :id, :author_id, :title, :description
end

Mixed attr_accessible and strong_parameters, how to skip attr_accessible?

First of all, I am fully aware of the bad practice with mixing the two.
I have a model that has attr_accessible set up. I'd like to start transitioning our application to strong_parameters. The problem is that I need to do this piecemeal as we refactor individual parts of the application. Is there a ActiveRecord method I can use to update the attributes that bypasses attr_accessible for right now? Or can I define a attr_accessible=false type of thing that bypasses it?
Code example:
Model:
class User < ActiveRecord::Base
attr_accessible :first_name, :last_name, :email, :password
end
Controller:
class UsersController < ApplicationController
before_action :set_user
def update
#user.assign_attributes(user_params)
#user.save!
end
private
def set_user
#user = User.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def user_params
params.require(:user).permit(
:first_name, :last_name, :email, :other_attribute_not_in_accessible
)
end
end
Found it!
In the controller, do this:
#user.update_attributes(user_params,:without_protection=>true)
And then it'll work.

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

Customize Devise user JSON response on creation of a user account

How do I customize the JSON output on creation of a devise User?
### User.rb ###
class User < ActiveRecord::Base
devise :database_authenticatable,
:registerable, ...
...
end
### Routes.rb ###
...
devise_for :users, :controllers => {:registrations => "registrations"}
...
I've got some extra fields in my User table that are secret, but they get returned in the JSON response when I do a User creation via JSON like this:
$ curl -H "Content-Type: application/json" -d '{"user" : {"username":"someone","email":"someone#somewhere.com","password":"awesomepass"}}' -X POST http://localhost:3000/users.json
which returns:
{"user":{"secret_field_1":"some value","secret_field_2":"some value","created_at":"2013-07-25T21:24:50-05:00","email":"someone#somewhere.com","first_name":null,"id":3226,"last_name":null,"updated_at":"2013-07-25T21:24:50-05:00","username":"someone"}}
I'd like to hide those secret fields, but don't know how to customize the JSON response.
I've tried a standard ActiveRecord serializer:
class UserSerializer < ActiveModel::Serializer
attributes :id, :created_at, :updated_at, :email, :first_name, :last_name, :username
end
to no avail, I'm guessing because of Devise.
I just ran into the same issue. I haven't pinpointed exactly why but it looks like respond_with in Devise's SessionsController (tested on Devise 3.0 and active_model_serializers 0.8.1) doesn't trigger the ActiveModel::Serializer.
So I overrode respond_with in my controller:
class SessionsController < Devise::SessionsController
def respond_with(resource, opts = {})
render json: resource # Triggers the appropriate serializer
end
end
It is, however, working in my RegistrationsController with respond_with. There I needed to do the following:
class RegistrationsController < Devise::RegistrationsController
respond_to :json
end
I recently ran into this and overriding respond_with didn't fix the issue. I ended up overriding to_json in user.rb like so:
def to_json(arg)
UserSerializer.new(self).to_json
end
Not sure what the extra arg is, but that seems to be required by one of the devise mixins.
I'm using the following:
Rails 4.2.0
Devise 3.4.1
Just a guess, but it sounds like rails is not finding your serializer and is using to_json(). Did you define active_model_serializer() in your model?
I just had the same problem, below is how I resolved it, very simple.
All these passed in active_model_serializers (0.9.5)
Override Devise registration method, and in your customize action:
def registration
//Some process, and you get a #user when registration is successful.
render :json => UserSerializer.new(#user)
end
If you want to pass some parameters to your customized Serializer(token for example), you can pass it in your action:
render :json => UserSerializer.new(#user).as_json({auth_token: your_token})
And in your serializer, just use:
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :avatar_url, :auth_token
def auth_token
serialization_options[:auth_token]
end
end
Depending on what you are doing with that JSON, you simply have to remove attributes you don't want from your serializer.
For example :
class UserSerializer < ActiveModel::Serializer
attributes :id, :email, :username
end
I presume that, in your case, you just want to do that.
But you also have the possibility to include an attribute on a specific condition :
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body, :author
def include_author?
current_user.admin?
end
end
And finally you can override the attributes method to return the hash you need :
class PersonSerializer < ActiveModel::Serializer
attributes :first_name, :last_name
def attributes
hash = super
if current_user.admin?
hash["ssn"] = object.ssn
hash["secret"] = object.mothers_maiden_name
end
hash
end
end
See README of ActiveModel::Serializers for more informations.

Resources