So I am working on creating a playercard, which is basically a profile page for a user. The issue I am having on the backend is my private method playercard_params is only returning user_id, and not all the information inputted into the form...although regular params shows all the data needed to create the playercard. I thought the issue might be on the frontend, but working my way backwards came to the conclusion the issue is here on the backend.
Here is my controller:
class Api::V1::PlayercardController < ApplicationController
before_action :set_user
def index
if params[:user_id]
#playercard = #user.playercard
else
#playercard = Playercard.all
end
render json: #playercard
end
def show
#playercard = Playercard.find(params[:id])
render json: #playercard
end
def create
#playercard = Playercard.new(playercard_params)
binding.pry
if #playercard.save
render json: #user
else
render json: {
error: #playercard.errors.full_messages.to_sentence
}
end
end
def update
#playercard = Playercard.find(params[:id])
if #playercard.update(playercard_params)
render json: #playercard
else
render json: {
error: #playercard.errors.full_messages.to_sentence
}
end
end
private
def playercard_params
params.require(:playercard).permit(:player_nickname, :player_height_in_feet, :player_height_in_inches, :player_weight, :player_age, :player_fav_player, :user_id)
end
def set_user
#user = User.find(params[:user_id])
end
end
My playercard model:
class Playercard < ApplicationRecord
belongs_to :user
validates :player_nickname, :player_height_in_feet, :player_height_in_inches, :player_weight, :player_age, :player_fav_player, presence: true
end
and the serializer if that helps:
class PlayercardSerializer < ActiveModel::Serializer
attributes :id, :player_nickname, :player_height_in_feet, :player_height_in_inches, :player_weight, :player_age, :player_fav_player
belongs_to :user
end
Here are my params:
<ActionController::Parameters {"playerNickname"=>"white mamba", "playerHeightFeet"=>"6", "playerHeightInches"=>"3", "playerAge"=>"30", "playerWeight"=>"170", "playerFavPlayer"=>"Kobe", "user_id"=>"1", "controller"=>"api/v1/playercard", "action"=>"create", "playercard"=><ActionController::Parameters {"user_id"=>1} permitted: false>} permitted: false>
When I submit the form on the front end, I get errors saying each field is empty...in the pry, if I type playercard_params, only user_id shows up (with the correct id)
I solved the issue by lining up the naming convention for the attributes with the front-end and back-end. And it worked!
Thank you #jvillian for the insight!!
Related
This section of Pundit section says that we could control which attributes are authorized to be updated. But it fails in case of the use of active_model_seriallizers gem:
def post_params
# originally geneated by scaffold
#params.require(:post).permit(:title, :body, :user_id)
#To deserialize with active_model_serializers
ActiveModelSerializers::Deserialization.jsonapi_parse!(
params,
only: [:title, :body, :user]
)
end
If I modify the PostsController update action as Pundit suggested:
def update
if #post.update(permitted_attributes(#post))
render jsonapi: #post
else
render jsonapi: #post.errors, status: :unprocessable_entity
end
end
it fails with error:
ActionController::ParameterMissing (param is missing or the value is empty: post):
app/controllers/posts_controller.rb:29:in `update'
I also create the PostPolicy as follows:
class PostPolicy < ApplicationPolicy
def permitted_attributes
if user.admin? || user.national?
[:title, :body]
else
[:body]
end
end
end
but it has no impact on the above error.
Any idea on how can we do that?
The solution I came to (thanks to #max for some tips and tricks) is as follows:
Add the following line to config/application.rb:
config.action_controller.action_on_unpermitted_parameters = :raise
Add the rescue_from either to the AplicationController or the one you are precisely interested:
class ApplicationController < ActionController::API
include ActionController::MimeResponds
include Pundit
rescue_from Pundit::NotAuthorizedError, ActionController::UnpermittedParameters, with: :user_not_authorized
...
private
def user_not_authorized
render jsonapi: errors_response, status: :unathorized
end
def errors_response
{
errors:
[
{ message: 'You are not authorized to perform this action.' }
]
}
end
end
Then add pundit_params_for method to the PostsController and change the update action (in my case I'd like to restrict some attributes in update action only:)
class PostsController < ApplicationController
...
def update
if #post.update(permitted_attributes(#post))
render jsonapi: #post
else
render jsonapi: #post.errors, status: :unprocessable_entity
end
end
private
def post_params
ActiveModelSerializers::Deserialization.jsonapi_parse!(
params,
only: [:title, :body, :user]
)
end
def pundit_params_for(_record)
params.fetch(:data, {}).fetch(:attributes, {})
end
end
VoilĂ . Now if an unpermitted attribute will be submitted for the update action, the response will have 500 status and contain the error as specified in ApplicationController#errors_response method.
ATTENTION: It still fails if you have some relations posted with the request (for example, you can have an Author as belongs_to relation with Post). Using pundit_params_for as before will fail to extract the corresponding author_id value. To see the way, here my another post where I explained how to use it.
Hope this helps.
I have a requirement in rails api application. client can have many orders and each order belongs to a client.
class Client < ApplicationRecord
has_many :orders
end
my order.rb is
class Order < ApplicationRecord
belongs_to :client, dependent: :destroy
accepts_nested_attributes_for :client
validates_presence_of :order_amount, :service_amount, :miners_amount
end
I have a route exposed /place_order and which creates client and orders.
class OrderProcessingController < ApplicationController
def place_order
#order = Order.new(order_processing_params)
if #order.save
render json: #order
else
render json: #order.errors.full_messages
end
end
private
def order_processing_params
params.require(:order).permit(:order_amount, :service_amount, :miners_amount, client_attributes: [:name, :email, :phone_number, :date])
end
end
Everything works fine so far. Now my requirement is, i have to check the client is already present in client table. if yes add the client_id for the orders and create new order. I don't want to create new client and order every time.
how can i achieve the same in before_filter or something like that. get the client from client params and if the client present delete the params key from incoming params ???
the post data for place_order is as follows
{
"order" : {
"order_amount" : "10000",
"service_amount" : "1000",
"miners_amount" : "10000",
"client_attributes": {
"name": "Ajith",
"email": "ajith#gmail.com",
"phone_number": "12231321312",
"date": "12/12/12"
}
}
}
Thanks in advance,
Ajith
The below code is not tested, mostly your approach should be around this
class OrderProcessingController < ApplicationController
before_action :find_client, only: [:place_order]
def place_order
#order = #client.orders.new(order_processing_params)
if #order.save
render json: #order
else
render json: #order.errors.full_messages
end
end
private
def order_processing_params
params.require(:order).permit(:order_amount, :service_amount, :miners_amount, client_attributes: [:name, :email, :phone_number, :date])
end
def find_client
#client = Client.find_or_create_by(email: params[:order][:client_attributes][:email])
#below line can be improved using a method, see the last line if later you want, never update a primary key which is email in bulk params
#client.update_attributes(name: params[:order][:client_attributes][:name], phone_number: params[:order][:client_attributes][:phone_number], date: params[:order][:client_attributes][:date])
end
#def get_client_params
# params.require(:order)
#end
end
I tried below approach to get a solution. not very sure that this is the right way to approach the problem
class OrderProcessingController < ApplicationController
before_action :find_client, only: :place_order
def place_order
if #client.present?
#order = #client.orders.build(order_processing_params)
else
#order = Order.new(order_processing_params)
end
if #order.save
render json: #order
else
render json: #order.errors.full_messages
end
end
private
def order_processing_params
params.require(:order).permit(:order_amount, :service_amount, :miners_amount, client_attributes: [:name, :email, :phone_number, :date])
end
def find_client
begin
#client = Client.find_by_email(params[:order][:client_attributes][:email])
rescue
nil
end
end
end
Thanks,
Ajith
I am new to stackoverflow, so my apologies if this is formatted poorly.
In my current project I have a model Driver, which has many trips. Those trips have many mileages, backhauls, picks, drops and hours. When I create a new trip, i want to be able to associate it to the driver, but I also want to be able to add the mileages, backhauls, picks and drops and hours on the same page. I am unsure how to structure my routes for this. I have been successful in creating a trip for a driver without adding on the additional models to the trip but from there I am stumped. I have only created the mileage model/controller so far for what needs to be associated with the trip. Any nudge in the right direction would be greatly appreciated.
Driver Model
class Driver < ApplicationRecord
has_many :trips
end
Trip Model
class Trip < ApplicationRecord
belongs_to :driver
has_many :mileages
accepts_nested_attributes_for :mileages
default_scope {order(date: :asc)}
validates :total, presence: true
validates :date, presence: true
validates_uniqueness_of :trip_number, :allow_nil => true, :allow_blank =>
true
end
Mileage Model
class Mileage < ApplicationRecord
belongs_to :trip
end
Trips controller
def index
#trips = Trip.all
end
def show
end
def new
#driver = Driver.find(params[:driver_id])
#trip = Trip.new
end
def edit
end
def create
#driver = Driver.find(params[:driver_id])
#trip = Trip.new(trip_params)
#driver.trips.create(trip_params)
respond_to do |format|
if #driver.trips.create(trip_params)
flash[:notice] = "Trip successfully created"
redirect_to new_driver_trip_path(#driver)
else
flash[:warning] = "Unable to create trip"
redirect_to new_driver_trip_path(#driver)
end
end
private
def set_trip
#trip = Trip.find(params[:id])
end
def trip_params
params.require(:trip).permit(:trip_number, :date, :driver_id, :total)
end
end
Mileage Controller
def new
#mileage = Mileage.new
end
def create
#mileage.create(mileage_params)
end
private
def mileage_params
params.require(:mileage).permit(:miles, :rate, :total)
end
end
end
Driver Controller
def index
#drivers = Driver.all
end
def show
end
def new
#driver = Driver.new
end
def edit
end
def create
#driver = Driver.new(driver_params)
respond_to do |format|
if #driver.save
format.html { redirect_to #driver, notice: 'Driver was
successfully created.' }
format.json { render :show, status: :created, location: #driver }
else
format.html { render :new }
format.json { render json: #driver.errors, status:
:unprocessable_entity }
end
end
end
private
def set_driver
#driver = Driver.find(params[:id])
end
def driver_params
params.require(:driver).permit(:first_name, :last_name, :email, :unit)
end
end
If you want to create nested models on the same page. i.e. milages within trip page using accepts_nested_attributes_for, You can use cocoon gem.
https://github.com/nathanvda/cocoon
Drifting Ruby has a video that shows the process in detail that is easy to follow.
https://www.youtube.com/watch?v=56xjUOAAZY8
You can do it manually as well but it will require a little bit more work.
With cocoon you will do have a Driver Controller and Trip controller but you don't need a Milage controller since it is handled with nested_attributes via Trip Controller.
If you want to do it manually, you will need a bit of JavaScript. You can follow Ryan Bates RailsCast on this topic.
railscasts.com/episodes/196-nested-model-form-revised
I'm using ActiveModel Serializers to serialize my models and I'm constantly in need to create a new serializer in order to satisfy the needs of an controller without including unnecessary information into another.
class ContactGroupSerializer < ActiveModel::Serializer
attributes :id, :name, :contacts, :contacts_count,
:company_id, :user_id
def contacts_count
object.contacts.count
end
end
Is there a way to define a single serializer, such as the one above, and them dinamically select which attributes to be included on my controller response?
class ContactsGroupsController < ApplicationController
def index
...
render json: #contact_groups // here I would like to return only id and name, for example
end
end
I know I can achieve that by creating another serializer, but I wouldn't like to.
Well, you can just define a method in your application_controller.rb to which you can pass all your objects to be rendered with array of methods to be returned as response..like for example,
def response_for(object, methods = [:id])
if object.blank?
head :no_content
elsif object.errors.any?
render json: { errors: object.errors.messages }, status: 422
else
render json: build_hash_for(object, methods), status: 200
end
end
private #or in your `application_helper.rb`
def build_hash_for(object, methods)
methods.inject({}) do |hash, method|
hash.merge!(method => object.send(method))
end
end
In your particular case above, you can just
class ContactsGroupsController < ApplicationController
def index
...
response_for #contact_groups, [:id, :name]
end
end
I'm stuck at defining a custom validation method that's purpose is to verify uniqueness of a property across two models
I realize this is bad code, but i wanted to get the test passing before refactor
here is a model with the custom validation to check another model property, error undefined local variable or method `params' (be gentle I'm still trying to figure out RoR)
class Widget < ActiveRecord::Base
include Slugable
validates :name, presence: true
validate :uniqueness_of_a_slug_across_models
def uniqueness_of_a_slug_across_models
#sprocket = Sprocket.where(slug: params[:widget_slug]).first
if #sprocket.present?
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
end
end
You don't have access to params in a model. It belongs to controller and view. What you could do is to call custom method in widgets controller (instead of regular save) in order to pass params to a model:
class WidgetsController < ActionController::Base
def create
#widget = Widget.new(widget_params)
if #widget.save_with_slug_validation(params)
redirect_to widgets_path
else
render :new
end
end
end
and define it:
class Widget < ActiveRecord::Base
# ...
def save_with_slug_validation(params)
sprocket = Sprocket.find_by(slug: params[:widget_slug])
if sprocket
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
save
end
end
I didn't test it but it should work.
P.S. Rails 4 style is used.
UPD
I should have tested it, sorry. Please use another approach.
Widgets controller:
# POST /widgets
# POST /widgets.json
def create
#widget = widget.new(widget_params)
#widget.has_sprocket! if Sprocket.find_by(slug: params[:widget_slug])
respond_to do |format|
if #widget.save
format.html { redirect_to [:admin, #widget], notice: 'widget was successfully created.' }
format.json { render action: 'show', status: :created, location: #widget }
else
format.html { render action: 'new' }
format.json { render json: #widget.errors, status: :unprocessable_entity }
end
end
end
Widget model:
class Widget < ActiveRecord::Base
include Slugable
validates :name, presence: true
validate :uniqueness_of_a_slug_across_models, if: 'has_sprocket?'
def uniqueness_of_a_slug_across_models
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
def has_sprocket!
#has_sprocket = true
end
def has_sprocket?
!!#has_sprocket
end
end
It would be better to move has_sprocket! and has_sprocket? methods and maybe validation itself to Slugable concern.