Skip to content

Commit

Permalink
Add date validator.
Browse files Browse the repository at this point in the history
This allows dates to be submitted in a few different formats, namely
month/day/year, or spelled out, as in November 17, 2014.

In the month/day/year format, both 2-digit and 4-digit years are
accepted.

The day/month/year format is also accepted, but to use it, you must
first update the `date_format` option in `settings.yml`.
  • Loading branch information
monfresh committed Nov 17, 2014
1 parent 9a41057 commit 8a5862a
Show file tree
Hide file tree
Showing 20 changed files with 154 additions and 13 deletions.
1 change: 1 addition & 0 deletions app/models/organization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class Organization < ActiveRecord::Base

validates :email, email: true, allow_blank: true
validates :website, url: true, allow_blank: true
validates :date_incorporated, date: true

validates :accreditations, :funding_sources, :licenses, pg_array: true

Expand Down
64 changes: 64 additions & 0 deletions app/validators/date_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
class DateValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
value = record.read_attribute_before_type_cast(attribute)
return if value.blank? || value.is_a?(Date)

default_message = "#{value} #{I18n.t('errors.messages.invalid_date')}"

unless date_valid?(value)
record.errors[attribute] << (options[:message] || default_message)
return
end

convert_value_to_date(record, attribute, value)
end

private

def date_valid?(date)
return true unless date.include?('/')
return Date.valid_date?(year(date), month(date), day(date)) if month_day?
return Date.valid_date?(year(date), day(date), month(date)) if day_month?
end

def month_day?
SETTINGS[:date_format] == '%m/%d/'
end

def day_month?
SETTINGS[:date_format] == '%d/%m/'
end

def split_date(date)
date.split('/')
end

def year(date)
split_date(date).last.to_i
end

def month(date)
split_date(date).first.to_i
end

def day(date)
split_date(date).second.to_i
end

def convert_value_to_date(record, attribute, value)
record[attribute] = parse_date(value)
end

def parse_date(date)
Date.strptime(date, date_format_for(date)) rescue Date.parse(date)
end

def date_format_for(date)
SETTINGS[:date_format] + year_format_for(date)
end

def year_format_for(date)
return '%Y' if date =~ /\d{4}/
'%y'
end
end
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ en:
blank_for_program: "can't be blank for Program"
blank_for_rs: "can't be blank for Regular Schedule"
blank_for_service: "can't be blank for Service"
invalid_date: 'is not a valid date'
invalid_email: 'is not a valid email'
invalid_fax: 'is not a valid US fax number'
invalid_phone: 'is not a valid US phone or fax number'
Expand Down
14 changes: 14 additions & 0 deletions config/settings.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,18 @@ valid_service_areas:
- 'San Mateo'
- 'South San Francisco'

############################
#
# DATE SETTINGS
#
############################
#
# The date format to use for the Organization `date_incorporated` field,
# and for the Holiday Schedule `start_date` and `end_date` fields.
# The default format is the US-style month/day/year. To use the day/month
# style, change the value below to '%d/%m/'.
date_format: '%m/%d/'

############################
#
# EMAIL DELIVERY SETTINGS
Expand Down Expand Up @@ -234,3 +246,5 @@ test:
valid_service_areas:
- 'Atherton'
- 'Belmont'

date_format: '%m/%d/'
14 changes: 14 additions & 0 deletions config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,18 @@ valid_service_areas:
- 'San Mateo County'
- 'South San Francisco'

############################
#
# DATE SETTINGS
#
############################
#
# The date format to use for the Organization `date_incorporated` field,
# and for the Holiday Schedule `start_date` and `end_date` fields.
# The default format is the US-style month/day/year. To use the day/month
# style, change the value below to '%d/%m/'.
date_format: '%m/%d/'

############################
#
# EMAIL DELIVERY SETTINGS
Expand Down Expand Up @@ -234,3 +246,5 @@ test:
valid_service_areas:
- 'Atherton'
- 'Belmont'

