diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..75e4c04e --- /dev/null +++ b/.babelrc @@ -0,0 +1,26 @@ +{ + "presets": [ + [ + "env", + { + "modules": false, + "targets": { + "browsers": "> 1%", + "uglify": true + }, + "useBuiltIns": true + } + ], + "react" + ], + "plugins": [ + "syntax-dynamic-import", + "transform-object-rest-spread", + [ + "transform-class-properties", + { + "spec": true + } + ] + ] +} diff --git a/.gitignore b/.gitignore index 7cc2c820..f3afbfeb 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,9 @@ vendor/assets/components/ public/uploads/ config/database.yml +package-lock.json +node_modules/ +/public/packs +/public/packs-test +/node_modules +yarn.lock diff --git a/.postcssrc.yml b/.postcssrc.yml new file mode 100644 index 00000000..a123d1fd --- /dev/null +++ b/.postcssrc.yml @@ -0,0 +1,3 @@ +plugins: + postcss-smart-import: {} + postcss-cssnext: {} diff --git a/.ruby-version b/.ruby-version index ac2cdeba..005119ba 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.1.3 +2.4.1 diff --git a/Gemfile b/Gemfile index 79edf9e6..3ac7be3d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,8 +1,7 @@ source 'https://rubygems.org' - # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '4.2.7' +gem 'rails', '4.2.9' # Use sqlite3 as the database for Active Record gem 'sqlite3' @@ -16,15 +15,25 @@ gem 'jbuilder', '~> 2.0' # bundle exec rake doc:rails generates the API under doc/api. gem 'sdoc', '~> 0.4.0', group: :doc +# Webpacker +gem 'webpacker', git: 'https://github.com/rails/webpacker.git' + +gem 'react-rails' +gem 'carrierwave' +gem 'sidekiq' +gem 'state_machines-activerecord' + group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'pry-rails' gem 'pry-nav' gem 'annotate' + gem 'puma' end group :development do - # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring + # Spring speeds up development by keeping your application running in the background. + # Read more: https://github.com/rails/spring gem 'spring' end @@ -32,7 +41,8 @@ group :test do gem 'rspec-rails' gem 'factory_girl_rails' gem 'shoulda-matchers' + gem 'rspec-sidekiq' + gem 'capybara' + gem 'poltergeist' end -gem 'react-rails' -gem 'carrierwave' diff --git a/Gemfile.lock b/Gemfile.lock index 09bdc98c..e4d57b55 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,194 +1,244 @@ +GIT + remote: https://github.com/rails/webpacker.git + revision: 3947386f8fc0dbec39d2315c6a336717b2a9e532 + specs: + webpacker (3.0.1) + activesupport (>= 4.2) + rack-proxy (>= 0.6.1) + railties (>= 4.2) + GEM remote: https://rubygems.org/ specs: - actionmailer (4.2.7) - actionpack (= 4.2.7) - actionview (= 4.2.7) - activejob (= 4.2.7) + actionmailer (4.2.9) + actionpack (= 4.2.9) + actionview (= 4.2.9) + activejob (= 4.2.9) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.7) - actionview (= 4.2.7) - activesupport (= 4.2.7) + actionpack (4.2.9) + actionview (= 4.2.9) + activesupport (= 4.2.9) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.7) - activesupport (= 4.2.7) + actionview (4.2.9) + activesupport (= 4.2.9) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (4.2.7) - activesupport (= 4.2.7) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (4.2.9) + activesupport (= 4.2.9) globalid (>= 0.3.0) - activemodel (4.2.7) - activesupport (= 4.2.7) + activemodel (4.2.9) + activesupport (= 4.2.9) builder (~> 3.1) - activerecord (4.2.7) - activemodel (= 4.2.7) - activesupport (= 4.2.7) + activerecord (4.2.9) + activemodel (= 4.2.9) + activesupport (= 4.2.9) arel (~> 6.0) - activesupport (4.2.7) + activesupport (4.2.9) i18n (~> 0.7) - json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - annotate (2.7.1) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + annotate (2.7.2) activerecord (>= 3.2, < 6.0) - rake (>= 10.4, < 12.0) - arel (6.0.3) + rake (>= 10.4, < 13.0) + arel (6.0.4) babel-source (5.8.35) babel-transpiler (0.7.0) babel-source (>= 4.0, < 6) execjs (~> 2.0) - builder (3.2.2) - carrierwave (0.11.2) - activemodel (>= 3.2.0) - activesupport (>= 3.2.0) - json (>= 1.7) + builder (3.2.3) + capybara (2.15.1) + addressable + mini_mime (>= 0.1.3) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) + carrierwave (1.1.0) + activemodel (>= 4.0.0) + activesupport (>= 4.0.0) mime-types (>= 1.16) - mimemagic (>= 0.3.0) - coderay (1.1.1) - coffee-script-source (1.10.0) - concurrent-ruby (1.0.2) - connection_pool (2.2.0) - diff-lcs (1.2.5) + cliver (0.3.2) + coderay (1.1.2) + concurrent-ruby (1.0.5) + connection_pool (2.2.1) + diff-lcs (1.3) erubis (2.7.0) execjs (2.7.0) - factory_girl (4.7.0) + factory_girl (4.8.0) activesupport (>= 3.0.0) - factory_girl_rails (4.7.0) - factory_girl (~> 4.7.0) + factory_girl_rails (4.8.0) + factory_girl (~> 4.8.0) railties (>= 3.0.0) - globalid (0.3.7) - activesupport (>= 4.1.0) - i18n (0.7.0) - jbuilder (2.6.0) - activesupport (>= 3.0.0, < 5.1) - multi_json (~> 1.2) - json (1.8.3) + globalid (0.4.0) + activesupport (>= 4.2.0) + i18n (0.8.6) + jbuilder (2.7.0) + activesupport (>= 4.2.0) + multi_json (>= 1.2) + json (1.8.6) loofah (2.0.3) nokogiri (>= 1.5.9) - mail (2.6.4) + mail (2.6.6) mime-types (>= 1.16, < 4) method_source (0.8.2) mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) - mimemagic (0.3.2) - mini_portile2 (2.1.0) - minitest (5.9.0) + mini_mime (0.1.4) + mini_portile2 (2.2.0) + minitest (5.10.3) multi_json (1.12.1) - nokogiri (1.6.8) - mini_portile2 (~> 2.1.0) - pkg-config (~> 1.1.7) - pkg-config (1.1.7) + nokogiri (1.8.0) + mini_portile2 (~> 2.2.0) + poltergeist (1.16.0) + capybara (~> 2.1) + cliver (~> 0.3.1) + websocket-driver (>= 0.2.0) pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) pry-nav (0.2.4) pry (>= 0.9.10, < 0.11.0) - pry-rails (0.3.4) - pry (>= 0.9.10) - rack (1.6.4) + pry-rails (0.3.6) + pry (>= 0.10.4) + public_suffix (3.0.0) + puma (3.10.0) + rack (1.6.8) + rack-protection (2.0.0) + rack + rack-proxy (0.6.2) + rack rack-test (0.6.3) rack (>= 1.0) - rails (4.2.7) - actionmailer (= 4.2.7) - actionpack (= 4.2.7) - actionview (= 4.2.7) - activejob (= 4.2.7) - activemodel (= 4.2.7) - activerecord (= 4.2.7) - activesupport (= 4.2.7) + rails (4.2.9) + actionmailer (= 4.2.9) + actionpack (= 4.2.9) + actionview (= 4.2.9) + activejob (= 4.2.9) + activemodel (= 4.2.9) + activerecord (= 4.2.9) + activesupport (= 4.2.9) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.7) + railties (= 4.2.9) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.7) + rails-dom-testing (1.0.8) activesupport (>= 4.2.0.beta, < 5.0) - nokogiri (~> 1.6.0) + nokogiri (~> 1.6) rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - railties (4.2.7) - actionpack (= 4.2.7) - activesupport (= 4.2.7) + railties (4.2.9) + actionpack (= 4.2.9) + activesupport (= 4.2.9) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (11.2.2) - rdoc (4.2.2) - json (~> 1.4) - react-rails (1.8.2) + rake (12.0.0) + rdoc (4.3.0) + react-rails (2.2.1) babel-transpiler (>= 0.7.0) - coffee-script-source (~> 1.8) connection_pool execjs railties (>= 3.2) tilt - rspec-core (3.5.2) - rspec-support (~> 3.5.0) - rspec-expectations (3.5.0) + redis (3.3.3) + rspec-core (3.6.0) + rspec-support (~> 3.6.0) + rspec-expectations (3.6.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-mocks (3.5.0) + rspec-support (~> 3.6.0) + rspec-mocks (3.6.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-rails (3.5.1) + rspec-support (~> 3.6.0) + rspec-rails (3.6.1) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 3.5.0) - rspec-expectations (~> 3.5.0) - rspec-mocks (~> 3.5.0) - rspec-support (~> 3.5.0) - rspec-support (3.5.0) - sdoc (0.4.1) + rspec-core (~> 3.6.0) + rspec-expectations (~> 3.6.0) + rspec-mocks (~> 3.6.0) + rspec-support (~> 3.6.0) + rspec-sidekiq (3.0.3) + rspec-core (~> 3.0, >= 3.0.0) + sidekiq (>= 2.4.0) + rspec-support (3.6.0) + sdoc (0.4.2) json (~> 1.7, >= 1.7.7) rdoc (~> 4.0) - shoulda-matchers (3.1.1) + shoulda-matchers (3.1.2) activesupport (>= 4.0.0) + sidekiq (5.0.4) + concurrent-ruby (~> 1.0) + connection_pool (~> 2.2, >= 2.2.0) + rack-protection (>= 1.5.0) + redis (~> 3.3, >= 3.3.3) slop (3.6.0) - spring (1.7.2) - sprockets (3.7.0) + spring (2.0.2) + activesupport (>= 4.2) + sprockets (3.7.1) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.1.1) + sprockets-rails (3.2.1) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sqlite3 (1.3.11) - thor (0.19.1) - thread_safe (0.3.5) - tilt (2.0.5) - tzinfo (1.2.2) + sqlite3 (1.3.13) + state_machines (0.5.0) + state_machines-activemodel (0.5.0) + activemodel (>= 4.1, < 5.2) + state_machines (>= 0.5.0) + state_machines-activerecord (0.5.0) + activerecord (>= 4.1, < 5.2) + state_machines-activemodel (>= 0.5.0) + thor (0.20.0) + thread_safe (0.3.6) + tilt (2.0.8) + tzinfo (1.2.3) thread_safe (~> 0.1) - uglifier (3.0.2) + uglifier (3.2.0) execjs (>= 0.3.0, < 3) + websocket-driver (0.6.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) + xpath (2.1.0) + nokogiri (~> 1.3) PLATFORMS ruby DEPENDENCIES annotate + capybara carrierwave factory_girl_rails jbuilder (~> 2.0) + poltergeist pry-nav pry-rails - rails (= 4.2.7) + puma + rails (= 4.2.9) react-rails rspec-rails + rspec-sidekiq sdoc (~> 0.4.0) shoulda-matchers + sidekiq spring sqlite3 + state_machines-activerecord uglifier (>= 1.3.0) + webpacker! BUNDLED WITH - 1.12.5 + 1.15.4 diff --git a/README.md b/README.md index fba4dd25..fcc5b1ee 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,15 @@ System sales analysis via log file (.txt) - ## Overview This system works with upload a log sales file (sales batch) and generate a total revenue about this batch. -> Warn: This is not a real system. It is just one example of use Ruby on Rails. +> Warn: This is not a real system. It is just one example of use Ruby on Rails and show my dev skill. ## How to deploy in production -> This project is not done to using in production. +> To write. ## How to build in Development Environment @@ -22,15 +21,19 @@ We recommend you use one ruby version manager as [rbenv](http://rbenv.org/) | [r - 2. In home path this project you should download all dependencies with bundler `bundle install` - 3. After run bundler you should you database config, one example file is in config/database.yml.sample, maybe you want use it with `cp config/database.yml.sample config/database.yml`. - 4. After configured database you go run `bundle exec rake db:migrate` to generate ou database schema. +- 5. This project manage frontend dependencies with [bower](https://bower.io/), to build dependencies use `bower install` +- 6. Init sidekiq with `bundle exec sidkiq -C config/sidekiq.yml` To run tests you user `bundle exec rspec spec`, to run webserver use `bundle exec rails server`, you server should listen in http://localhost:3000/ ### Requirements -- Ruby 2.1.3 -- Rails 4.2.7 +- Ruby 2.4.1 +- Rails 4.2.9 +- Redis (on ubuntu run `sudo apt install redis-server` - SQLite 3 (in ubuntu run `sudo apt-get install -y sqlite3 libsqlite3-dev`) - Gem Bundler (`gem install bundler`) - - +- NodeJS +- Bower (`npm install bower -g`) +- PhantomJS (to run features specs) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index f3f01c63..5b2880da 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -12,4 +12,7 @@ // //= require jquery/dist/jquery.min //= require bootstrap/dist/js/bootstrap.min +//= require react +//= require react_ujs +//= require components //= require_tree . diff --git a/app/assets/javascripts/components/batch_list_row.es6.jsx b/app/assets/javascripts/components/batch_list_row.es6.jsx new file mode 100644 index 00000000..0b467203 --- /dev/null +++ b/app/assets/javascripts/components/batch_list_row.es6.jsx @@ -0,0 +1,29 @@ +class BatchListRow extends React.Component { + render () { + let batch_path = (id) => "/sales_batches/"+id; + return ( + + + + + + + + + {this.props.batch.total_sales} + + + + R$ {this.props.batch.revenue} + + + +

