I have a problem with file uploading on Heroku server.
(Also a hint about the right way of doing such type of things with rails would be greatly appreciated - I'm very new in RoR).
All this code is about uploading some CSV file, then allowing user to tweak couple of settings, and parse file after all. This usually work on localhost (several times I get troubles with value stored in session), but on Heroku it is always die on upload.
In one of the neighborhood questions was written that Heroku store file only during singe instance run, but I still couldn't find anything about this in Heroku's docs. Should I store file data in the db right after upload, so in such case it always be available? The downside - files could be pretty big, about 10-20Mb, it isn't looks nice.
Heroku logs say:
2012-05-21T19:27:20+00:00 app[web.1]: Started POST "/products/upload" for 46.119
.175.140 at 2012-05-21 19:27:20 +0000
2012-05-21T19:27:20+00:00 app[web.1]: Processing by ProductsController#upload
as HTML
2012-05-21T19:27:20+00:00 app[web.1]: Parameters: {"utf8"=>"тЬУ", "authenticit
y_token"=>"aqJFg3aqENfxS2lKCE4o4txxkZTJgPx36SZ7r3nyZBw=", "upload"=>{"my_file"=>
#<ActionDispatch::Http::UploadedFile:0x000000053af020 #original_filename="marina
-AutoPalmaPriceList_2011-07-30.txt", #content_type="text/plain", #headers="Conte
nt-Disposition: form-data; name=\"upload[my_file]\"; filename=\"marina-AutoPalma
PriceList_2011-07-30.txt\"\r\nContent-Type: text/plain\r\n", #tempfile=#<File:/t
mp/RackMultipart20120521-1-10g8xmx>>}, "commit"=>"Upload"}
2012-05-21T19:27:20+00:00 app[web.1]:
2012-05-21T19:27:20+00:00 app[web.1]: LoadError (no such file to load -- CSV):
2012-05-21T19:27:20+00:00 app[web.1]: app/controllers/products_controller.rb:8
2:in `upload'
2012-05-21T19:27:20+00:00 app[web.1]:
2012-05-21T19:27:20+00:00 app[web.1]:
2012-05-21T19:27:20+00:00 app[web.1]: cache: [POST /products/upload] invalidate,
pass
The code itself:
ProductsController:
def import
respond_to do |format|
format.html
end
end
def import_adjust
case params[:commit]
when "Adjust"
#col_default = params[:col_data]
#abort #col_default.to_yaml
#update csv reader with form data, restore filters from params
when "Complete"
#all ok, read the whole file
#abort params.to_yaml
redirect_to import_complete
else
#col_default = nil
end
#read first part of the file
#tmp = session[:import_file]
#csv = []
source = CSV.open #tmp, {col_sep: ";"}
5.times do
line = source.readline
if line.size>0
#line_size = line.size
#csv.push line
end
end
#generate a selection array
#selection = select_tag 'col_data[]', options_for_select([['name','name'], ['brand','brand'], ['delivery_time','delivery_time'], ['price','price']])
##csv = [selection * line_size] + #csv
end
def import_complete
#remove all items
#todo check products with line items will not be destroyed.
Product.destroy_all
#abort params.to_yaml
map = {}
cnt = 0
#todo check for params count.
params[:col_data].each do |val|
map[cnt] = val if val != 'ignore'
cnt += 1
end
source = CSV.open session[:import_file], {col_sep: ';'}
source.each do |row|
cnt += 1
if row.size > 0
item = Product.new
map.each do |col, attr|
item[attr] = row[col]
end
item[:provider_id] = params[:adjust][:provider]
item.save
#abort item.to_yaml
end
end
#abort map.to_yaml
#todo response needed.
end
def upload
require 'CSV' #looks like I dont need this in fact.
#tmp = params[:upload][:my_file].path #tempfile
#csv = []
#source = CSV.open #tmp, {col_sep: ";"}
session[:import_file] = params[:upload][:my_file].path
respond_to do |format|
format.html { redirect_to action: 'import_adjust' }
end
end
upload.html.erb:
<h1>Uploaded</h1>
<%= #tmp %>
<% #csv.each do |val| %>
<%= val %>
<% end %>
_form_import.html.erb:
<%= form_for :upload, :html => {:multipart => true}, :url => {action: "upload"} do |f| %>
<%= f.file_field :my_file %>
<%= f.submit "Upload" %>
<% end %>
import_adjust.html.erb:
<h1>New product</h1>
<%= form_for :adjust, :url => {action: "import_adjust"} do |f| %>
<% if #csv %>
<table>
<tr>
<% #line_size.times do |cnt| %>
<td>
<%= select_tag 'col_data[]',
options_for_select([
['--ignore--', 'ignore'],
['name','name'],
['brand','brand'],
['delivery_time','delivery_time'],
['price','price']
], #col_default!=nil ? #col_default[cnt] : nil) %>
</td>
<% end %>
</tr>
<% #csv.each do |val| %>
<tr>
<% val.each do |cell| %>
<td>
<%= cell %>
</td>
<% end %>
</tr>
<% end %>
</table>
<% end %>
<%= f.label :delimiter, 'Разделитель' %>
<%= f.text_field :delimiter %>
<br>
<%= f.label :provider, 'Поставщик' %>
<%#todo default empty option needed! Human mistakes warning! %>
<%= f.select :provider, Provider.all.collect { |item| [item.name, item.id] } %>
<br>
<%= f.label :delimiter, 'Разделитель' %>
<%= f.text_field :delimiter %>
<br>
<%# Adjust for proceed adjusting or Complete for parsing %>
<%= f.submit "Adjust" %>
<%= f.submit "Complete" %>
<% end %>
<%= link_to 'Back', products_path %>
Could you paste the entire controller code? The problem is on line #82, but I can't be 100% confident what line that is if you've stripped the class def and before_filters out.
That said, it looks like the problem is with one of the CSV.open lines. The way you're trying to set session[:import_file] is not guaranteed to work. If you ever run the app on more than one dyno you could have the first request served by your web.1 dyno and the second served by web.2, and they have different file systems and would not be able to see the same temp files.
I'd suggest one of the following:
Do all the processing immediately on the upload and avoid the re-direct.
An improvement on that would be to have the upload store the data somewhere shared and accessible (the database or S3) and have start a background job/process to do the processing.
Best of all would be to upload directly to S3 (I believe the S3 Uploader library can do this, there are probably others) and issue a callback to create a background job to process.
That last option means your web dynos are never tied up handling massive uploads and you don't burden the user with waiting for the latency involved in upload to server->store in S3->schedule background job, it is reduced simply to store in S3 from their perspective.
I have an identical scenario as Lifecoder where a user uploads a file, names the columns using a map_fields plugin (by Andrew Timberlake), and then the file is parsed and processed. Here's how I handle it:
file_field = params[options[:file_field]]
map_fields_file_name = "map_fields_#{Time.now.to_i}_#{$$}"
bucket = S3.buckets[CSV_COUPON_BUCKET_NAME] # gets an existing bucket
obj = bucket.objects[map_fields_file_name]
obj.write( file_field.read )
# Save the name and bucket to retrieve on second pass
session[:map_fields][:bucket_name] = map_fields_file_name
Then on the second pass to process the file, I open the file and read it back into temp for the dyno to process:
# Get CSV data out of bucket and stick it back into temp, so we pick up where
# we left off as far as map_fields is concerned.
bucket = S3.buckets[CSV_COUPON_BUCKET_NAME]
obj = bucket.objects[session[:map_fields][:bucket_name]]
temp_path = File.join(Dir::tmpdir, "map_fields_#{Time.now.to_i}_#{$$}")
File.open(temp_path, 'wb') do |f|
f.write obj.read
end
I had to use the plugin so I could modify the code, as obviously the gem is handled by Heroku and doesn't allow for modifications.
Related
I am having trouble implementing the DELETE action on any object that I have for my application.
To show you the general code for my delete action in the Group Controller:
def destroy
#group = Group.find(params[:id])
respond_to do |format|
if #group.delete
format.html { redirect_to group_path }
format.json { head :no_content }
else
format.html { redirect_to group_path }
format.json { head :no_content }
end
end
end
And the code for my View layout:
<h1>My Groups</h1>
<table>
<tr>
<th>Name</th>
<th>Users</th>
<th></th>
<th></th>
</tr>
<% #groups.each do |group| %>
<tr>
<td><%= group.name %></td>
<td>
<% group.memberships.each do |membership| %>
<%= User.find(membership.user_id).name %>
<br/>
<% end %>
</td>
<td><%= link_to 'Show', group %></td>
<td><%= link_to 'Destroy', group, :confirm => 'Are you sure?', :method => :delete %></td>
</tr>
<% end %>
</table>
<br />
<%= link_to 'New Group', new_group_path %>
I am unable to delete the group object even though the rails server log gives me back a 200 OK response. The returned page is a blank screen:
Started DELETE "/groups/2" for 127.0.0.1 at 2013-03-19 01:22:01 +0800
Processing by GroupsController#destroy as HTML
Parameters: {"authenticity_token"=>"aH+z0DlL7NeoVlxda8Td76WdnrH7/G8UyWDqbJcTu9w=", "id"=>"2"}
Completed 200 OK in 0ms (ActiveRecord: 0.0ms)
ActiveRecord wasn't used and there were no changes to the database. There were no such problems in the past, but I only realized that I could not delete any kind of object recently.
I have been trying to find solutions to similar problems found on the net, but unfortunately it seems like no one has had this problem yet.
Update 1:
The following is my application layout:
<!DOCTYPE html>
<html>
<head>
<%= csrf_meta_tag %>
<title>Home</title>
<%= javascript_include_tag :application %>
<%= stylesheet_link_tag :application, :media => "all" %>
<%= javascript_include_tag "/javascripts/main.js" %>
<%= stylesheet_link_tag "/css/main.css" %>
</head>
<body>
<%= yield %>
</body>
</html>
But when I use the console to delete the object. It works:
1.9.3-p194 :003 > Group.find(23).delete
Group Load (0.5ms) SELECT "groups".* FROM "groups" WHERE "groups"."id" = $1 LIMIT 1 [["id", 23]]
SQL (3.0ms) DELETE FROM "groups" WHERE "groups"."id" = 23
=> #<Group id: 23, name: "groupie", created_at: "2013-03-18 15:29:42", updated_at: "2013-03-18 15:29:42", user_id: nil>
I just found the solution to the question. I actually wrote a code for an around_filter that messed up the destroy session for every controller:
around_filter :clear_registration_id_on_destroy_session, only: :destroy
def clear_registration_id_on_destroy_session
is_filter_active = (controller_path == 'devise/sessions' && params[:action] == 'destroy')
if is_filter_active && user_signed_in?
if current_user.update_attribute(:device_platform, nil)
logger.info "Updated device platform attributes to nil"
end
if current_user.update_attribute(:registration_id, nil)
logger.info "Updated registration id attributes to nil"
end
yield
end
end
The problem is that I didn't yield anything for any other controller other than devise:sessions#destroy.
So for those who are as forgetful as me, please remember to think about the other conditions when is_filter_active is not true.
def clear_registration_id_on_destroy_session
is_filter_active = (controller_path == 'devise/sessions' && params[:action] == 'destroy')
if is_filter_active && user_signed_in?
if current_user.update_attribute(:device_platform, nil)
logger.info "Updated device platform attributes to nil"
end
if current_user.update_attribute(:registration_id, nil)
logger.info "Updated registration id attributes to nil"
end
yield
else
yield
end
end
It sounds like your delete link is just acting as a link to the show action, as the method option is not being picked up.
You need to have the appropriate javascript included in your application layout, as the delete link requires JS to use the correct HTTP method.
If you are using Rails >= 3.1 make sure you have the following in the header of the layout file you are using for rendering the page:
<%= javascript_include_tag :application %>
Also make sure you have the csrf_meta_tag in your layout:
<%= csrf_meta_tag %>
I am using the following code to upload the image in /public/uploads/ folder in my root rails directory.
uploaded_io = params[:product_image]
File.open(Rails.root.join('public','uploads', uploaded_io.original_filename), 'w') do |file|
file.write(uploaded_io.read)
end
My form looks like this
<%= form_tag({:action => :configure_product}, :multipart => true) do %>
<%= label_tag(:product_image, "Image:") %><br />
<%= file_field_tag 'product_image' %>
<%= submit_tag "Save and add another", :name => 'save and add another' %>
<%= submit_tag "Save", :name => 'save' %>
<% end %>
but when trying to submit the form I get the following error.
Encoding::UndefinedConversionError in ConfigureCategoryController#configure_product
"\xFF" from ASCII-8BIT to UTF-8
I replaced the writing mode from 'w' to 'wb' and now I am getting
NoMethodError in ConfigureCategoryController#configure_product
undefined method `name' for nil:NilClass
New at rails. Would surely appreciate the help.
You'll need to open the file as a binary file by appending b to the open type.
File.open("#{ Rails.root }/tmp/uploaded_image.gif", "wb") do |f|
The other issue you're having is specific to whatever it is your application does.
I try to update my settings through a form but the update function is not called when I submit. It redirects to edit_settings_path when I submit and as per serve log update is not called. Why?
<%= form_tag settings_path, :method => :put do %>
<p>
<%= label_tag :"settings[:default_email]", "System Administrator" %>
<%= text_field_tag :"settings[:default_email]", Settings['default_email'] %>
</p>
<span class="submit"><%= submit_tag "Save settings" %></span>
<% end %>
Controller
class SettingsController < ApplicationController
def update
params[:settings].each do |name, value|
Settings[name] = value
end
redirect_to edit_settings_path, :notice => "Settings have been saved." }
end
end
** Update **
Update is now called properly (edited controller). Server log confirms Settings Load (0.2ms) SELECT "settings".* FROM "settings" WHERE "settings"."thing_type" IS NULL AND "settings"."thing_id" IS NULL AND "settings"."var" = ':default_email' LIMIT 1
UPDATE "settings" SET "value" = '--- 1111aaa2222...', "updated_at" = '2011-12-18 21:03:21.782075' WHERE "settings"."id" = 2
However it doesn't save to the Db and have no clue why. I'm using the Rails-settings gem 'git://github.com/100hz/rails-settings.git'
Don't know where to check since it says it updated record but in fact no.
why are you using the form_tag method?
If you are just trying to make a standard update form, use:
<%= form_for(#settings) do |f| %>
FORM CODE
<%= end %>
Your controller uses the edit method to render the view and the update method for the calback (to interact with the model)
If you insist on using
<%= form_tag setting_path, :method => :put do %>
Normally you would use the singular word if you are working on a member and the plural if you are working on an collection.
fyi: I dont know what your design is like, but i would have a model settings and a model settings_item...
I am trying to pass along a local variable to a partial using link_to. I have tried many different things, but it doesn't seem to work. The js file loads the partial fine, it just doesn't have the locals. This is what I have, thanks for any direction!
_health.html.erb (this is a partial on index.html of Contacts model)
<% #comments = Comment.find_all_by_api(#api) %>
<%= link_to 'Read Comments', comments_path(:comments => #comments), :action => 'comments', :remote => true %>
comments.js.erb
$("#comments").html("<%= escape_javascript(render(:partial => 'comment', :locals => {:comments => :comments})) %>");
comment.html.erb
<% unless #comments.blank? %>
<% #comments.each do |c| %>
<%= c %><br />
<% end %>
<% end %>
contacts_controller.rb
def comments
respond_to do | format |
format.js {render :layout => false}
end
end
The partial does not know about the comments because you never set them. The comments action in the controller needs to look like this:
def comments
#comments = Comment.find(params[:id])
respond_to do | format |
(replace params[:id] with the appropriate parameter from your route)
You're doing an AJAX request and since http is stateless, the comments action does not know anything about any previous requests - which means that the comments from _health.html.erb have ceased to exist for the comments action in the controller.
I've installed devise for my rails app, i can go to the sign in page or the sign up page. But I want them both on a welcome page...
So I've made a welcome_page_controller.rb with the following function:
class WelcomePageController < ApplicationController
def index
render :template => '/devise/sessions/new'
render :template => '/devise/registration/new'
end
end
But when i go to the welcome page i get this error:
NameError in Welcome_page#index
Showing /Users/tboeree/Dropbox/rails_projects/rebasev4/app/views/devise/sessions/new.html.erb where line #5 raised:
undefined local variable or method `resource' for #<#<Class:0x104931c>:0x102749c>
Extracted source (around line #5):
2: <% #header_title = "Login" %>
3:
4:
5: <%= form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| %>
6: <p><%= f.label :email %><br />
7: <%= f.email_field :email %></p>
8:
Does anybody knows a solution for this problem? Thanks in advance!
Does it have to do with the fact that it is missing the resource function? in the welcome_page controller? It's probably somewhere in the devise controller...?
Regards,
Thijs
Here's how I managed to did it.
I've put a sign up form in my home#index
My files:
view/home/index.html.erb
<%= render :file => 'registrations/new' %>
helper/home_helper.rb
module HomeHelper
def resource_name
:user
end
def resource
#resource = session[:subscription] || User.new
end
def devise_mapping
#devise_mapping ||= Devise.mappings[:user]
end
def devise_error_messages!
return "" if resource.errors.empty?
messages = resource.errors.full_messages.map { |msg| content_tag(:li, msg) }.join
sentence = I18n.t("errors.messages.not_saved",
:count => resource.errors.count,
:resource => resource_name)
html = <<-HTML
<div id="error_explanation">
<h2>#{sentence}</h2>
<ul>#{messages}</ul>
</div>
HTML
html.html_safe
end
end
You need that part because Devise works with something called resource and it should be defined so you can call your registration#new anywhere.
Like that, you should be able to register. However, I needed to display errors on the same page. Here's what I added:
layout/home.html.erb (the layout used by index view)
<% flash.each do |name, msg| %>
# New code (allow for flash elements to be arrays)
<% if msg.class == Array %>
<% msg.each do |message| %>
<%= content_tag :div, message, :id => "flash_#{name}" %>
<% end %>
<% else %>
# old code
<%= content_tag :div, msg, :id => "flash_#{name}" %>
<% end %> #don't forget the extra end
<% end %>
I found this code here
And here's something I created: I saved my resource object if invalid in a session so that the user hasn't to fill every field again. I guess a better solution exists but it works and it's enough for me ;)
controller/registration_controller.rb
def create
build_resource
if resource.save
if resource.active_for_authentication?
# We delete the session created by an incomplete subscription if it exists.
if !session[:subscription].nil?
session[:subscription] = nil
end
set_flash_message :notice, :signed_up if is_navigational_format?
sign_in(resource_name, resource)
respond_with resource, :location => redirect_location(resource_name, resource)
else
set_flash_message :notice, :inactive_signed_up, :reason => resource.inactive_message.to_s if is_navigational_format?
expire_session_data_after_sign_in!
respond_with resource, :location => after_inactive_sign_up_path_for(resource)
end
else
clean_up_passwords(resource)
# Solution for displaying Devise errors on the homepage found on:
# https://stackoverflow.com/questions/4101641/rails-devise-handling-devise-error-messages
flash[:notice] = flash[:notice].to_a.concat resource.errors.full_messages
# We store the invalid object in session so the user hasn't to fill every fields again.
# The session is deleted if the subscription becomes valid.
session[:subscription] = resource
redirect_to root_path #Set the path you want here
end
end
I think I didn't forget any code. Feel free to use whatever you need.
Also, you can add your sign in form in the same page (something like that:)
<%= form_for("user", :url => user_session_path) do |f| %>
<%= f.text_field :email %>
<%= f.password_field :password %>
<%= f.submit 'Sign in' %>
<%= f.check_box :remember_me %>
<%= f.label :remember_me %>
<%= link_to "Forgot your password?", new_password_path('user') %>
<% end %>
Cheers !