Rails Strong Parameters - create action with foreign key parameters - ruby-on-rails

I have three models, Server has many maintenances and Maintenance belongs to Server and User.
create_table :maintenances do |t|
t.references :server, foreign_key: true
t.references :user, foreign_key: true
t.string :name
t.text :content
end
In console I can create records as follows:
Server.create(:hostname => "Sissy", :description => "Webserver")
Maintenance.create(:server_id => 1, :user_id => 1, :name => "Test", :content => "Test" )
My Question is: How can I do this in my Controller create action?
Problem is that :user_id is not part of the maintenance params hash, so if I write
def create
#server = Server.find(params[:id])
#maintenance = #server.maintenances.create!(maintenance_params)
end
private
def maintenance_params
params.require(:maintenance).permit(:user_id => current_user.id,
:id,
:name,
:content)
end
I'm getting
Syntax error, unexpected ',', expecting =>
...ser_id => current_user.id, :id, :name, :content, :start, :pl...
... ^):
app/controllers/maintenances_controller.rb:41: syntax error, unexpected ',', expecting =>

You can add , user_id inside your create action itself. Try this.
def create
#server = Server.find(params[:id])
params[:maintenance][:user_id] = current_user.id
#maintenance = #server.maintenances.create!(maintenance_params)
end

A good way is by passing a block to the create! method:
#maintenance = #server.maintenances.create!(maintenance_params) do |m|
m.user = current_user
end
The record is yielded to the block (before it is validated/saved).
This also works with new, create, update and update!.
But you should consider if you should be using the bang method create! here as it will raise an uncaught ActiveRecord::RecordNotValid error if any of the validations fail.
def create
#server = Server.find(params[:id])
#maintenance = #server.maintenances.new(maintenance_params) do |m|
m.user = current_user
end
if #maintenance.save
redirect_to #maintenance
else
render :new
end
end
The ActiveRecord::Persistence bang methods should only really be used in things like seed files or where a record not passing the validations is an exceptional event.

Yes, you can't do anything like that in strong_params method. Nor is it its purpose. Separate the whitelisting and default params.
I usually do it like this:
def create
#server = Server.find(params[:id])
#maintenance = #server.maintenances.create!(maintenance_params.merge(user_id: current_user.id))
end
private
def maintenance_params
params.require(:maintenance).permit(:id, :name, :content)
end

Related

Rails multiple params permit calls

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

Ruby-On-Rails model level class array

I'm trying to think of a best solution for following scenario. I've a model called an 'Article' with an integer field called 'status'. I want to provide class level array of statuses as shown below,
class Article < ActiveRecord::Base
STATUSES = %w(in_draft published canceled)
validates :status, presence: true
validates_inclusion_of :status, :in => STATUSES
def status_name
STATUSES[status]
end
# Status Finders
def self.all_in_draft
where(:status => "in_draft")
end
def self.all_published
where(:status => "published")
end
def self.all_canceled
where(:status => "canceled")
end
# Status Accessors
def in_draft?
status == "in_draft"
end
def published?
status == "published"
end
def canceled?
status == "canceled"
end
end
So my question is if this is the best way to achieve without having a model to store statuses? And secondly how to use these methods in ArticlesController and corresponding views? I'm struggling to understand the use of these methods. To be specific, how to do following?
article = Article.new
article.status = ????
article.save!
or
<% if article.in_draft? %>
<% end %>
I greatly appreciate any sample code example. I'm using rails 4.0.0 (not 4.1.0 which has enum support).
You could define all the methods using define_method, and use a hash instead of an array:
STATUSES = {:in_draft => 1, :published => 2, :cancelled => 3}
# Use the values of the hash, to validate inclusion
validates_inclusion_of :status, :in => STATUSES.values
STATUSES.each do |method, val|
define_method("all_#{method)") do
where(:status => method.to_s)
end
define_method("#{method}?") do
self.status == val
end
end
In that way, you can add statuses in the future without needing to create the methods manually. Then you can do something like:
article = Article.new
article.status = Article::STATUSES[:published]
...
article.published? # => true

