I wan't to permit certain parameters depending on the current user's role.
E.g: only permit the role attribute if the user is an administrator.
Is this possible?
Yes, it's possible.
You can do something like this :
def user_params
# List of common params
list_params_allowed = [:email, :title, :last_name, :first_name, :phone]
# Add the params only for admin
list_params_allowed << :role if current_user.admin?
params.require(:user).permit(list_params_allowed)
end
This way, if later you have new params, you only have to add in one list (avoids error).
If you have more than one param to add for the admin, you can do this like this :
list_params_allowed << :role << other_param << another_param if current_user.admin?
Hope this help.
You can simply do the following:
def post_params
allowed = [:name, :age]
conditional = Some_Condition_Applies ? [:title, :description] : []
params_list = allowed + conditional
params.require(:post).permit(params_list)
end
Related
I am trying to use multiple permits in a single method similar to the following (psuedocode)
def index
model.create(
params.permit(:b, :c)
)
params.permit(:a)
end
This is my actual code
def create
params.permit(:create_special_categories)
balance_sheet = ::BalanceSheet.create!(
balance_sheet_params.merge(date: Time.zone.now.to_date, entity: #entity)
)
balance_sheet.create_special_categories if params[:create_special_categories]
render json: balance_sheet, serializer: ::Api::V3::BalanceSheetSerializer
end
def balance_sheet_params
params.permit(
:id,
:entity,
:entity_id,
:date,
:name
)
end
However, I get the following error...
ActionController::UnpermittedParameters:
found unpermitted parameter: :create_special_categories
UPDATE
my solution was to avoid strong parameters all together.
def create
balance_sheet = ::BalanceSheet.new(
date: Time.zone.now.to_date, entity: #entity
)
balance_sheet.name = params[:name]
balance_sheet.save!
balance_sheet.create_special_categories if params[:create_special_categories]
render json: balance_sheet, serializer: ::Api::V3::BalanceSheetSerializer
end
This line doesn't have any effect, params.permit are not chained or added to a previous permit, you must use the result, that is why it's almost always used in a separate method.
params.permit(:create_special_categories)
What you must do is use what that returns for your following statements
permitted_params = params.permit(:create_special_categories)
Model.create(permitted_params)
...however you really should outsource this to a special method like you already have. You will have to tweak this to your use-case obviously.
def balance_sheet_params
if params[:create_special_categories]
params.permit(:id,
:entity,
:entity_id,
:date,
:name,
:create_special_categories)
else
params.permit(
:id,
:entity,
:entity_id,
:date,
:name)
end
end
def place
#place = Activity.find(params[:id])
end
How would I choose specific params? We have activity.address1,2, zip state etc and just need an instance variable we can use which is equal to just the address attributes.
You could do something like:
def place
#place ||= Activity.find(params[:id])
#address_attributes = #place.attributes.slice('address_1', 'address_2', 'zip', 'state')
end
In which case you'll have an instance variable that contains a hash of the specified attributes that you might use something like:
#address_attributes['address_1']
Personally, I find typing the '' around the key values annoying and prefer using symbols. In which case you might do something like:
def place
#place ||= Activity.find(params[:id])
#address_attributes = #place.attributes.with_indifferent_access.slice(:address_1, :address_2, :zip, :state)
end
Which you would use something like:
#address_attributes[:address_1]
But, even the brackets and symbols are a little annoying. So, you might do something like:
def place
#place ||= Activity.find(params[:id])
#address_attributes = OpenStruct.new(#place.attributes.with_indifferent_access.slice(:address_1, :address_2, :zip, :state))
end
Which you could then use something like:
#address_attributes.address_1
Now, personally, I don't like that long line in the place method, so I would be tempted to do something like:
def place
#place ||= Activity.find(params[:id])
#address_attributes = get_address_attributes
end
private
def get_address_attributes
OpenStruct.new(
#place.
attributes.
with_indifferent_access.
slice(
:address_1,
:address_2,
:zip,
:state
)
)
end
Now, if this is all in a controller, and you're setting the #address_attributes variable just so that you can use it in a view, then maybe that's good enough.
But, if you're using the #address_attributes variable elsewhere in the current instance, then you might consider doing something like:
attr_accessor *%w(
address_attributes
).freeze
delegate *%w(
address_1
address_2
zip
state
), to: :address_attributes, prefix: place
def place
#place ||= Activity.find(params[:id])
#address_attributes = get_address_attributes
end
private
def get_address_attributes
OpenStruct.new(
#place.
attributes.
with_indifferent_access.
slice(
:address_1,
:address_2,
:zip,
:state
)
)
end
In which case, you could make calls like:
place_address_1
Which seems a lot nicer than something like:
#address_attributes['address_1']
I have an association as follows:
class Membership < ApplicationRecord
belongs_to :user
end
and
class User < ApplicationRecord
has_many :memberships
end
Now I have a form to create a new membership. Within this form I also want to input new user information. I am using SimpleForm. My basic structure is as follows (using HAML not erb files):
= simple_form_for #membership do |f|
...
= simple_fields_for #user do |uf|
.field= uf.input :firstname, label: 'First Name', required: true
...
= f.button :submit, 'Submit'
#user here is an instance variable set in the new action on the Memberships controller (#user = User.new). I would like to keep it this way so I can use SimpleForm's inference on user attributes (i.e., uf.input :firstname maps to the firstname attribute on the User model)
Now given this background, when I hit submit the goal is to create a new membership and a new user associated to that membership. How can I permit parameters for the single associated user?
At the moment I have:
# Never trust parameters from the scary internet, only allow the white list through.
def membership_params
params.require(:membership).permit(users_attributes: [:id, :firstname] )
end
Here is the request:
Started POST "/memberships" for ::1 at 2018-10-15 15:08:42 -0600
Processing by MembershipsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"omitted==", "membership"=>{"user"=>{"firstname"=>""}}, "commit"=>"Submit"}
Unpermitted parameter: :user
Unpermitted parameter: :user
Unpermitted parameter: :user
##################### {}
No template found for MembershipsController#create, rendering head :no_content
Completed 204 No Content in 264ms (ActiveRecord: 0.0ms)
I have no template rendering on purpose for now.
Given this parameter structure:
"membership"=>{"user"=>{"firstname"=>""}}
I have also tried the following for permitted parameters:
params.require(:membership).permit(user_attributes: [:id, :firstname] )
params.require(:membership).permit(user: [:id, :firstname] )
Notice I the ################ {}. This is a manual puts I have in the create action. puts '################ ' + membership_params.to_json. As you can see it yields nothing. Also why do I get the 'Unpermitted parameters' logged three times?
UPDATE 1 controller code:
class MembershipsController < ApplicationController
load_and_authorize_resource
# GET
def new
#membership_plans = Plan.active.sort_by { |plan| plan.sequence }
#user = #membership.build_user
end
# POST
def create
debug_puts(membership_params.to_json)
end
private
# Never trust parameters from the scary internet, only allow the white list through.
def membership_params
params.require(:membership).permit(user_attributes: [:id, :firstname, :lastname] )
end
end
UPDATE 2
I don't deem this as an acceptable answer (which is why I'm not formally "answering" my question), but what I have decided to do is invert my form. The relationship is still the same among Memberships and Users, but the parent form is for a User:
= simple_form_for #user do |f|
...
= f.simple_fields_for :memberships_attributes do |mf|
...
This means I put accepts_nested_attributes_for :membership on the User model (a has_many association with memberships) and all the rendering and param permitting is done within the Users Controller
# new action in users_controller.rb
def new
#membership_plans = Plan.active.sort_by { |plan| plan.sequence }
#user = User.new
#user.build_membership
end
# Never trust parameters from the scary internet, only allow the white list through.
def user_params
params.require(:user).permit(memberships_attributes: [:id, :field1, :field2] )
end
You need to build the user for membership in the new action of memberships_controller.
def new
#membership = Membership.new
#user = #membership.build_user
end
and make sure you have user_attributes not users_attributes in the membership_params
params.require(:membership).permit(user_attributes: [:id, :firstname] )
Update:
There is one more important piece of code which need to be fixed.
This
= simple_fields_for #user do |uf|
should be
= f.simple_fields_for #user do |uf|
In the tutorials I have seen, create has been implemented like this:
def create
#note = Note.new(note_params)
#note.save
redirect_to #note
end
private
def note_params
params.require(:note).permit(:title, :type, :description, :dueDate)
end
I have read about the fetch method for optional parameters, but how do I mix the two methods to require some parameters and permit others? Would it be like this:
private
def note_params
params.require(:note).permit(:title, :type)
params.fetch(:note, {}).permit(:description, :dueDate)
end
In this case, could I expect to pass 0, 1, or 2 of the fetched params?
You're reading it wrong, I think. In this line
params.require(:note).permit(:title, :type, :description, :dueDate)
The required parameter is :note. And :title, :type and others are simply permitted/allowed to appear under :note. None of them are required by this syntax. If you really need :title to be there, that is best handled by presence validation on your Note model.
class Note
validates_presence_of :title
end
Now, if you don't pass params[:note][:title], #note.save will return false and you can render form with user-friendly errors (highlight missing fields, etc.). You wouldn't be able to do that (as easily) if note_params method raised an exception on missing title attribute. So that is how you handle required record attributes in rails.
In my Rails 3.1.1 project I have an ActiveModel that talks to API (ripped from Paul Dix's book, shortened for readability):
class Job
include ActiveModel::Validations
include ActiveModel::Serializers::JSON
ATTRIBUTES = [ :id,
:title,
:description,
:company_id ]
attr_accessor *ATTRIBUTES
validates_presence_of :title, :description
validates_numericality_of :company_id, :id
def initialize(attributes = {})
self.attributes = attributes
end
def attributes
ATTRIBUTES.inject(
ActiveSupport::HashWithIndifferentAccess.new
) do |result, key|
result[key] = read_attribute_for_validation(key)
result
end
end
def attributes=(attrs)
attrs.each_pair {|k, v| send("#{k}=", v)}
end
def read_attribute_for_validation(key)
send(key)
end
# More method definitions...
end
I instantiate #job in my controller, new action (company_id is a segnment key in the route: /companies/:company_id/jobs/new) like this:
#job = Job.new(company_id: params[:company_id])
Then, using CanCan, I check user's permissions to create to create a job. Basically, CanCan checks if current_user's company_id attribute matches job's company_id. This check fails because #job.company_id is returned as String.
Certainly, I can use params[:company_id].to_i while instantiating the object, but this seems like a workaround that I would have to repeat later.
Question: is there a way to make my Job ActiveModel more "type-aware" and make it return int for #job.company_id call?
I googled around, checked activemodel source code, but doesn't seem to find an answer. Any help is greatly appreciated.
Update
I was thinking more of something like schema block for ActiveModel, just like the one in ActiveResource.
attr_accessor *ATTRIBUTES
create a method like this:
def company_id
#company_id
end
You can just override that with
def company_id
#company_id.to_i
end
Answering my own question....
mosch's answer suggested to override the getter for company_id in my ActiveModel. However, I would have to repeat this for all of _id attributes in the model. Therefore, I decided to cast all of the '_id' attributes to integers while initializing the object. As follows:
def attributes=(attrs)
attrs.each_pair do |k, v|
if "#{k}".include? "_id"
send("#{k}=", v.to_i)
else
send("#{k}=", v)
end
end
end
I'm assuming your Company has_many => :jobs? If so, you could try
def new
#company = Company.find(params[:company_id])
#job = #company.jobs.new
end