Rails Multiple Uploads with Shrine and nested attachments - amazon-web-services

I got an issue with uploading multiple files to AWS. I have two models Lessons && Attachments. Lessons has_many :attachments and I'm setting up a form that has lesson fields and I want to upload multiple attachments to that lesson. Everything uploads correctly except when I upload multiple files, it creates a new lesson and attachment. I'm working off:
https://gorails.com/episodes/multiple-file-uploads-with-shrine?autoplay=1
And
https://u.osu.edu/hasnan.1/2014/03/30/rails-4-multiple-file-upload-with-carrierwave-nested-form-and-jquery-file-upload/
Models
Lessons Model
class Lesson < ApplicationRecord
belongs_to :user
has_many :attachments, dependent: :destroy
accepts_nested_attributes_for :attachments
end
Attachment Model
class Attachment < ApplicationRecord
belongs_to :lesson, optional: true
include AttachmentUploader::Attachment.new(:media)
end
Controllers
Lessons controller
Class LessonsController < ApplicationController
# truncated for brevity.
def new
#lesson = current_user.lessons.build
end
def create
#lesson = current_user.lessons.build(lesson_params)
respond_to do |format|
if #lesson.save
if params[:media]
params[:media].each { |media|
#lesson.attachments.create(media_data: media)
}
end
format.html { redirect_to #lesson, notice: 'Lesson was successfully created.' }
format.json { render :show, status: :created, location: #lesson }
else
puts "\n\n\n#{#lesson.errors.full_messages.to_sentence}\n\n\n"
format.html { render :new, notice: #lesson.errors }
end
end
end
private
def set_lesson
#lesson = Lesson.find(params[:id])
end
def lesson_params
params.require(:lesson).permit(
:title,
:content,
:document,
:category_id,
:pinned,
:bootsy_image_gallery_id,
attachments_attributes: {media: []},
)
end
end
Attachment Controller
class AttachmentsController < ApplicationController
before_action :set_attachment, only: [:edit, :update, :destroy]
def create
#attachment = Attachment.new(attachment_params)
#attachment.save
end
private
def set_attachment
#attachment = Attachment.find(params[:id])
end
def attachment_params
params.fetch(:attachment, {})
end
end
ERB
_form.html.erb
<%= #lesson.errors.full_messages.first if #lesson.errors.any? %>
<%= form_for #lesson do |f| %>
<div class="control-group">
<%= f.label :title %>
<div class="controls">
<%= f.text_field :title, required: true %>
</div>
<div>
<%= f.label :content %>
<%= f.bootsy_area :content, editor_options: { html: false }, rows: "20", cols: "100" %>
</div>
<div>
<%= f.label 'File under at least one class' %>
<%= f.collection_select :category_id, Subject.all, :id, :name, { promt: "Choose a Class" } %>
</div>
<div>
<%= f.label :pinned %>
<%= f.label :pinned, "Yes", value: "Yes" %>
<%= f.radio_button :pinned, true%>
<%= f.label :pinned, "No", value: "No" %>
<%= f.radio_button :pinned, false, checked: true %>
</div>
<hr>
<div>
<%= f.label 'Or Upoad a file' %>
<%
######################
# This is where I have the attachment file field.
######################
%>
<%= file_field_tag "media[]", type: :file, multiple: true %>
</div>
<br>
<%= f.submit nil %>
<%= link_to 'Cancel', lessons_path%>
<div class="form-actions btn-a">
<%= link_to 'Cancel', lessons_path, class: "btn btn-default" %>
</div>
Routes
Rails.application.routes.draw do
mount AttachmentUploader::UploadEndpoint => "/attachments/upload"
resources :lessons do
member do
put "like", to: "lessons#upvote"
put "dislike", to: "lessons#downvote"
end
resources :comments
resources :attachments
end
root 'static#index'
end
JS
$(document).on("turbolinks:load", function () {
$("[type=file]").fileupload({
add: function (e, data) {
data.progressBar = $('<div class="progress" style="width: 300px"><div class="progress-bar"></div></dov>').insertAfter("#file-upload");
var options = {
extension: data.files[0].name.match(/(\.\w+)?$/)[0],
_: Date.now() // revent caching
}
$.getJSON("/attachments/upload/cache/presign", options, function (result) {
data.formData = result['fields'];
data.url = result['url'];
data.paramName = "file";
data.submit();
});
},
progress: function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
var percentage = progress.toString() + '%';
data.progressBar.find(".progress-bar").css("width", percentage).html(percentage);
},
done: function (e, data) {
console.log("done", data);
data.progressBar.remove();
var document = {
id: data.formData.key.match(/cache\/(.+)/)[1],
storage: 'cache',
metadata: {
size: data.files[0].size,
filename: data.files[0].name.match(/[^\/\\]+$/)[0],
mime_type: data.files[0].type
}
}
form = $(this).closest("form");
form_data = new FormData(form[0]);
form_data.append($(this).attr("name"), JSON.stringify(document))
$.ajax(form.attr("action"), {
contentType: false,
processData: false,
data: form_data,
method: form.attr("method"),
dataType: "json",
success: function(response) {
var $img = $("<img/>", { src: response.image_url, width: 400 });
var $div = $("<div/>").append($img);
$("#photos").append($div);
}
});
}
});
});

The reason this is happening is because the Javascript looks for the URL of the file field's parent form. You have the form for creating a new Lesson, which means every time you upload a file, it will create a new lesson.
Some options to consider are:
Place the upload form in a place that's only available after the Lesson has been created. This lets you create a form_for [#lesson, Attachment.new] which will point to the right URL and let you upload files really quickly, but the Lesson must be created first.
You could adjust the JS not to submit an AJAX request right away and instead attach hidden fields with the image data.
My screencast covered this method because you had to create the Album first before uploading on the Album's show page. I think this ends up being a better user experience so they aren't worrying about file uploads failing and causing their other form data to possibly get lost or not saved.

Related

Ruby on Rails 4: Searchkick displaying all items instead of searched results

I have a Forum that has_many :forum_threads and has_many :forum_posts, through: :forum_threads. So basically my Forums have their own Forum Threads that are related to that particular Forum topic. I've added Searchkick to handle the search form. Searchkick is working for my Forum table but not for my ForumThread table.
There is no error, it just renders out all of the forum threads instead of rendering the query that the form was given.
Searchkick GitHub
Here are my files:
forum.rb
class Forum < ActiveRecord::Base
accepts_nested_attributes_for :forum_threads
has_many :forum_posts, through: :forum_threads
accepts_nested_attributes_for :forum_posts
searchkick text_start: [:title], suggest: [:title], autocomplete: [:title]
def search_data
{
title: title
}
end
end
forums_controller.rb
class ForumsController < ApplicationController
def index
query = params[:q] || "*"
#forums = Forum.search(query, suggest: true, fields: [:title],
boost_where: {specific: :exact})
end
end
views/forums/index.html.erb
<%= form_tag forums_path, method: :get do |f| %>
<%= text_field_tag :q, nil, class: 'form-control', placeholder: 'Search...' %>
<% end %>
<% if #forums.suggestions.any? %>
<p class="lead">
<em>Did you mean: <strong><%= #forums.suggestions.first %></strong>?</em>
</p>
<% end %>
forum_thread.rb
class ForumThread < ActiveRecord::Base
belongs_to :user, counter_cache: true
belongs_to :forum, counter_cache: true, touch: true
has_many :forum_posts, dependent: :destroy
accepts_nested_attributes_for :forum_posts
validates :subject, presence: true
validates_associated :forum_posts
searchkick text_start: [:subject], suggest: [:subject], autocomplete: [:subject]
def search_data
{
subject: subject,
description: description
}
end
end
forum_threads_controller.rb
class ForumsController < ApplicationController
def index
query = params[:q].presence || "*"
#forum_threads = #forum.forum_threads.search(query, suggest: true, fields: [:subject, :description])
end
end
views/forum_threads/index.html.erb
<%= form_tag forum_forum_threads_path(<!-- something here? -->), method: :get do %>
<%= text_field_tag :q, nil, class: 'form-control', placeholder: 'Search...' %>
<% end %>
<% if #forum_threads.suggestions.any? %>
<p class="lead"><em>Did you mean: <%= #forum_threads.suggestions.first %>?</em></p>
<% end %>
routes.rb
Rails.application.routes.draw do
resources :forums do
resources :forum_threads do
resources :forum_posts do
member do
put 'like', to: 'forum_posts#upvote'
put 'dislike', to: 'forum_posts#downvote'
end
end
end
end
end
I just made a variable for suggestions to use in the view.
forum_threads_controller.rb
def index
query = params[:q].presence || "*"
#forum_threads = ForumThread.search(query,
where: { forum_id: #forum.id },
fields: [:subject, :description],
boost_where: {specific: :exact}, suggest: true, highlight: true)
#forum_threads_suggestions = []
if #forum_threads.empty? && #forum_threads.suggestions.any?
#forum_threads_suggestions = ForumThread.search(#forum_threads.suggestions.first, fields: [:subject], suggest: true,
boost_where: {specific: :exact}, highlight: true)
end
end
index.html.erb
<%= form_tag forum_forum_threads_path, method: :get do %>
<%= text_field_tag :q, nil, class: 'form-control', placeholder: 'Search...' %>
<% end %>
<% if #forum_threads.suggestions.any? %>
<p class="lead">
<em>
Did you mean: <strong><%= #forum_threads.suggestions.first %></strong>?
</em>
</p>
<% end %>
</div>
<!-- END SEARCH FORM -->
<!-- SHOW RESULTS AND SUGGESTIONS -->
<p class="light-grey"><em>
<%= pluralize #forum_threads.results.size, 'match' if params[:q].present? %>
<%= pluralize #forum_threads.suggestions.size, 'suggestion' if #forum_threads.suggestions.any? %>
</em></p>
<!-- END RESULTS AND SUGGESTIONS -->
<!-- SHOW FORUM THREADS -->
<% (#forum_threads.presence || #forum_threads_suggestions).each do |forum_thread| %>
I wrote an or statement to either show all forum_threads or suggestions.
forum_thread.rb)
searchkick fields: [:subject], suggest: ['subject'], highlight: [:subject]
def search_data
{
forum_id: forum.id,
forum_thread: #forum_thread,
subject: subject
}
end
I did the same thing for the forum model. I'm not sure if this is the best way to go about doing this so if someone else has something then please feel free to contribute.

