-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: main
Are you sure you want to change the base?
Conversation
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.
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]>
Co-authored-by: Michael Milewski <[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.
@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> |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
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: