paper trail editing previous object - paper-trail-gem

I've been using paper_trail 3.0 for a long time and been happily editing previous versions with the following code;
# Update a level version with a new date or costing
def update_version(version_id, data)
version = PaperTrail::Version.find(version_id) # Find the version
hash = YAML.load(version.object) # Convert object to a hash
hash["from_date"] = data[:from_date] unless data[:from_date].nil? # Update date
hash["cost_cents"] = data[:cost_cents] unless data[:cost_cents].nil? # Update cost
version.object = YAML.dump(hash) # Convert hash back into YAML
version.save # Save the version
end
After upgrading to paper trail 4.0 this has stopped working as I can no longer do a version.object call. Is there a way to keep editing previous versions easily with version 4.0 of a paper trail and future versions of paper trail?
Update
After looking into it further I think the update_version may work but how it's creating the versions is different in paper_trail 4.0
This is how I create the version
has_paper_trail :only => [:from_date],
:if => Proc.new { |level|
level.versions.count == 0 || level.versions.item.first != nil && (level.versions.first.item.from_date.nil? || level.from_date > level.versions.first.item.from_date)
}
After putting a byebug in here I found that level.versions.item is nil!
Update
Within my tests which are working in paper_trail 3.x but not in 4.x, I use update_attributes. I've noticed that between the two versions paper_trail treats the update_attributes differently. The ': if' rule above is run before update_attributes in 3.x, but in 4.x it runs after. Which means my check on the 'from_date' doesn't work. Therefore it's not creating another version.
Answer
I have to move my Proc into the only section like so;
has_paper_trail :only => [:from_date => Proc.new { |level|
level.versions.count == 0 || level.versions.first.item != nil && (level.versions.first.item.from_date.nil? || level.from_date > level.versions.first.item.from_date)
}
]

Related

Rails ignores the default date format after upgrading from 6.1 to 7.0

Our application previously defined the default date format as DD/MM/YYYY in config/application.rb like so:
Date::DATE_FORMATS[:default] = '%d/%m/%Y'
This worked as expected in Rails 6.1, but after upgrading to Rails 7.0 it now appears to be ignored by .to_s:
Loading development environment (Rails 7.0.2.2)
3.0.1 :001 > Date::DATE_FORMATS[:default]
=> "%d/%m/%Y"
3.0.1 :002 > Date.new(2022, 12, 31).to_s
=> "2022-12-31"
3.0.1 :003 > Date.new(2022, 12, 31).to_fs
=> "31/12/2022"
How can I have .to_s implement this behaviour in Rails 7.0+?
Bear in mind, config.active_support.disable_to_s_conversion doesn't restore the original functionality of to_s on a date. Your dates will not hit the default formatter you have configured.
I'm not sure if any of this is intentional.
You have two options to fix this
Find everywhere you need your date to display in your selected format and add .to_fs. This is probably the more correct way to fix this.
Or do what we did and defy the wishes of the rails devs. Add an initializer that overrides the .to_s method on a date because going to over 9000 different locations to add .to_fs to each date display in your code is just too much for you to think about right now.
/config/intitializers/date_format_fix.rb
require "date"
class Date
def to_s(format = :default)
if formatter = DATE_FORMATS[format]
if formatter.respond_to?(:call)
formatter.call(self).to_s
else
strftime(formatter)
end
else
to_default_s
end
end
end
This feature was deprecated in Rails 7. You can turn it back on with config.active_support.disable_to_s_conversion
Deprecate passing a format to #to_s in favor of #to_fs in Array, Range, Date, DateTime, Time, BigDecimal, Float and, Integer.
This deprecation is to allow Rails application to take advantage of a Ruby 3.1 optimization that makes interpolation of some types of objects faster.
New applications will not have the #to_s method overridden on those classes, existing applications can use config.active_support.disable_to_s_conversion.
Based on #scilence answer, same solution for datetime column from the database will look like this:
module ActiveSupport
class TimeWithZone
def to_s(format = :default)
if formatter = Time::DATE_FORMATS[format]
if formatter.respond_to?(:call)
formatter.call(self).to_s
else
strftime(formatter)
end
else
to_default_s
end
end
end
end
In this case for rails 7, used in form:
f.input_field :period_from, label: false, class: 'form-control',
data: { behaviour: 'datepicker', language: I18n.locale }, value: f.object.period_from&.to_fs

Hash condition in where method Ruby on Rails

I have an issue attempting to perform this call:
animals = pet_animals.where(
:healthy => true,
:owned => false,
:bought <= bought_date
)
This line is causing errors. :bought <= bought_date
How can I perform this comparison?
When you are running one an up-to-date version of Ruby and Ruby on Rails then you can use in infinite range to describe the condition:
animals = pet_animals.where(
healthy: true, owned: false, bought: (..bought_date)
)
For version for Ruby below Ruby 2.6 or prior Rails 6.0 you need to use the array syntax to describe such conditions:
animals = pet_animals.where(healthy: true, owned: false)
.where("bought <= ?", bought_date)
Btw I switched to the newer hash syntax because it is more common nowadays than the old hash rocket (=>) in cases like these.

Log changes from both attribute= and Array.push

