I have a model with a start_date and end_date.
When a version is created, if the old end_date is after the new start_date then the end_date in the paper_trail version should be set to start_date of the updated record
In sudo code this might look like...
(doesn't work and I'd prefer to not modify the version after creating it)
record.save
version = paper_trail.previous_version
if (version.end_date > record.start_date)
version.end_date = record.start_date
version.save
end
The sequence would look like this...
Create record
record: start_date=2021, end_date=nil
Update record
record: start_date=2022, end_date=2023
version1: start_date=2021, end_date=2022
Update record
record: start_date=2024, end_date=2037
version2: start_date=2022, end_date=2024
version1: start_date=2021, end_date=2022
rails 6.1, paper_trail 12
I would suggest keeping the Papertrail behavior as-is, meaning it would keep track of value changes for each field without any additional custom logic that would overwrite the values, thus breaking interface for rolling back to a version etc.
Instead, I would use meta to store and retrieve any custom data you need for the model https://github.com/paper-trail-gem/paper_trail#4c-storing-metadata
Related
I was wondering if there is anything wrong about updating the updated_at attribute of a record through a db trigger in terms of fragment caching (i.e. the partials dont get re-cached / the old cache keys do not disappear from memory).
additional info I'm using a trigger due to using the upsert gem which does not modify the updated_at attribute unless explicitly told to do so ( which I do not want to do ); also, due to the same gem I cannot use an active::record after_save or before_save on the model.
Please let me know if there any other information I should provide to add some clarity to my question.
There isn't nothing wrong, but if you need to do so you can simply use record.touch in a method so your code will be more clean and app will be more maintainable.
One more way to achive that is to use on_duplicate attribute of the Rails upsert_all method (without any gems). Check the documentation, pseudo code example:
YourModel.upsert_all(
array_of_data,
unique_by: %i[field_1 field_2],
on_duplicate: Arel.sql('updated_at = current_timestamp')
)
If you have other fields to update, don't forget to add them into Arel.sql:
on_duplicate: Arel.sql('updated_at = current_timestamp, field_to_update = excluded.field_to_update')
I'm using paper_trail 3.0.8 on a Rails 3.2 app and I've got a model called 'levels' and I keep versions of these levels. Each level has a from_date and a cost relating to it. Whenever someone changes the date a new version is created.
I allow people to remove old versions if they want and this works well. I would like the ability to modify an old paper_trail version and save it without creating a new version.
class Level < ActiveRecord::Base
has_paper_trail :only => [:from_date],
:if => Proc.new { |l|
l.versions.count == 0 || l.versions.first.item != nil && (l.versions.first.item.from_date.nil? || l.from_date > l.versions.first.item.from_date)
}
<snip code>
end
If I do the following it only updates the current level and not the version
level = Level.find 1
version=level.versions[1].reify
version.cost_cents = 1000
version.save
Is there anyway to update the cost_cents for an old version?
Also is there a way to update the from_date of an old version without creating a new version on the save?
Is there anyway to update the cost_cents for an old version?
Yes, but the only way I know is a bit awkward.
PaperTrail::Version is a normal ActiveRecord object, so it's easy to work with in that sense, but the data is serialized (in YAML, by default) so you'll have to de-serialize, make your change, and re-serialize.
v = PaperTrail::Version.last
hash = YAML.load(v.object)
hash[:my_attribute] = "my new value"
v.object = YAML.dump(hash)
v.save
There may be a better way to do this with ActiveRecord's automatic-serialization features (like ActiveRecord::AttributeMethods::Serialization).
PS: I see you were trying to use reify, which returns an instance of your model, not an instance of PaperTrail::Version.
I have a spreadsheet of items which I convert to CSV and import using a custom import script into my Rails based application.
The spreadsheet contains a row for each record but some rows hold different versions of previous rows.
When importing the CSV I currently mark the second row using a "past_version" field but I am now thinking that implementing a full versioning gem would be a much nicer way of going about it.
I have been reading through the docs for PaperTrail and it looks perfect for what I am after, however, I need the versions of some rows to be created as part of my import script. Can this be done with PaperTrail?
Basically I need to start an import, say record 1, 2, 3, 4, 5 are added normally, then record 6 is actually a newer version of record 2 and so I now need to manually create a PaperTrail version.
Is this possible?
#Flori's touch_with_version approach worked, but paper_trail deprecated this method when they made touch a recordable event.
On the latest version of paper_trail, you can just do:
model.paper_trail.save_with_version
If you are on an older version, and this is not available, you can use the record_update method (this is what save_with_version uses internally):
model.paper_trail.record_update(force: true, in_after_callback: false)
There is no way to customize the event... you can do create, restore, update and touch but can't do something like record it as 'manual' at least not without a lot of monkey patching or something.
Even doing this is a little treacherous, since you're calling some internals that could change in future versions. It has some required params, also, that have changed from version to version.
In my case, I am using paper_trail and paranoia together and wanted to record an explicit version on restore which seems to bypass paper_trail unless you do something like the above.
In case anyone stumble upon this issue: It's possible! You can call touch_with_version on any PaperTrail model like this: mymodel.paper_trail.touch_with_version. It will create a version for you.
For PaperTrail 4.0.0 and Rails 4.2.0
I had to manually create my own version so that I could use the update_column method (which would otherwise not trigger PaperTrail.
#instance method in model to generate new version and create object_changes
def generate_version!(object_attrs, changed_attrs)
object_changes_attrs = {}
changed_attrs.each do |k, v|
object_changes_attrs[k] = v
end
object_value = self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs)
object_changes_value = self.class.paper_trail_version_class.object_col_is_json? ? object_changes_attrs : PaperTrail.serializer.dump(object_changes_attrs)
data = {
event: 'update', # or any custom name you want
whodunnit: PaperTrail.whodunnit,
object: object_value,
object_changes: object_changes_value
}
send(self.class.versions_association_name).create! merge_metadata(data)
end
Then in your model you can call it wherever you want passing in: (1) a hash of the current object attributes (before the update); and (2) a hash of the attributes and changes made
#some method where you do not otherwise trigger PaperTrail
def my_method
old_updated_at = self.updated_at
new_updated_at = DateTime.now.utc
object_attrs = self.attributes
self.update_columns(prioritized: true, updated_at: new_updated_at)
self.generate_version!(object_attrs, { 'prioritized': [false, true], 'updated_at': [old_updated_at, new_updated_at] })
end
I'm building an activerecord to model a conversation tree, using an array column type to represent the materialized path of the record's place in that tree, using postgres 9.1, rails 4.0, and the pg gem.
What I really want to do is access currval('conversations_id_seq') when I create a new conversation object, so that I can pass in [grandparent_id, parent_id ... current_id] as the array to the object initializer. That way I can specify that this column is not null as a database constraint, and in the event of a parentless conversation, have it still default to [current_id].
The problem I have is getting access to the model's id value before I save it the first time. I could always relax the not null constraint and add an after_create hook, but that feels kludgy. I'm hopeful that there's a way I can grab the value that's getting pushed into #id inside the initializer, before the first save to the database.
EDIT to clarify for the bounty: In an ideal world, there would be a special token I could pass in to the object's create method: Conversation.create(reply_chain: [:lastval]), where the gem took that to mean lastval() in the generated SQL.
something like:
def before_create
self.id=Conversation.connection.execute("SELECT nextval('conversations_id_seq')")
self.path = [... , self.id];
true
end
or use a before insert/update trigger to maintain the path.
You could alias the attribute if you don't need the column in the database.
alias_attribute :current_id, :id
Or you could query for the id when you need it.
def self.last_val
ActiveRecord::Base.connection.execute("SELECT lastval('conversations_id_seq')")
end
def self.next_val
ActiveRecord::Base.connection.execute("SELECT nextval('conversations_id_seq')")
end
Conversation.create(reply_chain: Conversation.next_val)
Using after_save isn't the ugliest of code either.
How do I check if the data is changed when I edit a record?
So before update
game.player=1
after update / edit
game.player=2
for example
how to track changes (check if changed) and do something in ruby on rails, when the data is changed.
Have a look at ActiveModel::Dirty
You can check if a model has been changed by doing:
game.changed?
Or an individual field like:
game.player_changed?
Both return a boolean value.
All changes are also kept in a hash. This comes in handy if you want to use the values.
game.changes #=> {:player => [1,2]}