+ {this.props.batch.state == 'resolved' ? "Concluído" : "A processar"} +

+ + + ); + } +} + diff --git a/app/assets/javascripts/components/batch_list_table.es6.jsx b/app/assets/javascripts/components/batch_list_table.es6.jsx new file mode 100644 index 00000000..0777d9f3 --- /dev/null +++ b/app/assets/javascripts/components/batch_list_table.es6.jsx @@ -0,0 +1,25 @@ +class BatchListTable extends React.Component { + + + render () { + let createRow = (batch) => ; + + return ( + + + + + + + + + + + + {this.props.batches.map(createRow)} + +
Lote Total de vendas Receita Bruta Status
+ ); + } +} + diff --git a/app/controllers/sales_batches_controller.rb b/app/controllers/sales_batches_controller.rb index 6ee37063..cca3f507 100644 --- a/app/controllers/sales_batches_controller.rb +++ b/app/controllers/sales_batches_controller.rb @@ -9,7 +9,12 @@ def new def create @batch = SalesBatch.create(upload_params) - redirect_to sales_batch_path(@batch) + + if @batch.persisted? + redirect_to sales_batches_path + else + render json: { error: @batch.errors }, status: :unprocessable_entity + end end def show diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js new file mode 100644 index 00000000..54b106ee --- /dev/null +++ b/app/javascript/packs/application.js @@ -0,0 +1,10 @@ +/* eslint no-console:0 */ +// This file is automatically compiled by Webpack, along with any other files +// present in this directory. You're encouraged to place your actual application logic in +// a relevant structure within app/javascript and only use these pack files to reference +// that code so it'll be compiled. +// +// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate +// layout file, like app/views/layouts/application.html.erb + +console.log('Hello World from Webpacker') diff --git a/app/javascript/packs/hello_react.jsx b/app/javascript/packs/hello_react.jsx new file mode 100644 index 00000000..772fc97e --- /dev/null +++ b/app/javascript/packs/hello_react.jsx @@ -0,0 +1,26 @@ +// Run this example by adding <%= javascript_pack_tag 'hello_react' %> to the head of your layout file, +// like app/views/layouts/application.html.erb. All it does is render
Hello React
at the bottom +// of the page. + +import React from 'react' +import ReactDOM from 'react-dom' +import PropTypes from 'prop-types' + +const Hello = props => ( +
Hello {props.name}!
+) + +Hello.defaultProps = { + name: 'David' +} + +Hello.propTypes = { + name: PropTypes.string +} + +document.addEventListener('DOMContentLoaded', () => { + ReactDOM.render( + , + document.body.appendChild(document.createElement('div')), + ) +}) diff --git a/app/models/concerns/sales_from_file.rb b/app/models/concerns/sales_from_file.rb deleted file mode 100644 index a939d893..00000000 --- a/app/models/concerns/sales_from_file.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'csv' - -module Concerns - module SalesFromFile - extend ActiveSupport::Concern - - CSV_HEADERS = %w(customer description unit_price amount address supplier) - - private - - #TODO: Refactor this to use uploader filename - #TODO: Refactor this to move to Sidekiq - #TODO: Refactor this to reduce method lines - def process_data_file! - filename = self.attachment.file.file - data = CSV.read(filename, col_sep: "\t") - data.delete_at(0) - - sales_attrs = data.map do |row| - row.each_with_index.map do |value, index| - Hash[ CSV_HEADERS[index], value ] - end.reduce(&:merge) - end - - sales_attrs.each do |sale_attr| - self.sales.create(sale_attr) - end - - total_revenue = self.sales.map{|sale| sale.amount * sale.unit_price }.reduce(:+) - self.update_attributes({ - processed: true, - revenue: total_revenue, - batch_code: SecureRandom.uuid - }) - end - end -end diff --git a/app/models/sales_batch.rb b/app/models/sales_batch.rb index 5ad186c6..c9fb896f 100644 --- a/app/models/sales_batch.rb +++ b/app/models/sales_batch.rb @@ -4,20 +4,58 @@ # # id :integer not null, primary key # attachment :string -# processed :boolean default(FALSE) # revenue :decimal(12, 2) default(0.0) # created_at :datetime not null # updated_at :datetime not null # batch_code :string +# state :string # class SalesBatch < ActiveRecord::Base - include Concerns::SalesFromFile - has_many :sales, dependent: :destroy - - validates :batch_code, uniqueness: true, presence: false + before_create :generate_batch_code + after_create :queue! + validates :batch_code, uniqueness: true mount_uploader :attachment, SalesUploader - after_create :process_data_file! + + state_machine :state, :initial => :uploaded do + + after_transition on: :queue, do: :queue_on_sidekiq + event :queue do + transition :uploaded => :queued + end + + event :process do + transition [:queued, :processing] => :processing + end + + event :resolve do + transition :processing => :resolved + end + + event :reject do + transition :processing => :rejected + end + end + + def total_sales + self.sales.count + end + + #TODO: Comment this hack on codereview + def as_json(options={}) + options[:methods] = [:total_sales] + super + end + + private + + def generate_batch_code + self.batch_code = SecureRandom.uuid + end + + def queue_on_sidekiq + SalesBatchWorker.perform_async(self.id) + end end diff --git a/app/uploaders/sales_uploader.rb b/app/uploaders/sales_uploader.rb index 7e4c00d7..aa065d64 100644 --- a/app/uploaders/sales_uploader.rb +++ b/app/uploaders/sales_uploader.rb @@ -47,5 +47,4 @@ def extension_white_list # def filename # "something.jpg" if original_filename # end - end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 062a7776..b70ec718 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -3,11 +3,13 @@ Splunka <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> - <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> + <%= javascript_include_tag 'application' %> <%= csrf_meta_tags %> + <%= render partial: 'shareds/navbar' %> +
<%= yield %>
diff --git a/app/views/sales/show.html.erb b/app/views/sales/show.html.erb index 1475b37d..2857ad7f 100644 --- a/app/views/sales/show.html.erb +++ b/app/views/sales/show.html.erb @@ -1 +1,24 @@ -

