Dynamic Strong params for require - Rails - ruby-on-rails

I have an update method right now that will not work for all situations. It is hard coded in the strong params like this params.require(:integration_webhook).permit(:filters) that all fine right now but sometimes it may be integration_webhook and other times it needs to be integration_slack. Basically, is there a way that I don't need to hardcode the require in the strong params? I'll show my code for clarity.
Update Method:
def update
#integration = current_account.integrations.find(params[:id])
attrs = params.require(:integration_webhook).permit(:filters)
if #integration.update_attributes(attrs)
flash[:success] = "Filters added"
redirect_to account_integrations_path
else
render :filters
end
end
As you can see it's a standard update method. But I need the integration_webhook params to be dynamic. I'm wondering if there is a model method I could call to strip away the integration_webhook part?

Not totally sure how dynamic this needs to be, but assuming that we are either getting an integratino_webhook or a integration_slack.
def update
#integration = current_account.integrations.find(params[:id])
if #integration.update_attributes(update_params)
# ...
else
# ...
end
end
private
def update_params
params.require(:integration_webhook).permit(:filters) if params.has_key?(:integration_webhook)
params.require(:integration_slack).permit(:filters) if params.has_key?(:integration_slack)
end
Checkout Strong parameters require multiple if this didn't answer your question.
Edit
For more dynamic requiring:
def update_params
[:integration_webhook, :integration_slack].each do |model|
return params.require(model).permit(:filters) if params.has_key?(model)
end
end

Off the top of my head something like this should work. The naming convention isn't the best but it the structure will allow you to just add to the list if you need to.
def update
#integration = current_account.integrations.find(params[:id])
if #integration.update_attributes(webhook_and_slack_params)
flash[:success] = "Filters added"
redirect_to account_integrations_path
else
render :filters
end
end
def webhook_and_slack_params
[:integration_webhook, :integration_slack].each do |the_params|
if(params.has_key?(the_params))
params.require(the_params).permit(:filters)
end
end

Related

Ruby on Rails - how to handle belongs_to multiple on create

I'm trying to create a Comment that belongs_to multiple Models but I'm not sure on what the best practice is to assign it to both.
class Comment < ApplicationRecord
validates :message, :presence => true
belongs_to :user
belongs_to :discussion
end
When creating a Comment within a Discussion, I run into the error "Discussion must exist".
I'm using Devise so I am using current_user and building a Comment to align it with them, but I'm not sure on exactly how to point it to both. This seems to work, but seems very hacky.
def create
#comment = current_user.comments.build(comment_params)
#comment.discussion_id = params[:discussion_id]
...
end
What's the best practice?
I would add the :discussion_id to the comment_params method:
def create
#comment = current_user.comments.build(comment_params)
...
end
private
def comment_params
params.require(:comment)
.permit(...)
.merge(discussion_id: params.require(:discussion_id))
end
Or you could use build with a block:
def create
#comment = current_user.comments.build(comment_params) { |comment|
comment.discussion_id = params[:discussion_id]
}
# ...
end
No matter how you go about it, you do need to either set discussion_id or get it to be present in comment_params.
Does this look less hacky to you?
def create
comment_params.merge(discussion_id: params[:discussion_id])
#comment = current_user.comments.build(comment_params)
...
end
You can pass additional params in the block
params = ActionController::Parameters.new({
comment: {
name: 'Francesco',
body: 'Text'
}
})
comment_params = params.require(:comment).permit(:name, :body)
def create
#comment = current_user.comments.build(comment_params) do |comment|
comment.discussion_id = params[:discussion_id]
end
end
There are several things you need to keep in mind when you create an entity which is associated to more than one entity.
With respect to your question regarding to best practices. I would like to emphasise the usage of an interactor here:
# xx_controller.rb
def create
success = CreateComment.call(params)
if success
....
else
....
end
end
# ../../create_comment.rb
# pseudocode
class CreateComment
def call(params)
validate your params # check if nothing is missing, the discussion exists and you have a user
build_and_save_your_comment
return_your_result_object # return a tuple or a result or whatever you need in order to handle errors and success cases...
end
end
With this approach you keep the readability within your controller and you can focus on what matters to the comment creation in a dedicated place.

Rails create action: Forbidden Attributes Error

I am new to rails and am in the process of entering in information in a form and saving it to a database. I am following a tutorial which may be out of date. I am getting an error on the second line. Am I passing the wrong parameter?
def create
#student = Student.new(params[:student])
if #student.save
redirect_to new_student_path
end
end
I expect the problem is that you need to process the student parameters before passing them to Student.new, so as not to fall foul of the strong parameters system.
Try this:
def create
#student = Student.new(student_parameters)
if #student.save
redirect_to new_student_path
end
end
private
def student_parameters
params.require(:student).permit(:name, :age)
end
replace :name, :age with the list of attributes you want to white list (allow through)
More information about the mass assignment vulnerability that strong parameters mitigates can be found here.

How to use request.env in before_filter?

