How to update the database when users download an ActiveStorage blob attachment? - ruby-on-rails-5

Currently users can download an ActiveStorage blob in my app using the following link:
link_to 'download', rails_blob_path(pj.document.file, disposition: 'attachment')
However, I would like to update an attribute in the database for the associated model to register when the file was first downloaded. This field is called the downloaded_at field.
I have made the following attempt:
Changed the link_to > button_to as I'm updating the model.
Added the appropriate route
Added the following code in the database:
def download
#proofreading_job = ProofreadingJob.find(params[:id])
#proofreading_job.update(downloaded_at: Time.current) if current_user == #proofreading_job.proofreader.user
response.headers["Content-Type"] = #proofreading_job.document.file.content_type
response.headers["Content-Disposition"] = "attachment; #{#proofreading_job.document.file.filename.parameters}"
#proofreading_job.document.file.download do |chunk|
response.stream.write(chunk)
end
ensure
response.stream.close
end
However, this does not do anything except redirect to the #proofreading_job page which is not what I want.
Has anyone done this before and if so how can I accomplish this task.

I think you can also try using your action controller as a proxy, the concept is this:
download the file in your action
check if it is downloaded successfully and other validations
perform clean up operations (in your case the added code in your #3)
send the file back to user using the send_data/send_file rendering method
E.g. in your controller:
def download
file = open(params[:uri])
validate!
cleanup!
send_file file.path
end
Then in your view:
link_to 'download', your_controller_path
Above is just concept and I apologize for only providing pseudo code in advance.

In the end I just used some javascript to capture the click of the button as follows:
td = link_to rails_blob_path(pj.document.file, disposition: 'attachment'),
id: pj.document.id,
download: pj.document.file_name,
class: "btn btn-outline-secondary btn-sm btn-download" do
=pj.document.file_name
i.fa.fa-download.ml-3 aria-hidden="true"
coffee script:
$('.btn-download').on 'click', (e) ->
id = $(this).attr('id')
$.ajax {url: Routes.document_path(id), type: 'PUT'}
routes.rb
resources :documents, only: [:show, :update]
documents_controller.rb:
def update
document = Document.find(params[:id])
authorize([:proofreaders, document])
document.update(downloaded_at: Time.current) if document.downloaded_at.nil?
head :ok
end
This seems to work very well. It updates the database and the user gets the file downloaded to their computer.

Related

Why is this FactoryGirl-created object visible in the test, but not in the view that is being tested?

I'm using RSpec, FactoryGirl, and PhantomJS.
UPDATE:
I have verified that if I create this item in my spec/support/login_macros.rb login_user method, which I call below, the listing object is available in the view.
This seems like a FactoryGirl issue. Why can I create from a factory in that helper method, but can't inside the test itself? Here is the support method:
module LoginMacros
def login_user(admin = false)
myrepo = FactoryGirl.create(:repository, :myname)
plan = FactoryGirl.create(:plan, name: "Test Multi", listing_limit: 5000, repositories_allowed: 5)
user = FactoryGirl.create(:user)
subscription = FactoryGirl.create(:subscription, user: user, plan: plan)
user.add_repository(myrepo)
listing = FactoryGirl.create(:listing, user: user, repository: myrepo)
visit login_path
fill_in 'Email', :with => user.email
fill_in 'Password', :with => user.password
click_button 'Go'
user
end
I have set transactional_fixtures to false in my spec_helper.rb. Here is my spec:
require "spec_helper"
describe "App Integration" do
let!(:user) { login_user }
let!(:myrepo) { user.repositories.where(name: "myrepo" ).first }
it "lets a user add apps to a listing", js: true do
listing = FactoryGirl.create(:listing, user: user, repository: myrepo)
puts listing.inspect
Capybara::Screenshot.screenshot_and_open_image
end
end
Now here is the problem. See that puts line above? It prints out the object.
But in the screenshot, it's as if the object were never created. Like magic!
And yet, both the User and the Repository objects are visible in the view.
Also, I can go to a different view and see that Listing object. Just not on the main page of my application!
Why would that object be visible in one view and not the other? I'm just doing this on the main page:
<h3><%= Listing.count %></h3>
And it is always, always, always zero. Makes zero sense.
It doesn't look like you're ever calling visit inside of your test, so the page would always be blank. You need to call visit root_path # (or whatever path that heading is on) before saving the screenshot.
let! blocks are invoked before it blocks. Since you're viewing the page in a let! block but creating the listing in the it block, the listing isn't created until after you've viewed the page.

Passing a CSV file to rspec as a param

I have the following function in my users_controller to upload a csv file from local disk and export the user details in it into my application. The code is working correctly in the application but I am unsure as to how about passing the csv in the test. My function is as follows:
def upload
if request.post?
if (params[:user].nil? || params[:user][:csv].blank?)
flash[:notice] = "Please provide a csv file to upload."; return
end
file = params[:user][:csv].read
CSV.parse(file, headers: true, header_converters: :symbol).each { |row| Cortex::User.create(row.to_hash) }
respond_to do |format|
format.html { redirect_to admin_engine.cortex_users_path }
format.json { head :no_content }
end
else
# Provide upload view
end
end
Here is my attempt at trying to cover this in the rspec tests.
it "should be a success" do
post "admin/users/upload", :user => #user, :user_csv => fixture_file_upload(Admin::Engine.root.join('spec/dummy/tenants_sample.csv')), :session_token => #auth_token
response.code.should eq("200")
end
When I check the LOC coverage with the coverage gem I can see that the test enters the first two if statements before exiting.
Anyone got any tips on how I could go about passing this file in so that it could then be read by the rest of the function.
Cheers
IMHO, testing CSV composition or parsing doesn't belong in your current test. Ultimately, all such a test will do is test either the CSV module or the validity of your CSV file--which is a brittle test and tells you very little about your application.
A better testing practice would be to perform a test of the model to ensure that:
A properly-formatted CSV is imported the way you expect.
Malformed CSV files are handled appropriately for your application.
The controller should just stub or mock the CSV parsing because it's not relevant to the response code test, and the logic really belongs in the model anyway. YMMV.

Error when saving a Backbone.js model with Rails

I have Backbone.js collection and model for a project object:
window.Project = Backbone.Model.extend();
window.Projects = Backbone.Collection.extend({
model: Project,
url: '/projects'
});
I have setup a rails controller to respond to the Backbone.js collection:
class ProjectsController < ApplicationController
def index
render :json => Project.all
end
def create
project = Project.create! params
render :json => project
end
end
Index works fine and I get a list of projects in my web app. The problem is if I try and create a model on the Projects collection I get a 500 error from the server.
The error message on the server is as follows:
Started POST "/projects" for 127.0.0.1 at 2011-08-21 08:27:56 +0100
Processing by ProjectsController#create as JSON
Parameters: {"title"=>"another test"}
Completed 500 Internal Server Error in 16ms
ActiveRecord::UnknownAttributeError (unknown attribute: action):
app/controllers/projects_controller.rb:8:in `create'
I am not sure what the unknown attribute: action is referring to.
For info I have set up the projects_controller as resources :projects. I have also set rails to ActiveRecord::Base.include_root_in_json = false.
Yes, Rails always adds the action and controller to params. The parameters come from ActionDispatch::Http::Parameters:
def parameters
#env["action_dispatch.request.parameters"] ||= begin
params = request_parameters.merge(query_parameters)
params.merge!(path_parameters)
encode_params(params).with_indifferent_access
end
end
And path_parameters:
Returns a hash with the parameters used to form the path of the request. Returned hash keys are strings:
{'action' => 'my_action', 'controller' => 'my_controller'}
So you shouldn't be doing project = Project.create! params. You could go the update_attributes route:
project = Project.new
project.update_attributes params[:model_name]
But this assumes that you have what you need in a sub-hash of params and it won't call your validators. Backbone won't namespace your attributes by default but you could override Backbone.sync and do it yourself. Still, you probably want your validations so update_attributes should generally be avoided.
Your best bet is to pull exactly the attributes out of params that you're expecting to be there. This is even the Backbone recommended practise:
*(In real code, never use update_attributes blindly, and always whitelist the attributes you allow to be changed.)*
You can enable parameter wrapping. Add a file in the initializer directory with:
ActiveSupport.on_load(:action_controller) do
wrap_parameters format: [:json]
end
and, for json request, you post params will now be wrapped with the model name.

image_tag in mailer not using asset_host

image_tag isn't using the asset_host I've set. Any ideas why? The only thing I can think of is it having to do with it being a Mailer.
config/environment/development.rb
config.action_controller.asset_host = "http://localhost:3000"
myMailer.rb
<%= image_tag "logo.png", :style=>"margin-left:10px; padding-bottom:15px;" %>
rendered as:
<img alt="Logo" src="/images/logo.png?1303090162" style="margin-left:10px; padding-bottom:15px;" />
In console:
> MyApp::Application.config.action_controller
#<OrderedHash {… :asset_host=>"http://localhost:3000", …}>
I need the image_tag to create a full path url because it will be showing up in an email.
I was wrong before. This is the solution you need (until rails 3.1 where the asset_host configurations become unified):
config.action_mailer.asset_host = "http://localhost:3000"
We need to specify both config.action_controller.asset_host and config.action_mailer.asset_host, on Rails 3.1 and 3.2.
To add the hostname to the image_tag on both e-mail and non-email views, add the following to your environment file:
config.action_controller.asset_host = 'http://localhost:3000'
config.action_mailer.asset_host = config.action_controller.asset_host
Where 'http://localhost:3000' should be replaced by your host URL (and port if applicable).
This needs to be set on both action_controller and action_mailer, even in Rails 3.2.x.
The offending code as to why you can't do it is here:
# actionpack/lib/action_view/helpers/asset_paths.rb, line 27
def compute_public_path(source, dir, ext = nil, include_host = true)
# More code up here....
if controller && include_host
has_request = controller.respond_to?(:request)
source = rewrite_host_and_protocol(source, has_request)
end
end
Here is the offending file on GH: https://github.com/rails/rails/blob/master/actionpack/lib/action_view/helpers/asset_paths.rb
Since an ActionMailer View template lacks a Controller, you don't get the command to rewrite based on an asset_host. This should probably be a ticket opened to the Rails core team.
You can try the following config and see if it helps:
config.action_mailer.default_url_options = {:host=>"localhost", :port=>3000, :protocol=>"http://"}
I'm pretty sure it's only going to work for url_for though.

Testing authenticated file uploads in merb

This is something that has been driving me mad over the past few days. I have an action which allows authenticated users to upload assets to the site. I know that the controller action is correct as I can run through the process manually however I want to test it using rspec.
I have to use the request helper so I can reuse an authenticated session which is a :given for this set of tests.
it "should allow authenticated file uploads" do
file = File.open(a_valid_file)
mock_file = mock("file")
mock_file.stub!(:path).and_return(file.path)
request( resource(:assets), :method => "POST",
:params => { :file =>
{:tempfile => mock_file, :filename => File.basename(file.path)} }
)
end
If I breakpoint inside the spec it all works nicely, however when I run the spec and try to access the path in the controller action through the debugger I get this:
e file[:tempfile].path
NoMethodError Exception: undefined method `path' for "#[Spec::Mocks::Mock:0x3fda2a4736c0 #name=\"file\"]":String
My guess is that the stub!(:path) is not being set for whatever mock object is making it through the request.
The question is: Am I going about the right way for testing file uploads and if not what is another way?
I was doing it wrong. By using request it was calling to_s on all paramaters, so my mock object was being passed as "#[Spec::Mocks::Mock:0x3fda2a4736c0 #name=\"file\"]". That will teach me to pay more attention to exception output.
Instead I should use multipart_post and stub out the authentication calls in a block.
it "should allow authenticated file uploads" do
file = File.open(a_valid_file)
multipart_post( resource(:assets), :method => "POST",
:params => { :file => file } ) do |controller|
controller.stub!(:ensure_authenticated).and_return(true)
controller.session.stub!(:user).and_return(User.first)
)
end