Update2: I've cleaned up the code, which seems to have solved some of the problems. I've posted the new code as a new question here.
Update: Organization and User have a 1:many relationship. My question concerns a joined signup form where both an organization and user are required. After maxcal's help on the original post, I've written a new create
method for my nested form ("organization has many users"), as shown below. Also I added begin...rescue...end
to the create
method. The situation/problem now:
- Submitted with all valid info it works correctly.
- Submitted with invalid info for organization (doesn't matter if user is also invalid or not), it renders the page with the error messages, as we want it to, but it only shows errors for the organization details. Also, for the user details it has then emptied all the fields, which it shouldn't.
- Submitted with invalid info only for user, it renders the form again but without any error messages and all fields for user have been emptied.
Anyone got an idea what is wrong with the code? The problem seems to be more with the nested user than with organization (the parent). Also, users_attributes.empty?
doesn't work, since an empty submitted form still includes such attributes, according to the log:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"***", "organization"=>{"name"=>"", "bag"=>"", "users_attributes"=>{"0"=>{"email"=>"", "username"=>"", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "usertype"=>"2", "admin"=>"true"}}}, "commit"=>"Register"}
.
def create
@organization = Organization.new(new_params.except(:users_attributes))
begin
if users_attributes.empty?
@organisation.errors.add(:users, 'No user provided')
end
@organization.transaction do
@organization.save!
if users_attributes.any?
@organization.users.create!(users_attributes)
end
end
rescue ActiveRecord::RecordInvalid => invalid
if @organization.persisted?
if @organization.users.any?
@organization.users.each do |single_user|
single_user.send_activation_email
end
end
flash[:success] = "Confirmation email sent."
redirect_to root_url
else
@organization.users.build if @organization.users.blank?
render :new
end
end
end
private
# converts the hash of nested attributes hashes to an array
def users_attributes
new_params[:users_attributes].values
end
end
Original question:
I have two associated models and a nested form with validation. Unfortunately, it’s not working. 1) On seeding it generates the error Validation failed: Users organization can't be blank
. I previously posted a question about this and prematurely concluded it had solved it. It hasn’t. 2) Submitting my nested signup form with all fields filled in correctly, produces the flash error message The form contains 1 error. Users organization can't be blank
.
How should I adjust my code to solve these issues?
Model files:
#User model
belongs_to :organization, inverse_of: :users
validates_presence_of :organization_id, :unless => 'usertype == 1'
# Organization model
has_many :users, dependent: :destroy
accepts_nested_attributes_for :users, :reject_if => :all_blank, :allow_destroy => true
validate :check_user
private
def check_user
if users.empty?
errors.add(:base, 'User not present')
end
end
Organization Controller methods
def new
@organization = Organization.new
@user = @organization.users.build
end
def create
@organization = Organization.new(new_params)
if @organization.save
@organization.users.each do |single_user|
single_user.send_activation_email # Method in user model file.
end
flash[:success] = "Confirmation email sent."
redirect_to root_url
else
@organization.users.build if @organization.users.blank?
render 'new'
end
end
def new_params
params.require(:organization).permit(:name, :bag,
users_attributes: [:email, :username, :usertype, :password, :password_confirmation])
end
The form:
<%= form_for @organization, url: organizations_path do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.text_field :name %>
<%= f.text_field :bag %>
<%= f.fields_for :users do |p| %>
<%= p.email_field :email %>
<%= p.text_field :username %>
<%= p.text_field :fullname %>
<%= p.password_field :password %>
<%= p.password_field :password_confirmation %>
<%= p.hidden_field :usertype, value: 2 %>
<% end %>
In my Seeds file I have:
Organization.create!(name: "Fictious business",
address: Faker::Address.street_address,
city: Faker::Address.city,
users_attributes: [email: "[email protected]",
username: "helpyzghtst",
usertype: 2,
password: "foobar",
password_confirmation: "foobar"])
The log on the error on submitting the signup form:
Started POST "/organizations"
Processing by OrganizationsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"0cR***Nnx4iReMiePg==", "organization"=>{"name"=>"test21", "bag"=>"tes21", "users_attributes"=>{"0"=>{"email"=>"[email protected]", "username"=>"test21", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "usertype"=>"2"}}}, "commit"=>"Register"}
(0.2ms) BEGIN
User Exists (1.1ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER('[email protected]') LIMIT 1
(0.7ms) SELECT "users"."email" FROM "users" ORDER BY "users"."username" ASC
User Exists (0.3ms) SELECT 1 AS one FROM "users" WHERE LOWER(users"."username") = LOWER('test21') LIMIT 1
Organization Exists (0.6ms) SELECT 1 AS one FROM "organizations" WHERE LOWER("organizations"."name") = LOWER('test21') LIMIT 1
Organization Exists (0.4ms) SELECT 1 AS one FROM "organizations" WHERE LOWER("organizations"."bag") = LOWER('tes21') LIMIT 1
(0.2ms) ROLLBACK
Your validation does not work due to a Catch-22
ActiveRecord models get their ID from the database when they are saved. But the validation on the nested user runs before the organization is inserted into the the database.
You would guess that just checking
validates_presence_of
instead would pass:Unfortunatly not. In order for
validates_presence_of :organization
to pass the organization must be persisted to the database. Catch-22 again.In order for the validation to pass we would need to split creating the organization and user into two steps:
Unfortunatly the means that you cannot use
accepts_nested_attributes_for :users
- well at least not straight off the bat.By using a transaction we can insert the organization into the the database and and roll back if the user is not valid.
Followup questions
We use
@organization.persisted?
since we presumably want to redirect to the newly created organisation no matter if the there is a User record created.The transaction is not rolled back if there is no user created. Only if the user(s) fails to save due to invalid parameters. This is based on your requirement:
If you need the @organisation to be invalid without users you could do:
You would use
@organization.users.any?
to check if there are any users.@organization.users.persisted?
will not work since.persisted?
is a method on model instances - not collections.Right, since this will always issue two SQL insert statements it will not alter existing records.
It is up to you however to create validations that guarantee the uniqueness of the database columns (IE you don't want several records with the same user.email or organiation.name).
On the plus side is that none of these caveats apply when updating an existing organization:
Since you don't get the whole chicken or egg dilemma when validating the users.