How do I validate start date and end date? - ruby-on-rails

What's the best way to validate that the end date is not before the start date, and start date is after the end date in Rails?
I have this in my view controller:
<tr>
<td><%= f.label text="Starts:" %></td>
<td><%= f.datetime_select :start_date, :order => [:day, :month, :year]%></td>
</tr>
<tr>
<td><%= f.label text="Ends:" %></td>
<td><%= f.datetime_select :end_date,:order => [:day, :month, :year]</td>
</tr>
I want it to come up with a popup of sorts, with a meaningful message.
I would like to make a generic method that takes two parameters, start and end date, which I then can call in my viewcontroller ; fx in the code above. Or, do I need to use jQuery instead?

#YaBoyQuy
client-side validation can work and avoids a hit to the server...
The question is also about end_date being after the start, so the validation - using gem date_validator - should also state
validates :end_date, presence: true, date: { after_or_equal_to: :start_date}
The suggestion of
on: :create
would be incorrect for the end_date validation; logically this should also be run on an edit.
I upvoted on the basis of the succinct syntax.

Clean and Clear (and under control?)
I find this to be the clearest to read:
In Your Model
validates_presence_of :start_date, :end_date
validate :end_date_is_after_start_date
#######
private
#######
def end_date_is_after_start_date
return if end_date.blank? || start_date.blank?
if end_date < start_date
errors.add(:end_date, "cannot be before the start date")
end
end

Avoid client side validation, because it only validate client side...
Use the rails validaters built in.
validates :start_date, presence: true, date: { after_or_equal_to: Proc.new { Date.today }, message: "must be at least #{(Date.today + 1).to_s}" }, on: :create
validates :end_date, presence: true

If you want client side validation, use jQuery.
Or in rails, to validate server side, you could create your own I guess?
def date_validation
if self[:end_date] < self[:start_date]
errors[:end_date] << "Error message"
return false
else
return true
end
end
Rails >= 7.0 makes this a one-liner
validates_comparison_of :end_date, greater_than_or_equal_to: :end_date
PR, Rails Guides

to use your validates :dt_end, :date => {:after_or_equal_to => :dt_start} you need to have a DateValidator such as this:
class DateValidator > ActiveModel::Validator
def validate(record)
the_end = record.dt_end
the_start = record.dt_start
if the_end.present?
if the_end < the_start
record.errors[:dt_end] << "The end date can't be before the start date. Pick a date after #{the_start}"
end
end
end
end

Related

Adding a way to view assignments due within the next seven days in my Rails app

