has_addresses 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +19 -0
- data/MIT-LICENSE +20 -0
- data/README +111 -0
- data/Rakefile +82 -0
- data/app/models/address.rb +84 -0
- data/app/models/country.rb +114 -0
- data/app/models/region.rb +112 -0
- data/db/migrate/001_create_countries.rb +17 -0
- data/db/migrate/002_create_regions.rb +15 -0
- data/db/migrate/003_create_addresses.rb +21 -0
- data/init.rb +1 -0
- data/lib/has_addresses.rb +74 -0
- data/tasks/has_addresses_tasks.rake +23 -0
- data/test/app_root/app/models/company.rb +7 -0
- data/test/app_root/config/environment.rb +30 -0
- data/test/app_root/db/migrate/001_create_companies.rb +11 -0
- data/test/app_root/db/migrate/002_add_address_kinds.rb +9 -0
- data/test/app_root/test/fixtures/addresses.yml +47 -0
- data/test/app_root/test/fixtures/companies.yml +7 -0
- data/test/app_root/test/fixtures/countries.yml +12 -0
- data/test/app_root/test/fixtures/regions.yml +5 -0
- data/test/files/iso_3166.xml +1719 -0
- data/test/files/iso_3166_2.xml +8617 -0
- data/test/test_helper.rb +11 -0
- data/test/unit/address_test.rb +171 -0
- data/test/unit/country_test.rb +119 -0
- data/test/unit/has_addresses_test.rb +15 -0
- data/test/unit/region_test.rb +101 -0
- metadata +88 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
*SVN*
|
2
|
+
|
3
|
+
*0.0.1* (August 21st, 2007)
|
4
|
+
|
5
|
+
* Add documentation
|
6
|
+
|
7
|
+
* Add descriptive output for rake tasks
|
8
|
+
|
9
|
+
* Remove dependency on has_association_helper
|
10
|
+
|
11
|
+
* Remove default bootstrap files in favor of creating new ones through the bootstrap tasks
|
12
|
+
|
13
|
+
* Add tests for rake tests
|
14
|
+
|
15
|
+
* Fix not allowing single character abbreviations for regions
|
16
|
+
|
17
|
+
* Add countries:create_fixtures, countries:bootstrap, regions:create_fixtures, and regions:bootstrap files
|
18
|
+
|
19
|
+
* Refactor Rake code into Country/Region classes
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2006-2007 Aaron Pfeifer & Neil Abraham
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
= has_addresses
|
2
|
+
|
3
|
+
has_addresses adds a base skeleton for handling countries, regions, and
|
4
|
+
addresses.
|
5
|
+
|
6
|
+
== Resources
|
7
|
+
|
8
|
+
API
|
9
|
+
|
10
|
+
* http://api.pluginaweek.org/has_addresses
|
11
|
+
|
12
|
+
Wiki
|
13
|
+
|
14
|
+
* http://wiki.pluginaweek.org/Has_addresses
|
15
|
+
|
16
|
+
Announcement
|
17
|
+
|
18
|
+
* http://www.pluginaweek.org/
|
19
|
+
|
20
|
+
Source
|
21
|
+
|
22
|
+
* http://svn.pluginaweek.org/trunk/plugins/active_record/has/has_addresses
|
23
|
+
|
24
|
+
Development
|
25
|
+
|
26
|
+
* http://dev.pluginaweek.org/browser/trunk/plugins/active_record/has/has_addresses
|
27
|
+
|
28
|
+
== Description
|
29
|
+
|
30
|
+
Countries, regions, and addresses are all simple models whose data and
|
31
|
+
functionality should be able to be standardized across multiple applications.
|
32
|
+
has_addresses adds support for countries and regions based on the ISO 3166 and
|
33
|
+
ISO 3166-2 standard. The data for these standards is obtained through the
|
34
|
+
open-source Debian package, iso-codes.
|
35
|
+
|
36
|
+
Along with the simple Country and Region models, addresses can be defined and
|
37
|
+
integrated based on the data in these models. Addresses are minimalistic in
|
38
|
+
terms of the type of data required and follows the standard U.S. format.
|
39
|
+
|
40
|
+
=== Running migrations
|
41
|
+
|
42
|
+
To migrate the tables required for has_addresses, you can either run the
|
43
|
+
migration from the command line like so:
|
44
|
+
|
45
|
+
rake db:migrate:plugins PLUGIN=has_addresses
|
46
|
+
|
47
|
+
or (more ideally) generate a migration file that will integrate into your main
|
48
|
+
application's migration path:
|
49
|
+
|
50
|
+
ruby script/generate plugin_migration MigrateHasAddressesToVersion3
|
51
|
+
|
52
|
+
=== Bootstrapping the database
|
53
|
+
|
54
|
+
has_addresses comes bundled with tasks for bootstrapping the countries and
|
55
|
+
regions table based on the ISO 3166 and ISO 3166-2 standard. You can bootstrap
|
56
|
+
the database using built-in rake tasks or by creating fixtures which contain the
|
57
|
+
bootstrap data.
|
58
|
+
|
59
|
+
To bootstrap country/region data into the current database schema:
|
60
|
+
|
61
|
+
$ rake countries:bootstrap
|
62
|
+
(in /my/project)
|
63
|
+
Downloading ISO 3166 data...
|
64
|
+
Parsing countries...
|
65
|
+
Loading countries into database...
|
66
|
+
Done!
|
67
|
+
|
68
|
+
$ rake regions:bootstrap
|
69
|
+
Downloading ISO 3166 data...
|
70
|
+
Parsing countries...
|
71
|
+
Downloading ISO 3166-2 data...
|
72
|
+
Parsing regions...
|
73
|
+
Loading regions into database...
|
74
|
+
Done!
|
75
|
+
|
76
|
+
To create fixtures for the country/region bootstrap data:
|
77
|
+
|
78
|
+
$ rake countries:create_fixtures
|
79
|
+
(in /my/project)
|
80
|
+
Downloading ISO 3166 data...
|
81
|
+
Parsing countries...
|
82
|
+
Saving countries to /my/project/config/../db/bootstrap/countries.yml...
|
83
|
+
Done!
|
84
|
+
|
85
|
+
$ rake regions:create_fixtures
|
86
|
+
(in /my/project)
|
87
|
+
Downloading ISO 3166 data...
|
88
|
+
Parsing countries...
|
89
|
+
Downloading ISO 3166-2 data...
|
90
|
+
Parsing regions...
|
91
|
+
Saving regions to /my/project/config/../db/bootstrap/regions.yml...
|
92
|
+
Done!
|
93
|
+
|
94
|
+
== Testing
|
95
|
+
|
96
|
+
Since the rake tasks for installing TinyMCE and updating the configuration
|
97
|
+
options are part of the unit tests, already-downloaded files are included with
|
98
|
+
the plugin. If you want to perform a "live" test which actually downloads the
|
99
|
+
files off the Internet (rather than using the local versions), you must set
|
100
|
+
the LIVE environment variable to true. For example,
|
101
|
+
|
102
|
+
rake test LIVE=true
|
103
|
+
|
104
|
+
The following plugins/gems must be installed before any tests can be run:
|
105
|
+
* plugin_dependencies - http://wiki.pluginaweekk.org/Plugin_dependencies
|
106
|
+
* loaded_plugins - http://wiki.pluginaweek.org/Loaded_plugins
|
107
|
+
* appable_plugins - http://wiki.pluginaweek.org/Appable_plugins
|
108
|
+
|
109
|
+
== Dependencies
|
110
|
+
|
111
|
+
This plugin does not depend on the presence of any other plugin.
|
data/Rakefile
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rake/contrib/sshpublisher'
|
5
|
+
|
6
|
+
# Load custom rakefile extensions
|
7
|
+
Dir["#{File.dirname(__FILE__)}/tasks/**/*.rake"].sort.each {|ext| load ext}
|
8
|
+
|
9
|
+
PKG_NAME = 'has_addresses'
|
10
|
+
PKG_VERSION = '0.0.1'
|
11
|
+
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
12
|
+
RUBY_FORGE_PROJECT = 'pluginaweek'
|
13
|
+
|
14
|
+
desc 'Default: run unit tests.'
|
15
|
+
task :default => :test
|
16
|
+
|
17
|
+
desc 'Test the has_addresses plugin.'
|
18
|
+
Rake::TestTask.new(:test) do |t|
|
19
|
+
t.libs << 'lib'
|
20
|
+
t.pattern = 'test/unit/**/*_test.rb'
|
21
|
+
t.verbose = true
|
22
|
+
end
|
23
|
+
|
24
|
+
desc 'Generate documentation for the has_addresses plugin.'
|
25
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
26
|
+
rdoc.rdoc_dir = 'rdoc'
|
27
|
+
rdoc.title = 'HasAddresses'
|
28
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
29
|
+
rdoc.rdoc_files.include('README')
|
30
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
31
|
+
end
|
32
|
+
|
33
|
+
spec = Gem::Specification.new do |s|
|
34
|
+
s.name = PKG_NAME
|
35
|
+
s.version = PKG_VERSION
|
36
|
+
s.platform = Gem::Platform::RUBY
|
37
|
+
s.summary = 'Adds a base skeleton for handling countries, regions, and addresses.'
|
38
|
+
|
39
|
+
s.files = FileList['{app,db,lib,tasks,test}/**/*'].to_a + %w(CHANGELOG init.rb MIT-LICENSE Rakefile README)
|
40
|
+
s.require_path = 'lib'
|
41
|
+
s.autorequire = 'has_addresses'
|
42
|
+
s.has_rdoc = true
|
43
|
+
s.test_files = Dir['test/unit/**/*_test.rb']
|
44
|
+
|
45
|
+
s.author = 'Aaron Pfeifer, Neil Abraham'
|
46
|
+
s.email = 'info@pluginaweek.org'
|
47
|
+
s.homepage = 'http://www.pluginaweek.org'
|
48
|
+
end
|
49
|
+
|
50
|
+
Rake::GemPackageTask.new(spec) do |p|
|
51
|
+
p.gem_spec = spec
|
52
|
+
p.need_tar = true
|
53
|
+
p.need_zip = true
|
54
|
+
end
|
55
|
+
|
56
|
+
desc 'Publish the beta gem'
|
57
|
+
task :pgem => [:package] do
|
58
|
+
Rake::SshFilePublisher.new('pluginaweek@pluginaweek.org', '/home/pluginaweek/gems.pluginaweek.org/gems', 'pkg', "#{PKG_FILE_NAME}.gem").upload
|
59
|
+
end
|
60
|
+
|
61
|
+
desc 'Publish the API documentation'
|
62
|
+
task :pdoc => [:rdoc] do
|
63
|
+
Rake::SshDirPublisher.new('pluginaweek@pluginaweek.org', "/home/pluginaweek/api.pluginaweek.org/#{PKG_NAME}", 'rdoc').upload
|
64
|
+
end
|
65
|
+
|
66
|
+
desc 'Publish the API docs and gem'
|
67
|
+
task :publish => [:pdoc, :release]
|
68
|
+
|
69
|
+
desc 'Publish the release files to RubyForge.'
|
70
|
+
task :release => [:gem, :package] do
|
71
|
+
require 'rubyforge'
|
72
|
+
|
73
|
+
ruby_forge = RubyForge.new
|
74
|
+
ruby_forge.login
|
75
|
+
|
76
|
+
%w( gem tgz zip ).each do |ext|
|
77
|
+
file = "pkg/#{PKG_FILE_NAME}.#{ext}"
|
78
|
+
puts "Releasing #{File.basename(file)}..."
|
79
|
+
|
80
|
+
ruby_forge.add_release(RUBY_FORGE_PROJECT, PKG_NAME, PKG_VERSION, file)
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# Represents a mailing address
|
2
|
+
class Address < ActiveRecord::Base
|
3
|
+
belongs_to :addressable,
|
4
|
+
:polymorphic => true
|
5
|
+
belongs_to :region
|
6
|
+
belongs_to :country
|
7
|
+
|
8
|
+
validates_presence_of :addressable_id,
|
9
|
+
:addressable_type,
|
10
|
+
:street_1,
|
11
|
+
:city,
|
12
|
+
:postal_code
|
13
|
+
validates_presence_of :region_id,
|
14
|
+
:if => :known_region_required?
|
15
|
+
validates_presence_of :custom_region,
|
16
|
+
:if => :custom_region_required?
|
17
|
+
validates_format_of :postal_code,
|
18
|
+
:with => /^[0-9]{5}$/,
|
19
|
+
:allow_nil => true
|
20
|
+
|
21
|
+
before_save :ensure_exclusive_references
|
22
|
+
|
23
|
+
# Returns the region's country if the region is specified
|
24
|
+
def country_with_region_check
|
25
|
+
region ? region.country : country_without_region_check
|
26
|
+
end
|
27
|
+
alias_method_chain :country, :region_check
|
28
|
+
|
29
|
+
# Gets the name of the region that this address is for (whether it is a custom or
|
30
|
+
# stored region in the database)
|
31
|
+
def region_name
|
32
|
+
custom_region || (region ? region.name : nil)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Gets the value of the address on a single line
|
36
|
+
def single_line
|
37
|
+
multi_line.join(', ')
|
38
|
+
end
|
39
|
+
|
40
|
+
# Gets the value of the address on multiple lines
|
41
|
+
def multi_line
|
42
|
+
lines = []
|
43
|
+
lines << street_1 if street_1?
|
44
|
+
lines << street_2 if street_2?
|
45
|
+
|
46
|
+
line = ''
|
47
|
+
line << city if city?
|
48
|
+
if region_name
|
49
|
+
line << ', ' if !line.blank?
|
50
|
+
line << region_name
|
51
|
+
end
|
52
|
+
if postal_code?
|
53
|
+
line << ' ' if !line.blank?
|
54
|
+
line << postal_code
|
55
|
+
end
|
56
|
+
lines << line if !line.blank?
|
57
|
+
|
58
|
+
lines << country.name if country
|
59
|
+
lines
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def known_region_required?
|
64
|
+
country.nil? || country.regions.count != 0
|
65
|
+
end
|
66
|
+
|
67
|
+
# A custom region name is required if a known region was not specified and
|
68
|
+
# the country in which this address resides has no known regions in the
|
69
|
+
# database
|
70
|
+
def custom_region_required?
|
71
|
+
region_id.nil? && country && country.regions.count == 0
|
72
|
+
end
|
73
|
+
|
74
|
+
# Ensures that the country id/user region combo is not set at the same time as
|
75
|
+
# the region id
|
76
|
+
def ensure_exclusive_references
|
77
|
+
if known_region_required?
|
78
|
+
self.country_id = nil
|
79
|
+
self.custom_region = nil
|
80
|
+
end
|
81
|
+
|
82
|
+
true
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'cgi'
|
3
|
+
require 'rexml/document'
|
4
|
+
|
5
|
+
# Defined by the ISO 3166 standard. The ISO 3166 standard includes a
|
6
|
+
# "Country Subdivision Code", giving a code for the names of the principal
|
7
|
+
# administrative subdivisions of the countries coded in ISO 3166.
|
8
|
+
class Country < ActiveRecord::Base
|
9
|
+
# The url of the open source version of the ISO 3166 standard
|
10
|
+
ISO_3166_URL = 'http://svn.debian.org/wsvn/pkg-isocodes/trunk/iso-codes/iso_3166/iso_3166.xml?op=file'
|
11
|
+
|
12
|
+
class << self
|
13
|
+
# Creates a fixtures file containing all possible countries
|
14
|
+
def create_fixtures(output_file_path = nil)
|
15
|
+
output_file_path ||= 'db/bootstrap/countries.yml'
|
16
|
+
output_file_path = File.join(RAILS_ROOT, output_file_path)
|
17
|
+
|
18
|
+
FileUtils.mkdir_p(File.dirname(output_file_path))
|
19
|
+
output_file = File.new(output_file_path, File::CREAT|File::TRUNC|File::RDWR)
|
20
|
+
output = ''
|
21
|
+
|
22
|
+
countries = download_all
|
23
|
+
puts "Saving countries to #{output_file_path}..." if PluginAWeek::Has::Addresses.verbose
|
24
|
+
countries.each do |country|
|
25
|
+
record_name = country.name.gsub(' ', '_').gsub(/[^A-Za-z_]/, '').downcase
|
26
|
+
|
27
|
+
output << "#{record_name}:\n"
|
28
|
+
country.attributes.each do |attr, value|
|
29
|
+
output << " #{attr}: #{value}\n" if value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
output_file << output.slice(0..output.length-2)
|
34
|
+
output_file.close
|
35
|
+
|
36
|
+
puts 'Done!' if PluginAWeek::Has::Addresses.verbose
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
# Bootstraps the table by downloading the latest ISO 3166 standard and
|
41
|
+
# saving each country to the database
|
42
|
+
def bootstrap
|
43
|
+
countries = download_all
|
44
|
+
|
45
|
+
puts 'Loading countries into database...' if PluginAWeek::Has::Addresses.verbose
|
46
|
+
delete_all
|
47
|
+
countries.each {|country| country.save!}
|
48
|
+
|
49
|
+
puts 'Done!' if PluginAWeek::Has::Addresses.verbose
|
50
|
+
true
|
51
|
+
end
|
52
|
+
|
53
|
+
# Downloads the latest ISO 3166 standard and returns the data as a list of
|
54
|
+
# countries
|
55
|
+
def download_all
|
56
|
+
# Download the standard
|
57
|
+
puts 'Downloading ISO 3166 data...' if PluginAWeek::Has::Addresses.verbose
|
58
|
+
iso_3166 = open(ISO_3166_URL).readlines * "\n"
|
59
|
+
iso_3166 = CGI::unescapeHTML(/<pre>(.+)<\/pre>/im.match(iso_3166)[1].gsub(' ', ''))
|
60
|
+
|
61
|
+
# Parse and load the countries
|
62
|
+
puts 'Parsing countries...' if PluginAWeek::Has::Addresses.verbose
|
63
|
+
countries = []
|
64
|
+
REXML::Document.new(iso_3166).elements.each('*/iso_3166_entry') do |country|
|
65
|
+
name = country.attributes['name'].to_s.gsub("'","\\'")
|
66
|
+
official_name = country.attributes['official_name']
|
67
|
+
official_name = official_name.to_s.gsub!("'", "\\'") if official_name
|
68
|
+
alpha_2_code = country.attributes['alpha_2_code'].to_s.upcase
|
69
|
+
alpha_3_code = country.attributes['alpha_3_code'].to_s.upcase
|
70
|
+
country_id = country.attributes['numeric_code'].to_s.to_i
|
71
|
+
record_name = name.gsub(' ', '_').gsub(/[^A-Za-z_]/, '').downcase
|
72
|
+
|
73
|
+
country = new(
|
74
|
+
:name => name,
|
75
|
+
:official_name => official_name,
|
76
|
+
:alpha_2_code => alpha_2_code,
|
77
|
+
:alpha_3_code => alpha_3_code
|
78
|
+
)
|
79
|
+
country.id = country_id
|
80
|
+
countries << country
|
81
|
+
end
|
82
|
+
|
83
|
+
countries
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
has_many :regions
|
88
|
+
|
89
|
+
validates_presence_of :name,
|
90
|
+
:alpha_2_code,
|
91
|
+
:alpha_3_code
|
92
|
+
validates_uniqueness_of :name,
|
93
|
+
:alpha_2_code,
|
94
|
+
:alpha_3_code
|
95
|
+
validates_length_of :name,
|
96
|
+
:within => 2..80
|
97
|
+
validates_length_of :alpha_2_code,
|
98
|
+
:is => 2
|
99
|
+
validates_length_of :alpha_3_code,
|
100
|
+
:is => 3
|
101
|
+
|
102
|
+
alias_attribute :abbreviation_2, :alpha_2_code
|
103
|
+
alias_attribute :abbreviation_3, :alpha_3_code
|
104
|
+
|
105
|
+
# The official name of the country
|
106
|
+
def official_name
|
107
|
+
read_attribute(:official_name) || name
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns the name of the country
|
111
|
+
def to_s #:nodoc
|
112
|
+
name
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'cgi'
|
3
|
+
require 'rexml/document'
|
4
|
+
|
5
|
+
# Defined by the ISO 3166-2 standard. This is a standard that gives short codes
|
6
|
+
# for provinces, etc. within a country.
|
7
|
+
class Region < ActiveRecord::Base
|
8
|
+
# The url of the open source version of the ISO 3166-2 standard
|
9
|
+
ISO_3166_2_URL = 'http://svn.debian.org/wsvn/pkg-isocodes/trunk/iso-codes/iso_3166/iso_3166_2/iso_3166_2.xml?op=file'
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Creates a fixtures containing all possible regions
|
13
|
+
def create_fixtures(output_file_path = nil)
|
14
|
+
output_file_path ||= 'db/bootstrap/regions.yml'
|
15
|
+
output_file_path = File.join(RAILS_ROOT, output_file_path)
|
16
|
+
|
17
|
+
FileUtils.mkdir_p(File.dirname(output_file_path))
|
18
|
+
output_file = File.new(output_file_path, File::CREAT|File::TRUNC|File::RDWR)
|
19
|
+
output = ''
|
20
|
+
|
21
|
+
regions = download_all
|
22
|
+
puts "Saving regions to #{output_file_path}..." if PluginAWeek::Has::Addresses.verbose
|
23
|
+
regions.each do |region|
|
24
|
+
record_name = region.name.gsub(' ', '_').gsub(/[^A-Za-z_]/, '').downcase
|
25
|
+
|
26
|
+
output << "#{record_name}:\n"
|
27
|
+
region.attributes.each do |attr, value|
|
28
|
+
output << " #{attr}: #{value}\n" if value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
output_file << output.slice(0..output.length-2)
|
33
|
+
output_file.close
|
34
|
+
|
35
|
+
puts 'Done!' if PluginAWeek::Has::Addresses.verbose
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
# Bootstraps the table by downloading the latest ISO 3166-2 standard and
|
40
|
+
# saving each region to the database
|
41
|
+
def bootstrap
|
42
|
+
regions = download_all
|
43
|
+
|
44
|
+
puts 'Loading regions into database...' if PluginAWeek::Has::Addresses.verbose
|
45
|
+
delete_all
|
46
|
+
regions.each {|region| region.save!}
|
47
|
+
|
48
|
+
puts 'Done!' if PluginAWeek::Has::Addresses.verbose
|
49
|
+
true
|
50
|
+
end
|
51
|
+
|
52
|
+
# Downloads the latest ISO 3166-2 standard and returns the data as a list of
|
53
|
+
# regions
|
54
|
+
def download_all
|
55
|
+
countries = Country.download_all
|
56
|
+
alpha_2_code_to_country = Hash[*countries.collect {|c| [c.alpha_2_code, c]}.flatten]
|
57
|
+
|
58
|
+
# Download the standard
|
59
|
+
puts 'Downloading ISO 3166-2 data...' if PluginAWeek::Has::Addresses.verbose
|
60
|
+
iso_3166_2 = open(ISO_3166_2_URL).readlines * "\n"
|
61
|
+
iso_3166_2 = CGI::unescapeHTML(/<pre>(.+)<\/pre>/im.match(iso_3166_2)[1].gsub(' ', ''))
|
62
|
+
|
63
|
+
# Parse and load the regions
|
64
|
+
puts 'Parsing regions...' if PluginAWeek::Has::Addresses.verbose
|
65
|
+
region_id = 1
|
66
|
+
regions = []
|
67
|
+
REXML::Document.new(iso_3166_2).elements.each('*/iso_3166_country') do |country|
|
68
|
+
code = country.attributes['code'][0..1] # Belarius is more than 2 characters for some reason, so just get the first 2
|
69
|
+
country_record = alpha_2_code_to_country[code.upcase]
|
70
|
+
|
71
|
+
country.elements.each('iso_3166_2_entry') do |region|
|
72
|
+
name = region.attributes['name'].to_s.gsub("'","\\'").strip
|
73
|
+
abbreviation = region.attributes['code'].upcase.sub("#{code}-", '')
|
74
|
+
|
75
|
+
region = new(
|
76
|
+
:country_id => country_record.id,
|
77
|
+
:name => name,
|
78
|
+
:abbreviation => abbreviation
|
79
|
+
)
|
80
|
+
region.id = region_id
|
81
|
+
|
82
|
+
if !country_record.regions.any? {|known_region| known_region.name == region.name}
|
83
|
+
regions << region
|
84
|
+
country_record.regions << region
|
85
|
+
region_id += 1
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
regions
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
belongs_to :country
|
95
|
+
has_many :addresses
|
96
|
+
|
97
|
+
validates_presence_of :name,
|
98
|
+
:country_id,
|
99
|
+
:abbreviation
|
100
|
+
validates_length_of :name,
|
101
|
+
:within => 2..80
|
102
|
+
validates_length_of :abbreviation,
|
103
|
+
:within => 1..5
|
104
|
+
validates_uniqueness_of :name,
|
105
|
+
:scope => :country_id
|
106
|
+
validates_uniqueness_of :abbreviation,
|
107
|
+
:scope => :country_id
|
108
|
+
|
109
|
+
def to_s #:nodoc
|
110
|
+
name
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateCountries < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :countries do |t|
|
4
|
+
t.column :name, :string, :null => false, :limit => 80
|
5
|
+
t.column :official_name, :string, :limit => 80
|
6
|
+
t.column :alpha_2_code, :string, :null => false, :limit => 2
|
7
|
+
t.column :alpha_3_code, :string, :null => false, :limit => 3
|
8
|
+
end
|
9
|
+
add_index :countries, :name, :unique => true
|
10
|
+
add_index :countries, :alpha_2_code, :unique => true
|
11
|
+
add_index :countries, :alpha_3_code, :unique => true
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.down
|
15
|
+
drop_table :countries
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateRegions < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :regions do |t|
|
4
|
+
t.column :country_id, :integer, :null => false
|
5
|
+
t.column :name, :string, :null => false, :limit => 50
|
6
|
+
t.column :abbreviation, :string, :null => false, :limit => 5
|
7
|
+
end
|
8
|
+
add_index :regions, [:name, :country_id], :unique => true
|
9
|
+
add_index :regions, [:abbreviation, :country_id], :unique => true
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.down
|
13
|
+
drop_table :regions
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class CreateAddresses < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :addresses do |t|
|
4
|
+
t.column :addressable_id, :integer, :null => false, :references => nil
|
5
|
+
t.column :addressable_type, :string, :null => false
|
6
|
+
t.column :street_1, :string, :null => false, :limit => 100
|
7
|
+
t.column :street_2, :string, :limit => 100
|
8
|
+
t.column :city, :string, :null => false, :limit => 255
|
9
|
+
t.column :region_id, :integer
|
10
|
+
t.column :custom_region, :string, :limit => 50
|
11
|
+
t.column :postal_code, :string, :null => false, :limit => 5
|
12
|
+
t.column :country_id, :integer, :default => 223
|
13
|
+
t.column :created_at, :timestamp, :null => false
|
14
|
+
t.column :updated_at, :datetime, :null => false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.down
|
19
|
+
drop_table :addresses
|
20
|
+
end
|
21
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'has_addresses'
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module PluginAWeek #:nodoc:
|
2
|
+
module Has #:nodoc:
|
3
|
+
# Adds base models for interacting with addresses, including Country,
|
4
|
+
# Region, and Address. These have the minimal attribute definitions needed
|
5
|
+
# to store addresses.
|
6
|
+
module Addresses
|
7
|
+
# Whether or not to use verbose output
|
8
|
+
mattr_accessor :verbose
|
9
|
+
@@verbose = true
|
10
|
+
|
11
|
+
def self.included(base) #:nodoc:
|
12
|
+
base.extend(MacroMethods)
|
13
|
+
end
|
14
|
+
|
15
|
+
module MacroMethods
|
16
|
+
# Creates a new association for having a single address. This takes
|
17
|
+
# the same parameters as ActiveRecord::Associations::ClassMethods#has_one.
|
18
|
+
# By default, the following associations are the same:
|
19
|
+
#
|
20
|
+
# class Person < ActiveRecord::Base
|
21
|
+
# has_address
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# and
|
25
|
+
#
|
26
|
+
# class Person < ActiveRecord::Base
|
27
|
+
# has_one :address,
|
28
|
+
# :class_name => 'Address',
|
29
|
+
# :as => :addressable,
|
30
|
+
# :dependent => :destroy
|
31
|
+
# end
|
32
|
+
def has_address(*args, &extension)
|
33
|
+
create_address_association(:one, :address, *args, &extension)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Creates a new association for having a multiple addresses. This takes
|
37
|
+
# the same parameters as ActiveRecord::Associations::ClassMethods#has_many.
|
38
|
+
# By default, the following associations are the same:
|
39
|
+
#
|
40
|
+
# class Person < ActiveRecord::Base
|
41
|
+
# has_addresses
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# and
|
45
|
+
#
|
46
|
+
# class Person < ActiveRecord::Base
|
47
|
+
# has_many :addresses,
|
48
|
+
# :class_name => 'Address',
|
49
|
+
# :as => :addressable,
|
50
|
+
# :dependent => :destroy
|
51
|
+
# end
|
52
|
+
def has_addresses(*args, &extension)
|
53
|
+
create_address_association(:many, :addresses, *args, &extension)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
def create_address_association(cardinality, association_id, *args, &extension)
|
58
|
+
options = extract_options_from_args!(args)
|
59
|
+
options.symbolize_keys!.reverse_merge!(
|
60
|
+
:class_name => 'Address',
|
61
|
+
:as => :addressable,
|
62
|
+
:dependent => :destroy
|
63
|
+
)
|
64
|
+
|
65
|
+
send("has_#{cardinality}", args.first || association_id, options, &extension)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
ActiveRecord::Base.class_eval do
|
73
|
+
include PluginAWeek::Has::Addresses
|
74
|
+
end
|