<%= @sale.customer %>

+

Log de venda efetuada

+ + + + + + + + + + + + + + + + + + + + +
CompradorDescriçãoPreço unitárioQuantidadeEndereçoFornecedorValor total da compra
<%= @sale.customer %><%= @sale.description %><%= number_to_currency(@sale.unit_price, unit: "R$ ") %><%= @sale.amount %>
<%= @sale.address %>
<%= @sale.supplier %><%= number_to_currency(@sale.amount * @sale.unit_price, unit: "BRL ") %>
+ +<%= link_to "<< Voltar para listagem do lote", sales_batch_path(@sale.sales_batch_id) %> diff --git a/app/views/sales_batches/_form_new.html.erb b/app/views/sales_batches/_form_new.html.erb index 123cc9d3..fb99ee48 100644 --- a/app/views/sales_batches/_form_new.html.erb +++ b/app/views/sales_batches/_form_new.html.erb @@ -1,15 +1,28 @@ +
<%= form_for sales_batch, url: sales_batches_path, html: { class: 'form-inline' } do |f| %> - <%= f.file_field :attachment, class: 'form-control' %> - <%= f.submit 'Enviar', class: 'btn btn-success' %> -

Instruções para novo lote

+

Instruções para novo lote

Para criar um novo lote de vendas, selecione um arquivo no formato '.txt' com dados separados por TAB.
A primeira linha do arquivo deve conter os nome das colunas obrigatóriamente: +

