my_zipcode_gem 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|