address_concern 2.0.1 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Readme.md +73 -6
- data/address_concern.gemspec +2 -3
- data/app/models/concerns/address.rb +784 -0
- data/{lib/address_concern → app/models/concerns}/address_associations.rb +33 -12
- data/app/models/concerns/attributes_slice.rb +54 -0
- data/app/models/concerns/inspect_base.rb +32 -0
- data/config/address_concern.rb +3 -0
- data/lib/address_concern/attribute_normalizer.rb +10 -6
- data/lib/address_concern/engine.rb +24 -0
- data/lib/address_concern/version.rb +1 -1
- data/lib/address_concern.rb +4 -3
- data/lib/core_extensions/hash/reorder.rb +11 -0
- data/lib/core_extensions/string/cleanlines.rb +28 -0
- data/lib/generators/address_concern/templates/migration.rb +14 -10
- data/spec/models/acts_as_address_spec.rb +66 -0
- data/spec/models/address_spec.rb +345 -117
- data/spec/spec_helper.rb +1 -5
- data/spec/support/models/address.rb +2 -1
- data/spec/support/models/address_custom_attr_names.rb +11 -0
- data/spec/support/models/address_with_code_only.rb +12 -0
- data/spec/support/models/address_with_name_only.rb +5 -0
- data/spec/support/models/address_with_separate_address_columns.rb +5 -0
- data/spec/support/models/user.rb +6 -1
- data/spec/support/schema.rb +22 -0
- metadata +17 -34
- data/lib/address_concern/address.rb +0 -306
@@ -0,0 +1,784 @@
|
|
1
|
+
require_relative '../../../lib/core_extensions/hash/reorder'
|
2
|
+
using Hash::Reorder
|
3
|
+
|
4
|
+
require_relative '../../../lib/core_extensions/string/cleanlines'
|
5
|
+
using String::Cleanlines
|
6
|
+
|
7
|
+
require_relative 'inspect_base'
|
8
|
+
require_relative 'attributes_slice'
|
9
|
+
|
10
|
+
module AddressConcern
|
11
|
+
module Address
|
12
|
+
module Base
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
# These (Base) class methods are added to ActiveRecord::Base so that they will be available from _any_
|
16
|
+
# model class. Unlike the main AddressConcern::Address methods which are only included _after_
|
17
|
+
# you call acts_as_address on a model.
|
18
|
+
module ClassMethods
|
19
|
+
attr_reader :acts_as_address_config
|
20
|
+
def acts_as_address(**options)
|
21
|
+
# Have to use yield_self(¬_null) intead of presence because NullColumn.present? => true.
|
22
|
+
not_null = ->(column) {
|
23
|
+
column.type.nil? ? nil : column
|
24
|
+
}
|
25
|
+
options = options.deep_symbolize_keys
|
26
|
+
default_config = {
|
27
|
+
state: {
|
28
|
+
#normalize: false,
|
29
|
+
#validate: false,
|
30
|
+
|
31
|
+
code_attribute: column_for_attribute(:state_code).yield_self(¬_null)&.name ||
|
32
|
+
(column_for_attribute(:state).yield_self(¬_null)&.name unless options.dig(:state, :name_attribute).to_s == 'state'),
|
33
|
+
|
34
|
+
name_attribute: column_for_attribute(:state_name).yield_self(¬_null)&.name ||
|
35
|
+
(column_for_attribute(:state).yield_self(¬_null)&.name unless options.dig(:state, :code_attribute).to_s == 'state'),
|
36
|
+
|
37
|
+
on_unknown: ->(value, name_or_code) { },
|
38
|
+
},
|
39
|
+
|
40
|
+
country: {
|
41
|
+
#normalize: false,
|
42
|
+
#validate: false,
|
43
|
+
|
44
|
+
# By default, code (same as alpha_2_code) will be used
|
45
|
+
carmen_code: :code, # or alpha_2_code, alpha_3_code, :numeric_code
|
46
|
+
|
47
|
+
code_attribute: column_for_attribute(:country_code).yield_self(¬_null)&.name ||
|
48
|
+
(column_for_attribute(:country).yield_self(¬_null)&.name unless options.dig(:country, :name_attribute).to_s == 'country'),
|
49
|
+
|
50
|
+
name_attribute: column_for_attribute(:country_name).yield_self(¬_null)&.name ||
|
51
|
+
(column_for_attribute(:country).yield_self(¬_null)&.name unless options.dig(:country, :code_attribute).to_s == 'country'),
|
52
|
+
|
53
|
+
on_unknown: ->(value, name_or_code) { },
|
54
|
+
},
|
55
|
+
|
56
|
+
address: {
|
57
|
+
#normalize: false,
|
58
|
+
#validate: false,
|
59
|
+
|
60
|
+
# Try to auto-detect address columns
|
61
|
+
attributes: column_names.grep(/address$|^address_\d$/),
|
62
|
+
}
|
63
|
+
}
|
64
|
+
@acts_as_address_config = config = {
|
65
|
+
**default_config
|
66
|
+
}.deep_merge(options)
|
67
|
+
|
68
|
+
[:state, :country].each do |group|
|
69
|
+
# Can't use the same column for code and name, so if it would be the same (by default or
|
70
|
+
# otherwise), let it be used for name only instead.
|
71
|
+
if config[group][:code_attribute] == config[group][:name_attribute]
|
72
|
+
config[group].delete(:code_attribute)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
include ::AddressConcern::Address
|
77
|
+
end
|
78
|
+
|
79
|
+
def belongs_to_addressable(**options)
|
80
|
+
belongs_to :addressable, polymorphic: true, touch: true, optional: true, **options
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
include InspectBase
|
86
|
+
include AttributesSlice
|
87
|
+
|
88
|
+
extend ActiveSupport::Concern
|
89
|
+
included do
|
90
|
+
#═══════════════════════════════════════════════════════════════════════════════════════════════
|
91
|
+
# Config
|
92
|
+
|
93
|
+
delegate *[
|
94
|
+
:acts_as_address_config,
|
95
|
+
:country_config,
|
96
|
+
:state_config,
|
97
|
+
], to: 'self.class'
|
98
|
+
|
99
|
+
class << self
|
100
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────
|
101
|
+
def country_config
|
102
|
+
@acts_as_address_config[:country] || {}
|
103
|
+
end
|
104
|
+
|
105
|
+
# usually :code
|
106
|
+
def carmen_country_code
|
107
|
+
country_config[:carmen_code]
|
108
|
+
end
|
109
|
+
|
110
|
+
# usually :coded
|
111
|
+
def carmen_country_code_find_method
|
112
|
+
:"#{carmen_country_code}d"
|
113
|
+
end
|
114
|
+
|
115
|
+
# 'country' or similar
|
116
|
+
def country_name_attribute
|
117
|
+
country_config[:name_attribute]&.to_sym
|
118
|
+
end
|
119
|
+
|
120
|
+
def country_code_attribute
|
121
|
+
country_config[:code_attribute]&.to_sym
|
122
|
+
end
|
123
|
+
|
124
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────
|
125
|
+
|
126
|
+
def state_config
|
127
|
+
@acts_as_address_config[:state] || {}
|
128
|
+
end
|
129
|
+
|
130
|
+
def carmen_state_code
|
131
|
+
state_config[:carmen_code]
|
132
|
+
end
|
133
|
+
|
134
|
+
def state_name_attribute
|
135
|
+
state_config[:name_attribute]&.to_sym
|
136
|
+
end
|
137
|
+
|
138
|
+
def state_code_attribute
|
139
|
+
state_config[:code_attribute]&.to_sym
|
140
|
+
end
|
141
|
+
|
142
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────
|
143
|
+
|
144
|
+
def address_attr_config
|
145
|
+
@acts_as_address_config[:address] || {}
|
146
|
+
end
|
147
|
+
|
148
|
+
# TODO: rename to something different than the same name as #address_attributes, like
|
149
|
+
# street_address_attr_names
|
150
|
+
def address_attributes
|
151
|
+
Array(address_attr_config[:attributes]).map(&:to_sym)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Address line 1
|
155
|
+
def address_attribute
|
156
|
+
address_attributes[0]
|
157
|
+
end
|
158
|
+
|
159
|
+
def multi_line_address?
|
160
|
+
address_attributes.size == 1 && (
|
161
|
+
column = column_for_attribute(address_attribute)
|
162
|
+
column.type == :text
|
163
|
+
)
|
164
|
+
end
|
165
|
+
|
166
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────
|
167
|
+
|
168
|
+
# AKA configured_address_attributes
|
169
|
+
def address_attr_names
|
170
|
+
[
|
171
|
+
*address_attributes,
|
172
|
+
:city,
|
173
|
+
state_name_attribute,
|
174
|
+
state_code_attribute,
|
175
|
+
:postal_code,
|
176
|
+
country_name_attribute,
|
177
|
+
country_code_attribute,
|
178
|
+
].compact.uniq
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
#═════════════════════════════════════════════════════════════════════════════════════════════════
|
183
|
+
# Customizable validation (to add?)
|
184
|
+
|
185
|
+
#validates_presence_of :address
|
186
|
+
#validates_presence_of :state, if: :state_required?
|
187
|
+
#validates_presence_of :country
|
188
|
+
|
189
|
+
#═════════════════════════════════════════════════════════════════════════════════════════════════
|
190
|
+
# Attributes
|
191
|
+
|
192
|
+
def _assign_attributes(attributes)
|
193
|
+
attributes = attributes.symbolize_keys
|
194
|
+
attributes = reorder_language_attributes(attributes)
|
195
|
+
super(attributes)
|
196
|
+
end
|
197
|
+
|
198
|
+
def self.country_aliases ; [:country_name, :country_code] ; end
|
199
|
+
def self.state_aliases ; [:state_name, :state_code] ; end
|
200
|
+
|
201
|
+
# country needs to be assigned _before_ state for things to work as intended (can't look up
|
202
|
+
# state in state= unless we know which country it is for)
|
203
|
+
def reorder_language_attributes(attributes)
|
204
|
+
attributes.reorder(self.class.country_name_attribute, self.class.country_code_attribute, *self.class.country_aliases,
|
205
|
+
self.class. state_name_attribute, self.class. state_code_attribute, *self.class.state_aliases)
|
206
|
+
end
|
207
|
+
|
208
|
+
def address_attributes
|
209
|
+
attributes_slice(
|
210
|
+
*self.class.address_attr_names
|
211
|
+
)
|
212
|
+
end
|
213
|
+
|
214
|
+
#═════════════════════════════════════════════════════════════════════════════════════════════════
|
215
|
+
|
216
|
+
# TODO: automatically normalize if attribute_normalizer/normalizy gem is loaded? add a config option to opt out?
|
217
|
+
#normalize_attributes :city, :state, :postal_code, :country
|
218
|
+
#normalize_attribute *address_attributes, with: [:cleanlines, :strip]
|
219
|
+
|
220
|
+
#═════════════════════════════════════════════════════════════════════════════════════════════════
|
221
|
+
# Country & State (Carmen + custom)
|
222
|
+
|
223
|
+
# Some of these methods look up by either name or code
|
224
|
+
|
225
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────────
|
226
|
+
# find country
|
227
|
+
|
228
|
+
# Finds by name, falling back to finding by code.
|
229
|
+
def self.find_carmen_country(name)
|
230
|
+
return name if name.is_a? Carmen::Country
|
231
|
+
|
232
|
+
(
|
233
|
+
find_carmen_country_by_name(name) ||
|
234
|
+
find_carmen_country_by_code(name)
|
235
|
+
)
|
236
|
+
end
|
237
|
+
def self.find_carmen_country!(name)
|
238
|
+
find_carmen_country(name) or
|
239
|
+
raise "country #{name} not found"
|
240
|
+
end
|
241
|
+
|
242
|
+
def self.find_carmen_country_by_name(name)
|
243
|
+
name = recognize_country_name_alias(name)
|
244
|
+
Carmen::Country.named(name)
|
245
|
+
end
|
246
|
+
|
247
|
+
def self.find_carmen_country_by_code(code)
|
248
|
+
# Carmen::Country.coded(code)
|
249
|
+
Carmen::Country.send(carmen_country_code_find_method, code)
|
250
|
+
end
|
251
|
+
|
252
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────────
|
253
|
+
# find state
|
254
|
+
|
255
|
+
# Finds by name, falling back to finding by code.
|
256
|
+
def self.find_carmen_state(country_name, name)
|
257
|
+
return name if name.is_a? Carmen::Region
|
258
|
+
|
259
|
+
country = find_carmen_country!(country_name)
|
260
|
+
states = states_for_country(country)
|
261
|
+
(
|
262
|
+
states.named(name) ||
|
263
|
+
states.coded(name)
|
264
|
+
)
|
265
|
+
end
|
266
|
+
def self.find_carmen_state!(country_name, name)
|
267
|
+
find_carmen_state(country_name, name) or
|
268
|
+
raise "state #{name} not found for country #{country_name}"
|
269
|
+
end
|
270
|
+
|
271
|
+
def self.find_carmen_state_by_name(country_name, name)
|
272
|
+
country = find_carmen_country!(country_name)
|
273
|
+
states = states_for_country(country)
|
274
|
+
states.named(name)
|
275
|
+
end
|
276
|
+
|
277
|
+
def self.find_carmen_state_by_code(country_name, code)
|
278
|
+
country = find_carmen_country!(country_name)
|
279
|
+
states = states_for_country(country)
|
280
|
+
states.coded(code)
|
281
|
+
end
|
282
|
+
|
283
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────────
|
284
|
+
# country
|
285
|
+
|
286
|
+
# Calls country.code
|
287
|
+
_ = def self.carmen_country_code_for(country)
|
288
|
+
country.send(carmen_country_code)
|
289
|
+
end
|
290
|
+
delegate _, to: 'self.class'
|
291
|
+
|
292
|
+
# If you are storing both a country_name and country_code...
|
293
|
+
# This _should_ be the same as the value stored in the country attribute, but allows you to
|
294
|
+
# look it up just to make sure they match (or to update country field to match this).
|
295
|
+
def country_name_from_code
|
296
|
+
if (country = self.class.find_carmen_country_by_code(country_code))
|
297
|
+
country.name
|
298
|
+
end
|
299
|
+
end
|
300
|
+
def country_code_from_name
|
301
|
+
if (country = self.class.find_carmen_country_by_name(country_name))
|
302
|
+
self.class.carmen_country_code_for(country)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────────
|
307
|
+
# state
|
308
|
+
|
309
|
+
def state_name_from_code
|
310
|
+
if carmen_country && (state = self.class.find_carmen_state_by_code(carmen_country, state_code))
|
311
|
+
state.name
|
312
|
+
end
|
313
|
+
end
|
314
|
+
def state_code_from_name
|
315
|
+
if carmen_country && (state = self.class.find_carmen_state_by_name(carmen_country, state_name))
|
316
|
+
state.code
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────────
|
321
|
+
# country
|
322
|
+
|
323
|
+
def self.recognize_country_name_alias(name)
|
324
|
+
name = case name
|
325
|
+
when 'USA'
|
326
|
+
'United States'
|
327
|
+
when 'The Democratic Republic of the Congo', 'Democratic Republic of the Congo'
|
328
|
+
'Congo, the Democratic Republic of the'
|
329
|
+
when 'Republic of Macedonia', 'Macedonia, Republic of', 'Macedonia'
|
330
|
+
'Macedonia, Republic of'
|
331
|
+
else
|
332
|
+
name
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────────
|
337
|
+
|
338
|
+
scope :in_country, ->(country_name) {
|
339
|
+
country = find_carmen_country!(country_name)
|
340
|
+
where(addresses: { country_code: country&.code })
|
341
|
+
}
|
342
|
+
scope :in_state, ->(country_name, name) {
|
343
|
+
country = find_carmen_country!(country_name)
|
344
|
+
state = find_carmen_state!(country_name, name)
|
345
|
+
where(addresses: { country_code: country&.code, state_code: state&.code })
|
346
|
+
}
|
347
|
+
|
348
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────────
|
349
|
+
|
350
|
+
def carmen_country
|
351
|
+
self.class.find_carmen_country_by_code(country_code)
|
352
|
+
end
|
353
|
+
|
354
|
+
def carmen_state
|
355
|
+
if (country = carmen_country)
|
356
|
+
# country.subregions.coded(state_code)
|
357
|
+
self.class.states_for_country(country).coded(state_code)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
#═════════════════════════════════════════════════════════════════════════════════════════════════
|
362
|
+
# country attribute(s)
|
363
|
+
|
364
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────────
|
365
|
+
# setters
|
366
|
+
|
367
|
+
|
368
|
+
def clear_country
|
369
|
+
write_attribute(self.class.country_name_attribute, nil) if self.class.country_name_attribute
|
370
|
+
write_attribute(self.class.country_code_attribute, nil) if self.class.country_code_attribute
|
371
|
+
end
|
372
|
+
|
373
|
+
def set_country_from_carmen_country(country)
|
374
|
+
write_attribute(self.class.country_name_attribute, country.name ) if self.class.country_name_attribute
|
375
|
+
write_attribute(self.class.country_code_attribute, carmen_country_code_for(country)) if self.class.country_code_attribute
|
376
|
+
end
|
377
|
+
|
378
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────────
|
379
|
+
# code=
|
380
|
+
|
381
|
+
# def country_code=(code)
|
382
|
+
define_method :"#{country_code_attribute || 'country_code'}=" do |value|
|
383
|
+
if value.blank?
|
384
|
+
clear_country
|
385
|
+
else
|
386
|
+
if (country = self.class.find_carmen_country_by_code(value))
|
387
|
+
set_country_from_carmen_country(country)
|
388
|
+
else
|
389
|
+
country_config[:on_unknown].(value, :code)
|
390
|
+
write_attribute(self.class.country_code_attribute, value) if self.class.country_code_attribute
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
# Attribute alias
|
396
|
+
if country_code_attribute
|
397
|
+
unless :country_code == country_code_attribute
|
398
|
+
alias_attribute :country_code, :"#{country_code_attribute}"
|
399
|
+
#alias_method :country_code=, :"#{country_code_attribute}="
|
400
|
+
end
|
401
|
+
else
|
402
|
+
alias_method :country_code, :country_code_from_name
|
403
|
+
end
|
404
|
+
|
405
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────────
|
406
|
+
# name=
|
407
|
+
|
408
|
+
# def country_name=(name)
|
409
|
+
define_method :"#{country_name_attribute || 'country_name'}=" do |value|
|
410
|
+
if value.blank?
|
411
|
+
clear_country
|
412
|
+
else
|
413
|
+
if (country = self.class.find_carmen_country_by_name(value))
|
414
|
+
set_country_from_carmen_country(country)
|
415
|
+
else
|
416
|
+
country_config[:on_unknown].(value, :name)
|
417
|
+
write_attribute(self.class.country_name_attribute, value) if self.class.country_name_attribute
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
# Attribute alias
|
423
|
+
if country_name_attribute
|
424
|
+
unless :country_name == country_name_attribute
|
425
|
+
alias_attribute :country_name, country_name_attribute
|
426
|
+
#alias_method :country_name=, :"#{country_name_attribute}="
|
427
|
+
end
|
428
|
+
else
|
429
|
+
alias_method :country_name, :country_name_from_code
|
430
|
+
end
|
431
|
+
|
432
|
+
#════════════════════════════════════════════════════════════════════════════════════════════════════
|
433
|
+
# state attribute(s)
|
434
|
+
# (This is nearly identical to country section above with s/country/state/)
|
435
|
+
|
436
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────────
|
437
|
+
# setters
|
438
|
+
|
439
|
+
|
440
|
+
def clear_state
|
441
|
+
write_attribute(self.class.state_name_attribute, nil) if self.class.state_name_attribute
|
442
|
+
write_attribute(self.class.state_code_attribute, nil) if self.class.state_code_attribute
|
443
|
+
end
|
444
|
+
|
445
|
+
def set_state_from_carmen_state(state)
|
446
|
+
write_attribute(self.class.state_name_attribute, state.name) if self.class.state_name_attribute
|
447
|
+
write_attribute(self.class.state_code_attribute, state.code) if self.class.state_code_attribute
|
448
|
+
end
|
449
|
+
|
450
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────────
|
451
|
+
# code=
|
452
|
+
|
453
|
+
# def state_code=(code)
|
454
|
+
define_method :"#{state_code_attribute || 'state_code'}=" do |value|
|
455
|
+
if value.blank?
|
456
|
+
clear_state
|
457
|
+
else
|
458
|
+
if carmen_country && (state = self.class.find_carmen_state_by_code(carmen_country, value))
|
459
|
+
set_state_from_carmen_state(state)
|
460
|
+
else
|
461
|
+
#puts carmen_country ? "unknown state code '#{value}'" : "can't find state without country"
|
462
|
+
state_config[:on_unknown].(value, :code)
|
463
|
+
write_attribute(self.class.state_code_attribute, value) if self.class.state_code_attribute
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
# Attribute alias
|
469
|
+
if state_code_attribute
|
470
|
+
unless :state_code == state_code_attribute
|
471
|
+
alias_attribute :state_code, :"#{state_code_attribute}"
|
472
|
+
#alias_method :state_code=, :"#{state_code_attribute}="
|
473
|
+
end
|
474
|
+
else
|
475
|
+
alias_method :state_code, :state_code_from_name
|
476
|
+
end
|
477
|
+
|
478
|
+
# alias_method :province, :state
|
479
|
+
|
480
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────────
|
481
|
+
# name=
|
482
|
+
|
483
|
+
# def state_name=(name)
|
484
|
+
# Uses find_carmen_state so if your column was named 'state', you could actually do state = name
|
485
|
+
# or code.
|
486
|
+
define_method :"#{state_name_attribute || 'state_name'}=" do |value|
|
487
|
+
if value.blank?
|
488
|
+
clear_state
|
489
|
+
else
|
490
|
+
if carmen_country && (state = self.class.find_carmen_state(carmen_country, value))
|
491
|
+
set_state_from_carmen_state(state)
|
492
|
+
else
|
493
|
+
#puts carmen_country ? "unknown state name '#{name}'" : "can't find state without country"
|
494
|
+
state_config[:on_unknown].(value, :name)
|
495
|
+
write_attribute(self.class.state_name_attribute, value) if self.class.state_name_attribute
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
# Attribute alias
|
501
|
+
if state_name_attribute
|
502
|
+
unless :state_name == state_name_attribute
|
503
|
+
alias_attribute :state_name, state_name_attribute
|
504
|
+
#alias_method :state_name=, :"#{state_name_attribute}="
|
505
|
+
end
|
506
|
+
else
|
507
|
+
alias_method :state_name, :state_name_from_code
|
508
|
+
end
|
509
|
+
|
510
|
+
#════════════════════════════════════════════════════════════════════════════════════════════════════
|
511
|
+
# State/province options for country
|
512
|
+
|
513
|
+
# This is useful if want to list the state options allowed for a country in a select box and
|
514
|
+
# restrict entry to only officially listed state options.
|
515
|
+
# It is not required in the postal address for all countries, however. If you only want to show it
|
516
|
+
# if it's required in the postal address, you can make it conditional based on
|
517
|
+
# state_included_in_postal_address?.
|
518
|
+
def self.states_for_country(country)
|
519
|
+
return [] unless country
|
520
|
+
country = find_carmen_country!(country)
|
521
|
+
|
522
|
+
has_states_at_level_1 = country.subregions.any? { |region|
|
523
|
+
region.type == 'state' ||
|
524
|
+
region.type == 'province' ||
|
525
|
+
region.type == 'metropolitan region'
|
526
|
+
}
|
527
|
+
has_states_at_level_1 = false if country.name == 'United Kingdom'
|
528
|
+
|
529
|
+
if country.name == 'Kenya'
|
530
|
+
# https://github.com/jim/carmen/issues/227
|
531
|
+
# https://en.wikipedia.org/wiki/Provinces_of_Kenya
|
532
|
+
# Kenya's provinces were replaced by a system of counties in 2013.
|
533
|
+
# https://en.wikipedia.org/wiki/ISO_3166-2:KE confirms that they are "former" provinces.
|
534
|
+
# At the time of this writing, however, it doesn't look like Carmen has been updated to
|
535
|
+
# include the 47 counties listed under https://en.wikipedia.org/wiki/ISO_3166-2:KE.
|
536
|
+
country.subregions.typed('county')
|
537
|
+
#elsif country.name == 'France'
|
538
|
+
# # https://github.com/jim/carmen/issues/228
|
539
|
+
# # https://en.wikipedia.org/wiki/Regions_of_France
|
540
|
+
# # In 2016 what had been 27 regions was reduced to 18.
|
541
|
+
# # France is divided into 18 administrative regions, including 13 metropolitan regions and 5 overseas regions.
|
542
|
+
# # https://en.wikipedia.org/wiki/ISO_3166-2:FR
|
543
|
+
# []
|
544
|
+
elsif has_states_at_level_1
|
545
|
+
country.subregions
|
546
|
+
else
|
547
|
+
# Going below level-1 subregions is needed for Philippines, Indonesia, and possibly others
|
548
|
+
Carmen::RegionCollection.new(
|
549
|
+
country.subregions.
|
550
|
+
map { |_| _.subregions.any? ? _.subregions : _ }.
|
551
|
+
flatten
|
552
|
+
)
|
553
|
+
end
|
554
|
+
end
|
555
|
+
def states_for_country
|
556
|
+
self.class.states_for_country(carmen_country)
|
557
|
+
end
|
558
|
+
alias_method :state_options, :states_for_country
|
559
|
+
|
560
|
+
def country_with_states?
|
561
|
+
states_for_country.any?
|
562
|
+
end
|
563
|
+
|
564
|
+
#───────────────────────────────────────────────────────────────────────────────────────────────
|
565
|
+
|
566
|
+
# Used for checking/testing states_for_country.
|
567
|
+
# Example:
|
568
|
+
# Address.compare_subregions_and_states_for_country('France');
|
569
|
+
def self.compare_subregions_and_states_for_country(country)
|
570
|
+
country = find_carmen_country!(country)
|
571
|
+
states_for_country = states_for_country(country)
|
572
|
+
if country.subregions == states_for_country
|
573
|
+
puts '(Same:)'
|
574
|
+
pp country.subregions
|
575
|
+
else
|
576
|
+
puts %(country.subregions (#{country.subregions.size}):\n#{country.subregions.pretty_inspect})
|
577
|
+
puts
|
578
|
+
puts %(states_for_country(country) (#{states_for_country.size}):\n#{states_for_country})
|
579
|
+
states_for_country
|
580
|
+
end
|
581
|
+
end
|
582
|
+
|
583
|
+
#───────────────────────────────────────────────────────────────────────────────────────────────
|
584
|
+
|
585
|
+
# Is the state/province required in a postal address?
|
586
|
+
# If no, perhaps you want to collect it for other reasons (like seeing which people/things are in
|
587
|
+
# the same region). Or for countries where it *may* be included in a postal address but is not
|
588
|
+
# required to be included.
|
589
|
+
def state_required_in_postal_address?
|
590
|
+
[
|
591
|
+
'Australia',
|
592
|
+
'Brazil',
|
593
|
+
'Canada',
|
594
|
+
'Mexico',
|
595
|
+
'United States',
|
596
|
+
'Italy',
|
597
|
+
'Venezuela',
|
598
|
+
].include? country_name
|
599
|
+
end
|
600
|
+
def state_possibly_included_in_postal_address?
|
601
|
+
# https://ux.stackexchange.com/questions/64665/address-form-field-for-region
|
602
|
+
# http://www.bitboost.com/ref/international-address-formats/denmark/
|
603
|
+
# http://www.bitboost.com/ref/international-address-formats/poland/
|
604
|
+
return true if state_required_in_postal_address?
|
605
|
+
return false if [
|
606
|
+
'Algeria',
|
607
|
+
'Argentina',
|
608
|
+
'Austria',
|
609
|
+
'Denmark',
|
610
|
+
'France',
|
611
|
+
'Germany',
|
612
|
+
'Indonesia',
|
613
|
+
'Ireland',
|
614
|
+
'Israel',
|
615
|
+
'Netherlands',
|
616
|
+
'New Zealand',
|
617
|
+
'Poland',
|
618
|
+
'Sweden',
|
619
|
+
'United Kingdom',
|
620
|
+
].include? country_name
|
621
|
+
# Default:
|
622
|
+
country_with_states?
|
623
|
+
end
|
624
|
+
|
625
|
+
# It's not called a "State" in all countries.
|
626
|
+
# In some countries, it could technically be multiple different types of regions:
|
627
|
+
# - In United States, it could be a state or an outlying region or a district or an APO
|
628
|
+
# - In Canada, it could be a province or a territory.
|
629
|
+
# This attempts to return the most common, expected name for this field.
|
630
|
+
# See also: https://ux.stackexchange.com/questions/64665/address-form-field-for-region
|
631
|
+
#
|
632
|
+
# To see what it should be called in all countries known to Carmen:
|
633
|
+
# Country.countries_with_states.map {|country| [country.name, Address.new(country_name: country.name).state_label] }.to_h
|
634
|
+
# => {"Afghanistan"=>"Province",
|
635
|
+
# "Armenia"=>"Province",
|
636
|
+
# "Angola"=>"Province",
|
637
|
+
# "Argentina"=>"Province",
|
638
|
+
# "Austria"=>"State",
|
639
|
+
# "Australia"=>"State",
|
640
|
+
# ...
|
641
|
+
def state_label
|
642
|
+
# In UK, it looks like they (optionally) include the *county* in their addresses. They don't actually have "states" per se.
|
643
|
+
# Reference: http://bitboost.com/ref/international-address-formats/united-kingdom/
|
644
|
+
# Could also limit to Countries (England, Scotland, Wales) and Provinces (Northern Ireland).
|
645
|
+
# Who knows. The UK's subregions are a mess.
|
646
|
+
# If allowing the full list of subregions from https://en.wikipedia.org/wiki/ISO_3166-2:GB,
|
647
|
+
# perhaps Region is a better, more inclusive term.
|
648
|
+
if country_name.in? ['United Kingdom']
|
649
|
+
'Region'
|
650
|
+
elsif state_options.any?
|
651
|
+
state_options[0].type.capitalize
|
652
|
+
end
|
653
|
+
end
|
654
|
+
|
655
|
+
#════════════════════════════════════════════════════════════════════════════════════════════════════
|
656
|
+
|
657
|
+
def empty?
|
658
|
+
address_attributes.all? do |key, value|
|
659
|
+
value.blank?
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
def present?
|
664
|
+
address_attributes.any? do |key, value|
|
665
|
+
value.present?
|
666
|
+
end
|
667
|
+
end
|
668
|
+
|
669
|
+
#════════════════════════════════════════════════════════════════════════════════════════════════════
|
670
|
+
# Street address / Address lines
|
671
|
+
|
672
|
+
# Attribute alias for street address line 1
|
673
|
+
#if address_attribute
|
674
|
+
# unless :address == address_attribute
|
675
|
+
# alias_attribute :address, :"#{address_attribute}"
|
676
|
+
# end
|
677
|
+
#end
|
678
|
+
|
679
|
+
def address_lines
|
680
|
+
if self.class.multi_line_address?
|
681
|
+
address.to_s.cleanlines.to_a
|
682
|
+
else
|
683
|
+
self.class.address_attributes.map do |attr_name|
|
684
|
+
send attr_name
|
685
|
+
end
|
686
|
+
end
|
687
|
+
end
|
688
|
+
|
689
|
+
#════════════════════════════════════════════════════════════════════════════════════════════════════
|
690
|
+
# Formatting for humans
|
691
|
+
|
692
|
+
# Lines of a postal address
|
693
|
+
def lines
|
694
|
+
[
|
695
|
+
#name,
|
696
|
+
*address_lines,
|
697
|
+
city_line,
|
698
|
+
country_name,
|
699
|
+
].reject(&:blank?)
|
700
|
+
end
|
701
|
+
|
702
|
+
# Used by #lines
|
703
|
+
#
|
704
|
+
# Instead of using `state` method (which is really state_code). That's fine for some countries
|
705
|
+
# like US, Canada, Australia but not other countries (presumably).
|
706
|
+
#
|
707
|
+
# TODO: Put postal code and city in a different order, as that country's conventions dictate.
|
708
|
+
# See http://bitboost.com/ref/international-address-formats/new-zealand/
|
709
|
+
#
|
710
|
+
def city_line
|
711
|
+
[
|
712
|
+
#[city, state].reject(&:blank?).join(', '),
|
713
|
+
[city, state_for_postal_address].reject(&:blank?).join(', '),
|
714
|
+
postal_code,
|
715
|
+
].reject(&:blank?).join(' ')
|
716
|
+
end
|
717
|
+
|
718
|
+
def city_state_code
|
719
|
+
[city, state_code].reject(&:blank?).join(', ')
|
720
|
+
end
|
721
|
+
|
722
|
+
def city_state_name
|
723
|
+
[city, state_name].reject(&:blank?).join(', ')
|
724
|
+
end
|
725
|
+
|
726
|
+
def city_state_country
|
727
|
+
[city_state_name, country_name].join(', ')
|
728
|
+
end
|
729
|
+
|
730
|
+
def state_for_postal_address
|
731
|
+
# Possibly others use a code? But seems safer to default to a name until confirmed that they use
|
732
|
+
# a code.
|
733
|
+
if country_name.in? ['United States', 'Canada', 'Australia']
|
734
|
+
state_code
|
735
|
+
elsif state_possibly_included_in_postal_address?
|
736
|
+
state_name
|
737
|
+
else
|
738
|
+
''
|
739
|
+
end
|
740
|
+
end
|
741
|
+
|
742
|
+
#════════════════════════════════════════════════════════════════════════════════════════════════════
|
743
|
+
# Misc. output
|
744
|
+
|
745
|
+
# TODO: remove?
|
746
|
+
def parts
|
747
|
+
[
|
748
|
+
#name,
|
749
|
+
*address_lines,
|
750
|
+
city,
|
751
|
+
state_name,
|
752
|
+
postal_code,
|
753
|
+
country_name,
|
754
|
+
].reject(&:blank?)
|
755
|
+
end
|
756
|
+
|
757
|
+
# def inspect
|
758
|
+
# inspect_base(
|
759
|
+
# :id,
|
760
|
+
# #:name,
|
761
|
+
# :address,
|
762
|
+
# # address_2 ...
|
763
|
+
# :city,
|
764
|
+
# :state,
|
765
|
+
# :postal_code,
|
766
|
+
# :country,
|
767
|
+
# )
|
768
|
+
# end
|
769
|
+
|
770
|
+
def inspect
|
771
|
+
inspect_base(
|
772
|
+
:id,
|
773
|
+
address_attributes
|
774
|
+
)
|
775
|
+
end
|
776
|
+
|
777
|
+
#─────────────────────────────────────────────────────────────────────────────────────────────────
|
778
|
+
end
|
779
|
+
end
|
780
|
+
end
|
781
|
+
|
782
|
+
ActiveRecord::Base.class_eval do
|
783
|
+
include AddressConcern::Address::Base
|
784
|
+
end
|