Skip to content

Commit

Permalink
allow users to download device data as CSV
Browse files Browse the repository at this point in the history
  • Loading branch information
timcowlishaw committed Jan 12, 2025
1 parent 3fc80d8 commit 36a6a9d
Show file tree
Hide file tree
Showing 12 changed files with 234 additions and 7 deletions.
22 changes: 22 additions & 0 deletions app/controllers/ui/devices_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,28 @@ def destroy
redirect_to ui_user_path(current_user.username)
end

def download
find_device!
return unless authorize_device! :download?, :download_device_forbidden
@title = I18n.t(:download_device_title)
add_breadcrumbs(
[I18n.t(:show_user_title, owner: helpers.possessive(@device.owner, current_user)), ui_user_path(@device.owner.username)],
[I18n.t(:show_device_title, name: @device.name), ui_device_path(@device.id)],
[@title, download_ui_device_path(@device.id)]
)
end

def download_confirm
find_device!
return unless authorize_device! :download?, :download_device_forbidden
if @device.request_csv_archive_for!(current_user)
flash[:success] = I18n.t(:download_device_success)
else
flash[:alert] = I18n.t(:download_device_requested_too_soon)
end
redirect_to ui_device_path(@device.id)
end

private

def device_params
Expand Down
8 changes: 1 addition & 7 deletions app/controllers/v0/readings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,7 @@ def csv_archive
@device = Device.find(params[:id])
authorize @device, :update?

if !@device.csv_export_requested_at or (@device.csv_export_requested_at < 15.minutes.ago)
@device.update_column(:csv_export_requested_at, Time.now.utc)
if Rails.env.test?
UserMailer.device_archive(@device.id, current_user.id).deliver_now
else
UserMailer.device_archive(@device.id, current_user.id).deliver_later
end
if @device.request_csv_archive_for!(current_user)
render json: { id: "ok", message: "CSV Archive job added to queue", url: "", errors: "" }, status: :ok
else
render json: { id: "enhance_your_calm", message: "You can only make this request once every 6 hours, (this is rate-limited)", url: "", errors: "" }, status: 420
Expand Down
12 changes: 12 additions & 0 deletions app/models/device.rb
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,18 @@ def truncate_and_fuzz_location!
end
end

def csv_export_requested_recently?
!!self.csv_export_requested_at && self.csv_export_requested_at > 15.minutes.ago
end

def request_csv_archive_for!(user)
return false if self.csv_export_requested_recently?
self.update_column(:csv_export_requested_at, Time.now.utc)
mailer = UserMailer.device_archive(self.id, user.id)
mailer.deliver_later
return true
end

private

def set_state
Expand Down
4 changes: 4 additions & 0 deletions app/policies/device_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,8 @@ def create?
def destroy?
update?
end

def download?
update?
end
end
3 changes: 3 additions & 0 deletions app/views/ui/devices/_actions.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
<% if authorize? device, :update? %>
<%= link_to(t(:show_device_edit_cta, name: device.name), edit_ui_device_path(device), class: "btn btn-dark me-md-2 w-100 w-md-auto") %>
<% end %>
<% if authorize? device, :download? %>
<%= link_to(t(:show_device_download_cta), download_ui_device_path(device), class: "btn btn-dark me-md-2 w-100 w-md-auto") %>
<% end %>
</p>
13 changes: 13 additions & 0 deletions app/views/ui/devices/download.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<% if @device.csv_export_requested_recently? %>
<p><%= t(:download_device_recently_requested_blurb) %></p>
<div class="mt-4">
<%= link_to(t(:download_device_back), ui_device_path(@device.id), class: "btn btn-secondary w-100 w-md-auto") %>
</div>
<% else %>
<%= bootstrap_form_tag url: download_ui_device_path(@device.id), method: :post do |f| %>
<p><%= t(:download_device_confirmation_blurb) %></p>
<div class="mt-4">
<%= f.primary t(:download_device_submit), class: "btn btn-primary w-100 w-md-auto" %>
</div>
<% end %>
<% end %>
4 changes: 4 additions & 0 deletions config/locales/controllers/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ en:
delete_device_forbidden: "You are not allowed to delete that kit!"
delete_device_success: "The kit has been deleted!"
delete_device_wrong_name: "That kit name did not match! Please try again."
download_device_title: "Data download"
download_device_forbidden: "You are not allowed to download data for that kit!"
download_device_success: "Your CSV download has been requested, you'll shortly receive an email with a download link!"
download_device_requested_too_soon: "Sorry, requests for CSV downloads are rate-limited. Please wait before requesting another download."
5 changes: 5 additions & 0 deletions config/locales/views/devices/en.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
en:
show_device_headline: "Kit: %{name}"
show_device_edit_cta: "Edit kit settings"
show_device_download_cta: "Download data as CSV"
device_meta_last_reading_at: "Last reading %{time} ago"
device_meta_no_readings_message: "No readings received yet!"
edit_device_details_subhead: "Basic information"
Expand All @@ -26,3 +27,7 @@ en:
delete_device_warning_html: "🚨<strong>Warning!</strong> This will permanently delete the kit <strong>%{name}</strong>.🚨"
delete_device_name_label: "To confirm, type the kit name below:"
delete_device_submit: "I understand, delete the kit"
download_device_confirmation_blurb: "Click below to confirm - we will send you an email with a link to download your kit's data as a CSV."
download_device_submit: "Request data download"
download_device_recently_requested_blurb: "You've recently requested a CSV archive for this kit, please wait a little while before re-requesting. you will receive an email with a link to your CSV file shortly!"
download_device_back: "Return to kit page"
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
member do
get :edit
get :delete
get :download
post :download, to: "devices#download_confirm"
end
end

