Is there a way to run only validations of a specific type?
I have an application that updates multiple class instances in a single form. Validations are performed by creating an instance of building and validating on that.
The problem: if an attribute isn't being updated, the form field is left blank and the form submits an empty string. You can see an example here where params[:building][:name] is an empty string.
params = {:building=>{:name=>"", :short_name=>"", :code=>"test"}, :commit=>"Update Buildings", :building_ids=>["2", "5", "7"], :action=>"update_multiple", :controller=>"buildings"}
How can I run all the validations excluding those that check for the presence of an attribute?
def update_multiple
#building = Building.new(params[:building].reject {|k,v| v.blank?})
respond_to do |format|
if #building.valid?
Building.update_all( params[:building].reject {|k,v| v.blank?}, {:id => params[:building_ids]} )
format.html { redirect_to buildings_path, notice: 'Buildings successfully updated.' }
else
#buildings = Building.all
format.html { render action: 'edit_multiple' }
end
end
end
I've spent quite a bit of time working on this, and here's what I've found so far:
To retrieve a models validations
$ Building.validators
=> [#<ActiveModel::Validations::PresenceValidator:0x007fbdf4d6f0b0 #attributes=[:name], #options={}>]
To get a validators kind
$ Building.validators[0].kind
=> :presence
This is the method used by rails to run validations:
https://github.com/rails/rails/blob/master/activesupport/lib/active_support/callbacks.rb
line 353
# This method runs callback chain for the given kind.
# If this called first time it creates a new callback method for the kind.
# This generated method plays caching role.
#
def __run_callbacks(kind, object, &blk) #:nodoc:
name = __callback_runner_name(kind)
unless object.respond_to?(name, true)
str = object.send("_#{kind}_callbacks").compile
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{name}() #{str} end
protected :#{name}
RUBY_EVAL
end
object.send(name, &blk)
end
If there is a way to run validations directly? If so, I could iterate over the Building.validators and only run those with kind != :presence.
I'd love to hear any ideas you have.
I think circumventing specific validations is an interesting idea, but I think there's an easier way. I'd handle this by writing a custom method that validates batch updates, something like this:
def valid_for_batch?(params)
#building = Building.new(params[:building].reject {|k,v| v.blank?})
#building.name = "Foo" if #building.name.blank?
#building.shortname = "Bar" if #building.shortname.blank?
# etc...
#building.valid?
end
Just make sure that "Foo" and "Bar" up there are values that will pass your validation - all that code is doing is looking to see if the values are empty and if so, replacing them with a temporary value that will pass validation. That way, the only way that #building.valid? will return false at the end is if there were existing bad data.
Wow, after working on this issue for many hours, it looks to be a very difficult problem.
Create a class instance #building and set placeholders for attributes whose presence is validated.
I've tried many different approaches and this is the best I can come up with so far.
def update_multiple
valid = true
#building = Building.new(params[:building])
set_bulk_edit_placeholders_for_presence_validators(#building)
building_valid = #building.valid?
# Check if buildings were selected to edit
if params[:building_ids].blank?
valid = false
#building.errors.add(:base, 'You must select at least one Building')
end
# Check if all form fields are blank
if params[:building].values.delete_if {|v| v.blank?}.blank?
valid = false
#building.errors.add(:base, 'The edit form must not be empty')
end
respond_to do |format|
if valid && building_valid
#buildings = Building.find(params[:building_ids])
#buildings.each do |building|
building.update_attributes!(params[:building].reject { |k,v| v.blank? })
end
format.html { redirect_to buildings_path, notice: 'Buildings were successfully updated.' }
else
#buildings = Building.all
format.html { render edit_multiple_buildings_path }
end
end
end
This is a general function to set the placeholders. It can be used for any model from any controller.
application_controller.rb
private
def set_bulk_edit_placeholders_for_presence_validators(class_instance, hash={})
model_name = hash[:model_name] || self.class.name.sub("Controller", "").singularize.downcase
klass = self.class.name.sub("Controller", "").singularize.constantize
klass.send(:validators).each { |validator|
if (validator.kind == :presence)
validator.attributes.each { |attribute|
if params[model_name][attribute].blank?
class_instance.send("#{attribute}=", 'placeholder')
end
}
end
}
end
In order to display the errors properly on the form it has to be a form_for #buildings.
Since the placeholders are set for the #buildings object, we have to specify that the form values come directly from params.
edit_multiple.haml
= bootstrap_form_for #building, :url => update_multiple_buildings_path, :method => :put, :html => {:class => 'form-horizontal'} do |f|
= f.text_field :name, :value => params[:building].try(:send, :[], :name)
= f.text_field :short_name, :value => params[:building].try(:send, :[], :short_name)
= f.text_field :code, :value => params[:building].try(:send, :[], :code)
= f.submit 'Update Buildings', :name => nil
%table.table.table-striped.table-bordered
%thead
%tr
%th.check_box_column= check_box_tag 'select_all'
%th.sortable= sortable :name
%th Abbr Name
%th.sortable= sortable :code
%th
%tbody
- #buildings.each do |building|
%tr
%td.check_box_column= check_box_tag 'building_ids[]', building.id, (params[:building_ids].include?(building.id.to_s) if params[:building_ids])
%td= building.name
%td= building.short_name
%td= building.code
%td.links.span2
= link_to 'Show', building
= link_to 'Edit', edit_building_path(building)
= link_to 'Delete', building, :method => :delete, :confirm => 'Are you sure you want to delete this building?'
I hope this can be of help to others working on similar "bulk update" validation issues.
Related
I am implementing a method to hide records from a table but not delete from my database via a button. I added a column "revision_active" boolean attribute to my table which is set to false. With a view condition records can be shown that are only set to true.
My button(to set attributes to true via checkboxes):
<%= form_tag update_multiple_homeworks_path(:revision_active => true), method: :put do %>
....
<td><%= check_box_tag "homework[id][]", homework.id, checked = false %></td>
....
<%= submit_tag "Add to Bucket", :remote => true, :class => 'smallMobileRight button text-left' %>
In my routes:
resources :homeworks do
collection do
put 'update_multiple'
end
end
In my controller:
def update_multiple
if params[:homework][:id]
#homework_ids = params[:homework][:id]
#homework_ids.each do |id|
Homework.revision_active = true
end
else
redirect_to homeworks_homework_details_path
end
respond_to do |format|
flash[:notice] = "Added to your Bucket!"
format.html { redirect_to homeworks_homework_details_path }
format.json { head :no_content }
format.js { render :layout => false }
end
end
Params
...
"_method"=>"put",
"authenticity_token"=>"blah",
"homework"=>{"id"=>["107"]},
"commit"=>"Add to Bucket",
"revision_active"=>"true",
"id"=>"update_multiple"}
Error upon button click:
Couldn't find Homework with 'id'=update_multiple
Any ideas? I'm not catching an array of ids, error in controller? Thanks.
I am not going to give the exact answer because this is a very specific question and doesn't seem to be very beneficial to other people. I figured it's better for you to read into documentation to learn how to do that. So I will point you to some resources instead.
Your route
Looks like you are passing in update_multiple as an id, and your route helper takes an id as an argument.
Since this is updating a collection, you shouldn't be doing operation on a specific record.
You can run rake routes to find out what parameter it takes after you construct the route. (http://guides.rubyonrails.org/routing.html)
Your checkbox
<%= check_box_tag "homework[id][]", homework.id, checked = false %>
should be as below.
<%= check_box_tag "homework_ids[]", homework.id, checked = false %>
Refer to here: http://apidock.com/rails/ActionView/Helpers/FormTagHelper/check_box_tag
In your controller
What you want might be this params[:homework_ids]
Refer to here: http://guides.rubyonrails.org/action_controller_overview.html#parameters
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!
I'm reworking a Rails 2 website. Right now, I'm getting an error, and I think it's because a value is being submitted as blank instead of .nil. However, my attempts to keep it nil don't seem to be working. I appreciate any help you have to offer.
From Model, based on Make blank params[] nil
NULL_ATTRS = %w( start_masterlocation_id )
before_save :nil_if_blank
protected
def nil_if_blank
NULL_ATTRS.each { |attr| self[attr] = nil if self[attr].blank? }
end
View
I've got jQuery that adds a value to this hidden field when a start_masterlocation_id exists. I've also got jQuery that removes the field attribute when it does not exist.
<%= hidden_field :newsavedmap, :start_masterlocation_id, :id => "start-masterlocation-id-field" %>
Finally, here is the part of the controller that is throwing the error. This is the controller for the page that holds the form (Maptry), not the controller for Newsavedmap. I think I have to delete the #newsavedmap.id, #newsavedmap.mapname, and #newsavedmap.optimize lines now that I'm going with form handlers, but I don't think that's related to this error.
Controller
def maptry
#itinerary = Itinerary.find(params[:id])
if #itinerary.user_id == current_user.id
respond_to do |format|
#masterlocation = Masterlocation.find(:all)
format.html do
end
format.xml { render :xml => #masterlocation }
format.js do
#masterlocation = Masterlocation.find(:all)
render :json => #masterlocation.to_json(:only => [:id, :nickname, :latitude, :longitude])
end
end
if params[:newsavedmap_id]
#newsavedmap = Newsavedmap.find(:first, :conditions => {:id => params[:newsavedmap_id]})
#waypoint = Waypoint.find(:all, :conditions => {:newsavedmap_id => params[:newsavedmap_id]})
#newsavedmap.id = params[:newsavedmap_id]
#newsavedmap.mapname = Newsavedmap.find(:first, :conditions => {:id => params[:newsavedmap_id]}).mapname
#newsavedmap.optimize = Newsavedmap.find(:first, :conditions => {:id => params[:newsavedmap_id]}).optimize
if !#newsavedmap.start_masterlocation_id.nil?
#start_name = Masterlocation.find(:first, :conditions => {:id => #newsavedmap.start_masterlocation_id}).name
end
if !#newsavedmap.end_masterlocation_id.nil?
#end_name = Masterlocation.find(:first, :conditions => {:id => #newsavedmap.end_masterlocation_id}).name
end
else
#newsavedmap = Newsavedmap.new
end
else
redirect_to '/'
end
end
Error
This does not occur when a start_masterlocation_id is present in the database.
undefined method `name' for nil:NilClass
I solved this by ignoring the nil problem. Instead, I used
!#newsavedmap.start_masterlocation_id.blank?
This evaluates whether the data in the record is blank and then either does or doesn't do something. Obviously, this results in unwanted "blank" data being left inside my database, so it's not ideal, but it helped me move forward with the project. I'd appreciate any responses that deal directly with the nil issue and tell me how to have Rails ignore blank data in a form.
I have a form for creating/editing an event. There is a Client drop down which renders some custom questions and answers dependent on the selected Client.
I'm running Rails 2.3.9 with Ruby 1.8.6 and Mongrel on Windows.
Here is the relevant code:
Event form
- form_for #event do |f|
....
= f.collection_select(:client_id, Client.all, :id, :name)
#custom_client_fields
= render 'client_fields' if #client
= observe_field :event_client_id, :url => {:action => 'client_fields'},
:with => "'client_id=' + encodeURIComponent(value)"
....
_client_fields.html.haml
- fields_for "event[client_attributes]", #client do |client|
- client.fields_for :questions do |question|
%label.question= question.object.content
- question.fields_for :answers, Answer.new do |answer|
= answer.text_field:content, :maxlength => 150, :size => 40
Event Controller
def client_fields
if params[:client_id].blank?
render_no_client_fields #self explanatory
else
respond_to do |format|
format.js {
render :update do |page|
page[:custom_client_fields].replace_html :partial => 'client_fields', :layout => false
end
}
end
end
end
Parameter hash
Parameters: {
"event"=>{
"id"=>"2",
"client_attributes"=>{
"questions_attributes"=>{
"0"=>{
"id"=>"4",
"answers_attributes"=>{
"0"=>{
"content"=>"fjhkghjkk"
}
}
}
}
}
}
}
Basically the form passes the validations and everything except the nested attributes. Nothing is inserted into the database table.
Looking at the parameter hash my client_attributes doesn't have an id... hmmm...
In the client_fields partial I had to add the following code to set the client_attributes_id:
<input id="event_client_attributes_id" name="event[client_attributes][id]" type="hidden" value="#{#client.id}">
Note to self: When you don't use Rails' magic form helpers you have to build the rest of the form as well.
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]