two forms one model in Ruby, separate validations for each - ruby-on-rails

I have three fields in one form and two fields in another (same as the earlier form, but just missing one field). I want to validate only two fields in the smaller form, but the issue is that it is validating all the three.
I have written the following logic:
**
class User < ActiveRecord::Base
validate :validate_form #for form with 2 fields
private
def validate_form
if :classify_create
self.errors.add(:weight, "need weight") if weight.blank?
self.errors.add(:height, "need height") if height.blank?
end
end
# Validations of attributes (for form with three fields)
validates :weight, :presence => true
validates :height, :presence => true
validates :gender, :presence => true
end
**
and this is my controller action: basically I have written two separate creates:
**# for form with two fields
def classify
#user = User.new
#title = "Classify"
end
def classify_create
#user = User.where("weight = ? and height = ?", params[:weight] ,params[:height])
end
# for form with three fields
def new
#user = User.new
#title = "Train"
end
def create
#user = User.new(params[:user])
if #user.save
redirect_to #user
else
#title = "Train"
render 'new'
end
end**
When I submit to the two field form, it gives me an error for gender too and redirects to the page with form having three fields. How should I go about it?
Any help will be appreciated.
Regards,
Arun

First, I would not use classify as a method name. You may conflict with a core inflector provided by ActiveSupport. Call it classification or something.
Second, your validation is running on if #user.save in the create method.
In classify_create you use User.where(...) which is a finder method. You're pulling a matching record and setting it to #user. This does not run validation, yet you receive validation errors. You are posting to create, not classify_create. Bad routes will cause this.
Let's address conditional validation first. In your User model, create a variable to act as a bypass switch for your gender validation. Then tell your validation to check if this bypass switch is false before running:
User < ActiveRecord::Base
attr_accessor :skip_gender # defaults to nil (false)
# ...
validates :gender, :presence => true, :if => :validate_gender? # validate if...
# ...
private
def validate_gender?
!self.skip_gender # true = run validation, false = skip validation
end
# ...
end
Next, clean up your controller. Write two create methods, one setting the switch, one not. (This isn't DRY):
def new_classification
# for form with two fields
#user = User.new
#title = "Classify"
end
def new
# for form with three fields
#user = User.new
#title = "Train"
end
def create
#user = User.new(params[:user])
if #user.save
redirect_to #user
else
render :action => 'new' # render three-field form
end
end
def create_classification
#user = User.where(:weight => params[:weight], :height => params[:height])
# ... do something with #user ...
#user.skip_gender = true # tell #user to skip gender validation
if #user.save
redirect_to #user
else
render :action => 'new_classification' # render two-field form
end
end
Next, adjust config/routes.rb to specify routes to your custom methods.
resources :users do
member do
get 'new_classification', :to => 'users#new_classification', \
:as => :new_classification_for
post 'create_classification', :to => 'users#create_classification', \
:as => :create_classification_for
end
end
Now change your two-field form view. Specify where your form is submitted to.
<%= form_for #user, :url => create_classification_for_user_url(#user) do |f| %>
That should get you by with what you have...

Your problem is two-fold:
You're trying to use one controller for two distinct actions.
The Rails validation model is somewhat limited and inflexible, there should be separate validation passes for controller methods and models.
The easy solution is to kludge around the limitations with a separate controller:
def create_genderless
# Force the issue to an explicit "unknown" state so that
# "unknown" and "missing" can be differentiated.
params[:user][:gender] = 'U'
# And then punt to the existing `create` method.
create
end
Then a bit more validation in your model for good measure
class User < ActiveRecord::Base
validates :gender, :inclusion => { :in => %w[M F U] }
#...
end
Then update your forms to use UserController#create or UserController#create_genderless as appropriate.

Related

Check whether a record exists in controller

While registration, I want to check whether the given email by a new user already exists or not in my controller.
class LoginsController < ApplicationController
skip_before_action :verify_authenticity_token
def index
#subscriber = Subscriber.new()
end
def sign_up
subscriberNew = Subscriber.new
subscriberNew.name = params[:name]
subscriberNew.cus_user_name = params[:user_name]
subscriberNew.cus_password = params[:password]
subscriberNew.cus_email = params[:email]
subscriberNew.mobile_no = params[:phone]
#if Email exists sends and error message
#...................
#if email does not exist, save the response to database
result = subscriberNew.save
respond_to do |format|
msg = {:status => "ok", :message => "Success!"}
format.json {render :json => msg}
end
end
end
How can I do this?
There are multiple ways to validate unique records, one of the better approaches may be altering your database to set a unique index for the email:
add_index :users, :username, unique: true (in your migration)
The DB index approach is better in long terms performance (see this for example)
You can also validate it in your controller before_action:
before_action :validate_email, only: [:sign_up]
...
private
def validate_email
# Or whatever way of sending a message you prefer
flash[:notice] = "A user with this email already exists"
redirect_to root_path if User.where(email: params[:email]).exists?
end
I'd recommend further reading about Active Record validations in the Rails Guides.
Add a validation for the email with uniqueness: true https://guides.rubyonrails.org/active_record_validations.html#uniqueness
You can do something like:
class Subscriber < ApplicationRecord
validates :email, uniqueness: true
end
and on the action:
subscriberNew.valid?
if subscriberNew.errors[:email].present?
#show_error
else
#success
end
I'd really recommend you to read about rails naming conventions, validations using activerecord and also conventions when creating a form (with form_for helper) and Strong Parameters https://guides.rubyonrails.org/action_controller_overview.html#strong-parameters.

How do I display a validation error properly if my date format is not correct in Rails?

I’m using Rails 4.2.7. I would like to throw a validation error if a user doesn’t enter their date of birth field in the proper format, so I have
def update
#user = current_user
begin
#user.dob = Date.strptime(params[:user][:dob], '%m/%d/%Y')
rescue ArgumentError => ex
end
if #user.update_attributes(user_params)
and I have this in my view
<%= f.text_field :dob, :value => (f.object.dob.strftime('%m/%d/%Y') if f.object.dob), :size => "20", :class => 'textField', placeholder: 'MM/DD/YYYY' %>
<% if #user.errors[:dob] %><%= #user.errors[:dob] %><% end %>
However, even if someone enters a date like “01-01/1985”, the above doesn’t return a validation error to the view. What do I need to do to get the validation error to be returned properly?
Edit: Per one of the answers given, I tried
#user = current_user
begin
#user.dob = Date.strptime(params[:user][:dob], '%m/%d/%Y')
rescue ArgumentError => ex
puts "Setting error."
#user.errors.add(:dob, 'The birth date is not in the right format.')
end
if #user.update_attributes(user_params)
last_page_visited = session[:last_page_visited]
if !last_page_visited.nil?
session.delete(:last_page_visited)
else
flash[:success] = "Profile updated"
end
redirect_to !last_page_visited.nil? ? last_page_visited : url_for(:controller => 'races', :action => 'index') and return
else
render 'edit'
end
And even though I can see the "rescue" branch called, I'm not directed to my "render 'edit'" block.
Triggering an exception doesn't add anything to the errors list. If you just want to tweak this code slightly, you should be able to call errors.add inside the rescue block. Something like #user.errors.add(:dob, 'some message here').
Keep in mind that this will only validate the date of birth when using this controller method. If you want to validate the date of birth whenever the user is saved, you'll want to explicitly add the validation to the model. You can write your own custom validation class or method, and there are also some gems that add date validation.
Calling update_attributes clears out the errors that you set in the rescue. You should check for errors, and if none, then continue on, something like this:
#user = current_user
begin
#user.dob = Date.strptime(params[:user][:dob], '%m/%d/%Y')
rescue ArgumentError => ex
puts "Setting error."
#user.errors.add(:dob, 'The birth date is not in the right format.')
end
if !#user.errors.any? && #user.update_attributes(user_params)
last_page_visited = session[:last_page_visited]
if !last_page_visited.nil?
session.delete(:last_page_visited)
else
flash[:success] = "Profile updated"
end
redirect_to !last_page_visited.nil? ? last_page_visited : url_for(:controller => 'races', :action => 'index') and return
end
render 'edit'
Since you redirect_to ... and return you can close out the conditional and, if you make it this far, simply render the edit page.
You may also want to add a simple validation to your user model:
validates :dob, presence: true
This will always fail if the dob can't be set for some other, unforseen, reason.
To get the user entered string to populate the field on re-load, you could add an accessor to the user model for :dob_string
attr_accessor :dob_string
def dob_string
dob.to_s
#dob_string || dob.strftime('%m/%d/%Y')
end
def dob_string=(dob_s)
#dob_string = dob_s
date = Date.strptime(dob_s, '%m/%d/%Y')
self.dob = date
rescue ArgumentError
puts "DOB format error"
errors.add(:dob, 'The birth date is not in the correct format')
end
Then change the form to set the :dob_string
<%= form_for #user do |f| %>
<%= f.text_field :dob_string, :value => f.object.dob_string , :size => "20", :class => 'textField', placeholder: 'MM/DD/YYYY' %>
<% if #user.errors[:dob] %><%= #user.errors[:dob] %><% end %>
<%= f.submit %>
<% end %>
And update the controller to set the dob_string:
def update
#user = User.first
begin
##user.dob = Date.strptime(params[:user][:dob], '%m/%d/%Y')
#user.dob_string = user_params[:dob_string]
end
if ! #user.errors.any? && #user.update_attributes(user_params)
redirect_to url_for(:controller => 'users', :action => 'show') and return
end
render 'edit'
end
def user_params
params.require(:user).permit(:name, :dob_string)
end
I would add a validation rule in the model. Like:
validates_format_of :my_date, with: /\A\d{2}\/\d{2}\/\d{4}\z/, message: 'Invalid format'
Try adding validation rule in model.
validate :validate_date
def validate_date
begin
self.dob = Date.parse(self.dob)
rescue
errors.add(:dob, 'Date does not exists. Please insert valid date')
end
end
and in your controller update your code
...
#user.update_attributes(user_params)
if #user.save
....
I think this is a case where Active Model shines. I like to use it to implement form objects without extra dependencies. I don't know the exact details of your situation but below I pasted a small demo that you should be able to adapt to your case.
The biggest benefit is that you don't pollute your controllers or models with methods to support profile updates. They can be extracted into a separate model which simplifies things.
Step 1: Store dob in users
Your users table should have a column dob of type date. For example:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name, null: false
t.date :dob, null: false
end
end
end
Don't put anything fancy in your model:
class User < ActiveRecord::Base
end
Step 2: Add Profile
Put the following in app/models/profile.rb. See comments for explanations.:
class Profile
# This is an ActiveModel model.
include ActiveModel::Model
# Define accessors for fields you want to use in your HTML form.
attr_accessor :dob_string
# Use the validatiors API to define the validators you want.
validates :dob_string, presence: true
validate :dob_format
# We store the format in a constant to keep the code DRY.
DOB_FORMAT = '%m/%d/%Y'
# We store the user this form pertains to and initialize the DOB string
# to the one based on the DOB of the user.
def initialize(user)
# We *require* the user to be persisted to the database.
fail unless user.persisted?
#user = user
#dob_string = user.dob.strftime(DOB_FORMAT)
end
# This method triggers validations and updates the user if validations are
# good.
def update(params)
# First, update the model fields based on the params.
#dob_string = params[:dob_string]
# Second, trigger validations and quit if they fail.
return nil if invalid?
# Third, update the model if validations are good.
#user.update!(dob: dob)
end
# #id and #persisted? are required to make form_for submit the form to
# #update instead of #create.
def id
#user.id
end
def persisted?
true
end
private
# Parse dob_string and store the result in #dob.
def dob
#dob ||= Date.strptime(dob_string, DOB_FORMAT)
end
# This is our custom validator that calls the method above to parse dob_string
# provided via the params to #update.
def dob_format
dob
rescue ArgumentError
errors[:dob] << "is not a valid date of the form mm/dd/yyyy"
end
end
Step 3: Use the form in the controller
Use Profile in ProfilesController:
class ProfilesController < ApplicationController
def edit
# Ensure #profile is set.
profile
end
def update
# Update the profile with data sent via params[:profile].
unless profile.update(params[:profile])
# If the update isn't successful display the edit form again.
render 'edit'
return
end
# If the update is successful redirect anywhere you want (I chose the
# profile form for demonstration purposes).
redirect_to edit_profile_path(profile)
end
private
def profile
#profile ||= Profile.new(user)
end
def user
#user ||= User.find(params[:id])
end
end
Step 4: Render the form with form_for
In app/views/profiles/edit.html.erb use form_for to display the form:
<%= form_for(#form) do |f| %>
<%= f.label :dob_string, 'Date of birth:' %>
<%= f.text_field :dob_string %>
<%= f.submit 'Update' %>
<% end %>
Step 5: Add routing
Keep in mind to add routing to config/routes.rb:
Rails.application.routes.draw do
resources :profiles
end
That's it!

rails autocomplete how to use exising record

I'm new to rails , and I have a problem with the nested forms and all of that.
I have a User model, and an Organization model.
When I want to create a user, I want to specify from which organization does he comes from.
Either the organization name is already in the database or if it's not, I want to create a new record and associate that record the User model.
I have hard time understanding all the relations (many-to-many etc) implications in the rails framework, but so far I've got this.
model/organization.rb
class Organization < ActiveRecord::Base
has_many :user
validates_presence_of :name
end
model/user.rb (short)
class User < ActiveRecord::Base
belongs_to :organization
accepts_nested_attributes_for :organization
#####
end
From this, in the console, I can create user and specify and organization name , and it will create a new record for the user and a new record for the organization.
The problem is that it creates a new organization each time.
I want to be able to associate an already existing organization to a new user.
I can get the list of organization with things like typeahead.js for the form, so the name will be the same when the user selects one. But I don't know how to relate the two (the newly created user and already existing organization).
I thought of putting a hidden field with the id of the organization, and check in the controller if this id exists. If it does, put this id, if it doesn't create a new one. But I don't even know how to do this. In the console, when I update the attributes of a user, for example , with an organization_id = 3 which exists :
u.update_attributes( :organization_attributes => { id: 3 } )
It rejects saying he didn't find a user with ID=... with Organization.id = 3 ...
I don't understand.
I suppose since this is a common case, that this should be easy , but it's messing with my head.
If someone is willing to explain to me, I'd be very grateful.
Thank you.
EDIT
i've just tried something in my controller but that doesn't work either.
def create
#user = User.new(user_params) # :user object built from user inputform
org = Organization.find_by(name:user_params[:organization_attributes][:name])
if org
#user.organization.id = org.id
end
if #user.save
# signin the user (token etc)
sign_in #user
flash[:success] = "Registration sucessfull !"
redirect_to #user
else
render 'new'
end
end
+user_controller (strong params)
def user_params
params.require(:user).permit(:lname,:email,:fname,:password,:password_confirmation,
:gender,:role,:display_private,:link_li,:country,:city,:phone,:hobbies,
:avatar,:org_name, :organization_attributes => [ :id, :name])
end
+form.html.erb
<%= u.fields_for :organization do |o| %>
<%= o.label "Organization" %>
<!-- PUT ORGA -->
<%= o.text_field :name, class:"form-control" %>
<% end %>
I would write a custom method for this:
#in User
def organization_name
(org = self.organization) && org.name
end
def organization_name=(name)
if org = Organization.find_by_name(name)
self.organization = org
else
self.organization = Organization.create(:name => name)
end
end
Now, because you've got a getter and setter method (ie two methods with the same name, apart from the = sign), you can treat organization_name like an attribute of User and put it in a form field like
f.input :organization_name
The input will get the current value from #user.organization_name and will call #user.organization_name= with the new value.
First take away the accepts_nested_attributes from the model.
Then in your controller you should do something like:
def create
#user = User.new(user_params) # :user object built from user inputform
org = Organization.where(name: user_params[:organization_attributes][:name]).first || Organization.create(name: user_params[:organization_attributes][:name])
#user.organization = org
if #user.save
# signin the user (token etc)
sign_in #user
flash[:success] = "Registration sucessfull !"
redirect_to #user
else
render 'new'
end
end
In your app/model/user.rb
def self.create(name, attribute1, ... ,organization)
user = User.new(:name => name, :atr_1 => attribute_1, ....:atr_n => attribute_n)
user.organization = organization
raise "user not created" if !user.save
user
end
In users_controller.rb
def create
org = Organization.find params['organization'] #expecting the param to be Organization#id
user = User.create(params['name'], ..., org)
render :json => {:message => "user created"}
end

Passing params to a new template on a failed create action when using accepts_nested_attributes_for

I may just be missing something simple, but I am relatively inexperienced so it is likely. I've searched extensively for a solution without success.
I am using the fields_for function to build a nested form using the accepts_nested_attributes_for function. If the submit on the form fails the params are passed to the render of the new template only for the parent model. How do I pass the nested params for the child model so that fields that have been filled out previously remain filled. Note that I am using simple_form and HAML but I assume this shouldn't impact the solution greatly.
My models:
class Account < ActiveRecord::Base
attr_accessible :name
has_many :users, :dependent => :destroy
accepts_nested_attributes_for :users, :reject_if => proc { |a| a[:email].blank? }, :allow_destroy => true
end
class User < ActiveRecord::Base
attr_accessible :email, :password, :password_confirmation
belongs_to :account
end
My accounts controller:
def new
#account = Account.new
#account.users.build
end
def create
#account = Account.new(params[:account])
if #account.save
flash[:success] = "Welcome."
redirect_to #account
else
#account.users.build
<- I suspect I need something here but unsure what
render :new
end
end
The key part of the accounts/new view:
= simple_form_for #account do |f|
= f.input :name
= f.simple_fields_for :users do |u|
= u.input :email
= u.input :password
= u.input :password_confirmation
= f.button :submit, :value => "Sign up"
My params on a failed save are:
:account {"name"=>"In", "users_attributes"=>{"0"=>{"email"=>"u#e.com", "password"=>"pass", "password_confirmation"=>"pass"}}}
As you can see, the key information, in the users_attributes section, is stored but I can't seem to have the email address default into the new form. Account name on the other hand is filled automatically as per Rails standard. I'm not sure if the solution should live in the accounts controller or in the accounts/new view, and have not had any luck with either.
Answers with .erb are, of course, fine.
I'm fairly new to Ruby and Rails so any assistance would be much appreciated.
The problem lies with attr_accessible, which designates the only attributes allowed for mass assignment.
I feel a bit silly in that I actually stated the problem in a comment last night and failed to notice:
accepts_nested_attributes_for :users will add a users_attributes= writer to the account to update the account's users.
This is true, but with attr_accessible :name, you've precluded every attribute but name being mass-assigned, users_attributes= included. So when you build a new account via Account.new(params[:account]), the users_attributes passed along in params are thrown away.
If you check the log you might note this warning:
WARNING: Can't mass-assign protected attributes: users_attributes
You can solve your original problem by adding :users_attributes to the attr_accessible call in the account class, allowing it to be mass-assigned.
Amazingly, after reading a blog post this evening, and some more trial and error, I worked this out myself.
You need to assign an #user variable in the 'new' action so that the user params are available for use in the 'create' action. You then need to use both the #account and #user variables in the view.
The changes look like this.
Accounts Controller:
def new
#account = Account.new
#user = #account.users.build
end
def create
#account = Account.new(params[:account])
#user = #account.users.build(params[:account][:user]
if #account.save
flash[:success] = "Welcome."
redirect_to #account
else
render :new
end
end
The accounts/new view changes to:
= simple_form_for #account do |f|
= f.input :name
= f.simple_fields_for [#account, #user] do |u|
= u.input :email
= u.input :password
= u.input :password_confirmation
= f.button :submit, :value => "Sign up"
In this case the params remain nested but have the user component explicitly defined:
:account {"name"=>"In", "user"=>{"email"=>"user#example.com", "password"=>"pass", "password_confirmation"=>"pass"}}
It has the additional side effect of removing the #account.users.build from within the else path as #numbers1311407 suggested
I am not certain whether their are other implications of this solution, I will need to work through it in the next few days, but for now I get the information I want defaulted into the view in the case of a failed create action.
#Beerlington and #numbers1311407 I appreciate the help in guiding me to the solution.

How can I get form_for to autopopulate fields based upon a non-model hash?

I'm building a multi-step form in rails. It's not javascript driven, so each page has its own controller action like "step1" "step2" etc. I know how to do multi-step wizards through JQuery but I don't know how to keep rails validations per page without getting into javascript, hence this way.
Anyways, my model is a User object but I'm storing all my variables in an arbitrary Newuser variable and using the following in the view:
<% form_for :newuser, :url => { :action => "step3" } do |u| %>
In the controller, I merge the current page's info with the overall hash using:
session[:newuser].merge!(params[:newuser])
This works great except that if the user clicks back to a previous page, the fields are no longer populated. How do I keep them populated? Do I need to change the object in the form_for to somehow refer to the session[:newuser] hash?
EDIT:
I guess I'm looking for more info on how form_for autopopulates fields within the form. If it's not built around a model but an arbitrary hash (in this case, session[:newuser]), how do I get it to autopopulate?
This is how we did a multi-step form with validations
class User < ActiveRecord::Base
attr_writer :setup_step
with options :if => :is_step_one? do |o|
o.validates_presence_of :name
end
with options :if => :is_step_two? do |o|
o.validates_presence_of :email
end
def setup_step
#setup_step || 1
end
def is_step_one?
setup_step == 1
end
def is_step_two?
setup_step == 2
end
def last_step?
is_step_two? #change this to what your last step is
end
end
Then in the controller:
UsersController
SETUP_STEPS{1 => 'new', 2 => 'step_two'}
def new
#user = User.new
end
def step_two
end
def create
#user = User.new(params[:user])
if !#user.valid?
render :action => SETUP_STEPS[#user.setup_step]
elsif #user.last_step?
#user.save
#do stuff
else
render :action => SETUP_STEPS[#user.setup_step]
end
end
end
And then in your forms, they are like like any other rails form with one exception, you will need a hidden field to hold the values from your previous steps.
- form_for #user, :url => users_path do |f|
- [:login, :password].each do field
= f.hidden_field field
What about still using a class for your population?
class User
attr_accessor :credit_card, :name, :likes_fried_chicken
def initialize(options = {})
options.each do |key, value|
self.send("#{key}=", value)
end
end
end
you could use some tableless model functions here if you wanted to include some validations.
Then in your controller steps:
def step_one
#user = User.new(session[:new_user])
end
your forms should continue to work.
Another option is just to set the value of the form objects directly from your session hash
- form_for :user, :url => step_2_path do |f|
= f.text_field :name, :value => session[:new_user][:name]

Resources