diff --git a/Gemfile b/Gemfile index 5546b6d..1a4b8cb 100644 --- a/Gemfile +++ b/Gemfile @@ -37,6 +37,7 @@ group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platform: :mri gem 'rspec-rails', '~> 3.5.2' + gem 'rails-controller-testing', '~> 1.0.1' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index f703db9..d062984 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -98,6 +98,10 @@ GEM bundler (>= 1.3.0, < 2.0) railties (= 5.0.2) sprockets-rails (>= 2.0.0) + rails-controller-testing (1.0.1) + actionpack (~> 5.x) + actionview (~> 5.x) + activesupport (~> 5.x) rails-dom-testing (2.0.2) activesupport (>= 4.2.0, < 6.0) nokogiri (~> 1.6) @@ -180,6 +184,7 @@ DEPENDENCIES pg (~> 0.18) puma (~> 3.0) rails (~> 5.0.0, >= 5.0.0.1) + rails-controller-testing (~> 1.0.1) rspec-rails (~> 3.5.2) sass-rails (~> 5.0) spring diff --git a/README.md b/README.md index a148363..cfe1586 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,16 @@ * [Setup - Legacy Initial Steps](#part-2000) * [Setup - Replace Unit Test with RSpec](#part-3000) * [Setup - Git Repo](#part-4000) + * [Setup - Git Releases and Tags](#part-5000) --- +## Goals + +* [X] - Import pre-populated CSV into database from web form. +* [X] - Present populated data from database in table view +* [ ] - Use AJAX and apply basic filters on table so data updated without refreshing whole page. + ## System Requirements and Info * Show System Setup `rails about` @@ -24,6 +31,7 @@ * Rack - 2.0.1 * Node.js - 7.7.1 (V8 runtime) * PostgreSQL - 9.6.2 + * RSpec - 3.5.4 * OS - macOS El Capitan * Show Codebase Stats @@ -38,6 +46,11 @@ * Open PostgreSQL Database console automatically http://guides.rubyonrails.org/command_line.html `rails dbconsole` +* Show database table contents + ``` + select * from products; + ``` + ## Documentation Links * Testing @@ -101,8 +114,7 @@ * Migrate into PostgreSQL Database ``` - rake db:create - rake db:migrate RAILS_ENV=development + rake db:create db:migrate RAILS_ENV=development ``` * Launch the Rails server in separate Terminal tab automatically and opens it in web browser after 10 seconds using Shell Script: @@ -149,4 +161,9 @@ `git pull --rebase origin master` * Force push to remote branch to overwrite existing history - `git push -f origin master` \ No newline at end of file + `git push -f origin master` + +## Setup - Git Release and Tags + +* Create New Release https://github.com/ltfschoen/rails_csv_app/releases/new + * Pre-Release (non-production) i.e. v0.1 \ No newline at end of file diff --git a/app/assets/javascripts/products.coffee b/app/assets/javascripts/products.coffee new file mode 100644 index 0000000..24f83d1 --- /dev/null +++ b/app/assets/javascripts/products.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/stylesheets/products.scss b/app/assets/stylesheets/products.scss new file mode 100644 index 0000000..bff386e --- /dev/null +++ b/app/assets/stylesheets/products.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Products controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/controllers/products_controller.rb b/app/controllers/products_controller.rb new file mode 100644 index 0000000..35bc9c3 --- /dev/null +++ b/app/controllers/products_controller.rb @@ -0,0 +1,17 @@ +class ProductsController < ApplicationController + def index + @products = Product.all + end + + def import + # Validate inputs with block + begin + file = params[:file] + file_path = file.path + Product.import(file_path) + redirect_to root_url, notice: "Products imported." + rescue + redirect_to root_url, notice: "Invalid CSV file format." + end + end +end diff --git a/app/models/product.rb b/app/models/product.rb new file mode 100644 index 0000000..26bdec5 --- /dev/null +++ b/app/models/product.rb @@ -0,0 +1,18 @@ +class Product < ApplicationRecord + require 'csv' + + def self.import(file_path) + CSV.foreach(file_path, headers: true) do |row| + + product_hash = row.to_hash + product = Product.where(id: product_hash["id"]) + + if product.count == 1 + # Prevent CSV updates from changing the database comments attribute + product.first.update_attributes(product_hash.expect("comments")) + else + Product.create!(product_hash) + end + end + end +end diff --git a/app/views/products/import.html.erb b/app/views/products/import.html.erb new file mode 100644 index 0000000..70052d6 --- /dev/null +++ b/app/views/products/import.html.erb @@ -0,0 +1,2 @@ +

Products#import

+

Find me in app/views/products/import.html.erb

diff --git a/app/views/products/index.html.erb b/app/views/products/index.html.erb new file mode 100644 index 0000000..91e312c --- /dev/null +++ b/app/views/products/index.html.erb @@ -0,0 +1,30 @@ +<%= flash[:notice] %> + + + + + + + + + + + + <% @products.each do |product| %> + + + + + + + + <% end %> + +
IdNamePriceQuantityComments
<%= product.id %><%= product.name %><%= product.price %><%= product.quantity %><%= product.comments %>
+
+

Import a CSV File

+ <%= form_tag import_products_path, multipart: true do %> + <%= file_field_tag :file %> + <%= submit_tag "Import CSV" %> + <% end %> +
diff --git a/config/routes.rb b/config/routes.rb index 787824f..3ed34a0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,8 @@ Rails.application.routes.draw do + resources :products do + collection { post :import } + end + + root to: "products#index" # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html end diff --git a/db/migrate/20170307044647_create_products.rb b/db/migrate/20170307044647_create_products.rb new file mode 100644 index 0000000..30dcef6 --- /dev/null +++ b/db/migrate/20170307044647_create_products.rb @@ -0,0 +1,12 @@ +class CreateProducts < ActiveRecord::Migration[5.0] + def change + create_table :products do |t| + t.string :name + t.integer :quantity + t.decimal :price, precision: 12, scale: 2 + t.string :comments + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 2611543..59a8acf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,9 +10,18 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 0) do +ActiveRecord::Schema.define(version: 20170307044647) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "products", force: :cascade do |t| + t.string "name" + t.integer "quantity" + t.decimal "price", precision: 12, scale: 2 + t.string "comments" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + end diff --git a/products.csv b/products.csv new file mode 100644 index 0000000..99d59e7 --- /dev/null +++ b/products.csv @@ -0,0 +1,5 @@ +id,name,quantity,price,comments +1,Guitar,20,199.99,none +2,Trumpet,5,299.99,none +3,Piano,3,699.99,none +4,Clarinet,10,59.99,none \ No newline at end of file diff --git a/spec/controllers/products_controller_spec.rb b/spec/controllers/products_controller_spec.rb new file mode 100644 index 0000000..b6a82b7 --- /dev/null +++ b/spec/controllers/products_controller_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +RSpec.describe ProductsController, type: :controller do + + describe "GET #index" do + it "assigns @products" do + product = Product.create + get :index + expect(assigns(:products)).to eq([product]) + end + + it "renders the index template" do + get :index + expect(response).to render_template("index") + end + end + + describe "GET #import" do + it "redirects upon CSV import success to root url with success message" do + end + + it "redirects upon CSV import exception to root url with error message" do + end + end + +end diff --git a/spec/models/product_spec.rb b/spec/models/product_spec.rb new file mode 100644 index 0000000..514c070 --- /dev/null +++ b/spec/models/product_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +RSpec.describe Product, type: :model do + + describe 'Class' do + subject { Product } + + it { should respond_to(:import) } + + let(:data) { "id,name,quantity,price,comments\r1,Guitar,20,199.99,none" } + + describe "#import" do + it "should create a new record if id does not exist" do + File.stub(:open).with("filename", {:universal_newline=>false, :headers=>true}) { + StringIO.new(data) + } + Product.import("filename") + expect(Product.find_by(name: 'Guitar').price).to eq 199.99 + end + end + end + +end