Rails, custom validation and error not showing up - ruby-on-rails

I've currently five boolean attributes. I do a custom validation on them :
def limit_impacts
i = 0
choices = [self.first_attr, self.sec_attr, self.third_attr, self.fourth_attr, self.fifth_attr]
choices.each do |choice|
if choice == true
i+=1
end
end
if i > 2
errors[:base] << ("There must be one or two impacts.")
end
end
The idea is to test if more than two of them are set to true, if it's the case, set an error.
I'm setting a :base error because it's not related directly to only one attribute.
I'm simply doing this for my validation : validate :limit_impacts
and the part of the view handling this :
= f.input :first_attr, :as => :boolean
= f.input :sec_attr, :as => :boolean
= f.input :third_attr, :as => :boolean
= f.input :fouth_attr, :as => :boolean
= f.input :fifth_attr, :as => :boolean
The problem is, when I check more than 2 checkboxes the entrie is not saving and that's normal, but no error message is showing up in the view.
What am I doing wrong ?
By the way I tested it in rails console :
MyModel.errors[:base]
=> ["There must be one or two impacts."]
And this syntax doesn't work either :
errors.add :base, "message"
EDIT : Here's my controller. It's about the edit method.
def edit
#page_title = t('projects.edit.title')
#project = Project.find(params[:id])
#steps = #project.steps
#rewards = #project.rewards
#project_bearer = #project.user
end
Nothing linked with these attributes.
When I try to create a project via the rails console, it returns me false :
2.0.0p247 :001 > t = Project.create(:social_influence => true, :environmental_influence => true, :economical_influence => true)
=> <Project all my attributes ..>
2.0.0p247 :002 > t.save
(1.2ms) BEGIN
(2.0ms) ROLLBACK
=> false
SOLUTION :
The problem was my update method, bewteen render and redirect. Thanks to #delba I solved it.
If you want to see the solution, there's a discussion in the comments of his answer.

In the view containing the form, make sure you display the errors:
<%= form_for #my_model do |f|
<% if #my_model.errors.any? %>
<ul class="errors">
<% #my_model.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
<% end %>
<%# the rest of the form %>
<% end %>
In your controller:
def create
#my_model = MyModel.new(my_model_params)
if #my_model.save
redirect_to 'blabla'
else
render :new
end
end
In your model:
validate :limit_impacts
private
def limit_impacts
if [first_attr, sec_attr, third_attr, fourth_attr, fifth_attr].count(true) > 2
errors[:base] << "There must be one or two impacts."
end
end

Let's start with your validation method:
def limit_impacts
choices = [first_attr, sec_attr, third_attr, fourth_attr, fifth_attr]
errors[:base] << "There must be one or two impacts." if choices.count(true) > 2
end
Much cleaner, isn't it? :)
Could you show us your layout / your view bit which displays errors? I will update answer then.

Related

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 4 - saving images in database

