hooktstudios-globalize3 0.2.0.beta8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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