date_format: '%m/%d/'
2 changes: 1 addition & 1 deletion data/organizations.csv
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
id,accreditations,alternate_name,date_incorporated,description,email,funding_sources,legal_status,licenses,name,tax_id,tax_status,website
1,"BBB, State Board of Education",HFB,1/1/1970,"Harvest Food Bank provides fresh produce, dairy, and canned goods to food pantries throughout the city.",info@example.org,"Donations, Grants",Nonprofit,State Health Inspection License,Harvest Food Bank,12-456789,501(c)3,http://www.example.org
1,"BBB, State Board of Education",HFB,1/24/1970,"Harvest Food Bank provides fresh produce, dairy, and canned goods to food pantries throughout the city.",info@example.org,"Donations, Grants",Nonprofit,State Health Inspection License,Harvest Food Bank,12-456789,501(c)3,http://www.example.org
2,,,1/2/2014,Example org description,info@fakeorg.org,Donations,Nonprofit,,Example Agency,,,http://www.example.org
2 changes: 1 addition & 1 deletion lib/contact_importer.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class ContactImporter < EntityImporter
def valid?
valid_headers? && contacts.all?(&:valid?)
@valid ||= valid_headers? && contacts.all?(&:valid?)
end

def errors
Expand Down
4 changes: 2 additions & 2 deletions lib/location_importer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def valid_headers?
end

def valid?
valid_headers? && locations.all?(&:valid?)
@valid ||= valid_headers? && locations.all?(&:valid?)
end

def errors
Expand All @@ -29,7 +29,7 @@ def import
protected

def locations
@locations ||= csv_entries.map(&:to_hash).map do |p|
csv_entries.map(&:to_hash).map do |p|
LocationPresenter.new(p, addresses).to_location
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/mail_address_importer.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class MailAddressImporter < EntityImporter
def valid?
valid_headers? && mail_addresses.all?(&:valid?)
@valid ||= valid_headers? && mail_addresses.all?(&:valid?)
end

def errors
Expand Down
4 changes: 2 additions & 2 deletions lib/organization_importer.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class OrganizationImporter < EntityImporter
def valid?
valid_headers? && organizations.all?(&:valid?)
@valid ||= valid_headers? && organizations.all?(&:valid?)
end

def errors
Expand All @@ -20,7 +20,7 @@ def required_headers
protected

def organizations
@organizations ||= csv_entries.map(&:to_hash).map do |p|
csv_entries.map(&:to_hash).map do |p|
OrganizationPresenter.new(p).to_org
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/phone_importer.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class PhoneImporter < EntityImporter
def valid?
valid_headers? && phones.all?(&:valid?)
@valid ||= valid_headers? && phones.all?(&:valid?)
end

def errors
Expand Down
2 changes: 1 addition & 1 deletion lib/program_importer.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class ProgramImporter < EntityImporter
def valid?
valid_headers? && programs.all?(&:valid?)
@valid ||= valid_headers? && programs.all?(&:valid?)
end

def errors
Expand Down
2 changes: 1 addition & 1 deletion lib/regular_schedule_importer.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class RegularScheduleImporter < EntityImporter
def valid?
valid_headers? && regular_schedules.all?(&:valid?)
@valid ||= valid_headers? && regular_schedules.all?(&:valid?)
end

def errors
Expand Down
2 changes: 1 addition & 1 deletion lib/service_importer.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class ServiceImporter < EntityImporter
def valid?
valid_headers? && services.all?(&:valid?)
@valid ||= valid_headers? && services.all?(&:valid?)
end

def errors
Expand Down
37 changes: 36 additions & 1 deletion spec/lib/organization_importer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
describe OrganizationImporter do
let(:invalid_header_content) { Rails.root.join('spec/support/fixtures/invalid_org_headers.csv').read }
let(:invalid_content) { Rails.root.join('spec/support/fixtures/invalid_org.csv').read }
let(:invalid_date) { Rails.root.join('spec/support/fixtures/org_with_invalid_date.csv').read }
let(:valid_content) { Rails.root.join('spec/support/fixtures/valid_org.csv').read }
let(:spelled_out_date) { Rails.root.join('spec/support/fixtures/org_with_spelled_out_date.csv').read }
let(:org_with_2_digit_year) { Rails.root.join('spec/support/fixtures/org_with_2_digit_year.csv').read }

subject(:importer) { OrganizationImporter.new(content) }

Expand Down Expand Up @@ -61,6 +64,14 @@

its(:errors) { is_expected.to eq(errors) }
end

context 'when the date is not valid' do
let(:content) { invalid_date }

errors = ['Line 2: Date incorporated 24/2/70 is not a valid date']

