effective_addresses 0.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/MIT-LICENSE +20 -0
- data/README.md +151 -0
- data/Rakefile +23 -0
- data/app/assets/javascripts/effective_addresses.js +1 -0
- data/app/assets/javascripts/effective_addresses/address_fields.js.coffee +13 -0
- data/app/controllers/effective/addresses_controller.rb +16 -0
- data/app/helpers/effective_addresses_helper.rb +27 -0
- data/app/models/concerns/acts_as_addressable.rb +107 -0
- data/app/models/effective/address.rb +93 -0
- data/app/models/validators/billing_address_validator.rb +7 -0
- data/app/views/effective/addresses/_address_fields.html.haml +29 -0
- data/app/views/effective/addresses/_subregions.html.haml +1 -0
- data/config/routes.rb +4 -0
- data/db/migrate/01_create_effective_addresses.rb.erb +24 -0
- data/lib/effective_addresses.rb +16 -0
- data/lib/effective_addresses/engine.rb +28 -0
- data/lib/effective_addresses/version.rb +3 -0
- data/lib/generators/effective_addresses/install_generator.rb +32 -0
- data/lib/generators/templates/README +1 -0
- data/lib/generators/templates/effective_addresses.rb +12 -0
- data/lib/tasks/effective_addresses_tasks.rake +4 -0
- data/spec/controllers/addresses_controller_spec.rb +10 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +65 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/schema.rb +34 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +103 -0
- data/spec/dummy/log/test.log +125 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/dummy/spec_link +3 -0
- data/spec/models/address_spec.rb +52 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/support/factories.rb +13 -0
- metadata +339 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 Code and Effect Inc.
|
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.md
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
# Effective Addresses
|
2
|
+
|
3
|
+
Provides helper methods for dealing with a has_many :addresses relationship as a single method.
|
4
|
+
Such as @user.billing_address and @user.billing_address=
|
5
|
+
|
6
|
+
Includes full validations for addresses with multiple categories.
|
7
|
+
|
8
|
+
Includes a formtastic helper method to create/update the address of a parent object.
|
9
|
+
Uses the Carmen gem so when a Country is selected, an AJAX request populates the State/Province fields as appropriate.
|
10
|
+
|
11
|
+
Rails >= 3.2.x, Ruby >= 1.9.x. Has not been tested/developed for Rails4.
|
12
|
+
|
13
|
+
## Getting Started
|
14
|
+
|
15
|
+
Add to your Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'effective_addresses'
|
19
|
+
```
|
20
|
+
|
21
|
+
Run the bundle command to install it:
|
22
|
+
|
23
|
+
```console
|
24
|
+
bundle install
|
25
|
+
```
|
26
|
+
|
27
|
+
Then run the generator:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
rails generate effective_addresses:install
|
31
|
+
```
|
32
|
+
|
33
|
+
The generator will install an initializer which describes all configuration options and creates a database migration.
|
34
|
+
|
35
|
+
If you want to tweak the table name (to use something other than the default 'addresses'), manually adjust both the configuration file and the migration now.
|
36
|
+
|
37
|
+
Then migrate the database:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
rake db:migrate
|
41
|
+
```
|
42
|
+
|
43
|
+
If you'd like to use the form helper method, require the javascript in your application.js
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
//= require effective_addresses
|
47
|
+
```
|
48
|
+
|
49
|
+
|
50
|
+
## Usage
|
51
|
+
|
52
|
+
### Model
|
53
|
+
|
54
|
+
To use without any validations, just add the mixin to your existing model:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class User
|
58
|
+
acts_as_addressable
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
This adds the following getters, along with the setters:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
@user.address
|
66
|
+
@user.billing_address
|
67
|
+
@user.shipping_address
|
68
|
+
@user.primary_address
|
69
|
+
@user.secondary_address
|
70
|
+
```
|
71
|
+
|
72
|
+
You can also define validations as follows:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
class User
|
76
|
+
acts_as_addressable :require_billing => true, :require_shipping => true
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
This means when a User is created, it will not be valid unless a billing_address and shipping_address exist and are valid.
|
81
|
+
|
82
|
+
### Multiple Addresses
|
83
|
+
|
84
|
+
Everytime an address is changed, an additional address record is created. The latest address will always be returned by:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
@user.billing_address
|
88
|
+
```
|
89
|
+
|
90
|
+
You can find all past addresses (including the current one) by:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
@user.billing_addresses
|
94
|
+
```
|
95
|
+
|
96
|
+
### Form Helper
|
97
|
+
|
98
|
+
Use the helper in a formtastic form to quickly create the address fields 'f.inputs'. This example is in HAML:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
= semantic_form_for @user do |f|
|
102
|
+
= f.inputs :name => "Your Information" do
|
103
|
+
= f.input :email
|
104
|
+
= f.input :name
|
105
|
+
|
106
|
+
= effective_address_fields(f, :category => 'billing') # 'shipping', 'primary', 'secondary'
|
107
|
+
|
108
|
+
= f.action :submit
|
109
|
+
```
|
110
|
+
|
111
|
+
Currently only supports Formtastic.
|
112
|
+
|
113
|
+
Assuming the javascript has been properly required (as above), when you select a country from the dropdown
|
114
|
+
an AJAX GET request will be made to '/effective/address/subregions/:country_code' and populate the state dropdown with the appropriate states or provinces
|
115
|
+
|
116
|
+
|
117
|
+
## License
|
118
|
+
|
119
|
+
MIT License. Copyright Code and Effect Inc. http://www.codeandeffect.com
|
120
|
+
|
121
|
+
You are not granted rights or licenses to the trademarks of Code and Effect
|
122
|
+
|
123
|
+
## Notes
|
124
|
+
|
125
|
+
This is a work in progress gem. It needs smarter validations, dynamic methods and google maps integration
|
126
|
+
|
127
|
+
### Testing
|
128
|
+
|
129
|
+
The test suite for this gem is unfortunately not yet complete.
|
130
|
+
|
131
|
+
Run tests by:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
rake spec
|
135
|
+
```
|
136
|
+
|
137
|
+
|
138
|
+
|
139
|
+
|
140
|
+
|
141
|
+
|
142
|
+
|
143
|
+
|
144
|
+
|
145
|
+
|
146
|
+
|
147
|
+
|
148
|
+
|
149
|
+
|
150
|
+
|
151
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
|
8
|
+
# Our tasks
|
9
|
+
load 'lib/tasks/effective_addresses_tasks.rake'
|
10
|
+
|
11
|
+
# Testing tasks
|
12
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
13
|
+
load 'rails/tasks/engine.rake'
|
14
|
+
|
15
|
+
Bundler::GemHelper.install_tasks
|
16
|
+
|
17
|
+
require 'rspec/core'
|
18
|
+
require 'rspec/core/rake_task'
|
19
|
+
|
20
|
+
desc "Run all specs in spec directory (excluding plugin specs)"
|
21
|
+
RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare')
|
22
|
+
|
23
|
+
task :default => :spec
|
@@ -0,0 +1 @@
|
|
1
|
+
//= require_tree ./effective_addresses
|
@@ -0,0 +1,13 @@
|
|
1
|
+
$(document).on 'change', "select[data-behavior='address-country']", (event) ->
|
2
|
+
country_code = $(this).val()
|
3
|
+
|
4
|
+
url = "/effective/address/subregions/#{country_code}"
|
5
|
+
state_select = $(this).parent().parent().find("li > select[data-behavior='address-state']").first()
|
6
|
+
|
7
|
+
if country_code.length == 0
|
8
|
+
state_select.attr('disabled', 'disabled')
|
9
|
+
state_select.html('<option value="">please select a country</option>')
|
10
|
+
else
|
11
|
+
state_select.removeAttr('disabled')
|
12
|
+
state_select.find('option').first().text('loading...')
|
13
|
+
state_select.load(url)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Effective
|
2
|
+
class AddressesController < ApplicationController
|
3
|
+
skip_authorization_check if defined?(CanCan)
|
4
|
+
respond_to :json
|
5
|
+
|
6
|
+
def subregions
|
7
|
+
country_code = params[:country_code]
|
8
|
+
|
9
|
+
if country_code.present?
|
10
|
+
render :partial => 'effective/addresses/subregions', :locals => {:country_code => country_code}
|
11
|
+
else
|
12
|
+
render :text => '', :status => :unprocessable_entity
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'carmen-rails'
|
2
|
+
|
3
|
+
module EffectiveAddressesHelper
|
4
|
+
def effective_address_fields(form, options = {})
|
5
|
+
opts = {:f => form, :category => 'address', :skip_full_name => false}.merge(options)
|
6
|
+
|
7
|
+
case opts[:category]
|
8
|
+
when 'shipping'
|
9
|
+
opts[:name] = 'Shipping Address'
|
10
|
+
opts[:method] = :shipping_address
|
11
|
+
when 'billing'
|
12
|
+
opts[:name] = 'Billing Address'
|
13
|
+
opts[:method] = :billing_address
|
14
|
+
when 'primary'
|
15
|
+
opts[:name] = 'Primary Address'
|
16
|
+
opts[:method] = :primary_address
|
17
|
+
when 'secondary'
|
18
|
+
opts[:name] = 'Secondary Address'
|
19
|
+
opts[:method] = :secondary_address
|
20
|
+
else
|
21
|
+
opts[:name] = 'Address'
|
22
|
+
opts[:method] = :address
|
23
|
+
end
|
24
|
+
|
25
|
+
render :partial => 'effective/addresses/address_fields', :locals => opts
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# ActsAsAddressable
|
2
|
+
#
|
3
|
+
# This module creates .billing_address, .shipping_address and .address methods
|
4
|
+
# it tricks a has_many relationship into singleton methods
|
5
|
+
#
|
6
|
+
# Mark your model with 'acts_as_addressable'
|
7
|
+
#
|
8
|
+
# there are no additional migrations needed for this module to work.
|
9
|
+
#
|
10
|
+
# By default, addresses are NOT required
|
11
|
+
#
|
12
|
+
# If you want to validate presence, please mark your model
|
13
|
+
# acts_as_addressable :require_billing => true, :require_address => true, :require_shipping => true
|
14
|
+
#
|
15
|
+
# Please see app/views/admin/users/_form.html.haml for an example of how to use this in a formtastic form
|
16
|
+
#
|
17
|
+
|
18
|
+
module ActsAsAddressable
|
19
|
+
extend ActiveSupport::Concern
|
20
|
+
|
21
|
+
module ActiveRecord
|
22
|
+
def acts_as_addressable(options = {})
|
23
|
+
@acts_as_addressable_opts = {:require_shipping => false, :require_billing => false, :require_primary => false, :require_secondary => false, :require_address => false}.merge(options)
|
24
|
+
include ::ActsAsAddressable
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
included do
|
29
|
+
has_many :addresses, :as => :addressable, :class_name => "Effective::Address", :dependent => :delete_all
|
30
|
+
|
31
|
+
validates_with BillingAddressValidator if @acts_as_addressable_opts[:require_billing]
|
32
|
+
end
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
end
|
36
|
+
|
37
|
+
def single_addresses
|
38
|
+
addresses.select { |address| address.category == 'address' }
|
39
|
+
end
|
40
|
+
|
41
|
+
def address
|
42
|
+
single_addresses.last
|
43
|
+
end
|
44
|
+
|
45
|
+
def address=(address_atts)
|
46
|
+
add = (address_atts.kind_of?(Effective::Address) ? address_atts.dup : Effective::Address.new(address_atts))
|
47
|
+
add.category = 'address'
|
48
|
+
addresses << add if !add.empty? and !(add == address)
|
49
|
+
end
|
50
|
+
|
51
|
+
def shipping_addresses
|
52
|
+
addresses.select { |address| address.category == 'shipping' }
|
53
|
+
end
|
54
|
+
|
55
|
+
def shipping_address
|
56
|
+
shipping_addresses.last
|
57
|
+
end
|
58
|
+
|
59
|
+
def shipping_address=(address_atts)
|
60
|
+
add = (address_atts.kind_of?(Effective::Address) ? address_atts.dup : Effective::Address.new(address_atts))
|
61
|
+
add.category = 'shipping'
|
62
|
+
addresses << add if !add.empty? and !(add == shipping_address)
|
63
|
+
end
|
64
|
+
|
65
|
+
def billing_addresses
|
66
|
+
addresses.select { |address| address.category == 'billing' }
|
67
|
+
end
|
68
|
+
|
69
|
+
def billing_address
|
70
|
+
billing_addresses.last
|
71
|
+
end
|
72
|
+
|
73
|
+
def billing_address=(address_atts)
|
74
|
+
add = (address_atts.kind_of?(Effective::Address) ? address_atts.dup : Effective::Address.new(address_atts))
|
75
|
+
add.category = 'billing'
|
76
|
+
addresses << add if !add.empty? and !(add == billing_address)
|
77
|
+
end
|
78
|
+
|
79
|
+
def primary_addresses
|
80
|
+
addresses.select { |address| address.category == 'primary' }
|
81
|
+
end
|
82
|
+
|
83
|
+
def primary_address
|
84
|
+
primary_addresses.last
|
85
|
+
end
|
86
|
+
|
87
|
+
def primary_address=(address_atts)
|
88
|
+
add = (address_atts.kind_of?(Effective::Address) ? address_atts.dup : Effective::Address.new(address_atts))
|
89
|
+
add.category = 'primary'
|
90
|
+
addresses << add if !add.empty? and !(add == primary_address)
|
91
|
+
end
|
92
|
+
|
93
|
+
def secondary_addresses
|
94
|
+
addresses.select { |address| address.category == 'secondary' }
|
95
|
+
end
|
96
|
+
|
97
|
+
def secondary_address
|
98
|
+
secondary_addresses.last
|
99
|
+
end
|
100
|
+
|
101
|
+
def secondary_address=(address_atts)
|
102
|
+
add = (address_atts.kind_of?(Effective::Address) ? address_atts.dup : Effective::Address.new(address_atts))
|
103
|
+
add.category = 'secondary'
|
104
|
+
addresses << add if !add.empty? and !(add == secondary_address)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'carmen'
|
2
|
+
|
3
|
+
module Effective
|
4
|
+
class Address < ActiveRecord::Base
|
5
|
+
self.table_name = EffectiveAddresses.addresses_table_name.to_s
|
6
|
+
|
7
|
+
belongs_to :addressable, :polymorphic => true, :touch => true
|
8
|
+
|
9
|
+
structure do
|
10
|
+
category :string, :validates => [:presence, :inclusion => { :in => %w(billing shipping primary secondary address)}]
|
11
|
+
|
12
|
+
full_name :string, :validates => [:presence]
|
13
|
+
address1 :string, :validates => [:presence]
|
14
|
+
address2 :string
|
15
|
+
city :string, :validates => [:presence]
|
16
|
+
state_code :string, :validates => [:presence]
|
17
|
+
country_code :string, :validates => [:presence]
|
18
|
+
postal_code :string, :validates => [:presence]
|
19
|
+
timestamps
|
20
|
+
end
|
21
|
+
|
22
|
+
default_scope order(:updated_at)
|
23
|
+
|
24
|
+
scope :billing_addresses, -> { where(:category => 'billing') }
|
25
|
+
scope :shipping_addresses, -> { where(:category => 'shipping') }
|
26
|
+
|
27
|
+
def first_name
|
28
|
+
full_name.split(' ').first rescue full_name
|
29
|
+
end
|
30
|
+
|
31
|
+
def last_name
|
32
|
+
full_name.gsub(first_name, '').strip rescue full_name
|
33
|
+
end
|
34
|
+
|
35
|
+
def country
|
36
|
+
Carmen::Country.coded(country_code).name rescue ''
|
37
|
+
end
|
38
|
+
|
39
|
+
def state
|
40
|
+
Carmen::Country.coded(country_code).subregions.coded(state_code).name rescue ''
|
41
|
+
end
|
42
|
+
|
43
|
+
def province
|
44
|
+
Carmen::Country.coded(country_code).subregions.coded(state_code).name rescue ''
|
45
|
+
end
|
46
|
+
|
47
|
+
def country=(country_string)
|
48
|
+
value = Carmen::Country.named(country_string) || Carmen::Country.coded(country_string.try(:upcase))
|
49
|
+
self.country_code = value.code if value.present?
|
50
|
+
end
|
51
|
+
|
52
|
+
def state=(state_string)
|
53
|
+
if country.present?
|
54
|
+
value = Carmen::Country.coded(country_code).subregions.named(state_string) || Carmen::Country.coded(country_code).subregions.coded(state_string.try(:upcase))
|
55
|
+
self.state_code = value.code if value.present?
|
56
|
+
else
|
57
|
+
Rails.logger.info 'No country set. Try calling country= before state='
|
58
|
+
puts 'No country set. Try calling country= before state='
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def postal_code_looks_canadian?
|
63
|
+
postal_code.gsub(/ /, '').strip.match(/^\D{1}\d{1}\D{1}\-?\d{1}\D{1}\d{1}$/).present? rescue false # Matches T5T2T1 or T5T-2T1
|
64
|
+
end
|
65
|
+
|
66
|
+
def ==(other_address)
|
67
|
+
self_attrs = self.attributes
|
68
|
+
other_attrs = other_address.respond_to?(:attributes) ? other_address.attributes : {}
|
69
|
+
|
70
|
+
[self_attrs, other_attrs].each { |attrs| attrs.except!('id', 'created_at', 'updated_at', 'addressable_type', 'addressable_id', 'address2') }
|
71
|
+
|
72
|
+
self_attrs == other_attrs
|
73
|
+
end
|
74
|
+
|
75
|
+
# An address may be set with a category but nothing else
|
76
|
+
# This is considered empty
|
77
|
+
def empty?
|
78
|
+
!full_name.present? and !address1.present? and !address2.present? and !city.present? and !state_code.present? and !country_code.present? and !postal_code.present?
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_s
|
82
|
+
output = "#{full_name}\n"
|
83
|
+
output += "#{address1}\n"
|
84
|
+
output += "#{address2}\n" if address2.present?
|
85
|
+
output += "#{city}, #{state}\n"
|
86
|
+
output += "#{country}, #{postal_code}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def to_html
|
90
|
+
to_s.gsub(/\n/, '<br>').html_safe
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|