Comprador descrição Preço Unitário  Quantidade  Endereço  Fornecedor
+ Você pode baixar um arquivo de exemplo aqui. +
-
Comprador descrição Preço Unitário  Quantidade  Endereço  Fornecedor
- - Você pode baixar um arquivo de <%= link_to "exemplo aqui", "/sales.txt" %>. + <%= link_to "/sales.txt", class: 'btn btn-success' do %> + + Download de modelo de Arquivo de lote + <% end %>

+
+ +
+ <%= f.file_field :attachment, class: 'form-control' %> + <%= f.button :send, class: 'btn btn-success' do %> + + Enviar novo arquivo de lote + <% end %> +
<% end %> +
diff --git a/app/views/sales_batches/index.html.erb b/app/views/sales_batches/index.html.erb index 532dbbdb..0491ac5c 100644 --- a/app/views/sales_batches/index.html.erb +++ b/app/views/sales_batches/index.html.erb @@ -1,34 +1,12 @@ -

Splunka

-
<%= render partial: 'form_new', locals: { sales_batch: SalesBatch.new } %>
+
<% #TODO: Move this to presenter %> <% if @sales_batches.empty? %> Sem dados <% else %> - - - - - - - - - <% @sales_batches.each do |batch| %> - - - - - - - <% end %> - -
# Batch Total de vendas Receita Bruta Status
<%= link_to "##{batch.id}", sales_batch_path(batch) %><%= batch.sales.count %><%= number_to_currency(batch.revenue, :unit => "R$ ") %> -

- <%= (batch.processed) ? "Processado" : "A processar" %> -

