I have these models:
class Organisation < ActiveRecord::Base
has_many :people
has_one :address, :as => :addressable,
:dependent => :destroy
accepts_nested_attributes_for :address, :allow_destroy => true
end
class Person < ActiveRecord::Base
attr_accessible :first_name, :last_name, :email, :organisation_id, :address_attributes
belongs_to :user
belongs_to :organisation
has_one :address, :as => :addressable,
:dependent => :destroy
accepts_nested_attributes_for :address, :allow_destroy => true
# These two methods seem to have no effect at all!
validates_presence_of :organisation, :unless => "address.present?"
validates_associated :address, :unless => "organisation.present?"
end
class Address < ActiveRecord::Base
belongs_to :addressable, :polymorphic => true
validates_presence_of :line1, :line2, :city, :zip
end
...and these views:
_fields.html.erb:
<%= render 'shared/error_messages', :object => f.object %>
<fieldset>
<div class="left">
<%= f.label :first_name %><br/>
<%= f.text_field :first_name %>
</div>
<div>
<%= f.label :last_name %><br/>
<%= f.text_field :last_name %>
</div>
<div>
<%= f.label :email %><br/>
<%= f.text_field :email %>
</div>
<div>
<%= f.label :organisation_id %><br/>
<%= f.select(:organisation_id, current_user.organisation_names, {:include_blank => "--- None ---"}, :id => 'organisation_select') %>
</div>
</fieldset>
<%= f.fields_for :address do |address| %>
<%= render 'shared/address', :f => address %>
<% end %>
_address.html.erb:
<fieldset id="address_fields">
<div>
<%= f.label :line1 %>
<%= f.text_field :line1 %>
</div>
<div>
<%= f.label :line2 %>
<%= f.text_field :line2 %>
</div>
<div>
<%= f.label :zip %>
<%= f.text_field :zip %>
</div>
<div>
<%= f.label :city %>
<%= f.text_field :city %>
</div>
</fieldset>
people_controller.rb:
def new
puts params.inspect
#person = Person.new(:organisation_id => params[:organisation_id])
#person.build_address
#title = "New person"
end
{"action"=>"new", "controller"=>"people"}
def edit
puts params.inspect
#title = #person.name
end
{"action"=>"edit", "id"=>"69", "controller"=>"people"}
def create
puts params.inspect
if params[:organisation_id]
#person = current_user.organisations.build_person(params[:person])
else
#person = current_user.people.build(params[:person])
end
if #person.save
flash[:success] = "Person created."
redirect_to people_path
else
render :action => "new"
end
end
{"commit"=>"Create", "action"=>"create", "person"=>{"last_name"=>"Doe", "organisation_id"=>"9", "email"=>"john.doe#email.com", "first_name"=>"John", "address_attributes"=>{"city"=>"Chicago", "zip"=>"12345", "line2"=>"Apt 1", "line1"=>"1 Main Street"}}, "authenticity_token"=>"Jp3XVLbA3X1SOigPezYFfEol0FGjcMHRTy6jQeM1OuI=", "controller"=>"people", "utf8"=>"✓"}
Inside my Person model I need to make sure that only if a person's organisation_id is blank, that person's address fields have to be present.
I tried something like this:
validates :address, :presence => true, :if => "organisation_id.blank?"
But it's not working.
How can this be done?
Thanks for any help.
First of all, I want to be sure that you mean blank? rather than present?. Typically, I see this:
validate :address, :presence_of => true, :if => 'organisation.present?'
Meaning, you only want to validate address if organisation is also present.
Regarding, :accepts_nested_attributes_for, are you using this feature by passing in nested form attributes, or some such thing? I just want to make sure you absolutely need to use this functionality. If you are not actually dealing with nested form attributes, you can implement cascading validation using:
validates_associated :address
If you do need to use :accepts_nested_attributes, be sure to check out the :reject_if parameter. Basically, you can reject adding an attribute (and it's descendants) altogether if certain conditions apply:
accepts_nested_attributes_for :address, :allow_destroy => true, :reject_if => :no_organisation
def no_organisation(attributes)
attributes[:organisation_id].blank?
end
Now, if none of the above apply, let's take a look at your syntax:
It should work, :if/:unless take symbols, strings and procs. You don't need to point to the foreign_key, but can simplify by pointing to:
:if => "organisation.blank?"
You have other validations in the Address model, correct? Is Address being validated when you don't want it to? Or is Address not being validated? I can help you test it out in the console if you can give me some additional details.
To make things easier for myself re: mass-assignment, I changed the rails config: config.active_record.whitelist_attributes = false
I created a gist for you to follow along
I have a sample project as well. Let me know if you are interested.
Basic points:
Added the following to Person to ensure that either Org or Address are valid:
validates_presence_of :organisation, :unless => "address.present?"
validates_associated :address, :unless => "organisation.present?"
Added validation to Address to trigger errors when Org is not present:
validates_presence_of :line1, :line2, :city, :zip
I was able to produce the requirements you are seeking. Please look at the gist I created where I have a full console test plan.
I added a controller file to the previous gist.
Overview:
All you should need to create the person is:
#person = current_user.people.build(params[:person])
:organisation_id will always be found off of the :person param node, like so:
params[:person][:organisation_id]
So you're if will never be true.
I updated the gist with the necessary changes to the controller, the model and the form.
Overview:
You need to cleanup your controller. You are using accepts_nested_attribute, so in the :create, you only care about params[:person]. Additionally, in the render :new, you need to setup any instance variables that the partial will use. This does NOT go back through the :new action. The :new and :edit actions also need to be simplified.
Your Person model needs to use the :reject_if argument because the Address fields are coming back to the :create action as :address_attributes => {:line1 => '', :line2 => '', etc}. you only want to create the association if any have values. Then your validates_presence_of for :organisation will work just fine.
Your form needs to pass the organisation id to the controller, rather than the organisation names
It's all in the gist
Should be the final gist.
Overview:
Add the following to your edit action right after building the #person:
#person.build_address if #person.address.nil?
This ensure that you have the address inputs, even if the #person.address does not exist. It doesn't exist, because of the :reject_if condition on accepts_nested_attributes
I DRYed up the :reject_if as follows. It's a little hacky, but has some utility:
accepts_nested_attributes_for :address, :allow_destroy => true, :reject_if => :attributes_blank?
def attributes_blank?(attrs)
attrs.except('id').values.all?(&:blank?)
end
a. attrs -> the result of params[:person][:address]
b. .except('id') -> return all key-values except for 'id'
c. .values -> return all values from a hash as an array
d. .all? -> do all elements in the array satisfy the following check
e. &:blank -> ruby shorthand for a block, like this: all?{ |v| v.blank? }
Are you sure you didn't mean:
validates :address, :presence => true, :if => organisation_id.nil?
A more simple approach might be to add a custom validator. It's super easy, and you don't have to stumble on syntax or try to figure out why Rails' magic isn't working.
Inside my Person model I need to make sure that only if a person's organisation_id is blank, that person's address fields have to be present.
class Person < ActiveRecord::Base
...
validate :address_if_organisation_id_is_present
private
def address_if_organisation_id_is_present
return true unless organisation_id
errors.add(:address, "cannot be blank") unless address
end
end
Adding to a model's errors will prevent it from saving. Note: you may wish to use address.blank? or address.empty? as discussed in other answers, but you can define this for the behavior you'd like.
Related
I am trying to create a web application to practise my Ruby on Rails skill. I have a few entities in my database manufacturers, models, tints, prices
manufacturers {id, name} - stores the make of the car
models {id, manufacturer_id, name} - stores the models of the car
tints {id, manufacturer_id, model_id, front, sides, rear} - stores the length of tint required
prices {id, description, price } - stores the price of the item
I created a page to generate a quotation for window tinting. The page includes drop-down menus to let user to select manufacturer, model, type of film(front), type of film(side+rear)
Below is the code for the form
<%= form_tag('/quotation/tints/generate') do %>
<%= label :manufacturer_id, 'Manufacturer' %>
<div class="field">
<%= collection_select(:tint, :manufacturer_id, Manufacturer.order(:name), :id, :name, {:prompt => "Select Manufacturer"}) %>
</div>
Model:
<div class="field">
<%= grouped_collection_select(:tint, :model_id, Manufacturer.order(:name), :models, :name, :id, :name, {:prompt => "Select Model"}) %>
</div>
<%= label :price_front, 'Front Tint' %>
<div class="field">
<%= collection_select(:price, :price_front, Price.order(:name), :id, :name, {:prompt => "Select Front Tint"}) %>
</div>
<%= label :price_rear, 'Size and Back Tint' %>
<div class="field">
<%= collection_select(:price, :price_rear, Price.order(:name), :id, :name, {:prompt => "Select Side & Rear Tint"}) %>
</div>
<div class="form-group">
<%= submit_tag 'Submit' %>
</div>
<% end %>
When the form is submitted, it should be redirected to /quotation/tints/generate and display the value from the dropdown menu. However, I received an error, saying that Couldn't find Manufacturer with 'id'=. The code that caused the error is shown below
def generate
#manufacturers = Manufacturer.find(params[:manufacturer_id])
end
Here is the parameter from the debug log
{"utf8"=>"✓",
"authenticity_token"=>"Pl2bXiRT0AoF4i0h1RCHDbuvaKJNZOkV5ULQHKxDQgZzBWWLJ2mH7ddb9akwgxbloxBIHoVaT3pcwoIGcRufpg==",
"tint"=>{"manufacturer_id"=>"7", "model_id"=>"6"},
"price"=>{"price_front"=>"1", "price_rear"=>"2"},
"commit"=>"Submit"}
I can see that the id of each drop down value are shown up correctly in the parameter list. However, I coundn't able to print the value at /quotation/tints/generate nor get the name of the manufacturer or model.
Here is routes.rb:
get '/quotation/tints' => 'tints#quotation', :as => 'tints_quotation'
post '/quotation/tints/generate' => 'tints#generate', :as => 'generate_tints_quotation'
Tint.rb:
class Tint < ApplicationRecord
has_many :manufacturers
has_many :models
belongs_to :manufacturer
belongs_to :model
validates_uniqueness_of :model_id, :scope => [:manufacturer_id]
end
Model.rb:
class Model < ApplicationRecord
belongs_to :manufacturer, :dependent => :destroy
validates :name, :presence => true
validates_uniqueness_of :name, :scope => [:manufacturer_id]
before_save :capitalize_content
end
Manufacruter.rb:
class Manufacturer < ApplicationRecord
has_many :models, :dependent => :destroy
validates :name, :presence => true, uniqueness: { case_sensitive: false }
before_save :capitalize_content
end
tints.controller.rb:
def quotation
render 'quotation'
end
def generate
#manufacturers = Manufacturer.find(params[:manufacturer_id])
end
generate.html.erb:
<%= #manufacturers.name %>
I'm trying to print the manufacturer selected
I have tried multiple ways to define it, but I am still facing the same error. Any help is greatly appreciated.
In your params, manufacturer_id is a nested value of tint, as opposed to being a direct key of the params hash. Try the following:
def generate
#manufacturers = Manufacturer.find(params[:tint][:manufacturer_id])
end
I have two model like:
class Employee
field :name
field :login, type: Boolean
has_one :user
end
class User
field :username
field :email
belongs_to :employee
validates_presence_of :username
end
I want to create an user account when create employee if check box of login field is checked. For this my new action of employees controller is:
def index
#employee = Employee.new
#employee.build_user
end
For this my form code is:
<%= simple_form_for(#employee) %>
<%= f.input :login, :as => :boolean, :label => "Create User" %>
<div class="create-user" style="display: none">
<%= f.simple_fields_for :user do |u| %>
<%= render 'user_fields', {f: u} %>
<% end %>
</div>
<button class="btn btn-info">Save Change</button>
<% end %>
and the _user_fields.html.erb is:
<%= f.input :username %>
I want to validate the user model when check_box of :login field is checked. On unchecked condition the form should be submit. What is the better solution.
Maybe you can move your user validation into employer model ?
something like code below should solve your problems
validates_presence_of :username, :if => login?
What do you think?
maybe use validates_associated:
http://guides.rubyonrails.org/active_record_validations_callbacks.html#validates_associated
or delegation + validates presence might also work
delegate :username, to: :user, allow_nil: true
validates :username, presence: true, if: :login
Finally I got its solution:
class Employee
field :name
field :login, type: Boolean
has_one :user
has_one :user, :class_name => "User", :dependent => :destroy
accepts_nested_attributes_for :user, :reject_if => :login_blank
def login_blank
return false if self.login == true
return true if self.login == false
end
end
I am trying to use the nested_form gem to create a dropdown that will show a list of chains and their locations. When the user clicks the submit button, the nested_form should create a new entry in a joiner table called users_locations. However, the form spits out an error as follows:
User locations user can't be blank
In other words, its complaining that the user_id is NULL and therefore it cannot create an entry in the table (because user_id is a required field in the user_locations table).
_form.html.erb (In the User view folder)
<div class="field" id="location_toggle">
<%= f.label "Location:", :class => "form_labels", required: true %>
<%= f.fields_for :user_locations do |location| %>
<%= location.grouped_collection_select :location_id, Chain.all, :locations, :name, :id, :name, include_blank: false %>
<% end %>
<%= f.link_to_add "Add a Location", :user_locations, :class => "btn btn-primary btn-small" %>
<br/>
</div>
user.rb (model)
belongs_to :chain
has_many :user_locations
has_many :locations, :through => :user_locations
...
accepts_nested_attributes_for :user_locations, :reject_if => :all_blank, :allow_destroy => true
validates_associated :user_locations, :message => ": you have duplicate entries."
...
attr_accessible :first_name, :last_name, ... , :user_locations_attributes
users_controller.rb
def create
#user = User.new(params[:user])
#user.save ? (redirect_to users_path, notice: 'User was successfully created.') : (render action: "new")
end
Log shows:
SELECT 1 AS one FROM "user_locations" WHERE ("user_locations"."user_id" IS NULL AND "user_locations"."location_id" = 1)
I'm assuming you're user_location validates :user_id, presence: true or something, right?
I had a look at this recently in response to this question, it seems that, when creating nested models, validations for all objects to be created are run before any of them are saved, and thus even though your model will get have user_id set when it is saved, it cannot have it when validated.
To get around this you may want to disable the validation on creation, for example with:
# user_locations.rb
validates :user_id, presence: true, on: :update
It looks like you're blending simple_form with the default Rails form helpers and getting some odd behavior. I haven't worked with simple_form, but my guess is that you can fix your problem by changing f.fields_for to f.simple_fields_for.
Here's an example that may help.
I am trying to build a simple_nested_form in my Ruby on Rails app. When I submit my form I am getting some unknown error because it is just redirecting back to the form to input again. Here is the output in the rails server console for when I submit the form. It looks like there is some random "0" => thrown in there.
Parameters: {"machine"=>{"name"=>"2134", "ip_adress"=>"2", "machine_employees_attributes"=>{"0"=>{"machine_id"=>"1", "employee_id"=>"2"}}}, "commit"=>"Create Machine"}
I have a machine model which has_many :machine_employees
and a machineemployee model which belongs_to :machine
Do you have any idea why this 0 => could be appearing because I think it is what is giving me the issues.
Here is the code for my models.
Machine
class Machine < ActiveRecord::Base
# Relationships
has_many :machine_employees
has_many :employees, :through => :machine_employees
accepts_nested_attributes_for :machine_employees, :reject_if => lambda{ |me| me[:employee_id].blank? }
attr_accessible :ip_adress, :name, :machine_employees_attributes
# Validations
validates_presence_of :name, :ip_adress
end
MachineEmployee
class MachineEmployee < ActiveRecord::Base
before_validation :set_default
# Relationships
belongs_to :machine
belongs_to :employee
attr_accessible :employee_id, :machine_id, :end_date, :start_date
# Validations
validates_presence_of :employee_id, :machine_id, :start_date
private
# Callback Methods
def set_default
self.start_date = Date.today
self.end_date = nil
end
end
New Machine Form
<div class="row-fluid">
<div class="span3">
<h1>Add a Machine</h1>
<br />
<%= simple_nested_form_for #machine do |f| %>
<%= render "machine_fields", :f => f %>
<%= f.button :submit %>
<%= link_to 'Back', machines_path %>
</div>
<div class="span4">
<h4>Assign an Employee to This Machine</h4>
<%= f.simple_fields_for :machine_employees do |me_form| %>
<!-- render nested machine_employee fields-->
<%= render "machine_employee_fields", :f => me_form %>
<% end %>
</div>
<% end %>
</div>
Machine Employee Fields Partial
<%= f.input :machine_id, :as => :hidden, :input_html => { :value => #machine.id } %>
<%= f.input :employee_id, collection: #employees, :id => :name, :prompt => "Select ..." %>
The 0 is thrown in there because the machine model has_many machine_employees. When you use nested forms, it passes a pseudo-array for has_many relations. So, if you tried to submit 2 machine employees, your hash would look like this:
Parameters: {"machine"=>{"name"=>"2134", "ip_adress"=>"2", "machine_employees_attributes"=>{
"0"=>{"machine_id"=>"1", "employee_id"=>"2"},
"1"=>{"machine_id"=>"1", "employee_id"=>"3"}
}
}, "commit"=>"Create Machine"}
This way you can access the machine_employees passed from the form by doing params[:machine][:machine_employees_attributes][0] or params[:machine][:machine_employees_attributes][1]. Note that if this was a has_one relationship, then the machine_employees_attributes key would be changed to machine_employee_attributes and there would be no numerical index.
I suspect the problem is that your machine model must accept_nested_attributes_for :machine_employees and must also have attr_accessible :machine_employees_attributes.
I am kinda new to Rails and this is my first post to StackOverflow.
Say I have 3 models:
class Product < ActiveRecord::Base
default_scope :order => :title
has_many :line_items
has_many :promo_products
has_many :promotions, :through => :promo_products, :foreign_key => :promotion_id
before_destroy :ensure_not_referenced_by_any_line_item
before_destroy :ensure_not_referenced_by_any_promo_product
validates :title, :presence => true, :uniqueness => true
validates :description, :presence => true
validates :price, :numericality => {:greater_than_or_equal_to => 0.01}
private
def ensure_not_referenced_by_any_line_item
if line_items.empty?
return true
else
errors.add(:base, 'Line Items present')
return false
end
end
def ensure_not_referenced_by_any_promo_product
if promo_products.empty?
return true
else
errors.add(:base, 'Some promotions are still in effect')
return false
end
end
end
class Promotion < ActiveRecord::Base
CART_OR_PRODUCT = ['Cart', 'Product']
PROMOTION_TYPE = ['Percentage based', 'Value based']
has_many :promo_products
accepts_nested_attributes_for :promo_products
has_many :products, :through => :promo_products, :foreign_key => :product_id
accepts_nested_attributes_for :products
#attr_accessible :promo_products_attributes, :title, :description, :cart_or_product, :promotion_type, :discount, :minimum_price, :minimum_quantity
validates :title, :description, :presence => true
validates :cart_or_product, :inclusion => {:in => CART_OR_PRODUCT, :message =>
"is invlaid. Please select a valid option"}
validates :promotion_type, :inclusion => {:in => PROMOTION_TYPE, :message =>
"is invalid. Please select a valid option"}
validates :discount, :minimum_price, :numericality => {:greater_than_or_equal_to => 0.00}
validates :minimum_quantity, :numericality => {:greater_than_or_equal_to => 0}
end
class PromoProduct < ActiveRecord::Base
belongs_to :promotion
belongs_to :product
accepts_nested_attributes_for :products
end
In the promotions new page, I would like to show list of products that could be part of a promotion. A user may select 0, 1 or more products, depending on the type of promotion.
In the action new of promotions_controller, I built like this:
#promotion.promo_products.build.build_product
In the _form of promotions, I needed to show the list of products for user to select. I made a nested form like:
<%= form_for(#promotion) do |f| %>
<!-- other promotion fields -->
<%= f.fields_for :promo_products do |pp| %>
<%= pp.fields_for :products do |p| %>
<div class="field">
<%= f.label "Products" %><br />
<%= collection_select :promo_product, :product_id, Product.all, :id, :title {:selected => #promotion.product_ids}, {:multiple => true} %>
</div>
<% end %>
<% end %>
<% end %>
I have 2 issues.
First my code throws an error:
ArgumentError in PromotionsController#new
No association found for name `products'. Has it been defined yet?
If I change the line in PromoProduct model:
accepts_nested_attributes_for :products
to
accepts_nested_attributes_for :product
Then there are no errors, and everything works fine.
The data doesn't get saved to promo_product table. I have the create action in promo_product controller as:
def create
#promotion = current_promotion
products = Product.select(:id => params[:product_id])
products.each do |p|
promo_product = #promotion.promo_products.build(p)
promo_product.save
end
##promo_product = PromoProduct.new(params[:promo_product])
redirect_to promotions_path
end
How can I go about it?
Thank you.
You shouldn't put the "accept_nested_attribute_for" in the association table PromoProducts. It should exist in the model that you want to use for creating association to another model. "accept_nested_attribute_for" IIRC simply inserts an "[association]_attributes=" method for your model. For instance, if you add this method to your Product class for Promotion, you will get "promotion_attributes=" method inserted in the Product class. Then a nested form can use this function to create new objects with a hash that represents the model and association.
Base on the above, the create action shouldn't be in PromoProduct controller, instead it should be in Promotion controller.
<%= form_for(#promotion) do |f| %>
<!-- other promotion fields -->
<%= f.fields_for :products do |pp| %>
<div class="field">
<%= f.label "Products" %><br />
<%= collection_select :promo_product, :product_id, Product.all, :id, :title {:selected => #promotion.product_ids}, {:multiple => true} %>
</div>
<% end %>
<% end %>
I don't know without trying if the above collection_select line is correct. But you can debug this by checking the parameter returned by the form to the controller in the server console log. Basically you should see a nested hash of
{:promotion => {:products => ...}}
Let me know if you need more help on this. In my solution I used a combination of select_tag and options_from_collection_for_select. (But I don't recall the behavior of all these offhand without looking at the API doc.)
Lastly, do you need the :through model? I think since you created the through model you need to handle saving that in your create action. But since you don't have other attributes on the PromoProducts table I wonder if you want to simply leave it as a HABTM association and let rails deal with the rest?