Papertrail and Carrierwave
Asked Answered
M

6

9

I have a model that use both: Carrierwave for store photos, and PaperTrail for versioning.

I also configured Carrierwave for store diferent files when updates (That's because I want to version the photos) with config.remove_previously_stored_files_after_update = false

The problem is that PaperTrail try to store the whole Ruby Object from the photo (CarrierWave Uploader) instead of simply a string (that would 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 store a simple string in the photo version?

Maintop answered 23/2, 2012 at 23:44 Comment(0)
C
10

You can override item_before_change on your versioned model so you don't call the uploader accesor directly and use write_attribute instead. Alternatively, since you might want to do that for several models, you can monkey-patch the method directly, like this:

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 it's the best solution, but it seems to work.

Calculate answered 25/2, 2012 at 0:47 Comment(3)
I tried putting this code in /config/initializers/papertrail.rb, but it's still adding the full upload object. This is with Rails 4.1.Acidhead
As an alternative, I found it's easiest to just mount on a different attribute, which avoids the issue all together with paper trail, no monkey patching neededFerdinand
This doesn't work in versions of PaperTrail released since January 2015 or so when item_before_change was renamed. Since then, it's been renamed and changed in various ways.Rowell
C
6

Adding @beardedd's comment as an answer because I think this is a better way to handle the problem.

Name your database columns something like picture_filename and then in your model mount the uploader 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 revisions under picture_filename.

Cervicitis answered 25/2, 2015 at 7:52 Comment(2)
Is this really a good way? Will there only change the file name when replacing an uploaded file, or will there also be a lot of meta information like image width, height, etc.? This would be lost when only tracking filename, and when restoring an old version the infos would be incorrect.Jijib
I made a quick test: I compared the dumps of two full carrierwave objects (one with an uploaded file avatar.jpg and one with nayeli.jpg). It seems that really only the timestamps and file names are different, except one line (#237) where the file's name of the previously uploaded file is stored (url.jpg, which was the file before avatar.jpg). As far as I can see, your solution should basically work, while line 237 would be wrong when restoring revisions as it isn't tracked correctly. See results here: github.com/jmuheim/base/commit/…Jijib
A
2

Here is a bit 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.

The second method override is required if you use optional object_changes column for versions. It works in a bit strange way for carrierwave + fog if you override filename in uploader, old value will be from cloud and new one from local filename, but in my case it's ok.

Also I have not checked if it works correctly when you restore 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
Anomalism answered 7/4, 2015 at 12:19 Comment(3)
This doesn't prevent a previous file to be overwritten with a new file of the same name.Jijib
Restoring an old version DOESN'T work on model.reload! If you do a user.previous_version.save, user will still point to the latest file, and user.reload also won't fix this. You have to do a manual user = User.find(user.id), then it will point to the previous file. Is there a fix for this? Typically between requests this isn't a problem, but it's still a bug.Jijib
This doesn't work in versions of PaperTrail released since January 2015 or so when item_before_change and changes_for_paper_trail were renamed.Rowell
I
1

This is what actually functions for me, put this on 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 to work on S3 + heroku

For uploaders to keep old files from updated or deleted records do this in the uploader

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

Then make up some routine to clear old files from time to time, good luck

Immediately answered 20/12, 2016 at 20:55 Comment(2)
model.send("#{k}").recreate_versions! does not seem to reinstate the file object for me :(Submaxillary
Actually, I think it is working. I just needed to call save on the model where I was testing this out. Thanks!Submaxillary
J
0

I want to add to the previous answers the following:

It can happen that you upload different files with the same name, and this may overwrite your previous file, so you won't be able to restore the old one.

You may use a timestamp in file names or create random and unique filenames for all versioned files.

Update

This doesn't seem to work in all edge cases for me, when assigning more than a single file to the same object within a single request request.

I'm using this right now:

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

This seems to work, as the @cache_id is generated for each and every upload again (which isn't the case as it seems for the ideas provided in the links above).

Jijib answered 11/4, 2015 at 21:41 Comment(0)
O
0

@Sjors Provoost

We also need to override pt_recordable_object method in 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
Overcapitalize answered 11/2, 2016 at 8:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.