Paper Trail saving virtual attribute changes within object_changes - ruby-on-rails

I am trying to persist virtual attribute changes within object_changes and have been working with the meta option.
has_paper_trail(
meta: {
status_name: proc { |foo| foo.status&.name }
}
)
I added status_name to the versions table as the docs mention, and that attribute does get persisted. However, instead of being saved within the object_changes hash, it is in its separate column, and the value does not include the previous value.
# version attributes:
object_changes:
"{\"updated_at\":[\"2023-01-18T22:50:22.394Z\",\"2023-01-18T22:50:22.408Z\"],\"status_id\":[1,2]}",
status_name: "Status 2"
This is working as the gem intends, but I'd like to push the status_name changes into object_changes and include the previous value. One way I've been able to get the before and after changes stored is by creating a new method using the meta flag. But it seems clunky, and I'm wondering if anyone has an alternative:
has_paper_trail(
meta: {
status_name: :track_status_name_changes
}
)
def track_status_name_changes
if versions.blank?
[nil, status&.name].to_json # "[null,\"Status 1\"]"
else
prev_name = JSON.parse(versions.last.status_name).last
[prev_name, status&.name].to_json # "[\"Status 1\",\"Status 2\"]"
end
end
I'm currently exploring creating my own adapter like mentioned in the docs:
PaperTrail.config.object_changes_adapter = MyObjectChangesAdapter.new
class MyObjectChangesAdapter
# #param changes Hash
# #return Hash
def diff(changes)
# ...
end
end
Going with the custom method in the meta flag I am able to track the changes of the virtual attribute, however, I'd also like to have those changes stored in the object_changes column of the versions table.

Related

How to make a serialized column empty?

I have a column in Company, that is serialized as Array:
class Company
serialize :column_name, Array
end
In rails console, when I try the following:
existing = Company.last.column_name
# it gives a array with single element, say [1]
existing.delete(1)
# now existing is []
Company.last.update_attributes!(column_name: existing)
# It empties the array, so:
Company.last.column_name #gives []
But, when I try the same code in remove method of some controller, it never removes the last element. It always returns [1].
How can I empty the serialized column?
NOTE:- This logic works when I have multiple elements, but doesn't work for the last element alone.
CONTROLLER CODE
def remove_restricted_num
company = Company.where(id: params[:id]).first
restricted_numbers = company.restricted_numbers
num = params[:num].to_i
if restricted_numbers.include?(num)
restricted_numbers.delete(num)
company.update_attributes!(restricted_numbers: restricted_numbers)
render js: "alert('#{num} was removed')"
else
render js: "alert('Number not found in the list')"
end
end
I got the fix, we need to use dup to get a independent variable existing, which otherwise was referencing Company.last.column_name
existing = Company.last.column_name.dup # THIS WORKS!!!
existing.delete(1)
Company.last.update_attributes!(column_name: existing)
This updates the column to [], as I need.

How to lazy load constants into rails environment

