I've been asked to design a multiple language application and I need advice with which is the best approach with Rails.
Basically all the tables have some common fields that doesn't need to
be translated and some others that need translation.
thank you
For this purpose, will approach gem globalize3. Easy to use.
In your gemfile:
gem 'globalize'
Model:
class Article < ActiveRecord::Base
translates :title, :text
end
And migration:
class CreateArticles < ActiveRecord::Migration
def up
create_table :articles do |t|
t.timestamps
end
Article.create_translation_table! :title => :string, :text => :text
end
def down
drop_table :articles
Article.drop_translation_table!
end
end
And run
rake db:migrate
Related
In my domain, many models have names, descriptions, etc. These properties need translations. I know how to represent this in a database. However I struggle finding a way to represent this with Rails.
|-------translations-table--------|
|translation_id|locale|translation|
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
|----------------------modelx-table---------------------|
|id|name_translation_id|description_translation_id|price|
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
|-------modely-table--------|
|id|name_translation_id|date|
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
You do not need to create extra models for translations, yo just need to set up locales in .yml format, check this for further instructions
Update
Ok now I understood your point, you want to add translatable fields on your entities/models, so users can manage those translations through a UI right?, well your approach is correct, however there is a gem called Globalize that does the exact same thing but with more toys, and much more standardized as you want.
This is the solution I eventually came up with:
#Models
class Translation
has_many :translation_records
end
class TranslationRecord
(translation_records.find_by :locale => I18n.locale).text
end
class ModelX
belongs_to :name_translation, :class_name => 'Translation'
belongs_to :description_translation, :class_name => 'Translation'
def name
name_translation.current
end
def description
description_translation.current
end
end
#Migrations
class CreateTranslationRecords < ActiveRecord::Migration[5.0]
def change
create_table :translation_records do |t|
t.references :translation
t.string :locale
t.string :text
end
add_index :translation_records, :locale
end
end
class CreateTranslation < ActiveRecord::Migration[5.0]
def change
create_table :translations do |t|
# only id column
end
end
end
class AddTranslationToModelXs < ActiveRecord::Migration[5.0]
def change
add_reference :model_xs, :name_translation
add_reference :model_xs, :description_translation
end
end
I am stuck. I've been trying to figure out how to include the association changes (has_many, has_many through) on a model that has papertrail. I would like to call MyModel.versions.first.changeset and have any changes that took place on associated objects be included in the .changeset hash that is returned from that version of the object.
I've added the migrations for version associations:
class CreateVersionAssociations < ActiveRecord::Migration
def self.up
create_table :version_associations do |t|
t.integer :version_id
t.string :foreign_key_name, :null => false
t.integer :foreign_key_id
end
add_index :version_associations, [:version_id]
add_index :version_associations, [:foreign_key_name, :foreign_key_id], :name => 'index_version_associations_on_foreign_key'
end
def self.down
remove_index :version_associations, [:version_id]
remove_index :version_associations, :name => 'index_version_associations_on_foreign_key'
drop_table :version_associations
end
end
class AddTransactionIdColumnToVersions < ActiveRecord::Migration
def self.up
add_column :versions, :transaction_id, :integer
add_index :versions, [:transaction_id]
end
def self.down
remove_index :versions, [:transaction_id]
remove_column :versions, :transaction_id
end
end
I have added Papertrail to the associated objects, but as far as I can tell, there is no documentation discussing retrieving changes that took place on the associated objects. Can anyone assist on if this is possible using Papertrail?
I am trying to implement an audit trail of changes on a model and its associated objects that can be accessed in one changeset.
The information you need is ultimately stored in the relevant tables versions and version_associations.
However, paper_trail does not provide the methods for you to access the information in the way you want. But you can write a custom method yourself to get a list of the associations's versions of an object.
Let's say you have the following models:
class Article < ApplicationRecord
has_many :comments
has_paper_trail
end
class Comment < ApplicationRecord
belongs_to :article
has_paper_trail
end
You can find all the comment versions of an article object article this way:
PaperTrail::Version.where(item_type: 'Comment')
.joins(:version_associations)
.where(version_associations: { foreign_key_name: 'article_id', foreign_key_id: article.id })
.order('versions.created_at desc')
You can monkey patch the gem, or define this method as an instance method on the Article class so you can call it easily, eg article.comment_versions
Note, the above information isn't available in the article.versions.first.changeset, because if you change a comment but not the article, the article is not versioned, only the comment is.
But the method above allows you to access the history of changes to the associations.
Looks like this has been added as an experimental feature to the papertrail gem
check out the docs here
https://github.com/airblade/paper_trail/blob/v4.2.0/README.md#associations
This change will require the addition of a new database table for papertrail to keep track of associated models.
I'm reading Rails 3 in Action and following the commands verbatim. However, when I run the commands
rails new things_i_bought
cd things_i_bought
bundle install
rails generate scaffold purchase name:string cost:float
The book says I should get this code:
class CreatePurchases < ActiveRecord::Migration
def self.up #not created in my code
create_table :purchases do |t|
t.string :name
t.float :cost
t.timestamps
end
end
def self.down # not created in my code
drop_table :purchases
end
end
I get this code instead:
class CreatePurchases < ActiveRecord::Migration
def change
create_table :purchases do |t|
t.string :name
t.float :cost
t.timestamps
end
end
end
Why are the class methods up and down not being created for me? I'm using
rails 3.1.1 and ruby 1.9.2.
thanks for reading my book!
As JacobM and dbalatero have already explained, this is a new feature in Rails 3.1. This particular feature was added by Aaron Patterson as a way to simplify the migration syntax. In earlier versions of Rails, you would have to do as the book shows:
class CreatePurchases < ActiveRecord::Migration
def self.up
create_table :purchases do |t|
t.string :name
t.float :cost
t.timestamps
end
end
def self.down
drop_table :purchases
end
end
But that's kind of repeating yourself. Aaron created a migration syntax that looks good and is simpler, calling only the methods necessary for migrating forward, but also allowing the migrations backwards (known as a "rollback") too. The same migration written with the Rails 3.1 syntax is this:
class CreatePurchases < ActiveRecord::Migration
def change
create_table :purchases do |t|
t.string :name
t.float :cost
t.timestamps
end
end
end
So when this migration runs "forwards", Rails will create the purchases table with the fields. When you roll it back (or run it "backwards") then Rails will know to drop the table.
This syntax isn't entirely perfect however, and you'll run into problems with methods such as change_column. When that happens, it's best to stick with defining both the def up and def down methods in the migrations:
class CreatePurchases < ActiveRecord::Migration
def up
change_column :purchases, :cost, :integer
end
def down
change_column :purchases, :cost, :float
end
end
That's because in this example Rails won't know how to switch it back to the previous type. I hope this explains it better!
This is a new feature in Rails 3.1. For changes that Rails can figure out how to reverse, such as creating a table, you simply create a "change" method with the code that would have gone in "up", and it figures out how to do "down" on it's own.
You can also define "up" and "down" methods yourself -- for some changes (such as dropping a column) Rails won't be able to figure it out -- but the syntax is a bit different; it's not just def up instead of def self.up (they're now instance methods instead of class methods).
I believe in the new Rails 3.1, the database migration methods are self-aware about how to run an up/down migration.
Therefore, if you define a def change method, it will try to use those self-aware methods: in this case, create_table knows to do DROP TABLE in a down context, and CREATE TABLE in an up context.
If you want the old style, you can probably keep using it and define your own self.down and self.up methods as the book describes.
Edit: here's a link to the blog post on this, called "Reversible Migrations": http://www.edgerails.info/articles/what-s-new-in-edge-rails/2011/05/06/reversible-migrations/index.html
The docs for the Globalize3 gem are clear about how to create a translation table, but I don't see any information about how to add a field to a translation table during a later migration. For example, I initially included Category.create_translation_table! :name => :string when I created my Category model. Now, however, I need to add a translated field to the model.
How do I do that with a Rails migration? I don't see any docs for an alter_translation_table! method or anything similar...
You can do it by hand, something like the following:
class AddNewFieldToYourTable < ActiveRecord::Migration
def self.up
change_table(:your_tables) do |t|
t.string :new_field
end
change_table(:your_table_translations) do |t|
t.string :new_field
end
end
def self.down
remove_column :your_tables, :new_field
remove_column :your_table_translations, :new_field
end
end
With Globalize4, just :
class AddHintToCategory < ActiveRecord::Migration
def up
Category.add_translation_fields! hint: :text
end
def down
remove_column :category_translations, :hint
end
end
Don't forget to add the new field in your model :
translate :name, :hint
https://github.com/globalize/globalize/blob/master/lib/globalize/active_record/migration.rb:34 line (globalize 4)
add_translation_fields!(fields, options)
P.S. Just a typo in a previous comment, 'add_transaction_fields' isn't defined.
I would like to create an enum field at sone migration I'm doing, I tried searching in google but I can't find the way to do it in the migration
the only thing I found was
t.column :status, :enum, :limit => [:accepted, :cancelled, :pending]
but looks like the above code runs only on rails 1.xxx and since I'm running rails 2.0
this what I tried but it fails
class CreatePayments < ActiveRecord::Migration
def self.up
create_table :payments do |t|
t.string :concept
t.integer :user_id
t.text :notes
t.enum :status, :limit => [:accepted, :cancelled, :pending]
t.timestamps
end
end
def self.down
drop_table :payments
end
end
So, in case that isn't allowed, what do you think could be a good solution? just a text field, and validating from the model?
You can manually specify the type by using the t.column method instead. Rails will interpret this as a string column, and you can simply add a validator to the model like Pavel suggested:
class CreatePayments < ActiveRecord::Migration
def self.up
create_table :payments do |t|
t.string :concept
t.integer :user_id
t.text :notes
t.column :status, "ENUM('accepted', 'cancelled', 'pending')"
t.timestamps
end
end
def self.down
drop_table :payments
end
end
class Payment < ActiveRecord::Base
validates_inclusion_of :status, :in => %w(accepted cancelled pending)
end
Look at tip #3 on http://zargony.com/2008/04/28/five-tips-for-developing-rails-applications
This exactly what you need!
class User < ActiveRecord::Base
validates_inclusion_of :status, :in => [:active, :inactive]
def status
read_attribute(:status).to_sym
end
def status= (value)
write_attribute(:status, value.to_s)
end
end
HTH
You can try the (very) comprehensive jeff's enumerated_attribute gem OR go with this simple workaround:
class Person < ActiveRecord::Base
SEX = [:male, :female]
def sex
SEX[read_attribute(:sex)]
end
def sex=(value)
write_attribute(:sex, SEX.index(value))
end
end
And then declare the sex attribute as an integer:
t.integer :sex
This worked very fine for me! =D
I have dozens of these little enums, with 3-300 entries in each. I implement them as lookup tables. I don't have a model file for each one; I use some metaprogramming to generate a model for each, since each table has the same set of columns (id, name, description).
Since some of the sets had enough elements to warrant their own table, it was more consistent to move them all to tables. Just another option if you'll have more of these enums later.
EDIT: Here's how I generate the models:
ACTIVE_RECORD_ENUMS = %w{
AccountState
ClientType
Country
# ...
}
ACTIVE_RECORD_ENUMS.each do |klass|
eval "class #{klass} < ActiveRecord::Base; end"
klass.constantize.class_eval do
class << self
def id_for(name)
ids[name.to_s.strip.humanize.downcase]
end
def value_for(id)
values[id.to_i]
end
def values
#values ||= find(:all).inject({}) {|h,m| h[m.send(primary_key)] = m.name; h}
end
def ids
#ids ||= self.values.inject({}) {|h, {k, v}| h[v.downcase] = k; h}
end
end
end
end
This file lives in the models directory, and is included in application_config.rb. This lets me do stuff like this:
AccountState.ids
# => {"active" => 1, "deleted" => 2}
AccountState.values
# => {1 => "Active", 2 => "Deleted"}
AccountState.id_for("Active")
# => 1
AccountState.value_for(1)
# => "active"
Have you looked at the enum-column plugin on RubyForge?
Similarly, the enumerated_attribute gem manages enums at the object level.
enum_attr :status, %w(accepted cancelled ^pending)
Define a string in the migration
t.string :status
Also provides some nice features like dynamic predicate methods.
http://github.com/jeffp/enumerated_attribute/tree/master
Add the following:
module ActiveRecord
module ConnectionAdapters #:nodoc:
class TableDefinition
def enum(*args)
options = args.extract_options!
column_names = args
column_names.each { |name| column(name, 'enum', options) }
end
end
end
end
to lib/enum/table_definition.rb and include it in your init.rb.
ok, just read the whole rails api and found what I neeed and I dont like :(
Rails doesn't support emum as native type on migrations, here is the info, I need to search for a plugin or other method.
I will keep you posted.
Another option: drop to SQL.
def self.up
execute "ALTER TABLE `payments` ADD `status` ENUM('accepted', 'cancelled', 'pending')"
end
With simple_form i use this:
<%= f.input :gender, :collection => {'Male' => 'male','Female' => 'female'}, :include_blank => false %>