Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Round Trip Data] A start on importing user data via CSV #15

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

Benratelade
Copy link
Collaborator

Created a test for data upload.
Some changes to the html for the Stats page, to allow for better targeting in tests.
Added an ugly upload button to Home.js
Created a support class for handling CSV uploads (and downloads, later).
Also created a sample csv file, which really ought to be replaced with some Factory.

@saramic this is definitely not ready for a proper pull request, but help is needed!

Several things:

  • I created a test feature for this, but there are a few things I couldn't figure out. In particular, how do we get those ActionCable notifications to show up. Not sure if there's only Devise doing some magic for now, or if we already have a way of passing our own info to those notifications.
  • Ideally we should have a Factory for creating a CSV file (rather than having a static file in our tests)
  • I haven't created a route or a controller for the file upload. I'm not sure how you would prefer to build those.
  • CSV Files would need to be uploaded and saved somewhere. I can see we serve images from an S3 bucket, so I assume it's the way to go?
  • I assume we should have a queue for processing those uploads (otherwise the server would just be hanging during processing)?

Created a test for data upload.
Some changes to the html for the Stats page, to allow for better targeting in tests.
Added an ugly upload button to Home.js
Created a support class for handling CSV uploads (and downloads, later).
Also created a sample csv file, which really ought to be replaced with some Factory.
@Benratelade Benratelade requested review from saramic and achhetr and removed request for achhetr September 5, 2021 05:41
saramic and others added 5 commits September 9, 2021 19:32
the method generate_file_with_contents can be used to generate a file
with a HEREDOC or with an array of hashses which will create a sparse
CSV file

Co-authored-by: Ben Ratelade <[email protected]>
Created the data importer alongside some tests to make sure it does the basic things.
CSV importer class for DailyStats, in functionnal programming style.
Updated rubocop.yml to allow for big code blocks in spec/services
Reinstated the changes to user_adds_stats_using_nice_ui_spec.rb so that it uses Factory Bot.
Tweaked file_helper.rb module to allow changing the file extension.
… csv

Created a Settings controller to handle the import route.
Created a view (settings/show.html.erb) to display the upload form.
Added a link to /settings in the React nav. It points to a plain Rails page.
Cleaned up the React view to remove the upload button.
Renamed test for spec/features/user_uploads_own_data.rb to spec/features/user_uploads_own_data_spec.rb
Style tweak for Flash Notices so they are always visible.
@Benratelade Benratelade marked this pull request as ready for review September 13, 2021 03:18
@Benratelade
Copy link
Collaborator Author

@saramic I finally have something that works for importing Daily Stats via CSV. Let me know if there is anything we could improve.

</>
)}
</div>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we are in react here - I would have just added a plain old button in Rails, it would be like in a separate section - fetch your data - or upload your data on an account setup page? that said you "could" make it more "interactive" by using JS and react

@@ -186,7 +186,7 @@
# ==> Configuration for :timeoutable
# The time you want to timeout the user session without activity. After this
# time the user will be asked for credentials again. Default is 30 minutes.
# config.timeout_in = 30.minutes
config.timeout_in = 2.weeks
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool - would have been nice in a separate pull request - so that I could merge that in :)

page.find("[data-widget-type='stats-app'] .nav-tabs a[href='/app']", text: "Add").click
# TODO dynamically generate the file with fixtures
# let's say the file should have 10 records
attach_file("CSV-file-upload", Rails.root + "spec/support/csv_sample.csv")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could do something like

import_file = generate_file_with_contents("health_sample.csv") do
  <<~HEALTH_SAMPLE_CSV
    Date,Activity Name,Quantity
    15/08/2021,Weight,67.1
    14/08/2021,Weight,66.8
    13/08/2021,Weight,66.5
    12/08/2021,Weight,66
    11/08/2021,Pull ups,50
    11/08/2021,Weight,66.5
    10/08/2021,Weight,67
    08/08/2021,Push ups,200
    08/08/2021,Weight,66
    08/08/2021,Push ups,100
  HEALTH_SAMPLE_CSV
