rails: adding image descriptions, migrating from has_many_attached to has_many - ruby-on-rails

In my rails 7 app, products can have images. At first I used:
# products.rb
has_many_attached :images do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
end
Now, I need each image to have a caption. So I created a images polymorphic record:
# products.rb
has_many :photos, as: :imageable, class_name: :Image, dependent: :destroy do
def attach(args)
build.attach(args)
end
end
# images.rb
has_one_attached :file
My question is:
Is it possible to have products images variant definition in the products.rb file ?
For example a product would have thumb variants (100x100), and another model, a home could also has_many :images but with a medium (200x200) variant
resources
https://www.bigbinary.com/blog/rails-7-adds-ability-to-use-predefined-variants
https://jonsully.net/blog/a-generic-image-wrapper-for-active-storage/

Yes it is possible. Iterating over Jon Sullivan's post, there only are a few changes to make to be able to define variants from the parent model.
I've setup an example repo here.
It is just a proof of concept, this is not tested, I've probably forgotten cases, etc... it seems to work but I would not bet my production environment on it.
Furthermore, this code may be too "clever" and make things more difficult to maintain and debug. I suspect there is a way to make this simpler, but did not take the time to dig further.
Anyway, the TLDR is that, in order to to handle some difference between has_one and has_many relationships, we create a new method in the Imageable concern, heavily inspired by Jon's ones, and adapt the Image class a bit:
# app/model/concerns/imageable.rb
module Imageable
extend ActiveSupport::Concern
class_methods do
def has_images(relation_name, **kwargs, &block)
has_many relation_name, as: :imageable, class_name: 'Image', dependent: :destroy
# this creates an has_one_attached relation, specific to this call of
# has_images, to which attachment options will be set. This allows to
# have distinct options set per call
image_attachment = "#{name.downcase}_#{relation_name.to_s.singularize}".to_sym
Image.has_one_attached image_attachment, **kwargs, &block
# not strictly necessary but allows for nicer code when building forms
accepts_nested_attributes_for relation_name
# We override attribute writers generated by `has_many` and `accepts_nested_attributes_for`
# to force the correct name attribute on Image
class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{relation_name}=(images)
super(images.map {|image|
image.name = "#{image_attachment}"
image
})
end
# not strictly necessary but allows for nicer code when building forms
def #{relation_name}_attributes=(attrs)
super(attrs.transform_values {|image_attrs|
{name: "#{image_attachment}"}.merge!(image_attrs)
})
end
CODE
end
end
end
# app/models/image.rb
class Image < ApplicationRecord
belongs_to :imageable, polymorphic: true
has_one_attached :file
delegate :persisted?, to: :imageable, allow_nil: true
delegate_missing_to :file
# The name attribute value is the name of the relation we create on Image
# when calling Imageable.has_images. We call the attr_reader created by
# the relation definition, or fallback to a generic attachment if no name
# exists (eg on Image.new)
def file
name ? public_send(name) : ActiveStorage::Attached::One.new('file', self)
end
def file=(attrs)
file.attach attrs unless attrs.blank?
end
end
You can setup a relation using the newly defined has_images method
# app/model/product.rb
class Product < ApplicationRecord
include Imageable
has_images :photos do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
attachable.variant :medium, resize_to_limit: [300, 300]
end
end
You can then use this in your views as in the blog post
<%# app/views/products/_form.html.erb %>
<div id="<%= dom_id product %>">
<p>
<strong>Images:</strong>
<% product.photos.each do |image| %>
<%= image_tag image.file.variant(:thumb) %>
<%= image_tag image.file.variant(:medium) %>
<% end %>
</p>
</div>
<%# app/views/products/_form.html.erb %>
<%= form_with(model: product) do |form| %>
<% if product.errors.any? %>
<div style="color: red">
<h2><%= pluralize(product.errors.count, "error") %> prohibited this product from being saved:</h2>
<ul>
<% product.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<%= form.fields_for :photos do |photo_fields| %>
<div>
<%= photo_fields.label :preview %>
<br>
<%= image_tag(photo_fields.object.variant(:thumb)) if photo_fields.object.attached? %>
</div>
<div>
<%= photo_fields.file_field :file, direct_upload: true %>
</div>
<div>
<%= photo_fields.label :image_alt_text %><br>
<%= photo_fields.text_field :alt_text, required: true %>
</div>
<div>
<%= photo_fields.label :image_caption %><br>
<%= photo_fields.text_field :caption, required: true %>
</div>
<% end %>
<div>
<%= form.submit %>
</div>
<% end %>

Related

rails file_field not showing

