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)