address_concern 2.0.1 → 2.1.0
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.
- 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
|