globalize-danibachar 5.2.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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/Appraisals +33 -0
  3. data/CHANGELOG.md +111 -0
  4. data/CONTRIBUTING.md +52 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE +22 -0
  7. data/README.md +437 -0
  8. data/Rakefile +55 -0
  9. data/lib/globalize.rb +98 -0
  10. data/lib/globalize/active_record.rb +14 -0
  11. data/lib/globalize/active_record/act_macro.rb +96 -0
  12. data/lib/globalize/active_record/adapter.rb +108 -0
  13. data/lib/globalize/active_record/adapter_dirty.rb +56 -0
  14. data/lib/globalize/active_record/attributes.rb +26 -0
  15. data/lib/globalize/active_record/class_methods.rb +129 -0
  16. data/lib/globalize/active_record/exceptions.rb +13 -0
  17. data/lib/globalize/active_record/instance_methods.rb +246 -0
  18. data/lib/globalize/active_record/migration.rb +215 -0
  19. data/lib/globalize/active_record/translated_attributes_query.rb +181 -0
  20. data/lib/globalize/active_record/translation.rb +45 -0
  21. data/lib/globalize/interpolation.rb +28 -0
  22. data/lib/globalize/version.rb +3 -0
  23. data/lib/i18n/missing_translations_log_handler.rb +41 -0
  24. data/lib/i18n/missing_translations_raise_handler.rb +25 -0
  25. data/lib/patches/active_record/persistence.rb +17 -0
  26. data/lib/patches/active_record/query_method.rb +3 -0
  27. data/lib/patches/active_record/rails4/query_method.rb +35 -0
  28. data/lib/patches/active_record/rails4/serialization.rb +22 -0
  29. data/lib/patches/active_record/rails4/uniqueness_validator.rb +42 -0
  30. data/lib/patches/active_record/rails5/uniqueness_validator.rb +47 -0
  31. data/lib/patches/active_record/rails5_1/serialization.rb +22 -0
  32. data/lib/patches/active_record/rails5_1/uniqueness_validator.rb +45 -0
  33. data/lib/patches/active_record/relation.rb +12 -0
  34. data/lib/patches/active_record/serialization.rb +5 -0
  35. data/lib/patches/active_record/uniqueness_validator.rb +7 -0
  36. data/lib/patches/active_record/xml_attribute_serializer.rb +23 -0
  37. metadata +264 -0
