I'm trying to add a validation to PaperTrail::Version which will prevent sensitive data from being stored in the versions table. The idea being you'll get lots of obvious errors if you forget to sanitize your has_paper_trail call within your model.
If I add a custom validator in config/initializers/paper_trail it works ... for a while. Then PaperTrail starts acting with its default behavior and my methods are undefined.
Example Code:
PaperTrail::Rails::Engine.eager_load!
module PaperTrail
class Version
# Ensure no sensitive values end up in the versions table
validate :prohibited_attributes
...
Try a custom version class. See documentation section 6.a. Custom Version Classes.
6.a. Custom Version Classes
You can specify custom version subclasses with the :class_name
option:
class PostVersion < PaperTrail::Version
# custom behaviour, e.g:
self.table_name = :post_versions
end
class Post < ActiveRecord::Base
has_paper_trail :class_name => 'PostVersion'
end
Using PaperTrail::Rails::Engine.eager_load! was a good idea. Not sure why that didn't work for you. Hopefully this is a workaround.
Related
I have came across a scenario in an existing code base.
Im upgrading Rails to 5.1 from 5.0 and we now need to define relationships as optional where its not required.
I have came across a situation where we have a class and a mixin that are causing conflict.
Is there a way to define the relationship with the :user so that it will meet requirements in both scenarios.
In the Model, a user is not required, but within the mixin, a users presence is validated, causing the conflict.
As the Mixin is included, writing a proc to evaluate whether to mark the relationship as optional by using a function/proc results in the class method being used at runtime. If you prepend the mixin instead, it will default to the method in the mixin, meaning either way they conflict.
class Foo < ActiveRecord::Base
belongs_to :user, optional: true
end
module Mixins
module Foo
extend ActiveSupport::Concern
validates :user, presence: true, if: :user_is_required?
private
def user_is_required?
<!-- LOGIC HERE -->
end
end
end
At an engine level.. the mixin is included as so:
::Foo.include(Mixins::Foo)
I am reading through The Rails 4 way (by Obie Fernandez), a well-known book about Rails, and from what I've read so far, I can highly recommend it.
However, there is an example section 9.2.7.1: Multiple Callback Methods in One Class that confuses me:
Bear with me, to make the problem clear for everyone, I have replicated the steps the book describes in this question.
The section talks about Active Record callbacks (before_create, before_update and so on), and that it is possible to create a class that handles multiple callbacks for you. The listed code is as follows:
class Auditor
def initialize(audit_log)
#audit_log = audit_log
end
def after_create(model)
#audit_log.created(model.inspect)
end
def after_update(model)
#audit_log.updated(model.inspect)
end
def after_destroy(model)
#audit_log.destroyed(model.inspect)
end
end
The book says then that to add this audit logging to an Active Record class, you would do the following:
class Account < ActiveRecord::Base
after_create Auditor.new(DEFAULT_AUDIT_LOG)
after_update Auditor.new(DEFAULT_AUDIT_LOG)
after_destroy Auditor.new(DEFAULT_AUDIT_LOG)
...
end
The book then notes that this code is very ugly, having to add three Auditors on three lines, and that it not DRY. It then goes ahead and tells us that to solve this problem, we should monkey-patch an acts_as_audited method into the Active Record::Base object, as follows:
(the book suggests putting this file in /lib/core_ext/active_record_base.rb)
class ActiveRecord::Base
def self.acts_as_audited(audit_log=DEFAULT_AUDIT_LOG)
auditor = Auditor.new(audit_log)
after_create auditor
after_update auditor
after_destroy auditor
end
end
which enables you to write the Account Model class as follows:
class Account < ActiveRecord::Base
acts_as_audited
...
end
Before reading the book, I have already made something similar that adds functionality to multiple Active Record models. The technique I used was to create a Module. To stay with the example, what I have done was similar to:
(I would put this file inside /app/models/auditable.rb)
module Auditable
def self.included(base)
#audit_log = base.audit_log || DEFAULT_AUDIT_LOG #The base class can override it if wanted, by specifying a self.audit_log before including this module
base.after_create audit_after_create
base.after_update audit_after_update
base.after_destroy audit_after_destroy
end
def audit_after_create
#audit_log.created(self.inspect)
end
def audit_after_update
#audit_log.updated(self.inspect)
end
def audit_after_destroy
#audit_log.destroyed(self.inspect)
end
end
Note that this file both replaces the Auditor and the monkey-patched ActiveRecord::Base method. The Account class would then look like:
class Account < ActiveRecord::Base
include Auditable
...
end
Now you've read both the way the book does it, and the way I would have done it in the past. My question: Which version is more sustainable in the long-term? I realize that this is a slightly opinionated question, just like everything about Rails, but to keep it answerable, I basically want to know:
Why would you want to monkey-patch ActiveRecord::Base directly, over creating and including a Module?
I would go for the module for a few reasons.
Its obvious; that is to say, I can quickly find the code that defines this behavior. In acts_as_* I don't know if its from some gem, library code, or defined within this class. There could be implications about it being overridden or piggy-backed in the call-stack.
Its portable. It uses method calls that are commonly defined in libraries that define callbacks. You could conceivably distribute and use this library in non-active-record objects.
It avoids the addition of unnecessary code on the static level. I'm a fan of having less code to manage (less code to break). I like using Ruby's niceties without doing to much to force it to be "nicer" than it already it is.
In a monkey-patch setting you are tying the code to a class or module that could go away and there are scenarios where it would fail silently until your class can't call acts_as_*.
One downfall of the portability argument is the testing argument. In which case I would say you can write your code to protect against portability, or fail early with smart warnings about what will and won't work when used portably.
Currently, I use api-versions gem which nicely creates api routes and contains the version information in the request HEAD.
However, sometimes the models need to know the api version being used. Example, validations. Let's say presence of username needs to be validated on any version >= 2, but not on versions < 2.
In the Controller layer, it's easy to retrieve the api version by parsing out the appropriate request HEAD. But what's the best way to communicate that version number to the model?
How about defining a psudo-attribute in the model class with validators acting conditionally
class SomeModel < ActiveRecord::Base
attr_accessor :api_version
validates_presence_of :username, :if => :api_version > 2
...
Then from the controller, set the api_version on the model before saving etc
#someModel = SomeModel.new(params[:some_model])
#someModel.api_version = <<extracted from HEAD>>
#someModel.save!
I need to store the specific version of a model with an order. I'm planning to use a versioning gem like paper_trail or vestal_versions. I'd like the correct version automatically loaded with the order.
Ideally, I'd simply store the object with order.update_attributes(:stuff => bought_stuff) and the order would remember the version of the stuff so that subsequent loads would make order.reload.stuff still be the object as it was when the order was saved.
Is there a gem that would provide such a functionality? I couldn't find one.
Otherwise, how can I achieve that with ActiveRecord and a versioning gem?
Actually, I could achieve almost what I want with PaperTrail and this :
class Stuff < ActiveRecord::Base
has_paper_trail
end
class Order < ActiveRecord::Base
belongs_to :stuff
def stuff_with_version
stuff_without_version.version_at(created_at) if stuff_without_version
end
alias_method_chain :stuff, :version
end
Not sure this is necessarily the best design for you, but you could use paper_trail for this. Simply add the macro method 'has_paper_trail' at the top of your model class and any time an instance changes, a serialised copy of it is created in a table called "versions" along with a polymorphic relationship back to the actual model.
Supposing you want to relate a particular version of a 'product' to an order, start by adding a relationship to the versions table - i.e. a migration that adds a 'version_id' to your order, and then set up the relationship as follows:
class Order
belongs_to :version
def product
version
end
def product=(p)
version=p.versions.last
end
end
class Product
has_paper_trail
end
Using this, when you add a product to an order, it will relate the order to the latest version of the product instead. When you retrieve the product, it will pull out the version; i.e. the product as it was when you created the order. Getting the relationship to work the other way around (i.e. relating products back to orders) might be more complicated, but this is a start.
Is it possible to do an association extension on a belongs_to relation?
I have tried to do it using a module but keep being told that the method I'm calling is private:
module TestExtension
def test
puts 'test successful'
end
end
class Question < ActiveRecord::Base
belongs_to :user, extend: TestExtension
end
Every time I run it though it complains that the method is private
q = Question.first
q.test
# => NoMethodError: Attempt to call private method `test'
I'm not 100% clear whether it's possible to do AR Extensions on belongs_to. It was working fine on Rails 3.0.7 but is now failing in 3.1.0
This is a known issue in 3.1.0 that hasn't been resolved yet. Basically, the new association design in 3.1.0 doesn't support extending belongs_to associations since it was never a supported feature in the first place. However, the issue is still open so it may be resolved in the future; you should probably comment on the issue to voice support if you want it.
Also, the private method error you're getting, you would be getting even if you didn't have the extend: TestExtension part; I believe #test is a private method on all ActiveRecord objects.