Expand Down
68 changes: 68 additions & 0 deletions spec/controllers/ui/devices_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -233,5 +233,73 @@
end
end
end

describe "download" do
context "when the device's owner is logged in" do
it "displays the download device page" do
get :download, params: { id: device.id }, session: { user_id: user.try(:id) }
expect(response).to have_http_status(:success)
expect(response).to render_template(:download)
end
end

context "when a different user is logged in" do
let(:owner) { create(:user) }
it "redirects to the ui users page" do
get :download, params: { id: device.id }, session: { user_id: user.try(:id) }
expect(response).to redirect_to(ui_user_path(user.username))
expect(flash[:alert]).to be_present
end
end

context "when no user is logged in" do
let(:user) { nil }
it "redirects to the login page" do
get :download, params: { id: device.id }, session: { user_id: user.try(:id) }
expect(response).to redirect_to(login_path)
expect(flash[:alert]).to be_present
end
end
end

describe "download_confirm" do
context "when the device's owner is logged in" do
context "when a CSV download has not yet been requested during the timeout period" do
it "requests a CSV archive and redirects to the device page, setting the success flash" do
expect_any_instance_of(Device).to receive(:request_csv_archive_for!).with(user).and_return(true)
post :download_confirm, params: { id: device.id }, session: { user_id: user.try(:id) }
expect(response).to redirect_to(ui_device_path(device.id))
expect(flash[:success]).to be_present
end
end

context "when a CSV download has already been requested during the timeout period" do
it "requests a CSV archive and redirects to the device page, setting the alert flash" do
expect_any_instance_of(Device).to receive(:request_csv_archive_for!).with(user).and_return(false)
post :download_confirm, params: { id: device.id }, session: { user_id: user.try(:id) }
expect(response).to redirect_to(ui_device_path(device.id))
expect(flash[:alert]).to be_present
end
end
end

context "when a different user is logged in" do
let(:owner) { create(:user) }
it "redirects to the ui users page" do
post :download_confirm, params: { id: device.id }, session: { user_id: user.try(:id) }
expect(response).to redirect_to(ui_user_path(user.username))
expect(flash[:alert]).to be_present
end
end

context "when no user is logged in" do
let(:user) { nil }
it "redirects to the login page" do
post :download_confirm, params: { id: device.id }, session: { user_id: user.try(:id) }
expect(response).to redirect_to(login_path)
expect(flash[:alert]).to be_present
end
end
end
end

20 changes: 20 additions & 0 deletions spec/features/device_management_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,24 @@
expect(page).not_to have_content(device_name)
expect(device.reload).to be_archived
end

scenario "User downloads CSV archive for a device" do
fake_file = double(:file)
allow(fake_file).to receive(:url).and_return("https://example.com")
allow(DeviceArchive).to receive(:create).and_return(fake_file)
password = "password123"
username = "username"
device_name = "devicename"
user = create(:user, username: username, password: password, password_confirmation: password)
device = create(:device, name: device_name, owner: user)
visit "/login"
fill_in "Username or email", with: user.email
fill_in "Password", with: password
click_on "Sign into your account"
click_on device_name
click_on "Download data as CSV"
click_on "Request data download"
expect(page).to have_current_path(ui_device_path(device.id))
expect(page).to have_content("Your CSV download has been requested, you'll shortly receive an email with a download link!")
end
end
80 changes: 80 additions & 0 deletions spec/models/device_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -720,4 +720,84 @@
end
end

describe "#csv_export_requested_recently?" do
context "when no CSV export has ever been requested" do
it "returns false" do
device.csv_export_requested_at = nil
expect(device.csv_export_requested_recently?).to be(false)
end
end

context "when a CSV export has been requested in the last 15 minutes" do
it "returns true" do
device.csv_export_requested_at = 14.minutes.ago
expect(device.csv_export_requested_recently?).to be(true)
end
end

context "when a CSV export has been requested, but more than 15 minutes ago" do
it "returns false" do
device.csv_export_requested_at = 16.minutes.ago
expect(device.csv_export_requested_recently?).to be(false)
end
end
end

describe "#request_csv_archive_for!" do
let(:current_user) { FactoryBot.create(:user) }

context "when a CSV export has been requested recently" do
before do
allow(device).to receive(:csv_export_requested_recently?).and_return(true)
end

it "does not update the csv_export_requested_at column" do
expect(device).not_to receive(:update_column)
device.request_csv_archive_for!(current_user)
end

it "does not create a device archive mailer" do
expect(UserMailer).not_to receive(:device_archive)
device.request_csv_archive_for!(current_user)
end

it "returns false" do
expect(device.request_csv_archive_for!(current_user)).to be(false)
end
end

context "when no CSV export has been requested recently" do

let(:mailer) {
double(:mailer).tap do |mailer|
allow(mailer).to receive(:deliver_later)
end
}

before do
allow(device).to receive(:csv_export_requested_recently?).and_return(false)
allow(UserMailer).to receive(:device_archive).and_return(mailer)
end

it "updates the csv_export_requested_at column" do
expect(device).to receive(:update_column).with(:csv_export_requested_at, anything)
device.request_csv_archive_for!(current_user)
end

it "creates a device archive mailer" do
expect(UserMailer).to receive(:device_archive).with(device.id, current_user.id)
device.request_csv_archive_for!(current_user)
end

it "delivers the device archive mailer" do
expect(mailer).to receive(:deliver_later)
device.request_csv_archive_for!(current_user)
end

it "returns true" do
expect(device.request_csv_archive_for!(current_user)).to be(true)
end
end
end

end

0 comments on commit 36a6a9d

Please sign in to comment.