my_zipcode_gem 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.textile +126 -0
- data/Rakefile +11 -0
- data/features/step_definitions/common_steps.rb +76 -0
- data/features/step_definitions/rails_setup_steps.rb +17 -0
- data/features/support/env.rb +8 -0
- data/features/support/string.rb +128 -0
- data/features/zipcodes.feature +24 -0
- data/lib/generators/my_zipcode_gem/models_generator.rb +46 -0
- data/lib/generators/my_zipcode_gem/templates/county_model.rb +17 -0
- data/lib/generators/my_zipcode_gem/templates/migration.rb +44 -0
- data/lib/generators/my_zipcode_gem/templates/state_model.rb +15 -0
- data/lib/generators/my_zipcode_gem/templates/zipcode_model.rb +27 -0
- data/lib/generators/my_zipcode_gem/templates/zipcodes.rake +96 -0
- data/lib/generators/my_zipcode_gem/version.rb +3 -0
- data/lib/my_zipcode_gem.rb +33 -0
- data/my_zipcode_gem.gemspec +32 -0
- metadata +220 -0
data/Gemfile
ADDED
data/README.textile
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
h1. My Zipcode Gem
|
2
|
+
|
3
|
+
Simple gem to handle zipcode lookups and related functionality.
|
4
|
+
|
5
|
+
h2. Installation
|
6
|
+
|
7
|
+
Add the following line to your Gemfile:
|
8
|
+
|
9
|
+
bc. gem 'my_zipcode_gem'
|
10
|
+
|
11
|
+
Run:
|
12
|
+
|
13
|
+
bc. rake bundle install
|
14
|
+
|
15
|
+
Generate the models and populate the data:
|
16
|
+
|
17
|
+
bc. rails g my_zipcode_gem:models
|
18
|
+
rake db:migrate
|
19
|
+
rake zipcodes:update
|
20
|
+
|
21
|
+
You should now have three new tables and three new models, Zipcode, State, County.
|
22
|
+
|
23
|
+
h2. Usage
|
24
|
+
|
25
|
+
bc. zipcode = Zipcode.find_by_code '66206'
|
26
|
+
zipcode.state.abbr # => 'KS'
|
27
|
+
zipcode.city # => 'Shawnee Mission'
|
28
|
+
zipcode.county.name # => 'Johnson'
|
29
|
+
zipcode.lat.to_s # => '38.959356', it is actually a BigDecimal object converted to_s for documentation.
|
30
|
+
zipcode.lon.to_s # => '-94.716155', ditto
|
31
|
+
zipcode.is_geocoded? # => true, most if not all should be pre-geocoded.
|
32
|
+
|
33
|
+
You can also look for a zipcode from a city and state:
|
34
|
+
|
35
|
+
bc. Zipcode.find_by_city_state "Shawnee Mission", "KS"
|
36
|
+
|
37
|
+
You can use State and County objects as follows:
|
38
|
+
|
39
|
+
bc. state = State.find_by_abbr "MO"
|
40
|
+
state.cities.count # => 963
|
41
|
+
state.cities # gives you an sorted array of all cities for the state
|
42
|
+
state.zipcodes.count # => 1195
|
43
|
+
...
|
44
|
+
county = state.counties.first
|
45
|
+
county.cities.count # => 5
|
46
|
+
county.cities # gives you an sorted array of all cities for the county
|
47
|
+
county.zipcodes.count # => 5
|
48
|
+
|
49
|
+
h3. Automatic JQuery/AJAX lookup
|
50
|
+
|
51
|
+
You can have a user enter a zipcode and automatically lookup their city, state and county.
|
52
|
+
|
53
|
+
Put something like this in your view:
|
54
|
+
|
55
|
+
bc. f.text_field :zip, :size => 5, :maxlength => 5, :class => 'zipcode_interactive'
|
56
|
+
f.text_field :city, :size => 20, :maxlength => 60, :readonly => true
|
57
|
+
f.text_field(:state, :size => 2, :maxlength => 2, :readonly => true)
|
58
|
+
f.text_field(:county, :size => 20, :maxlength => 60, :readonly => true)
|
59
|
+
|
60
|
+
Then add this to your application.js, but remember to replace [mycontrollername] with your own controller.
|
61
|
+
|
62
|
+
bc. $(document).ready(function() {
|
63
|
+
// Interactive Zipcodes
|
64
|
+
$('input.zipcode_interactive').blur(function(data) {
|
65
|
+
var elem_id = $(this).attr("id");
|
66
|
+
var base_id = elem_id.substring(0, elem_id.lastIndexOf("_"));
|
67
|
+
$.get("/mycontrollername/get_zip_data/" + this.value, {},
|
68
|
+
function(data) {
|
69
|
+
var zipcode = $.parseJSON(data);
|
70
|
+
var city = $('#' + base_id + '_city');
|
71
|
+
var state = $('#' + base_id + '_state');
|
72
|
+
var county = $('#' + base_id + '_county');
|
73
|
+
if (zipcode.err) {
|
74
|
+
alert(zipcode.err);
|
75
|
+
} else {
|
76
|
+
city.val(zipcode.city);
|
77
|
+
state.val(zipcode.state)
|
78
|
+
county.val(zipcode.county)
|
79
|
+
}
|
80
|
+
})
|
81
|
+
});
|
82
|
+
});
|
83
|
+
|
84
|
+
You will also need a controller method similar to this, which will return the data to your form:
|
85
|
+
|
86
|
+
bc. def get_zip_data
|
87
|
+
@zipcode = Zipcode.find_by_code(params[:code], :include => [:county, :state])
|
88
|
+
if @zipcode
|
89
|
+
@counties = County.find(:all, :conditions => [ "state_id = ?", @zipcode.county.state_id ])
|
90
|
+
data = {
|
91
|
+
'state' => @zipcode.state.abbr,
|
92
|
+
'county' => @zipcode.county.name,
|
93
|
+
'city' => @zipcode.city.titleize
|
94
|
+
}
|
95
|
+
render :text => data.to_json
|
96
|
+
else
|
97
|
+
if params[:code].blank?
|
98
|
+
return true
|
99
|
+
else
|
100
|
+
if params[:code].is_zipcode?
|
101
|
+
data = {
|
102
|
+
'err' => "Could not find Zipcode [#{params[:code]}]. If this is a valid zipcode please notify support <support@mydomain.com>, so we can update our database."
|
103
|
+
}
|
104
|
+
else
|
105
|
+
data = {
|
106
|
+
'err' => "[#{params[:code]}] is not a valid Zipcode."
|
107
|
+
}
|
108
|
+
end
|
109
|
+
render :text => data.to_json
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
And define a route for the AJAX function in routes.rb:
|
115
|
+
|
116
|
+
bc. get 'mycontrollername/get_zip_data/:code', :controller => 'mycontrollername', :action => 'get_zip_data'
|
117
|
+
|
118
|
+
That's about it.
|
119
|
+
|
120
|
+
Let me know if there are any errors. I cut and pasted the code above from a working application, but there may be some gotchas that I missed.
|
121
|
+
|
122
|
+
h2. LOG
|
123
|
+
|
124
|
+
h3. 05/03/2011:
|
125
|
+
|
126
|
+
Initial Release
|
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'cucumber'
|
5
|
+
require 'cucumber/rake/task'
|
6
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
7
|
+
t.cucumber_opts = "features --format progress"
|
8
|
+
end
|
9
|
+
|
10
|
+
task :default => :features
|
11
|
+
task :test => :features
|
@@ -0,0 +1,76 @@
|
|
1
|
+
When(/^I run "([^\"]*)"$/) do |command|
|
2
|
+
system("cd #{@current_directory} && #{command}").should be_true
|
3
|
+
end
|
4
|
+
|
5
|
+
When(/^I add "([^\"]*)" to file "([^\"]*)"$/) do |content, short_path|
|
6
|
+
path = File.join(@current_directory, short_path)
|
7
|
+
File.should exist(path)
|
8
|
+
File.open(path, 'a') { |f| f.write(content + "\n") }
|
9
|
+
end
|
10
|
+
|
11
|
+
When(/^I replace "([^\"]*)" with "([^\"]*)" in file "([^\"]*)"$/) do |old_content, new_content, short_path|
|
12
|
+
path = File.join(@current_directory, short_path)
|
13
|
+
File.should exist(path)
|
14
|
+
content = File.read(path).gsub(old_content, new_content)
|
15
|
+
File.open(path, 'w') { |f| f.write(content) }
|
16
|
+
end
|
17
|
+
|
18
|
+
When(/^I insert "([^\"]*)" into "([^\"]*)" after line (\d+)$/) do |content, short_path, after_line|
|
19
|
+
path = File.join(@current_directory, short_path)
|
20
|
+
File.should exist(path)
|
21
|
+
lines = File.read(path).split("\n")
|
22
|
+
lines[after_line.to_i, 0] = content
|
23
|
+
File.open(path, 'w') { |f| f.write(lines.join("\n")) }
|
24
|
+
end
|
25
|
+
|
26
|
+
Then(/^I should see file "([^\"]*)"$/) do |path|
|
27
|
+
File.should exist(File.join(@current_directory, path))
|
28
|
+
end
|
29
|
+
|
30
|
+
Then(/^I should see "(.*)" in file "([^\"]*)"$/) do |content, short_path|
|
31
|
+
path = File.join(@current_directory, short_path)
|
32
|
+
File.should exist(path)
|
33
|
+
File.readlines(path).join.should include(content)
|
34
|
+
end
|
35
|
+
|
36
|
+
Then(/^I should not see "(.*)" in file "([^\"]*)"$/) do |content, short_path|
|
37
|
+
path = File.join(@current_directory, short_path)
|
38
|
+
File.should exist(path)
|
39
|
+
File.readlines(path).join.should_not include(content)
|
40
|
+
end
|
41
|
+
|
42
|
+
Then(/^I should see the following files$/) do |table|
|
43
|
+
table.raw.flatten.each do |path|
|
44
|
+
File.should exist(File.join(@current_directory, path))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
Then(/^I should see the following in file "([^\"]*)"$/) do |short_path, table|
|
49
|
+
path = File.join(@current_directory, short_path)
|
50
|
+
File.should exist(path)
|
51
|
+
table.raw.flatten.each do |content|
|
52
|
+
File.readlines(path).join.should include(content)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
Then(/^I should successfully run "([^\"]*)"$/) do |command|
|
57
|
+
system("cd #{@current_directory} && #{command}").should be_true
|
58
|
+
end
|
59
|
+
|
60
|
+
Then(/^I should see "([^\"]*)" when running "([^\"]*)"$/) do |expected_response, command|
|
61
|
+
`cd #{@current_directory} && #{command}`.should include(expected_response)
|
62
|
+
end
|
63
|
+
|
64
|
+
Then(/^I should see (\d+) records in the "([^\"]*)" table$/) do |count, table_name|
|
65
|
+
FileUtils.chdir(@current_directory)
|
66
|
+
ActiveRecord::Base.establish_connection(
|
67
|
+
:adapter => 'sqlite3',
|
68
|
+
:database => "db/development.sqlite3",
|
69
|
+
:pool => 5,
|
70
|
+
:timeout => 5000
|
71
|
+
)
|
72
|
+
sql = "SELECT COUNT(*) FROM #{table_name}"
|
73
|
+
result = ActiveRecord::Base.connection.select_rows(sql)
|
74
|
+
puts ">>> result: [#{result.flatten.first}]"
|
75
|
+
"Record Count:#{result.flatten.first}".should == "Record Count:#{count}"
|
76
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
Given(/^a new Rails app$/) do
|
2
|
+
FileUtils.rm_rf "tmp/rails_app"
|
3
|
+
FileUtils.mkdir_p("tmp")
|
4
|
+
system("rails new tmp/rails_app").should be_true
|
5
|
+
system("ln -s ../../../lib/generators tmp/rails_app/lib/generators").should be_true
|
6
|
+
@current_directory = File.expand_path("tmp/rails_app")
|
7
|
+
end
|
8
|
+
|
9
|
+
Given %{a new migrated Rails app} do
|
10
|
+
# Don't delete the rails app
|
11
|
+
FileUtils.mkdir_p("tmp")
|
12
|
+
system("rails new tmp/rails_app").should be_true
|
13
|
+
system("ln -s ../../../lib/generators tmp/rails_app/lib/generators").should be_true
|
14
|
+
@current_directory = File.expand_path("tmp/rails_app")
|
15
|
+
When %{I run "rails g my_zipcode_gem:models"}
|
16
|
+
Then %{I should successfully run "rake db:migrate"}
|
17
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
class String
|
2
|
+
|
3
|
+
class << self
|
4
|
+
def random(count = 6, ranges = [('a'..'z'),('A'..'Z'),('0'..'9')])
|
5
|
+
o = ranges.map{|i| i.to_a}.flatten;
|
6
|
+
string = (0..(count-1)).map{ o[rand(o.length)] }.join;
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def left(count)
|
11
|
+
self.slice(0,count)
|
12
|
+
end
|
13
|
+
|
14
|
+
def right(count)
|
15
|
+
self.slice(-count,count)
|
16
|
+
end
|
17
|
+
|
18
|
+
def left_trim
|
19
|
+
# remove leading whitespace
|
20
|
+
self.gsub(/^[\t\s]+/, '')
|
21
|
+
end
|
22
|
+
|
23
|
+
def right_trim
|
24
|
+
# remove trailing whitespace
|
25
|
+
self.gsub(/[\t\s]+$/, '')
|
26
|
+
end
|
27
|
+
|
28
|
+
def trim
|
29
|
+
# remove leading and trailing whitespace
|
30
|
+
self.left_trim.right_trim
|
31
|
+
end
|
32
|
+
|
33
|
+
# html = <<-stop.here_with_pipe
|
34
|
+
# |<!-- Begin: comment -->
|
35
|
+
# |<script type="text/javascript">
|
36
|
+
# stop
|
37
|
+
def here_with_pipe(linefeeds = false)
|
38
|
+
lines = self.split("\n")
|
39
|
+
lines.map! {|c| c.sub!(/\s*\|/, '')}
|
40
|
+
new_string = lines.join(linefeeds ? "\n" : " ")
|
41
|
+
self.replace(new_string)
|
42
|
+
end
|
43
|
+
|
44
|
+
def is_alpha_numeric?
|
45
|
+
regex = /^[a-zA-Z0-9]+$/
|
46
|
+
return (self =~ regex) == 0 ? true : false
|
47
|
+
end
|
48
|
+
|
49
|
+
def is_email_address?
|
50
|
+
# //Email address
|
51
|
+
# //Use this version to seek out email addresses in random documents and texts.
|
52
|
+
# //Does not match email addresses using an IP address instead of a domain name.
|
53
|
+
# //Does not match email addresses on new-fangled top-level domains with more than 4 letters such as .museum.
|
54
|
+
# //Including these increases the risk of false positives when applying the regex to random documents.
|
55
|
+
# '\b[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b'
|
56
|
+
#
|
57
|
+
# //Email address (anchored)
|
58
|
+
# //Use this anchored version to check if a valid email address was entered.
|
59
|
+
# //Does not match email addresses using an IP address instead of a domain name.
|
60
|
+
# //Does not match email addresses on new-fangled top-level domains with more than 4 letters such as .museum.
|
61
|
+
# //Requires the "case insensitive" option to be ON.
|
62
|
+
# '^[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$'
|
63
|
+
#
|
64
|
+
# //Email address (anchored; no consecutive dots)
|
65
|
+
# //Use this anchored version to check if a valid email address was entered.
|
66
|
+
# //Improves on the original email address regex by excluding addresses with consecutive dots such as john@aol...com
|
67
|
+
# //Does not match email addresses using an IP address instead of a domain name.
|
68
|
+
# //Does not match email addresses on new-fangled top-level domains with more than 4 letters such as .museum.
|
69
|
+
# //Including these increases the risk of false positives when applying the regex to random documents.
|
70
|
+
# '^[A-Z0-9._%-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}$'
|
71
|
+
#
|
72
|
+
# //Email address (no consecutive dots)
|
73
|
+
# //Use this version to seek out email addresses in random documents and texts.
|
74
|
+
# //Improves on the original email address regex by excluding addresses with consecutive dots such as john@aol...com
|
75
|
+
# //Does not match email addresses using an IP address instead of a domain name.
|
76
|
+
# //Does not match email addresses on new-fangled top-level domains with more than 4 letters such as .museum.
|
77
|
+
# //Including these increases the risk of false positives when applying the regex to random documents.
|
78
|
+
# '\b[A-Z0-9._%-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b'
|
79
|
+
#
|
80
|
+
# //Email address (specific TLDs)
|
81
|
+
# //Does not match email addresses using an IP address instead of a domain name.
|
82
|
+
# //Matches all country code top level domains, and specific common top level domains.
|
83
|
+
# '^[A-Z0-9._%-]+@[A-Z0-9.-]+\.(?:[A-Z]{2}|com|org|net|biz|info|name|aero|biz|info|jobs|museum|name)$'
|
84
|
+
#
|
85
|
+
# //Email address: Replace with HTML link
|
86
|
+
# '\b(?:mailto:)?([A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4})\b'
|
87
|
+
|
88
|
+
email_regex = %r{^[A-Z0-9._%-]+@[A-Z0-9.-]+\.(?:[A-Z]{2}|com|org|net|biz|info|name|aero|biz|info|jobs|museum|name)$}xi # Case insensitive
|
89
|
+
|
90
|
+
return (self =~ email_regex) == 0 ? true : false
|
91
|
+
end
|
92
|
+
|
93
|
+
def is_zipcode?
|
94
|
+
self =~ %r{^(\d{5})(-\d{4})?$}x ? true : false
|
95
|
+
end
|
96
|
+
|
97
|
+
def format_phone
|
98
|
+
'(' << slice(0..2) << ')' << slice(3..5) << '-' << slice(-4,4)
|
99
|
+
end
|
100
|
+
|
101
|
+
def is_numeric?
|
102
|
+
begin
|
103
|
+
Float(self)
|
104
|
+
rescue
|
105
|
+
false # not numeric
|
106
|
+
else
|
107
|
+
true # numeric
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def sanitize
|
112
|
+
clean_string = self.gsub(/[^a-z0-9,! \-\(\)\:\;\.\&\$]+/i, '')
|
113
|
+
#p "SAN: #{clean_string}"
|
114
|
+
clean_string
|
115
|
+
end
|
116
|
+
|
117
|
+
def shorten(count = 30)
|
118
|
+
if self.length >= count
|
119
|
+
shortened = self[0, count]
|
120
|
+
splitted = shortened.split(/\s/)
|
121
|
+
words = splitted.length
|
122
|
+
splitted[0, words-1].join(" ") + ' ...'
|
123
|
+
else
|
124
|
+
self
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
Feature: My Zipcode Gem
|
2
|
+
In order to manage zipcode resources
|
3
|
+
As a rails developer
|
4
|
+
I want to generate models for zipcode, county and state, and populate their tables
|
5
|
+
|
6
|
+
Scenario: Generate models and migration for zipcode, county and state
|
7
|
+
Given a new Rails app
|
8
|
+
Then I should see "my_zipcode_gem:models" when running "rails g"
|
9
|
+
When I run "rails g my_zipcode_gem:models"
|
10
|
+
Then I should see the following files
|
11
|
+
| app/models/zipcode.rb |
|
12
|
+
| app/models/state.rb |
|
13
|
+
| app/models/county.rb |
|
14
|
+
| lib/tasks/zipcodes.rake |
|
15
|
+
| db/migrate |
|
16
|
+
And I should see "gem "mocha", :group => :test" in file "Gemfile"
|
17
|
+
And I should successfully run "rake db:migrate"
|
18
|
+
|
19
|
+
Scenario: Update data for zipcodes, counties and states tables
|
20
|
+
Given a new migrated Rails app
|
21
|
+
Then I should successfully run "rake zipcodes:update"
|
22
|
+
And I should see 51 records in the "states" table
|
23
|
+
And I should see 3142 records in the "counties" table
|
24
|
+
And I should see 42366 records in the "zipcodes" table
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module MyZipcodeGem
|
2
|
+
class ModelsGenerator < Base
|
3
|
+
include Rails::Generators::Migration
|
4
|
+
|
5
|
+
source_root File.expand_path('../templates', __FILE__)
|
6
|
+
|
7
|
+
def initialize(*args, &block)
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def generate_models
|
12
|
+
# puts ">>> generate_zipcodes:"
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_gems
|
16
|
+
add_gem "mocha", :group => :test
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_models
|
20
|
+
template 'zipcode_model.rb', "app/models/zipcode.rb"
|
21
|
+
template 'county_model.rb', "app/models/county.rb"
|
22
|
+
template 'state_model.rb', "app/models/state.rb"
|
23
|
+
end
|
24
|
+
|
25
|
+
# Implement the required interface for Rails::Generators::Migration.
|
26
|
+
# taken from http://github.com/rails/rails/blob/master/activerecord/lib/generators/active_record.rb
|
27
|
+
def self.next_migration_number(dirname)
|
28
|
+
if ActiveRecord::Base.timestamped_migrations
|
29
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
30
|
+
else
|
31
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def create_migration
|
36
|
+
migration_template 'migration.rb', "db/migrate/create_my_zipcode_gem_models.rb"
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_rakefile
|
40
|
+
template 'zipcodes.rake', "lib/tasks/zipcodes.rake"
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# /Users/cblackburn/.rvm/gems/ruby-1.9.2-p136/gems/activerecord-3.0.3/lib/rails/generators/active_record/
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class County < ActiveRecord::Base
|
2
|
+
extend ActiveSupport::Memoizable
|
3
|
+
attr_accessible :state_id, :region_id, :abbr, :name, :count_seat
|
4
|
+
|
5
|
+
belongs_to :state
|
6
|
+
has_many :zipcodes
|
7
|
+
|
8
|
+
validates :name, :uniqueness => {:scope => :state_id, :case_sensitive => false}, :presence => true
|
9
|
+
|
10
|
+
scope :without_zipcodes, joins("LEFT JOIN zipcodes ON zipcodes.county_id = counties.id").where("zipcodes.county_id IS NULL")
|
11
|
+
scope :without_state, where("state_id IS NULL")
|
12
|
+
|
13
|
+
def cities
|
14
|
+
zipcodes.map(&:city).sort.uniq
|
15
|
+
end
|
16
|
+
memoize :cities
|
17
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
class CreateMyZipcodeGemModels < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
# Zipcodes Table
|
4
|
+
create_table :zipcodes do |t|
|
5
|
+
t.string :code
|
6
|
+
t.string :city
|
7
|
+
t.integer :state_id
|
8
|
+
t.integer :county_id
|
9
|
+
t.string :area_code
|
10
|
+
t.decimal :lat, :precision => 15, :scale => 10
|
11
|
+
t.decimal :lon, :precision => 15, :scale => 10
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
add_index :zipcodes, :code
|
15
|
+
add_index :zipcodes, :county_id
|
16
|
+
add_index :zipcodes, :state_id
|
17
|
+
add_index :zipcodes, [:lat, :lon]
|
18
|
+
|
19
|
+
# States Table
|
20
|
+
create_table :states do |t|
|
21
|
+
t.string :abbr, :limit => 2
|
22
|
+
t.string :name
|
23
|
+
t.timestamps
|
24
|
+
end
|
25
|
+
add_index :states, :abbr
|
26
|
+
|
27
|
+
# Counties Table
|
28
|
+
create_table :counties do |t|
|
29
|
+
t.integer :state_id
|
30
|
+
t.string :abbr
|
31
|
+
t.string :name
|
32
|
+
t.string :county_seat
|
33
|
+
t.timestamps
|
34
|
+
end
|
35
|
+
add_index :counties, :name
|
36
|
+
add_index :counties, :state_id
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.down
|
40
|
+
drop_table :counties
|
41
|
+
drop_table :states
|
42
|
+
drop_table :zipcodes
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class State < ActiveRecord::Base
|
2
|
+
extend ActiveSupport::Memoizable
|
3
|
+
attr_accessible :abbr, :name
|
4
|
+
|
5
|
+
has_many :zipcodes
|
6
|
+
has_many :counties
|
7
|
+
|
8
|
+
validates :abbr, :uniqueness => { :case_sensitive => false }, :presence => true
|
9
|
+
validates :name, :uniqueness => { :case_sensitive => false }, :presence => true
|
10
|
+
|
11
|
+
def cities
|
12
|
+
zipcodes.map(&:city).sort.uniq
|
13
|
+
end
|
14
|
+
memoize :cities
|
15
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Zipcode < ActiveRecord::Base
|
2
|
+
attr_accessible :code, :city, :state_id, :county_id, :lat, :lon
|
3
|
+
|
4
|
+
belongs_to :county
|
5
|
+
belongs_to :state
|
6
|
+
|
7
|
+
validates :code, :uniqueness => true, :presence => true
|
8
|
+
validates :state_id, :county_id, :city, :presence => true
|
9
|
+
|
10
|
+
scope :without_county, where("county_id IS NULL")
|
11
|
+
scope :without_state, where("state_id IS NULL")
|
12
|
+
scope :ungeocoded, where("lat IS NULL OR lon IS NULL")
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def find_by_city_state(city, state)
|
16
|
+
find(:first, :conditions => "city like '#{city}%' AND states.abbr like '%#{state}%'", :include => [:county => :state])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def latlon
|
21
|
+
[lat, lon]
|
22
|
+
end
|
23
|
+
|
24
|
+
def is_geocoded?
|
25
|
+
(!lat.nil? && !lon.nil?)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'fastercsv'
|
3
|
+
namespace :zipcodes do
|
4
|
+
|
5
|
+
desc "Update states table"
|
6
|
+
task :update_states => :environment do
|
7
|
+
puts ">>> Begin update of states table..."
|
8
|
+
url = "https://github.com/midwire/free_zipcode_data/raw/master/all_us_states.csv"
|
9
|
+
data = open(url)
|
10
|
+
file = nil
|
11
|
+
if data.is_a? StringIO
|
12
|
+
file = Tempfile.new('all_us_states.csv')
|
13
|
+
file.write(data.read)
|
14
|
+
file.flush
|
15
|
+
file.close
|
16
|
+
else
|
17
|
+
file = data
|
18
|
+
end
|
19
|
+
FasterCSV.foreach(file.path, :headers => true) do |row|
|
20
|
+
puts "Updating state: [#{row['name']}]"
|
21
|
+
state = State.find_or_initialize_by_abbr(row['abbr'])
|
22
|
+
state.update_attribute(:name, row['name'])
|
23
|
+
end
|
24
|
+
data.close
|
25
|
+
puts ">>> End update of states table..."
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Update counties table"
|
29
|
+
task :update_counties => :update_states do
|
30
|
+
puts ">>> Begin update of counties table..."
|
31
|
+
url = "https://github.com/midwire/free_zipcode_data/raw/master/all_us_counties.csv"
|
32
|
+
data = open(url)
|
33
|
+
file = nil
|
34
|
+
if data.is_a? StringIO
|
35
|
+
file = Tempfile.new('all_us_counties.csv')
|
36
|
+
file.write(data.read)
|
37
|
+
file.flush
|
38
|
+
file.close
|
39
|
+
else
|
40
|
+
file = data
|
41
|
+
end
|
42
|
+
FasterCSV.foreach(file.path, :headers => true) do |row|
|
43
|
+
puts "Updating county: [#{row['name']}]"
|
44
|
+
# lookup state
|
45
|
+
state = State.find_by_abbr!(row['state'])
|
46
|
+
county = County.find_or_initialize_by_name_and_state_id(row['name'], state.to_param)
|
47
|
+
county.update_attribute(:county_seat, row['county_seat'])
|
48
|
+
end
|
49
|
+
data.close
|
50
|
+
puts ">>> End update of counties table..."
|
51
|
+
end
|
52
|
+
|
53
|
+
desc "Update zipcodes table"
|
54
|
+
task :update_zipcodes => :update_counties do
|
55
|
+
puts ">>> Begin update of zipcodes table..."
|
56
|
+
url = "https://github.com/midwire/free_zipcode_data/raw/master/all_us_zipcodes.csv"
|
57
|
+
data = open(url)
|
58
|
+
file = nil
|
59
|
+
if data.is_a? StringIO
|
60
|
+
file = Tempfile.new('all_us_zipcodes.csv')
|
61
|
+
file.write(data.read)
|
62
|
+
file.flush
|
63
|
+
file.close
|
64
|
+
else
|
65
|
+
file = data
|
66
|
+
end
|
67
|
+
FasterCSV.foreach(file.path, :headers => true) do |row|
|
68
|
+
puts "Updating zipcode: [#{row['code']}], '#{row['city']}, #{row['state']}, #{row['county']}"
|
69
|
+
# lookup state
|
70
|
+
state = State.find_by_abbr!(row['state'])
|
71
|
+
begin
|
72
|
+
county = County.find_by_name_and_state_id!(row['county'], state.to_param)
|
73
|
+
rescue Exception => e
|
74
|
+
puts ">>> e: [#{e}]"
|
75
|
+
puts ">>>> No county found for zipcode: [#{row['code']}], '#{row['city']}, #{row['state']}, #{row['county']}... SKIPPING..."
|
76
|
+
next
|
77
|
+
end
|
78
|
+
zipcode = Zipcode.find_or_initialize_by_code(row['code'])
|
79
|
+
zipcode.update_attributes!(
|
80
|
+
:city => row['city'].titleize,
|
81
|
+
:state_id => state.to_param,
|
82
|
+
:county_id => county.to_param,
|
83
|
+
:lat => row['lat'],
|
84
|
+
:lon => row['lon']
|
85
|
+
)
|
86
|
+
end
|
87
|
+
data.close
|
88
|
+
puts ">>> End update of zipcodes table..."
|
89
|
+
end
|
90
|
+
|
91
|
+
desc "Populate or update the zipcodes related tables"
|
92
|
+
task :update => :environment do
|
93
|
+
Rake::Task['zipcodes:update_zipcodes'].invoke
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rails/generators/base'
|
2
|
+
|
3
|
+
module MyZipcodeGem
|
4
|
+
class Base < Rails::Generators::Base #:nodoc:
|
5
|
+
# def self.source_root
|
6
|
+
# @_source_root ||= File.expand_path(File.join(File.dirname(__FILE__), 'nifty', generator_name, 'templates'))
|
7
|
+
# puts ">>> @_source_root: [#{@_source_root}]"
|
8
|
+
# @_source_root
|
9
|
+
# end
|
10
|
+
|
11
|
+
def self.banner
|
12
|
+
"rails generate my_zipcode_gem:#{generator_name} #{self.arguments.map{ |a| a.usage }.join(' ')} [options]"
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def add_gem(name, options = {})
|
18
|
+
gemfile_content = File.read(destination_path("Gemfile"))
|
19
|
+
File.open(destination_path("Gemfile"), 'a') { |f| f.write("\n") } unless gemfile_content =~ /\n\Z/
|
20
|
+
gem name, options unless gemfile_content.include? name
|
21
|
+
end
|
22
|
+
|
23
|
+
def print_usage
|
24
|
+
self.class.help(Thor::Base.shell.new)
|
25
|
+
exit
|
26
|
+
end
|
27
|
+
|
28
|
+
def destination_path(path)
|
29
|
+
File.join(destination_root, path)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "generators/my_zipcode_gem/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "my_zipcode_gem"
|
7
|
+
s.version = MyZipcodeGem::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Chris Blackburn"]
|
10
|
+
s.email = ["chris [at] midwiretech [dot] com"]
|
11
|
+
s.homepage = "https://github.com/midwire/my_zipcode_gem"
|
12
|
+
s.summary = %q{A Ruby gem to handle all things zipcode.}
|
13
|
+
s.description = %q{A Ruby gem for looking up and manipulating US postal codes and geocodes.}
|
14
|
+
|
15
|
+
s.rubyforge_project = "my_zipcode_gem"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_dependency('rails', '3.0.3')
|
23
|
+
s.add_dependency('rubigen', '1.5.6')
|
24
|
+
s.add_dependency('fastercsv')
|
25
|
+
|
26
|
+
s.add_development_dependency('sqlite3-ruby')
|
27
|
+
s.add_development_dependency('shoulda', '2.11.3')
|
28
|
+
s.add_development_dependency('rspec')
|
29
|
+
s.add_development_dependency('rspec-rails')
|
30
|
+
s.add_development_dependency('cucumber')
|
31
|
+
s.add_development_dependency('cucumber-rails')
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: my_zipcode_gem
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 25
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 1
|
10
|
+
version: 0.1.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Chris Blackburn
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-05-03 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: rails
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - "="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 1
|
30
|
+
segments:
|
31
|
+
- 3
|
32
|
+
- 0
|
33
|
+
- 3
|
34
|
+
version: 3.0.3
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rubigen
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - "="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 15
|
46
|
+
segments:
|
47
|
+
- 1
|
48
|
+
- 5
|
49
|
+
- 6
|
50
|
+
version: 1.5.6
|
51
|
+
type: :runtime
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: fastercsv
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 3
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
version: "0"
|
65
|
+
type: :runtime
|
66
|
+
version_requirements: *id003
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: sqlite3-ruby
|
69
|
+
prerelease: false
|
70
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
hash: 3
|
76
|
+
segments:
|
77
|
+
- 0
|
78
|
+
version: "0"
|
79
|
+
type: :development
|
80
|
+
version_requirements: *id004
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: shoulda
|
83
|
+
prerelease: false
|
84
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - "="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
hash: 37
|
90
|
+
segments:
|
91
|
+
- 2
|
92
|
+
- 11
|
93
|
+
- 3
|
94
|
+
version: 2.11.3
|
95
|
+
type: :development
|
96
|
+
version_requirements: *id005
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec
|
99
|
+
prerelease: false
|
100
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
101
|
+
none: false
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
hash: 3
|
106
|
+
segments:
|
107
|
+
- 0
|
108
|
+
version: "0"
|
109
|
+
type: :development
|
110
|
+
version_requirements: *id006
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec-rails
|
113
|
+
prerelease: false
|
114
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
hash: 3
|
120
|
+
segments:
|
121
|
+
- 0
|
122
|
+
version: "0"
|
123
|
+
type: :development
|
124
|
+
version_requirements: *id007
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: cucumber
|
127
|
+
prerelease: false
|
128
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
hash: 3
|
134
|
+
segments:
|
135
|
+
- 0
|
136
|
+
version: "0"
|
137
|
+
type: :development
|
138
|
+
version_requirements: *id008
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: cucumber-rails
|
141
|
+
prerelease: false
|
142
|
+
requirement: &id009 !ruby/object:Gem::Requirement
|
143
|
+
none: false
|
144
|
+
requirements:
|
145
|
+
- - ">="
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
hash: 3
|
148
|
+
segments:
|
149
|
+
- 0
|
150
|
+
version: "0"
|
151
|
+
type: :development
|
152
|
+
version_requirements: *id009
|
153
|
+
description: A Ruby gem for looking up and manipulating US postal codes and geocodes.
|
154
|
+
email:
|
155
|
+
- chris [at] midwiretech [dot] com
|
156
|
+
executables: []
|
157
|
+
|
158
|
+
extensions: []
|
159
|
+
|
160
|
+
extra_rdoc_files: []
|
161
|
+
|
162
|
+
files:
|
163
|
+
- .gitignore
|
164
|
+
- Gemfile
|
165
|
+
- README.textile
|
166
|
+
- Rakefile
|
167
|
+
- features/step_definitions/common_steps.rb
|
168
|
+
- features/step_definitions/rails_setup_steps.rb
|
169
|
+
- features/support/env.rb
|
170
|
+
- features/support/string.rb
|
171
|
+
- features/zipcodes.feature
|
172
|
+
- lib/generators/my_zipcode_gem/models_generator.rb
|
173
|
+
- lib/generators/my_zipcode_gem/templates/county_model.rb
|
174
|
+
- lib/generators/my_zipcode_gem/templates/migration.rb
|
175
|
+
- lib/generators/my_zipcode_gem/templates/state_model.rb
|
176
|
+
- lib/generators/my_zipcode_gem/templates/zipcode_model.rb
|
177
|
+
- lib/generators/my_zipcode_gem/templates/zipcodes.rake
|
178
|
+
- lib/generators/my_zipcode_gem/version.rb
|
179
|
+
- lib/my_zipcode_gem.rb
|
180
|
+
- my_zipcode_gem.gemspec
|
181
|
+
has_rdoc: true
|
182
|
+
homepage: https://github.com/midwire/my_zipcode_gem
|
183
|
+
licenses: []
|
184
|
+
|
185
|
+
post_install_message:
|
186
|
+
rdoc_options: []
|
187
|
+
|
188
|
+
require_paths:
|
189
|
+
- lib
|
190
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
191
|
+
none: false
|
192
|
+
requirements:
|
193
|
+
- - ">="
|
194
|
+
- !ruby/object:Gem::Version
|
195
|
+
hash: 3
|
196
|
+
segments:
|
197
|
+
- 0
|
198
|
+
version: "0"
|
199
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
200
|
+
none: false
|
201
|
+
requirements:
|
202
|
+
- - ">="
|
203
|
+
- !ruby/object:Gem::Version
|
204
|
+
hash: 3
|
205
|
+
segments:
|
206
|
+
- 0
|
207
|
+
version: "0"
|
208
|
+
requirements: []
|
209
|
+
|
210
|
+
rubyforge_project: my_zipcode_gem
|
211
|
+
rubygems_version: 1.6.2
|
212
|
+
signing_key:
|
213
|
+
specification_version: 3
|
214
|
+
summary: A Ruby gem to handle all things zipcode.
|
215
|
+
test_files:
|
216
|
+
- features/step_definitions/common_steps.rb
|
217
|
+
- features/step_definitions/rails_setup_steps.rb
|
218
|
+
- features/support/env.rb
|
219
|
+
- features/support/string.rb
|
220
|
+
- features/zipcodes.feature
|