Rendering json from two models in Rails index

I would like to render json in an index method that joins data from a foreign-key related table in Rails. I have a model that has user_id as a foreign key.
class Batch < ActiveRecord::Base
belongs_to :user
I am then rendering that table and exposing the user_id field.
def index
panels = Batch.where("user_id = #{current_user.id} or (user_id <> #{current_user.id} and public_panel = true)")
render json: panels, only: [:id, :description, :name, :public_panel, :updated_at, :user_id], root: false
end
What I would like to do is to somehow expose the users name, which is on the Users model. ie: user_id.user_name
EDIT
From reading documentation, I think I need to use include but would also like to alias one of the fields. I have a field called name in both tables.
Something is wrong with this include
def index
panels = Batch.include([:users]).where("user_id = #{current_user.id} or (user_id <> #{current_user.id} and public_panel = true)")
render json: panels, only: [:id, :name, :description, :public_panel, :updated_at, :user_id], root: false
end
EDIT2
Thank you #Paritosh Piplewar ... I am getting some errors with the syntax. To complicate matters the field I am after is user.name, not user.user_name. This will conflict with batches.name, so I need to alias it.
Started GET "/api/batches" for 10.0.2.2 at 2014-08-13 15:39:03 +0200
SyntaxError (/home/assay/assay/app/controllers/api/batches_controller.rb:11: syntax error, unexpected tLABEL
user: { only: :first_name },
^
/home/assay/assay/app/controllers/api/batches_controller.rb:11: syntax error, unexpected ',', expecting keyword_end
user: { only: :first_name },
^
/home/assay/assay/app/controllers/api/batches_controller.rb:12: syntax error, unexpected ',', expecting keyword_end):
EDIT 3
The original question has been answered, but the json returned is like this
data: [{"id"=>1306, "updated_at"=>Wed, 13 Aug 2014 12:37:23 UTC +00:00, "description"=>"asc", "user_id"=>1, "public_panel"=>true, "user"=>{"name"=>"Joe Bloggs"}}, {"id"=>1307,
This bit is causing problems for my Angular.js front-end.
"user"=>{"name"=>"Joe Bloggs"}}
i assume you mean user.user_name
this is how you can do it
def index
panels = Batch.where("user_id = #{current_user.id} or (user_id <> #{current_user.id} and public_panel = true)")
data = panels.as_json(include:
{user: { only: :name }},
only: [:id, :description, :public_panel, :updated_at, :user_id],
root: false)
render json: data
end
You can define custom method and do it. Something like this
class Batch < ActiveRecord::Base
belongs_to :user
def user_name
user.name
end
end
Now, in controller you can simply do this
def index
panels = Batch.where("user_id = ? or (user_id <> ? and public_panel = true)", current_user.id, current_user.id)
data = panels.as_json(methods: :user_name,
only: [:id, :description, :public_panel, :updated_at, :user_id, :user_name],
root: false)
render json: data
end
For more complex json views than just redering a single model, I'd tell you to use jbuilder. Jbuilder is now part of the rails framework.
It's as easy as
Remove render line and make panels an instance variavle (#panels)
Create a index.json.jbuilder file under app/views/api/batches
Create the view you want
json.array #panels do |panel|
panel.(panel,
:id,
:description,
:public_panel,
:user_id,
:updated_at
)
json.user panel.user.user_name
end

How should I refactor create_unique methods in Rails 3?

I have following ugly create_unique method in few models ex:
def self.create_unique(p)
s = Subscription.find :first, :conditions => ['user_id = ? AND app_id = ?', p[:user_id], p[:app_id]]
Subscription.create(p) if !s
end
And then in controllers #create actions I have
s = Subscription.create_unique({:user_id => current_user.id, :app_id => app.id})
if s
raise Exceptions::NotAuthorized unless current_user == s.user
#app = s.app
s.destroy
flash[:notice] = 'You have been unsubscribed from '+#app.name+'.'
redirect_to '/'
end
did you try dynamic finders ?
find_or_initialize_by_user_id_and_app_id
find_or_create_by_user_id_and_app_id
first_or_initialize...
first_or_create....
check manual http://guides.rubyonrails.org/active_record_querying.html#dynamic-finders
also option is to create validation rule for check unique value
class Subscription < ActiveRecord::Base
validates_uniqueness_of :user_id, :scope => :app_id
end
then
sub = Subscription.new({:user_id => current_user.id, :app_id => app.id})
sub.valid? #false
You can use validates_uniquness_of :app_id,:scope=>:user_id so app id is uniq for respected user_id

How do I catch/solve an ActiveRecord:RecordInvalid exception caused when I save a built assocation

I am hoping to get some help solving a problem that I'm sure many of you could avoid in your sleep.
I have two models in a habtm relationship. A package can have many locations, and a location can have many packages. If my location model fails validation (due to an empty location address, for example), I get anActiveRecord:RecordInvalid exception. I understand that I'm getting this error because when I call package.save, rails automatically calls save! on the location association.
I'm not sure how to avoid the error or at least rescue the error. Do any of you have any good advice, both on how to solve the problem and on Rails best practices?
Here is the code:
def create
#package = current_user.package.build(params[:package])
package_location
if #package.save
flash[:success] = "Package created!"
redirect_to root_path
else
render 'pages/home'
end
end
def package_location
gps_processing if !session[:gps_aware]
#package.locations.build(:address => session[:address])
end
def gps_processing
session[:address] = [params[:story][:street_address], params[:story][:city], params[:story][:state], params[:story][:country]].compact.join(', ')
end
class Package< ActiveRecord::Base
belongs_to :user
has_and_belongs_to_many :locations
validates :content, :presence => true,
:length => {:maximum => 140}
validates :user_id, :presence => true
default_scope :order => 'package.created_at DESC'
end
class Location < ActiveRecord::Base
attr_accessible :lng, :lat, :address
validates :lng, :presence => true
validates :lat, :presence => true
validates :address, :presence => true
geocoded_by :full_street_address, :latitude => :lat, :longitude => :lng
before_validation :geocode
has_and_belongs_to_many :packages
def full_street_address
address
end
end
`
Thanks in advance for your help!
The selected answer is not accurate. According to documentation here there's a simple way to catch rescue this exception:
begin
complex_operation_that_calls_save!_internally
rescue ActiveRecord::RecordInvalid => invalid
puts invalid.record.errors
end
You can access the messages instance variable of errors and get the field and error message associated.
A couple ideas off the top of my head:
Use #package.save! and a rescue block:
def create
#package = current_user.package.build(params[:package])
package_location
#package.save!
flash[:success] = "Package created!"
redirect_to root_path
rescue
render 'pages/home'
end
Use validates_associated in your Package model, and only save if it's valid:
def create
#package = current_user.package.build(params[:package])
package_location
# You might be able to just use if(#package.save), but I'm not positive.
if(#package.valid?)
#package.save!
flash[:success] = "Package created!"
redirect_to root_path
else
render 'pages/home'
end
end
And I'm sure there are a couple more ways, too, as you're working in Ruby...
Hope that helps!
Here's the code that I used to solve the problem while giving the user good feedback on the why the save failed. Please forgive my inelegant ruby code.
One small problem remains . . . if the package and the location both fail validation, only the location error message is displayed on reload. If the user then corrects the location error but not the package error, he is shown the package error message. I'm working on how to show all of the errors on the first reload
def create
#package= current_user.package.build(params[:package])
if package_location && #package.save
flash[:success] = "Package created!"
redirect_to root_path
else
render 'pages/home'
end
end
def package_location
gps_processing if !session[:gps_aware]
location = #package.locations.build(:address => session[:address])
if !location.valid?
#package.errors.add(:address, "You have entered an invalid address")
return false
else
return true
end
end
def gps_processing
session[:address] = [params[:story][:street_address], params[:story][:city],
params[:story][:state], params[:story][:country]].compact.join(', ')
end

Resources