I have a Client model which has many projects. In the project model I want to validate that the project start date is always before or on the same day as the project end date. This is my project model:
class Project < ActiveRecord::Base
attr_accessible :end_on, :start_on, :title
validates_presence_of :client_id, :end_on, :start_on, :title
validate :start_has_to_be_before_end
belongs_to :clients
def start_has_to_be_before_end
if start_on > end_on
errors[:start_on] << " must not be after end date."
errors[:end_on] << " must not be before start date."
end
end
end
My application works as expected and gives me the specified errors in case the validation fails.
However, in my unit test for the projects, I am trying to cover this scenario, deliberately setting the start date after the end date:
test "project must have a start date thats either on the same day or before the end date" do
project = Project.new(client_id: 1, start_on: "2012-01-02", end_on: "2012-01-01", title: "Project title")
assert !project.save, "Project could be saved although its start date was after its end date"
assert !project.errors[:start_on].empty?
assert !project.errors[:end_on].empty?
end
Strangely, running this test gives me three errors, all referring to this line if start_on > end_on in my validation method, saying undefined method '>' for nil:NilClass twice and comparison of Date with nil failed once.
What can I do to make the tests pass?
You are creating a Project that has string values for :start_on and :end_on. That's unlikely to work. Rails might try to be smart and parse those, I'm not sure.. I wouldn't count on it. Odds are some coercion is going on and the values are getting set to nil.
I would do this:
project = Project.new(client_id: 1,
start_on: 2.days.from_now.to_date,
end_on: Time.now.to_date,
title: "Project title")
Related
I am trying to get a date from a user and send it inside an email as plain text in the following format: "07/30/2015".
In order to do that, if the output I am getting is a string, I could just do:
Date.parse("2015-07-30").strftime("%m/%d/%Y")
The problem is, I am getting a FixNum.
The issues are many:
If I try to convert to a string to parse it with Date.parse, it becomes "2001".
If I apply the code I just wrote, Date.parse... it will throw 'invalid date'.
For instance:
(2016-02-13).to_s #=> "2001"
(2016-02-13).to_date #=> NoMethodError: undefined method `to_date' for 2001:Fixnum
Date.parse("2001").strftime("%m/%d/%Y") #=> invalid date
So if I can convert 2015-07-30 into "2015-07-30", it would work:
Date.parse("2015-07-30").strftime("%m/%d/%Y") #=> "07/30/2015"
Then I tried using date_select instead of date_field, but now the message arrives with those fields empty.
Any suggestions?
Here is my form:
= form_for #contact do |f|
= f.text_field :product_name
= f.date_field :purchase_date
= f.submit
Here is my code:
<%= message.subject %>
<% #resource.mail_form_attributes.each do |attribute, value|
if attribute == "mail_subject"
next
end
%>
<%= "#{#resource.class.human_attribute_name(attribute)}: #{Date.parse(value).class == Date ? Date.parse(value).strftime("%m/%d/%Y") : value}" %>
<% end %>
My controller:
class ContactsController < ApplicationController
before_action :send_email, except: [:create]
def create
#contact = Contact.new(params[:contact])
#contact.request = request
if #contact.deliver
#thank = "Thank you for your message!"
#message = "We have received your inquiry and we'll be in touch shortly."
else
#error = "Cannot send message. Please, try again."
end
end
def contact_page
end
def product_complaint
#the_subject = "Product Complaint Form"
end
private
def send_email
#contact = Contact.new
end
end
My model:
class Contact < MailForm::Base
# all forms
attribute :mail_subject
attribute :first_name, validate: true
attribute :last_name, validate: true
# product-complaint
attribute :best_by, validate: true, allow_blank: true # date
attribute :bag_code, validate: true, allow_blank: true
attribute :purchase_date, validate: true, allow_blank: true # date
attribute :bag_opened, validate: true, allow_blank: true # date
attribute :problem_noticed, validate: true, allow_blank: true # date
# all forms
attribute :message, validate: true
attribute :nickname, captcha: true
def headers
{
content_type: "text/plain",
subject: %(#{mail_subject}),
to: "xxxxx#xxxxxxx.com",
# from: %("#{first_name.capitalize} #{last_name.capitalize}" <#{email.downcase}>)
from: "xxx#xxxxx.com"
}
end
end
(2016-02-13).to_date #=> NoMethodError: undefined method `to_date' for 2001:Fixnum
youre getting this error because you dont have quotes around the value. i.e. its not a string, its a number that is having subtraction applied to it. this is being interpreted as
2016 - 2
2014 - 13
2001.to_date
it needs to be ('2016-02-13').to_date
if youre unable to get it as a string, can you post how you're getting it from the user to begin with? (a date field ought to be sending you a string to your controller, not a series of numbers)
You're not understanding something about receiving values from forms: You can NOT receive an integer, a fixnum or anything else other than strings. So, you can't have received 2016-02-13. Instead you got "2016-02-13" or "2016", "02" or "2" and "13" depending on the form. If you're running under Rails, then it got the strings, and through its meta-data understands you want an integer (which really should probably be defined as a string), and it converts it to an integer for you.
Either way, when you write:
(2016-02-13).to_s
(2016-02-13).to_date
you're propagating that misunderstanding into your testing. This is how it MUST be written because you need to be working with strings:
require 'active_support/core_ext/string/conversions'
("2016-02-13").to_s # => "2016-02-13"
("2016-02-13").to_date # => #<Date: 2016-02-13 ((2457432j,0s,0n),+0s,2299161j)>
You can create dates without them being strings though: Ruby's Date initializer allows us to pass the year, month and day value and receive a new Date object:
year, month, day = 2001, 1, 2
date = Date.new(year, month, day) # => #<Date: 2001-01-02 ((2451912j,0s,0n),+0s,2299161j)>
date.year # => 2001
date.month # => 1
date.day # => 2
Moving on...
Parsing dates in Ruby quickly demonstrates it's not a U.S.-centric language. Americans suppose all dates of 01/01/2001 are in "MM/DD/YYYY" but that's a poor assumption because much of the rest of the world uses "DD/MM/YYYY". Not knowing that means that code written under that assumption is doing the wrong thing. Consider this:
require 'date'
date = Date.parse('01/02/2001')
date.month # => 2
date.day # => 1
Obviously something "wrong" is happening, at least for 'mericans. This is very apparent with:
date = Date.parse('01/31/2001')
# ~> -:3:in `parse': invalid date (ArgumentError)
This occurs because there is no month "31". In the previous example of '01/02/2001', that misunderstanding means the programmer thinks it should be "January 2" but the code thinks it's "February 1", and work with that. That can cause major havoc in an enterprise system, or anything dealing with financial calculations, product scheduling, shipping or anything else that works with dates.
Because the code is assuming DD/MM/YYYY format for that sort of string, the sensible things to do are:
KNOW what format your users are going to send dates in. Don't assume, ever. ASK them and make your code capable of dealing with alternates, or tell them what they MUST use and vet out their data prior to actually committing it to your system. Or, provide a GUI that forces them to pick their dates from popups and never allows them to enter it by hand.
Force the date parser to use explicit formats of dates so it can always do the right thing:
Date.strptime('01/31/2001', '%m/%d/%Y') # => #<Date: 2001-01-31 ((2451941j,0s,0n),+0s,2299161j)>
Date.strptime('31/01/2001', '%d/%m/%Y') # => #<Date: 2001-01-31 ((2451941j,0s,0n),+0s,2299161j)>
The last point is the crux of writing code: We're telling the language what to do, not subjecting ourselves, and our employers, to code that's guessing. Give code half a chance and it'll do the wrong thing, so you control it. That's why programming is hard.
What is the best way to validate a date in Ruby on Rails? I need to make sure the "birthday" is a date, is less than 125 years ago and is not in the future (today is ok).
I have tried three methods:
1) date_validator gem
I used the following code (after installing the gem):
validates :birthday,
date: {
after: Proc.new {Time.now - 125.years}, message: :after,
before: Proc.new {Time.now}, message: :cannot_be_in_the_future
}
This worked except that I could set the date to the number 12 and pass validation.
2) Checking if the date is in a range of dates, in a custom validation method, like so:
from = 125.years.ago.to_date
to = Time.now.to_date
unless (from..to).include? birthday
errors.add(:birthday, :custom_error_msg)
end
This worked well and passed all my tests, but the drawback is that you only get one error message. I would have liked separate error messages for the case when the date is in the future, when it is too long ago and when the input is not a date.
3) Multiple checks in a custom validation method, like so:
begin
birthday.to_date
rescue
errors.add(:birthday, "must be a date")
else
if birthday > Time.now
errors.add(:birtday, "cannot be in the future")
elsif birthday < Time.now - 125.years
errors.add(:birthday, "cannot be over 125 years ago")
end
end
This also passes all my test, and I get different messages as explained above.
So I am wondering, is this the best method? Can it be improved at all (except that the error message text needs work)?
Thanks
For this simple validation, I think following ruby code is enough!
Please check :
validate :is_valid_dob?
private
def is_valid_dob?
if((birthday.is_a?(Date) rescue ArgumentError) == ArgumentError)
errors.add(:birthday, 'Sorry, Invalid Date of Birth Entered.')
end
end
Just use gem 'validates_timeliness'
In your case, using this gem
validates_date :birthday, on_or_after: lambda { 125.years.ago }
You can easily use validates_each method
Just put there 2 validations:
1) For birthday in the past
validates_each :birthday do |record, attr, value|
record.errors.add(attr, 'must be in the past') if value >= Time.now.to_date
end
2) For birthday not more than 150 years ago
validates_each :birthday do |record, attr, value|
record.errors.add(attr, 'must be less than 150 years in the past')
if value <= (Time.now.to_date - 125.years)
end
sorry for my english,
I have this validation on my model
validates_uniqueness_of :price, scope: [:brand, :establishment, :presentation, :user], conditions: -> { where(created_at: Date.today.beginning_of_day..Date.today.end_of_day) }
And this is my factory and test for it.
FactoryGirl.define do
factory :price do
association :establishment
association :presentation
association :brand
association :user
price 9.99
end
end
it "is invalid on duplicated by date" do
price = create(:price)
expect(build(:price, price.attributes)).to have(1).errors
end
And I am getting...
1) Price is invalid on duplicated by date
Failure/Error: expect(build(:price, price.attributes)).to have(1).errors
expected 1 errors, got 0
Why?
validation errors get added to new objects(unsaved) only when you call valid? on the object
it "is invalid on duplicated by date" do
price = create(:price)
new_price = build(:price, price.attributes)
new_price.valid?
expect(new_price).to have(1).errors
end
I solved it, my error was on a database conception, I had a float type for price field, I changed it to decimal type to fix the error.
I'm a bit new to Rails and Rspec and as such I'm not sure how to test that date time validations are correct in my model.
I've made a model Event that has start and end times and there's a few imporant conditions on these such as a start time cannot be in the past and the end time must be after the start time.
To ensure these validations I'm using ValidatesTimeliness https://github.com/adzap/validates_timeliness
My model is as follows:
class Event < ActiveRecord::Base
...
validates_datetime :start_date,
:after => :now,
:after_message => "Event cannot start in the past"
validates_datetime :end_date,
:after => :start_date,
:after_message => "End time cannot be before start time"
end
In my RSpec test I have:
describe Event do
let(:event) { FactoryGirl.build :event }
subject { event }
context "when start_date is before the current time" do
it {should_not allow_value(1.day.ago).
for(:start_date)}
end
context "when end_date is before or on start date" do
it {should_not allow_value(event.start_date - 1.day).
for(:end_date)}
it {should_not allow_value(event.start_date).
for(:end_date)}
end
context "when the end_date is after the start_date" do
it {should allow_value(event.start_date + 1.day).
for(:end_date)}
end
end
However this doesn't really test that my start date had to be before the exact date time.
For example if I'd accidentally used :today instead of :now in my model, these tests would also pass.
I read online that there used to be an RSpec matcher called validate_date (http://www.railslodge.com/plugins/1160-validates-timeliness) which would be exactly what I'm looking for but as far as I can tell it's been removed.
My question is how can I improve my tests, do I need to add tests that try the smallest amount of time (i.e. a ms) to ensure the pass/fail accordingly or is there a better way to do it?
Thanks in advance!
You could work with valid? and errors.messages:
Build anEvent that passes validation except for your start_date and end_date
Set the start_date and end_date in the right order, and assert that event.valid? is true
Set the start_date and end_date in the wrong order, and assert that it is not valid? and that event.errors.messages includes the right validation errors. (Note, you have to call event.valid? before checking event.errors.messages, otherwise they will be empty)
Example for valid? and errors.messages:
user = User.new
user.errors.messages #=> {} # no messages, since validations never ran
user.valid? # => false
user.errors.messages #=> {:email=>["can't be blank"]}
user.email = "foo#bar.com"
user.valid? #=> true
user.errors.messages #=> {}
Try this
validates_date :end_time, :after => [:start_time, Proc.new {1.day.from_now_to_date}]
validates_date :start_time, :after => Time.now
I'm trying out the whole TDD and I'm running into a problems with validate presence. I have a model called Event and I want to ensure that when an Event is created that a title a price and a summary exists.
Unit Test Code
class EventTest < ActiveSupport::TestCase
test "should not save without a Title" do
event = Event.new
event.title = nil
assert !event.save, "Save the Event without title"
end
test "should not save without a Price" do
event = Event.new
event.price = nil
assert !event.save, "Saved the Event without a Price"
end
test "should not save without a Summary" do
event = Event.new
event.summary = nil
assert !event.save, "Saved the Event without a Summary"
end
end
I run the test I get 3 FAILS. Which is Good.
Now I want to to just get the title test to pass first with the following code in the Event model.
class Event < ActiveRecord::Base
validates :title, :presence => true
end
When I re-run the test I get 3 PASSES where I would think I should have gotten 1 PASS and 2 FAILS. Why am I getting 3 PASSES?
I have two test helper methods that can make this sort of thing easier to diagnose:
def assert_created(model)
assert model, "Model was not defined"
assert_equal [ ], model.errors.full_messages
assert model.valid?, "Model failed to validate"
assert !model.new_record?, "Model is still a new record"
end
def assert_errors_on(model, *attrs)
found_attrs = [ ]
model.errors.each do |attr, error|
found_attrs << attr
end
assert_equal attrs.flatten.collect(&:to_s).sort, found_attrs.uniq.collect(&:to_s).sort
end
You'd use them in cases like this:
test "should save with a Title, Price or Summary" do
event = Event.create(
:title => 'Sample Title',
:price => 100,
:summary => 'Sample summary...'
)
assert_created event
end
test "should not save without a Title, Price or Summary" do
event = Event.create
assert_errors_on event, :title, :price, :summary
end
This should show if you're missing a validation that you expected and will also give you feedback on specific validations that have failed when not expected.
When you created the model with Event.new, all attributes initially have a value of nil. This means that all 3 attributes you are checking are already nil (so event.title = nil and event.price = nil don't actually do anything). Since title has been marked for validation to ensure its presence, unless you set title to something other than nil, you will not be able to save the model.
Perhaps try adding this to your test class:
setup do
#event_attributes = {:title => "A title", :price => 3.99, :summary => "A summary"}
end
Then instead of:
event = Event.new
event.title = nil
Use:
event = Event.new(#event_attributes.merge(:title => nil))
Do the same for all your tests (substituting :title with whatever attribute you are validating presence for)
Also, there's no reason to call save to test for a valid state. You can just call event.valid? to avoid trips to the database where not needed.