-
+ <%= react_component 'BatchListTable', { batches: @sales_batches } %> <% end %> diff --git a/app/views/sales_batches/show.html.erb b/app/views/sales_batches/show.html.erb index 8572b0e5..ac0490b9 100644 --- a/app/views/sales_batches/show.html.erb +++ b/app/views/sales_batches/show.html.erb @@ -1,5 +1,5 @@ -

Dados de venda

-<% if @sales_batch.processed? %> +

Dados do lote de venda

+<% if @sales_batch.resolved? %> @@ -22,11 +22,21 @@ <% end %>
#
- Código do Lote: - <%= @sales_batch.batch_code %> +

+ + + Download do arquivo original + +
+ Código do Lote: + <%= @sales_batch.batch_code %> +

<% else %>

A processar

<%end%> -<%= link_to "Voltar para home", root_path %> +<%= link_to root_path, class: "btn btn-info" do %> + + Voltar +<% end %> diff --git a/app/views/shareds/_navbar.html.erb b/app/views/shareds/_navbar.html.erb new file mode 100644 index 00000000..b640aa3a --- /dev/null +++ b/app/views/shareds/_navbar.html.erb @@ -0,0 +1,45 @@ + diff --git a/app/workers/sales_batch_worker.rb b/app/workers/sales_batch_worker.rb new file mode 100644 index 00000000..cb28edc5 --- /dev/null +++ b/app/workers/sales_batch_worker.rb @@ -0,0 +1,50 @@ +require 'csv' + +class SalesBatchWorker + include Sidekiq::Worker + + sidekiq_options queue: :sales + + CSV_HEADERS = %w(customer description unit_price amount address supplier) + + def perform(id) + batch = SalesBatch.find(id) + + batch.process! + + tsv = generate_tsv(batch.attachment.file.path) + + generate_sales_attrs(tsv).each do |sale_attr| + batch.sales.create(sale_attr) + end + + calc_revenue(batch) + batch.resolve! + # rescue => e + # logger.error "Erro de execução de job no sidekiq #{e}" + # batch.reject! + end + + private + + def calc_revenue(batch) + total_revenue = batch.sales.map do |sale| + sale.amount * sale.unit_price + end.reduce(:+) + batch.update_attributes({ revenue: total_revenue }) + end + + def generate_sales_attrs(tsv) + tsv.map do |row| + row.each_with_index.map do |value, index| + Hash[ CSV_HEADERS[index], value ] + end.reduce(&:merge) + end + end + + def generate_tsv(file_path) + data = CSV.read(file_path, col_sep: "\t") + data.delete_at(0) + data + end +end diff --git a/bin/webpack b/bin/webpack new file mode 100755 index 00000000..528233a7 --- /dev/null +++ b/bin/webpack @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +$stdout.sync = true + +require "shellwords" + +ENV["RAILS_ENV"] ||= "development" +RAILS_ENV = ENV["RAILS_ENV"] + +ENV["NODE_ENV"] ||= RAILS_ENV +NODE_ENV = ENV["NODE_ENV"] + +APP_PATH = File.expand_path("../", __dir__) +NODE_MODULES_PATH = File.join(APP_PATH, "node_modules") +WEBPACK_CONFIG = File.join(APP_PATH, "config/webpack/#{NODE_ENV}.js") + +unless File.exist?(WEBPACK_CONFIG) + puts "Webpack configuration not found." + puts "Please run bundle exec rails webpacker:install to install webpacker" + exit! +end + +env = { "NODE_PATH" => NODE_MODULES_PATH.shellescape } +cmd = [ "#{NODE_MODULES_PATH}/.bin/webpack", "--config", WEBPACK_CONFIG ] + ARGV + +Dir.chdir(APP_PATH) do + exec env, *cmd +end diff --git a/bin/webpack-dev-server b/bin/webpack-dev-server new file mode 100755 index 00000000..c9672f66 --- /dev/null +++ b/bin/webpack-dev-server @@ -0,0 +1,68 @@ +#!/usr/bin/env ruby +$stdout.sync = true + +require "shellwords" +require "yaml" +require "socket" + +ENV["RAILS_ENV"] ||= "development" +RAILS_ENV = ENV["RAILS_ENV"] + +ENV["NODE_ENV"] ||= RAILS_ENV +NODE_ENV = ENV["NODE_ENV"] + +APP_PATH = File.expand_path("../", __dir__) +CONFIG_FILE = File.join(APP_PATH, "config/webpacker.yml") +NODE_MODULES_PATH = File.join(APP_PATH, "node_modules") +WEBPACK_CONFIG = File.join(APP_PATH, "config/webpack/#{NODE_ENV}.js") + +DEFAULT_LISTEN_HOST_ADDR = NODE_ENV == 'development' ? 'localhost' : '0.0.0.0' + +def args(key) + index = ARGV.index(key) + index ? ARGV[index + 1] : nil +end + +begin + dev_server = YAML.load_file(CONFIG_FILE)[RAILS_ENV]["dev_server"] + + HOSTNAME = args('--host') || dev_server["host"] + PORT = args('--port') || dev_server["port"] + HTTPS = ARGV.include?('--https') || dev_server["https"] + DEV_SERVER_ADDR = "http#{"s" if HTTPS}://#{HOSTNAME}:#{PORT}" + LISTEN_HOST_ADDR = args('--listen-host') || DEFAULT_LISTEN_HOST_ADDR + +rescue Errno::ENOENT, NoMethodError + $stdout.puts "Webpack dev_server configuration not found in #{CONFIG_FILE}." + $stdout.puts "Please run bundle exec rails webpacker:install to install webpacker" + exit! +end + +begin + server = TCPServer.new(LISTEN_HOST_ADDR, PORT) + server.close + +rescue Errno::EADDRINUSE + $stdout.puts "Another program is running on port #{PORT}. Set a new port in #{CONFIG_FILE} for dev_server" + exit! +end + +# Delete supplied host, port and listen-host CLI arguments +["--host", "--port", "--listen-host"].each do |arg| + ARGV.delete(args(arg)) + ARGV.delete(arg) +end + +env = { "NODE_PATH" => NODE_MODULES_PATH.shellescape } + +cmd = [ + "#{NODE_MODULES_PATH}/.bin/webpack-dev-server", "--progress", "--color", + "--config", WEBPACK_CONFIG, + "--host", LISTEN_HOST_ADDR, + "--public", "#{HOSTNAME}:#{PORT}", + "--port", PORT.to_s +] + ARGV + +Dir.chdir(APP_PATH) do + exec env, *cmd +end diff --git a/bower.json b/bower.json index 63931ebf..c22c2dd2 100644 --- a/bower.json +++ b/bower.json @@ -1,8 +1,6 @@ { "name": "Splunka", - "authors": [ - "Paulo Patto " - ], + "authors": [ "Paulo Patto " ], "description": "", "main": "", "license": "MIT", diff --git a/config/routes.rb b/config/routes.rb index 15a82e78..07544a95 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,4 +3,7 @@ resources :sales_batches, only: [:new, :create, :show, :index] resources :sales, only: [:show] + + require 'sidekiq/web' + mount Sidekiq::Web => '/sidekiq' end diff --git a/config/sidekiq.yml b/config/sidekiq.yml new file mode 100644 index 00000000..ad1cbaa7 --- /dev/null +++ b/config/sidekiq.yml @@ -0,0 +1,23 @@ +default: &default + :service: 'redis://localhost:6379' + :logfile: ./log/sidekiq.log + :queues: + - sales + +development: + <<: *default + +test: + <<: *default + +qa: + <<: *default + +production: + :service: 'redis://localhost:6379' + :user: 'admin' + :password: 'admin' + :logfile: ./log/sidekiq.log + :queues: + - sales + diff --git a/config/sidekiq.yml.example b/config/sidekiq.yml.example new file mode 100644 index 00000000..ad1cbaa7 --- /dev/null +++ b/config/sidekiq.yml.example @@ -0,0 +1,23 @@ +default: &default + :service: 'redis://localhost:6379' + :logfile: ./log/sidekiq.log + :queues: + - sales + +development: + <<: *default + +test: + <<: *default + +qa: + <<: *default + +production: + :service: 'redis://localhost:6379' + :user: 'admin' + :password: 'admin' + :logfile: ./log/sidekiq.log + :queues: + - sales + diff --git a/config/webpack/development.js b/config/webpack/development.js new file mode 100644 index 00000000..81269f65 --- /dev/null +++ b/config/webpack/development.js @@ -0,0 +1,3 @@ +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/config/webpack/environment.js b/config/webpack/environment.js new file mode 100644 index 00000000..d16d9af7 --- /dev/null +++ b/config/webpack/environment.js @@ -0,0 +1,3 @@ +const { environment } = require('@rails/webpacker') + +module.exports = environment diff --git a/config/webpack/production.js b/config/webpack/production.js new file mode 100644 index 00000000..81269f65 --- /dev/null +++ b/config/webpack/production.js @@ -0,0 +1,3 @@ +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/config/webpack/test.js b/config/webpack/test.js new file mode 100644 index 00000000..81269f65 --- /dev/null +++ b/config/webpack/test.js @@ -0,0 +1,3 @@ +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/config/webpacker.yml b/config/webpacker.yml new file mode 100644 index 00000000..e62a7acd --- /dev/null +++ b/config/webpacker.yml @@ -0,0 +1,56 @@ +# Note: You must restart bin/webpack-dev-server for changes to take effect + +default: &default + source_path: app/javascript + source_entry_path: packs + public_output_path: packs + cache_path: tmp/cache/webpacker + + # Additional paths webpack should lookup modules + # ['app/assets', 'engine/foo/app/assets'] + resolved_paths: [] + + # Reload manifest.json on all requests so we reload latest compiled packs + cache_manifest: false + + extensions: + - .coffee + - .erb + - .js + - .jsx + - .ts + - .vue + - .sass + - .scss + - .css + - .png + - .svg + - .gif + - .jpeg + - .jpg + +development: + <<: *default + compile: true + + dev_server: + host: localhost + port: 3035 + hmr: false + https: false + +test: + <<: *default + compile: true + + # Compile test packs to a separate directory + public_output_path: packs-test + +production: + <<: *default + + # Production depends on precompilation of packs prior to booting for performance. + compile: false + + # Cache manifest.json for performance + cache_manifest: true diff --git a/db/migrate/20160828191211_add_field_state_to_sales_batches.rb b/db/migrate/20160828191211_add_field_state_to_sales_batches.rb new file mode 100644 index 00000000..1badf710 --- /dev/null +++ b/db/migrate/20160828191211_add_field_state_to_sales_batches.rb @@ -0,0 +1,6 @@ +class AddFieldStateToSalesBatches < ActiveRecord::Migration + def change + add_column :sales_batches, :state, :string + remove_column :sales_batches, :processed + end +end diff --git a/db/schema.rb b/db/schema.rb index 37a28f16..71d112df 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160826050946) do +ActiveRecord::Schema.define(version: 20160828191211) do create_table "sales", force: :cascade do |t| t.string "customer" @@ -29,11 +29,11 @@ create_table "sales_batches", force: :cascade do |t| t.string "attachment" - t.boolean "processed", default: false t.decimal "revenue", precision: 12, scale: 2, default: 0.0 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "batch_code" + t.string "state" end add_index "sales_batches", ["batch_code"], name: "index_sales_batches_on_batch_code" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..374de222 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '2' +services: + db: + image: postgres + redis: + image: redis + ports: + - "6379":"6379" + # worker: + # build: . + # command: bundle exec sidkiq -C config/sidekiq.yml + # volumes: + # - .:/opt/splunka + web: + build: . + command: bundle exec rails server -p 3000 -b '0.0.0.0' + volumes: + - .:/opt/splunka + ports: + - '3000':'3000' + depends_on: + - db + - redis diff --git a/package.json b/package.json new file mode 100644 index 00000000..2d010ffa --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "splunka", + "version": "0.0.1", + "description": "My description", + "main": "index.js", + "directories": { + "lib": "lib" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/paulopatto/Splunka.git" + }, + "keywords": [ + "rails", + "react" + ], + "author": "Paulo Patto ", + "license": "MIT", + "bugs": { + "url": "https://github.com/paulopatto/Splunka/issues" + }, + "homepage": "https://github.com/paulopatto/Splunka#readme", + "dependencies": { + "@rails/webpacker": "^3.0.1", + "babel-preset-react": "^6.24.1", + "bower": "^1.8.0", + "phantomjs": "^2.1.7", + "prop-types": "^15.5.10", + "react": "^15.6.1", + "react-dom": "^15.6.1", + "yarn": "^0.27.5" + }, + "devDependencies": { + "webpack-dev-server": "^2.7.1" + } +} diff --git a/spec/controllers/sales_batches_controller_spec.rb b/spec/controllers/sales_batches_controller_spec.rb new file mode 100644 index 00000000..0011942f --- /dev/null +++ b/spec/controllers/sales_batches_controller_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +describe SalesBatchesController, type: :controller do + describe '#index' do + before do + get :index + end + + it 'returns with :success' do + expect(response).to be_success + end + + it 'render template :index' do + expect(response).to render_template :index + end + + context 'when does not have sales batches' do + it 'assigns :sales_batches empty' do + expect(assigns(:sales_batches)).to be_empty + end + end + + context 'when have any sales batches' do + let(:registers_no) { 3 } + before do + registers_no.times.each {|x| create(:sales_batch) } + get :index + end + + it 'assigns :sales_batches with all batches' do + expect(assigns(:sales_batches).count).to eq registers_no + end + end + end + + describe '#create' do + let(:sales_batches_params) do + { + attachment: Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec', 'fixtures', 'sales.txt')) + } + end + + + before do + allow(SalesBatchWorker).to receive(:perform_async) + post :create, sales_batch: sales_batches_params + end + + it 'batch persisted' do + expect(assigns(:batch)).to be_persisted + end + + it 'enqued job' do + expect(SalesBatchWorker).to have_received(:perform_async) + end + end +end diff --git a/spec/factories/sales_batches_factorie.rb b/spec/factories/sales_batches_factorie.rb index 87a60ad7..8f2dee83 100644 --- a/spec/factories/sales_batches_factorie.rb +++ b/spec/factories/sales_batches_factorie.rb @@ -1,5 +1,6 @@ FactoryGirl.define do factory :sales_batch do attachment { Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec', 'fixtures', 'sales.txt')) } + batch_code SecureRandom.uuid end end diff --git a/spec/features/uploading_new_sales_batch_spec.rb b/spec/features/uploading_new_sales_batch_spec.rb new file mode 100644 index 00000000..8095839c --- /dev/null +++ b/spec/features/uploading_new_sales_batch_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +feature 'Uploading and creating new Sales Batch', type: :feature do + let(:sales_batch_file_path) { 'spec/fixtures/sales.txt' } + + before do + visit root_path + attach_file 'sales_batch[attachment]', File.absolute_path(sales_batch_file_path) + click_on 'Enviar' + end + + it '' do + + end +end diff --git a/spec/models/sales_batch_spec.rb b/spec/models/sales_batch_spec.rb index 460d27d2..c8a6808b 100644 --- a/spec/models/sales_batch_spec.rb +++ b/spec/models/sales_batch_spec.rb @@ -4,11 +4,11 @@ # # id :integer not null, primary key # attachment :string -# processed :boolean default(FALSE) # revenue :decimal(12, 2) default(0.0) # created_at :datetime not null # updated_at :datetime not null # batch_code :string +# state :string # require 'rails_helper' @@ -18,7 +18,6 @@ is_expected.to have_many :sales end - context 'validates' do subject { build(:sales_batch) } @@ -26,8 +25,4 @@ is_expected.to validate_uniqueness_of :batch_code end end - - context 'callbacks' do - pending "Warn: Callbacks as anti pattern" - end end diff --git a/spec/support/capybara_support.rb b/spec/support/capybara_support.rb new file mode 100644 index 00000000..bbcfd251 --- /dev/null +++ b/spec/support/capybara_support.rb @@ -0,0 +1,2 @@ +require 'capybara/poltergeist' +Capybara.javascript_driver = :poltergeist diff --git a/spec/support/rspec_sidekiq_support.rb b/spec/support/rspec_sidekiq_support.rb new file mode 100644 index 00000000..a3c4c6e7 --- /dev/null +++ b/spec/support/rspec_sidekiq_support.rb @@ -0,0 +1,10 @@ +RSpec::Sidekiq.configure do |config| + # Clears all job queues before each example + config.clear_all_enqueued_jobs = true # default => true + + # Whether to use terminal colours when outputting messages + config.enable_terminal_colours = true # default => true + + # Warn when jobs are not enqueued to Redis but to a job array + config.warn_when_jobs_not_processed_by_sidekiq = true # default => true +end diff --git a/spec/support/sidekiq_support.rb b/spec/support/sidekiq_support.rb new file mode 100644 index 00000000..bba06318 --- /dev/null +++ b/spec/support/sidekiq_support.rb @@ -0,0 +1,7 @@ +require 'sidekiq/testing' + +RSpec.configure do |config| + config.before(:each) do + Sidekiq::Worker.clear_all + end +end diff --git a/task.md b/task.md index 5e5ee7a3..61a764b3 100644 --- a/task.md +++ b/task.md @@ -4,10 +4,10 @@ A idéia deste desafio é nos permitir avaliar melhor as habilidades de candidat Este desafio deve ser feito por você em sua casa. Gaste o tempo que você quiser, porém normalmente você não deve precisar de mais do que algumas horas. ## Instruções de entrega do desafio -1. Primeiro, faça um fork deste projeto para sua conta no Github (crie uma se você não possuir). -2. Em seguida, implemente o projeto tal qual descrito abaixo, em seu próprio fork. -3. Crie as instruções de instalação e execução do aplicativo em seu readme.md -4. Por fim, envie o link do seu repositorio para avaliarmos seu código +- 1. Primeiro, faça um fork deste projeto para sua conta no Github (crie uma se você não possuir). +- 2. Em seguida, implemente o projeto tal qual descrito abaixo, em seu próprio fork. +- 3. Crie as instruções de instalação e execução do aplicativo em seu readme.md +- 4. Por fim, envie o link do seu repositorio para avaliarmos seu código ## Descrição do projeto @@ -17,27 +17,29 @@ Sua tarefa é criar uma interface web que aceite upload de arquivos, normalize o Sua aplicação web DEVE: -1. Aceitar (via um formulário) o upload de arquivos text, com dados separados por TAB testar o aplicativo usando o arquivo fornecido. A primeira linha do arquivo tem o nome das colunas. Você pode assumir que as colunas estarão sempre nesta ordem, e que sempre haverá uma linha de cabeçalho. Um arquivo de exemplo chamado 'dados.txt' está incluído neste repositório. -2. Interpretar ("parsear") o arquivo recebido, normalizar os dados, e salvar corretamente a informação em um banco de dados relacional. -3. Exibir todos os registros importados, bem como a receita bruta total dos registros contidos no arquivo enviado após o upload + parser. -4. Se sua vaga é para Ruby e Ruby On Rails, ser escrita obrigatoriamente em: Ruby 2.1+ Rails 4 e SQLite -5. Se sua vaga é para .Net ser escrita obrigatoriamente em: VB# ou C#, última versão, SQL Server (pode ser express) -6. Ser simples de configurar e rodar a partir das instruções fornecidas, -7. funcionando em ambiente compatível com Unix (Linux ou Mac OS X) para Ruby On Rails e Windows para .Net. Ela deve utilizar apenas linguagens e bibliotecas livres ou gratuitas. -8. Ter um teste de model e controller automatizado para a funcionalidade pedida -9. Ter uma boa aparecia e ser fácil de usar +- 1. Aceitar (via um formulário) o upload de arquivos text, com dados separados por TAB testar o aplicativo usando o arquivo fornecido. A primeira linha do arquivo tem o nome das colunas. Você pode assumir que as colunas estarão sempre nesta ordem, e que sempre haverá uma linha de cabeçalho. Um arquivo de exemplo chamado 'dados.txt' está incluído neste repositório. +- 2. Interpretar ("parsear") o arquivo recebido, normalizar os dados, e salvar corretamente a informação em um banco de dados relacional. +- 3. Exibir todos os registros importados, bem como a receita bruta total dos registros contidos no arquivo enviado após o upload + parser. +- 4. Se sua vaga é para Ruby e Ruby On Rails, ser escrita obrigatoriamente em: Ruby 2.1+ Rails 4 e SQLite +- 5. Se sua vaga é para .Net ser escrita obrigatoriamente em: VB# ou C#, última versão, SQL Server (pode ser express) +- 6. Ser simples de configurar e rodar a partir das instruções fornecidas, +- 7. funcionando em ambiente compatível com Unix (Linux ou Mac OS X) para Ruby On Rails e Windows para .Net. Ela deve utilizar apenas linguagens e bibliotecas livres ou gratuitas. +- 8. Ter um teste de model e controller automatizado para a funcionalidade pedida +- 9. Ter uma boa aparecia e ser fácil de usar ## Avaliação Seu projeto será avaliado de acordo com os seguintes critérios. -1. Sua aplicação atende funcionalmente o que foi pedido -2. Você documentou a maneira de configurar o ambiente e rodar sua aplicação na maquina do avaliador -3. Você seguiu as instruções enviadas -4. Voce segue as boas práticas de programação e entrega para o Cliente -5. O código escrito é facil de entender e manter -6. Você se preocupa com o uso do aplicativo pelo Usuário +- 1. Sua aplicação atende funcionalmente o que foi pedido +- 2. Você documentou a maneira de configurar o ambiente e rodar sua aplicação na maquina do avaliador +- 3. Você seguiu as instruções enviadas +- 4. Voce segue as boas práticas de programação e entrega para o Cliente +- 5. O código escrito é facil de entender e manter +- 6. Você se preocupa com o uso do aplicativo pelo Usuário -Adicionalmente, tentaremos verificar a sua familiarização com as bibliotecas padrões (standard libs), bem como sua experiência com programação orientada a objetos a partir da estrutura de seu projeto, preucupação com o objetivo da aplicação e do seu uso pelo usuário, suporte e manutenção do código por outros desenvolvdores +Adicionalmente, tentaremos verificar a sua familiarização com as bibliotecas padrões (standard libs), +bem como sua experiência com programação orientada a objetos a partir da estrutura de seu projeto, +preucupação com o objetivo da aplicação e do seu uso pelo usuário, suporte e manutenção do código por outros desenvolvdores. ### Referência