That ActiveModel::Dirty doesn't cover Array.push (or any other modify-in-place methods, as I've read extremely recently) for attributes pertaining to, say, postgres arrays is pretty well-established. For example, if an Apple model has an array Apple.seeds, you'll see the following in a Rails console.
johnny = Apple.new()
# => <Apple #blahblahblah>
johnny.seeds
# => [] (assuming [] default)
johnny.seeds << "Oblong"
# => ["Oblong"]
johnny.changed?
# => false
johnny.seeds = []
johnny.seeds += ["Oblong"]
# => ["Oblong"]
johnny.changed?
# => true
So you can use two different ways of changing the array attribute, but Rails only recognizes the one that uses a setter. My question is, is there a way (that won't mangle the Array class) to get push to behave like a setter in the context of an ActiveRecord object, so that johnny.seeds << (x) will reflect in johnny.changes?
(On my end, this is to prevent future developers from using push on array attributes, unwittingly failing to record changes because they were not aware of this limitation.)
This is a problem with any column with a mutable object, not just Array objects.
seeder = Apple.first
seeder.name
=> "Johnny "
seeder.name << " Appleseed"
seeder.changed?
=> false
You're better off leaving a note for future developers, but otherwise you can consider replacing the changed? method
class Apple
alias_method 'old_changed?', 'changed?'
def changed?
return old_changed? if old_changed?
return (seeds.count > 0) if new_record?
return seeds != Apple.find(id).seeds
end
end
However, note that just because changed? comes backtrue, does not assure you that fields with unchanged object_ids will be updated in update_attributes... you may find that they're not. You might need to hire competent rails developers who understand these pitfalls.

Converting Rails model to SQL insert Query?

Is there a way to convert a Rails model into an insert query?
For instance, if I have a model like:
m = Model.new
m.url = "url"
m.header = "header"
How can I get the corresponding SQL query ActiveRecord would generate if I did m.save?
I want to get: "INSERT INTO models(url, header) VALUES('url', 'header')" if possible.
Note: I don't want to actually save the model and get the query back (from log file, etc). I want to get the query IF I chose to save it.
On Rails 4.1, I found the below code snippet working:
record = Post.new(:title => 'Yay', :body => 'This is some insert SQL')
record.class.arel_table.create_insert
.tap { |im| im.insert(record.send(
:arel_attributes_with_values_for_create,
record.attribute_names)) }
.to_sql
Thanks to https://coderwall.com/p/obrxhq/how-to-generate-activerecord-insert-sql
Tested in Rails 3.2.13: I think I got it right this time, it definitely does not persist to the db this time. It also won't fire validations or callbacks so anything they change won't be in the results unless you've called them some other way.
Save this in lib as insert_sqlable.rb and you can then
#in your models or you can send it to ActiveRecord::Base
include InsertSqlable
Then it is model.insert_sql to see it.
#lib/insert_sqlable
module InsertSqlable
def insert_sql
values = arel_attributes_values
primary_key_value = nil
if self.class.primary_key && Hash === values
primary_key_value = values[values.keys.find { |k|
k.name == self.class.primary_key
}]
if !primary_key_value && connection.prefetch_primary_key?(self.class.table_name)
primary_key_value = connection.next_sequence_value(self.class.sequence_name)
values[self.class.arel_table[self.class.primary_key]] = primary_key_value
end
end
im = self.class.arel_table.create_insert
im.into self.class.arel_table
conn = self.class.connection
substitutes = values.sort_by { |arel_attr,_| arel_attr.name }
binds = substitutes.map do |arel_attr, value|
[self.class.columns_hash[arel_attr.name], value]
end
substitutes.each_with_index do |tuple, i|
tuple[1] = conn.substitute_at(binds[i][0], i)
end
if values.empty? # empty insert
im.values = Arel.sql(self.class.connectionconnection.empty_insert_statement_value)
else
im.insert substitutes
end
conn.to_sql(im,binds)
end
end
It turns out the code is in ActiveRecord::Relation and not ActiveRecord::Persistence. The only significant change is the last line which generates the sql instead of performing it.
If you dont want to save the model you call m.destroy when you are done with the object.
You can log the sql query by debugging it like this
Rails.logger.debug "INSERT INTO models(url, header) VALUES(#{m.url}, #{m.header}).inspect
After search a lot over the Internet and forums, I think I found a better solution for your problem: just requires two line of code.
I found a good gem that do exactly what you want, but this gem only works for Rails 3.2 and older. I talked with author and he doesn't want support this gem anymore. So I discovered by myself how to support Rails 4.0 and now I'm maintaining this gem.
Download the "models-to-sql-rails" gem here, supporting Rails 4.0 and older.
With this gem, you can easily do the following. (the examples inside values are just a joke, you will get the correct values when using it in your object).
For objects:
object.to_sql_insert
# INSERT INTO modelName (field1, field2) VALUES ('Wow, amaze gem', 'much doge')
For array of objets:
array_of_objects.to_sql_insert
# INSERT INTO modelName (field1, field2) VALUES ('Awesome doge', "im fucking cop")
# INSERT INTO modelName (field1, field2) VALUES ('much profit', 'much doge')
# (...)
Just see the Github of this project and you'll find how to install and use this wonderful gem.

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

Resources