its(:errors) { is_expected.to eq(errors) }
end
end

describe '#import' do
Expand All @@ -78,7 +89,7 @@

its(:accreditations) { is_expected.to eq ['BBB', 'State Board of Education'] }
its(:alternate_name) { is_expected.to eq 'HFB' }
its(:date_incorporated) { is_expected.to eq Date.parse('January 1, 1970') }
its(:date_incorporated) { is_expected.to eq Date.parse('January 2, 1970') }
its(:description) { is_expected.to eq 'Harvest Food Bank provides fresh produce, dairy, and canned goods to food pantries throughout the city.' }
its(:email) { is_expected.to eq 'info@example.org' }
its(:funding_sources) { is_expected.to eq %w(Donations Grants) }
Expand All @@ -91,6 +102,30 @@
end
end

context 'when the date is formatted as month, day, year' do
let(:content) { spelled_out_date }

describe 'the org' do
before { importer.import }

subject { Organization.first }

its(:date_incorporated) { is_expected.to eq Date.parse('January 20, 1970') }
end
end

context 'when the year only contains two digits' do
let(:content) { org_with_2_digit_year }

describe 'the org' do
before { importer.import }

subject { Organization.first }

its(:date_incorporated) { is_expected.to eq Date.parse('January 24, 1970') }
end
end

context 'when one of the fields required for a org is blank' do
let(:content) { invalid_content }

Expand Down
6 changes: 6 additions & 0 deletions spec/models/organization_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@
with_message('BBB is not an Array.')
end

it do
is_expected.not_to allow_value('24/2/2014').
for(:date_incorporated).
with_message('24/2/2014 is not a valid date')
end

describe 'auto_strip_attributes' do
it 'strips extra whitespace before validation' do
org = build(:org_with_extra_whitespace)
Expand Down
2 changes: 2 additions & 0 deletions spec/support/fixtures/org_with_2_digit_year.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
id,accreditations,alternate_name,date_incorporated,description,email,funding_sources,legal_status,licenses,name,tax_id,tax_status,website
1,"BBB, State Board of Education",HFB,1/24/70,"Harvest Food Bank provides fresh produce, dairy, and canned goods to food pantries throughout the city.",info@example.org,"Donations, Grants",Nonprofit,State Health Inspection License,Parent Agency,12-456789,501(c)3,http://www.example.org
2 changes: 2 additions & 0 deletions spec/support/fixtures/org_with_invalid_date.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
id,accreditations,alternate_name,date_incorporated,description,email,funding_sources,legal_status,licenses,name,tax_id,tax_status,website
1,"BBB, State Board of Education",HFB,24/2/70,"Harvest Food Bank provides fresh produce, dairy, and canned goods to food pantries throughout the city.",info@example.org,"Donations, Grants",Nonprofit,State Health Inspection License,Parent Agency,12-456789,501(c)3,http://www.example.org
2 changes: 2 additions & 0 deletions spec/support/fixtures/org_with_spelled_out_date.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
id,accreditations,alternate_name,date_incorporated,description,email,funding_sources,legal_status,licenses,name,tax_id,tax_status,website
1,"BBB, State Board of Education",HFB,"January 20, 1970","Harvest Food Bank provides fresh produce, dairy, and canned goods to food pantries throughout the city.",info@example.org,"Donations, Grants",Nonprofit,State Health Inspection License,Parent Agency,12-456789,501(c)3,http://www.example.org
2 changes: 1 addition & 1 deletion spec/support/fixtures/valid_org.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
id,accreditations,alternate_name,date_incorporated,description,email,funding_sources,legal_status,licenses,name,tax_id,tax_status,website
1,"BBB, State Board of Education",HFB,1/1/1970,"Harvest Food Bank provides fresh produce, dairy, and canned goods to food pantries throughout the city.",info@example.org,"Donations, Grants",Nonprofit,State Health Inspection License,Parent Agency,12-456789,501(c)3,http://www.example.org
1,"BBB, State Board of Education",HFB,1/2/1970,"Harvest Food Bank provides fresh produce, dairy, and canned goods to food pantries throughout the city.",info@example.org,"Donations, Grants",Nonprofit,State Health Inspection License,Parent Agency,12-456789,501(c)3,http://www.example.org

0 comments on commit 8a5862a

Please sign in to comment.