ActiveModel::ForbiddenAttributesError use only params

I'm getting ActiveModel::ForbiddenAttributesError in MicropostsController#create on line #2 in the create action.
Tried also changing f.hidden_field to hidden_field_tag but still getting ForbiddenAttributesError
micropost_controller
def create
tag = Tag.find(params[:micropost][:tag_id])
#micropost = tag.microposts.build(params[:micropost])
#micropost.user_id = current_user.id
if #micropost.save
flash[:success] = "Posted!"
redirect_to root_path
else
render 'static_pages/home'
end
end
tags_controller
def details
#tag = Tag.find(params[:id])
#microposts = #tag.microposts
#micropost = #tag.microposts.build if sign_in?
end
micropost form
<%= form_for(#micropost) do |f| %>
<%= render 'shared/error_messages', object: #micropost %>
<div class="field">
<%= f.text_area :content, placeholder: "Your post" %>
<%= f.hidden_field :tag_id %>
</div>
<%= f.submit "Post", class: "btn btn-large btn-primary" %>
<% end %>
in tags.rb
has_many :microposts, dependent: :destroy
in microposts.rb
belongs_to :user
belongs_to :tag
It seems that you need to delete tag_id from your microposts parameters:
tag = Tag.find(params[:micropost].delete(:tag_id))
#micropost = tag.microposts.build(params[:micropost])
If that won't fix it, just whit-list the params (it's good idea anyway):
micropost_params = params.require(:micropost).permit(:name, :title, ...)
#micropost = tag.microposts.build(micropost_params)
this
def create
tag = Tag.find(params[:micropost][:tag_id])
#...
end
should probably be changed to this
def create
tag = Tag.find(params[:tag_id])
#...
end

