Versioning for Rails i18n translations - ruby-on-rails

I'm in the process of building a volunteer based translation engine for a new site built in Rails 4.0. Since it's volunteer based, there is always the possibility that a user may enter a translation that others do not agree with, accidentally remove a translation, etc. In such an event, I would like to give users the option to revert to a previous translation.
I did some searching around but have yet to find a solution aside from writing my own I18n backend. Is there a simpler way of storing previous versions of translations?
I'm currently using Sven Fuchs' Active Record as a backend, however I'm seriously thinking about switching due to possible performance issues later on down the road.

We had a very successful experience using Globalize (github page: https://github.com/globalize/globalize) and as for the versioning part we didn't try it but Globalize does have support for that in a seperate gem github page: (https://github.com/globalize/globalize-versioning)
After tens of painful gem experiences, i found that comparing gems by last update date and how frequent is new releases, bugs fixes and support is a major factor to decide which one will make your life easier and which one won't.
Update:
You can use Globalize to dynamically translate views (check tutorial) but i came across a github project called iye. I think it suits your needs best (github page: https://github.com/firmafon/iye)

I used Nimir's help to find this solution. Like Globalize Versioning, you can add Paper Trail support for the Active Record's Translation class, however there are a few pitfalls to this method currently.
First you need to include the gems in your Gemfile:
gem "i18n-active_record"
gem "paper_trail"
Then you need to make sure your Translation model class is inheriting from I18n Active Record::Translation and call and calls has_paper_trail:
class Translation < I18n::Backend::ActiveRecord::Translation
has_paper_trail
end
This should really be enough, however the store_translations method in I18n Active Record do not update existing records. Instead, each time a record is added, all records with the given key are deleted and a new record is created. This causes confusion for Paper Trail since it relied on an id.
To get around this issue, I created my own store_translation method, which will update records if they exist:
def store_translations(locale, data, options = {})
escape = options.fetch(:escape, true)
I18n.backend.flatten_translations(locale, data, escape, false).each do |key, value|
t = Translation.find_or_create_by!(locale: locale.to_s, key: key.to_s)
t.value = value
t.save
end
I18n.backend.reload!
end
Notice I also included I18n.backend.reload!, this is because I am running Memoize to cache translations but it seems that it needs to be told to recache whenever a record is updated.
Now I can simply call:
store_translations(lang, {key => key}, :escape => false)
to a store a new translation and ensure we keep a record of the old one and who made the change.

Related

Ransack: Can page size be customized?

I'm using Spree and in the products/search controller action it uses Ransack. The Spree documentation says that Spree provides no way to customize page size, so I thought I'd customize the action itself to use Ransack to turn off pagination entirely. Does Ransack support a way to either not paginate results or to at least make the pagination huge?
This is the code in the existing controller action:
#products = product_scope.ransack(params[:q]).result.page(params[:page])
When I puts the type of #products after that, it is ActiveRecord::Relation. When I puts of
#products = product_scope.ransack(params[:q]).result
it is also ActiveRecord::Relation but in the first case there's a method 'total_count' used in the rabl template that is not present in the second one. I have dug through the source for ransack and I can't find where the .page is defined. I'm wondering if perhaps it's included in some class_eval of ActiveRecord::Relation in one of the gems that Spree pulls in.
Someone somewhere has to have faced this problem and come up with a solution. I'm hoping against hope to find that person :P
jd
The .page method comes from the Kaminari gem, and not Ransack.
You can make the pagination "huge" by changing the default per_page value in Kaminari:
Kaminari.config.default_per_page = 1000
Ryan's already got you, but I'm not sure if there isn't another way to do it as well. I can't tell from your question if you're talking about just the number of products shown per page, but it sounds like you are.
If so, I've done the same modification by using the built-in Spree preference products_per_page. When Spree provides a preference, I prefer (no pun intended) to use that over modifying gem parameters since it's part of their design already.
I fired up the console with bundle exec rails c, then ran the following command to set the preference on my store to 16 products per page: Spree::Config.products_per_page = 16
You can also put it in one of your initialization files so it doesn't get accidentally erased if you rebuild your database from scratch...I learned about it from this post in the Spree group. I also found details on setting Spree preferences here.
You want to use .per, for example:
collection.per(100)
collection.page(3).per(100)
collection.per(100).per(3)
Any of these will work.

Elasticsearch versioning through Tire gem

I have an existing Rails app that uses tire (0.4.0) to interface with an Elasticsearch (0.17.4) engine. It already has a couple of models using Tire::Persistence. I want to add a new model that takes advantage of Elasticsearch versioning, to track all changes and be able to revert to previous versions.
Right now when I retrieve any 'persisted' model instance, I check _version and it is always nil. I have not found any tire documentation that relates to versioning. Do I have to activate it somehow, or manually save records with version values? Am I even on the right track here?
I do see that certain methods return _version values for items, but others don't...
Article.first._version # => nil
Article.search("sample query").first._version # => nil
Article.find("id_123")._version # => 8
Also, versioning seems to increment by 2. Perhaps tire is not fully equipped to deal with versioning. Is it saving previous versions? How can I retrieve a previous version of a record?
[EDIT] I may have misunderstood what 'versioning' actually is in Elasticsearch. Seems like it's mostly for concurrency control. Oh wellz. (I would love to hear otherwise, though)
First of all, yes, as you write in the Edit, versions in ElasticSearch are not meant to store revisions of the document, but for concurrency control (eg. not overwriting a document with stale version).
Second, you have to declare that you want the versions returned back from search; http://www.elasticsearch.org/guide/reference/api/search/version.html
This code shows you how to do it in Tire.
require 'tire'
Tire.index('articles') do
delete
create
store id: 1, title: 'One'
store id: 2, title: 'Two'
store id: 2, title: 'Two again'
refresh
end
articles = Tire.search('articles') do
query { all }
version true
end.results
articles.each do |article|
puts "Article '#{article.title}' has version #{article._version}"
end
For the moment, it's best to read the Tire integration test suite for documentation. The documentation is of course something which should and will improve.
As for your original requirement itself, similar questions have regularly popped up for CouchDB, in the past. The Simple Document Versioning with CouchDB blog post describes one nice strategy. It would be beneficial for you to research the CouchDB solutions, since the document model is similar. (Of course, CouchDB, contrary to ElasticSearch does physically store the document revisions, thus it opens a different range of strategies.)
In ElasticSearch, your basic decision regarding working with revisions would be:
Do I want to store full revisions directly in the JSON itself?
This could make sense for smaller documents and smaller databases. Depending on your requirements, it could make searching for historic documents very simple.
The Nested Type in ElasticSearch would make working with these “revisions as nested documents” convenient and easy.
(You could also store just “diffs” of the documents in the JSON itself, but that would put more strain on your application logic, I bet.)
Do I want to store revisions as separate documents?
You may want to store revisions separately, and “link” them with their base document. The parent/child support in ElasticSearch would make it possible to work with those relationships and queries.

Rails model versioning - one model with independently versioned columns

I have a model for a multilingual website with wiki functionality which contains various fields I wish to have versioned, however I need to revert changes made to some columns but not others.
For example, I store the English and Spanish versions of a written guide for each model instance.
Imagine:
User A edits the Spanish guide and adds profanity
User B later edits the English guide and makes useful changes.
Typically versioning means that reverting the changes made by user A also will revert later changes by User B, despite the fact that in my case these are two separate concerns.
So I need to essentially have scoped version histories. For example using papertrail:
#guide.rb
has_paper_trail :only => [:en_title, :en_body]
has_paper_trail :only => [:es_title, :es_body]
Any easiest solution for this? I really don't want to move my guides into separate models with a one to one relationship just to achieve this.
Although I'd personally extract translations into their own model to avoid this, you should still be able to achieve this without too much trickery.
PaperTrail creates a new Version object every time a record is changed. Every version is accessible through the object.versions array.
You'll have to come up with a way to decide which attribute you want to revert and which version you want to revert it back to. Once you have this, reverting it shouldn't be very difficult.
For example, in your model:
# Arguments:
# attr - Must respond to to_s with name of attribute to revert
# ver - Integer (e.g. -1 for previous version) or actual Version object
def revert_attribute(attr, ver)
ver = self.versions[ver] if ver.class == Integer
ver = ver.reify
self.write_attribute( attr, ver.read_attribute(attr) )
self.save
end
Code isn't tested or syntax checked, but ought to work from what I've seen in the PaperTrail source.
It's not a perfectly integrated solution, but it should be adequate for most needs.

Using ar-extensions' import: synchronise doesn't work

I am using AR-Extensions to import a large number of objects to db, but synching them back from DB just isn't working.
MY code:
posts = [Post.new(:name=>"kuku1"), Post.new(:name=>"kuku2"), ...]
Post.import posts, :synchronize=>posts
posts are submitted to db, and each one is allocated with primary key (id) automatically. But when afterwards checking the objects in posts array, I see that they don't have id field, and new_record flag is still true.
I also tried adding :reload=>true, but that doesn't help as well.
Any idea why synch doesn't work?
This is not possible right now with new records. As of ar-extensions 0.9.3 this will not work when synchronizing new records as synchronizing expects the records you're sync'ing to already exist. It uses the primary key under the covers to determine what to load (but with new records the primary key is nil). This limitation* also exists in activerecord-import 0.2.5. If you can synchronize on other conditions I'd be happy to release a new gem allowing conditions to be passed in. For Rails 3.x you need to use activerecord-import though (it replaces ar-extensions). Please create ticket/issue on github: https://github.com/zdennis/activerecord-import/issues
For Rails 2.x you still want to use ar-extensions, and I'd likely backport the activerecord-import update and push out a new gem as well. If you'd like this functionality here please create a ticket/issue on github: https://github.com/zdennis/ar-extensions/
Patches are welcome as well.
*The limitation here is a database constraint, as its impossible to get the ids of all newly created records after a single insert/import without doing something strange like table locking, which I don't think is a good solution to that problem. If anyone has ideas I'm all ears.
UPDATE
activerecord-import 0.2.6 and ar-extensions 0.9.4 have been released and includes support for specifying the fields you want to synchronize on. Those fields should be unique. See http://www.continuousthinking.com/2011/4/6/activerecord-import-0-2-6-and-ar-extensions-0-9-4

rails has_and_belongs_to_many or manual implementation

I'm designing a ruby on rails app for a pharmacy, and one of the features is that there are stores who have pharmacists who work there. In addition, there are pharmacists, who can work at many stores. This sounds like a job for HABTM, right? Well, being the novice I am, I manually designed a workaround (because I never heard of HABTM - I basically taught myself rails and never got to some of the more advanced relationships). Right now, when a pharmacist is saved, there's a couple of lines in the create and update action of the pharmacists controller that turns the stores that they work at into a string, with each store_id separated by a comma. Then, when a store is displayed, it does a MYSQL request by
#pharmacists = Pharmacist.find :all, :conditions => "stores REGEXP '#{#store.id}'"
Would moving this system over to a rails based HABTM system be more efficient? Of course it would require less code in the end, but would it be worth it? In other words, what benefits, other than less code, would I get from moving this association to be managed by rails?
The benefit is that you will be using the right tool for the job! The whole point of using a framework such as Rails is that it helps you solve common problems without having to re-invent the wheel, which is what you've done here. By using associations you'll also be using a relational database properly and can take advantage of benefits like foreign key indexing, which will be faster than string manipulation.
You should use a has_and_belongs_to_many relationship unless you need to store extra attributes on the join model (for example the date a pharmacist started working at a store) in which case use has_many :through.
Using Rails associations will give you all the convenient methods that Rails provides, such as these:
# Find the stores the first pharmacist works at
#stores = Pharmacist.first.stores
# Find the pharmacists who work at a store
#pharmacists = Store.find_by_name('A Store').pharmacists
A Guide to ActiveRecord Associations

Resources