hooktstudios-globalize3 0.2.0.beta8

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.
@@ -0,0 +1,27 @@
1
+ # Helper class for storing values per locale. Used by Globalize::Adapter
2
+ # to stash and cache attribute values.
3
+
4
+ module Globalize
5
+ module ActiveRecord
6
+ class Attributes < Hash # TODO: Think about using HashWithIndifferentAccess ?
7
+ def [](locale)
8
+ locale = locale.to_sym
9
+ self[locale] = {} unless has_key?(locale)
10
+ self.fetch(locale)
11
+ end
12
+
13
+ def contains?(locale, name)
14
+ self[locale].has_key?(name.to_s)
15
+ end
16
+
17
+ def read(locale, name)
18
+ self[locale][name.to_s]
19
+ end
20
+
21
+ def write(locale, name, value)
22
+ #raise 'z' if value.nil? # TODO
23
+ self[locale][name.to_s] = value
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,156 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ module ClassMethods
4
+ delegate :translated_locales, :set_translations_table_name, :to => :translation_class
5
+
6
+ def with_locales(*locales)
7
+ scoped.merge(translation_class.with_locales(*locales))
8
+ end
9
+
10
+ def with_translations(*locales)
11
+ locales = translated_locales if locales.empty?
12
+ includes(:translations).with_locales(locales).with_required_attributes
13
+ end
14
+
15
+ def with_required_attributes
16
+ required_translated_attributes.inject(scoped) do |scope, name|
17
+ scope.where("#{translated_column_name(name)} IS NOT NULL")
18
+ end
19
+ end
20
+
21
+ def with_translated_attribute(name, value, locales = nil)
22
+ locales ||= Globalize.fallbacks
23
+ with_translations.where(
24
+ translated_column_name(name) => value,
25
+ translated_column_name(:locale) => Array(locales).map(&:to_s)
26
+ )
27
+ end
28
+
29
+ def translated?(name)
30
+ translated_attribute_names.include?(name.to_sym)
31
+ end
32
+
33
+ def required_attributes
34
+ validators.map { |v| v.attributes if v.is_a?(ActiveModel::Validations::PresenceValidator) }.flatten
35
+ end
36
+
37
+ def required_translated_attributes
38
+ translated_attribute_names & required_attributes
39
+ end
40
+
41
+ def translation_class
42
+ @translation_class ||= begin
43
+ klass = self.const_get(:Translation) rescue nil
44
+ if klass.nil? || klass.class_name != (self.class_name + "Translation")
45
+ klass = self.const_set(:Translation, Class.new(Globalize::ActiveRecord::Translation))
46
+ end
47
+
48
+ klass.belongs_to name.underscore.gsub('/', '_')
49
+ klass
50
+ end
51
+ end
52
+
53
+ def translations_table_name
54
+ translation_class.table_name
55
+ end
56
+
57
+ def translated_column_name(name)
58
+ "#{translation_class.table_name}.#{name}"
59
+ end
60
+
61
+ if RUBY_VERSION < '1.9'
62
+ def respond_to?(method_id, *args, &block)
63
+ supported_on_missing?(method_id) || super
64
+ end
65
+ else
66
+ def respond_to_missing?(method_id, include_private = false)
67
+ supported_on_missing?(method_id) || super
68
+ end
69
+ end
70
+
71
+ def supported_on_missing?(method_id)
72
+ return super unless RUBY_VERSION < '1.9' || respond_to?(:translated_attribute_names)
73
+ match = ::ActiveRecord::DynamicFinderMatch.match(method_id) || ::ActiveRecord::DynamicScopeMatch.match(method_id)
74
+ return false if match.nil?
75
+
76
+ attribute_names = match.attribute_names.map(&:to_sym)
77
+ translated_attributes = attribute_names & translated_attribute_names
78
+ return false if translated_attributes.empty?
79
+
80
+ untranslated_attributes = attribute_names - translated_attributes
81
+ return false if untranslated_attributes.any?{|unt| ! respond_to?(:"scoped_by_#{unt}")}
82
+ return [match, attribute_names, translated_attributes, untranslated_attributes]
83
+ end
84
+
85
+ def method_missing(method_id, *arguments, &block)
86
+ match, attribute_names, translated_attributes, untranslated_attributes = supported_on_missing?(method_id)
87
+ return super unless match
88
+
89
+ scope = scoped
90
+
91
+ translated_attributes.each do |attr|
92
+ scope = scope.with_translated_attribute(attr, arguments[attribute_names.index(attr)])
93
+ end
94
+
95
+ untranslated_attributes.each do |unt|
96
+ index = attribute_names.index(unt)
97
+ raise StandarError unless index
98
+ scope = scope.send(:"scoped_by_#{unt}", arguments[index])
99
+ end
100
+
101
+ if match.is_a?(::ActiveRecord::DynamicFinderMatch)
102
+ if match.instantiator? and scope.blank?
103
+ return scope.find_or_instantiator_by_attributes match, attribute_names, *arguments, &block
104
+ end
105
+
106
+ return scope.send(match.finder).tap do |found|
107
+ found.is_a?(Array) ? found.map { |f| f.translations.reload } : found.translations.reload unless found.nil?
108
+ end
109
+ end
110
+ return scope
111
+ end
112
+
113
+ def find_or_instantiator_by_attributes(match, attributes, *args)
114
+ options = args.size > 1 && args.last(2).all?{ |a| a.is_a?(Hash) } ? args.extract_options! : {}
115
+ protected_attributes_for_create, unprotected_attributes_for_create = {}, {}
116
+ args.each_with_index do |arg, i|
117
+ if arg.is_a?(Hash)
118
+ protected_attributes_for_create = args[i].with_indifferent_access
119
+ else
120
+ unprotected_attributes_for_create[attributes[i]] = args[i]
121
+ end
122
+ end
123
+
124
+ record = if ::ActiveRecord::VERSION::STRING < "3.1.0"
125
+ class_name.constantize.new do |r|
126
+ r.send(:attributes=, protected_attributes_for_create, true) unless protected_attributes_for_create.empty?
127
+ r.send(:attributes=, unprotected_attributes_for_create, false) unless unprotected_attributes_for_create.empty?
128
+ end
129
+ else
130
+ class_name.constantize.new(protected_attributes_for_create, options) do |r|
131
+ r.assign_attributes(unprotected_attributes_for_create, :without_protection => true)
132
+ end
133
+ end
134
+ yield(record) if block_given?
135
+ record.send(match.bang? ? :save! : :save) if match.instantiator.eql?(:create)
136
+
137
+ record
138
+ end
139
+
140
+ protected
141
+
142
+ def translated_attr_accessor(name)
143
+ define_method(:"#{name}=") do |value|
144
+ write_attribute(name, value)
145
+ end
146
+ define_method(name) do |*args|
147
+ read_attribute(name, {:locale => args.first})
148
+ end
149
+ alias_method :"#{name}_before_type_cast", name
150
+ end
151
+
152
+ end
153
+
154
+ end
155
+
156
+ end
@@ -0,0 +1,19 @@
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
+
12
+ class BadFieldType < MigrationError
13
+ def initialize(name, type)
14
+ super("Bad field type for field #{name.inspect} (#{type.inspect}), should be :string or :text")
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,175 @@
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 self.included(base)
15
+ # Maintain Rails 3.0.x compatibility while adding Rails 3.1.x compatibility
16
+ if base.method_defined?(:assign_attributes)
17
+ base.class_eval %{
18
+ def assign_attributes(attributes, options = {})
19
+ with_given_locale(attributes) { super }
20
+ end
21
+ }
22
+ else
23
+ base.class_eval %{
24
+ def attributes=(attributes, *args)
25
+ with_given_locale(attributes) { super }
26
+ end
27
+
28
+ def update_attributes!(attributes, *args)
29
+ with_given_locale(attributes) { super }
30
+ end
31
+
32
+ def update_attributes(attributes, *args)
33
+ with_given_locale(attributes) { super }
34
+ end
35
+ }
36
+ end
37
+ end
38
+
39
+ def write_attribute(name, value, options = {})
40
+ # raise 'y' if value.nil? # TODO.
41
+
42
+ if translated?(name)
43
+ # Deprecate old use of locale
44
+ unless options.is_a?(Hash)
45
+ warn "[DEPRECATION] passing 'locale' as #{options.inspect} is deprecated. Please use {:locale => #{options.inspect}} instead."
46
+ options = {:locale => options}
47
+ end
48
+ options = {:locale => nil}.merge(options)
49
+ attribute_will_change! name.to_s
50
+ the_locale = options[:locale] || Globalize.locale
51
+ self.translations.reject!{|t| t.new_record? && t.locale != the_locale}
52
+ globalize.write(the_locale, name, value)
53
+ else
54
+ super(name, value)
55
+ end
56
+ end
57
+
58
+ def read_attribute(name, options = {})
59
+ # Deprecate old use of locale
60
+ unless options.is_a?(Hash)
61
+ warn "[DEPRECATION] passing 'locale' as #{options.inspect} is deprecated. Please use {:locale => #{options.inspect}} instead."
62
+ options = {:locale => options}
63
+ end
64
+
65
+ options = {:translated => true, :locale => nil}.merge(options)
66
+ if self.class.translated?(name) and options[:translated]
67
+ globalize.fetch(options[:locale] || Globalize.locale, name)
68
+ else
69
+ super(name)
70
+ end
71
+ end
72
+
73
+ def attribute_names
74
+ translated_attribute_names.map(&:to_s) + super
75
+ end
76
+
77
+ def translated?(name)
78
+ self.class.translated?(name)
79
+ end
80
+
81
+ def translated_attributes
82
+ translated_attribute_names.inject({}) do |attributes, name|
83
+ attributes.merge(name.to_s => translation.send(name))
84
+ end
85
+ end
86
+
87
+ # This method is basically the method built into Rails
88
+ # but we have to pass {:translated => false}
89
+ def untranslated_attributes
90
+ attrs = {}
91
+ attribute_names.each do |name|
92
+ attrs[name] = read_attribute(name, {:translated => false})
93
+ end
94
+ attrs
95
+ end
96
+
97
+ def set_translations(options)
98
+ options.keys.each do |locale|
99
+ translation = translation_for(locale) ||
100
+ translations.build(:locale => locale.to_s)
101
+ translation.update_attributes!(options[locale])
102
+ end
103
+ end
104
+
105
+ def reload(options = nil)
106
+ translated_attribute_names.each { |name| @attributes.delete(name.to_s) }
107
+ globalize.reset
108
+ super(options)
109
+ end
110
+
111
+ def clone
112
+ obj = super
113
+ return obj unless respond_to?(:translated_attribute_names)
114
+
115
+ obj.instance_variable_set(:@translations, nil) if new_record? # Reset the collection because of rails bug: http://pastie.org/1521874
116
+ obj.instance_variable_set(:@globalize, nil )
117
+ each_locale_and_translated_attribute do |locale, name|
118
+ obj.globalize.write(locale, name, globalize.fetch(locale, name) )
119
+ end
120
+
121
+ return obj
122
+ end
123
+
124
+ def translation
125
+ translation_for(::Globalize.locale)
126
+ end
127
+
128
+ def translation_for(locale)
129
+ @translation_caches ||= {}
130
+ unless @translation_caches[locale]
131
+ # Fetch translations from database as those in the translation collection may be incomplete
132
+ _translation = translations.detect{|t| t.locale.to_s == locale.to_s}
133
+ _translation ||= translations.with_locale(locale).first
134
+ _translation ||= translations.build(:locale => locale)
135
+ @translation_caches[locale] = _translation
136
+ end
137
+ @translation_caches[locale]
138
+ end
139
+
140
+ def rollback
141
+ @translation_caches[::Globalize.locale] = translation.previous_version
142
+ end
143
+
144
+ protected
145
+
146
+ def each_locale_and_translated_attribute
147
+ used_locales.each do |locale|
148
+ translated_attribute_names.each do |name|
149
+ yield locale, name
150
+ end
151
+ end
152
+ end
153
+
154
+ def used_locales
155
+ locales = globalize.stash.keys.concat(globalize.stash.keys).concat(translations.translated_locales)
156
+ locales.uniq!
157
+ locales
158
+ end
159
+
160
+ def save_translations!
161
+ globalize.save_translations!
162
+ @translation_caches = {}
163
+ end
164
+
165
+ def with_given_locale(attributes, &block)
166
+ attributes.symbolize_keys! if attributes.respond_to?(:symbolize_keys!)
167
+ if locale = attributes.try(:delete, :locale)
168
+ Globalize.with_locale(locale, &block)
169
+ else
170
+ yield
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,161 @@
1
+ require 'digest/sha1'
2
+
3
+ module Globalize
4
+ module ActiveRecord
5
+ module Migration
6
+ attr_reader :globalize_migrator
7
+
8
+ def globalize_migrator
9
+ @globalize_migrator ||= Migrator.new(self)
10
+ end
11
+
12
+ delegate :create_translation_table!, :drop_translation_table!,
13
+ :translation_index_name, :to => :globalize_migrator
14
+
15
+ class Migrator
16
+ include Globalize::ActiveRecord::Exceptions
17
+
18
+ attr_reader :model, :fields
19
+ delegate :translated_attribute_names, :connection, :table_name,
20
+ :table_name_prefix, :translations_table_name, :columns, :to => :model
21
+
22
+ def initialize(model)
23
+ @model = model
24
+ end
25
+
26
+ def create_translation_table!(fields = {}, options = {})
27
+ @fields = fields
28
+ complete_translated_fields
29
+ validate_translated_fields
30
+
31
+ create_translation_table
32
+ move_data_to_translation_table if options[:migrate_data]
33
+ remove_source_columns if options[:remove_source_columns]
34
+ create_translations_index
35
+ end
36
+
37
+ def remove_source_columns
38
+ translated_attribute_names.each do |attribute|
39
+ connection.remove_column(table_name, attribute)
40
+ end
41
+ end
42
+
43
+ def drop_translation_table!(options = {})
44
+ move_data_to_model_table if options[:migrate_data]
45
+ drop_translations_index
46
+ drop_translation_table
47
+ end
48
+
49
+ def complete_translated_fields
50
+ translated_attribute_names.each do |name|
51
+ fields[name] = column_type(name) unless fields[name]
52
+ end
53
+ end
54
+
55
+ def create_translation_table
56
+ connection.create_table(translations_table_name) do |t|
57
+ t.references table_name.sub(/^#{table_name_prefix}/, '').singularize
58
+ t.string :locale
59
+ fields.each { |name, type| t.column name, type }
60
+ t.timestamps
61
+ end
62
+ end
63
+
64
+ def create_translations_index
65
+ connection.add_index(
66
+ translations_table_name,
67
+ "#{table_name.sub(/^#{table_name_prefix}/, "").singularize}_id",
68
+ :name => translation_index_name
69
+ )
70
+ # index for select('DISTINCT locale') call in translation.rb
71
+ connection.add_index(
72
+ translations_table_name,
73
+ :locale
74
+ )
75
+ end
76
+
77
+ def drop_translation_table
78
+ connection.drop_table(translations_table_name)
79
+ end
80
+
81
+ def drop_translations_index
82
+ connection.remove_index(translations_table_name, :name => translation_index_name)
83
+ end
84
+
85
+ def move_data_to_translation_table
86
+ # this refactored version might be a good idea,
87
+ # but only if made into a code that doesn't break tests
88
+ # model.find_each do |record|
89
+ # translation = record.translations.build(:locale => I18n.default_locale)
90
+ # translated_attribute_names.each do |attribute|
91
+ # translation[attribute] = record[attribute]
92
+ # end
93
+ # translation.save!
94
+ # end
95
+
96
+ # Find all of the existing untranslated attributes for this model.
97
+ all_model_fields = @model.all
98
+ model_attributes = all_model_fields.map(&:untranslated_attributes)
99
+ all_model_fields.each do |model_record|
100
+ # Assign the attributes back to the model which will enable globalize3 to translate them.
101
+ model_record.attributes = model_attributes.detect{|a| a['id'] == model_record.id}
102
+ model_record.save!
103
+ end
104
+ end
105
+
106
+ def move_data_to_model_table
107
+ add_missing_columns
108
+
109
+ # Find all of the translated attributes for all records in the model.
110
+ all_translated_attributes = @model.all.collect{|m| m.attributes}
111
+ all_translated_attributes.each do |translated_record|
112
+ # Create a hash containing the translated column names and their values.
113
+ translated_attribute_names.inject(fields_to_update={}) do |f, name|
114
+ f.update({name.to_sym => translated_record[name.to_s]})
115
+ end
116
+
117
+ # Now, update the actual model's record with the hash.
118
+ @model.update_all(fields_to_update, {:id => translated_record['id']})
119
+ end
120
+ end
121
+
122
+ def validate_translated_fields
123
+ fields.each do |name, type|
124
+ raise BadFieldName.new(name) unless valid_field_name?(name)
125
+ raise BadFieldType.new(name, type) unless valid_field_type?(name, type)
126
+ end
127
+ end
128
+
129
+ def column_type(name)
130
+ columns.detect { |c| c.name == name.to_s }.try(:type)
131
+ end
132
+
133
+ def valid_field_name?(name)
134
+ translated_attribute_names.include?(name)
135
+ end
136
+
137
+ def valid_field_type?(name, type)
138
+ !translated_attribute_names.include?(name) || [:string, :text].include?(type)
139
+ end
140
+
141
+ def translation_index_name
142
+ # FIXME what's the max size of an index name?
143
+ index_name = "index_#{translations_table_name}_on_#{table_name.singularize}_id"
144
+ index_name.size < 50 ? index_name : "index_#{Digest::SHA1.hexdigest(index_name)}"
145
+ end
146
+
147
+
148
+ private
149
+
150
+ def add_missing_columns
151
+ translated_attribute_names.map(&:to_s).each do |attribute|
152
+ unless model.column_names.include?(attribute)
153
+ connection.add_column(table_name, attribute, model::Translation.columns_hash[attribute].type)
154
+ end
155
+ end
156
+ end
157
+
158
+ end
159
+ end
160
+ end
161
+ end