Papertrail and Carrierwave

余生长醉 提交于 2019-12-04 18:49:06

问题


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?


回答1:


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.




回答2:


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.




回答3:


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



回答4:


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




回答5:


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).




回答6:


@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


来源:https://stackoverflow.com/questions/9423279/papertrail-and-carrierwave

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!