has_localization_table 0.0.4 → 0.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.
- data/README.md +22 -6
- data/lib/has_localization_table.rb +7 -5
- data/lib/has_localization_table/active_record.rb +4 -171
- data/lib/has_localization_table/class_methods.rb +121 -0
- data/lib/has_localization_table/config.rb +4 -0
- data/lib/has_localization_table/instance_methods.rb +78 -0
- data/lib/has_localization_table/version.rb +1 -1
- data/spec/active_record_spec.rb +28 -28
- metadata +11 -9
data/README.md
CHANGED
@@ -52,21 +52,37 @@ The gem assumes that the localization table has already been migrated, and the m
|
|
52
52
|
### `has_localization_table` Arguments
|
53
53
|
If given, the first argument is the name used for the association, otherwise it defaults to `strings`.
|
54
54
|
|
55
|
-
* `class_name` (default: base class name +
|
55
|
+
* `class_name` (default: base class name + class_suffix) - the name of the localization class.
|
56
56
|
* `required` (default: false) - if true, at least a localization object for the primary language (see Configuration section) must be present or validation will fail
|
57
57
|
* `optional` (default: []) - if `required` is true, can be used to specify that specific attributes are optional
|
58
58
|
|
59
59
|
Any options that can be passed into `has_many` can also be passed along and will be used when creating the association.
|
60
60
|
|
61
61
|
## Configuration
|
62
|
-
`HasLocalizationTable` can also be configured as follows
|
62
|
+
`HasLocalizationTable` can also be configured as follows. Note that if any configuration option responds to `call`, it will be called.
|
63
63
|
|
64
64
|
HasLocalizationTable.configure do |config|
|
65
|
+
# Default suffix to use for Localization class names
|
66
|
+
# ie. Article -> ArticleLocalization
|
67
|
+
config.class_suffix = "Localization"
|
68
|
+
|
69
|
+
# Default localizations association name
|
70
|
+
config.default_association_name = :localizations
|
71
|
+
|
72
|
+
# Class name for Locale objects
|
65
73
|
config.locale_class = "Locale"
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
74
|
+
|
75
|
+
# Foreign key used in localization tables to relate to a Locale
|
76
|
+
config.locale_foreign_key = "locale_id"
|
77
|
+
|
78
|
+
# Primary (main) locale
|
79
|
+
config.primary_locale = ->{ Locale.primary_language }
|
80
|
+
|
81
|
+
# Current locale
|
82
|
+
config.current_locale = ->{ Locale.current_language }
|
83
|
+
|
84
|
+
# All available locales
|
85
|
+
config.all_locales = ->{ Locale.all }
|
70
86
|
end
|
71
87
|
|
72
88
|
## Contributing
|
@@ -1,15 +1,17 @@
|
|
1
1
|
require "has_localization_table/version"
|
2
2
|
require "has_localization_table/config"
|
3
|
+
require "has_localization_table/class_methods"
|
4
|
+
require "has_localization_table/instance_methods"
|
3
5
|
require "has_localization_table/active_record"
|
4
6
|
|
5
7
|
ActiveRecord::Base.extend(HasLocalizationTable::ActiveRecord) if defined?(ActiveRecord::Base)
|
6
8
|
|
7
9
|
module HasLocalizationTable
|
8
|
-
|
9
|
-
define_singleton_method
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
HasLocalizationTable.config.config.keys.each do |key|
|
11
|
+
define_singleton_method key do
|
12
|
+
val = config.send(key)
|
13
|
+
val = val.call if val.respond_to?(:call)
|
14
|
+
val
|
13
15
|
end
|
14
16
|
end
|
15
17
|
end
|
@@ -2,184 +2,17 @@ module HasLocalizationTable
|
|
2
2
|
module ActiveRecord
|
3
3
|
def has_localization_table(*args)
|
4
4
|
options = args.extract_options!
|
5
|
-
options[:association_name] = args.first
|
5
|
+
options[:association_name] = args.first || HasLocalizationTable.default_association_name
|
6
6
|
|
7
7
|
class_attribute :localization_table_options
|
8
|
-
self.localization_table_options = options
|
8
|
+
self.localization_table_options = { dependent: :delete_all, class_name: localization_class.name }.merge(options)
|
9
9
|
|
10
10
|
extend(ClassMethods)
|
11
11
|
include(InstanceMethods)
|
12
12
|
end
|
13
|
-
end
|
14
|
-
|
15
|
-
module ClassMethods
|
16
|
-
def self.extended(klass)
|
17
|
-
options = { dependent: :delete_all }.merge(klass.localization_table_options)
|
18
|
-
|
19
|
-
association_name = options.delete(:association_name) || :strings
|
20
|
-
|
21
|
-
# If class_name isn't explicitly defined, try adding String onto the current class name
|
22
|
-
options[:class_name] = klass.name + "String" if options[:class_name].blank? and (Module.const_get(klass.name + "String") rescue false)
|
23
|
-
|
24
|
-
# Define the association
|
25
|
-
klass.has_many association_name, options.except(:required, :optional)
|
26
|
-
association = klass.reflect_on_association(association_name)
|
27
|
-
|
28
|
-
klass.class_eval do
|
29
|
-
# Initialize string records after main record initialization
|
30
|
-
after_initialize do
|
31
|
-
build_missing_strings
|
32
|
-
end
|
33
|
-
|
34
|
-
before_validation do
|
35
|
-
reject_empty_strings
|
36
|
-
build_missing_strings
|
37
|
-
end
|
38
|
-
|
39
|
-
# Reject any blank strings before saving the record
|
40
|
-
# Validation will have happened by this point, so if there is a required string that is needed, it won't be rejected
|
41
|
-
before_save do
|
42
|
-
reject_empty_strings
|
43
|
-
end
|
44
|
-
|
45
|
-
# Add validation to ensure a string for the primary locale exists if the string is required
|
46
|
-
validate do
|
47
|
-
if self.class.localization_table_options[:required] || false
|
48
|
-
errors.add(association_name, :primary_lang_string_required) unless send(association_name).any? do |string|
|
49
|
-
string.send(HasLocalizationTable.config.locale_foreign_key) == HasLocalizationTable.primary_locale.id
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
define_method :build_missing_strings do
|
55
|
-
locale_ids = HasLocalizationTable.all_locales.map(&:id)
|
56
|
-
HasLocalizationTable.all_locales.each do |l|
|
57
|
-
send(association_name).build(HasLocalizationTable.config.locale_foreign_key => l.id) unless send(association_name).detect{ |str| str.send(HasLocalizationTable.config.locale_foreign_key) == l.id }
|
58
|
-
send(association_name).sort_by!{ |s| locale_ids.index(s.send(HasLocalizationTable.config.locale_foreign_key)) || 0 }
|
59
|
-
end
|
60
|
-
end
|
61
|
-
private :build_missing_strings
|
62
|
-
|
63
|
-
define_method :reject_empty_strings do
|
64
|
-
send(association_name).reject! { |s| !s.persisted? and self.class.localized_attributes.all?{ |attr| s.send(attr).blank? } }
|
65
|
-
end
|
66
|
-
private :reject_empty_strings
|
67
|
-
|
68
|
-
# Find a record by multiple string values
|
69
|
-
define_singleton_method :find_by_localized_attributes do |attributes, locale = HasLocalizationTable.current_locale|
|
70
|
-
string_record = association.klass.where({ HasLocalizationTable.config.locale_foreign_key => locale.id }.merge(attributes)).first
|
71
|
-
string_record.send(klass.to_s.underscore.to_sym) rescue nil
|
72
|
-
end
|
73
|
-
private_class_method :find_by_localized_attributes
|
74
|
-
end
|
75
|
-
|
76
|
-
klass.localized_attributes.each do |attribute|
|
77
|
-
# Add validation to make all string fields required for the primary locale
|
78
|
-
association.klass.class_eval do
|
79
|
-
validates attribute, presence: { message: :custom_this_field_is_required },
|
80
|
-
if: proc { |model| klass.name.constantize.localized_attribute_required?(attribute) && model.send(HasLocalizationTable.config.locale_foreign_key) == HasLocalizationTable.current_locale.id }
|
81
|
-
end
|
82
|
-
|
83
|
-
# Set up accessors and ordering named_scopes for each non-FK attribute on the base model
|
84
|
-
klass.class_eval do
|
85
|
-
define_method attribute do |locale = HasLocalizationTable.current_locale|
|
86
|
-
# Try to load a string for the given locale
|
87
|
-
# If that fails, try for the primary locale
|
88
|
-
get_cached_localized_attribute(locale, association_name, attribute) || get_cached_localized_attribute(HasLocalizationTable.primary_locale, association_name, attribute)
|
89
|
-
end
|
90
|
-
|
91
|
-
define_method "#{attribute}=" do |value|
|
92
|
-
cache_localized_attribute(HasLocalizationTable.current_locale, association_name, attribute, value)
|
93
|
-
end
|
94
|
-
|
95
|
-
define_singleton_method "ordered_by_#{attribute}" do |direction = :asc|
|
96
|
-
direction = direction == :asc ? "ASC" : "DESC"
|
97
|
-
|
98
|
-
joins(%{
|
99
|
-
LEFT OUTER JOIN #{association.table_name}
|
100
|
-
ON #{association.table_name}.#{association.foreign_key} = #{self.table_name}.#{self.primary_key}
|
101
|
-
AND #{association.table_name}.#{HasLocalizationTable.config.locale_foreign_key} = %d
|
102
|
-
} % HasLocalizationTable.current_locale.id
|
103
|
-
).
|
104
|
-
order( "#{association.table_name}.#{attribute} #{direction}")
|
105
|
-
#order{ Squeel::Nodes::Order.new(Squeel::Nodes::Stub.new(association.table_name).send(attribute), direction) }
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
def localized_attributes
|
112
|
-
# Determine which attributes of the association model should be accessable through the base class
|
113
|
-
# ie. everything that's not a primary key, foreign key, or timestamp attribute
|
114
|
-
association_name = self.localization_table_options[:association_name] || :strings
|
115
|
-
association = reflect_on_association(association_name)
|
116
|
-
|
117
|
-
attribute_names = association.klass.attribute_names
|
118
|
-
timestamp_attrs = association.klass.new.send(:all_timestamp_attributes_in_model).map(&:to_s)
|
119
|
-
foreign_keys = association.klass.reflect_on_all_associations.map{ |a| a.association_foreign_key }
|
120
|
-
primary_keys = [association.klass.primary_key]
|
121
|
-
# protected_attrs = association.klass.protected_attributes.to_a
|
122
|
-
|
123
|
-
(attribute_names - timestamp_attrs - foreign_keys - primary_keys).map(&:to_sym)
|
124
|
-
end
|
125
|
-
|
126
|
-
def localized_attribute_required?(attribute)
|
127
|
-
return false unless localization_table_options[:required] || false
|
128
|
-
return true unless localization_table_options[:optional]
|
129
|
-
|
130
|
-
!localization_table_options[:optional].include?(attribute)
|
131
|
-
end
|
132
13
|
|
133
|
-
def
|
134
|
-
|
135
|
-
attributes = $1.split("_and_").map(&:to_sym)
|
136
|
-
if (attributes & localized_attributes).size == attributes.size and args.size == attributes.size
|
137
|
-
raise ArgumentError, "expected #{attributes.size} #{"argument".pluralize(attributes.size)}" unless args.size == attributes.size
|
138
|
-
args = attributes.zip(args).inject({}) { |memo, (key, val)| memo[key] = val; memo }
|
139
|
-
return find_by_localized_attributes(args)
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
super
|
144
|
-
end
|
145
|
-
|
146
|
-
def respond_to?(*args)
|
147
|
-
if args.first.to_s =~ /\Afind_by_([a-z0-9_]+(_and_[a-z0-9_]+)*)\Z/
|
148
|
-
attributes = $1.split("_and_").map(&:to_sym)
|
149
|
-
return ((attributes & localized_attributes).size == attributes.size)
|
150
|
-
end
|
151
|
-
|
152
|
-
super
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
module InstanceMethods
|
157
|
-
private
|
158
|
-
# Both strings and the associations are memoized, so that if an association adds more than one attribute to the main model, the association doesn't need
|
159
|
-
# to be loaded each time a different attribute is accessed.
|
160
|
-
def get_cached_localized_attribute(locale, association, attribute)
|
161
|
-
@_localized_attribute_cache ||= {}
|
162
|
-
@_localized_attribute_cache[attribute] ||= {}
|
163
|
-
|
164
|
-
@_localized_association_cache ||= {}
|
165
|
-
@_localized_association_cache[association] ||= {}
|
166
|
-
|
167
|
-
@_localized_attribute_cache[attribute][locale.id] ||= begin
|
168
|
-
@_localized_association_cache[association][locale.id] ||= send(association).detect{ |a| a.send(HasLocalizationTable.config.locale_foreign_key) == locale.id }
|
169
|
-
s = @_localized_association_cache[association][locale.id].send(attribute) rescue nil
|
170
|
-
s.blank? ? nil : s
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
def cache_localized_attribute(locale, association, attribute, value)
|
175
|
-
string = send(association).detect{ |a| a.send(HasLocalizationTable.config.locale_foreign_key) == locale.id } || send(association).build(HasLocalizationTable.config.locale_foreign_key => locale.id)
|
176
|
-
value = value.to_s
|
177
|
-
|
178
|
-
string.send(:"#{attribute}=", value)
|
179
|
-
|
180
|
-
@_localized_attribute_cache ||= {}
|
181
|
-
@_localized_attribute_cache[attribute] ||= {}
|
182
|
-
@_localized_attribute_cache[attribute][locale.id] = value.blank? ? nil : value
|
14
|
+
def localization_class
|
15
|
+
(self.name + HasLocalizationTable.class_suffix).constantize
|
183
16
|
end
|
184
17
|
end
|
185
18
|
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module HasLocalizationTable
|
2
|
+
module ClassMethods
|
3
|
+
def self.extended(klass)
|
4
|
+
klass.class_eval do
|
5
|
+
create_localization_association!
|
6
|
+
|
7
|
+
# Initialize string records after main record initialization
|
8
|
+
after_initialize do
|
9
|
+
build_missing_localizations!
|
10
|
+
end
|
11
|
+
|
12
|
+
before_validation do
|
13
|
+
reject_empty_localizations!
|
14
|
+
build_missing_localizations!
|
15
|
+
end
|
16
|
+
|
17
|
+
# Reject any blank strings before saving the record
|
18
|
+
# Validation will have happened by this point, so if there is a required string that is needed, it won't be rejected
|
19
|
+
before_save do
|
20
|
+
reject_empty_localizations!
|
21
|
+
end
|
22
|
+
|
23
|
+
# Add validation to ensure a string for the primary locale exists if the string is required
|
24
|
+
validate do
|
25
|
+
if localization_table_options[:required] || false
|
26
|
+
errors.add(localization_association_name, :primary_lang_string_required) unless localization_association.any? do |string|
|
27
|
+
string.send(HasLocalizationTable.locale_foreign_key) == HasLocalizationTable.primary_locale.id
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
klass.localized_attributes.each do |attribute|
|
34
|
+
# Add validation to make all string fields required for the primary locale
|
35
|
+
klass.send(:localization_class).class_eval do
|
36
|
+
validates attribute, presence: { message: :custom_this_field_is_required },
|
37
|
+
if: proc { |model| klass.name.constantize.localized_attribute_required?(attribute) && model.send(HasLocalizationTable.locale_foreign_key) == HasLocalizationTable.current_locale.id }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Alias the scoping method to use the actual association name
|
42
|
+
alias_method :"with_#{klass.localization_association_name}", :with_localizations
|
43
|
+
end
|
44
|
+
|
45
|
+
def localization_association_name
|
46
|
+
localization_table_options[:association_name]
|
47
|
+
end
|
48
|
+
|
49
|
+
def localized_attributes
|
50
|
+
# Determine which attributes of the association model should be accessable through the base class
|
51
|
+
# ie. everything that's not a primary key, foreign key, or timestamp attribute
|
52
|
+
association_name = self.localization_table_options[:association_name] || :strings
|
53
|
+
association = reflect_on_association(association_name)
|
54
|
+
|
55
|
+
attribute_names = association.klass.attribute_names
|
56
|
+
timestamp_attrs = association.klass.new.send(:all_timestamp_attributes_in_model).map(&:to_s)
|
57
|
+
foreign_keys = association.klass.reflect_on_all_associations.map{ |a| a.association_foreign_key }
|
58
|
+
primary_keys = [association.klass.primary_key]
|
59
|
+
# protected_attrs = association.klass.protected_attributes.to_a
|
60
|
+
|
61
|
+
(attribute_names - timestamp_attrs - foreign_keys - primary_keys).map(&:to_sym)
|
62
|
+
end
|
63
|
+
|
64
|
+
def localized_attribute_required?(attribute)
|
65
|
+
return false unless localization_table_options[:required] || false
|
66
|
+
return true unless localization_table_options[:optional]
|
67
|
+
|
68
|
+
!localization_table_options[:optional].include?(attribute)
|
69
|
+
end
|
70
|
+
|
71
|
+
def method_missing(name, *args, &block)
|
72
|
+
if name.to_s =~ /\Afind_by_([a-z0-9_]+(_and_[a-z0-9_]+)*)\Z/
|
73
|
+
attributes = $1.split("_and_").map(&:to_sym)
|
74
|
+
if (attributes & localized_attributes).size == attributes.size
|
75
|
+
raise ArgumentError, "expected #{attributes.size} #{"argument".pluralize(attributes.size)}: #{attributes.join(", ")}" unless args.size == attributes.size
|
76
|
+
args = attributes.zip(args).inject({}) { |memo, (key, val)| memo[key] = val; memo }
|
77
|
+
return find_by_localized_attributes(args)
|
78
|
+
end
|
79
|
+
elsif name.to_s =~ /\Aordered_by_([a-z0-9_]+)\Z/
|
80
|
+
attribute = $1.to_sym
|
81
|
+
return ordered_by_localized_attribute(attribute, *args) if localized_attributes.include?(attribute)
|
82
|
+
end
|
83
|
+
|
84
|
+
super
|
85
|
+
end
|
86
|
+
|
87
|
+
def respond_to?(*args)
|
88
|
+
if args.first.to_s =~ /\Afind_by_([a-z0-9_]+(_and_[a-z0-9_]+)*)\Z/
|
89
|
+
attributes = $1.split("_and_").map(&:to_sym)
|
90
|
+
return true if (attributes & localized_attributes).size == attributes.size
|
91
|
+
elsif name.to_s =~ /\Aordered_by_([a-z0-9_]+)\Z/
|
92
|
+
return true if localized_attributes.include?($1.to_sym)
|
93
|
+
end
|
94
|
+
|
95
|
+
super
|
96
|
+
end
|
97
|
+
|
98
|
+
def with_localizations
|
99
|
+
scoped.joins((<<-eoq % HasLocalizationTable.current_locale.id).gsub(/\s+/, " "))
|
100
|
+
LEFT OUTER JOIN #{localization_class.table_name}
|
101
|
+
ON #{localization_class.table_name}.#{self.name.underscore}_id = #{self.table_name}.#{self.primary_key}
|
102
|
+
AND #{localization_class.table_name}.#{HasLocalizationTable.locale_foreign_key} = %d
|
103
|
+
eoq
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
def create_localization_association!
|
108
|
+
self.has_many localization_association_name, localization_table_options.except(:association_name, :required, :optional)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Find a record by multiple localization values
|
112
|
+
def find_by_localized_attributes(attributes, locale = HasLocalizationTable.current_locale)
|
113
|
+
with_localizations.where(localization_class.table_name => attributes).first
|
114
|
+
end
|
115
|
+
|
116
|
+
# Order records by localization value
|
117
|
+
def ordered_by_localized_attribute(attribute, asc = true, locale = HasLocalizationTable.current_locale)
|
118
|
+
with_localizations.order("#{localization_class.table_name}.#{attribute} #{asc ? "ASC" : "DESC"}")
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -25,12 +25,16 @@ module HasLocalizationTable
|
|
25
25
|
config_accessor :primary_locale
|
26
26
|
config_accessor :current_locale
|
27
27
|
config_accessor :all_locales
|
28
|
+
config_accessor :class_suffix
|
29
|
+
config_accessor :default_association_name
|
28
30
|
end
|
29
31
|
|
30
32
|
# this is ugly. why can't we pass the default value to config_accessor...?
|
31
33
|
configure do |config|
|
32
34
|
config.locale_class = "Locale"
|
33
35
|
config.locale_foreign_key = "locale_id"
|
36
|
+
config.class_suffix = "Localization"
|
37
|
+
config.default_association_name = :localizations
|
34
38
|
config.primary_locale = ->{ config.locale_class.constantize.first }
|
35
39
|
config.current_locale = ->{ config.locale_class.constantize.first }
|
36
40
|
config.all_locales = ->{ config.locale_class.constantize.all }
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module HasLocalizationTable
|
2
|
+
module InstanceMethods
|
3
|
+
def read_localized_attribute(attribute, locale = HasLocalizationTable.current_locale)
|
4
|
+
attribute_cache[attribute.to_sym][locale.id] ||= localization_association.detect{ |a| a.send(HasLocalizationTable.locale_foreign_key) == locale.id }.send(attribute) rescue nil
|
5
|
+
end
|
6
|
+
|
7
|
+
def write_localized_attribute(attribute, value, locale = HasLocalizationTable.current_locale)
|
8
|
+
value = value.to_s
|
9
|
+
localization = localization_association.detect{ |a| a.send(HasLocalizationTable.locale_foreign_key) == locale.id } ||
|
10
|
+
localization_association.build(HasLocalizationTable.locale_foreign_key => locale.id)
|
11
|
+
|
12
|
+
localization.send(:"#{attribute}=", value)
|
13
|
+
attribute_cache[attribute.to_sym][locale.id] = value.blank? ? nil : value
|
14
|
+
end
|
15
|
+
|
16
|
+
# Define attribute getters and setters
|
17
|
+
def method_missing(name, *args, &block)
|
18
|
+
if name.to_s =~ /\A([a-z0-9_]+)(=)?\Z/i
|
19
|
+
if localized_attributes.include?($1.to_sym)
|
20
|
+
if $2.nil? # No equals sign -- not a setter
|
21
|
+
# Try to load a string for the given locale
|
22
|
+
# If that fails, try for the primary locale
|
23
|
+
raise ArgumentError, "wrong number of arguments (#{args.size} for 0 or 1)" unless args.size.between?(0, 1)
|
24
|
+
return read_localized_attribute($1, args.first) || read_localized_attribute($1, HasLocalizationTable.primary_locale)
|
25
|
+
else
|
26
|
+
raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" unless args.size == 1
|
27
|
+
return write_localized_attribute($1, args.first)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
def respond_to?(*args)
|
36
|
+
return true if args.first.to_s =~ /\A([a-z0-9_]+)=?\Z/i and localized_attributes.include?($1.to_sym)
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
# Add localization objects for any available locale that doesn't have one
|
42
|
+
def build_missing_localizations!
|
43
|
+
locale_ids = HasLocalizationTable.all_locales.map(&:id)
|
44
|
+
HasLocalizationTable.all_locales.each do |l|
|
45
|
+
unless localization_association.detect{ |str| str.send(HasLocalizationTable.locale_foreign_key) == l.id }
|
46
|
+
localization_association.build(HasLocalizationTable.locale_foreign_key => l.id)
|
47
|
+
end
|
48
|
+
|
49
|
+
localization_association.sort_by!{ |l| locale_ids.index(l.send(HasLocalizationTable.locale_foreign_key)) || 0 }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Remove localization objects that are not filled in
|
54
|
+
def reject_empty_localizations!
|
55
|
+
localization_association.reject! { |l| !l.persisted? and localized_attributes.all?{ |attr| l.send(attr).blank? } }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Helper method for getting the localization association without having to look up the name each time
|
59
|
+
def localization_association
|
60
|
+
@localization_association ||= begin
|
61
|
+
association_name = localization_table_options[:association_name]
|
62
|
+
send(association_name)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def attribute_cache
|
67
|
+
@localized_attribute_cache ||= localized_attributes.inject({}) { |memo, attr| memo[attr] = {}; memo }
|
68
|
+
end
|
69
|
+
|
70
|
+
def localized_attributes
|
71
|
+
self.class.localized_attributes
|
72
|
+
end
|
73
|
+
|
74
|
+
def localization_table_options
|
75
|
+
self.class.localization_table_options
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/spec/active_record_spec.rb
CHANGED
@@ -11,7 +11,7 @@ ActiveRecord::Migration.tap do |m|
|
|
11
11
|
t.string :name
|
12
12
|
end
|
13
13
|
|
14
|
-
m.create_table :
|
14
|
+
m.create_table :article_localizations do |t|
|
15
15
|
t.integer :article_id
|
16
16
|
t.integer :locale_id
|
17
17
|
t.string :name
|
@@ -24,7 +24,7 @@ Locale = Class.new(ActiveRecord::Base)
|
|
24
24
|
Locale.create!(name: "English")
|
25
25
|
Locale.create!(name: "French")
|
26
26
|
|
27
|
-
|
27
|
+
ArticleLocalization = Class.new(ActiveRecord::Base) do
|
28
28
|
belongs_to :article
|
29
29
|
belongs_to :locale
|
30
30
|
end
|
@@ -46,33 +46,33 @@ describe HasLocalizationTable do
|
|
46
46
|
end
|
47
47
|
|
48
48
|
it "should track any given options" do
|
49
|
-
Article.has_localization_table :
|
50
|
-
Article.localization_table_options.must_equal({ association_name: :
|
49
|
+
Article.has_localization_table :strings, required: true, optional: [:description]
|
50
|
+
Article.localization_table_options.slice(:association_name, :required, :optional).must_equal({ association_name: :strings, required: true, optional: [:description] })
|
51
51
|
end
|
52
52
|
|
53
|
-
it "should define has_many association on the base class with a default name of :
|
53
|
+
it "should define has_many association on the base class with a default name of :localizations" do
|
54
54
|
Article.has_localization_table
|
55
|
-
assoc = Article.reflect_on_association(:
|
55
|
+
assoc = Article.reflect_on_association(:localizations)
|
56
56
|
assoc.wont_be_nil
|
57
57
|
assoc.macro.must_equal :has_many
|
58
|
-
assoc.klass.must_equal
|
58
|
+
assoc.klass.must_equal ArticleLocalization
|
59
59
|
end
|
60
60
|
|
61
61
|
it "should use the given association name" do
|
62
|
-
Article.has_localization_table :
|
63
|
-
assoc = Article.reflect_on_association(:
|
62
|
+
Article.has_localization_table :strings
|
63
|
+
assoc = Article.reflect_on_association(:strings)
|
64
64
|
assoc.wont_be_nil
|
65
65
|
assoc.macro.must_equal :has_many
|
66
|
-
assoc.klass.must_equal
|
66
|
+
assoc.klass.must_equal ArticleLocalization
|
67
67
|
end
|
68
68
|
|
69
69
|
it "should use the given class" do
|
70
|
-
ArticleText = Class.new(
|
70
|
+
ArticleText = Class.new(ArticleLocalization)
|
71
71
|
Article.has_localization_table class_name: ArticleText
|
72
|
-
assoc = Article.reflect_on_association(:
|
72
|
+
assoc = Article.reflect_on_association(:localizations)
|
73
73
|
assoc.wont_be_nil
|
74
74
|
assoc.macro.must_equal :has_many
|
75
|
-
assoc.klass.
|
75
|
+
assoc.klass.must_equal ArticleText
|
76
76
|
|
77
77
|
Object.send(:remove_const, :ArticleText)
|
78
78
|
end
|
@@ -81,10 +81,10 @@ describe HasLocalizationTable do
|
|
81
81
|
Article.has_localization_table required: true
|
82
82
|
a = Article.new
|
83
83
|
refute a.valid?
|
84
|
-
a.errors[:
|
84
|
+
a.errors[:localizations].wont_be_empty
|
85
85
|
|
86
86
|
a = Article.new(description: "Wishing the world hello!")
|
87
|
-
s = a.
|
87
|
+
s = a.localizations.first
|
88
88
|
refute s.valid?
|
89
89
|
s.errors[:name].wont_be_empty
|
90
90
|
end
|
@@ -92,11 +92,11 @@ describe HasLocalizationTable do
|
|
92
92
|
it "should not add validations if given required: false" do
|
93
93
|
Article.has_localization_table required: false
|
94
94
|
a = Article.new
|
95
|
-
a.valid? or raise a.
|
96
|
-
a.errors[:
|
95
|
+
a.valid? or raise a.localizations.map(&:errors).inspect
|
96
|
+
a.errors[:localizations].must_be_empty
|
97
97
|
|
98
98
|
a = Article.new(description: "Wishing the world hello!")
|
99
|
-
s = a.
|
99
|
+
s = a.localizations.first
|
100
100
|
assert s.valid?
|
101
101
|
s.errors[:name].must_be_empty
|
102
102
|
end
|
@@ -105,10 +105,10 @@ describe HasLocalizationTable do
|
|
105
105
|
Article.has_localization_table
|
106
106
|
a = Article.new
|
107
107
|
assert a.valid?
|
108
|
-
a.errors[:
|
108
|
+
a.errors[:localizations].must_be_empty
|
109
109
|
|
110
110
|
a = Article.new(description: "Wishing the world hello!")
|
111
|
-
s = a.
|
111
|
+
s = a.localizations.first
|
112
112
|
assert s.valid?
|
113
113
|
s.errors[:name].must_be_empty
|
114
114
|
end
|
@@ -117,8 +117,8 @@ describe HasLocalizationTable do
|
|
117
117
|
Article.has_localization_table required: true, optional: [:description]
|
118
118
|
a = Article.new(name: "Test")
|
119
119
|
assert a.valid?
|
120
|
-
a.errors[:
|
121
|
-
assert a.
|
120
|
+
a.errors[:localizations].must_be_empty
|
121
|
+
assert a.localizations.all?{ |s| s.errors[:description].empty? }
|
122
122
|
end
|
123
123
|
end
|
124
124
|
|
@@ -132,8 +132,8 @@ describe HasLocalizationTable do
|
|
132
132
|
let(:a) { Article.new(name: "Test", description: "Description") }
|
133
133
|
|
134
134
|
it "should set localized attributes" do
|
135
|
-
a.
|
136
|
-
a.
|
135
|
+
a.localizations.first.name.must_equal "Test"
|
136
|
+
a.localizations.first.description.must_equal "Description"
|
137
137
|
end
|
138
138
|
|
139
139
|
it "should create accessor methods" do
|
@@ -153,8 +153,8 @@ describe HasLocalizationTable do
|
|
153
153
|
a.description = "Changed Description"
|
154
154
|
a.name.must_equal "Changed"
|
155
155
|
a.description.must_equal "Changed Description"
|
156
|
-
a.
|
157
|
-
a.
|
156
|
+
a.localizations.first.name.must_equal "Changed"
|
157
|
+
a.localizations.first.description.must_equal "Changed Description"
|
158
158
|
end
|
159
159
|
|
160
160
|
it "should use the current locale when setting" do
|
@@ -167,8 +167,8 @@ describe HasLocalizationTable do
|
|
167
167
|
a.name = "French Name"
|
168
168
|
a.description = "French Description"
|
169
169
|
|
170
|
-
eng = a.
|
171
|
-
fre = a.
|
170
|
+
eng = a.localizations.detect{ |s| s.locale_id == Locale.first.id }
|
171
|
+
fre = a.localizations.detect{ |s| s.locale_id == Locale.last.id }
|
172
172
|
|
173
173
|
eng.name.must_equal "Test"
|
174
174
|
eng.description.must_equal "Description"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: has_localization_table
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -13,7 +13,7 @@ date: 2012-08-15 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
16
|
-
requirement: &
|
16
|
+
requirement: &11803100 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 3.0.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *11803100
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: activerecord
|
27
|
-
requirement: &
|
27
|
+
requirement: &11802200 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 3.0.0
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *11802200
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: minitest
|
38
|
-
requirement: &
|
38
|
+
requirement: &11800680 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *11800680
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: sqlite3
|
49
|
-
requirement: &
|
49
|
+
requirement: &11866300 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,7 +54,7 @@ dependencies:
|
|
54
54
|
version: '0'
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *11866300
|
58
58
|
description: Automatically sets up usage of a relational table to contain user-created
|
59
59
|
multi-locale string attributes
|
60
60
|
email:
|
@@ -71,7 +71,9 @@ files:
|
|
71
71
|
- has_localization_table.gemspec
|
72
72
|
- lib/has_localization_table.rb
|
73
73
|
- lib/has_localization_table/active_record.rb
|
74
|
+
- lib/has_localization_table/class_methods.rb
|
74
75
|
- lib/has_localization_table/config.rb
|
76
|
+
- lib/has_localization_table/instance_methods.rb
|
75
77
|
- lib/has_localization_table/version.rb
|
76
78
|
- spec/active_record_spec.rb
|
77
79
|
- spec/spec_helper.rb
|