问题
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