end

assuming you have a file_helper.rb somewhere in your spec with code similar to

module FileHelper
  def generate_file_with_contents(filename)
    file = Tempfile.new(filename)
    content = yield
    content = csv_lines_to_string(content) if content.is_a?(Array)
    file.write(content)
    file.close
    file
  end

  private

  def csv_lines_to_string(csv_lines)
    ordered_columns = csv_lines.map(&:keys).flatten.uniq.sort
    CSV.generate do |csv|
      csv << ordered_columns
      csv_lines
        .each do |csv_line|
        csv << ordered_columns.map { |col| csv_line[col] }
      end
    end
  end
end

end

def import
DailyStatsDataImporter.import(params[:file].path, current_user.id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool, although we will probably want to move it into a job so that it does NOT execute in a web request but in its own job


def import
DailyStatsDataImporter.import(params[:file].path, current_user.id)
redirect_to "/app/stats", notice: "Upload complete"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably better to use the app_stats_path url thing although I can never remember which way it is - or should this go back to action: show ?

read_file(filepath)
.map { |data| process_data(data) }
.map { |data| user.daily_stats.create(data) }
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, feels like we could describe more of the process in this "chain of functions"

read_file(filepath)
  .then { |file| read_csv(file) }
  .map { |csv_line| process_csv_line(csv_line) }
  .map { |data| process_data(data) }
  .map { |data| user.daily_stats.create(data) }

maybe? or something similar?

ALSO should the last line be a daily_stats.create ? or should we allow for find_or_create ? - ie to allow updates?

@@ -6,6 +6,8 @@
root to: "users#index"
end
mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql" if Rails.env.development?
resource :settings, only: [:show]
post :import, to: "settings#import"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so the "more rails" way to do this would be something like

resource :settings, only: [:show] do
  collection
    post :import
  end
end

another 'nice' alternative would be to ask for the user id /settings/long-user-id/import which later we could allow an "admin" to import / export on behalf of a user - very handy!!!

page
.find_all(".daily-stats-list .daily-stats-item")
.size,
).to be(7)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good start - although we must remember to put this into a page fragment

also I am starting to be more and more anti the "count" test .to be(7) mostly because if it break the first thing I want to see is what you actually got.

in this case our 10 data points across 7 days are completely arbitrary - a good "real data representation" but not specific enough to help with testing. I would say that in BDD (behaviour driven development) a form of "example mapping" (you can look it up in google) is useful to take a real data set and break it down into a minimum set that exercise constraints.

int this case we could have 3 data points, 1 occurs on 1 day and 2 on the other day - which would make this easy to test that they are all there and exactly what is inside


it "Fails gracefully when the wrong file type is provided" do
expect do
described_class.import(incorrect_file_extension.path, user.id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice I really like this - only comment is that we could bring the "input files" closer to the tests by wrapping each example in a context this would also have the positive side effect that the it statement would no longer need the then clause and there is then no need to name the file as it would be the name of the context. This name of the context also gets displayed when you run the tests OR when they break in CI so if one of the tests failed it would tell you that eg

context "with a file that has the wrong file extension" do
  let(:file) { ... }

  it "raises an unknown file type RUntimeError" do
    ...
  end
end

context "...
  let(:file) {...
...

the fact that each let is file also slightly reduces the load on reading the test as everywhere you see described_class.import(file.path, user.id)

which theoretically, given that described_class is already hiding the name of the class, you could actually change into a subject at the top of the file

say

subject(:import_file) { described_class.import(file.path, user.id) }
...
it "..." do
  import_file
  expect(...)

@@ -0,0 +1,23 @@
module FileHelper
def generate_file_with_contents(*filename)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is cool but does kind of hide the implementation of what exactly is *filename vs using (filename, file_extension) maybe?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants