So I have a items/new.html.erb page that has a form for a new Items record.
Now the page has an image uploads section also, beside the new record form, where the user can upload multiple images. This UI design suggests that the user should be able to upload images, or attach images to the to-be-created record.
My current setup is when the user uploads an image in the page(this is handled via AJAX/rails UJS), the id of the uploaded image/images will be added in a hidden field in the new Item form. Then only when the main form is submitted will the images be attached to the newly created Item resource. I feel that this is an unusual way of handling this issue and that there is a more easy, clear, Rails way for this.
I call the images like this:
item.images
The page also allows that the user be able to sort or update the order of the Image attachments, but this I think is a topic that should be in another discussion.
How do you handle this, the first issue, properly and clearly in Rails?
Nested attributes
Rails has a built in mechanism called accepts_nested_attributes. Which lets you create nested records in the same request:
class Item
has_many :images
accepts_nested_attributes_for :images
end
class Image
belongs_to :item
has_one_attached :file
end
This will let you create an item and images with:
Item.create(name: 'Foo', image_attributes: [{ file: 'foo.jpg'}, { file: 'bar.jpg'}])
ActiveRecord will handle inserting the records in the correct order.
This allows you to have a non-nullable foreign key (item_id) and avoid orphaned records which is a very real problem with your solution. Referential integrity should be pretty high on your list of priorities.
This is used in the model together with fields_for in the view (the form).
<%= form_with(model: #item) do |form| %>
<%= form.fields_for(:images) do |image_fields| %>
<%= image_fields.file_field :file %>
<% end %>
<% end %>
And by passing an array of keys in the strong parameters:
params.require(:item).permit(:name, image_attributes: [:file])
ActiveStorage's has_many_attached
ActiveStorage also lets you setup a one to many assocation without a model by using has_many_attached. The attachements are stored in the active_storage_attachments table. However there is no way as far as I know of attaching additional metadata (such as the ordering) to the attachements.
Related
In my Rails 5 application, building a JSON API & using Activestorage for my attachments:
To avoid accessing an attachment that doesn't exist, I am using this if condition as shown in this answer but unfortunately this if condition makes a SQL query to check if the attachment is available or not, causing n+1 queries although I have already included (pre-loaded) the attachments for the records I have
How to check if the attachment exist on a record without hitting the DB, if I have preloaded the attachments already?
I tried to use .nil? but that will give a false positive that there is attachment even if there isn't (as shown below)
ActionView::Template::Error (to_model delegated to attachment, but attachment is nil):
if !product.logo.nil?
json.logo_img url_for(product.logo)
.attached? check works but causes the n+1 queries:
if product.logo.attached?
json.logo_img url_for(product.logo)
So while the below code looks very ugly, it works, preloading the attachments for all the records at once, but when checking if an attachment exists => this leads to n+1 queries
json.array!(#some_records.includes(integrated_product: [company: [ logo_attachment: :blob ]])) do |one_record|
product ||= one_record.integrated_product
if !product.logo.attached?
json.logo_img url_for(product.logo)
elsif !product.company.logo.attached?
json.logo_img url_for(product.company.logo)
end
....
Any thoughts on how to handle this in a proper way (preloading attachments & avoiding n+1) are appreciated.
Update:
I want to have the whole collection that I passed, including items with & without attachment, searching for a condition that will check if that already loaded item has an attachment or not to display an image if it has attachment
Rails generates a collection method for has_one_attached relationships. You can just do something like this:
Product.all.with_attached_logo.map{|product| product.logo.attached? }
Which will make a single eager loaded query.
Lets say you want to show the logo's of each product inside views/products/index.htm.erb
So let's set up the #products variable in the index action of your controllers/products_controller.rb
def index
#products = Product.all.with_attached_logo
end
This will eager load the logo attachment into each product if they have one. Now we can create the products index page without the N+1 problem like so:
<% #product.each do |product| %>
<% if product.logo.attached? %>
<%= image_tag product.logo %>
<% else %>
<%= image_tag "product_logo_placeholder.jpg" %>
<% end %>
... the rest of the product's information ...
<% end %>
The problem turned out to be that the complex includes I had was not actually preloading the attachments (it was preloading for the nested record but not the main one), therefore the .attached? method was making a SQL query to check.
However, when the correct preloading is done, the .attached? works correctly, checking the in-memory object without firing a seperate SQL query to check again
I am leaving the question for the reference of the community if someone face a similar issue.
If I understand you correctly, you might want do do something like this.
In your model:
class Product < ApplicationRecord
has_one_attached :logo
scope :with_eager_loaded_logo, -> { eager_load(logo_attachment: :blob) }
end
Then in your controller, for example:
def index
#products = current_user.products.with_eager_loaded_logo
end
Not a JSON, but still some view example(HTML):
<%= image_tag (product.logo) if product.logo.attached? %>
See more in this post.
campaign.rb
class Campaign < ActiveRecord::Base
has_many :items
accepts_nested_attributes_for :item
end
item.rb
class Item < ActiveRecord::Base
belongs_to :campaign
end
Campaign has 2 attributes: title and description
Item has 1 attirubte: name
I'll try explain myself by words, I want to create a nested form where they user insert the campaign's name and description but he can insert more than just 1 item, he can insert a list of items (in particular there will be a "+" button that when clicked a new item row will appear and the user can insert items).
At the end all is send all together clicking just one submit button.
How can I reach my goal with rails?
I answered a question just yesterday. Here's the link: Rails accepts_nested_attributes_for with f.fields_for and AJAX
I'll write out how it works & provide some resources to help give you some more ideas:
How It Works
Loading associative fields into a form is done by using f.fields_for
You'll do it like this:
#app/views/campaigns/new.html.erb
<%= form_for #campaign do |f| %>
<%= f.fields_for :items do |a| %>
<%= a.text_field :information %>
<% end %>
<% end %>
In order to get this to work, you have to build the associated ActiveRecord objects in the backend before you render the view, like this:
#app/controllers/campaigns_controller.rb
def new
#campaign = Campaign.new
#campaign.items.build
end
Adding Extra Fields Via Ajax
Adding extra fields with Ajax requires engineering a new solution to the issue
The way you do this is to take the f.fields_for text & put it into a partial. This partial can be called from the original view, as well as another view (which we can render through Ajax)
The Ajax part works by basically taking a request from your form (the ajax request), and then using another action in your controller to build a new ActiveRecord object & render another partial that will contain another form. This partial will then call the original f.fields_for partial, allowing you to render another field
Your Ajax can then extract the new field & append it to your page. The way you get around the id issue (keeping the IDs sequential & unique) is to employ the child_index method, and use Time.now.to_i to generate a timestamp
If you read my answer referenced at the top of this answer, all of this will make sense :)
Some great resources for this:
RailsCasts Nested Forms
Adding Fields With Ajax
A nice gem along with tutorial is available from ryanbates who is the author of railscasts.com site.You can use this and have a look at tutorial here
And also if you want to try manually use the fields_for while writing in the form like here and manage some jquery code for add or remove.
I want to implement Papereclip in Rails 3.2 without a model.
I am very new to RoR and Paperclip.
I followed through THIS tutorial to use paperclip. It is running successfully.
But in this tutorial they have shown use of the model, I dont want to use the model or dont want to modify the existing model.
Rails is object oriented.
# Article model
attr_accessible :image
# Paperclip attachment code and whatever rules and conditions you want.
The image attribute is actually a column from your database and an attribute accessible from your Article model. That image attribute will need to be a string type (varchar) to store the url where the image is saved.
For example if your article has an image attribute, in your view file Article#Show you can display your image through the article object with the image attribute as <%= image_tag #article.image.url %>.
If you want to have many images in a same article then you will want to create 2 models (Article and Image) and have an association where
# Article model
has_many :images
accepts_nested_attributes_for :images
# Image model
attr_accessible :image_url
belongs_to :article
# paperclip attachments...
With this association you can then fetch and display your images by doing
<% #article.images.each do |image| %>
<%= image_tag image.image_url.url %>
<% end %>
Technically you don't need to store the url of your image in the database, you can store it in variable, but then it will only be valid during the session and will disappear afterwards. If you want to view your image at a later date, then yes you need to store it in a database.
For not using a seperate model you can check Paperclip Docs. What is basically done here is that the image is stored as an atrribute of an existing model.
About not using a model at all, how do you want to associate the image object to some existing record ? Wont that be required in your project ?
Some pseudocode from my app:
User has many Products
User has many Projects
Project and Product belong to User
Furthermore:
Project has one Video
Video belongs to Project
I have a multi-step wizard built using the Wicked gem. In step one I create and save a Project. In step two I add a Video to that Project:
= form_for #project do |f|
= f.fields_for :video_attributes do |v|
= v.file_field :file
Everything works fine, but I'd like to add a Product to the Project's User during this same step. I'm a little confused as to how accepts nested attributes works for this sort of thing.
I imagine I need to do something like this in my wicked controller:
#user = current_user
# wicked makes us use :project_id as it hijacks :id
#project = #user.projects.find(params[:project_id])
#user.products.build
But where do I stick the 'nested attributes for' call? Do I need more than one call to accepts_nested_attributes_for? Would this work?
Make Project model accept nested attributes for User
Make User model accept nested attributes for Product
= form_for #product do |f|
= f.fields_for :user_attributes do |u|
= u.fields_for :product_attributes do |p|
= p.file_field :image
I can't try the code out till tomorrow, but i will sleep better knowing i can solve this when i get to it.
You certainly can extend nested attributes through several objects by nesting the fields_for calls... But you can sometimes get into trouble if you jump back and forth between objects like it looks like you're headed towards here. I've had problems with circular save issues resulting from structures like that. For this reason, I recommend keeping the accepts_nested_attributes_for as a one-way street. So, if a user accepts_nested_attributes_for projects then a project should not also accepts_nested_attributes_for users. Given that, your form has to be built based on the root object. I don't know your project but for mine that was the user. Basically, it's more likely the user would be the central relationship. Hope this helps.
Also, I'm not sure why your fields_for calls are using <some object>_attributes. Unless you're doing something special, those should be relation names like f.fields_for :video. This way the fields_for call loops through each object of that type in the collection.
Let's say:
[ Micropost ] -> has_one -> [ MicropostPhotoAlbum ] -> has_many -> [ MicropostPhotos ]
If Micropost had one photo it wouldn't need a MicropostPhotoAlbum and I could just add an "image" column to my microposts table and work smoothly with carrierwave.
My form below would work fine:
= form_for #micropost, :remote => true do |f|
= f.file_field :image # Needed for image previewing with html5 file api
= f.hidden_field :user_id
= f.text_area :content
= f.submit "Post"
All I'd need to do is mount an upload to my micropost model and I'd be set. The image path would upload directly to the microposts table image column e.g. photo.png.
Well as you can see above each micropost won't have any photos but instead have one album that contains many photos. Now my form won't work seeing as there will be a micropost_album_id column instead of an image column.
How can I use my current form to upload a photos to the photos table and post my micropost content in one submit? Is there any simple way around this other than reverting back to making each micropost have one photo?
Kind regards
This looks like a job for nested attributes.
Here is an (old, admittedly) article on nested attributes in forms. fields_for is the key.