I want to check some field before save and change it into default browser language.
I want to use before_save filter:
def update
#website = Website.find(params[:id])
if #website.language == "Automatic (by user's browser language)"
#website.language = full_language(request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first)
end
respond_to do |format|
if #website.update_attributes(params[:website])
format.html { redirect_to #website,
notice: 'Note: code has been updated. Please replace the code you have on your website with the code below. Only then changes will take effect.'}
format.js
end
end
end
I need to check:
if #website.language == "Automatic (by user's browser language)"
#website.language = full_language(request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first)
end
And it works in create action, but not in update.
How can I do this ?
if you want to something update after create action but not update action then see example
# write in your model
after_create :xyz
def xyz
...
...
end
above method xyz call after create action. when update call then it will not call.
Before save is used in model and request is usually not available in model.
However if you really want to do it there, check: http://m.onkey.org/how-to-access-session-cookies-params-request-in-model for more detail on how to do it.
-- edit --
There are several way to do it.
First one to cross my mind is that you add: attr_accessor :request_language to model, then pass request.env['HTTP_ACCEPT_LANGUAGE'] from controller to model:
if #website.update_attributes(params[:website])
#website.request_language = request.env['HTTP_ACCEPT_LANGUAGE']
# ...
and now you can proceed like you did before with few modifications:
def auto_language
if self.language == "Automatic (by user's browser language)"
self.language = full_language(self.request_language.scan(/^[a-z]{2}/).first)
end
end
Second way I can think of is to use before/after filters in controller to influence params before they are passed to model.
...
request object isn't available in models. You should do those tweaks on the controller layer.
def action
if #model.language == "Automatic (by user's browser language)"
#model.language = full_language(request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first)
end
if #model.save
...
else
...
end
end
private
def full_language
...
end
You need to change the value of :language in the params hash, since that's what you're ultimately passing to #website.update_attributes. I would also suggest moving that conditional into the model layer to make your controller code more readable:
# app/controllers/websites_controller.rb
def update
#website = Website.find(params[:id])
if #website.language_automatic?
params[:website][:language] = full_language(request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first)
end
respond_to do |format|
if #website.update_attributes(params[:website])
format.html { redirect_to #website,
notice: 'Note: code has been updated. Please replace the code you have on your website with the code below. Only then changes will take effect.'}
format.js
end
end
end
# app/models/website.rb
def language_automatic?
language == "Automatic (by user's browser language)"
end

Rails: How can we detect which parameters is updated in CRUD controller

I was just wondering: How can I identify which parameters is being updated in a classic CRUD controller?
I would like to redirect to different path depending on which parameters is being changed.
For example, if I have a Person with 3 attributes id name and firstname
In my controller:
def update
#person = Person.find(params[:id])
respond_to do |format|
if #person.update_attributes(params[:person])
#...
else
#...
end
end
end
How can I detect that name is being changed, and not firstname
Would something like !params[:name].nil? work?
Thanks for your help!
so you could check the params like you said. Something like if !params.[:name].nil? ...
OR
You could use the _changed? methods
#person.attributes = params[:person]
if #person.name_changed?
next_path = name_changed_path
else
...
end
#person.save
redirect_to next_path

Controller best practices: Multiple Methods or Multiple cases in Show

I'm frequently building controllers where i would like multiple methods
(in addition to index, edit, show, etc.). Most of the time the actions i
desire could be lumped into show as they are simple GET operations,
however I don't want to put too much logic in any one controller action.
Here is a quick example of two different ways to achieve the same
thing...
class TwitterFriendController < ApplicationController
## lump everything into show?
def show
if params[:id] == "follow"
users = current_user.following
elsif params[:id] == "follow_me"
users = current_user.users_who_follow_me
elsif params[:id] == "following_follow_me"
users = current_user.following_who_follow_me
elsif params[:id] == "following_who_do_not_follow_me"
users = current_user.following_who_do_not_follow_me
...
end
respond_with do |format|
format.json do {...}
end
end
## or split everything out into separate methods, this requires
additional routing
def following
...
end
def users_who_follow_me
...
end
def following_who_follow_me
...
end
def following_who_do_not_follow_me
...
end
end
Everything in show
a ton of logic in one method
DRY ? # lots of extra code needed for logic
Less routing
Seperate Methods
More routing
not DRY
Easy method lookup
Easier to read individual methods
So again the real question is, which one of those techniques are less
bad.
I would do something like:
FOLLOW_WHITELIST = %w[ follow follow_me following_follow_me following_who_follow_me following_who_do_not_follow_me ]
def show
if FOLLOW_WHITELIST.include? params[:id]
users = current_user.send params[:id].to_sym
end
respond_with do |format|
format.json do {...}
end
end
This will call whatever method is passed in params[:id], as long as it's in the whitelist (to prevent arbitrary code injection).
If having separate routes was a plus to you (nicer urls?), you could also dynamically generate the methods and routes with something like this:
class TwitterFriendController < ApplicationController
FOLLOW_ACTIONS = %w[ follow follow_me following_follow_me following_who_follow_me following_who_do_not_follow_me ]
FOLLOW_ACTIONS.each do |action|
define_method action do
users = current_user.send action.to_sym
respond_with do |format|
format.json do {...}
end
end
end
end
And then in routes.rb:
FOLLOW_ACTIONS.each do |action|
match action.to_sym => "controller##{action}"
end

Resources