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)
Related
Is there a callback for active storage files on a model
after_update or after_save is getting called when a field on the model is changed. However when you update (or rather upload a new file) no callback seems to be called?
context:
class Person < ApplicationRecord
#name :string
has_one_attached :id_document
after_update :call_some_service
def call_some_service
#do something
end
end
When a new id_document is uploaded after_update is not called however when the name of the person is changed the after_update callback is executed
For now, it seems like there is no callback for this case.
What you could do is create a model to handle the creation of an active storage attachment which is what is created when you attach a file to your person model.
So create a new model
class ActiveStorageAttachment < ActiveRecord::Base
after_update :after_update
private
def after_update
if record_type == 'Person'
record.do_something
end
end
end
You normally have created the model table already in your database so no need for a migration, just create this model
Erm i would just comment but since this is not possible without rep..
Uelb's answer works but you need to fix the error in comments and add it as an initializer instead of model. Eg:
require 'active_storage/attachment'
class ActiveStorage::Attachment
before_save :do_something
def do_something
puts 'yeah!'
end
end
In my case tracking attachment timestamp worked
class Person < ApplicationRecord
has_one_attached :id_document
after_save do
if id_document.attached? && (Time.now - id_document.attachment.created_at)<5
Rails.logger.info "id_document change detected"
end
end
end
The answer from #Uleb got me 90% of the way, but for completion sake I will post my final solution.
The issue I had was that I was not able to monkey patch the class (not sure why, even requiring the class as per #user10692737 did not help)
So I copied the source code (https://github.com/rails/rails/blob/fc5dd0b85189811062c85520fd70de8389b55aeb/activestorage/app/models/active_storage/attachment.rb#L20)
and modified it to include the callback
require "active_support/core_ext/module/delegation"
# Attachments associate records with blobs. Usually that's a one record-many blobs relationship,
# but it is possible to associate many different records with the same blob. If you're doing that,
# you'll want to declare with <tt>has_one/many_attached :thingy, dependent: false</tt>, so that destroying
# any one record won't destroy the blob as well. (Then you'll need to do your own garbage collecting, though).
class ActiveStorage::Attachment < ActiveRecord::Base
self.table_name = "active_storage_attachments"
belongs_to :record, polymorphic: true, touch: true
belongs_to :blob, class_name: "ActiveStorage::Blob"
delegate_missing_to :blob
#CUSTOMIZED AT THE END:
after_create_commit :analyze_blob_later, :identify_blob, :do_something
# Synchronously purges the blob (deletes it from the configured service) and destroys the attachment.
def purge
blob.purge
destroy
end
# Destroys the attachment and asynchronously purges the blob (deletes it from the configured service).
def purge_later
blob.purge_later
destroy
end
private
def identify_blob
blob.identify
end
def analyze_blob_later
blob.analyze_later unless blob.analyzed?
end
#CUSTOMIZED:
def do_something
end
end
Not sure its the best method, and will update if I find a better solution
None of these really hit the nail on the head, but you can achieve what you were looking for by following this blog post https://redgreen.no/2021/01/25/active-storage-callbacks.html
I was able to modify the code there to work on attachments instead of blobs like this
Rails.configuration.to_prepare do
module ActiveStorage::Attachment::Callbacks
# Gives us some convenient shortcuts, like `prepended`
extend ActiveSupport::Concern
# When prepended into a class, define our callback
prepended do
after_commit :attachment_changed, on: %i[create update]
end
# callback method
def attachment_changed
record.after_attachment_update(self) if record.respond_to? :after_attachment_update
end
end
# After defining the module, call on ActiveStorage::Blob to prepend it in.
ActiveStorage::Attachment.prepend ActiveStorage::Attachment::Callbacks
end
What I do is add a callback on my record:
after_touch :check_after_touch_data
This gets called if an ActiveStorage object is added, edited or deleted. I use this callback to check if something changed.
I've just started to learn Ruby and Ruby on Rails, and this is actually the first time I really have to ask a question on SO, is really making me mad.
I'm programming a REST api where I need to receive an image url and store it in my db.
For this, I've done a model called ImageSet that uses carrierwave to store the uploaded images like this:
class ImageSet < ActiveRecord::Base
has_one :template
mount_uploader :icon1, Icon1Uploader
mount_uploader :icon2, Icon2Uploader
def icon1=(url)
super(url)
self.remote_icon1_url = url
end
def icon2=(url)
super(url)
self.remote_icon2_url = url
end
end
This icon1 and icon2 are both received as urls, hence the setter override, and they can't be null.
My uploader classes are creating some versions with a whitelist of extensions and a override of full_name.
Then, I have this template class that receives nested attributes for ImageSet.
class Template < ActiveRecord::Base
belongs_to :image_set
accepts_nested_attributes_for :image_set
(other stuff)
def image_set
super || build_image_set
end
end
This model has a image_set_id that can't be null.
Considering a simple request, like a post with a json:
{
"template":
{
"image_set_attributes":
{
"icon1": "http....",
"icon2": "http...."
}
}
}
It gives always : ImageSet can't be blank.
I can access temp.image_set from the console,if temp is a Template, and I can set values there too, like, temp.image_set.icon = 'http...' but I can't seem to figure out why is it breaking there.
It should create the image_set, set its attributes a save it for the template class, which would assign its id to the respective column in its own model-
My controller is doing:
(...)
def create
#template = Template.create(params)
if #template
render status: 200
else
render status: 422
end
end
private
def params
params.require(:template).permit(image_set_attributes: [:id, :icon1, :icon2])
end
(...)
Hope you can give me tip on this one.
Thanks!
accepts_nested_attributes doesn't work as expected with belongs_to.
Don't use accepts_nested_attributes_for with belongs_to
Does accepts_nested_attributes_for work with belongs_to?
It can be tricked into working in certain circumstances, but you're better off changing your application elsewhere to do things the "rails way."
Templates validate_presence_of :image_set, right? If so, the problem is that this means ImageSets must always be created before their Templates, but accepts_nested_attributes thinks Template is the parent, and is trying to save the parent first.
The simplest thing you can do is switch the relationship, so Template has_one :image_set, and ImageSet belongs_to :template. Otherwise you're going to have to write some rather odd controller logic to deal with the params as expected.
We had the following class to process SOAP responses from external API(s) which worked fine in ruby 1.8.7, but it is looking for a table with these columns post migration (which has never been there) to ruby 1.9.2/rails 3.1, How do I handle this migratation?
class SoapResponse < ActiveRecord::Base
def self.columns
#columns ||= [];
end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(
name.to_s, default, sql_type.to_s, null)
end
def save(validate = true)
validate ? valid? : true
end
column :soap_payload, :text
serialize :soap_payload
end
You don't (have any migrations for it).
You don't have migrations and you don't inherit from ActiveRecord::Base as that is the database ORM component.
If you use a generator to create the model use --skip-migration to avoid generating the database migration file.
You can still get validations and conversions though, e.g.
class SoapResponse
include ActiveModel::Validations
include ActiveModel::Conversion
If you want some setup data (i.e. Constants, given there is no db! ) you can just define them here (Constants start with uppercase).
I guess you want the serialization capability of ActiveRecord::Base. That seems to be the only thing this class is using it for. If so, try calling this in your class definition:
self.abstract_class = true
Or you could try using ActiveModel::Serialization.
The pattern in your code looks like what's suggested in this answer for table-less AR models in Rails 2.
I'm working with a nested form that encompasses a total of 7 models, each with different validations. When simply editing the form, the validations run and display fine, and data is saved properly. However, I need to have different validations run depending on who is submitting the form (ie, admins can skip some otherwise required fields).
I thought I could get certain validations to be skipped by using attr_accessible :editing_user in my models, then set this within the controller.
class ModelExample < ActiveRecord::Base
attr_accessible :editing_user
validates_presence_of :email, :unless => "editing_user == 'admin'"
end
class ModelExamplesController < ActionController::Base
def create
#model_example = ModelExample.new(params[:model_example])
#model_example.editing_user = 'admin'
#model_example.save
end
end
I used this basic structure within the nested models, checking to see if I could save properly. This is where the weird behavior starts. For some reason, it looks like ActiveRecord is trying to save nested models multiple times, running validations each time. What makes this odd is I call #model_example.save, which should just return false if it fails. But, the first validation goes through (since editing_user is set), but later validations fail and raise exceptions, so the normal .save methods ends up raising an exception instead of returning.
Does anyone know how to either avoid having ActiveRecord do all extra validations and saves, or how to persist editing_user across those duplicate actions?
ha! just did this yeasterday, well, almost same use case anyway (persist the user). Here is how i solved it (with all credit to my buddy Jason Dew that I copied from):
class User < ActiveRecord::Base
module ClassMethods
attr_accessor :current
end
extend ClassMethods
end
This code block adds a singleton accessor :current to the User class methods, and can be called as User.current. nicer looking that a method called self.currrent
then in the app controller
before_filter :require_user #=> which in this case goes off and sets the current_user var
before_filter {|c| User.current = current_user}
which passes the app controller to the block and sets the User.current var.
Then in any other model
class MyClass < ActiveRecord::Base
def log
"This was done by #{User.current}"
end
end
I want to create a Rails (2.1 and 2.2) model with ActiveRecord validations, but without a database table. What is the most widely used approach? I've found some plugins that claim to offer this functionality, but many of them don't appear to be widely used or maintained. What does the community recommend I do? Right now I am leaning toward coming up with my own solution based on this blog post.
There is a better way to do this in Rails 3: http://railscasts.com/episodes/219-active-model
This is an approach I have used in the past:
In app/models/tableless.rb
class Tableless < ActiveRecord::Base
def self.columns
#columns ||= [];
end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default,
sql_type.to_s, null)
end
# Override the save method to prevent exceptions.
def save(validate = true)
validate ? valid? : true
end
end
In app/models/foo.rb
class Foo < Tableless
column :bar, :string
validates_presence_of :bar
end
In script/console
Loading development environment (Rails 2.2.2)
>> foo = Foo.new
=> #<Foo bar: nil>
>> foo.valid?
=> false
>> foo.errors
=> #<ActiveRecord::Errors:0x235b270 #errors={"bar"=>["can't be blank"]}, #base=#<Foo bar: nil>>
There is easier way now:
class Model
include ActiveModel::Model
attr_accessor :var
validates :var, presence: true
end
ActiveModel::Model code:
module ActiveModel
module Model
def self.included(base)
base.class_eval do
extend ActiveModel::Naming
extend ActiveModel::Translation
include ActiveModel::Validations
include ActiveModel::Conversion
end
end
def initialize(params={})
params.each do |attr, value|
self.public_send("#{attr}=", value)
end if params
end
def persisted?
false
end
end
end
http://api.rubyonrails.org/classes/ActiveModel/Model.html
I think the blog post you are linking is the best way to go. I would only suggest moving the stubbed out methods into a module not to pollute your code.
just create a new file ending in ".rb" following the conventions you're used to (singular for file name and class name, underscored for file name, camel case for class name) on your "models/" directory. The key here is to not inherit your model from ActiveRecord (because it is AR that gives you the database functionality).
e.g.: for a new model for cars, create a file called "car.rb" in your models/ directory and inside your model:
class Car
# here goes all your model's stuff
end
edit: btw, if you want attributes on your class, you can use here everything you use on ruby, just add a couple lines using "attr_accessor":
class Car
attr_accessor :wheels # this will create for you the reader and writer for this attribute
attr_accessor :doors # ya, this will do the same
# here goes all your model's stuff
end
edit #2: after reading Mike's comment, I'd tell you to go his way if you want all of the ActiveRecord's functionality but no table on the database. If you just want an ordinary Ruby class, maybe you'll find this solution better ;)
For the sake of completeness:
Rails now (at V5) has a handy module you can include:
include ActiveModel::Model
This allows you to initialise with a hash, and use validations amongst other things.
Full documentation is here.
There's a screencast about non-Active Record model, made up by Ryan Bates. A good place to start from.
Just in case you did not already watch it.
I have built a quick Mixin to handle this, as per John Topley's suggestion.
http://github.com/willrjmarshall/Tableless
What about marking the class as abstract?
class Car < ActiveRecord::Base
self.abstract = true
end
this will tell rails that the Car class has no corresponding table.
[edit]
this won't really help you if you'll need to do something like:
my_car = Car.new
Use the Validatable gem. As you say, there are AR-based solutions, but they tend to be brittle.
http://validatable.rubyforge.org/
Anybody has ever tried to include ActiveRecord::Validations and ActiveRecord::Validations::ClassMethods in a non-Active Record class and see what happens when trying to setup validators ?
I'm sure there are plenty of dependencies between the validation framework and ActiveRecord itself. But you may succeed in getting rid of those dependencies by forking your own validation framework from the AR validation framework.
Just an idea.
Update: oopps, this is more or less what's suggested in the post linked with your question. Sorry for the disturbance.
Do like Tiago Pinto said and just don't have your model inherit from ActiveRecord::Base. It'll just be a regular Ruby class that you stick in a file in your app/models/ directory. If none of your models have tables and you're not using a database or ActiveRecord at all in your app, be sure to modify your environment.rb file to have the following line:
config.frameworks -= [:active_record]
This should be within the Rails::Initializer.run do |config| block.
You ought to checkout the PassiveRecord plugin. It gives you an ActiveRecord-like interface for non-database models. It's simple, and less hassle than fighting ActiveRecord.
We're using PassiveRecord in combination with the Validatable gem to get the OP's desired behaviour.