Browse Source
* Save a csv download record * Allow downloading CSVs * Make links expire * Send correct download link * Fix world * Make csv downloads work locally * Set expiration time on the record * lint * Update download urls * Update expiration time * Update expired link content * Add a page to view the download * lint * lint againpull/2805/head
kosiakkatrina
3 months ago
committed by
GitHub
15 changed files with 371 additions and 15 deletions
@ -0,0 +1,27 @@ |
|||||||
|
class CsvDownloadsController < ApplicationController |
||||||
|
before_action :authenticate_user! |
||||||
|
|
||||||
|
def show |
||||||
|
@csv_download = CsvDownload.find(params[:id]) |
||||||
|
authorize @csv_download |
||||||
|
|
||||||
|
return render "errors/download_link_expired" if @csv_download.expired? |
||||||
|
end |
||||||
|
|
||||||
|
def download |
||||||
|
csv_download = CsvDownload.find(params[:id]) |
||||||
|
authorize csv_download |
||||||
|
|
||||||
|
return render "errors/download_link_expired" if csv_download.expired? |
||||||
|
|
||||||
|
downloader = Csv::Downloader.new(csv_download:) |
||||||
|
|
||||||
|
if Rails.env.development? |
||||||
|
downloader.call |
||||||
|
send_file downloader.path, filename: csv_download.filename, type: "text/csv" |
||||||
|
else |
||||||
|
presigned_url = downloader.presigned_url |
||||||
|
redirect_to presigned_url, allow_other_host: true |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,10 @@ |
|||||||
|
class CsvDownload < ApplicationRecord |
||||||
|
enum download_type: { lettings: "lettings", sales: "sales", schemes: "schemes", locations: "locations", combined: "combined" } |
||||||
|
|
||||||
|
belongs_to :user |
||||||
|
belongs_to :organisation |
||||||
|
|
||||||
|
def expired? |
||||||
|
created_at < expiration_time.seconds.ago |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,16 @@ |
|||||||
|
class CsvDownloadPolicy |
||||||
|
attr_reader :current_user, :csv_download |
||||||
|
|
||||||
|
def initialize(current_user, csv_download) |
||||||
|
@current_user = current_user |
||||||
|
@csv_download = csv_download |
||||||
|
end |
||||||
|
|
||||||
|
def show? |
||||||
|
@current_user == @csv_download.user || @current_user.support? || @current_user.organisation == @csv_download.organisation |
||||||
|
end |
||||||
|
|
||||||
|
def download? |
||||||
|
@current_user == @csv_download.user || @current_user.support? || @current_user.organisation == @csv_download.organisation |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,50 @@ |
|||||||
|
class Csv::Downloader |
||||||
|
attr_reader :csv_download |
||||||
|
|
||||||
|
delegate :path, to: :file |
||||||
|
|
||||||
|
def initialize(csv_download:) |
||||||
|
@csv_download = csv_download |
||||||
|
end |
||||||
|
|
||||||
|
def call |
||||||
|
download |
||||||
|
end |
||||||
|
|
||||||
|
def delete_local_file! |
||||||
|
file.unlink |
||||||
|
end |
||||||
|
|
||||||
|
def presigned_url |
||||||
|
s3_storage_service.get_presigned_url(csv_download.filename, 60, response_content_disposition: "attachment; filename=#{csv_download.filename}") |
||||||
|
end |
||||||
|
|
||||||
|
private |
||||||
|
|
||||||
|
def download |
||||||
|
io = storage_service.get_file_io(csv_download.filename) |
||||||
|
file.write(io.read) |
||||||
|
io.close |
||||||
|
file.close |
||||||
|
end |
||||||
|
|
||||||
|
def file |
||||||
|
@file ||= Tempfile.new |
||||||
|
end |
||||||
|
|
||||||
|
def storage_service |
||||||
|
@storage_service ||= if FeatureToggle.upload_enabled? |
||||||
|
s3_storage_service |
||||||
|
else |
||||||
|
local_disk_storage_service |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def s3_storage_service |
||||||
|
Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["BULK_UPLOAD_BUCKET"]) |
||||||
|
end |
||||||
|
|
||||||
|
def local_disk_storage_service |
||||||
|
Storage::LocalDiskService.new |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,10 @@ |
|||||||
|
<% title = "Downlaod CSV file" %> |
||||||
|
<% content_for :title, title %> |
||||||
|
|
||||||
|
<div class="govuk-grid-row"> |
||||||
|
<div class="govuk-grid-column-two-thirds-from-desktop"> |
||||||
|
<h1 class="govuk-heading-l govuk-!-margin-bottom-6">You are about to download a CSV file</h1> |
||||||
|
<p class="govuk-body-m">Filename: <%= @csv_download.filename %></p> |
||||||
|
<%= govuk_button_link_to "Download CSV", download_csv_download_path(@csv_download) %> |
||||||
|
</div> |
||||||
|
</div> |
@ -0,0 +1,8 @@ |
|||||||
|
<% content_for :title, "This link has expired" %> |
||||||
|
|
||||||
|
<div class="govuk-grid-row"> |
||||||
|
<div class="govuk-grid-column-two-thirds"> |
||||||
|
<h1 class="govuk-heading-xl govuk-!-margin-bottom-6">This link has expired.</h1> |
||||||
|
<p class="govuk-body-m">Download the logs again to get a new link.</p> |
||||||
|
</div> |
||||||
|
</div> |
@ -0,0 +1,12 @@ |
|||||||
|
class AddCsvDownloadTable < ActiveRecord::Migration[7.0] |
||||||
|
def change |
||||||
|
create_table :csv_downloads do |t| |
||||||
|
t.column :download_type, :string |
||||||
|
t.column :filename, :string |
||||||
|
t.column :expiration_time, :integer |
||||||
|
t.timestamps |
||||||
|
t.references :user |
||||||
|
t.references :organisation |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,9 @@ |
|||||||
|
FactoryBot.define do |
||||||
|
factory :csv_download do |
||||||
|
download_type { "lettings" } |
||||||
|
user { create(:user) } |
||||||
|
organisation { user.organisation } |
||||||
|
filename { "lettings.csv" } |
||||||
|
expiration_time { 24.hours.to_i } |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,136 @@ |
|||||||
|
require "rails_helper" |
||||||
|
|
||||||
|
RSpec.describe CsvDownloadsController, type: :request do |
||||||
|
describe "GET #show" do |
||||||
|
let(:page) { Capybara::Node::Simple.new(response.body) } |
||||||
|
let(:csv_user) { create(:user) } |
||||||
|
let(:csv_download) { create(:csv_download, user: csv_user, organisation: csv_user.organisation) } |
||||||
|
let(:get_file_io) do |
||||||
|
io = StringIO.new |
||||||
|
io.write("hello") |
||||||
|
io.rewind |
||||||
|
io |
||||||
|
end |
||||||
|
let(:mock_storage_service) { instance_double(Storage::S3Service, get_file_io:, get_presigned_url: "https://example.com") } |
||||||
|
|
||||||
|
before do |
||||||
|
allow(Storage::S3Service).to receive(:new).and_return(mock_storage_service) |
||||||
|
end |
||||||
|
|
||||||
|
context "when user is not signed in" do |
||||||
|
it "redirects to sign in page" do |
||||||
|
get "/csv-downloads/#{csv_download.id}" |
||||||
|
expect(response).to redirect_to("/account/sign-in") |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context "when user is signed in" do |
||||||
|
before do |
||||||
|
sign_in user |
||||||
|
end |
||||||
|
|
||||||
|
context "and the user is from a different organisation" do |
||||||
|
let(:user) { create(:user) } |
||||||
|
|
||||||
|
before do |
||||||
|
get "/csv-downloads/#{csv_download.id}" |
||||||
|
end |
||||||
|
|
||||||
|
it "returns page not found" do |
||||||
|
expect(response).to have_http_status(:unauthorized) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context "and is the user who generated the csv" do |
||||||
|
let(:user) { csv_user } |
||||||
|
|
||||||
|
before do |
||||||
|
get "/csv-downloads/#{csv_download.id}" |
||||||
|
end |
||||||
|
|
||||||
|
it "allows downloading the csv" do |
||||||
|
expect(response).to have_http_status(:ok) |
||||||
|
expect(page).to have_link("Download CSV", href: "/csv-downloads/#{csv_download.id}/download") |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context "and is the user is from the same organisation" do |
||||||
|
let(:user) { create(:user, organisation: csv_user.organisation) } |
||||||
|
|
||||||
|
before do |
||||||
|
get "/csv-downloads/#{csv_download.id}" |
||||||
|
end |
||||||
|
|
||||||
|
it "allows downloading the csv" do |
||||||
|
expect(response).to have_http_status(:ok) |
||||||
|
expect(page).to have_link("Download CSV", href: "/csv-downloads/#{csv_download.id}/download") |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "GET #download" do |
||||||
|
let(:csv_user) { create(:user) } |
||||||
|
let(:csv_download) { create(:csv_download, user: csv_user, organisation: csv_user.organisation) } |
||||||
|
let(:get_file_io) do |
||||||
|
io = StringIO.new |
||||||
|
io.write("hello") |
||||||
|
io.rewind |
||||||
|
io |
||||||
|
end |
||||||
|
let(:mock_storage_service) { instance_double(Storage::S3Service, get_file_io:, get_presigned_url: "https://example.com") } |
||||||
|
|
||||||
|
before do |
||||||
|
allow(Storage::S3Service).to receive(:new).and_return(mock_storage_service) |
||||||
|
end |
||||||
|
|
||||||
|
context "when user is not signed in" do |
||||||
|
it "redirects to sign in page" do |
||||||
|
get "/csv-downloads/#{csv_download.id}/download" |
||||||
|
expect(response).to redirect_to("/account/sign-in") |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context "when user is signed in" do |
||||||
|
before do |
||||||
|
sign_in user |
||||||
|
end |
||||||
|
|
||||||
|
context "and the user is from a different organisation" do |
||||||
|
let(:user) { create(:user) } |
||||||
|
|
||||||
|
before do |
||||||
|
get "/csv-downloads/#{csv_download.id}/download" |
||||||
|
end |
||||||
|
|
||||||
|
it "returns page not found" do |
||||||
|
expect(response).to have_http_status(:unauthorized) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context "and is the user who generated the csv" do |
||||||
|
let(:user) { csv_user } |
||||||
|
|
||||||
|
before do |
||||||
|
get "/csv-downloads/#{csv_download.id}/download" |
||||||
|
end |
||||||
|
|
||||||
|
it "allows downloading the csv" do |
||||||
|
expect(response).to have_http_status(:found) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context "and is the user is from the same organisation" do |
||||||
|
let(:user) { create(:user, organisation: csv_user.organisation) } |
||||||
|
|
||||||
|
before do |
||||||
|
get "/csv-downloads/#{csv_download.id}/download" |
||||||
|
end |
||||||
|
|
||||||
|
it "allows downloading the csv" do |
||||||
|
expect(response).to have_http_status(:found) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
Loading…
Reference in new issue