I have a set of API keys that I want to load into the rails environment, for easy and frequent access (without hitting the database). How can I lazy load it:
1) Let's say there is an API_KEY hash in rails environment (that I initialize using an initializer)
2) When I look up for a key, I first look up in API_KEY hash, if not found, I fetch from database, and at the same time add to API_KEY hash, such that it is accessible for all future requests.
3) In case someone changes the the api key, I can update API_KEY hash in case the key exists.
Can someone help me with the calls to create, delete and update the API_KEY hash in the rails environment (from within the code, instead of the initial rails loading)? Will there be a problem in this approach if each passenger thread loads the rails environment separately?
Any other problems that you see with this approach? The data set (number of api keys) is finite.
Since you're actually just storing values in a Hash (its being assigned to a constant is immaterial since you're not freezing it or anything) I would use the block form of Hash.new. I don't know what your database looks like, but supposing you had these values stored in a model called APIKey that has attributes name and value:
API_KEY = Hash.new do |hash, key_name|
hash[key_name] = APIKey.where(:name => key_name).pluck(:value)
end
Now when you access the API_KEY hash with a key that doesn't exist, it will query the APIKey model and assign the value of the value attribute to that element of the hash. Suppose you have this in your api_keys table:
name value
--------- ----------------
S3_ACCESS 0123456789abcdef
Then you could access the hash defined above like this:
puts API_KEY[:S3_ACCESS]
# Query: SELECT `value` FROM `api_keys` WHERE `name` = 'S3_ACCESS']
# => 0123456789abcdef
puts API_KEY.inspect
# => { :S3_ACCESS => "0123456789abcdef" }
puts API_KEY[:S3_ACCESS]
# No query!
# => 0123456789abcdef
If you want to update the value at runtime then you can do it like any hash:
API_KEY[:S3_ACCESS] = '5555555555ffffff'
# => "5555555555ffffff"
puts API_KEY.inspect
# => { :S3_ACCESS => "5555555555ffffff" }
However, changing the hash will not update the database record.
Advanced
If you want the database record to be updated if you update the hash, you'll have to override Hash#[]=, and if you're going to go that far you might as well use ActiveRecord directly. For example:
class APIKey < ActiveRecord::Base
attr_accessible :name, :value
##_cached_values = Hash.new do |_cached_values, key_name|
# This will return nil if there's no record in the database with the
# given name; alternatively you could use `first!` which would raise a
# RecordNotFound exception which you could rescue and do something
# useful with.
_cached_values[key_name] = self.where(:name => key_name).pluck(:value)
end
def self.[](key_name)
##_cached_values[key_name]
end
def self.[]=(key_name, new_value)
# If the database already has a value for this key_name, fetch the object;
# otherwise initialize a new object
api_key = self.where(:name => key_name).first_or_initialize
# Update the value and save the record
api_key.update_attributes!(:value => new_value)
# Update the cached value
##_cached_values[key_name] = new_value
end
end
puts APIKey[:S3_ACCESS]
# Query: SELECT `value` FROM `api_keys` WHERE `name` = 'S3_ACCESS'
# => 0123456789abcdef
APIKey[:S3_ACCESS] = '5555555555ffffff'
# Query: UPDATE `api_keys` SET `value` = '5555555555ffffff'
# WHERE `name` = 'S3_ACCESS'
# => '5555555555ffffff'
APIKey[:NEW_KEY] = 'new_val'
# Query: INSERT INTO `api_keys` (`name`, `value`)
# VALUES ('NEW_KEY', 'new_val')
# => 'new_val'
With this kind of implementation APIKey[:S3_ACCESS] would work the same as the API_KEY example above; and APIKey[:S3_ACCESS] = 'foo' would perform an UPDATE or INSERT as necessary.
This is probably reinventing the wheel, though; at this point you're probably better off using one of the many configuration management gems people much smarter than me have written. Also, let us not forget The Twelve-Factor App, which exhorts us to store config in the environment.
P.S. You get ridiculous overimplementation points if you define APIKey.const_missing so you can do APIKey::S3_ACCESS instead of APIKey[:S3_ACCESS].

Rails activerecord dirty flag being set on attribute reader method

I'm trying to avoid having my model set the updated_at attribute when it's saved without any changes being made. Through the debugging process I've noticed that the 'changed' array lists an attribute which I know wasn't actually changed from within the form. That attribute does however have a reader method. That method gives a result which is different from its current state. Is there a way of manually clearing the 'changed' flag on that attribute within the reader method?
Wine.rb
before_save :check_for_changes
def blends
if read_attribute(:blends).nil?
"100% #{ name.split(" ").map { |w| varietals.include?(w.downcase) ? w : nil }.compact.join(" ") }"
else
read_attribute(:blends)
end
end
def check_for_changes
return changed?
end
Basically the 'blends' method shoots out "100% [varietal name]" if there aren't any blend %'s listed. This apparently interferes with the 'changed' flag...
I avoid overriding field readers because that keeps me from accessing values stored in the database directly (i.e. without having to call read_attribute) for other logic. That's troublesome when you need to check if there is actually a value stored in the database or encounter situations when there is a value stored but it is also in a format of "100% some-varietal", as I think you would if you have a form field for this.
I usually add in something like this instead:
def blends_description
if blends.nil?
"100% #{ name.split(" ").map { |w| varietals.include?(w.downcase) ? w : nil }.compact.join(" ") }"
else
blends
end
end