Rails 4 multiple image or file upload using carrierwave

How can I upload multiple images from a file selection window using Rails 4 and CarrierWave? I have a post_controller and post_attachments model. How can I do this?
Can someone provide an example? Is there a simple approach to this?
This is solution to upload multiple images using carrierwave in rails 4 from scratch
Or you can find working demo :
Multiple Attachment Rails 4
To do just follow these steps.
rails new multiple_image_upload_carrierwave
In gem file
gem 'carrierwave'
bundle install
rails generate uploader Avatar
Create post scaffold
rails generate scaffold post title:string
Create post_attachment scaffold
rails generate scaffold post_attachment post_id:integer avatar:string
rake db:migrate
In post.rb
class Post < ActiveRecord::Base
has_many :post_attachments
accepts_nested_attributes_for :post_attachments
end
In post_attachment.rb
class PostAttachment < ActiveRecord::Base
mount_uploader :avatar, AvatarUploader
belongs_to :post
end
In post_controller.rb
def show
#post_attachments = #post.post_attachments.all
end
def new
#post = Post.new
#post_attachment = #post.post_attachments.build
end
def create
#post = Post.new(post_params)
respond_to do |format|
if #post.save
params[:post_attachments]['avatar'].each do |a|
#post_attachment = #post.post_attachments.create!(:avatar => a)
end
format.html { redirect_to #post, notice: 'Post was successfully created.' }
else
format.html { render action: 'new' }
end
end
end
private
def post_params
params.require(:post).permit(:title, post_attachments_attributes: [:id, :post_id, :avatar])
end
In views/posts/_form.html.erb
<%= form_for(#post, :html => { :multipart => true }) do |f| %>
<div class="field">
<%= f.label :title %><br>
<%= f.text_field :title %>
</div>
<%= f.fields_for :post_attachments do |p| %>
<div class="field">
<%= p.label :avatar %><br>
<%= p.file_field :avatar, :multiple => true, name: "post_attachments[avatar][]" %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
To edit an attachment and list of attachment for any post.
In views/posts/show.html.erb
<p id="notice"><%= notice %></p>
<p>
<strong>Title:</strong>
<%= #post.title %>
</p>
<% #post_attachments.each do |p| %>
<%= image_tag p.avatar_url %>
<%= link_to "Edit Attachment", edit_post_attachment_path(p) %>
<% end %>
<%= link_to 'Edit', edit_post_path(#post) %> |
<%= link_to 'Back', posts_path %>
Update form to edit an attachment views/post_attachments/_form.html.erb
<%= image_tag #post_attachment.avatar %>
<%= form_for(#post_attachment) do |f| %>
<div class="field">
<%= f.label :avatar %><br>
<%= f.file_field :avatar %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
Modify update method in post_attachment_controller.rb
def update
respond_to do |format|
if #post_attachment.update(post_attachment_params)
format.html { redirect_to #post_attachment.post, notice: 'Post attachment was successfully updated.' }
end
end
end
In rails 3 no need to define strong parameters and as you can define attribute_accessible in both the model and accept_nested_attribute to post model because attribute accessible is deprecated in rails 4.
For edit an attachment we cant modify all the attachments at a time. so we will replace attachment one by one, or you can modify as per your rule, Here I just show you how to update any attachment.
If we take a look at CarrierWave's documentation, this is actually very easy now.
https://github.com/carrierwaveuploader/carrierwave/blob/master/README.md#multiple-file-uploads
I will use Product as the model I want to add the pictures, as an example.
Get the master branch Carrierwave and add it to your Gemfile:
gem 'carrierwave', github:'carrierwaveuploader/carrierwave'
Create a column in the intended model to host an array of images:
rails generate migration AddPicturesToProducts pictures:json
Run the migration
bundle exec rake db:migrate
Add pictures to model Product
app/models/product.rb
class Product < ActiveRecord::Base
validates :name, presence: true
mount_uploaders :pictures, PictureUploader
end
Add pictures to strong params in ProductsController
app/controllers/products_controller.rb
def product_params
params.require(:product).permit(:name, pictures: [])
end
Allow your form to accept multiple pictures
app/views/products/new.html.erb
# notice 'html: { multipart: true }'
<%= form_for #product, html: { multipart: true } do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
# notice 'multiple: true'
<%= f.label :pictures %>
<%= f.file_field :pictures, multiple: true, accept: "image/jpeg, image/jpg, image/gif, image/png" %>
<%= f.submit "Submit" %>
<% end %>
In your views, you can reference the images parsing the pictures array:
#product.pictures[1].url
If you choose several images from a folder, the order will be the exact order you are taking them from top to bottom.
Some minor additions to the SSR answer:
accepts_nested_attributes_for does not require you to change the parent object's controller. So if to correct
name: "post_attachments[avatar][]"
to
name: "post[post_attachments_attributes][][avatar]"
then all these controller changes like these become redundant:
params[:post_attachments]['avatar'].each do |a|
#post_attachment = #post.post_attachments.create!(:avatar => a)
end
Also you should add PostAttachment.new to the parent object form:
In views/posts/_form.html.erb
<%= f.fields_for :post_attachments, PostAttachment.new do |ff| %>
<div class="field">
<%= ff.label :avatar %><br>
<%= ff.file_field :avatar, :multiple => true, name: "post[post_attachments_attributes][][avatar]" %>
</div>
<% end %>
This would make redundant this change in the parent's controller:
#post_attachment = #post.post_attachments.build
For more info see Rails fields_for form not showing up, nested form
If you use Rails 5, then change Rails.application.config.active_record.belongs_to_required_by_default value from true to false (in config/initializers/new_framework_defaults.rb) due to a bug inside accepts_nested_attributes_for (otherwise accepts_nested_attributes_for won't generally work under Rails 5).
EDIT 1:
To add about destroy:
In models/post.rb
class Post < ApplicationRecord
...
accepts_nested_attributes_for :post_attachments, allow_destroy: true
end
In views/posts/_form.html.erb
<% f.object.post_attachments.each do |post_attachment| %>
<% if post_attachment.id %>
<%
post_attachments_delete_params =
{
post:
{
post_attachments_attributes: { id: post_attachment.id, _destroy: true }
}
}
%>
<%= link_to "Delete", post_path(f.object.id, post_attachments_delete_params), method: :patch, data: { confirm: 'Are you sure?' } %>
<br><br>
<% end %>
<% end %>
This way you simply do not need to have a child object's controller at all! I mean no any PostAttachmentsController is needed anymore. As for parent object's controller (PostController), you also almost don't change it - the only thing you change in there is the list of the whitelisted params (to include the child object-related params) like this:
def post_params
params.require(:post).permit(:title, :text,
post_attachments_attributes: ["avatar", "#original_filename", "#content_type", "#headers", "_destroy", "id"])
end
That's why the accepts_nested_attributes_for is so amazing.
Also I figured out how to update the multiple file upload and I also refactored it a bit. This code is mine but you get the drift.
def create
#motherboard = Motherboard.new(motherboard_params)
if #motherboard.save
save_attachments if params[:motherboard_attachments]
redirect_to #motherboard, notice: 'Motherboard was successfully created.'
else
render :new
end
end
def update
update_attachments if params[:motherboard_attachments]
if #motherboard.update(motherboard_params)
redirect_to #motherboard, notice: 'Motherboard was successfully updated.'
else
render :edit
end
end
private
def save_attachments
params[:motherboard_attachments]['photo'].each do |photo|
#motherboard_attachment = #motherboard.motherboard_attachments.create!(:photo => photo)
end
end
def update_attachments
#motherboard.motherboard_attachments.each(&:destroy) if #motherboard.motherboard_attachments.present?
params[:motherboard_attachments]['photo'].each do |photo|
#motherboard_attachment = #motherboard.motherboard_attachments.create!(:photo => photo)
end
end
Here is my second refactor into the model:
Move private methods to model.
Replace #motherboard with self.
Controller:
def create
#motherboard = Motherboard.new(motherboard_params)
if #motherboard.save
#motherboard.save_attachments(params) if params[:motherboard_attachments]
redirect_to #motherboard, notice: 'Motherboard was successfully created.'
else
render :new
end
end
def update
#motherboard.update_attachments(params) if params[:motherboard_attachments]
if #motherboard.update(motherboard_params)
redirect_to #motherboard, notice: 'Motherboard was successfully updated.'
else
render :edit
end
end
In motherboard model:
def save_attachments(params)
params[:motherboard_attachments]['photo'].each do |photo|
self.motherboard_attachments.create!(:photo => photo)
end
end
def update_attachments(params)
self.motherboard_attachments.each(&:destroy) if self.motherboard_attachments.present?
params[:motherboard_attachments]['photo'].each do |photo|
self.motherboard_attachments.create!(:photo => photo)
end
end
When using the association #post.post_attachments you do not need to set the post_id.

rails render action with anchor

i have a form with simple_form gem. if i digit the url in the browser with the anchor tag it works:
localhost:3000/contacts/#new_contact #show me the correct form in the page
this is my controller:
class ContactsController < ApplicationController
def index
#message = Contact.new()
end
def create
#message = Contact.new(msg_params)
#message.request = request
if #message.deliver
redirect_to contacts_path , flash: { success: 'Ok' }
else
render action: :index #how can i render the new_contact anchor??
end
rescue ScriptError
redirect_to contacts_path , flash: { error: 'spam' }
end
private
def msg_params
params.require(:contact).permit(:name,:email,:message,:spam)
end
end
this is my form:
<%= simple_form_for #message, defaults: { label: false} do |f| %>
<%= f.input :name %>
<%= f.input :email %>
<%= f.input :message, :as => :text,input_html: { rows: 7 } %>
<div class="hidden">
<%= f.input :spam, :hint => 'Leave this field blank!' %>
</div>
<%= f.button :submit, 'Send', class: 'btn-block btn-success btn-lg' %>
<% end %>
how can i show the new_contact anchor with the render method?
You can't use render to modify the anchor tag. In fact, render has nothing at all to do with the URI.
You can use existing links to link to "/url#anchor", redirect there, or use javascript to achieve the same effect.
You might also need to add :id => 'anchor' if the anchor itself isn't setup on the form.

Commenting model not saving and displaying :body section of comments

I'm new to rails and am having challenges adding a commenting system to my listings model. Effectively I have listings that are created by users, and I want the ability to allow other users to make comments on these listings.
What I have so far:
A listing model that includes:
has_many :comments
a comment model that includes:
belongs_to :listing
a comments controller:
class CommentsController < ApplicationController
def create
#listing = Listing.find(params[:listing_id])
#comment = #listing.comments.build(params[:body]) # ***I suspected that I needed to pass :comment as the params, but this throws an error. I can only get it to pass with :body ***
respond_to do |format|
if #comment.save
format.html { redirect_to #listing, notice: "Comment was successfully created" }
format.json { render json: #listing, status: :created, location: #comment }
else
format.html { render action: "new" }
format.json { render json: #comment.errors, status: :unprocessable_entity }
end
end
end
end
def comment_params
params.require(:comment).permit(:body, :listing_id)
end
and finally a listing view that includes the following code to collect and display comments:
<div class="form-group">
<%= form_for [#listing, Comment.new] do |f| %>
<%= f.label :comments %>
<%= f.text_area :body, :placeholder => "Tell us what you think", class: "form-control", :rows => "3" %>
<p><%= f.submit "Add comment", class: "btn btn-primary" %></p>
<% end %>
</div>
<%= simple_form_for [#listing, Comment.new] do |f| %>
<p>
<%= f.input :body, :label => "New comment", as: :text, input_html: { rows: "3" } %>
</p>
<p><%= f.submit "Add comment", class: "btn btn-primary" %></p>
<% end %>
The comment box is displaying correctly in the view, and I'm able to submit comments, however it appears that the :body isn't being saved, and therefore the "submitted x minutes ago" is the only thing that's showing up in my comments section.
Any ideas on what I could be doing incorrectly? I suspect it's a params issue, but haven't been able to work it out.
Thanks!
Since you are using the strong_parameters paradigm in Rails 4, I think you should change the comment creation line to this:
#comment = #listing.comments.build(comment_params)
And I'd change the listing finding line to this:
#listing = Listing.find(params.permit(:listing_id))
It should work fine, as long as you correctly whitelist all required params in the comment_params method.