I am desperately trying to merge a set of default values into my nested params. Unfortunately, using deep_merge no longer works in Rails 5 since it no longer inherits from Hash.
So this does not work:
class CompaniesController < ApplicationController
def create
#company = current_account.companies.build(company_params)
if #company.save
flash[:success] = "Company created."
redirect_to companies_path
else
render :new
end
end
private
def company_params
params.require(:company).permit(
:name,
:email,
:people_attributes => [
:first_name,
:last_name
]
).deep_merge(
:creator_id => current_user.id,
:people_attributes => [
:creator_id => current_user.id
]
)
end
end
It gives me this error:
undefined method `deep_merge' for ActionController::Parameters:0x007fa24c39cfb0
So how can it be done?
Since there seems to be no similar implementation of deep_merge in Rails 5 ActionController::Parameters by looking at this docs, then you can just simply do .to_h to convert it first into a ActiveSupport::HashWithIndifferentAccess (which is a subclass of a Hash):
to_h() Returns a safe ActiveSupport::HashWithIndifferentAccess representation of the parameters with all unpermitted keys removed.
def company_params
params.require(:company).permit(
:name,
:email,
:people_attributes => [
:first_name,
:last_name
]
).to_h.deep_merge(
:creator_id => current_user.id,
:people_attributes => [
:creator_id => current_user.id
]
)
end
Related
I have multiple Lessons and most of them have some prerequisites which are themselves other lessons. The Prerequisite model represents the relationship and each has a lesson_id and prerequisite_lesson_id.
class Lesson < ApplicationRecord
has_many :prerequisites
has_many :prerequisite_lessons, :through => :prerequisites
end
class Prerequisite < ApplicationRecord
belongs_to :lesson
belongs_to :prerequisite_lesson, :class_name => "Lesson"
end
I'm trying to figure out a way so that:
When I create a new Lesson and select one or more prerequisites from a collection_select
multiple Prerequisite objects are created based on what was selected, with the lesson_id being the created Lesson id.
Here is part of my Lesson controller:
def create
#lesson = Lesson.new(lesson_params)
if #lesson.save
if #lesson.prerequisite_ids.length > 0
#lesson.prerequisite_ids.each do |p|
Prerequisite.new(lesson_id: #lesson.id, prerequisite_lesson_id: p)
end
end
flash[:notice] = "Lesson created..."
redirect_to root_url
else
render :new
end
end
...
def lesson_params
params.require(:lesson).permit(
:name,
:high_tempo,
:low_tempo,
:interval,
:advance_tempo,
prerequisites_attributes: [
:lesson_id,
:prerequisite_lesson_id
]
)
end
and my form:
<%= f.collection_select(:prerequisite_ids, Lesson.all, :id, :name, {}, {:multiple => true}) %>
UPDATE:
In the logs, I see:
"lesson"=>{"name"=>"Lesson 6", "high_tempo"=>"200",
"low_tempo"=>"100", "interval"=>"10", "advance_tempo"=>"140",
"prerequisite_ids"=>["", "3", "4"]}, "commit"=>"Save"} Unpermitted
parameter: prerequisite_ids'`
It looks like there are a couple of problems here:
One is that you are using Prerequisite.new(...) when I think you want to be using Prerequisite.create(...)
So your create action should be:
def create
#lesson = Lesson.new(lesson_params)
if #lesson.save
if #lesson.prerequisite_ids.length > 0
#lesson.prerequisite_ids.each do |p|
Prerequisite.create(lesson_id: #lesson.id, prerequisite_lesson_id: p)
end
end
flash[:notice] = "Lesson created..."
redirect_to root_url
else
render :new
end
end
The other is this (from your logs):
"lesson"=>{"name"=>"Lesson 6", "high_tempo"=>"200",
"low_tempo"=>"100", "interval"=>"10", "advance_tempo"=>"140",
"prerequisite_ids"=>["", "3", "4"]}, "commit"=>"Save"} Unpermitted
parameter: prerequisite_ids'`
In your controller, you are permitting
def lesson_params
params.require(:lesson).permit(
:name,
:high_tempo,
:low_tempo,
:interval,
:advance_tempo,
prerequisites_attributes: [
:lesson_id,
:prerequisite_lesson_id
]
)
end
But note that your actual data is coming in as "lesson" => {..., "prerequisite_ids"=>["", "3", "4"]}
So you should instead permit like this:
def lesson_params
params.require(:lesson).permit(
:name,
:high_tempo,
:low_tempo,
:interval,
:advance_tempo,
prerequisite_ids: []
)
end
Additionally, it looks like your prerequisite_ids are including a value that is an empty string. Based on this post, it looks like you may need to add include_hidden: true to your select tag, like this:
<%= f.collection_select(:prerequisite_ids, Lesson.all, :id, :name, {}, {:multiple => true, include_hidden: true}) %>
In rails 3 we could use update_attributes as below
#customer.update_attributes(params[:customer], :as => :default)
or
#customer.update_attributes(params[:customer], :as => :admin)
and our attributes accessible would be defined as
attr_accessible :fname, :lname, :as => [:default, :admin]
attr_accessible :status, :as => [:admin]
But in rails 4, update_attributes does not accept second parameter. So how can I convert the above code to work in rails 4?
I tried something like this
#customer.update_attributes(customer_params)
private
def customer_params
params.require(:customer).permit(:fname, :lname, :status )
end
But I don't know how to pass the role in rails 4 while updating attributes. I can not do something like this.
#customer.update_attributes(customer_params, :as => :admin)
This is not allowed in rails 4. So how can I do similar thing in rails 4?
Have a look at strong parameters. The permitted attributes should be determined in the controller not the model
http://edgeguides.rubyonrails.org/action_controller_overview.html#strong-parameters
For example the controller would have
def customer_params
if current_user.admin?
params.require(:customer).permit(:fname, :lname, :status)
else
params.require(:customer).permit(:fname, :lname)
end
end
def update
#customer.update_attributes(customer_params)
...
end
I am have two actions of which renders eventually should output json using the same rabl template, but at the moment, they each have a template with their own name
dashboard.json.rabl and batch_create.json.rabl
they are the exact same, how can I specify in the batch_create template to use the dashboard's template?
Thank you!
EDIT #including controller's two actions and their rabl views
line_items_controller.rb
def dashboard
#line_items ||= LineItem.limit(1000).all.to_a
pids = [1,2,3,4,5]
#projects = Project.only(:id, :name).where(:_id.in => pids).to_a
#users = User.only(:first, :last, :role, :company_id).all.to_a
#companies= Company.where(:_id.in => #users.map(&:company_id)).to_a
#specs = Spec.where(:specable_id.in => pids).to_a
spec_ids= #specs.map { |e| e.id }
#contact_infos = ContactInfo.where(:infoable_id.in => spec_ids).to_a end
gon.rabl
respond_to do |format|
format.html
format.json
end
end
def batch_create
#line_items = LineItem.where(:_id.in => params[:line_item_ids]).to_a
# else same as #dashboard
end
app/views/line_items/dashboard.json.rabl SAME AS app/views/line_items/batch_create.json.rabl
object false
child #projects => :projects do
attributes :id, :name
end
child #companies => :companies do
attributes :id, :name
end
child #users => :users do
attributes :id, :full, :company_id
end
child #specs => :specs do
attributes :id, :style, :due_at_note
end
child #contact_infos => :contact_infos do
attributes :info, :infoable_id
end
child #line_items do
attributes :id, :title, :dashboard_length, :dashboard_created_at, :original_file_url, :project_id
end
Have you tried using this in the batch_create.json.rabl:
extends '<insert_object_name>/dashboard'
I feel like I have no idea what I'm doing.. I have a vague Idea. I'm hoping I did this all right so far.
Any way you can see to refactor this would be greatly appreciated.
One thing I noticed it does wrong is it won't load the proper options that were previously submitted if there is an error and it posts to the same URL. The text inputs seem to load the previous value but the select and the radio buttons reset to the default every submit.
ResourcesController#new
def new
#resource = Resource.new
#title = "Submit Resource"
#categories = Category.all
end
ResourcesController#create (notice I have #categories = Category.all in both... according to DRY im not sure where else it should go, or it only works on the first form submit.
def create
#title = "Submit Resource"
#categories = Category.all
#resource = Resource.new(params[:resource])
category_ids = #categories.map { |c| c[1] }
if #resource.valid? and category_ids.include? params[:category_id]
#resource.cost = params[:cost]
#resource.category_id = params[:category_id]
#resource.save
redirect_to root_url
else
render :action => :new
end
end
Resource.rb (model)
# == Schema Information
#
# Table name: resources
#
# id :integer not null, primary key
# upvotes :integer default(0)
# downvotes :integer default(0)
# url :string(255)
# title :string(255)
# cost :integer default(0)
# description :text
# flags :integer
# category_id :integer
# user_id :integer
# created_at :datetime not null
# updated_at :datetime not null
#
class Resource < ActiveRecord::Base
belongs_to :category
belongs_to :user
has_many :favorites
has_many :resource_tags
has_many :tags, :through => :resource_tags
attr_accessible :url, :title, :cost, :description, :category_id, :user_id
# Pseudo-Enum
COST = [:free, :paid, :both]
url_regex = /^(?:http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6}(:[0-9]{1,5})?(\/.*)?$/ix
validates :url, :presence => true,
:format => { :with => url_regex,
:message => "must be valid"},
:uniqueness => { :case_sensitive => false,
:message => "has already been submitted"}
validates :title, :presence => true,
:length => { :within => 6..75 }
validates :cost, :presence => true
validates :description, :presence => true,
:length => { :within => 25..200 }
validates :category_id, :presence => true,
:format => { :with => /\d+/ }
validates :user_id, :presence => true,
:format => { :with => /\d+/ }
def cost
COST[read_attribute(:cost)]
end
def cost=(value)
write_attribute(:cost, COST.index(value.downcase.to_sym))
end
def category_id
read_attribute(:category_id).to_i
end
def category_id=(value)
write_attribute(:category_id, value.to_i)
end
end
My view file for the Resource#new form
<div class="field">
<%= f.label :category %>
<%= select_tag(:category_id, options_for_select(#categories.map {|c|[c.name, c.id]})) %>
</div>
Last Q: i havent worked with the user_id field yet. This is going to be pulled from devise and will associate a User with a submitted resource. But how do I assign this without making some sort of input, like a hidden input. Would this go on behind the scenes in the controller?
To your last question:
devise adds a current_user method which is the logged in user. So if a user has multiple resources you could do something like:
#resource = current_user.resources.new(params[:resource])
First question:
When a form is rendered it is done so based on the #resource & #categories variables. When you post the form the create action is called which creates a new #resource. If the save fails for whatever reason the form is rerendered using the new #resource variable. The problem you have is that #resource.category is not set when you show the form again. So you'll have to do this before the is_valid? check.
def create
#title = "Submit Resource"
#categories = Category.all
#resource = Resource.new(params[:resource])
#resource.category = Category.find(params[:category_id])
if #resource.valid? # won't be valid if there is no category found.
#resource.cost = params[:cost]
#resource.save
redirect_to root_url
else
render :action => :new
end
end
But the real problem is with your form. It should nest the category_id in the resource params so that the category is set when you do Resource.new(params[:resource]).
Check the POST request body in your console or something and see if it's nested in the resource or not. I don't know the exact syntax for it but if you change this you can drop the #resource.category = Category.find line.
To piggyback on Sandip, you can dry up your actions by using a before_filter
class ResourcesController < ApplicationController
before_filter :load_categories, :only => [:show, :create]
def new
#resource = Resource.new
end
def create
#resource = Resource.new(params[:resource])
#resource.category = Category.find(params[:category_id])
if #resource.valid? # won't be valid if there is no category found.
#resource.cost = params[:cost]
#resource.save
redirect_to root_url
else
render :action => :new
end
end
private
def load_categories
#categories = Category.all
end
end
also if you plan on sticking #title inside of your application layout, I would change #title in your view to:
yield(:title) || 'My Site'
and on the appropriate pages use:
content_for(:title) do
Submit Resource
It will default to 'My Site' unless otherwise specified.
Looks like there is problem with create action
def create
#title = "Submit Resource"
#categories = Category.all
#resource = Resource.new(params[:resource])
if #categories.collect(&:id).include?(params[:category_id].to_i)
#resource.category_id = params[:category_id]
end
#resource.user = current_user
if #resource.valid?
#resource.cost = params[:cost]
#resource.save
redirect_to root_url
else
render :action => :new
end
end
view
<div class="field">
<%= f.label :category %>
<%= select_tag(:category_id, options_for_select(#categories.map {|c|[c.name, c.id]}, :selected => #resource.category_id)) %>
</div>
I have a controller for an API that looks like this:
def index
respond_to do |format|
format.json { render :json => #groups.to_json(:only => [:id, :name, :description, :created_at, :updated_at])}
end
end
def show
respond_to do |format|
format.json { render :json => #group.to_json(:only => [:id, :name, :description, :created_at, :updated_at]) }
end
end
# #todo add store to item
def create
if #group.save
render :json => #group.to_json(:only => [:id, :name, :description, :created_at, :updated_at])
else
render :status => 406
end
end
def update
if #group.update_attributes(params[:group])
render :json => #group.to_json(:only => [:id, :name, :description, :created_at, :updated_at])
else
render :status => 406
end
end
def destroy
#group.destroy
render :text => ""
end
As you can see, I'm repeating my self a lot. I'd love to make these (and only these) attributes available by way of the model, but couldn't find a fitting solution. Is there anything to protect attributes from mass writing? Or do I possibly mean mass reading?
As noted in comments below I want to have a model with attributes, name and i_am_private. When I render that model as json - render :json => #model - I want only name to show up.
Ruby 1.8.7
Rails 3
How about overriding as_json method in your Group model?
class Group < ActiveRecord:Base
...
def as_json(options={})
{
:id => id,
:name => name,
:description => description,
:created_at => created_at,
:updated_at => updated_at
}
end
end
To prevent mass assignment, add the following to your model:
attr_accessible :attr1, :attr2, :attr3
where attr1, attr2, attr3 and so on are the attributes you want to allow for mass assignment, the rest of the attributes for that model will not be allowed for mass assignment.