I am building a basic student planner application using Ruby on Rails and I am trying to implement a way for a student to see which assignments are due within the next seven days. I would like to use a scope method, something like due_soon that can be called within a view.
Currently when I run my code I'm getting an ArgumentError in Assignments#index and it's telling me that the comparison of Date with :due_date failed
My Assignments model:
class Assignment < ActiveRecord::Base
belongs_to :course
belongs_to :student
# accepts_nested_attributes_for :course
validates :course_id, :student_id, :due_date, :title, presence: true
scope :due_soon, -> { where(Date.current + 7.days >= :due_date) }
def course_attributes=(attributes)
binding.pry
if attributes[:course_name].blank?
self.course = Course.find_by(id: params[:course_id])
else
self.course = Course.find_or_create_by(attributes)
self.course
end
end
end
In my view:
<h3>Due Soon</h3>
<% Assignment.due_soon.each do |assignment| %>
<% if assignment.course %>
<tr>
<td><%= assignment.title %></td>
<td><%= assignment.course.course_name %></td>
<td><%= assignment.due_date %></td>
<td><%= link_to "View/Edit Assignment", student_assignment_path(#student, assignment) %></td>
</tr>
<% end %>
<% end %>
Any pointers would be much appreciated as I try and continue to familiarize myself with rails! Thanks!
The issue you are having here is related to your usage of where:
Assignment.where(Date.current + 7.days >= :due_date)
Is not valid for ActiveRecord's where method.
What you need is to either use ActiveRecord's lib to generate SQL (1), or write the SQL yourself (2):
# (1) Use ActiveRecord combined with a Ruby Range
Assignment.where(due_date: Date.current..7.days.from_now)
# (2) Assignment.where('due_date between ? and ?', Date.current, 7.days.from_now)
You have a syntax error there.
One way is...
scope :due_soon, -> { where('due_date =< ?', Date.current + 7.days) }
https://apidock.com/rails/ActiveRecord/NamedScope/ClassMethods/scope

Ruby on Rails - show current date only

I'm creating a small lesson app, with a different lesson for each day. I'm wanting to show the current days lesson only on the index, and can't figure out how. I googled and found some info that came close, but still couldn't fix the problem. I only have one controller, DaysController. No user controller.
For my model(day.rb) I've tried this
class Day < ActiveRecord::Base
validates :description, :date, :link_to, :presence => true
scope :created_on, lambda {|date| {:conditions => ['created_at >= ? AND created_at <= ?', date.beginning_of_day, date.end_of_day]}}
def self.today
self.created_on(Date.today)
end
end
And for my index I've tried these
<% #day.created_on(Date.today) %>
<% #day.today %>
any advice??
If I understand correctly and for simplicity sake is this essentially what you are trying to achieve?
Controller:
def index
#days = Day.all
end
View (index.html.erb):
<% #days.each do |day| %>
<% if day.created_at == Date.today %>
<%= day.field_name %>
<% end %>
I would change the scope to just use the Date object and I assume that you want your condition to use the newer syntax:
scope :created_on, ->(date) { where(created_at: date) }
Then the Day.today method should work without any change.
You can do the following:
class Day < ActiveRecord::Base
scope :today, lambda { where('CAST(created_at AS date) = ?', Date.today) }
And use it like this:
#days = Day.today
# returns a list of `Day` records where the `created_at`'s date is equal to today's date

How to validate start date on update?

How to validate start date on update that it should not be previous date than previously saved date.
eg:- like I have created record with start date as 07/11/2013, on update it should not before the 07/11/2013.
in view:
f.input :start_date, as: :datepicker
f.input :end_date, as: :datepicker
model:
validates :start_date, allow_nil: true, date: { after: Date.today - 1, message: 'must be today or after today' }, on: :create
validates :end_date, allow_nil: true, date: { after: :start_date, message: 'must be after start date' }
Thanks.
I can't test it right now but I think it might work:
validate :previous_start_date
def previous_start_date
old_start_date = Model.find(self.id).start_date
if(old_start_date > self.start_date)
self.errors.add(:start_date, "Can't be previous than the initial date")
end
end
At the moment of the validation, the object hasn't been saved yet, so, I believe that retrieving the object from the database will give you the previous value. With the value in hand you can compare with the current start_date and then add your custom error.
I hope it helps.
You can add attr_accessor :previous_start_date(dont forget also about attr_accessible) plus add hidden field on form. This field must have value equal to start_date from DB.
Then you can use after :previous_start_date.
Note: value previous_start_date must be set from DB, maybe better to do it in model in getter method or set in before_validation callback.

Rails: Virtual Attribute Reader Causing Error During Validations

Rails 3.0.3 application. . .
I'm using a virtual attribute in a model to convert a value stored in the database for display based on a user's preference (U.S. or metric units). I'm doing the conversion in the reader method, but when I test my presence validation I get a NoMethodError because the real attribute is nil. Here's the code:
class Weight < ActiveRecord::Base
belongs_to :user
validates :converted_weight, :numericality => {:greater_than_or_equal_to => 0.1}
before_save :convert_weight
attr_accessor :converted_weight
def converted_weight(attr)
self.weight_entry = attr
end
def converted_weight
unless self.user.nil?
if self.user.miles?
return (self.weight_entry * 2.2).round(1)
else
return self.weight_entry
end
else
return nil
end
end
...
This is the line that's causing the problem:
return (self.weight_entry * 2.2).round(1)
I understand why self.weight_entry is nil, but what's the best way to handle this? Should I just throw in an unless self.weight_entry.nil? check in the reader? Or should I perform this conversion somewhere else? (if yes, where?)
Thanks!
Here's what I've done:
Model
validates :weight_entry, :numericality => {:greater_than_or_equal_to => 0.1}
before_save :convert_weight
attr_reader :converted_weight
def converted_weight
unless self.user.nil?
unless self.weight_entry.nil?
if self.user.miles?
return (self.weight_entry * 2.2).round(1)
else
return self.weight_entry
end
end
else
return nil
end
end
Form
<%= f.label :weight_entry, 'Weight' %><br />
<%= f.text_field :weight_entry, :size => 8, :value => #weight.converted_weight %> <strong><%= weight_units %></strong> (<em>Is this not right? Go to your <%= link_to 'profile', edit_user_registration_path %> to change it</em>)
The unless.self.weight_entry.nil? check allows the validation to do it's job. If anyone knows of a better way to do this I'm open to suggestion.
Thanks!
P.S. The before_save convert_weight method converts U.S. units to metric. I want to store values in the same units consistently so if a user changes her preference later previously stored values don't become invalid.

Rails validation error messages: Displaying only one error message per field

Rails displays all validation error messages associated with a given field. If I have three validates_XXXXX_of :email, and I leave the field blank, I get three messages in the error list.
Example:
validates_presence_of :name
validates_presence_of :email
validates_presence_of :text
validates_length_of :name, :in => 6..30
validates_length_of :email, :in => 4..40
validates_length_of :text, :in => 4..200
validates_format_of :email, :with => /^([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})$/i<br/>
<%= error_messages_for :comment %> gives me:
7 errors prohibited this comment from being saved
There were problems with the following fields:
Name can't be blank
Name is too short (minimum is 6 characters)
Email can't be blank
Email is too short (minimum is 4 characters)
Email is invalid
Text can't be blank
Text is too short (minimum is 4 characters)
It is better to display one messages at a time. Is there an easy way to fix this problem? It looks straightforward to have a condition like: If you found an error for :email, stop validating :email and skip to the other field.
[Update] Jan/2013 to Rails 3.2.x - update syntax; add spec
Inspired by new validation methods in Rails 3.0 I'm adding this tiny Validator. I call it ReduceValidator.
lib/reduce_validator.rb:
# show only one error message per field
#
class ReduceValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return until record.errors.messages.has_key?(attribute)
record.errors[attribute].slice!(-1) until record.errors[attribute].size <= 1
end
end
My Model looking like - notice the :reduce => true:
validates :title, :presence => true, :inclusion => { :in => %w[ Mr Mrs ] }, :reduce => true
validates :firstname, :presence => true, :length => { :within => 2..50 }, :format => { :without => /^\D{1}[.]/i }, :reduce => true
validates :lastname, :presence => true, :length => { :within => 2..50 }, :format => { :without => /^\D{1}[.]/i }, :reduce => true
Works like a charm in my current Rails Project.
The advantageous is, i've put the validator only on a few fields not all.
spec/lib/reduce_validator_spec.rb:
require 'spec_helper'
describe ReduceValidator do
let(:reduce_validator) { ReduceValidator.new({ :attributes => {} }) }
let(:item) { mock_model("Item") }
subject { item }
before(:each) do
item.errors.add(:name, "message one")
item.errors.add(:name, "message two")
end
it { should have(2).error_on(:name) }
it "should reduce error messages" do
reduce_validator.validate_each(item, :name, '')
should have(1).error_on(:name)
end
end
Imo simplier is:
<% #model.errors.each do |attr, msg| %>
<%= "#{attr} #{msg}" if #model.errors[attr].first == msg %>
<% end %>
Bert over at RailsForum wrote about this a little while back. He wrote the code below and I added some minor tweaks for it to run on Rails-3.0.0-beta2.
Add this to a file called app/helpers/errors_helper.rb and simply add helper "errors" to your controller.
module ErrorsHelper
# see: lib/action_view/helpers/active_model_helper.rb
def error_messages_for(*params)
options = params.extract_options!.symbolize_keys
objects = Array.wrap(options.delete(:object) || params).map do |object|
object = instance_variable_get("##{object}") unless object.respond_to?(:to_model)
object = convert_to_model(object)
if object.class.respond_to?(:model_name)
options[:object_name] ||= object.class.model_name.human.downcase
end
object
end
objects.compact!
count = objects.inject(0) {|sum, object| sum + object.errors.count }
unless count.zero?
html = {}
[:id, :class].each do |key|
if options.include?(key)
value = options[key]
html[key] = value unless value.blank?
else
html[key] = 'errorExplanation'
end
end
options[:object_name] ||= params.first
I18n.with_options :locale => options[:locale], :scope => [:errors, :template] do |locale|
header_message = if options.include?(:header_message)
options[:header_message]
else
locale.t :header, :count => count, :model => options[:object_name].to_s.gsub('_', ' ')
end
message = options.include?(:message) ? options[:message] : locale.t(:body)
error_messages = objects.sum do |object|
object.errors.on(:name)
full_flat_messages(object).map do |msg|
content_tag(:li, ERB::Util.html_escape(msg))
end
end.join.html_safe
contents = ''
contents << content_tag(options[:header_tag] || :h2, header_message) unless header_message.blank?
contents << content_tag(:p, message) unless message.blank?
contents << content_tag(:ul, error_messages)
content_tag(:div, contents.html_safe, html)
end
else
''
end
end
####################
#
# added to make the errors display in a single line per field
#
####################
def full_flat_messages(object)
full_messages = []
object.errors.each_key do |attr|
msg_part=msg=''
object.errors[attr].each do |message|
next unless message
if attr == "base"
full_messages << message
else
msg=object.class.human_attribute_name(attr)
msg_part+= I18n.t('activerecord.errors.format.separator', :default => ' ') + (msg_part=="" ? '': ' & ' ) + message
end
end
full_messages << "#{msg} #{msg_part}" if msg!=""
end
full_messages
end
end
I wrote a custom helper
def display_error(field)
if #user.errors[field].any?
raw #user.errors[field].first+"<br>"
end
end
and then I use it in view under the text field like so
<%= display_error(:password) %>
How about this
#event.errors[:title].first?
I use this code for Ruby on Rails 3.0 release, which I put in lib/core_ext/rails/active_model/errors.rb:
module ActiveModel
class Errors
def full_message_per_field
messages_per_field = []
handled_attributes = []
each do |attribute, messages|
next if handled_attributes.include? attribute
messages = Array.wrap(messages)
next if messages.empty?
if attribute == :base
messages_per_field << messages.first
else
attr_name = attribute.to_s.gsub('.', '_').humanize
attr_name = #base.class.human_attribute_name(attribute, :default => attr_name)
options = { :default => "%{attribute} %{message}", :attribute => attr_name }
messages_per_field << I18n.t(:"errors.format", options.merge(:message => messages.first))
end
handled_attributes << attribute
end
messages_per_field
end
end
end
This is essentially the same code as ActiveModel::Errors#full_messages, but won't show more than one error per attribute. Be sure to require the file (say, in an initializer) and now you can call #model.errors.full_message_per_field do |message| ...
Similar to olovwia's answer:
<% #errors.keys.each do |attr| %>
<%= "#{attr.capitalize} #{#errors[attr].first}."%>
<% end %>"
Add a method to ActiveModel::Errors class
module ActiveModel
class Errors
def full_unique_messages
unique_messages = messages.map { |attribute, list_of_messages| [attribute, list_of_messages.first] }
unique_messages.map { |attribute_message_pair| full_message *attribute_message_pair }
end
end
end
Add it to a file, like lib/core_ext/rails/active_model/errors.rb. Create a file config/initializers/core_ext.rb and add a require "core_ext/rails/active_model/errors.rb" to it.
I would display all the error messages on one line and in a sentence format. You don't want the user to fix one error and end up having another error he was not aware of after submission. Telling them all the rules will save them clicks. With that said, this is how I'd do it:
flash_message_now("error",
#album.errors.keys.map { |k| "#{Album.human_attribute_name(k)} #{#album.errors[k].to_sentence}"}.to_sentence
)
with flash_message_now defined in ApplicationController (you can add it to a helper)
def flash_message_now(type, text)
flash.now[type] ||= []
flash.now[type] << text
end
Or you can simply modify the array (with 'bang' method delete_at), so everything after stays default rails, i18n etc.
<% #article.errors.keys.each { |attr| #article.errors[attr].delete_at(1) } %>
Complete working code:
<% if #article.errors.any? %>
<% #article.errors.keys.each { |attr| #article.errors[attr].delete_at(1) } %>
<ul>
<% #article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
<% end %>
# Extracts at most <strong>one error</strong> message <strong>per field</strong> from the errors-object.
# #param [ActiveModel::Errors] the_errors_object The errors-object.
# #raise [ArgumentError] If the given argument is not an instance of ActiveModel::Errors.
# #return [Array] A string-array containing at most one error message per field from the given errors-object.
def get_one_error_per_field(the_errors_object)
if the_errors_object.is_a? ActiveModel::Errors
errors = {}
the_errors_object.each do |field_name, associated_error|
errors[field_name] = the_errors_object.full_message(field_name, associated_error) unless errors[field_name]
end
return errors.values
else
raise ArgumentError.new('The given argument isn\'t an instance of ActiveModel::Errors!')
end
end
My monkey patch of ActiveModel::Errors class lib/core_ext/rails/active_model/errors.rb (I use this code for Ruby on Rails 5.0 release):
module ActiveModel
class Errors
# don't add an attribute's error message to details
# if it already contains at least one message
alias_method :old_add, :add
def add(attribute, message = :invalid, options = {})
if details[attribute.to_sym].size.zero?
old_add(attribute, message, options)
end
end
end
end
Create a file config/initializers/core_ext.rb and add a require core_ext/rails/active_model/errors.rb to it.
I think the easiest way is to use allow_bank option.
For example, to avoid display the message that the name is too short when the field is left blank, you can do the following:
validates_length_of :name, allow_blank:true, :in => 6..30

Resources