I would like to make my app upload multiple files with Shrine, but one doc suggests two file_fields whereas the other suggests only one. After posting a question to their discourse forum, it was suggested that I hide the one named files[]. Whether I do this or not, the first file_field always fails to render. Why does this field not display?
<%= form_for #item, html: { enctype: "multipart/form-data" } do |f| %>
<%= f.fields_for :photos do |i| %>
<%= i.label :image %>
<%= i.hidden_field :image, value: i.object.cached_photos_data, class: "upload-data" %>
<%= i.file_field :image, class: "upload-file" %> /// why is this not rendering?😢
<% end %>
<%= file_field_tag "files[]", multiple: true %> // what purpose does this one serve?
<%= f.text_field :title %>
<%= f.submit "Submit" %>
<% end %>
Models:
class Item < ApplicationRecord
has_many :photos, as: :imageable, dependent: :destroy
end
class Photo < ApplicationRecord
include ImagesUploader::Attachment(:image)
belongs_to :imageable, polymorphic: true
validates_presence_of :image
end
Controller:
class ItemsController < ApplicationController
def new
#item = current_user.items.new
end
def create
#item = current_user.items.create(item_params)
#item.save
end
private
def item_params
params.require(:item).permit(:title, photos_attributes: { image: [] })
end
end
Read the first link carefully: It says that the single field (i.file_field :image) is used to display existing images (which is why it's wrapped in f.fields_for :photos in the example) and the multiple field (file_field_tag "files[]", multiple: true) is used to upload new files. So if your #item doesn't have an :image, then the field isn't shown.
Let me know if this needs any further clarification – happy to help!

Rails - How can I display one nested attributes

I have a new problem, I Create a web where I upload many images, using nested attributes and polymorphic table, in my index.html I want to show only one image, but I can't find how. But I'm new in rails.
photography.rb
class Photography < ActiveRecord::Base
validates :title, :description, presence: true
belongs_to :user
has_many :images, as: :imageable, dependent: :destroy
accepts_nested_attributes_for :images, :reject_if => lambda { |a| a[:img_str].blank? }, :allow_destroy => true
end
image.rb
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
mount_uploader :img_str, AssetUploader
end
index.html.erb
<% for photo in #photo %>
<%= link_to photo.title, photography_path(photo) %>
<% photo.images.each do |images| %>
<%= images.img_str %>
<% end %>
<% end %>
With the for method I show all the image, try add .first, but says undefined method first for 5:Fixnum. I think that I have to create a helper method, but I not sure. Can anyone help me?. Thanks
Try:
<% for photo in #photo %>
<%= link_to photo.title, photography_path(photo) %>
<%= photo.images.first.img_str if photo.images.any? %>
<% end %>
Also, for is very rarely used in ruby, instead do:
<% #photos.each do |photo| %>

Refactoring view loops to use fewer queries via includes with a HABTM relationship

I have been trying to refactor this code to reduce the db calls by possibly using "includes". I would like to replace the three nested loops in the view. Tried various options but got stuck... I'm still getting familiar with active record querying.
How can I make this more efficient with less queries?
Is using includes the best option?
If so, how do I access the various fields through my HABTM relationships?
Thanks.
Models:
class Category < ActiveRecord::Base
has_many :pub_types
end
class PubType < ActiveRecord::Base
belongs_to :category
has_and_belongs_to_many :issues
end
class Issue < ActiveRecord::Base
has_and_belongs_to_many :pub_types
has_many :images, :dependent => :destroy
end
Controller:
def home
#categories = Category.all
#issues_and_pubs = Issue.joins(:pub_types).uniq
end
View:
<% #categories.each do |category| %>
<%= category.name %>
<% #issues_and_pubs.where(:pub_types => {:category_id => ["#{category.id}"]}).each do |issue| %>
<% issue.images.each do |img| %>
<% if img.featured == true %>
<%= cl_image_tag img.image, :width => 295, :height => 155, :alt => img.name, :crop => :fill %>
<%= link_to issue.name, issue %>
<% end %>
<% end %>
<%= issue.issue_date.try(:strftime, "%B %d, %Y") %>
<%= issue.pub_types.map(&:name).join(", ") %>
<% end %>
<% end %>
Try this instead.
Add this to Category:
has_many :issues, through: :pub_types
Controller:
#categories = Category.includes(:issues => :images)
View:
#categories.each do |category|
category.issues.each do |issue|
issue.images.each do |image|
issue.pub_types # might still result in N+1, haven't tested
On the note above about N+1 on pub_types, I have had occasions where I've eager loaded associations, but Rails has not taken them into account when calling from children to parents. One approach I've used in the past is to be explicit with the references:
Category.includes(:issues => [:pub_types, :images])
Without the has_many through:, this would look rightly peculiar:
Category.includes(:pub_types => [:issues => [:pub_types, :images]])

Rails: How do I submit multiple objects into strong params?

I am making a goal tracking app. Right now outcome, purpose, action, priority, resources, and direction are all things which are part of Outcome in the database. However, I want to make purpose and action their own model objects. What I am confused about is how do I submit Outcome, Purpose, and Action, which will be 3 separate model objects, in a single HTTP request?
Should I just use multiple strong params in my controller?
app/view/outcomes/new.html.erb
You need to have model associations of outcomes with purpose and action.
Then you will need to create nested form. So that outform form can wrap purpose and action model attributes.
As you want to have different models for actions and purposes, I'm assuming outcome can has_many purposes and has_many actions. As per this type of association, below is the code you should have.
Your form will become something like:
<%= form_for #outcome do |f| %>
<%= f.label :outcome, "Outcome" %>
<%= f.text_area :outcome %>
<%= f.fields_for :purpose, #outcome.purpose.build do |p| %>
<%= p.text_area :desc, label: "Purpose" %>
<% end %>
<%= f.fields_for :action, #outcome.action.build do |p| %>
<%= p.text_area :desc, label: "Action" %>
<% end %>
<%= f.submit "submit" %>
<% end %>
Models:
# outcome.rb
has_many :purposes, :dependent => :destroy
has_many :actions, :dependent => :destroy
accepts_nested_attributes_of :purposes, :actions
-----------------------------------------
# purpose.rb
belongs_to :outcome
-----------------------------------------
# action.rb
belongs_to :outcome
Controller:
# outcomes_controller.rb
def outcome_params
params.require(:outcome).permit(:outcome, purpose_attributes:[:desc], action_attributes: [:desc])
end
SUGGESTION: You should rename your action model name to avoid unwanted conflicts with rails keyword action.
This may help you
Nestd Attributes
If the objects are associated (as below), you'll be best using the accepts_nested_attributes_for method:
#app/models/outcome.rb
Class Outcome < ActiveRecord::Base
has_many :purposes
has_many :actions
accepts_nested_attributes_for :purposes, :actions
end
#app/models/purpose.rb
Class Purpose < ActiveRecord::Base
belongs_to :outcome
end
#app/models/action.rb
Class Action < ActiveRecord::Base
belongs_to :outcome
end
accepts_nested_attributes_for means you'll be able to send the associated objects through the Outcome model - meaning you can send them all in a single HTTP request
You have to remember the way Rails is set up (MVC pattern), meaning if you send a single request; any further model objects you have will be able to be stored too.
Here's how you can set it up:
#app/controllers/outcomes_controller.rb
Class OutcomesController < ApplicationController
def new
#outcome = Outcome.new
#outcome.purposes.build
#outcoe.actions.build
end
def create
#outcome = Outcome.new(outcome_params)
#outcome.save
end
private
def outcome_params
params.require(:outcome).permit(:outcome, purpose_attributes:[:purpose], action_attributes: [:action])
end
end
Which will give you the ability to use this form:
#app/views/outcomes/new.html.erb
<%= form_for #outcome do |f| %>
<%= f.label :outcome %>
<%= f.text_area :outcome %>
<%= f.fields_for :purposes do |p| %>
<%= p.text_area :purpose %>
<% end %>
<%= f.fields_for :actions do |a| %>
<%= a.text_area :action %>
<% end %>
<%= f.submit %>
<% end %>
--
Recommendation
From the looks of it, I'd recommend you'll be able to keep all of these details in a single model - storing in multiple models seems overkill

Adding empty record for nested attribute in rails

I'm working in Rails 4/Ruby 2.0.0. I have a two models - Articles and Graphics. Articles has_many Graphics. So, in my code I am trying to add an empty record to the graphics collection on the article so that in the form, there will be an empty set of fields to let a new record be added. I cannot figure out why the fields do not show up on the form though.
I've tried multiple methods of building the graphics collection but none seem to do the trick. Surely I must be missing something insanely small.
Article.rb
class Article < ActiveRecord::Base
has_many :graphics, :dependent => :destroy, :foreign_key => 'article_id'
accepts_nested_attributes_for :graphics,
:allow_destroy => true,
:reject_if => :all_blank
end
Graphic.rb
class Graphic < ActiveRecord::Base
belongs_to :article
validates_presence_of :path, :caption
end
_form.html.erb
...
<% f.fields_for :graphics do |g| %>
<div class="clear clearfix pad-b-20">
<div class="w-1-2 left f-left">
<div class="field">
<%= g.label :path %><br>
<%= g.text_field :path %>
</div>
</div>
<div class="w-1-2 left f-left">
<div class="field">
<%= g.label :caption %><br>
<%= g.text_field :caption %>
</div>
</div>
</div>
<% end %>
...
Building it in a form helper method
articles/_form.html.erb
<%= form_for(setup_article(#article)) do |f| %>
form_helper.rb
module FormHelper
def setup_article(article)
article.graphics.build
article
end
end
Using an ActiveRecord callback
Article.rb
...
after_initialize :build_graphics
private
def build_graphics
self.graphics.build
end
Building it in the controller
ArticleController.rb
...
def new
#article = Article.new
#article.graphics.build
end
...
The problem is that both for form_for and for fields_for you need to use <%=, because they render the contents of the form.
So, to solve your problem, you need to write
...
<%= f.fields_for :graphics do |g| %>
Your content
<% end %>
...

Resources