Skip to content

Commit

Permalink
Merge pull request #10661 from mkllnk/report-download
Browse files Browse the repository at this point in the history
[Hidden] Provide download link for reports generated in the background
  • Loading branch information
filipefurtad0 authored Apr 19, 2023
2 parents a409d3b + 43cbac7 commit f206b7e
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 52 deletions.
20 changes: 14 additions & 6 deletions app/controllers/admin/reports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

module Admin
class ReportsController < Spree::Admin::BaseController
include ActiveStorage::SetCurrent
include ReportsActions
helper ReportsHelper

Expand All @@ -21,7 +22,7 @@ def index
def show
@report = report_class.new(spree_current_user, params, render: render_data?)

if report_format.present?
if params[:report_format].present?
export_report
else
show_report
Expand Down Expand Up @@ -57,23 +58,30 @@ def render_data?

def render_report_as(format)
if OpenFoodNetwork::FeatureToggle.enabled?(:background_reports, spree_current_user)
job = ReportJob.perform_later(
report_class, spree_current_user, params, format
@blob = ReportBlob.create_for_upload_later!(report_filename)
ReportJob.perform_later(
report_class, spree_current_user, params, format, @blob
)
Timeout.timeout(max_wait_time) do
sleep 1 until job.done?
sleep 1 until @blob.content_stored?
end

# This result has been rendered by Rails in safe mode already.
job.result.html_safe # rubocop:disable Rails/OutputSafety
@blob.result.html_safe # rubocop:disable Rails/OutputSafety
else
@report.render_as(format)
end
end

def render_timeout_error
assign_view_data
@error = ".report_taking_longer"
if @blob
@error = ".report_taking_longer_html"
@error_url = @blob.url
else
@error = ".report_taking_longer"
@error_url = ""
end
render "show"
end

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/concerns/reports_actions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def ransack_params
end

def report_format
params[:report_format]
params[:report_format].presence || "html"
end

def report_filename
Expand Down
32 changes: 4 additions & 28 deletions app/jobs/report_job.rb
Original file line number Diff line number Diff line change
@@ -1,34 +1,10 @@
# frozen_string_literal: true

# Renders a report and saves it to a temporary file.
class ReportJob < ActiveJob::Base
def perform(report_class, user, params, format)
# Renders a report and stores it in a given blob.
class ReportJob < ApplicationJob
def perform(report_class, user, params, format, blob)
report = report_class.new(user, params, render: true)
result = report.render_as(format)
write(result)
end

def done?
@done ||= File.file?(filename)
end

def result
@result ||= read_result
end

private

def write(result)
File.write(filename, result, mode: "wb")
end

def read_result
File.read(filename)
ensure
File.unlink(filename)
end

def filename
Rails.root.join("tmp/report-#{job_id}")
blob.store(result)
end
end
36 changes: 36 additions & 0 deletions app/models/report_blob.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

# Stores a generated report.
class ReportBlob < ActiveStorage::Blob
def self.create_for_upload_later!(filename)
# ActiveStorage discourages modifying a blob later but we need a blob
# before we know anything about the report file. It enables us to use the
# same blob in the controller to read the result.
create_before_direct_upload!(
filename: filename,
byte_size: 0,
checksum: "0",
content_type: content_type(filename),
).tap do |blob|
ActiveStorage::PurgeJob.set(wait: 1.month).perform_later(blob)
end
end

def self.content_type(filename)
MIME::Types.of(filename).first&.to_s || "application/octet-stream"
end

def store(content)
io = StringIO.new(content)
upload(io, identify: false)
save!
end

def content_stored?
@content_stored ||= reload.checksum != "0"
end

def result
@result ||= download
end
end
2 changes: 1 addition & 1 deletion app/views/admin/reports/show.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@
- if request.post? && !@error
%button.btn-print.icon-print{ onclick: "window.print()"}= t(:report_print)

= t(@error) if @error
= t(@error, link: link_to(t(".report_link_label"), @error_url)) if @error
= @table
5 changes: 5 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,11 @@ en:
Sorry, this report took too long to process.
It may contain a lot of data or we are busy with other reports.
You can try again later.
report_taking_longer_html: >
This report is taking longer to process.
It may contain a lot of data or we are busy with other reports.
Once it's finished, you can download it: %{link}
report_link_label: Download report (when available)
revenues_by_hub:
name: Revenues By Hub
description: Revenues by hub
Expand Down
31 changes: 17 additions & 14 deletions spec/jobs/report_job_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,37 @@
require 'spec_helper'

describe ReportJob do
let(:report_args) { [report_class, user, params, format] }
let(:report_args) { [report_class, user, params, format, blob] }
let(:report_class) { Reporting::Reports::UsersAndEnterprises::Base }
let(:user) { enterprise.owner }
let(:enterprise) { create(:enterprise) }
let(:params) { {} }
let(:format) { :csv }
let(:blob) { ReportBlob.create_for_upload_later!("report.csv") }

it "generates a report" do
job = ReportJob.new
job.perform(*report_args)
expect_csv_report(job)
job = perform_enqueued_jobs(only: ReportJob) do
ReportJob.perform_later(*report_args)
end
expect_csv_report
end

it "enqueues a job for asynch processing" do
it "enqueues a job for async processing" do
job = ReportJob.perform_later(*report_args)
expect(job.done?).to eq false
expect(blob.content_stored?).to eq false

# This performs the job in the same process but that's good enought for
# testing the job code. I hope that we can rely on the job worker.
ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
job.retry_job
perform_enqueued_jobs(only: ReportJob)

expect(job.done?).to eq true
expect_csv_report(job)
expect(blob.content_stored?).to eq true
expect_csv_report
end

def expect_csv_report(job)
table = CSV.parse(job.result)
def expect_csv_report
blob.reload
expect(blob.filename.to_s).to eq "report.csv"
expect(blob.content_type).to eq "text/csv"

table = CSV.parse(blob.result)
expect(table[0][1]).to eq "Relationship"
expect(table[1][1]).to eq "owns"
end
Expand Down
13 changes: 11 additions & 2 deletions spec/system/admin/reports_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
expect(page).to have_content "EMAIL FIRST NAME"
end

it "displays a friendly timeout message" do
it "displays a friendly timeout message and offers download" do
ActiveJob::Base.queue_adapter.perform_enqueued_jobs = false
login_as_admin
visit admin_report_path(
Expand All @@ -57,7 +57,16 @@

click_button "Go"

expect(page).to have_content "this report took too long"
expect(page).to have_content "report is taking longer"

perform_enqueued_jobs(only: ReportJob)

click_link "Download report"

expect(downloaded_filename).to match /customers_[0-9]+\.html/

content = File.read(downloaded_filename)
expect(content).to match "<th>\nFirst Name\n</th>"
end
end

Expand Down

0 comments on commit f206b7e

Please sign in to comment.