I have a form with multiple file uploads, The issue is when i am submitting the form and an validation error occurs, the file input field gets reset.
I basically wanted to persist those files inside the file input field for the complete process.
I have also gone through few links
How can I "keep" the uploaded image on a form validation error?
Please let me know what are the various options in such cases that one can follow.
Carrierwave is a great tool for handling file uploads and can handle this for you
https://github.com/jnicklas/carrierwave#making-uploads-work-across-form-redisplays
I took a completely different approach to the other solutions on offer here, as I didn't fancy switching to CarrierWave or using yet another gem to implement a hack to get around this.
Basically, I define placeholders for validation error messages and then make an AJAX call to the relevant controller. should it fail validation I simply populate the error message placeholders - this leaves everything in place client side including the file input ready for resubmission.
Example follows, demonstrating an organisation with nested address model and a nested logo model (that has a file attachment) - this has been cut for brevity :
organisations/_form.html.erb
<%= form_for #organisation, html: {class: 'form-horizontal', role: 'form', multipart: true}, remote: true do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<p class='name error_explanation'></p>
<%= f.fields_for :operational_address do |fa| %>
<%= fa.label :postcode %>
<%= fa.text_field :postcode %>
<p class='operational_address postcode error_explanation'></p>
<% end %>
<%= f.fields_for :logo do |fl| %>
<%= fl.file_field :image %>
<p class='logo image error_explanation'></p>
<% end %>
<% end %>
organisations_controller.rb
def create
if #organisation.save
render :js => "window.location = '#{organisations_path}'"
else
render :validation_errors
end
end
organisations/validation_errors.js.erb
$('.error_explanation').html('');
<% #organisation.errors.messages.each do |attribute, messages| %>
$('.<%= attribute %>.error_explanation').html("<%= messages.map{|message| "'#{message}'"}.join(', ') %>");
<% end %>
Created a repo with a example of using Paperclip on rails and mainting your files when validation error occurs
https://github.com/mariohmol/paperclip-keeponvalidation
I had to fix this on a recent project using the Paperclip Gem. It's a bit hacky but it works. I've tried calling cache_images() using after_validation and before_save in the model but it fails on create for some reason that I can't determine so I just call it from the controller instead. Hopefully this saves someone else some time!
model:
class Shop < ActiveRecord::Base
attr_accessor :logo_cache
has_attached_file :logo
def cache_images
if logo.staged?
if invalid?
FileUtils.cp(logo.queued_for_write[:original].path, logo.path(:original))
#logo_cache = encrypt(logo.path(:original))
end
else
if #logo_cache.present?
File.open(decrypt(#logo_cache)) {|f| assign_attributes(logo: f)}
end
end
end
private
def decrypt(data)
return '' unless data.present?
cipher = build_cipher(:decrypt, 'mypassword')
cipher.update(Base64.urlsafe_decode64(data).unpack('m')[0]) + cipher.final
end
def encrypt(data)
return '' unless data.present?
cipher = build_cipher(:encrypt, 'mypassword')
Base64.urlsafe_encode64([cipher.update(data) + cipher.final].pack('m'))
end
def build_cipher(type, password)
cipher = OpenSSL::Cipher::Cipher.new('DES-EDE3-CBC').send(type)
cipher.pkcs5_keyivgen(password)
cipher
end
end
controller:
def create
#shop = Shop.new(shop_params)
#shop.user = current_user
#shop.cache_images
if #shop.save
redirect_to account_path, notice: 'Shop created!'
else
render :new
end
end
def update
#shop = current_user.shop
#shop.assign_attributes(shop_params)
#shop.cache_images
if #shop.save
redirect_to account_path, notice: 'Shop updated.'
else
render :edit
end
end
view:
= f.file_field :logo
= f.hidden_field :logo_cache
- if #shop.logo.file?
%img{src: #shop.logo.url, alt: ''}
Well - I thought of taking a different approach to this; Instead of temporarily storing the file on the server, why not serve it back to the client to be resubmitted when the user fixes the validation issues.
This might still need a bit of refinement but it's the general concept:
# in the controller - save the file and its attributes to params
def create
# ...
if params[:doc] # a regular file uploaded through the file form element
# when the form re-renders, it will have those additional params available to it
params[:uploaded_file] = params[:doc].read # File contents
params[:uploaded_file_original_filename] = params[:doc].original_filename
params[:uploaded_file_headers] = params[:doc].headers
params[:uploaded_file_content_type] = params[:doc].content_type
elsif params[:uploaded_file] # a file coming through the form-resubmit
# generate an ActionDispatch::Http::UploadedFile
tempfile = Tempfile.new("#{params[:uploaded_file_original_filename]}-#{Time.now}")
tempfile.binmode
tempfile.write CGI.unescape(params[:uploaded_file]) #content of the file / unescaped
tempfile.close
# merge into the params
params.merge!(doc:
ActionDispatch::Http::UploadedFile.new(
:tempfile => tempfile,
:filename => params[:uploaded_file_original_filename],
:head => params[:uploaded_file_headers],
:type => params[:uploaded_file_content_type]
)
)
end
#...
# params (including the UploadedFile) can be used to generate and save the model object
end
# in the form (haml)
- if !params[:uploaded_file].blank?
# file contents in hidden textarea element
= text_area_tag(:uploaded_file, CGI.escape(params[:uploaded_file]), style: 'display: none;') #escape the file content
= hidden_field_tag :uploaded_file_headers, params[:uploaded_file_headers]
= hidden_field_tag :uploaded_file_content_type, params[:uploaded_file_content_type]
= hidden_field_tag :uploaded_file_original_filename, params[:uploaded_file_original_filename]
A workaround for this rather than an outright solution is to use client side validation so that the file isn't lost because the whole form persists.
The few users that don't have JavaScript enabled will lose the files between requests, but perhaps this % is so low for you as to make it an acceptable compromise. If this is the route you decide to go down I'd recommend this gem
https://github.com/bcardarella/client_side_validations
Which makes the whole process really simple and means you don't have to rewrite your validation in JavaScript
Browsers block against setting the value attribute on input of file
type for security reasons so that you can't upload a file without the
user's selected any file itself.
Pre-Populate HTML form file input
You can use carrierwave: https://github.com/carrierwaveuploader/carrierwave
Or validate the model via js request.
I found a way to keep files without using gems, it can probably be improved but I am still a young dev :)
I draw the whole thing from solution 2 contained in this article: https://medium.com/earthvectors/validations-and-file-caching-using-activestorage-e16418060f8f
First of all, you need to add an hidden_field within your form that contains the signed_id of the attachment:
<%= f.hidden_field :file_signed_id, value: #model.file.signed_id if #model.file.attached? %>
<%= f.input :file %>
The problem when validations fail (in my case), it keeps the file in memory but do not send anymore the ActionDispatch object as parameters. To override it, I did the following in my controller:
if file = params.dig(:model, :file)
blob = ActiveStorage::Blob.create_and_upload!(
io: File.open(file.tempfile),
filename: file.original_filename
)
#model.file.attach(blob)
elsif file_signed_id = params.dig(:model, file_signed_id)
blob = ActiveStorage::Blob.find_signed(file_signed_id)
#model.file.attach(blob)
end
You then can display your file when rendering your view again:
<%= link_to #model.file.filename, url_for(#model.file) if #model.file.attached? %>
The only problem I see with this workaround it is that it will create a blob object even if validations fail.
I hope it will help someone!
Related
I'm trying to refactor some code out of my rails controller to my model, and I've discovered a gap in my understanding of how rails works. I'm attempted to make the 2 methods available to the Raffle class, and I'm struggling to understand how to do so. When I hit this code, the error "undefined local variable or method `params' for #Raffle:0x00007f88c9af8940" is returned.
How can I work around params not being available to the model? Apologies if this is a beginner question- I am definitely a beginner.
#app/models/raffle.rb
class Raffle < ApplicationRecord
has_many :tickets
has_many :users, through: :tickets
def bid_tickets(tier)
params[:raffle][:"tier"].to_i #error returned here
end
def bid_total
(bid_tickets(gold) * 3) + (bid_tickets(silver) * 2) + bid_tickets(bronze)
end
#app/views/edit.html.erb
<%= form_for(#raffle) do |f| %>
<%=f.label :ticket, "Gold" %>: <br>
<%= image_tag "gold.jpg", class: "small_ticket" %><br>
<%=f.number_field :gold%><br>
<%=f.label :ticket, "Silver" %>:<br>
<%= image_tag "silver.jpg", class: "small_ticket" %><br>
<%=f.number_field :silver %><br>
<%=f.label :ticket, "Bronze" %>:<br>
<%= image_tag "bronze.jpg", class: "small_ticket" %><br>
<%=f.number_field :bronze %> <br><br>
<%= f.submit "Use Tickets" %>
<% end %><br>
#app/controllers/raffles_controller.rb
def update
if #raffle.enough_slots? + #raffle.current_bids > #raffle.number_of_ticket_slots
if enough_tickets?
redirect_to edit_raffle_path, notice: "You do not have enough tickets."
else
redirect_to edit_raffle_path, notice: "There aren't enough spots left in this raffle to handle your entry! Please use less tickets."
end
else #raffle.update_tickets(current_user)
if #raffle.slots_filled?
#raffle.select_winner
end
redirect_to edit_raffle_path(#raffle)
end
end
returned parameters:
{"_method"=>"patch",
"authenticity_token"=>"xyz",
"raffle"=>{"gold"=>"1", "silver"=>"", "bronze"=>""},
"commit"=>"Use Tickets",
"id"=>"1"}
EDIT:
#app/controllers/raffles_controller.rb (StrongParameters)
class RafflesController < ApplicationController
private
def raffle_params
params.require(:raffle).permit(:product_name, :product_description,
:product_image, :number_of_ticket_slots, :filter)
end
end
params is made available to you in the Controller (by Rails) because it's a "web request thing". It's a good practice not to pass it deeper into your system, as it contains external "untrusted" input. That's why it's not automatically available in Models.
For example:
# controller
def update
safe_value = validate_value(params[:unsafe_value]) # extract and validate
# no "web request things" passed in
result = MyFeature.do_stuff(#current_user, safe_value) # pass it to other parts of system
# then convert the result back into a "web request thing", e.g:
# return render json: as_json(result) # return result as json
# return redirect_to endpoint_for(result) # redirect based on result
# etc
end
It's good practice to pre-process the params (extracting the values out, validating them, etc) before passing that data to other parts of your system as arguments, like your models.
This will keep the rest of your system agnostic of "web request things", keeping them purpose-focused and well-organized.
For one, StrongParameters is a built-in rails feature that helps with this.
Another suggestion would be put a binding.pry or byebug (after making sure it's installed and updated via bundle) in code where params are utilized and then run your code. After it's triggered type params and you will see the breakdown.
2 questions really:
1) I am trying to save a note that belongs to the Client model (which also is used as login to create the session - therefore session[:id] = #client.id)
2)The note is an image, uploaded using ActiveRecord model.
Post request seems to be working fine but the note doesn't save. I am trying to only save the image first and client.id reference at first. The rest of the columns in the model will be filled in later using edit.
db schema contains the 2 ActiveRecord tables.
There seems to be no errors coming up but no new note is created (#note.save = false), which could be because currently nothing is being saved to a new note, just a new blob... in cmd you can see blob creation insert works fine, and then the next transaction is rolledback.
How would I add the logic to save #client.id in the note?
Below my code snippets...
the form for from the view:
<div class="image-upload">
<h3>Upload image of sick note</h3>
<div class="signup-form">
<%= form_for(#note) do |f| %>
<%= f.file_field :image %>
<%= f.submit "Load Sick Note", class: "btn-submit" %>
<% end %>
</div>
</div>
The create in the controller:
def create
#note = Note.new(note_params)
if #note.save
redirect_to '/'
else
redirect_to '/dashboard'
end
end
private
def note_params
params.require(:note).permit(:image)
end
Note model:
class Note < ApplicationRecord
belongs_to :client
has_one_attached :image
end
Instead of
#note = Note.new(note_params)
try
#note = Client.new.notes.build(note_params)
the .notes method (i.e. "Client.new.notes" ) comes from assuming you have a has_many :notes declaration in your Client model.
If you are only using a has_one :note declaration in your Client model then the .build() method will not work.
I'm trying to build an app in Rails which will take audio file uploads and read metadata off them to populate a database. I'm using the Taglib-ruby gem to handle various file types. The uploads seem to be working on their own, but Taglib considers any file given to it as null.
Here's my controller:
class UploadsController < ApplicationController
require 'taglib'
def new
end
def create
file = params[:upload]
TagLib::FileRef.open(file) do |fileref|
unless fileref.null?
tag = fileref.tag
# properties = fileref.audio_properties
#song = Song.new(title: tag.title, artist: tag.artist, album: tag.album,
year: tag.year, track: tag.track, genre: tag.genre)
if #song.save
redirect_to songs_path
else
render 'new'
end
else
raise "file was null"
end
end
end
end
and my view for form submission:
<h1> Upload </h1>
<%= form_tag(url: { action: :create }, html: { multipart: true }) do %>
<%= label_tag :upload, "Scan your song:" %>
<%= file_field_tag :upload, multiple: true %>
<br />
<%= submit_tag "Submit" %>
<% end %>
Taglib itself seems to be working - adding "require 'taglib'" removed the error I had been getting in regards to that, and a mock-up I made of this outside of rails worked fine (so the files I'm using are also not the problem). Every time I run this, the control flow hits my raise command, and no record is saved. It's clear that fileref.null? is returning true, which suggests to me that there's something wrong with the upload process... but I'm not sure what.
Ideally, I'd like to use the multiple uploads option and run this process on each file sequentially, but I can't even get a single upload to register as anything but null.
I have little experience in rails and web-development in general. I think what I have here is bad design from what I've been studying . I don't think I'm using a rails way of doing this since this is an upload file functionality, how can I make it like the familiar CRUD style. I created a custom controller called FileUploadController. I also have a custom class called FileProcessorService
User will navigate to the upload page which has the multipart form button to select and upload the file. The import action is what gets called when the submit button on the multipart form is clicked.
<div class="row">
<h1>Upload File</h1>
<%= form_tag({action: :import}, multipart: true) do %>
<%= file_field_tag :file %>
<%= submit_tag("Upload and Tabulate", :class => 'button', id: 'upload_button') %>
<% end %>
Can this be railsified? How can I make this design like a familiar CRUD like controller? It wasn't exactly obvious to me on how to do that since this is upload file functionality. Thank you in advance.
class FileUploadController < ApplicationController
def index
end
def import
initialize_processor(params[:file])
if (#file_sample != nil || #index_name != nil) then
render 'index'
end
end
def upload
end
def initialize_processor(file_in)
File.open(Rails.root.join('public', 'uploads', file_in.original_filename), 'wb') do |file|
file.write(file_in.read)
end
#file_processor = FileProcessorService.new(file_in)
#file_sample = #file_processor.present_data_sample()
#index_name = #file_processor.load_index()
end
end
I have the following model:
class Activity < ActiveRecord::Base
has_many :clientships, :dependent => :destroy, :after_add => :default_client_info
accepts_nested_attributes_for :clientships, :allow_destroy => true
end
In my controller, if I perform the following
def new
#activity = IndividualActivity.new(params[:activity])
#activity.clientships.build(:client => Client.first)
...
end
and then save the form, it creates the relevant params and submits successfully.
However, if I chose to call the following through a remote link
#activity.clientships.build(:client => Client.last)
the view is updated with the new clientship record but when I submit the form, the params[:activity] is not created for the second nested attribute. (Why not!?)
This is the view:
%h1 Create a new Activity
- form_for #activity do |f|
%div
= render "activities/client_selector", :f => f
%div
= f.submit "Save!"
Here is the remote_link's controller action
def add_client
#activity = IndividualActivity.new(session[:individual_activity])
# Refresh client
#activity.clientships.build(:client => Client.find(params[:client_id]))
respond_to do |format|
format.js
end
end
This is the add_client.html.js:
page.replace_html "selected_clients", :partial => 'activities/clients'
This is the activities/clients partial:
- form_for #activity do |f|
- f.fields_for :clientships do |client_f|
%tr
%td= client_f.hidden_field :client_id
%td= client_f.object.client.full_name
Does anyone know how I can troubleshoot this further? I seem to have come to a dead-end with my debugging... One thing to note, there is a double use of the following form_for used in new.html.haml and the activities/clients partial (is this problematic?)
- form_for #activity do |f|
I am on rails v2.3.5
Thanks
You ask about debugging, so the first step may be looking at the server log (log/development.log).
There you should see the "params" hash.
Maybe your params contain "activity"=>{"client_id"=>..} instead of "client_id"=>.. ?
Also look at the generated HTML page - use a Firebug or just use a "view source" method of your browser. Look, especially, for input names.
If everything looks OK, put a few debug calls in your action, and look at the development.log for some database activity - do the SQL queries look like they are doing what you want?
In your question there is no 'save' method. The 'build' method does NOT save the created record. Maybe this is your problem?
def add_client
logger.debug "Creating Activity"
#activity = IndividualActivity.new(session[:individual_activity])
logger.debug "Building clientship"
# Refresh client
#activity.clientships.build(:client => Client.find(params[:client_id]))
logger.debug "#activity = #{#activity.inspect}"
# Maybe you were missing this part of code?
logger.debug "Saving #activity"
#activity.save! # use a ! to easily see any problems with saving.
# Remove in production and add a proper if
logger.debug "Saved. #activity = #{#activity.inspect}"
respond_to do |format|
format.js
end
end
You should create a functional test (in case you haven't already) and ensure that if you send proper parameters, your action works as intended.
The test will narrow your search. If the test fails, you know you have a problem in the action. If the test is OK, you need to ensure the parameters are sent properly, and you probably have the problem in your view.
UPDATE:
You said you have TWO forms on the page. This may be the problem, since only one form may be sent at a time. Otherwise it would need to work in a way which can send two requests in one request.
First thing (useful in all similar problems): validate whether your page has correct HTML structure - for example http://validator.w3.org would be a good start. Try to make the code validate. I know that some people treat a "green" status as a unachievable mastery, but just it's really not so hard. With valid code you may be sure that the browser really understands what you mean.
Second: Place all your inputs in a single form. You have problems with nested attributes. For start, try to manually insert inputs with name like <input name="activity[clientship_attributes][0][name]" value="John"/>, and for existing clientships ensure that there is an input with name = activity[clientship_attributes][0][id].
This is the way nested attributes are handled.
Your view may create such fields automagically. This construction should be what you need: (it worked in one of my old project in rails 2.x, I have just replaced the names with ones you use)
<% form_for(#activity) do |f| %>
<p><%= f.text_field :activity_something %></p>
<% #activity.clientships.each do |clientship| %>
<% f.fields_for :clientships, clientship do |cform| %>
<p><%= cform.text_field :name %></p>
<p><%= cform.text_fiels :something %></p>
<% end %>
<% end %>
<% end %>
If you really want to use a partial there, don't create a new form in the partial. Use only the parts of above code.
To pass a variable to the partial, use :locals attribute in the place where you call render :partial:
<%= render :partial => 'clientship', :locals => {:form => f} %>
Then, in your partial, you may use a local variable form where you would use f outside of the partial. You may, of course, map the variables to the same name: :locals => {:f => f}