We have been using rails 5.2RC1 for a couple weeks on a small number of production apps to test ActiveStorage. We've been able to get everything working with our Heroku instances (including PDF previews), but are now running into some questions around best practices.
Say we have the following model:
class Contract < ApplicationRecord
has_many_attached :documents
end
This works perfectly.
However now we want to add some additional data about each individual document. Perhaps things like the type of document for the contract or some other type of metadata.
Our first thought was to try and stuff this into the metadata attribute of the blob, but that doesn't feel right.
The other thought that we had was to change the design to something like this:
class Contract < ApplicationRecord
has_many :documents
end
class Document < ApplicationRecord
belongs_to :contract
has_many_attached :files
end
Then using the document model to keep information about each attached file. Say in this example a contract has an original document, but then in the future there could be addendum attached to it that have their own unique properties that we would want to keep track of.
Thoughts?
At the time this question was asked, it's probably the case that metadata was getting overwritten as described in this Github issue. While metadata still isn't queryable (come on guys, nearly 5 years has passed), you can definitely add custom attributes.
The weird thing is that the attach() function param metadata says in the docs it is supposed to be a string, but that doesn't work and gets overwritten by the analyzer. If you just change it to an object-literal instead, that works and the custom metadata gets merged with the analyzer metadata.
So this works:
payment.archives.attach(
io: File.open("./archive.zip"),
filename: "archive.zip",
content_type: "application/gzip",
metadata: { customTag: "some value" }
)
But passing the metadata as a stringified object does not:
payment.archives.attach(
io: File.open("./archive.zip"),
filename: "archive.zip",
content_type: "application/gzip",
metadata: "{ \"customTag\": \"some value\" }"
)
Also, you can pass an ActionDispatch::Http::UploadedFile object as a param to io: which the docs also aren't clear on.. In my use case (and I imagine many others) I had an UploadedFile object coming from a multipart form request and needed to add some custom attributes to each file. hopefully this helps someone who is lost in legacy rails hell another 5 years from now.
The metadata on ActiveStorage blobs aren't queryable (https://github.com/rails/rails/issues/31780#issuecomment-360356381) and you cannot add custom attributes to them. Which is slightly limiting I feel.
It seems the solution is your second suggestion, having a new model object to wrap the files. This is explained further here: Rails 5.2 Active Storage add custom attributes
Related
With the release of Rails 5.2, the much used Paperclip gem is now deprecated and it's advised to use Active Storage that ships with Rails. I'm starting a new project and set up Active Storage with ease, but my problem comes when trying to add a name or description to the file uploads.
With Paperclip I would add a column to the model called something like file_upload_name, so that as well as having a file name "something.pdf" I could also add a name or description such as "My Important Document" on the upload form.
For the projects that I'm doing, this is a vital part of the upload process and ideally needs to be done at the time of upload. As Active Record doesn't store to a model in such a way it's not as simple as just adding a column and adding fields to a form. It seems something that should be relatively simple but I can't figure it out or find any information about how best to do it. Any help much appreciated.
Here's an example of what I'm trying to achieve:
With Active Storage the end result is a multiple file upload button, with no naming etc.
You should create a new model to wrap each attached file. That model would then have the ActiveStorage attachment defined on it, as well as whatever other attributes you need to capture. Ex:
class Attachment < ApplicationRecord
has_one_attached :file
end
Rails then treats file kind of like an attribute for each Attachment. You can define your other attributes (e.g. upload_name, etc.) on the Attachment model. Based on your screenshot, it looks like maybe a Quotation has many attached files, so you'd do something like:
class Quotation < ApplicationRecord
has_many :attachments
end
In Rails 5, is it possible to use the new attributes API with a field exposed via store_accessor on a jsonb column?
For example, I have:
class Item < ApplicationRecord
# ...
store_accessor :metadata, :publication_date
attribute :publication_date, :datetime
end
Then I'd like to call i = Item.new(publication_date: '2012-10-24'), and have metadata be a hash like: { 'publication_date' => #<DateTimeInstance> }.
However, the attribute call doesn't seem to be doing any coercion.
Hopefully I am missing something--it seems like being able to use these two features in conjunction would be very useful when working with jsonb columns. (Speaking of which, why doesn't the attributes API expose a generic array: true option? That would also be very useful for this case.)
I see that there is a project jsonb_accessor, but it seems a little heavyweight. It also seems to be designed for Rails 4 (I haven't checked whether it supports Rails 5).
You might check out a rather new (as of this writing) gem built atop the Rails 5+ Attributes API: AttrJson. I've recently started using it; some rough edges still, but the author/maintainer seems keen to improve it.
After digging in a little more, I see that the Attributes API (as it currently exists in ActiveRecord) is not really appropriate for handling jsonb data—there would be duplicate info in the attributes hash, etc.
I do think it would be nice if ActiveRecord provided typecasting/coercion for jsonb fields. I see that there is a project jsonb_accessor, but it seems a little heavyweight. It also seems to be designed for Rails 4 (I haven't checked whether it supports Rails 5).
I guess something like this might be in the works for Rails since the ActiveRecord::Type values are actually defined in ActiveModel.
For now I am using the following. I've never really loved Hashie, but this is relatively lightweight and easy to use:
class Item < ApplicationRecord
class Metadata < Hashie::Dash
include Hashie::Extensions::Dash::Coercion
include Hashie::Extensions::Dash::IndifferentAccess
property :publication_date, coerce: Time
def self.dump(obj); obj.as_json; end
def self.load(obj); new(obj); end
end
serialize :metadata, Metadata
store_accessor :metadata, :publication_date
end
I have a model called User. It has two fields: small_logo and big_logo.
Those are actually different pictures, not just one resized picture.
class User < ActiveRecord::Base
...
mount_uploader :big_logo, UserLogoUploader
mount_uploader :small_logo, UserLogoUploader
...
end
I use UserLogoUploader to upload this pictures.
And I'm running onto a bug - as long as the name of the model is the same, uploaded files get the same route, so if I try to upload two different files with same names - second one overwrites first one.
The obvious solution is to use different uploaders for those fields. But I don't want to create another uploader just to fix this bug - is there anything I can do to modify filename, for example, with something meaningful like the name of a formfield that submitted this file or access name of a model field that is being processed.
Found an answer to my own question after some searching
There is a mounted_as attribute inside an uploader, which, reffering to docs, does exactly what I need:
If a model is given as the first parameter, it will stored in the uploader, and
available throught +#model+. Likewise, mounted_as stores the name of the column
where this instance of the uploader is mounted. These values can then be used inside
your uploader.
So the whole solution looks like this:
def UserLogoUploader < CarrierWave::Uploader::Base
...
def store_dir
File.join [
Settings.carrierwave.store_dir_prefix,
model.class.to_s.underscore,
mounted_as.to_s,
model.id.to_s
].select(&:present?)
end
...
end
This code creates different subfolders for different model fields, which helps preventing names duplication and files overwriting.
I have two scenarios for using Paperclip, but I'm unsure of how to tweak the settings or if it's possible or even necessary. Need the advice of more seasoned professionals on this one.
First up, I have a Document model for uploads such as PDFs, which would be defined:
has_attached_file :document...
This would give me column names like #document.document_file_name. Anyway that I could have #document.file_name instead?
Secondly, I have Gallery.rb which has many Picture.rb. Same scenario here as well. Can I avoid having #picture.picture_file_name? Or is this something that should really be overlooked with the gains that Paperclip affords.
Thanks in advance for any input.
My take on this: The actual document (PDF file) is not the same as a document record (which comprises the physical document plus metadata). Therefore it makes sense to see the Paperclip attachment as an attribute of the model and have its methods be called after the attribute, and not operate on the model record itself.
One of my apps has a Document model with an attached file too, and I simply called the attribute attachment.
If this is too much of an inconvenience for you, you could always implement your own getters in the model:
class Document < ActiveRecord::Base
has_attached_file :attachment # ... or whatever you are calling it
def file_name
self.attachment.file_name
end
def file_size
self.attachment.file_size
end
def file_type
self.attachment.file_type
end
end
The Paperclip gem requires three attributes on the associated object.
attribute_file_name
attribute_file_size
attribute_file_type
attribute of course if the name of your file and it is the has_attached_file :attribute filed commonly called picture, image, whatever.
If you want to change one of those names you will need to edit the gem itself which seems crazy for just changing the attribute name :)
Here are the methods I had to create:
{attribute}_file_name
{attribute}_file_size
{attribute}_content_type
{attribute}_updated_at
I'm currently using the awesome attachment-fu plugin for a Rails app, but as a novice developer, I've never encountered a scenario like the one I've found myself in.
Essentially, I'm using the attachment-fu plugin on two levels.
Is for user avatars in the user class.
Is to allow file attachments (PDFs, etc) in a messaging system.
My question is what the best use practice would be in these situations to remain DRY, clear, and consistent.
Clearly it would make no sense to define and execute the plugin in both classes, but there's something deeply strange to me (possibly unfounded) about just going ahead and setting it all up in the godly Application class.
Is there something in between, or is the parent class the way to go?
Thanks!
What's the DRY issue with defining the attachment_fu settings twice?
Unless the files are of the same type and being stored in the same place, you're not going to be repeating anything in the configuration.
Sure, you'll have two has_attachment declarations, but the options will mostly differ (one declaration for your avatars and the other for your pdf's etc.
99.99% of the code to handle attachment will be buried in the attachment_fu libs, your configuration code should be pretty DRY by default =)
Is "outsourcing" avatar support entirely to Gravatar an option? There are some Rails plugins that will display avatars hosted by Gravatar. You might not need to re-invent the wheel there.
What wfarr is describing would be single table inheritance, which is what I currently do in this situation. I have one table for Assets which contains all the necessary attachment_fu columns, plus an extra column called type, which will hold the actual model name. I have a model for assets and additional models for specific upload types that inherit from assets:
asset.rb:
class Asset < ActiveRecord::Base
... attachment_fu logic ...
end
avatar.rb:
class Avatar < Asset
... avatar specific attachment_fu logic ...
end
pdf.rb:
class PDF < Asset
... PDF specific attachment_fu logic ...
end
I would lean towards using a parent class, with subclassing for the different ways you intend to actually use the attachments in your application. It may not be the DRYest solution available, however, it lends itself to a logical pattern rather well.
Couldn't you use Polymorphic Associations?
I'm about to hit this in my app with attachment_fu, so I'm not exactly sure on attachment_fu, but for the old school File Column plugin, I would use Polymorphic Associations.
My "file" model would be:
class FileUpload < ActiveRecord::Base
belongs_to :fileable, :polymorphic => true
file_column :name
end
and then any models that needed a file attachment would be like:
class Company < ActiveRecord::Base
has_many :file_uploads, :as => :fileable
end
File Column is no good anymore as it borks on Safari 3.x and is no longer maintained. It was nice and simple though... Ah, the good old days...
For what it's worth, I think Patrick Berkeley has done a good job with handling multiple attachments through the Paperclip plugin. He's outlined his work here:
http://gist.github.com/33011