How to validate foreign keys in Rails validations? - ruby-on-rails

In my Rails app I have users who can have many projects which in turn can have many invoices.
How can I make sure that a user can only create an invoice for one of his projects and not for another user's projects?
class Invoice < ActiveRecord::Base
attr_accessible :number, :date, :project_id
validates :project_id, :presence => true,
:inclusion => { :in => ????????? }
end
Thanks for any help.
class InvoicesController < ApplicationController
def new
#invoice = current_user.invoices.build(:project_id => params[:project_id])
end
def create
#invoice = current_user.invoices.build(params[:invoice])
if #invoice.save
flash[:success] = "Invoice saved."
redirect_to edit_invoice_path(#invoice)
else
render :new
end
end
end

I think that shouldn't be on a validation. You should ensure the project the user selected is one his projects.
You could do something on your controller like:
project = current_user.projects.find params[:project_id]
#invoice = Invoice.new(project: project)
# ...
Your create action could look something like this.
def create
#invoice = current_user.invoices.build(params[:invoice])
#invoice.project = current_user.projects.find params[:invoice][:project_id]
if #invoice.save
flash[:success] = "Invoice saved."
redirect_to edit_invoice_path(#invoice)
else
render :new
end
end

project_id is "sensitive" attribute - so remove it from attr_accessible. You are right that you should not believe params from the form and you must check it.
def create
#invoice = current_user.invoices.build(params[:invoice])
# #invoice.project_id is nil now because this attr not in attr_accessible list
#invoice.project_id = params[:invoice][:project_id] if current_user.project_ids.include?(params[:invoice][:project_id])
if #invoice.save
flash[:success] = "Invoice saved."
redirect_to edit_invoice_path(#invoice)
else
render :new
end
end
If user tries to hack your app and change project_id to not owned value then method create render partial new with invalid #invoice. Do not forget to leave the validation of project_id on presence.
If you get exception Can't mass-assign protected attributes... there are several ways what to do. The simplest ways are:
1. remove line from environment configs (development, test, production)
# Raise exception on mass assignment protection for Active Record models
config.active_record.mass_assignment_sanitizer = :strict
2. Reject sensitive parameters from params before assigning.
# changes in method create
def create
project_id = params[:invoice].delete(:project_id)
#invoice = current_user.invoices.build(params[:invoice])
#invoice.project_id = project_id if current_user.project_ids.include?(project_id)
...
end

OK, luckily I managed to come up with a solution of my own this time.
I didn't make any changes to my controller ("let's keep 'em skinny"), but added a validation method to my model instead:
class Invoice < ActiveRecord::Base
attr_accessible :number, :date, :project_id
validates :project_id, :presence => true,
:numericality => { :only_integer => true },
:inclusion => { :in => proc { |record| record.available_project_ids } }
def available_project_ids
user.project_ids
end
end
I am not sure if this is good or bad coding practice. Maybe someone can shed some light on this. But for the moment it seems pretty safe to me and I haven't been able to hack it in any way so far.

Related

How to add default values to nested items in a Rails controller?

In the Sign up form of my Rails 6 application an Account with a nested User can be created.
class AccountsController < ApplicationController
def new
#account = Account.new
#account.users.build(
:owner => true,
:language => "FR"
)
end
def create
#account = Account.new(account_params)
if #account.save
redirect_to root_url, :notice => "Account created."
else
render :new
end
end
private
def account_params
safe_attributes = [
:name,
:users_attributes => [:first_name, :last_name, :email, :password, :owner, :language]
]
params.require(:account).permit(*safe_attributes)
end
end
What is the best way to define default values on the new user here?
Right now, I use hidden_fields for the default values in my sign up form thus making them publicly available. This is of course not what I want because it's very insecure.
Is there a better way to deal with this?
I know that there's Rails' with_defaults method but I couldn't get it to work on nested items so far.
try with:
account_params[:users_attributes] = account_params[:users_attributes].with_defaults({ first_name: 'John', last_name: 'Smith'})
in first line of create action

Rails 4 ActiveRecord, if record exists, use id, else create a new record

When an employee is created, he is given a title. If the title is unique, the record saves normally. If the title is not unique, I want to find the existing title, and use that instead. I can't figure out how to do this in the create action.
employer.rb
class Employee < ActiveRecord::Base
belongs_to :title, :class_name => :EmployeeTitle, :foreign_key => "employee_title_id"
accepts_nested_attributes_for :title
end
employer_title.rb
class EmployerTitle < ActiveRecord::Base
has_many :employees
validates :name, presence: true, length: { maximum: 50 },
uniqueness: { case_sensitive: true }
end
new.html.erb
<%= f.simple_fields_for :title do |title| %>
<%= title.input :name, label: "Title" %>
<% end %>
employees_controller.rb
def create
if EmployeeTitle.exists?(name: employee_params[:title_attributes][:name])
# find title and use it?
else
#employee = current_user.employee.build(employee_params)
end
if #employee.save
flash[:success] = "Employee #{#employee.title.name} created."
redirect_to #employee
else
render 'new'
end
end
Edit: Using first_or_create
def create
EmployeeTitle.where(name: employee_params[:title_attributes][:name]).first_or_create do |title|
#employee = current_user.employees.build(employee_params, :title => title)
end
if #employee.save
flash[:success] = "Employee #{#employee.title.name} created."
redirect_to #employee
else
render 'new'
end
end
This makes the #employee go out of scope. Error: Undefined method `save' for nil:NilClass.
In addition, if I do this, won't the title be created regardless of whether the rest of the employee data is valid?
Using private method
employee.rb
private
def title_attributes=(attributes)
self.title = EmployeeTitle.find_or_create_by_name(name: attributes[:name])
end
The value is not being set. I get a "cannot be blank" validation error. The parameters include
employee: !ruby/hash:ActionController::Parameters
title: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
name: Consultant
The !ruby/hash:ActiveSupport::HashWithIndifferentAccess was not there before.
employee_params
private
def employee_params
params.require(:employee).permit(
title_attributes: [:id, :name],
)
end
What you need to do is to change this:
def create
if EmployeeTitle.exists?(name: employee_params[:title_attributes][:name])
# find title and use it?
else
#employee = current_user.employee.build(employee_params)
end
if #employee.save
flash[:success] = "Employee #{#employee.title.name} created."
redirect_to #employee
else
render 'new'
end
end
with this:
def create
#employee = current_user.employee.build(employee_params)
if #employee.save
flash[:success] = "Employee #{#employee.title.name} created."
redirect_to #employee
else
render 'new'
end
end
Now, override title_attributes method by putting this code in your app/models/employee.rb file:
def title_attributes=(attributes)
self.title = EmployeeTitle.find_or_create_by_name(attributes[:name])
end
Now, every time you'll create an employee whose name already exists with the particular name, it'll be used by default for associating it as title. Let the controller be skinny as it used to be.
Read more about find_or_create_by method here.
However, your question's title says: Rails 4, but you have tagged ruby-on-rails-3.2. If you're using Rails 4 then you can use this instead:
EmployeeTitle.find_or_create_by(name: attributes[:name])

How to use strong parameters on ruby on rails?

I am new to ruby on rails 4, and I am trying to use strong parameters to require that a "project" exists before adding a "role" to a project. The "role" itself requires a "project" to be associated to.
The issue that I am having is that with my current code, I am getting the error
"undefined methodpermit' for "1":String"` - how can I resolve this???
The error is identified in my "roles" controller -->
private
def role_params
params.require(:project_id).permit(:role)
end
def project
#project ||= Project.find(params[:project_id])
end`
My create method in the controller is
def create
#role = project.roles.create(role_params)
new_was_successful = #role.save
end
the model is:
class Role < ActiveRecord::Base
belongs_to :project
validates :project_id , :presence => true
end
What am I doing wrong??
Update
def role_params
params.require(:project_id).permit(:role)
end
to
def role_params
params.require(:role).permit(:project_id) ## if more fields are present in role model then add them as arguments to permit
end
You are getting error because you have set the strong parameters incorrectly. In the params hash, you would get something like this:
Example :
"role"=>{"project_id"=>1,...} ### ... refers to other fields in role model, if present
EDIT
Update your create action as below
def create
#role = project.roles.create(role_params)
if #role.save
redirect_to #role, notice: 'Role was successfully created.'
else
render action: 'new'
end
end

Rails ArgumentError in OrdersController#new validation

Second question on here, I'd really like to solve this one myself but I just don't know where to start to debug it.
So here is my error in the browser (which occurs when I go to check out and enter my details in order/_form.html.erb)
ArgumentError in OrdersController#new
You need to supply at least one validation
Rails.root: C:/Users/Ruby/rails_practice/depot4
Application Trace | Framework Trace | Full Trace
app/models/payment_type.rb:6:in <class:PaymentType>'
app/models/payment_type.rb:1:in'
app/models/order.rb:7:in <class:Order>'
app/models/order.rb:1:in'
app/controllers/orders_controller.rb:1:in `'
And here is my def new in OrdersController:
def new
#cart = current_cart
if #cart.line_items.empty?
redirect_to store_url, :notice => "Your cart is empty"
return
end
#hide_checkout_button = true
#order = Order.new
respond_to do |format|
format.html # new.html.erb
format.json { render json: #order }
end
end
The thing is that I haven't touch def new, I've been working on def create, which is here:
def create
#order = Order.new(params[:order])
#order.add_line_items_from_cart(current_cart)
#cart = current_cart
#hide_checkout_button = true
pay_type = PaymentType.find( :conditions => ['pay_type = ?', #order.pay_type] )
#order.payment_type_id = pay_type.id
respond_to do |format|
if #order.save
Cart.destroy(session[:cart_id])
session[:cart_id] = nil
format.html { redirect_to(store_url, :notice => 'Thank you for your order.') }
format.json { render json: #order, status: :created, location: #order }
else
format.html { render action: "new" }
format.json { render json: #order.errors, status: :unprocessable_entity }
end
end
end
What I am trying to do there is create an order which is which belongs_to a payment_type and has_many line_items which belongs_to a cart.
Incidentally, I am also trying to hide_checkout_button with an instance variable when I am on the order page.
The Orders table has a foreign key to the PaymentTypes table and I am trying to find the correct id from this PaymentTypes table for the payment_type submitted by the user.
If I comment out these two lines:
pay_type = PaymentType.find( :conditions => ['pay_type = ?', #order.pay_type] )
#order.payment_type_id = pay_type.id
Sometimes I get a different error:
NoMethodError in OrdersController#new
undefined method `key?' for nil:NilClass
I think this is to do with incorrect caching in the browser but I'm not sure what the connection is.
I will update with the rest after I post this first
Part deux
I know that this is about validation, but I can't see what I am doing wrong... order.rb:
class Order < ActiveRecord::Base
attr_accessible :address, :email, :name, :pay_type, :payment_type_id, :cart_id,
:product_id
has_many :line_items, :dependent => :destroy
belongs_to :payment_type
PAYMENT_TYPES = PaymentType.pluck(:pay_type)
validates :name, :address, :email, :pay_type, :presence => true
validates :pay_type, :inclusion => PAYMENT_TYPES
And then you've got the other side of that belongs_to in payment_type.rb
class PaymentType < ActiveRecord::Base
attr_accessible :pay_type
has_many :orders
validates ***:pay_type,*** :uniqueness
end
I know that I am totally just confusing things but I have one fail in the functionals tests and one error that has something to do with updating an order but I don't know what yet. I am going to work on them to see if by solving them I inadvertently solve this weird error.
If anyone can give me tips on hacking and debugging in rails that would be great. I would love to be able to solve this without typing all of this in here.
I don't think the server trace gives any more information than the browser window in this case but if you need it, or anything else please ask.
UPDATE:
So my problem is that I know how to solve it with a global variable in payment_type.rb, but this means that I have one column of payment types in the Orders table and another of names and payment_type_ids in another column, which is the foreign key.
Since I have the foreign key I shouldn't need a specific column for payment_types in the Orders table. I should just be able to see the value from the PaymentType table in the Orders view.
How do you do this without a Global variable?
UPDATE deux:
Ok, so I never posted this before (from orders_form.html.erb):
26: <div class="field">
27: <%= f.label :pay_type %><br />
28: <%= f.select :pay_type, PaymentType::PAYMENT_TYPES,
29: :prompt => 'Select a payment method' %>
30: </div>
31: <div class="actions">
So I've tried to select for :pay_type in Orders but given options from :pay_type in PaymentTypes.
I can't imagine that matters does it? Seems to be where my problem lies, but can't be sure.
I think the syntax of your validate inclusion of is wrong. It should be something like:
validates :pay_type, :inclusion => { :in => PAYMENT_TYPES }

How to set values in rails without form field

We have an field in the Database which should be set automatic as an UUID String. how do we that. the view doesn't contain this field because it will be autogenerated.
Our new-form will called so.
def new
#list = List.new
respond_to do |format|
format.html
end
Our create-action is here
def create
#list = List.new(params[:list])
#list = list.create!(params[:list])
end
If we try this
#list.admin_key = UUIDTools::UUID.timestamp_create().to_s
we get an validation error and the field is empty. the controller require
require 'uuidtools'
Our validation for the field is that is prencense and unique
validates :admin_key,
:presence => true,
:uniqueness => true
How did we get the admin_key into the database?
u have to do this in your model
before_validation(:on => :create) do
self.admin_key = UUIDTools::UUID.timestamp_create().to_s
end

Resources