Papertrail and carrierwave

I have a model that uses both: Carrierwave for storing photos and PaperTrail for versions.

I also configured Carrierwave to store different files when updating (this is because I want the version of the photo) with config.remove_previously_stored_files_after_update = false

The problem is that PaperTrail trying to save the entire Ruby object with a photo (CarrierWave Uploader) instead of a simple line (this will be its url)

(version table, column object)

 --- first_name: Foo last_name: Bar photo: !ruby/object:PhotoUploader model: !ruby/object:Bla attributes: id: 2 first_name: Foo1 segundo_nombre: 'Bar1' ........ 

How can I fix this to keep a simple string in the photo version?

+9
ruby-on-rails ruby-on-rails-3 carrierwave paper-trail-gem
source share
6 answers

You can override item_before_change on your model with the version so that you do not directly call the loader user and use write_attribute . Alternatively, since you can do this for multiple models, you can defuse the method directly, for example:

 module PaperTrail module Model module InstanceMethods private def item_before_change previous = self.dup # `dup` clears timestamps so we add them back. all_timestamp_attributes.each do |column| previous[column] = send(column) if respond_to?(column) && !send(column).nil? end previous.tap do |prev| prev.id = id changed_attributes.each do |attr, before| if defined?(CarrierWave::Uploader::Base) && before.is_a?(CarrierWave::Uploader::Base) prev.send(:write_attribute, attr, before.url && File.basename(before.url)) else prev[attr] = before end end end end end end end 

Not sure if this is the best solution, but it seems to work.

+10
source share

Adding a @beardedd comment as an answer, because I believe this is the best way to handle this problem.

Name your database columns something like picture_filename , and then install the loader in your model using:

class User < ActiveRecord::Base has_paper_trail mount_uploader :picture, PictureUploader, mount_on: :picture_filename end

You still use the user.picture.url attribute to access your model, but PaperTrail will store versions under picture_filename .

+6
source share

Here is a slightly updated version of monkeypatch from @rabusmar, I use it for rails 4.2.0 and paper_trail 4.0.0.beta2, in /config/initializers/paper_trail.rb .

A second method override is required if an optional object_changes column is used for versions. It works a little strange for carrier + fog, if you redefine filename in the bootloader, the old value will be the cloud and the new one from the local file name, but in my case this is normal.

Also, I did not check if it works when restoring an old version.

 module PaperTrail module Model module InstanceMethods private # override to keep only basename for carrierwave attributes in object hash def item_before_change previous = self.dup # `dup` clears timestamps so we add them back. all_timestamp_attributes.each do |column| if self.class.column_names.include?(column.to_s) and not send("#{column}_was").nil? previous[column] = send("#{column}_was") end end enums = previous.respond_to?(:defined_enums) ? previous.defined_enums : {} previous.tap do |prev| prev.id = id # `dup` clears the `id` so we add that back changed_attributes.select { |k,v| self.class.column_names.include?(k) }.each do |attr, before| if defined?(CarrierWave::Uploader::Base) && before.is_a?(CarrierWave::Uploader::Base) prev.send(:write_attribute, attr, before.url && File.basename(before.url)) else before = enums[attr][before] if enums[attr] prev[attr] = before end end end end # override to keep only basename for carrierwave attributes in object_changes hash def changes_for_paper_trail _changes = changes.delete_if { |k,v| !notably_changed.include?(k) } if PaperTrail.serialized_attributes? self.class.serialize_attribute_changes(_changes) end if defined?(CarrierWave::Uploader::Base) Hash[ _changes.to_hash.map do |k, values| [k, values.map { |value| value.is_a?(CarrierWave::Uploader::Base) ? value.url && File.basename(value.url) : value }] end ] else _changes.to_hash end end end end end 
+2
source share

This is what actually works for me, put this in config / initializers / paper_trail / .rb

 module PaperTrail module Reifier class << self def reify_attributes(model, version, attrs) enums = model.class.respond_to?(:defined_enums) ? model.class.defined_enums : {} AttributeSerializers::ObjectAttribute.new(model.class).deserialize(attrs) attrs.each do |k, v| is_enum_without_type_caster = ::ActiveRecord::VERSION::MAJOR < 5 && enums.key?(k) if model.send("#{k}").is_a?(CarrierWave::Uploader::Base) if v.present? model.send("remote_#{k}_url=", v["#{k}"][:url]) model.send("#{k}").recreate_versions! else model.send("remove_#{k}!") end else if model.has_attribute?(k) && !is_enum_without_type_caster model[k.to_sym] = v elsif model.respond_to?("#{k}=") model.send("#{k}=", v) elsif version.logger version.logger.warn( "Attribute #{k} does not exist on #{version.item_type} (Version id: #{version.id})." ) end end end end end end end 

This overrides the reify method for working with S3 + heroku

For downloaders, so that old files from updated or deleted records do this in the bootloader

 configure do |config| config.remove_previously_stored_files_after_update = false end def remove! true end 

Then create some routine to delete old files from time to time, good luck

+1
source share

I want to add the following to the previous answers:

It may happen that you upload different files with the same name, and this may overwrite the previous file so that you cannot restore the old one.

You can use a timestamp in file names or create random and unique file names for all file versions .

Update

This does not seem to work in all cases for me, when assigning more than one file to the same object within the same request request.

I am using this right now:

 def filename [@cache_id, original_filename].join('-') if original_filename.present? end 

This seems to work, as @cache_id is created for every download again (which is not the way it seems for the ideas given in the links above).

0
source share

@Sjors Provoost

We also need to override the pt_recordable_object method in the PaperTrail :: Model :: InstanceMethods module

  def pt_recordable_object attr = attributes_before_change object_attrs = object_attrs_for_paper_trail(attr) hash = Hash[ object_attrs.to_hash.map do |k, value| [k, value.is_a?(CarrierWave::Uploader::Base) ? value.url && File.basename(value.url) : value ] end ] if self.class.paper_trail_version_class.object_col_is_json? hash else PaperTrail.serializer.dump(hash) end end 
0
source share

All Articles