Rails 3 check if attribute changed

Need to check if a block of attributes has changed before update in Rails 3.
street1, street2, city, state, zipcode
I know I could use something like
if #user.street1 != params[:user][:street1]
then do something....
end
But that piece of code will be REALLY long. Is there a cleaner way?
Check out ActiveModel::Dirty (available on all models by default). The documentation is really good, but it lets you do things such as:
#user.street1_changed? # => true/false
This is how I solved the problem of checking for changes in multiple attributes.
attrs = ["street1", "street2", "city", "state", "zipcode"]
if (#user.changed & attrs).any?
then do something....
end
The changed method returns an array of the attributes changed for that object.
Both #user.changed and attrs are arrays so I can get the intersection (see ary & other ary method). The result of the intersection is an array. By calling any? on the array, I get true if there is at least one intersection.
Also very useful, the changed_attributes method returns a hash of the attributes with their original values and the changes returns a hash of the attributes with their original and new values (in an array).
You can check APIDock for which versions supported these methods.
http://apidock.com/rails/ActiveModel/Dirty
For rails 5.1+ callbacks
As of Ruby on Rails 5.1, the attribute_changed? and attribute_was ActiveRecord methods will be deprecated
Use saved_change_to_attribute? instead of attribute_changed?
#user.saved_change_to_street1? # => true/false
More examples here
ActiveModel::Dirty didn't work for me because the #model.update_attributes() hid the changes. So this is how I detected changes it in an update method in a controller:
def update
#model = Model.find(params[:id])
detect_changes
if #model.update_attributes(params[:model])
do_stuff if attr_changed?
end
end
private
def detect_changes
#changed = []
#changed << :attr if #model.attr != params[:model][:attr]
end
def attr_changed?
#changed.include :attr
end
If you're trying to detect a lot of attribute changes it could get messy though. Probably shouldn't do this in a controller, but meh.
Above answers are better but yet for knowledge we have another approch as well,
Lets 'catagory' column value changed for an object (#design),
#design.changes.has_key?('catagory')
The .changes will return a hash with key as column's name and values as a array with two values [old_value, new_value] for each columns. For example catagory for above is changed from 'ABC' to 'XYZ' of #design,
#design.changes # => {}
#design.catagory = 'XYZ'
#design.changes # => { 'catagory' => ['ABC', 'XYZ'] }
For references change in ROR

Rails3-jquery-autocomplete distinct values

I'm using the rails3-jquery-autocomplete gem on a field with non-unique values, but I want the results it retrieves to be duplicate-free. Any ideas on how to accomplish this?
I had the same problem in my project https://github.com/marciomr/Terra-Livre and I solved it doing the following:
I installed rails3-jquery-autocomplete as a plugin in vendor/plugin directory
I changed the file helpers.rb like this:
def json_for_autocomplete(items, method, extra_data)
json = items.collect do |item| # here I put the result in a variable
hash = {"label" => item.send(method), "value" => item.send(method)} #here I removed the id
extra_data.each do |datum|
hash[datum] = item.send(datum)
end if extra_data
hash
end
json.uniq # this line is new
end
I removed the id from the json file and then retrieved uniq values.
Since I didn't need the id it worked fine for me. I think if I need the id I can put it in extra_data, but I am not sure.
I have just forked the project with this alteration: git://github.com/marciomr/rails3-jquery-autocomplete.git
Since I ran into this myself, I thought I would record my own solution for posterity, since it does not require editing the gem's source. This is for the officially maintained fork of the gem: https://github.com/bigtunacan/rails-jquery-autocomplete.
You can handle the json encoding directly via the autocomplete block in the controller, which we can leverage to change the array of records.
Here is an example in which we get a unique list of schools that students go to:
autocomplete :student, :school do |items|
ActiveSupport::JSON.encode( items.uniq{ |i| i["value"] } )
end
"items" is an array of hashes, which by default contain an id, a label, and a value, so this passes only unique values into the json encoder (of your choice).

Resources