Extending ActiveStorage::Attachment - Adding custom fields
Asked Answered
D

4

17

I want to extend the class ActiveStorage::Attachment and add an enum attribute for visibility of attachments.

My initial approach was to create a new file attachment.rb in the \app\models directory as follows.

class ActiveStorage::Attachment < ActiveRecord::Base
    enum visibility: [ :privately_visible, :publicly_visible]
end

This doesn't work.

Any suggestions are welcome. What's the Rails way to extend classes?

Update

I have a solution that works partially now. For this, I have created an extension active_storage_attachment_extension.rb and placed it in \lib

module ActiveStorageAttachmentExtension

  extend ActiveSupport::Concern

  included do
    enum visibility: [ :privately_visible, :publicly_visible]

    def describe_me
      puts "I am part of the extension"
    end

  end
end

The extension is loaded during initialization in extensions.rb

ActiveStorage::Attachment.send(:include, ::ActiveStorageAttachmentExtension)

Unfortunately, it is only working partly: While the enum methods publicly_visible? and privately_visible? are available in the views, they are not available in the controller. When invoking any of the methods in the controller, then the enum seems to have disappeared. I get a "NoMethodError - undefined method" error. Surprisingly, once the enum methods are called once in the controller, they are also not available any more in the views. I assume that the ActiveStorage::Attachment class gets reloaded dynamically and that the extensions are lost as they are only added during initialization.

Any ideas?

Deficient answered 9/6, 2018 at 3:31 Comment(1)
I never tried, but I think you should call the file active_storage_attachment.rb, then migrate adding the column visibility to active_storage_attachments table.Wiles
T
11

I assume that the ActiveStorage::Attachment class gets reloaded dynamically and that the extensions are lost as they are only added during initialization.

You’re correct. Use Rails.configuration.to_prepare to mix your module in after application boot and every time code is reloaded:

Rails.configuration.to_prepare do
  ActiveStorage::Attachment.send :include, ::ActiveStorageAttachmentExtension
end
Tamatave answered 10/6, 2018 at 20:21 Comment(0)
C
4

This works for me in Rails 6.

# frozen_string_literal: true

module ActiveStorageAttachmentExtension
  extend ActiveSupport::Concern

  included do
    has_many :virus_scan_results
  end
end

Rails.configuration.to_prepare do
  ActiveStorage::Attachment.include ActiveStorageAttachmentExtension
end
Chalkstone answered 8/1, 2020 at 14:9 Comment(0)
W
3

As mentioned in my comment, it requires the file app/models/active_storage_attachment.rb with this content:

class ActiveStorageAttachment < ApplicationRecord
  enum visibility: [ :privately_visible, :publicly_visible]
end

Then you also need to add the column visibility of type integer to the table active_storage_attachments.

class AddVisibilityToActiveStorageAttachments < ActiveRecord::Migration[5.2]
  def change
    add_column :active_storage_attachments, :visibility, :integer

  end
end

Accessing the new column of ActiveStorageAttachment

I make an example using my model: I have a User which has_one_attached :avatar.

I can access the active_storage_attachments table through user.avatar.attachment.inspect which returns for example #<ActiveStorage::Attachment id: 1, name: "avatar", record_type: "User", record_id: 1, blob_id: 3, created_at: "2018-06-03 13:26:20", visibility: 0>.

Note that the value of the column visibility is a pure integer, not converted by the visibility array (I'm still wondering why).

One possible workaround is to define a method like avatar_attachment in User model like this:

class User < ApplicationRecord
  has_one_attached :avatar

  def avatar_attachment
    ActiveStorageAttachment.find_by(name: 'avatar', record_type: 'User', record_id: self.id)
  end
end

Now user.avatar_attachment.inspect returns #<ActiveStorageAttachment id: 1, name: "avatar", record_type: "User", record_id: 1, blob_id: 3, created_at: "2018-06-03 13:26:20", visibility: "privately_visible">

Now all the methods related to visibility array are available. Also the record update works:

user.avatar_attachment.publicly_visible! # => true
Wiles answered 9/6, 2018 at 15:10 Comment(4)
I also do have the migration which adds the field to the ActiveStorage::Attachment model, and the field with an integer value is also available. However, the enum is not available for me with this approach. I also tried giving a different class name ActiveStorage::Attachment as it is actually part of a module. Still no success. Please also see my updates to the original question.Deficient
@PatrickFrey, I'm using User which has_one_attached :avatar. Using the set up showed in my answer I can access the attachment record in in active_storage_attachments table by user.avatar.attachment. Both from controller and view. So you could. But, I noted that the content of the column visibility is pure integer, no conversion through the emun array. While if I access directly the active_storage_attachments table I can see the column visibility properly converted through the enum array. I must find out why. 🤔Wiles
@PatrickFrey, I guess I found a solution. Take a look to the updated post.Wiles
The appoach you describe also worked for me, however, only partially. When invoking any of the methods in the controller, then the enum seems to have disappeared. Currently, I am not using the enum but simply the integers. I don't have the comfort of the enum, but it works.Deficient
P
0

If someone else runs into this, I didn't like the solutions entered here so I came up with another way. Not 100% sure if it's as good as the Rails.configuration.to_prepare solution, but the thing I like about it is that it's just one file in the app/models directory, so no magic going on in configuration files somewhere else in your project.

I make a file named: app/models/active_storage/attachment.rb. Because it's in your project it takes loading precedence over the Gem version. Then inside we load the Gem version, and then monkeypatch it using class_eval:

active_storage_gem_path = Gem::Specification.find_by_name('activestorage').gem_dir
require "#{active_storage_gem_path}/app/models/active_storage/attachment"

ActiveStorage::Attachment.class_eval do
  acts_as_taggable on: :tags
end

The slightly nasty part is locating the original file, since we can't find it normally because our new file takes precedence. This is not necessary in production, so you could put a if Rails.env.production? around it if you like I think.

Pellucid answered 11/1, 2019 at 15:44 Comment(1)
I've had to do something similar before to override a file from the gem. It's usually not necessary — doesn't seem to be in this case, because you can simply reopen the class after the upstream file has defined it — but in certain rare cases where you have to run some code before the upstream file (and no hook is provided), it could be. I wish there were an easier/cleaner way to load the original file (like a "super" for requires). But when you put something at the same path, you "shadow" the path from the library and it makes it very hard to access the upstream file with the same path.Gunyah

© 2022 - 2024 — McMap. All rights reserved.