Skip to content
Browse files

Refactoring to use aliases for saving, and adding testing support.

Validations of translated data are now always handled by the parent.
  • Loading branch information...
1 parent a798aae commit de3936e414df9140d569d86525b327680a80807a @samlown committed
View
2 Rakefile
@@ -8,7 +8,7 @@ task :default => :test
desc 'Test the translate_columns plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
- t.pattern = 'test/**/*_test.rb'
+ t.pattern = 'test/*_test.rb'
t.verbose = true
end
View
4 init.rb
@@ -3,5 +3,5 @@
require 'translate_columns'
ActiveRecord::Base.class_eval do
- include Translate::Columns
-end
+ include TranslateColumns
+end
View
348 lib/translate_columns.rb
@@ -3,193 +3,205 @@
#
# Copyright (c) 2007 Samuel Lown <me@samlown.com>
#
-module Translate
- module Columns
-
- def self.included(mod)
- mod.extend(ClassMethods)
- end
-
- # methods used in the class definition
- module ClassMethods
-
- # Read the provided list of symbols as column names and
- # generate methods for each to access translated versions.
- #
- # Possible options, after the columns, include:
- #
- # * :locale_field - Name of the field in the parents translation table
- # of the locale. This defaults to 'locale'.
- #
- def translate_columns( *options )
+module TranslateColumns
+
+ class MissingParent < StandardError
+ end
+
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ # methods used in the class definition
+ module ClassMethods
+
+ # Read the provided list of symbols as column names and
+ # generate methods for each to access translated versions.
+ #
+ # Possible options, after the columns, include:
+ #
+ # * :locale_field - Name of the field in the parents translation table
+ # of the locale. This defaults to 'locale'.
+ #
+ def translate_columns( *options )
+
+ locale_field = 'locale'
- locale_field = 'locale'
-
- columns = [ ]
- if ! options.is_a? Array
- raise "Provided parameter to translate_columns is not an array!"
- end
- # extract all the options
- options.each do | opt |
- if opt.is_a? Symbol
- columns << opt
- elsif opt.is_a? Hash
- # Override the locale class if set.
- locale_field = opt[:locale_field]
- end
+ columns = [ ]
+ if ! options.is_a? Array
+ raise "Provided parameter to translate_columns is not an array!"
+ end
+ # extract all the options
+ options.each do | opt |
+ if opt.is_a? Symbol
+ columns << opt
+ elsif opt.is_a? Hash
+ # Override the locale class if set.
+ locale_field = opt[:locale_field]
end
+ end
- define_method 'columns_to_translate' do
- columns.collect{ |c| c.to_s }
- end
+ define_method 'columns_to_translate' do
+ columns.collect{ |c| c.to_s }
+ end
+
+ # set the instance Methods first
+ include TranslateColumns::InstanceMethods
+
+ # Rails magic to override the normal save process
+ alias_method_chain :save, :translation
+ alias_method_chain :save!, :translation
+
+ # Generate a module containing methods that override access
+ # to the ActiveRecord methods.
+ # This dynamic module is then included in the parent such that
+ # the super method will function correctly.
+ mod = Module.new do | m |
- # set the instance Methods first
- include Translate::Columns::InstanceMethods
-
- # Generate a module containing methods that override access
- # to the ActiveRecord methods.
- # This dynamic module is then included in the parent such that
- # the super method will function correctly.
- mod = Module.new do | m |
-
- columns.each do | column |
+ columns.each do | column |
- next if ['id', locale_field].include?(column.to_s)
+ next if ['id', locale_field].include?(column.to_s)
+
+ # This is strange, so allow me to explain:
+ # We define access to the original method and its super,
+ # a normal "alias" can't find the super which is the method
+ # created by ActionBase.
+ # The Alias_method function takes a copy, and retains the
+ # ability to call the parent with the same name.
+ # Finally, the method is overwritten to support translation.
+ #
+ # All this is to avoid defining parameters for the overwritten
+ # accessor which normally doesn't have them.
+ # (Warnings are produced on execution when a metaprogrammed
+ # function is called without parameters and its expecting them)
+ #
+ # Sam Lown (2007-01-17) dev at samlown dot com
+ define_method(column) do
+ # This super should call the missing_method method in ActiveRecord.
+ super()
+ end
- # This is strange, so allow me to explain:
- # We define access to the original method and its super,
- # a normal "alias" can't find the super which is the method
- # created by ActionBase.
- # The Alias_method function takes a copy, and retains the
- # ability to call the parent with the same name.
- # Finally, the method is overwritten to support translation.
- #
- # All this is to avoid defining parameters for the overwritten
- # accessor which normally doesn't have them.
- # (Warnings are produced on execution when a metaprogrammed
- # function is called without parameters and its expecting them)
- #
- # Sam Lown (2007-01-17) dev at samlown dot com
- define_method(column) do
- # This super should call the missing_method method in ActiveRecord.
+ alias_method("#{column}_before_translation", column)
+
+ # overwrite accessor to read
+ define_method("#{column}") do
+ if translation and ! translation.send(column).blank?
+ translation.send(column)
+ else
super()
end
-
- alias_method("#{column}_before_translation", column)
-
- # overwrite accessor to read
- define_method("#{column}") do
- if translation and ! translation.send(column).blank?
- translation.send(column)
- else
- super()
- end
- end
-
- define_method("#{column}_before_type_cast") do
- if (translation)
- translation.send("#{column}_before_type_cast")
- else
- super
- end
+ end
+
+ define_method("#{column}_before_type_cast") do
+ if (translation)
+ translation.send("#{column}_before_type_cast")
+ else
+ super
end
-
- define_method("#{column}=") do |value|
- # translation object must have already been set up for this to work!
- if (translation)
- translation.send("#{column}=",value)
- else
- super( value )
- end
- end
end
- end # dynamic module
+
+ define_method("#{column}=") do |value|
+ # translation object must have already been set up for this to work!
+ if (translation)
+ translation.send("#{column}=",value)
+ else
+ super( value )
+ end
+ end
- # include the anonymous module so that the "super" method
- # will work correctly in the child!
- include mod
- end
-
+ end
+ end # dynamic module
+
+ # include the anonymous module so that the "super" method
+ # will work correctly in the child!
+ include mod
end
-
- # Methods that are specific to the current class
- # and only called when translate_columns is used
- module InstanceMethods
-
- # Provide the locale which is currently in use with the object or the current global locale.
- # If the default is in use, always return nil.
- def locale
- locale = @locale || I18n.locale.to_s
- locale == I18n.default_locale.to_s ? nil : locale
- end
-
- # Setting the locale will always enable translation.
- # If set to nil the global locale is used.
- def locale=(locale)
- @disable_translation = false
- # TODO some checks for available translations would be nice.
- # I18n.available_locales only available as standard with rails 2.3
- @locale = locale.to_s.empty? ? nil : locale.to_s
- end
-
- # Do not allow translations!
- def disable_translation
- @disable_translation = true
- end
-
- # If the current object has a locale set, return
- # a translation object from the translations set
- def translation
- if !@disable_translation and locale
- if !@translation || (@translation.locale != locale)
- # try to find entity in translations array
- @translation = translations.find_by_locale(locale)
- @translation = self.translations.build(:locale => locale) unless @translation
- end
- @translation
- else
- nil
+
+ end
+
+ # Methods that are specific to the current class
+ # and only called when translate_columns is used
+ module InstanceMethods
+
+ # Provide the locale which is currently in use with the object or the current global locale.
+ # If the default is in use, always return nil.
+ def translation_locale
+ locale = @translation_locale || I18n.locale.to_s
+ locale == I18n.default_locale.to_s ? nil : locale
+ end
+
+ # Setting the locale will always enable translation.
+ # If set to nil the global locale is used.
+ def translation_locale=(locale)
+ @disable_translation = false
+ # TODO some checks for available translations would be nice.
+ # I18n.available_locales only available as standard with rails 2.3
+ @translation_locale = locale.to_s.empty? ? nil : locale.to_s
+ end
+
+ # Do not allow translations!
+ def disable_translation
+ @disable_translation = true
+ end
+
+ # If the current object has a locale set, return
+ # a translation object from the translations set
+ def translation
+ if !@disable_translation and translation_locale
+ if !@translation || (@translation.locale != translation_locale)
+ # try to find entity in translations array
+ raise MissingParent, "Cannot create translations without a stored parent" if new_record?
+ @translation = translations.find_by_locale(translation_locale)
+ @translation = self.translations.build(:locale => translation_locale) unless @translation
end
+ @translation
+ else
+ nil
end
-
- # As this is included in a mixin, a "super" call from inside the
- # child (inheriting) class will infact look here before looking to
- # ActiveRecord for the real 'save'. This method should therefore
- # be safely overridden if needed.
- def save(options = nil)
- save_and_disable_translation
- r = super(options)
- enable_translation
- r
- end
-
- def save!(options = nil)
- save_and_disable_translation!
- r = super(options)
- enable_translation
- r
- rescue
+ end
+
+ # As this is included in a mixin, a "super" call from inside the
+ # child (inheriting) class will infact look here before looking to
+ # ActiveRecord for the real 'save'. This method should therefore
+ # be safely overridden if needed.
+ #
+ # Assumes validation enabled in ActiveRecord and performs validation
+ # before saving. This means the base records validation checks will always
+ # be used.
+ #
+ def save_with_translation(perform_validation = true)
+ if perform_validation && valid? || !perform_validation
+ translation.save(false) if (translation)
+ disable_translation
+ save_without_translation(false)
enable_translation
- raise
+ true
+ else
+ false
end
-
- protected
-
- def save_and_disable_translation
- translation.save if (translation)
- @disable_translation = true
- end
-
- def save_and_disable_translation!
+ end
+
+ def save_with_translation!
+ if valid?
translation.save! if (translation)
- @disable_translation = true
+ disable_translation
+ save_without_translation!
+ enable_translation
+ else
+ raise RecordInvalid.new(self)
end
-
- def enable_translation
- @disable_translation = false
- end
-
+ rescue
+ enable_translation
+ raise
end
+
+ protected
+
+ def enable_translation
+ @disable_translation = false
+ end
+
end
+
end
View
4 test/database.yml
@@ -0,0 +1,4 @@
+sqlite3:
+ database: ":memory:"
+ adapter: sqlite3
+ timeout: 500
View
10 test/fixtures/document.rb
@@ -0,0 +1,10 @@
+class Document < ActiveRecord::Base
+
+ has_many :translations, :class_name => 'DocumentTranslation'
+ translate_columns :title, :body
+
+ validates_presence_of :title
+ validates_length_of :title, :within => 3..200
+
+ validates_length_of :body, :within => 3..500
+end
View
3 test/fixtures/document_translation.rb
@@ -0,0 +1,3 @@
+class DocumentTranslation < ActiveRecord::Base
+
+end
View
14 test/fixtures/document_translations.yml
@@ -0,0 +1,14 @@
+translation1:
+ id: 1
+ document_id: 1
+ locale: es
+ title: Este es el titulo de un documento en Español
+ body: Nada
+
+translation2:
+ id: 2
+ document_id: 1
+ locale: fr
+ title: Un title en francais
+ body:
+
View
12 test/fixtures/documents.yml
@@ -0,0 +1,12 @@
+document1:
+ id: 1
+ title: Test Document Number 1
+ body: This is a test document with some kind of body.
+ published_at: "2009-09-23 21:53:04"
+
+document2:
+ id: 2
+ title: Test Document Number 2
+ body: This is a second test document with some random content for the body.
+ published_at: "2009-09-23 21:53:46"
+
View
17 test/fixtures/schema.rb
@@ -0,0 +1,17 @@
+ActiveRecord::Schema.define do
+ create_table "documents", :force => true do |t|
+ t.column "title", :string
+ t.column "body", :text
+ t.column "published_at", :datetime
+ t.timestamps
+ end
+
+ create_table "document_translations", :force => true do |t|
+ t.references "document"
+ t.string :locale
+ t.column "title", :string
+ t.column "body", :text
+ t.timestamps
+ end
+end
+
View
7 test/lib/activerecord_connector.rb
@@ -0,0 +1,7 @@
+require 'rubygems'
+require 'active_record'
+require 'active_record/fixtures'
+
+conf = YAML::load(File.open(File.dirname(__FILE__) + '/../database.yml'))
+ActiveRecord::Base.establish_connection(conf['sqlite3'])
+
View
10 test/lib/activerecord_test_helper.rb
@@ -0,0 +1,10 @@
+
+require 'test/lib/activerecord_connector'
+require 'test/fixtures/schema.rb'
+
+module ActiverecordTestHelper
+ FIXTURES_PATH = File.join(File.dirname(__FILE__), '/../fixtures')
+ dep = defined?(ActiveSupport::Dependencies) ? ActiveSupport::Dependencies : ::Dependencies
+ dep.load_paths.unshift FIXTURES_PATH
+end
+
View
125 test/translate_columns_test.rb
@@ -1,8 +1,125 @@
require 'test/unit'
+require 'test/lib/activerecord_test_helper'
+require 'init'
-class TranslateColumnsTest < Test::Unit::TestCase
- # Replace this with your real tests.
- def test_this_plugin
- flunk
+class TranslateColumnsTest < Test::Unit::TestCase
+
+ include ActiverecordTestHelper
+
+ def setup
+ @docs = Fixtures.create_fixtures(FIXTURES_PATH, ['documents', 'document_translations'])
+ end
+
+ def teardown
+ Fixtures.reset_cache
+ end
+
+ def test_basic_document_fields
+ doc = Document.find(:first)
+ assert_equal "Test Document Number 1", doc.title, "Document not found!"
+ assert_not_nil doc.body, "Empty document body"
+ assert_not_nil doc.published_at, "Missing published date"
+ end
+
+ def test_basic_document_fields_for_default_locale
+ I18n.locale = "en"
+ doc = Document.find(:first)
+ assert_equal "Test Document Number 1", doc.title, "Document not found!"
+ assert_not_nil doc.body, "Empty document body"
+ assert_not_nil doc.published_at, "Missing published date"
end
+
+ def test_count_translations
+ doc = Document.find(:first)
+ assert_equal 2, doc.translations.count, "Count doesn't match!"
+ end
+
+ def test_basic_document_fields_for_spanish
+ I18n.locale = "es"
+ doc = Document.find(:first)
+ assert_equal "Este es el titulo de un documento en Espa\303\261ol", doc.title, "Document not found!"
+ assert_equal "Nada", doc.body, "Different document body"
+ assert_not_nil doc.published_at, "Missing published date"
+ assert_equal "Test Document Number 1", doc.title_before_translation
+ end
+
+ def test_missing_fields_resort_to_original
+ I18n.locale = 'fr'
+ doc = Document.find(:first)
+ assert_equal "Un title en francais", doc.title
+ assert_match /body/, doc.body
+ assert doc.body_before_type_cast.to_s.empty?
+ end
+
+ def test_switching_languages_for_reading
+ I18n.locale = I18n.default_locale
+ doc1 = Document.find(:first)
+ assert_equal "Test Document Number 1", doc1.title
+ I18n.locale = 'es'
+ doc2 = Document.find(:first)
+ assert_equal doc1.title, doc2.title
+ assert_not_equal "Test Document Number 1", doc1.title
+ I18n.locale = 'en'
+ assert_equal doc1.title, doc2.title
+ assert_equal "Test Document Number 1", doc1.title
+ end
+
+ def test_setting_fields_in_default_language
+ time_now = Time.now
+ I18n.locale = I18n.default_locale
+ doc1 = Document.find(:first)
+ doc1.title = "A new title"
+ doc1.published_at = time_now
+ assert doc1.save, "Unable to save document"
+ assert_equal "A new title", doc1.title
+ # Now change language
+ I18n.locale = 'es'
+ doc1 = Document.find(:first)
+ assert_not_equal "A new title", doc1.title
+ assert_equal time_now.to_s, doc1.published_at.to_s
+ end
+
+ def test_saving_changes_in_translations
+ time_now = Time.now
+ I18n.locale = 'es'
+ doc1 = Document.find(:first)
+ doc1.title = "Un nuevo título"
+ doc1.published_at = time_now
+ assert doc1.save
+ I18n.locale = I18n.default_locale
+ doc1 = Document.find(:first)
+ assert_not_equal "Un nuevo título", doc1.title
+ assert_equal time_now.to_s, doc1.published_at.to_s
+ end
+
+ def test_creating_new_documents
+ I18n.locale = I18n.default_locale
+ doc = Document.new(:title => "A new document", :body => "The Body")
+ assert doc.save
+ end
+
+ def test_creating_new_documents_under_locale_fails
+ I18n.locale = 'es'
+ assert_raise TranslateColumns::MissingParent do
+ Document.new(:title => "Un nuevo documento", :body => 'El cuerpo')
+ end
+ end
+
+ def test_failed_validations
+ I18n.locale = I18n.default_locale
+ doc = Document.find(:first)
+ doc.title = "a"
+ assert !doc.save
+ assert doc.errors.on(:title)
+ end
+
+ def test_failed_validations_on_translation
+ I18n.locale = 'es'
+ doc = Document.find(:first)
+ doc.title = "a"
+ assert !doc.save
+ assert doc.errors.on(:title)
+ end
+
+
end

0 comments on commit de3936e

Please sign in to comment.
Something went wrong with that request. Please try again.