Hello dear Programmers,
I'm trying to develop a web application with the ebook "Praxiswissen - Ruby on Rails". My problem is that I want to save Images through a form to my project directory. The database just saves the name of the pictures with the saving time:
def unique_and_proper_filename(filename)
Time.now.to_i.to_s + '_' + File.basename(filename)
end
My problem is that my pictures dont get saved after submitting my form. I dont get some exceptions, thats why I dont know where my issue is.
Controller:
class PostsController < ApplicationController
require 'will_paginate'
def new
#post = Post.new
end
# information about saving the picture
def create
#post = Post.new(params[:post].permit(:title, :description, :date, :image_file, :thumbnail_file))
# Form isn't correctly filled message
if !#post.valid?
flash.now[:notice] = "Bitte füllen Sie alle Felder aus und überprüfen Sie Ihre Angaben."
render(:action => :new)
# Files weren't saved message
elsif !#post.save_files
flash.now[:notice] = "Es trat ein Fehler beim Hochladen der Dateien auf."
render(:action => :new)
# Files saved correctly message
else
#post.save
flash[:notice] = "Dateien wurden hochgeladen und die Daten wurden gespeichert."
redirect_to(:action => :list)
end
end
# list action for listing my pictures
def list
#posts = Post.paginate(:page => params[:page], :order => "date DESC", :per_page => 15)
#post_pages = Post.paginate(:page => params[:page], :order => "date DESC", :per_page => 15)
end
end
HTML Form:
<h2>Neues Foto anlegen</h2>
<%= form_tag({:action => :create}, :multipart => true) %>
<h3>Bilddaten</h3>
<p>
Titel<br/>
<%= text_field(:post, :title) %>
</p>
<p>
Beschreibungen<br/>
<%= text_field(:post, :description) %>
</p>
<p>
Datum und Uhrzeit<br/>
<%= datetime_select(:post, :date, :order => [:day, :month, :year, :hour]) %>
</p>
<p>
<h3>Datei-Upload</h3>
<p>
Bilddatei:<br/>
<%= file_field(:post, :image_file) %>
</p>
<p>
Thumbnail:<br/>
<%= file_field(:post, :thumbnail_file) %>
</p>
<%= submit_tag("Speichern") %>
</p>
</form>
Model:
class Post < ActiveRecord::Base
validates_presence_of(:title, :description, :date, :image, :thumbnail)
I18n.enforce_available_locales = false
def image_file= (fileobj)
if fileobj.size > 0
#image_file = fileobj
self.image = unique_and_proper_filename(fileobj.original_filename)
end
end
def thumbnail_file= (fileobj)
if fileobj.size > 0
#thumbnail_file = fileobj
self.thumbnail = unique_and_proper_filename(fileobj.original_filename)
end
end
def save_files
# Bilddatei save
if !save_uploaded_file(#image_file, IMAGE_DIR, self.image)
return false
end
# Thumbnail save
if !save_uploaded_file(#thumbnail_file, THUMBNAIL_DIR, self.thumbnail)
return false
end
end
private
def unique_and_proper_filename(filename)
Time.now.to_i.to_s + "_" + File.basename(filename)
end
private
def save_uploaded_file(fileobj, filepath, filename)
# Complete Path
complete_path = Rails.root + "/public/" + filepath
# if neccessary, create directory
FileUtils.mkdir_p(complete_path) unless File.exists?(complete_path)
# save data
begin
f = File.open(complete_path + "/" + filename, "wb")
f.write(fileobj.read)
rescue
return false
ensure
f.close unless f.nil?
end
end
end
I'm only getting the message that there went something wrong with saving the files when i fill the form correctly but it should return a message that says that my file were saved.
I'm sorry for that massive length of my question but I really dont know where my issue is... If there's a need for more information or code, I will add it as fast as I can.
Thank you very much in advance!
Update 17/02/22:
Paperclip has since been deprecated, it is recommended you use Rails' own Active Storage.
Original Answer:
I'm sorry but I'll only be able to recommend what we use:
Paperclip
I appreciate you're using a tutorial, but I'd highly recommend using the Paperclip gem for this
This handles ALL the heavy lifting for you:
#GemFile
gem "paperclip", "~> 4.1.1"
Model
#app/models/post.rb
Class Post < ActiveRecord::Base
has_attached_file :image
end
#migration
add_attachment :posts, :image
Controller
#app/controllers/posts_controller.rb
def new
#post = Post.new
end
def create
#post = Post.new(post_params)
end
private
def post_params
params.require(:post).permit(:image, :other, :params)
end
View
#app/views/posts/new.html.erb
<%= form_for #post do |f| %>
<%= f.file_field :image %>
<% end %>
I'm lucky to tell that I found my issue. My save_files method in my Post model doesn't returned true..
I post this answer because maybe someone could use this question as an answer for his own problem. Here's where I added my return true :
def save_files
# Bilddatei save
if !save_uploaded_file(#image_file, IMAGE_DIR, self.image)
return false
end
# Thumbnail save
if !save_uploaded_file(#thumbnail_file, THUMBNAIL_DIR, self.thumbnail)
return false
end
return true # <--------- Need to be added!
end
Try to add enctype= multipart/formdata in your form tag if you are using form to post your data

Rails - how to change <select> to be just a hidden field when called with an id?

I have a form with a <select> element for the Group.
[The application stores bookmark links and groups for them]
When doing a 'new', everything work correctly. The new form works, the <select> dropdown has the list of groups and has their ID's.
The problem now is that I want to call the 'new link' from a different place that will already know the group. In this case I don't want an option to select the group, I just want to use the group id passed to it.
The form itself has:
= f.select :group_id, #groups
My routes include:
resources :groups do
resources :links # Added so that I can do group/:id/link/new ...
collection do
post 'order_links'
end
end
match 'search' => 'links#index'
match 'advanced_search' => 'links#advanced_search'
resources :links do
collection do
get 'groups'
end
end
The controller that's showing the form for this "new link" has:
def new
#link = Link.new
#groups = Group.all.collect { |g| [g.group_name, g.id] }
#group_name =
if params[:group_id]
'for the '+Group.find(params[:group_id]).group_name + ' group.'
else
''
end
respond_to do |format|
format.html
end
end
I tried changing the view to have this:
-if params[:group_id]
= f.hidden_field :group_id, :value => params[:group_id]
-else
= f.select :group_id, #groups
but it didn't work, I still got the <select> element, defaulting to its first <option>.
I actually had this working as some point in the past but has broken since, so hopefully I'm fairly close.
The indentation in your HAML template is wrong. It should be:
- if params[:group_id]
= f.hidden_field :group_id, :value => params[:group_id]
- else
= f.select :group_id, #groups
(If that was just the way it came out when you entered the question, I will delete this answer.)

Ruby on Rails, find if a certain value exists in a column

I'm building a website with user authentication. And I just noticed that if I create a user with an existing email, it just doesn't work which is normal, but I'd like to give feedback to the user. So for that I need to determine if any user has already that email.
I've tried some things like:
if User.email.include? params[:user][:email]
flash.now[:error] = "A user with this password already exists"
render :action => :new, :layout => 'signin-layout.html.erb'
Those are the columns for User:
2.1.0 :014 > User.column_names
=> ["id", "name", "email", "created_at", "updated_at", "password_digest", "remember_token", "admin", "team_id", "teamLeader"]
And the result I get is a big fat error:
undefined method `email' for #<Class:0x00000102b9a908>
So if anybody sees what I'm doing wrong, or knows another way to do it, that would be great.
Cheers
Try this:
if User.exists?(:email => params[:user][:email])
flash.now[:error] = "A user with this password already exists"
render :action => :new, :layout => 'signin-layout.html.erb'
...
else
# more code here..
end
Also, you can add validations when you're creating the object:
class User
validates_uniqueness_of :email
More on different validations here: http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html
I believe this way of doing the validation is wrong, you should validate the uniqueness of the email in the User model itself like below
validates :email, uniqueness: true #User model
This way the validation would be on the the User model. The problem with the condition you are using is that it is accessing an instance method specific to objects as a class method. So User.email means that there is a method called email that has the same logic for all the instances of the user class or more formally a class method which you don't have here. The email is an attribute specific to each user an instance attribute/variable (Each user has a different email).
You can see/show the validation errors present on the model using #user.errors.full_messages where #user is the instance you are trying to register/save.
This is how I would normally do it if this action is for registering users i.e. creating new users.
class User < ActiveRecord::Base
#attribute accessors and accessible
validates :email, uniqueness: true
end
class UsersController < ApplicationController
def create
#user = User.new params[:user]
if #user.save
#code for redirect or rendering the page you want
else
render 'new'
end
end
end
#new.html.erb
<%= form_for #user do |f| %>
<% if #user.errors.any? %>
<div>
<ul>
<% #job.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
#form fields
<% end %>
This way you display all the error messages to the user at the top of the registration form.

Run All Validations Except Any of "kind" :presence

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.

Resources