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.
Related
This is somewhat related to #837 in that I have a large data column on my models, however I think I may be better served by the opposite of what's proposed in that issue - that is, to maintain the object column but not the object_changes column.
We had been running with no versions.object_changes column. Now that I've added this column, I realized I am writing a lot of data I don't care about for the data column in object_changes - since a tiny change to data causes it to be written out to versions effectively 3x (once in object and twice in object_changes for the before and after).
I don't think skip or ignore is what I want, because I would indeed like the changes to data to produce a new version.
Should I go down the custom version model route? Or what do you recommend?
Some options, in descending order of recommendation (most highly recommended first):
version_limit (Supported) - Save disk space instead by limiting the number of versions you create for a given record, using version_limit. (https://github.com/airblade/paper_trail#2e-limiting-the-number-of-versions-created)
Custom table (Supported) - Custom version model, custom table without object_changes column. Precludes the experimental associations feature (track_associations must be false [the default])
Patch recordable_object_changes, method 1 (Not supported) - Custom version model, but still using the versions table. Override #paper_trail to return a custom child class of RecordTrail which overrides RecordTrail#recordable_object_changes. Overriding these methods breaks your warranty.
Patch recordable_object_changes, method 2 (Not supported) - Override RecordTrail#recordable_object_changes, adding a class-check conditional. Call super for all but the model you want to hack. Overriding this method breaks your warranty.
Custom serializer (Supported, but not for this) - Custom serializer with class-check conditional, and some way of telling whether you're serializing object_changes and not object. Probably a bad idea, seems really hacky.
Finally, I'd be happy to review a PR that adds a new feature, the ability to configure, on a per-model basis, which data should be written to the object_changes column. If you're serious about working on that, and seeing it through to the finish, please open a new issue so we can discuss it further. There are a few different designs that could work.
Update, 2019: We now have object_changes_adapter It's only for expert users, and probably not my top recommendation.
I have quite an odd usecase for PaperTrail. In my scenario a version is based on a physical event happening. Due to this, versions of my model are only created when I say.
Because of the above. I wish those versions to be capable of the following things:
When I run something like Model.all, this should ignore versions as expected but in some cases I wish versions to be returned as normal so I need to be able to do something like Model.withVersions to be returned all the current records and all associated versions. Is this possible?
Also, I need to be able to build a page for versions. Ie: Rails Show Action and View.
How would I accomplish this? I need to be able to craft a link to one of the pages that would output something similar to /model/23 which would take you to a page about a particular version of that item.
I know this is sort of an odd issue but wondering if anyone would have any input on this strange usecase and how I can sometimes bring versions to the foreground to act like existing things and othertimes to stay in the background as normal.
Basically what we're doing here is using the fact that PaperTrail adds a new method to your model called versions which returns an array of PaperTrail::Version objects, one for each version of the model. On any of those version objects you can call .reify to get the model at that version.
Now, I'm not sure how you'd want those versions to be returned, so let's say we create an array of versions for each model and store them in a hash keyed by the primary keys of each model:
hash = {}
Model.all.each { |m| hash[m.id] = m.versions.map(&:reify) }
# so to see the array of all versions of model with id=1
hash[1]
Re your show page, you will need to pass the version as a parameter, eg. /model/23?version=5 and then in your show action you'll do something like:
#model = Model.find(params[:id])
if params[:version]
#model = #model.versions[params[:version]-1].reify
end
I want to save settings for my users and some of them would be one out of a predefined list! Using https://github.com/ledermann/rails-settings ATM.
The setting for f.e. weight_unit would be out of [:kg, :lb].
I don't really want to hardcode that stuff into controller or view code.
It's kind of a common functionality, so I was wondering: Did anyone come up with some way of abstracting that business into class constants or the database in a DRY fashion?
Usually, when I have to store some not important information which I don't care to query individually, I store them on a serialized column.
In your case you could create a new column in your users table (for example call it "settings").
After that you add to user model
serialize :settings, Hash
from this moment you can put whatever you like into settings, for example
user.settings = {:weight_unit => :kg, :other_setting1 => 'foo', :other_setting2 => 'bar'}
and saving with user.save you will get, in settings column, the serialized data.
Rails does also de-serialize it so after fetching a user's record, calling user.settings, you will get all saved settings for the user.
To get more information on serialize() refer to docs: http://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html#method-i-serialize
UPDATE1
To ensure that settings are in the predefined list you can use validations on your user model.
UPDATE2
Usually, if there are some pre-defined values it's a good habit to store them in a constant inside the related model, in this way you have access to them from model (inside and outside). Acceptable values does not change by instance so it makes sense to share them between all. An example which is more valuable than any word. Defining in your User model:
ALLOWED_SETTINGS = {:weight_unit => [:kg, :lb],
:eyes_color => [:green, :blue, :brows, :black],
:hair_length => [:short, :long]}
you can use it BOTH
outside the model itself, doing
User::ALLOWED_SETTINGS
inside your model (in validations, instance methods or wherever you want) using:
ALLOWED_SETTINGS
Based on your question, it sounds like these are more configuration options that a particular user will choose from that may be quite static, rather than dynamic in nature in that the options can change over time. For example, I doubt you'll be adding various other weight_units other than :kg and :lb, but it's possible I'm misreading your question.
If I am reading this correctly, I would recommend (and have used) a yml file in the config/ directory for values such as this. The yml file is accessible app wide and all your "settings" could live in one file. These could then be loaded into your models as constants, and serialized as #SDp suggests. However, I tend to err on the side of caution, especially when thinking that perhaps these "common values" may want to be queried some day, so I would prefer to have each of these as a column on a table rather than a single serialized value. The overhead isn't that much more, and you would gain a lot of additional built-in benefits from Rails having them be individual columns.
That said, I have personally used hstore with Postgres with great success, doing just what you are describing. However, the reason I chose to use an hstore over individual columns was because I was storing multiple different demographics, in which all of the demographics could change over time (e.g. some keys could be added, and more importantly, some keys could be removed.) It sounds like in your case it's highly unlikely you'll be removing keys as these are basic traits, but again, I could be wrong.
TL;DR - I feel that unless you have a compelling reason (such as regularly adding and/or removing keys/settings), these should be individual columns on a database table. If you strongly feel these should be stored in the database serialized, and you're using Postgres, check out hstore.
If you are using PostgreSQL, I think you can watch to HStore with Rails 4 + this gem https://github.com/devmynd/hstore_accessor
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.
I've been looking around all afternoon to try and find the dominant convention for setting up user-modifiable application-wide settings in Rails... no dice so far.
I'm working on a simple application which is intended to be used by one organization, so there's no need for a user model, there's only a single administrator. That administrator needs to have the ability to modify certain site-wide preferences, things like the logo, color scheme, tagline, etc.
What's the best practice for creating this kind of application-wide settings in Rails 3.1, and making them easily accessible to the end-user? Bonus points for any example apps you can link to.
The dominant convention to store editable app-wide settings seems to be the concept of a key-value store, backed either by ActiveRecord or other mechanisms. And, as far as I know, there are at least two nice strategies for storing your app-wide settings, depending on your requisites.
If you want a generic approach, yet extremely flexible for defining a couple of (not) scoped settings that can be in association with AR Models, you have Rails-Settings (or its cached version Rails-Settings-Cached). I haven't tried using the plugin in Rails 3.1 but it works well on 3.0. It allows you to have things like:
Settings.main_color = '#3333CC'
Settings.logo_file_name = 'images/logo.png'
Setting['preferences.color'] = :blue
In case you want a robust approach, with Single-Table-Inheritance and allowing you to perform validations in certain settings as you would with actual AR Records, you have this nice article, written by Jeff Dean, which steps you through the process. This way you scope settings by grouping them into subclasses and you can have things like:
class ApplicationSettings::PageLayout < ApplicationSetting
validates :title, :presence => true
...
def title
value
end
def title=(value)
self.value = value
end
And I guess that with some simple tuning you can even have has_many and belongs_to associations in some of your settings (like a variable-sized list of phone numbers or e-mails).
Personally I prefer the latter approach (when settings are a big issue) because it gives you more control over the settings you store and keeps your code clean and DRY, allowing you to follow the MVC pattern.
I generally set up a Properties model with some basic scaffolding in the admin section - then call the relevant Property 'field' where required, say Property.find(:field => "EARLIEST_DATE:YEAR") which would have a user settable value.
Properties might not be the best name for a database table (tend to think there's too much chance of a reserved name collision somewhere down the line) - but you get the idea. Advantage is you can set up scopes to access the values set by the user.