@@ -0,0 +1,129 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ module ClassMethods
4
+ delegate :translated_locales, :set_translations_table_name, :to => :translation_class
5
+
6
+ if ::ActiveRecord::VERSION::STRING < "5.0.0"
7
+ def columns_hash
8
+ super.except(*translated_attribute_names.map(&:to_s))
9
+ end
10
+ end
11
+
12
+ def with_locales(*locales)
13
+ all.merge translation_class.with_locales(*locales)
14
+ end
15
+
16
+ def with_translations(*locales)
17
+ locales = translated_locales if locales.empty?
18
+ preload(:translations).joins(:translations).readonly(false).with_locales(locales).tap do |query|
19
+ query.distinct! unless locales.flatten.one?
20
+ end
21
+ end
22
+
23
+ def with_required_attributes
24
+ warn 'with_required_attributes is deprecated and will be removed in the next release of Globalize.'
25
+ required_translated_attributes.inject(all) do |scope, name|
26
+ scope.where("#{translated_column_name(name)} IS NOT NULL")
27
+ end
28
+ end
29
+
30
+ def with_translated_attribute(name, value, locales = Globalize.fallbacks)
31
+ with_translations.where(
32
+ translated_column_name(name) => value,
33
+ translated_column_name(:locale) => Array(locales).map(&:to_s)
34
+ )
35
+ end
36
+
37
+ def translated?(name)
38
+ translated_attribute_names.include?(name.to_sym)
39
+ end
40
+
41
+ def required_attributes
42
+ warn 'required_attributes is deprecated and will be removed in the next release of Globalize.'
43
+ validators.map { |v| v.attributes if v.is_a?(ActiveModel::Validations::PresenceValidator) }.flatten
44
+ end
45
+
46
+ def required_translated_attributes
47
+ warn 'required_translated_attributes is deprecated and will be removed in the next release of Globalize.'
48
+ translated_attribute_names & required_attributes
49
+ end
50
+
51
+ def translation_class
52
+ @translation_class ||= begin
53
+ if self.const_defined?(:Translation, false)
54
+ klass = self.const_get(:Translation, false)
55
+ else
56
+ klass = self.const_set(:Translation, Class.new(Globalize::ActiveRecord::Translation))
57
+ end
58
+
59
+ klass.belongs_to :globalized_model,
60
+ class_name: self.name,
61
+ foreign_key: translation_options[:foreign_key],
62
+ inverse_of: :translations,
63
+ touch: translation_options.fetch(:touch, false)
64
+ klass
65
+ end
66
+ end
67
+
68
+ def translations_table_name
69
+ translation_class.table_name
70
+ end
71
+
72
+ def translated_column_name(name)
73
+ "#{translation_class.table_name}.#{name}"
74
+ end
75
+
76
+ private
77
+
78
+ # Override the default relation method in order to return a subclass
79
+ # of ActiveRecord::Relation with custom finder and calculation methods
80
+ # for translated attributes.
81
+ def relation
82
+ super.extending!(TranslatedAttributesQuery)
83
+ end
84
+
85
+ protected
86
+
87
+ def define_translated_attr_reader(name)
88
+ define_method(name) do |*args|
89
+ Globalize::Interpolation.interpolate(name, self, args)
90
+ end
91
+ alias_method :"#{name}_before_type_cast", name
92
+ end
93
+
94
+ def define_translated_attr_writer(name)
95
+ define_method(:"#{name}=") do |value|
96
+ write_attribute(name, value)
97
+ end
98
+ end
99
+
100
+ def define_translated_attr_accessor(name)
101
+ attribute(name, ::ActiveRecord::Type::Value.new)
102
+ define_translated_attr_reader(name)
103
+ define_translated_attr_writer(name)
104
+ end
105
+
106
+ def define_translations_reader(name)
107
+ define_method(:"#{name}_translations") do
108
+ hash = translated_attribute_by_locale(name)
109
+ globalize.stash.keys.each_with_object(hash) do |locale, result|
110
+ result[locale] = globalize.fetch_stash(locale, name) if globalize.stash_contains?(locale, name)
111
+ end
112
+ end
113
+ end
114
+
115
+ def define_translations_writer(name)
116
+ define_method(:"#{name}_translations=") do |value|
117
+ value.each do |(locale, _value)|
118
+ write_attribute name, _value, :locale => locale
119
+ end
120
+ end
121
+ end
122
+
123
+ def define_translations_accessor(name)
124
+ define_translations_reader(name)
125
+ define_translations_writer(name)
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,13 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ module Exceptions
4
+ class MigrationError < StandardError; end
5
+
6
+ class BadFieldName < MigrationError
7
+ def initialize(field)
8
+ super("Missing translated field #{field.inspect}")
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,246 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ module InstanceMethods
4
+ delegate :translated_locales, :to => :translations
5
+
6
+ def globalize
7
+ @globalize ||= Adapter.new(self)
8
+ end
9
+
10
+ def attributes
11
+ super.merge(translated_attributes)
12
+ end
13
+
14
+ def attributes=(new_attributes, *options)
15
+ super unless new_attributes.respond_to?(:stringify_keys) && new_attributes.present?
16
+ attributes = new_attributes.stringify_keys
17
+ with_given_locale(attributes) { super(attributes.except("locale"), *options) }
18
+ end
19
+
20
+ if Globalize.rails_52?
21
+
22
+ # In Rails 5.2 we need to override *_assign_attributes* as it's called earlier
23
+ # in the stack (before *assign_attributes*)
24
+ # See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/attribute_assignment.rb#L11
25
+ def _assign_attributes(new_attributes)
26
+ attributes = new_attributes.stringify_keys
27
+ with_given_locale(attributes) { super(attributes.except("locale")) }
28
+ end
29
+
30
+ else
31
+
32
+ def assign_attributes(new_attributes, *options)
33
+ super unless new_attributes.respond_to?(:stringify_keys) && new_attributes.present?
34
+ attributes = new_attributes.stringify_keys
35
+ with_given_locale(attributes) { super(attributes.except("locale"), *options) }
36
+ end
37
+
38
+ end
39
+
40
+ def write_attribute(name, value, *args, &block)
41
+ return super(name, value, *args, &block) unless translated?(name)
42
+
43
+ options = {:locale => Globalize.locale}.merge(args.first || {})
44
+
45
+ globalize.write(options[:locale], name, value)
46
+ end
47
+
48
+ def [](attr_name)
49
+ if translated?(attr_name)
50
+ read_attribute(attr_name)
51
+ else
52
+ read_attribute(attr_name) { |n| missing_attribute(n, caller) }
53
+ end
54
+ end
55
+
56
+ def read_attribute(attr_name, options = {}, &block)
57
+ name = if self.class.attribute_alias?(attr_name)
58
+ self.class.attribute_alias(attr_name).to_s
59
+ else
60
+ attr_name.to_s
61
+ end
62
+
63
+ name = self.class.primary_key if name == "id".freeze && self.class.primary_key
64
+
65
+ _read_attribute(name, options, &block)
66
+ end
67
+
68
+ def _read_attribute(attr_name, options = {}, &block)
69
+ translated_value = read_translated_attribute(attr_name, options, &block)
70
+ translated_value.nil? ? super(attr_name, &block) : translated_value
71
+ end
72
+
73
+ def attribute_names
74
+ translated_attribute_names.map(&:to_s) + super
75
+ end
76
+
77
+ delegate :translated?, :to => :class
78
+
79
+ def translated_attributes
80
+ translated_attribute_names.inject({}) do |attributes, name|
81
+ attributes.merge(name.to_s => send(name))
82
+ end
83
+ end
84
+
85
+ # This method is basically the method built into Rails
86
+ # but we have to pass {:translated => false}
87
+ def untranslated_attributes
88
+ attribute_names.inject({}) do |attrs, name|
89
+ attrs[name] = read_attribute(name, {:translated => false}); attrs
90
+ end
91
+ end
92
+
93
+ def set_translations(options)
94
+ options.keys.each do |locale|
95
+ translation = translation_for(locale) ||
96
+ translations.build(:locale => locale.to_s)
97
+
98
+ options[locale].each do |key, value|
99
+ translation.send :"#{key}=", value
100
+ translation.globalized_model.send :"#{key}=", value
101
+ end
102
+ translation.save if persisted?
103
+ end
104
+ globalize.reset
105
+ end
106
+
107
+ def reload(options = nil)
108
+ translation_caches.clear
109
+ translated_attribute_names.each { |name| @attributes.reset(name.to_s) }
110
+ globalize.reset
111
+ super(options)
112
+ end
113
+
114
+ def initialize_dup(other)
115
+ @globalize = nil
116
+ @translation_caches = nil
117
+ super
118
+ other.each_locale_and_translated_attribute do |locale, name|
119
+ globalize.write(locale, name, other.globalize.fetch(locale, name) )
120
+ end
121
+ end
122
+
123
+ def translation
124
+ translation_for(::Globalize.locale)
125
+ end
126
+
127
+ def translation_for(locale, build_if_missing = true)
128
+ unless translation_caches[locale]
129
+ # Fetch translations from database as those in the translation collection may be incomplete
130
+ _translation = translations.detect{|t| t.locale.to_s == locale.to_s}
131
+ _translation ||= translations.with_locale(locale).first unless translations.loaded?
132
+ _translation ||= translations.build(:locale => locale) if build_if_missing
133
+ translation_caches[locale] = _translation if _translation
134
+ end
135
+ translation_caches[locale]
136
+ end
137
+
138
+ def translation_caches
139
+ @translation_caches ||= {}
140
+ end
141
+
142
+ def translations_by_locale
143
+ translations.each_with_object(HashWithIndifferentAccess.new) do |t, hash|
144
+ hash[t.locale] = block_given? ? yield(t) : t
145
+ end
146
+ end
147
+
148
+ def translated_attribute_by_locale(name)
149
+ translations_by_locale(&:"#{name}")
150
+ end
151
+
152
+ # Get available locales from translations association, without a separate distinct query
153
+ def available_locales
154
+ translations.map(&:locale).uniq
155
+ end
156
+
157
+ def globalize_fallbacks(locale)
158
+ Globalize.fallbacks(locale)
159
+ end
160
+
161
+ def save(*)
162
+ result = Globalize.with_locale(translation.locale || I18n.default_locale) do
163
+ without_fallbacks do
164
+ super
165
+ end
166
+ end
167
+ if result
168
+ globalize.clear_dirty
169
+ end
170
+
171
+ result
172
+ end
173
+
174
+ def column_for_attribute name
175
+ return super if translated_attribute_names.exclude?(name)
176
+
177
+ globalize.send(:column_for_attribute, name)
178
+ end
179
+
180
+ def cache_key
181
+ [super, translation.cache_key].join("/")
182
+ end
183
+
184
+ def changed?
185
+ changed_attributes.present? || translations.any?(&:changed?)
186
+ end
187
+
188
+ # need to access instance variable directly since changed_attributes
189
+ # is frozen as of Rails 4.2
190
+ def original_changed_attributes
191
+ @changed_attributes
192
+ end
193
+
194
+ protected
195
+
196
+ def each_locale_and_translated_attribute
197
+ used_locales.each do |locale|
198
+ translated_attribute_names.each do |name|
199
+ yield locale, name
200
+ end
201
+ end
202
+ end
203
+
204
+ def used_locales
205
+ locales = globalize.stash.keys.concat(globalize.stash.keys).concat(translations.translated_locales)
206
+ locales.uniq!
207
+ locales
208
+ end
209
+
210
+ def save_translations!
211
+ globalize.save_translations!
212
+ translation_caches.clear
213
+ end
214
+
215
+ def with_given_locale(_attributes, &block)
216
+ attributes = _attributes.stringify_keys
217
+
218
+ if locale = attributes.try(:delete, "locale")
219
+ Globalize.with_locale(locale, &block)
220
+ else
221
+ yield
222
+ end
223
+ end
224
+
225
+ def without_fallbacks
226
+ before = self.fallbacks_for_empty_translations
227
+ self.fallbacks_for_empty_translations = false
228
+ yield
229
+ ensure
230
+ self.fallbacks_for_empty_translations = before
231
+ end
232
+
233
+ # nil or value
234
+ def read_translated_attribute(name, options)
235
+ options = {:translated => true, :locale => nil}.merge(options)
236
+ return nil unless options[:translated]
237
+ return nil unless translated?(name)
238
+
239
+ value = globalize.fetch(options[:locale] || Globalize.locale, name)
240
+ return nil if value.nil?
241
+
242
+ block_given? ? yield(value) : value
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,215 @@
1
+ require 'digest/sha1'
2
+
3
+ module Globalize
4
+ module ActiveRecord
5
+ module Migration
6
+ def globalize_migrator
7
+ @globalize_migrator ||= Migrator.new(self)
8
+ end
9
+
10
+ delegate :create_translation_table!, :add_translation_fields!,
11
+ :drop_translation_table!, :translation_index_name,
12
+ :translation_locale_index_name, :to => :globalize_migrator
13
+
14
+ class Migrator
15
+ include Globalize::ActiveRecord::Exceptions
16
+
17
+ attr_reader :model
18
+ delegate :translated_attribute_names, :connection, :table_name,
19
+ :table_name_prefix, :translations_table_name, :columns, :to => :model
20
+
21
+ def initialize(model)
22
+ @model = model
23
+ end
24
+
25
+ def fields
26
+ @fields ||= complete_translated_fields
27
+ end
28
+
29
+ def create_translation_table!(fields = {}, options = {})
30
+ extra = options.keys - [:migrate_data, :remove_source_columns, :unique_index]
31
+ if extra.any?
32
+ raise ArgumentError, "Unknown migration #{'option'.pluralize(extra.size)}: #{extra}"
33
+ end
34
+ @fields = fields
35
+ # If we have fields we only want to create the translation table with those fields
36
+ complete_translated_fields if fields.blank?
37
+ validate_translated_fields
38
+
39
+ create_translation_table
40
+ add_translation_fields!(fields, options)
41
+ create_translations_index(options)
42
+ clear_schema_cache!
43
+ end
44
+
45
+ def add_translation_fields!(fields, options = {})
46
+ @fields = fields
47
+ validate_translated_fields
48
+ add_translation_fields
49
+ clear_schema_cache!
50
+ move_data_to_translation_table if options[:migrate_data]
51
+ remove_source_columns if options[:remove_source_columns]
52
+ clear_schema_cache!
53
+ end
54
+
55
+ def remove_source_columns
56
+ column_names = *fields.keys
57
+ column_names.each do |column|
58
+ if connection.column_exists?(table_name, column)
59
+ connection.remove_column(table_name, column)
60
+ end
61
+ end
62
+ end
63
+
64
+ def drop_translation_table!(options = {})
65
+ move_data_to_model_table if options[:migrate_data]
66
+ drop_translations_index
67
+ drop_translation_table
68
+ clear_schema_cache!
69
+ end
70
+
71
+ # This adds all the current translated attributes of the model
72
+ # It's a problem because in early migrations would add all the translated attributes
73
+ def complete_translated_fields
74
+ translated_attribute_names.each do |name|
75
+ @fields[name] ||= column_type(name)
76
+ end
77
+ end
78
+
79
+ def create_translation_table
80
+ connection.create_table(translations_table_name) do |t|
81
+ t.references table_name.sub(/^#{table_name_prefix}/, '').singularize, :null => false, :index => false, :type => column_type(model.primary_key).to_sym
82
+ t.string :locale, :null => false
83
+ t.timestamps :null => false
84
+ end
85
+ end
86
+
87
+ def add_translation_fields
88
+ connection.change_table(translations_table_name) do |t|
89
+ fields.each do |name, options|
90
+ if options.is_a? Hash
91
+ t.column name, options.delete(:type), options
92
+ else
93
+ t.column name, options
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ def create_translations_index(options)
100
+ foreign_key = "#{table_name.sub(/^#{table_name_prefix}/, "").singularize}_id".to_sym
101
+ connection.add_index(
102
+ translations_table_name,
103
+ foreign_key,
104
+ :name => translation_index_name
105
+ )
106
+ # index for select('DISTINCT locale') call in translation.rb
107
+ connection.add_index(
108
+ translations_table_name,
109
+ :locale,
110
+ :name => translation_locale_index_name
111
+ )
112
+
113
+ if options[:unique_index]
114
+ connection.add_index(
115
+ translations_table_name,
116
+ [foreign_key, :locale],
117
+ :name => translation_unique_index_name,
118
+ unique: true
119
+ )
120
+ end
121
+ end
122
+
123
+ def drop_translation_table
124
+ connection.drop_table(translations_table_name)
125
+ end
126
+
127
+ def drop_translations_index
128
+ if connection.indexes(translations_table_name).map(&:name).include?(translation_index_name)
129
+ connection.remove_index(translations_table_name, :name => translation_index_name)
130
+ end
131
+ if connection.indexes(translations_table_name).map(&:name).include?(translation_locale_index_name)
132
+ connection.remove_index(translations_table_name, :name => translation_locale_index_name)
133
+ end
134
+ end
135
+
136
+ def move_data_to_translation_table
137
+ model.find_each do |record|
138
+ translation = record.translation_for(I18n.locale) || record.translations.build(:locale => I18n.locale)
139
+ fields.each do |attribute_name, attribute_type|
140
+ translation[attribute_name] = record.read_attribute(attribute_name, {:translated => false})
141
+ end
142
+ translation.save!
143
+ end
144
+ end
145
+
146
+ def move_data_to_model_table
147
+ add_missing_columns
148
+
149
+ # Find all of the translated attributes for all records in the model.
150
+ all_translated_attributes = model.all.collect{|m| m.attributes}
151
+ all_translated_attributes.each do |translated_record|
152
+ # Create a hash containing the translated column names and their values.
153
+ translated_attribute_names.inject(fields_to_update={}) do |f, name|
154
+ f.update({name.to_sym => translated_record[name.to_s]})
155
+ end
156
+
157
+ # Now, update the actual model's record with the hash.
158
+ model.where(model.primary_key.to_sym => translated_record[model.primary_key]).update_all(fields_to_update)
159
+ end
160
+ end
161
+
162
+ def validate_translated_fields
163
+ fields.each do |name, options|
164
+ raise BadFieldName.new(name) unless valid_field_name?(name)
165
+ end
166
+ end
167
+
168
+ def column_type(name)
169
+ columns.detect { |c| c.name == name.to_s }.try(:type) || :string
170
+ end
171
+
172
+ def valid_field_name?(name)
173
+ translated_attribute_names.include?(name)
174
+ end
175
+
176
+ def translation_index_name
177
+ truncate_index_name "index_#{translations_table_name}_on_#{table_name.singularize}_id"
178
+ end
179
+
180
+ def translation_locale_index_name
181
+ truncate_index_name "index_#{translations_table_name}_on_locale"
182
+ end
183
+
184
+ def translation_unique_index_name
185
+ truncate_index_name "index_#{translations_table_name}_on_#{table_name.singularize}_id_and_locale"
186
+ end
187
+
188
+ def clear_schema_cache!
189
+ connection.schema_cache.clear! if connection.respond_to? :schema_cache
190
+ model::Translation.reset_column_information
191
+ model.reset_column_information
192
+ end
193
+
194
+ private
195
+
196
+ def truncate_index_name(index_name)
197
+ if index_name.size < connection.index_name_length
198
+ index_name
199
+ else
200
+ "index_#{Digest::SHA1.hexdigest(index_name)}"[0, connection.index_name_length]
201
+ end
202
+ end
203
+
204
+ def add_missing_columns
205
+ clear_schema_cache!
206
+ translated_attribute_names.map(&:to_s).each do |attribute|
207
+ unless model.column_names.include?(attribute)
208
+ connection.add_column(table_name, attribute, model::Translation.columns_hash[attribute].type)
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end