Rails4 and Paperclip 4.0.2 callback turns as infinite loop - ruby-on-rails

I set paperclip to My "Document" model, with a fairly standard config. It works well, but I want to separate the addtitional styles file generation inside a background job (using Resque).
(I would like to stop the creation of the :orginal style to to assign a file manually, but it doesn't seem to be possible)
So, to extract styles inside a background job, I first stop paperclip styles processors.
Then I call the reprocess! inside an after_save callback to generate them.
Doing this put the update action inside an infinite loop, and it is exactly when I call the reprocess!
Here is my Document model (simplified for understanding purpose)
class Document < ActiveRecord::Base
has_attached_file :attachment
before_post_process :stop_process
after_save :process_styles #same result with after_update (since user can only add attachment when updating profile)
# Kill all paperclip styles
#still generate the :orginal style. Block that too to copy it manually from remote folder would be nice
def stop_process
false
end
#call for the generation of the additional styles (:medium, :thumb)
def process_styles
Profile.processDocumentJob(id)
end
#will be a background job
def self.processDocumentJob(id)
document = Document.find(id)
document.attachment.reprocess!
document.save(validate: false)
end
end
Despite my document.save(validate: false), the process loop during the updating.
I tried after_update callback, tweak my code and conditions, with no success.
Thank you in advance for your precious help

I know this is an old question, but happened to me today and solved this way....a bit hacky btw... I'm not proud of my solution. This works in Rails 5.
Added a virtual attribute to my model "skip_callbacks"
Added a method to check if the attribute is true "should_skip_callbacks?"
In my callback added "unless: :should_skip_callbacks?" condition
Before the paperclip reprocess, set the virtual attribute "skip_callbacks" to true
Here's a simplified version of my model:
class Organization < ApplicationRecord
attr_accessor :skip_callbacks
has_attached_file :logo, styles: { header: "380x160>" }
validates_attachment :logo, content_type: {content_type: ['image/png','image/jpg','image/gif']}
after_save :reprocess_logo, unless: :should_skip_callbacks?
def should_skip_callbacks?
self.skip_callbacks
end
def reprocess_logo
self.skip_callbacks = true
self.logo.reprocess!
end
end

Related

Rails Active Storage - Keep Existing Files / Uploads?

I have a Rails model with:
has_many_attached :files
When uploading via Active Storage by default if you upload new files it deletes all the existing uploads and replaces them with the new ones.
I have a controller hack from this which is less than desirable for many reasons:
What is the correct way to update images with has_many_attached in Rails 6
Is there a way to configure Active Storage to keep the existing ones?
Looks like there is a configuration that does exactly that
config.active_storage.replace_on_assign_to_many = false
Unfortunately it is deprecated according to current rails source code and it will be removed in Rails 7.1
config.active_storage.replace_on_assign_to_many is deprecated and will be removed in Rails 7.1. Make sure that your code works well with config.active_storage.replace_on_assign_to_many set to true before upgrading.
To append new attachables to the Active Storage association, prefer using attach.
Using association setter would result in purging the existing attached attachments and replacing them with new ones.
It looks like explicite usage of attach will be the only way forward.
So one way is to set everything in the controller:
def update
...
if model.update(model_params)
model.files.attach(params[:model][:files]) if params.dig(:model, :files).present?
else
...
end
end
If you don't like to have this code in controller. You can for example override default setter for the model eg like this:
class Model < ApplicationModel
has_many_attached :files
def files=(attachables)
files.attach(attachables)
end
end
Not sure if I'd suggest this solution. I'd prefer to add new method just for appending files:
class Model < ApplicationModel
has_many_attached :files
def append_files=(attachables)
files.attach(attachables)
end
end
and in your form use
<%= f.file_field :append_files %>
It might need also a reader in the model and probably a better name, but it should demonstrate the concept.
The solution suggested for overwriting the writer by #edariedl DOES NOT WORK because it causes a stack level too deep
1st solution
Based on ActiveStorage source code at this line
You can override the writer for the has_many_attached like so:
class Model < ApplicationModel
has_many_attached :files
def files=(attachables)
attachables = Array(attachables).compact_blank
if attachables.any?
attachment_changes["files"] =
ActiveStorage::Attached::Changes::CreateMany.new("files", self, files.blobs + attachables)
end
end
end
Refactor / 2nd solution
You can create a model concern that will encapsulate all this logic and make it a bit more dynamic, by allowing you to specify the has_many_attached fields for which you want the old behaviour, while still maintaining the new behaviour for newer has_many_attached fields, should you add any after you enable the new behaviour.
in app/models/concerns/append_to_has_many_attached.rb
module AppendToHasManyAttached
def self.[](fields)
Module.new do
extend ActiveSupport::Concern
fields = Array(fields).compact_blank # will always return an array ( worst case is an empty array)
fields.each do |field|
field = field.to_s # We need the string version
define_method :"#{field}=" do |attachables|
attachables = Array(attachables).compact_blank
if attachables.any?
attachment_changes[field] =
ActiveStorage::Attached::Changes::CreateMany.new(field, self, public_send(field).public_send(:blobs) + attachables)
end
end
end
end
end
end
and in your model :
class Model < ApplicationModel
include AppendToHasManyAttached['files'] # you can include it before or after, order does not matter, explanation below
has_many_attached :files
end
NOTE: It does not matter if you prepend or include the module because the methods generated by ActiveStorage are added inside this generated module which is called very early when you inherit from ActiveRecord::Base here
==> So your writer will always take precedence.
Alternative/Last solution:
If you want something even more dynamic and robust, you can still create a model concern, but instead you loop inside the attachment_reflections of your model like so :
reflection_names = Model.reflect_on_all_attachments.filter { _1.macro == :has_many_attached }.map { _1.name.to_s } # we filter to exclude `has_one_attached` fields
# => returns ['files']
reflection_names.each do |name|
define_method :"#{name}=" do |attachables|
# ....
end
end
However I believe for this to work, you need to include this module after all the calls to your has_many_attached otherwise it won't work because the reflections array won't be fully populated ( each call to has_many_attached appends to that array)

ActiveStorage hook after analyze

I have pictures attached to a model. Those pictures are analyzed and the EXIF data is saved as metadata on the ActiveStorage::Blob.
class Foo < ApplicationRecord
has_one_attached :picture
end
There is an attribute on this model that I use for sorting the instances called order_date. This attribute has to be updated with the EXIF time after the blob got analyzed.
Using paperclip, a before_commit callback method was sufficient. With ActiveStorage, I also tried before_save and after_touch but both are not working.
How can I run code right after the ActiveStorage::AnalyzeJob has run successfully?
(I want to avoid monkey-patching ActiveStorage::AnalyzeJob, because it is also performed for other attachments.)
Thanks very much for your help!
I couldn't find anything official. I ended up overriding the analyze job since it is very simple anyways. It looks like below.
#/app/jobs/active_storage/analyze_job.rb
class ActiveStorage::AnalyzeJob < ActiveStorage::BaseJob
def perform(blob)
blob.analyze
blob.attachments.includes(:record).each do |attachment|
if attachment.record_type == 'Content'
record = attachment.record
record.set_file_info
record.save!
end
end
end
end

How can I programmatically copy ActiveModel validators from one model to another?

I'm writing a library that will require programmatically copying validations from one model to another, but I'm stumped on how to pull this off.
I have a model that is an ActiveModel::Model with some validation:
class User < ActiveRecord::Base
validates :name, presence: true
end
And another model that I'd like to have the same validations:
class UserForm
include ActiveModel::Model
attr_accessor :name
end
Now I'd like to give UserForm the same validations as User, and without modifying User. Copying the validators over doesn't work, because ActiveModel::Validations hooks into callbacks during the validation check:
UserForm._validators = User._validators
UserForm.new.valid?
# => true # We wanted to see `false` here, but no validations
# are actually running because the :validate callback
# is empty.
Unfortunately, there doesn't seem to be an easy way that I can see to programmatically give one model another's validation callbacks and still have it work. I think my best bet is if I can ask Rails to regenerate the validation callbacks based on the validators that are present at a given moment in time.
Is that possible? If not, is there a better way to do this?
Checking into the code of activerecord/lib/active_record/validations/presence.rb reveals how this can be achieved:
# File activerecord/lib/active_record/validations/presence.rb, line 60
def validates_presence_of(*attr_names)
validates_with PresenceValidator, _merge_attributes(attr_names)
end
So I guess I would try to hook into validates_with with an alias_method
alias_method :orig_validates_with :validates_with
Now you have a chance to get ahold of the values passed, so you can store them somewhere and retrieve them when you need to recreate the validation on UserForm
alias_method :orig_validates_with, :validates_with
def validates_with(*args)
# save the stuff you need, so you can recreate this method call on UserForm
orig_validates_with(*args)
end
Then you should be able to just call UserForm.validates_with(*saved_attrs). Sorry this is not something you can just copy/paste, but this should get you started. HTH

How to make localized Paperclip attachments with Globalize3?

I have a project using Paperclip gem for attachments and Globalize3 for attribute translation. Records need to have a different attachment for each locale.
I though about moving Paperclip attributes to translation table, and that might work, but I don't think that would work when Paperclip needs to delete attachments.
What's the best way to achieve something like that?
UPDATE: to be clear, I want this because my client wants to upload different images for each locale.
Unfortunately I didn't find a way to do this using Globalize3. In theory, I could have added a separate model for image and add image_id to list of translated columns (to have something like MainModel -> Translation -> Image), but it seems that Globalize has some migration issues with non-string columns.
Instead of using Globalize3, I did this with a separate Image model with locale attribute and main model which accepts nested attributes for it. Something along the lines of:
class MainModel < ActiveRecord::Base
has_many :main_model_images
accepts_nested_attributes_for :main_model_images
# return image for locale or any other as a fallback
def localized_image(locale)
promo_box_images.where(:locale => locale).first || promo_box_images.first
end
end
class MainModelImage < ActiveRecord::Base
belongs_to :main_model
has_attached_file :image
validates :locale,
:presence => true,
:uniqueness => { :scope => :main_model_id }
end
Tricky part was getting form to accept nested attributes only for one image, instead of all images in has_many relation.
=f.fields_for :main_model_images, #main_model.image_for_locale(I18n.locale) do |f_image|
=f_image.hidden_field :locale
=f_image.label :image
You could also try the paperclip-globalize3 gem, it should handle the case you describe. https://github.com/emjot/paperclip-globalize3
Ok since you asked me to share my solution to this problem even though I am using Carrierwave as a library for uploading here is it:
Ok so I would have a model setup like this:
class MyModel < ActiveRecord::Base
# ...
translates :attr_one, :attr_two, :uploaded_file
Now what I need for CarrierWave to work is place to attach the uploader to and that can be done on the Translation model
Translation.mount_uploader :uploaded_file, FileUploader
end
Now for your question about deleting, I think though I haven't needed to do it but it should work as the README says it should but on the translation model. https://github.com/jnicklas/carrierwave#removing-uploaded-files
MyModel.first.translation.remove_uploaded_file!
I haven't taken a look at paperclip for a good 2 years and if this is not applicable knowledge I suggest you try out carrierwave.

Rails carrierwave mounting on condition

I need to mount picture uploader after some verification function.
But if I call as usual mounting uploader in model:
mount_uploader :content, ContentUploader
carrierwave first download content, and then Rails start verification of model.
Specifically, I don't want to load big files at all! I want to check http header Content-length and Content-type and then, if it's OK, mount uploader.
Maybe something like that:
if condition
mount_uploader :content, ContentUploader
end
How can I do it?
P.S. Rails version 3.2.12
This is not the way to go if you just want avoid loading big files! That said, one can have conditional mount overriding content=.
As CarrierWave v1.1.0 there is still no conditional mount. But note that mount_uploader first includes a module in the class and then overrides the original content= to call the method content= defined in the included module. So, a workaround is just to redefine the accessors after you have called mount_uploader:
class YourModel < ActiveRecord::Base
mount_uploader :content, ContentUploader
def content=(arg)
if condition
super
else
# original behavior
write_attribute(:content, arg)
end
end
def content
# some logic here
end
end

Resources