How can I customise AttributeSerializerFactory in papertrail? - ruby-on-rails

I am having an issue creating versions for a model that contains datetime columns.
I do not know why. I've already asked a question. See Error on reify a version using ruby PaperTrail
Because I do not find a solution and I've verified that everything else works if I just skip these columns, I would like to solve the issue implementing my own serializer/deserializer.
I can see that papertrails relies on a class called CastAttributeSerializer and that this class uses a factory as shown in the following snippet:
def deserialize(attr, val)
if defined_enums[attr] && val.is_a?(::String)
# Because PT 4 used to save the string version of enums to `object_changes`
val
elsif PaperTrail::RAILS_GTE_7_0 && val.is_a?(ActiveRecord::Type::Time::Value)
# Because Rails 7 time attribute throws a delegation error when you deserialize
# it with the factory.
# See ActiveRecord::Type::Time::Value crashes when loaded from YAML on rails 7.0
# https://github.com/rails/rails/issues/43966
val.instance_variable_get(:#time)
else
AttributeSerializerFactory.for(#klass, attr).deserialize(val)
end
def serialize(attr, val)
AttributeSerializerFactory.for(#klass, attr).serialize(val)
end
If I understand correctly, I should somehow modify the factory AttributeSerializerFactory to introduce my own serializer/deserializer for the class Time (that is failing).
What's the correct way to do it?

Related

Paper trail manual versioning isn't working as expected

I'm using the paper_trail gem. My use case involves creating versions when I explicitly want to instead of on callbacks like on: [:update] or something.
I did so by adding on: [] in my my_model.rb and using paper_trail.touch_with_version on my model instance. The problem is whenever I do this the first time the version is saved with nil attributes. Weird enough, the next time call paper_trail.touch_with_version it saves it correctly with all attributes correctly initialised.
Sample logs from my rails console:
document = Document.first
#<Document:0x0000123456
id: 1,
name: "Sample document">
document.paper_trail.touch_with_version
document.versions.last.reify.name
=> nil
document.paper_trail.touch_with_version
document.versions.last.reify.name
=> "Sample document"
What's really peculiar is that if I try doing document.paper_trail.touch_with_version followed by document.versions.last.reify.name I correctly get name of my document but if I exit console and repeat this process the first time the attributes of the object saved in that version is again nil.
I'm probably doing something obvious but didn't find anything on wiki that talks about this. Can someone explain to me where I'm messing up?
Update: I traced back the code in paper_trail library and found where the issue is. I found the anomaly in record_trail.rb:
# #api private
def attribute_in_previous_version(attr_name)
if #in_after_callback && RAILS_GTE_5_1
#record.attribute_before_last_save(attr_name.to_s)
else
#record.attribute_was(attr_name.to_s)
end
end
So in console the #record.attribute_before_last_save(attr_name.to_s) is giving me nil but if I try #record.attribute_was('name') in my console it gives me the correct attribute(in this case name) of the record.

Audited Gem current_user_method not working

I am using the Audited gem in my application for tracking user log. Everything is working fine except for current user tracking.
In my case, I have 2 models: Instructor and Student. Instructor will be the current_admin_user, and I need to find the student manually.
To overcome this issue, I tried to override current_user_method and create an audited.rb file in the initializers with below content:
Audited.current_user_method = :current_admin_user
This is working fine, but when I use any other method like current_user_or_student ...
Audited.current_user_method = :current_user_or_student
in application_controller.rb ...
def current_user_or_student
current_admin_user || InstructorStudent.find_by_id(id)
end
it is not going into this method, even current_admin_user is also not storing in audits.
Why isn't my current_user_or_student method being called when overriding it in application_controller.rb?
Finally I resolved my issue, I am using STI (single table inheritance) .
my other controllers of instructor was not inherite from application controller. so my current_user_or_student was not called for instructor login .
To overcome this issue, i create one controller concern called audited_user.rb
and write following method in concern
def current_user_or_student
current_admin_user || InstructorStudent.find_by_id(id)
end
and included this concern in both my instructor base controller and application controller.
now everything is working fine and my audited user is also save correctly.
I hope this will save any others day.
You have defined current_user_or_student as such:
def current_user_or_student
current_admin_user || InstructorStudent.find_by_id(id)
end
Assuming current_admin_user is nil, it will try to find InstructorStudent. You are using find_by_id which will return nil if the InstructorStudent with that id cannot be found. It's likely that is what is happening. It's not clear from this source what id is -- are you sure it's set to an id that can be found in instructor_students? I would encourage you to do the following to debug:
def current_user_or_student
current_admin_user || InstructorStudent.find(id)
end
This will raise an error if the method's conditional falls through to the InstructorStudent.find branch and the id cannot be found. If my hypothesis is correct, this will prove that the id cannot be found, so your original code is returning nil (while this updated code now errors instead). The error message will also tell you what the value of id is which you can use for further debugging.
Alternatively, you can debug this without changing code by running a request that you expect to be audited but isn't and then inspecting the server logs. You will see the queries run there and may be able to debug that way as well.

How to save nil into serialized attribute in Rails 4.2

I am upgrading an app to Rails 4.2 and am running into an issue where nil values in a field that is serialized as an Array are getting interpreted as an empty array. Is there a way to get Rails 4.2 to differentiate between nil and an empty array for a serialized-as-Array attribute?
Top level problem demonstration:
#[old_app]
> Rails.version
=> "3.0.3"
> a = AsrProperty.new; a.save; a.keeps
=> nil
#[new_app]
> Rails.version
=> "4.2.3"
> a = AsrProperty.new; a.save; a.keeps
=> []
But it is important for my code to distinguish between nil and [], so this is a problem.
The model:
class AsrProperty < ActiveRecord::Base
serialize :keeps, Array
#[...]
end
I think the issue lies with Rails deciding to take a shortcut for attributes that are serialized as a specific type (e.g. Array) by storing the empty instance of that type as nil in the database. This can be seen by looking at the SQL statement executed in each app:
[old_app]: INSERT INTO asr_properties (lock_version, keeps)
VALUES (0, NULL)
Note that the above log line has been edited for clarity; there are other serialized attributes that were being written due to old Rails' behavior.
[new_app]: INSERT INTO asr_properties (lock_version)
VALUES (0)
There is a workaround: by removing the "Array" declaration on the serialization, Rails is forced to save [] and {} differently:
class AsrProperty < ActiveRecord::Base
serialize :keeps #NOT ARRAY
#[...]
end
Changing the statement generated on saving [] to be:
INSERT INTO asr_properties (keeps, lock_version) VALUES ('---[]\n', 0)
Allowing:
> a = AsrProperty.new; a.save; a.keeps
=> nil
I'll use this workaround for now, but:
(1) I feel like declaring a type might allow more efficiency, and also prevents bugs by explicitly prohibiting the wrong data type being stored
(2) I'd really like to figure out the "right" way to do it, if Rails does allow it.
So: can Rails 4.2 be told to store [] as its own thing in a serialized-as-Array attribute?
What's going on?
What you're experiencing is due to how Rails 4 treats the 2nd argument to the serialize call. It changes its behavior based on the three different values the argument can have (more on this in the solution). The last branch here is the one we're interested in as when you pass the Array class, it gets passed to the ActiveRecord::Coders::YAMLColumn instance that is created. The load method receives the YAML from the database and attempts to turn it back into a Ruby object here. If the coder was not given the default class of Object and the yaml argument is nil in the case of a null column, it will return a new instance of the class, hence the empty array.
Solution
There doesn't appear to be a simple Rails-y way to say, "hey, if this is null in the database, give me nil." However looking at the second branch here we see that we can pass any object that implements the load and dump methods or what I call the basic coder protocol.
Example code
One of the members of my team built this simple class to handle just this case.
class NullableSerializer < ActiveRecord::Coders::YAMLColumn
def load(yaml)
return nil if yaml.nil?
super
end
end
This class inherits from the same YAMLColumn class provided by ActiveRecord so it already handles the load and dump methods. We do not need any modifications to dump but we want to slightly handle loading differently. We simply tell it to return nil when the database column is empty and otherwise call super to work as if we made no other modification.
Usage
To use it, it simply needs to be instantiated with your intended serialization class and passed to the Rails serialize method as in the following, using your naming from above:
class AsrProperty < ActiveRecord::Base
serialize :keeps, NullableSerializer.new(Array)
# …
end
The "right" way
Getting things done and getting your code shipped is paramount and I hope this helps you. After all, if the code isn't being used and doing good, who cares how ideal it is?
I would argue that Rails' approach is the right way in this case especially when you take Ruby's philosophy of The Principle of Least Surprise into account. When an attribute can possibly be an array, it should always return that type, even if empty, to avoid having to constantly special case nil. I would argue the same for any database column that you can put a reasonable default on (i.e. t.integer :anything_besides_a_foreign_key, default: 0). I've always been grateful to past-Aaron for remembering this most of the time whenever I get an unexpected NoMethodError: undefined method 'whatever' for nil:NilClass. Almost always my special case for this nil is to supply a sensible default.
This varies greatly on you, your team, your app, and your application and it's needs so it's never hard and fast. It's just something I've found helps me out immensely when I'm working on something and wondering if amount could default to 0 or if there's some reason buried in the code or in the minds of your teammates why it needs to be able to be nil.

Devise current_user weird behavior after upgrade to ruby 1.9.3

I have upgraded by project from ree-1.8.7 to 1.9.2p429.
I have an attribute in my devise model named silhouette_user_id. In views and occasionally in my ruby code, calling
current_user.silhouette_user_id
returns a different (and WRONG) value than
current_user[:silhouette_user_id]
which returns the right value.
Though I could search and replace to fix this, I'm worried that other attributes will behave the same way. This is a LARGE project and I really need to determine why this is happening.
Any ideas would be greatly appreciated.
I solved this issue. During the upgrade I had tried to update how I was defining new methods for the User class. Unfortunately, I did it wrong. I used
def define_new_method(key, value)
self.class.send(:define_method, key.to_sym) do
value
end
end
which actually changed the methods for the CLASS User rather than the instance for which it was intended. Changing to this:
def define_new_method(key, value)
define_singleton_method key.to_sym, lambda { value }
end
resulted in the new methods being defined only for the instance intended and not for the current_user instance.

Validation: how to check for specific error

I know how to check an attribute for errors:
#post.errors[:title].any?
Is it possible to check which validation failed (for example "uniqueness")?
Recently I came across a situation where I need the same thing: The user can add/edit multiple records at once from a single form.
Since at validation time not all records have been written to the database I cannot use #David's solution. To make things even more complicated it is possible that the records already existing in the database can become duplicates, which are detected by the uniqueness validator.
TL;DR: You can't check for a specific validator, but you can check for a specific error.
I'm using this:
# The record has a duplicate value in `my_attribute`, detected by custom code.
if my_attribute_is_not_unique?
# Check if a previous uniqueness validator has already detected this:
unless #record.errors.added?(:my_attribute, :taken)
# No previous `:taken` error or at least a different text.
#record.errors.add(:my_attribute, :taken)
end
end
Some remarks:
It does work with I18n, but you have to provide the same interpolation parameters to added? as the previous validator did.
This doesn't work if the previous validator has written a custom message instead of the default one (:taken)
Regarding checking for uniqueness validation specifically, this didn't work for me:
#post.errors.added?(:title, :taken)
It seems the behaviour has changed so the value must also be passed. This works:
#post.errors.added?(:title, :taken, value: #post.title)
That's the one to use ^ but these also work:
#post.errors.details[:title].map { |e| e[:error] }.include? :taken
#post.errors.added?(:title, 'has already been taken')
Ref #34629, #34652
By "taken", I assume you mean that the title already exists in the database. I further assume that you have the following line in your Post model:
validates_uniqueness_of :title
Personally, I think that checking to see if the title is already taken by checking the validation errors is going to be fragile. #post.errors[:title] will return something like ["has already been taken"]. But what if you decide to change the error message or if you internationalize your application? I think you'd be better off writing a method to do the test:
class Post < ActiveRecord::Base
def title_unique?
Post.where(:title => self.title).count == 0
end
end
Then you can test if the title is unique with #post.title_unique?. I wouldn't be surprised if there's already a Rubygem that dynamically adds a method like this to ActiveRecord models.
If you're using Rails 5+ you can use errors.details. For earlier Rails versions, use the backport gem: https://github.com/cowbell/active_model-errors_details
is_duplicate_title = #post.errors.details[:title].any? do |detail|
detail[:error] == :uniqueness
end
Rails Guide: http://guides.rubyonrails.org/active_record_validations.html#working-with-validation-errors-errors-details

Resources