Module: MongoTranslatable::Translated::ClassMethods

Defined in:
lib/mongo_translatable.rb

Instance Method Summary collapse

Instance Method Details

#mongo_translate(*args) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/mongo_translatable.rb', line 60

def mongo_translate(*args)
  # don't allow multiple calls
  return if self.included_modules.include?(MongoTranslatable::Translated::InstanceMethods)

  send :include, MongoTranslatable::Translated::InstanceMethods

  options = args.last.is_a?(Hash) ? args.pop : Hash.new
  redefine_find = !options[:redefine_find].nil? ? options[:redefine_find] : true

  translatable_attributes = args.is_a?(Array) ? args : [args]

  cattr_accessor :translatable_attributes, :as_foreign_key_sym

  # expects a single attribute name symbol or array of attribute names as symbols
  self.translatable_attributes = translatable_attributes
  self.as_foreign_key_sym = self.name.foreign_key.to_sym

  before_save :set_locale_if_necessary
  before_destroy :destroy_translations

  # create the dynamic translation model
  const_set("Translation", Class.new).class_eval do
    include MongoMapper::Document

    cattr_accessor :translatable_class
    self.translatable_class = self.name.split('::').first.constantize

    def self.declare_key_from(spec)
      key_type = String
      key_name = spec

      key_name, key_type = spec if spec.is_a?(Array)

      unless keys.include?(key_name.to_s)
        class_eval do
          key key_name, key_type
        end
      end
    end

    self.translatable_class.translatable_attributes.each { |a| declare_key_from(a) }

    key :locale, String, :required => true

    before_save :locale_to_string

    # for classes that have dynamic translatable_attributes
    # add definition of keys for mongo_translatable Translation class, if not already defined
    # add accessor methods, too, if not already defined
    def self.update_keys_if_necessary_with(new_translatable_attributes)
      attribute_type = String

      new_translatable_attributes.each { |a| declare_key_from(a) }
    end

    # TODO: add validation for locale unique to translatable_class.as_foreign_key_sym scope
    # not implemented in mongo mapper yet

    def translatable
      self.translatable_class.find(self.send(self.translatable_class.as_foreign_key_sym))
    end

    protected
    # always store string version of locale (rather than symbol)
    def locale_to_string
      self.locale = self.locale.to_s
    end
  end

  class_eval do

    # dynamically define translation accessor methods
    def self.define_translation_accessor_method_for(attribute_name)
      # create the template code
      code = Proc.new { |locale|
        translation = translation_for(locale)
        translation.send(attribute_name.to_sym) if translation
      }

      define_method(attribute_name.to_s + "_translation_for", &code)
    end

    # define convenience method for each translatable_attribute
    # uses class method, see class method definitions
    translatable_attributes.each do |attribute|
      define_translation_accessor_method_for(attribute)
    end

    # take a collection of this class
    # and process translations accordingly
    def self.translatable_processing_of(results, *args)
      # at least for now, we skip :select queries
      # as we can't rely on having attributes available that we need
      # to compare against
      # we also return if results are nil
      return results if (args.is_a?(Array) &&
                         args.last.is_a?(Hash) &&
                         args.last.keys.include?(:select) &&
                         !args.last[:select].nil?) || results.nil?

      # handle single record
      if results.is_a?(self)
        result = results
        results = swap_in_translation_for_single(result)
      else
        # handle multiple records
        results = swap_in_translation_for_multiple(results)
      end
    end

    # does the actual swapping of translatable_attributes for a result
    def self.swap_in_translation_for_single(result)
      # only look up translation if the required locale is not the default for the record
      if result.present? && result.locale != I18n.locale.to_s
        # look up translation and swap in its attributes for result
        translated = result.translation_for(I18n.locale)
        if translated.present?
          result.translatable_attributes.each do |translated_attribute|
            unless translated.attributes[translated_attribute].blank?
              result.set_translation_for_this(translated_attribute, translated.attributes[translated_attribute])
            end
          end
          result.locale = translated.locale
        end
      end

      result
    end

    # does the actual swapping of translatable_attributes per result
    # and returns new collection of translated results
    def self.swap_in_translation_for_multiple(results)
      # do second query of translations
      # if item locale is different than current locale
      # swap in attributse from translation for item that is current locale

      # rather than rerun the full query, simply get ids and add locale
      result_ids = results.collect(&:id)

      conditions = {:locale => I18n.locale.to_s}
      conditions[as_foreign_key_sym] = result_ids

      translations = self::Translation.all(conditions)

      translated_results = results
      index = 0
      results.each do |result|
        unless result.locale == I18n.locale.to_s
          matching_translation = translations.select { |t| t[as_foreign_key_sym] == result.id }.first

          if matching_translation
            result.translatable_attributes.each do |key|
              unless matching_translation.attributes[key].blank?
                result.set_translation_for_this(key, matching_translation.attributes[key])
              end
            end

            result.locale = I18n.locale.to_s

            translated_results[index] = result
          end
        end
        index += 1
      end
      results = translated_results
    end

  end

  if redefine_find

    class_eval do
      # override find, this is called by all the dynamic finders
      # it isn't called by find_by_sql though (may be other exceptions)
      def self.find(*args)
        # get the standard results from find
        # this will throw a RecordNotFound before executing our code
        # if that is the response
        results = super(*args)

        results = translatable_processing_of(results, *args)
      end
    end
  end
end