diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE
index cfd5939478..c39592e8ab 100644
--- a/.github/PULL_REQUEST_TEMPLATE
+++ b/.github/PULL_REQUEST_TEMPLATE
@@ -4,12 +4,12 @@ Congratulations! You're submitting your assignment! These comprehension question
## Comprehension Questions
Question | Answer
:------------- | :-------------
-How did your team break up the work to be done? |
-How did your team utilize git to collaborate? |
-What did your group do to try to keep your code DRY while many people collaborated on it? |
-What was a technical challenge that you faced as a group? |
-What was a team/personal challenge that you faced as a group? |
-What could your team have done better? |
-What was your application's ERD? (include a link) |
-What is your Trello URL? |
-What is the Heroku URL of your deployed application? |
+How did your team break up the work to be done? | We divided work by MVC. We have one Model/DB person, one View person, and two Controller people. Most of the individual work were done at home. Whenever there was a need to collaborate or communicate, we did it at Ada as a whole group or pairs.
+How did your team utilize git to collaborate? | All of us had our own branches on local and/or github, especially for features we were not sure about. We learned about how to resolve merge conflicts, and realized that no merge conflicts doesn't mean no errors during merging. Although we planned to use PR in our project, we didn't try it because of time constraint. We should definitely use it in our future group project.
+What did your group do to try to keep your code DRY while many people collaborated on it? | It's hard... We kinda prioritize our work to make the app work, rather than writing beautiful codes. We did some cleanup when the app was 90% ready for demo. We probably will do more after final deployment.
+What was a technical challenge that you faced as a group? | Cart logic. After discussions with tutor and PM, we realized that there were several ways to implement it. We compared them and chose one based on how comfortable we were with the different options. It still took us a long time to implement that function.
+What was a team/personal challenge that you faced as a group? | We believed that all members in the team did their best to make our app not just work but better than we expected.
+What could your team have done better? | We probably should write controller tests first next time.
+What was your application's ERD? (include a link) | https://www.lucidchart.com/documents/edit/619b0172-f884-42d1-91fe-ea00101d45c1/0
+What is your Trello URL? | https://trello.com/b/2tpzc7EV/puppsy
+What is the Heroku URL of your deployed application? | https://puppsy.herokuapp.com/
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..8a800278b3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+# See https://help.github.com/articles/ignoring-files for more about ignoring files.
+#
+# If you find yourself ignoring temporary files generated by your text editor
+# or operating system, you probably want to add a global ignore instead:
+# git config --global core.excludesfile '~/.gitignore_global'
+
+# Ignore bundler config.
+/.bundle
+
+# Ignore all logfiles and tempfiles.
+/log/*
+/tmp/*
+!/log/.keep
+!/tmp/.keep
+
+/node_modules
+/yarn-error.log
+
+.env
+.byebug_history
+
+.DS_Store
+coverage
+
+.env
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000000..e94a42424a
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,80 @@
+source 'https://rubygems.org'
+
+git_source(:github) do |repo_name|
+ repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
+ "https://github.com/#{repo_name}.git"
+end
+
+
+# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
+gem 'rails', '~> 5.1.6'
+# Use postgresql as the database for Active Record
+gem 'pg', '>= 0.18', '< 2.0'
+# Use Puma as the app server
+gem 'puma', '~> 3.7'
+# Use SCSS for stylesheets
+gem 'sass-rails', '~> 5.0'
+# Use Uglifier as compressor for JavaScript assets
+gem 'uglifier', '>= 1.3.0'
+# See https://github.com/rails/execjs#readme for more supported runtimes
+# gem 'therubyracer', platforms: :ruby
+
+# Use CoffeeScript for .coffee assets and views
+# gem 'coffee-rails', '~> 4.2'
+# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
+gem 'turbolinks', '~> 5'
+# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
+gem 'jbuilder', '~> 2.5'
+# Use Redis adapter to run Action Cable in production
+# gem 'redis', '~> 4.0'
+# Use ActiveModel has_secure_password
+# gem 'bcrypt', '~> 3.1.7'
+
+# Use Capistrano for deployment
+# gem 'capistrano-rails', group: :development
+
+group :development, :test do
+ # Call 'byebug' anywhere in the code to stop execution and get a debugger console
+ gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
+ # Adds support for Capybara system testing and selenium driver
+ gem 'capybara', '~> 2.13'
+ gem 'selenium-webdriver'
+end
+
+group :development do
+ # Access an IRB console on exception pages or by using <%= console %> anywhere in the code.
+ gem 'web-console', '>= 3.3.0'
+ gem 'listen', '>= 3.0.5', '< 3.2'
+ # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
+ gem 'spring'
+ gem 'spring-watcher-listen', '~> 2.0.0'
+end
+
+# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
+gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
+
+gem 'jquery-turbolinks'
+gem 'jquery-rails'
+gem 'foundation-rails'
+gem 'normalize-rails'
+group :development, :test do
+ gem 'pry-rails'
+end
+
+group :development do
+ gem 'better_errors'
+ gem 'binding_of_caller'
+ gem 'dotenv-rails'
+end
+
+group :test do
+ gem 'minitest-rails'
+ gem 'minitest-reporters'
+ gem 'simplecov', require: false
+end
+
+
+gem "omniauth"
+gem "omniauth-github"
+
+gem 'client_side_validations'
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000000..0cb6d30b5b
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,280 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ actioncable (5.1.6)
+ actionpack (= 5.1.6)
+ nio4r (~> 2.0)
+ websocket-driver (~> 0.6.1)
+ actionmailer (5.1.6)
+ actionpack (= 5.1.6)
+ actionview (= 5.1.6)
+ activejob (= 5.1.6)
+ mail (~> 2.5, >= 2.5.4)
+ rails-dom-testing (~> 2.0)
+ actionpack (5.1.6)
+ actionview (= 5.1.6)
+ activesupport (= 5.1.6)
+ rack (~> 2.0)
+ rack-test (>= 0.6.3)
+ rails-dom-testing (~> 2.0)
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
+ actionview (5.1.6)
+ activesupport (= 5.1.6)
+ builder (~> 3.1)
+ erubi (~> 1.4)
+ rails-dom-testing (~> 2.0)
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
+ activejob (5.1.6)
+ activesupport (= 5.1.6)
+ globalid (>= 0.3.6)
+ activemodel (5.1.6)
+ activesupport (= 5.1.6)
+ activerecord (5.1.6)
+ activemodel (= 5.1.6)
+ activesupport (= 5.1.6)
+ arel (~> 8.0)
+ activesupport (5.1.6)
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ i18n (>= 0.7, < 2)
+ minitest (~> 5.1)
+ tzinfo (~> 1.1)
+ addressable (2.5.2)
+ public_suffix (>= 2.0.2, < 4.0)
+ ansi (1.5.0)
+ arel (8.0.0)
+ babel-source (5.8.35)
+ babel-transpiler (0.7.0)
+ babel-source (>= 4.0, < 6)
+ execjs (~> 2.0)
+ better_errors (2.4.0)
+ coderay (>= 1.0.0)
+ erubi (>= 1.0.0)
+ rack (>= 0.9.0)
+ bindex (0.5.0)
+ binding_of_caller (0.8.0)
+ debug_inspector (>= 0.0.1)
+ builder (3.2.3)
+ byebug (10.0.2)
+ capybara (2.18.0)
+ addressable
+ mini_mime (>= 0.1.3)
+ nokogiri (>= 1.3.3)
+ rack (>= 1.0.0)
+ rack-test (>= 0.5.4)
+ xpath (>= 2.0, < 4.0)
+ childprocess (0.9.0)
+ ffi (~> 1.0, >= 1.0.11)
+ client_side_validations (11.1.2)
+ jquery-rails (~> 4.3)
+ js_regex (~> 2.2)
+ rails (>= 5.0.0.1, <= 6.0)
+ coderay (1.1.2)
+ concurrent-ruby (1.0.5)
+ crass (1.0.4)
+ debug_inspector (0.0.3)
+ docile (1.3.0)
+ dotenv (2.3.0)
+ dotenv-rails (2.3.0)
+ dotenv (= 2.3.0)
+ railties (>= 3.2, < 6.0)
+ erubi (1.7.1)
+ execjs (2.7.0)
+ faraday (0.12.2)
+ multipart-post (>= 1.2, < 3)
+ ffi (1.9.23)
+ foundation-rails (6.4.3.0)
+ railties (>= 3.1.0)
+ sass (>= 3.3.0, < 3.5)
+ sprockets-es6 (>= 0.9.0)
+ globalid (0.4.1)
+ activesupport (>= 4.2.0)
+ hashie (3.5.7)
+ i18n (1.0.1)
+ concurrent-ruby (~> 1.0)
+ jbuilder (2.7.0)
+ activesupport (>= 4.2.0)
+ multi_json (>= 1.2)
+ jquery-rails (4.3.3)
+ rails-dom-testing (>= 1, < 3)
+ railties (>= 4.2.0)
+ thor (>= 0.14, < 2.0)
+ jquery-turbolinks (2.1.0)
+ railties (>= 3.1.0)
+ turbolinks
+ js_regex (2.2.1)
+ regexp_parser (>= 0.4.11, <= 0.5.0)
+ json (2.1.0)
+ jwt (1.5.6)
+ listen (3.1.5)
+ rb-fsevent (~> 0.9, >= 0.9.4)
+ rb-inotify (~> 0.9, >= 0.9.7)
+ ruby_dep (~> 1.2)
+ loofah (2.2.2)
+ crass (~> 1.0.2)
+ nokogiri (>= 1.5.9)
+ mail (2.7.0)
+ mini_mime (>= 0.1.1)
+ method_source (0.9.0)
+ mini_mime (1.0.0)
+ mini_portile2 (2.3.0)
+ minitest (5.11.3)
+ minitest-rails (3.0.0)
+ minitest (~> 5.8)
+ railties (~> 5.0)
+ minitest-reporters (1.2.0)
+ ansi
+ builder
+ minitest (>= 5.0)
+ ruby-progressbar
+ multi_json (1.13.1)
+ multi_xml (0.6.0)
+ multipart-post (2.0.0)
+ nio4r (2.3.0)
+ nokogiri (1.8.2)
+ mini_portile2 (~> 2.3.0)
+ normalize-rails (4.1.1)
+ oauth2 (1.4.0)
+ faraday (>= 0.8, < 0.13)
+ jwt (~> 1.0)
+ multi_json (~> 1.3)
+ multi_xml (~> 0.5)
+ rack (>= 1.2, < 3)
+ omniauth (1.8.1)
+ hashie (>= 3.4.6, < 3.6.0)
+ rack (>= 1.6.2, < 3)
+ omniauth-github (1.3.0)
+ omniauth (~> 1.5)
+ omniauth-oauth2 (>= 1.4.0, < 2.0)
+ omniauth-oauth2 (1.5.0)
+ oauth2 (~> 1.1)
+ omniauth (~> 1.2)
+ pg (1.0.0)
+ pry (0.11.3)
+ coderay (~> 1.1.0)
+ method_source (~> 0.9.0)
+ pry-rails (0.3.6)
+ pry (>= 0.10.4)
+ public_suffix (3.0.2)
+ puma (3.11.4)
+ rack (2.0.4)
+ rack-test (1.0.0)
+ rack (>= 1.0, < 3)
+ rails (5.1.6)
+ actioncable (= 5.1.6)
+ actionmailer (= 5.1.6)
+ actionpack (= 5.1.6)
+ actionview (= 5.1.6)
+ activejob (= 5.1.6)
+ activemodel (= 5.1.6)
+ activerecord (= 5.1.6)
+ activesupport (= 5.1.6)
+ bundler (>= 1.3.0)
+ railties (= 5.1.6)
+ sprockets-rails (>= 2.0.0)
+ rails-dom-testing (2.0.3)
+ activesupport (>= 4.2.0)
+ nokogiri (>= 1.6)
+ rails-html-sanitizer (1.0.4)
+ loofah (~> 2.2, >= 2.2.2)
+ railties (5.1.6)
+ actionpack (= 5.1.6)
+ activesupport (= 5.1.6)
+ method_source
+ rake (>= 0.8.7)
+ thor (>= 0.18.1, < 2.0)
+ rake (12.3.1)
+ rb-fsevent (0.10.3)
+ rb-inotify (0.9.10)
+ ffi (>= 0.5.0, < 2)
+ regexp_parser (0.4.13)
+ ruby-progressbar (1.9.0)
+ ruby_dep (1.5.0)
+ rubyzip (1.2.1)
+ sass (3.4.25)
+ sass-rails (5.0.7)
+ railties (>= 4.0.0, < 6)
+ sass (~> 3.1)
+ sprockets (>= 2.8, < 4.0)
+ sprockets-rails (>= 2.0, < 4.0)
+ tilt (>= 1.1, < 3)
+ selenium-webdriver (3.11.0)
+ childprocess (~> 0.5)
+ rubyzip (~> 1.2)
+ simplecov (0.16.1)
+ docile (~> 1.1)
+ json (>= 1.8, < 3)
+ simplecov-html (~> 0.10.0)
+ simplecov-html (0.10.2)
+ spring (2.0.2)
+ activesupport (>= 4.2)
+ spring-watcher-listen (2.0.1)
+ listen (>= 2.7, < 4.0)
+ spring (>= 1.2, < 3.0)
+ sprockets (3.7.1)
+ concurrent-ruby (~> 1.0)
+ rack (> 1, < 3)
+ sprockets-es6 (0.9.2)
+ babel-source (>= 5.8.11)
+ babel-transpiler
+ sprockets (>= 3.0.0)
+ sprockets-rails (3.2.1)
+ actionpack (>= 4.0)
+ activesupport (>= 4.0)
+ sprockets (>= 3.0.0)
+ thor (0.20.0)
+ thread_safe (0.3.6)
+ tilt (2.0.8)
+ turbolinks (5.1.1)
+ turbolinks-source (~> 5.1)
+ turbolinks-source (5.1.0)
+ tzinfo (1.2.5)
+ thread_safe (~> 0.1)
+ uglifier (4.1.10)
+ execjs (>= 0.3.0, < 3)
+ web-console (3.6.1)
+ actionview (>= 5.0)
+ activemodel (>= 5.0)
+ bindex (>= 0.4.0)
+ railties (>= 5.0)
+ websocket-driver (0.6.5)
+ websocket-extensions (>= 0.1.0)
+ websocket-extensions (0.1.3)
+ xpath (3.0.0)
+ nokogiri (~> 1.8)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ better_errors
+ binding_of_caller
+ byebug
+ capybara (~> 2.13)
+ client_side_validations
+ dotenv-rails
+ foundation-rails
+ jbuilder (~> 2.5)
+ jquery-rails
+ jquery-turbolinks
+ listen (>= 3.0.5, < 3.2)
+ minitest-rails
+ minitest-reporters
+ normalize-rails
+ omniauth
+ omniauth-github
+ pg (>= 0.18, < 2.0)
+ pry-rails
+ puma (~> 3.7)
+ rails (~> 5.1.6)
+ sass-rails (~> 5.0)
+ selenium-webdriver
+ simplecov
+ spring
+ spring-watcher-listen (~> 2.0.0)
+ turbolinks (~> 5)
+ tzinfo-data
+ uglifier (>= 1.3.0)
+ web-console (>= 3.3.0)
+
+BUNDLED WITH
+ 1.16.1
diff --git a/README.md b/README.md
index 2758975bf5..8c9f25b6ab 100644
--- a/README.md
+++ b/README.md
@@ -203,3 +203,9 @@ This project is due EOD Apr 27 via PR against Ada-C9/betsy.
## What Instructors Are Looking For
Check out the [feedback template](feedback.md) which lists the items instructors will be looking for as they evaluate your project.
+
+
+
+CODE PARKING LOT:
+
+ <%= button_to "PAY AND PLACE ORDER", {:action =>"update_to_paid", :controller => "cart" }, method: :patch, class: "button" %>
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000000..e85f913914
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,6 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require_relative 'config/application'
+
+Rails.application.load_tasks
diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js
new file mode 100644
index 0000000000..b16e53d6d5
--- /dev/null
+++ b/app/assets/config/manifest.js
@@ -0,0 +1,3 @@
+//= link_tree ../images
+//= link_directory ../javascripts .js
+//= link_directory ../stylesheets .css
diff --git a/app/assets/images/.keep b/app/assets/images/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/app/assets/images/1.png b/app/assets/images/1.png
new file mode 100644
index 0000000000..ffccfd1943
Binary files /dev/null and b/app/assets/images/1.png differ
diff --git a/app/assets/images/10.jpg b/app/assets/images/10.jpg
new file mode 100644
index 0000000000..44c89dd6e3
Binary files /dev/null and b/app/assets/images/10.jpg differ
diff --git a/app/assets/images/11.jpg b/app/assets/images/11.jpg
new file mode 100644
index 0000000000..5d2fa18247
Binary files /dev/null and b/app/assets/images/11.jpg differ
diff --git a/app/assets/images/12.jpg b/app/assets/images/12.jpg
new file mode 100644
index 0000000000..b67541bd58
Binary files /dev/null and b/app/assets/images/12.jpg differ
diff --git a/app/assets/images/13.jpg b/app/assets/images/13.jpg
new file mode 100644
index 0000000000..59a7b4ab85
Binary files /dev/null and b/app/assets/images/13.jpg differ
diff --git a/app/assets/images/14.jpg b/app/assets/images/14.jpg
new file mode 100644
index 0000000000..7618f7d92d
Binary files /dev/null and b/app/assets/images/14.jpg differ
diff --git a/app/assets/images/15.jpg b/app/assets/images/15.jpg
new file mode 100644
index 0000000000..9cb995e412
Binary files /dev/null and b/app/assets/images/15.jpg differ
diff --git a/app/assets/images/16.jpg b/app/assets/images/16.jpg
new file mode 100644
index 0000000000..d83aa74520
Binary files /dev/null and b/app/assets/images/16.jpg differ
diff --git a/app/assets/images/17.jpg b/app/assets/images/17.jpg
new file mode 100644
index 0000000000..d531d07d35
Binary files /dev/null and b/app/assets/images/17.jpg differ
diff --git a/app/assets/images/2.png b/app/assets/images/2.png
new file mode 100644
index 0000000000..ad4587d7e2
Binary files /dev/null and b/app/assets/images/2.png differ
diff --git a/app/assets/images/3.jpg b/app/assets/images/3.jpg
new file mode 100644
index 0000000000..97032b212e
Binary files /dev/null and b/app/assets/images/3.jpg differ
diff --git a/app/assets/images/4.jpg b/app/assets/images/4.jpg
new file mode 100644
index 0000000000..01cf1456f6
Binary files /dev/null and b/app/assets/images/4.jpg differ
diff --git a/app/assets/images/5.jpg b/app/assets/images/5.jpg
new file mode 100644
index 0000000000..5a151ab5c0
Binary files /dev/null and b/app/assets/images/5.jpg differ
diff --git a/app/assets/images/6.jpg b/app/assets/images/6.jpg
new file mode 100644
index 0000000000..cc81723be2
Binary files /dev/null and b/app/assets/images/6.jpg differ
diff --git a/app/assets/images/7.png b/app/assets/images/7.png
new file mode 100644
index 0000000000..02e5ad40a6
Binary files /dev/null and b/app/assets/images/7.png differ
diff --git a/app/assets/images/8.png b/app/assets/images/8.png
new file mode 100644
index 0000000000..5bd0f1e7d9
Binary files /dev/null and b/app/assets/images/8.png differ
diff --git a/app/assets/images/9.png b/app/assets/images/9.png
new file mode 100644
index 0000000000..cc4550d518
Binary files /dev/null and b/app/assets/images/9.png differ
diff --git a/app/assets/images/dog-1.jpg b/app/assets/images/dog-1.jpg
new file mode 100644
index 0000000000..37b83da6eb
Binary files /dev/null and b/app/assets/images/dog-1.jpg differ
diff --git a/app/assets/images/dog-2.jpg b/app/assets/images/dog-2.jpg
new file mode 100644
index 0000000000..1b34373dbd
Binary files /dev/null and b/app/assets/images/dog-2.jpg differ
diff --git a/app/assets/images/dog-3.jpg b/app/assets/images/dog-3.jpg
new file mode 100644
index 0000000000..0ceeae54c7
Binary files /dev/null and b/app/assets/images/dog-3.jpg differ
diff --git a/app/assets/images/dog-4.jpg b/app/assets/images/dog-4.jpg
new file mode 100644
index 0000000000..64c7f4c9db
Binary files /dev/null and b/app/assets/images/dog-4.jpg differ
diff --git a/app/assets/images/dog-5.jpg b/app/assets/images/dog-5.jpg
new file mode 100644
index 0000000000..248048e801
Binary files /dev/null and b/app/assets/images/dog-5.jpg differ
diff --git a/app/assets/images/dog-with-keys.png b/app/assets/images/dog-with-keys.png
new file mode 100644
index 0000000000..3013e014c4
Binary files /dev/null and b/app/assets/images/dog-with-keys.png differ
diff --git a/app/assets/images/favicon.png b/app/assets/images/favicon.png
new file mode 100644
index 0000000000..a7e1171dd9
Binary files /dev/null and b/app/assets/images/favicon.png differ
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
new file mode 100644
index 0000000000..a0e7aa5784
--- /dev/null
+++ b/app/assets/javascripts/application.js
@@ -0,0 +1,22 @@
+// This is a manifest file that'll be compiled into application.js, which will include all the files
+// listed below.
+//
+// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
+// vendor/assets/javascripts directory can be referenced here using a relative path.
+//
+// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+// compiled file. JavaScript code in this file should be added after the last require_* statement.
+//
+// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
+// about supported directives.
+//= require jquery
+
+//
+//= require rails-ujs
+//= require foundation
+//= require turbolinks
+//= require_tree .
+
+//= require rails.validations
+
+$(function(){ $(document).foundation(); });
diff --git a/app/assets/javascripts/cable.js b/app/assets/javascripts/cable.js
new file mode 100644
index 0000000000..739aa5f022
--- /dev/null
+++ b/app/assets/javascripts/cable.js
@@ -0,0 +1,13 @@
+// Action Cable provides the framework to deal with WebSockets in Rails.
+// You can generate new channels where WebSocket features live using the `rails generate channel` command.
+//
+//= require action_cable
+//= require_self
+//= require_tree ./channels
+
+(function() {
+ this.App || (this.App = {});
+
+ App.cable = ActionCable.createConsumer();
+
+}).call(this);
diff --git a/app/assets/javascripts/cart.js b/app/assets/javascripts/cart.js
new file mode 100644
index 0000000000..dee720facd
--- /dev/null
+++ b/app/assets/javascripts/cart.js
@@ -0,0 +1,2 @@
+// Place all the behaviors and hooks related to the matching controller here.
+// All this logic will automatically be available in application.js.
diff --git a/app/assets/javascripts/categories.js b/app/assets/javascripts/categories.js
new file mode 100644
index 0000000000..dee720facd
--- /dev/null
+++ b/app/assets/javascripts/categories.js
@@ -0,0 +1,2 @@
+// Place all the behaviors and hooks related to the matching controller here.
+// All this logic will automatically be available in application.js.
diff --git a/app/assets/javascripts/channels/.keep b/app/assets/javascripts/channels/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/app/assets/javascripts/homepage.js b/app/assets/javascripts/homepage.js
new file mode 100644
index 0000000000..dee720facd
--- /dev/null
+++ b/app/assets/javascripts/homepage.js
@@ -0,0 +1,2 @@
+// Place all the behaviors and hooks related to the matching controller here.
+// All this logic will automatically be available in application.js.
diff --git a/app/assets/javascripts/order_items.js b/app/assets/javascripts/order_items.js
new file mode 100644
index 0000000000..dee720facd
--- /dev/null
+++ b/app/assets/javascripts/order_items.js
@@ -0,0 +1,2 @@
+// Place all the behaviors and hooks related to the matching controller here.
+// All this logic will automatically be available in application.js.
diff --git a/app/assets/javascripts/orders.js b/app/assets/javascripts/orders.js
new file mode 100644
index 0000000000..dee720facd
--- /dev/null
+++ b/app/assets/javascripts/orders.js
@@ -0,0 +1,2 @@
+// Place all the behaviors and hooks related to the matching controller here.
+// All this logic will automatically be available in application.js.
diff --git a/app/assets/javascripts/products.js b/app/assets/javascripts/products.js
new file mode 100644
index 0000000000..dee720facd
--- /dev/null
+++ b/app/assets/javascripts/products.js
@@ -0,0 +1,2 @@
+// Place all the behaviors and hooks related to the matching controller here.
+// All this logic will automatically be available in application.js.
diff --git a/app/assets/javascripts/rails.validations.js b/app/assets/javascripts/rails.validations.js
new file mode 100644
index 0000000000..661990d961
--- /dev/null
+++ b/app/assets/javascripts/rails.validations.js
@@ -0,0 +1,584 @@
+
+/*!
+ * Client Side Validations - v11.1.2 (https://github.com/DavyJonesLocker/client_side_validations)
+ * Copyright (c) 2018 Geremia Taglialatela, Brian Cardarella
+ * Licensed under MIT (http://opensource.org/licenses/mit-license.php)
+ */
+
+(function() {
+ var $, ClientSideValidations, initializeOnEvent, validateElement, validateForm, validatorsFor,
+ indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+
+ $ = jQuery;
+
+ $.fn.disableClientSideValidations = function() {
+ ClientSideValidations.disable(this);
+ return this;
+ };
+
+ $.fn.enableClientSideValidations = function() {
+ this.filter(ClientSideValidations.selectors.forms).each(function() {
+ return ClientSideValidations.enablers.form(this);
+ });
+ this.filter(ClientSideValidations.selectors.inputs).each(function() {
+ return ClientSideValidations.enablers.input(this);
+ });
+ return this;
+ };
+
+ $.fn.resetClientSideValidations = function() {
+ this.filter(ClientSideValidations.selectors.forms).each(function() {
+ return ClientSideValidations.reset(this);
+ });
+ return this;
+ };
+
+ $.fn.validate = function() {
+ this.filter(ClientSideValidations.selectors.forms).each(function() {
+ return $(this).enableClientSideValidations();
+ });
+ return this;
+ };
+
+ $.fn.isValid = function(validators) {
+ var obj;
+ obj = $(this[0]);
+ if (obj.is('form')) {
+ return validateForm(obj, validators);
+ } else {
+ return validateElement(obj, validatorsFor(this[0].name, validators));
+ }
+ };
+
+ validatorsFor = function(name, validators) {
+ var captures, validator, validator_name;
+ if (validators.hasOwnProperty(name)) {
+ return validators[name];
+ }
+ name = name.replace(/\[(\w+_attributes)\]\[[\da-z_]+\](?=\[(?:\w+_attributes)\])/g, '[$1][]');
+ if (captures = name.match(/\[(\w+_attributes)\].*\[(\w+)\]$/)) {
+ for (validator_name in validators) {
+ validator = validators[validator_name];
+ if (validator_name.match("\\[" + captures[1] + "\\].*\\[\\]\\[" + captures[2] + "\\]$")) {
+ name = name.replace(/\[[\da-z_]+\]\[(\w+)\]$/g, '[][$1]');
+ }
+ }
+ }
+ return validators[name] || {};
+ };
+
+ validateForm = function(form, validators) {
+ var valid;
+ form.trigger('form:validate:before.ClientSideValidations');
+ valid = true;
+ form.find(ClientSideValidations.selectors.validate_inputs).each(function() {
+ if (!$(this).isValid(validators)) {
+ valid = false;
+ }
+ return true;
+ });
+ if (valid) {
+ form.trigger('form:validate:pass.ClientSideValidations');
+ } else {
+ form.trigger('form:validate:fail.ClientSideValidations');
+ }
+ form.trigger('form:validate:after.ClientSideValidations');
+ return valid;
+ };
+
+ validateElement = function(element, validators) {
+ var afterValidate, destroyInputName, executeValidators, failElement, local, passElement, remote;
+ element.trigger('element:validate:before.ClientSideValidations');
+ passElement = function() {
+ return element.trigger('element:validate:pass.ClientSideValidations').data('valid', null);
+ };
+ failElement = function(message) {
+ element.trigger('element:validate:fail.ClientSideValidations', message).data('valid', false);
+ return false;
+ };
+ afterValidate = function() {
+ return element.trigger('element:validate:after.ClientSideValidations').data('valid') !== false;
+ };
+ executeValidators = function(context) {
+ var fn, i, kind, len, message, ref, valid, validator;
+ valid = true;
+ for (kind in context) {
+ fn = context[kind];
+ if (validators[kind]) {
+ ref = validators[kind];
+ for (i = 0, len = ref.length; i < len; i++) {
+ validator = ref[i];
+ if (message = fn.call(context, element, validator)) {
+ valid = failElement(message);
+ break;
+ }
+ }
+ if (!valid) {
+ break;
+ }
+ }
+ }
+ return valid;
+ };
+ if (element.attr('name').search(/\[([^\]]*?)\]$/) >= 0) {
+ destroyInputName = element.attr('name').replace(/\[([^\]]*?)\]$/, '[_destroy]');
+ if ($("input[name='" + destroyInputName + "']").val() === '1') {
+ passElement();
+ return afterValidate();
+ }
+ }
+ if (element.data('changed') === false) {
+ return afterValidate();
+ }
+ element.data('changed', false);
+ local = ClientSideValidations.validators.local;
+ remote = ClientSideValidations.validators.remote;
+ if (executeValidators(local) && executeValidators(remote)) {
+ passElement();
+ }
+ return afterValidate();
+ };
+
+ ClientSideValidations = {
+ callbacks: {
+ element: {
+ after: function(element, eventData) {},
+ before: function(element, eventData) {},
+ fail: function(element, message, addError, eventData) {
+ return addError();
+ },
+ pass: function(element, removeError, eventData) {
+ return removeError();
+ }
+ },
+ form: {
+ after: function(form, eventData) {},
+ before: function(form, eventData) {},
+ fail: function(form, eventData) {},
+ pass: function(form, eventData) {}
+ }
+ },
+ enablers: {
+ form: function(form) {
+ var $form, binding, event, ref;
+ $form = $(form);
+ form.ClientSideValidations = {
+ settings: $form.data('clientSideValidations'),
+ addError: function(element, message) {
+ return ClientSideValidations.formBuilders[form.ClientSideValidations.settings.html_settings.type].add(element, form.ClientSideValidations.settings.html_settings, message);
+ },
+ removeError: function(element) {
+ return ClientSideValidations.formBuilders[form.ClientSideValidations.settings.html_settings.type].remove(element, form.ClientSideValidations.settings.html_settings);
+ }
+ };
+ ref = {
+ 'submit.ClientSideValidations': function(eventData) {
+ if (!$form.isValid(form.ClientSideValidations.settings.validators)) {
+ eventData.preventDefault();
+ eventData.stopImmediatePropagation();
+ }
+ },
+ 'ajax:beforeSend.ClientSideValidations': function(eventData) {
+ if (eventData.target === this) {
+ $form.isValid(form.ClientSideValidations.settings.validators);
+ }
+ },
+ 'form:validate:after.ClientSideValidations': function(eventData) {
+ ClientSideValidations.callbacks.form.after($form, eventData);
+ },
+ 'form:validate:before.ClientSideValidations': function(eventData) {
+ ClientSideValidations.callbacks.form.before($form, eventData);
+ },
+ 'form:validate:fail.ClientSideValidations': function(eventData) {
+ ClientSideValidations.callbacks.form.fail($form, eventData);
+ },
+ 'form:validate:pass.ClientSideValidations': function(eventData) {
+ ClientSideValidations.callbacks.form.pass($form, eventData);
+ }
+ };
+ for (event in ref) {
+ binding = ref[event];
+ $form.on(event, binding);
+ }
+ return $form.find(ClientSideValidations.selectors.inputs).each(function() {
+ return ClientSideValidations.enablers.input(this);
+ });
+ },
+ input: function(input) {
+ var $form, $input, binding, event, form, ref;
+ $input = $(input);
+ form = input.form;
+ $form = $(form);
+ ref = {
+ 'focusout.ClientSideValidations': function() {
+ $(this).isValid(form.ClientSideValidations.settings.validators);
+ },
+ 'change.ClientSideValidations': function() {
+ $(this).data('changed', true);
+ },
+ 'element:validate:after.ClientSideValidations': function(eventData) {
+ ClientSideValidations.callbacks.element.after($(this), eventData);
+ },
+ 'element:validate:before.ClientSideValidations': function(eventData) {
+ ClientSideValidations.callbacks.element.before($(this), eventData);
+ },
+ 'element:validate:fail.ClientSideValidations': function(eventData, message) {
+ var element;
+ element = $(this);
+ ClientSideValidations.callbacks.element.fail(element, message, function() {
+ return form.ClientSideValidations.addError(element, message);
+ }, eventData);
+ },
+ 'element:validate:pass.ClientSideValidations': function(eventData) {
+ var element;
+ element = $(this);
+ ClientSideValidations.callbacks.element.pass(element, function() {
+ return form.ClientSideValidations.removeError(element);
+ }, eventData);
+ }
+ };
+ for (event in ref) {
+ binding = ref[event];
+ $input.filter(':not(:radio):not([id$=_confirmation])').each(function() {
+ return $(this).attr('data-validate', true);
+ }).on(event, binding);
+ }
+ $input.filter(':checkbox').on('change.ClientSideValidations', function() {
+ $(this).isValid(form.ClientSideValidations.settings.validators);
+ });
+ return $input.filter('[id$=_confirmation]').each(function() {
+ var confirmationElement, element, ref1, results;
+ confirmationElement = $(this);
+ element = $form.find("#" + (this.id.match(/(.+)_confirmation/)[1]) + ":input");
+ if (element[0]) {
+ ref1 = {
+ 'focusout.ClientSideValidations': function() {
+ element.data('changed', true).isValid(form.ClientSideValidations.settings.validators);
+ },
+ 'keyup.ClientSideValidations': function() {
+ element.data('changed', true).isValid(form.ClientSideValidations.settings.validators);
+ }
+ };
+ results = [];
+ for (event in ref1) {
+ binding = ref1[event];
+ results.push($("#" + (confirmationElement.attr('id'))).on(event, binding));
+ }
+ return results;
+ }
+ });
+ }
+ },
+ formBuilders: {
+ 'ActionView::Helpers::FormBuilder': {
+ add: function(element, settings, message) {
+ var form, inputErrorField, label, labelErrorField;
+ form = $(element[0].form);
+ if (element.data('valid') !== false && (form.find("label.message[for='" + (element.attr('id')) + "']")[0] == null)) {
+ inputErrorField = $(settings.input_tag);
+ labelErrorField = $(settings.label_tag);
+ label = form.find("label[for='" + (element.attr('id')) + "']:not(.message)");
+ if (element.attr('autofocus')) {
+ element.attr('autofocus', false);
+ }
+ element.before(inputErrorField);
+ inputErrorField.find('span#input_tag').replaceWith(element);
+ inputErrorField.find('label.message').attr('for', element.attr('id'));
+ labelErrorField.find('label.message').attr('for', element.attr('id'));
+ labelErrorField.insertAfter(label);
+ labelErrorField.find('label#label_tag').replaceWith(label);
+ }
+ return form.find("label.message[for='" + (element.attr('id')) + "']").text(message);
+ },
+ remove: function(element, settings) {
+ var errorFieldClass, form, inputErrorField, label, labelErrorField;
+ form = $(element[0].form);
+ errorFieldClass = $(settings.input_tag).attr('class');
+ inputErrorField = element.closest("." + (errorFieldClass.replace(/\ /g, ".")));
+ label = form.find("label[for='" + (element.attr('id')) + "']:not(.message)");
+ labelErrorField = label.closest("." + errorFieldClass);
+ if (inputErrorField[0]) {
+ inputErrorField.find("#" + (element.attr('id'))).detach();
+ inputErrorField.replaceWith(element);
+ label.detach();
+ return labelErrorField.replaceWith(label);
+ }
+ }
+ }
+ },
+ patterns: {
+ numericality: {
+ "default": new RegExp('^[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?$'),
+ only_integer: new RegExp('^[+-]?\\d+$')
+ }
+ },
+ selectors: {
+ inputs: ':input:not(button):not([type="submit"])[name]:visible:enabled',
+ validate_inputs: ':input:enabled:visible[data-validate]',
+ forms: 'form[data-client-side-validations]'
+ },
+ validators: {
+ all: function() {
+ return $.extend({}, local, remote);
+ },
+ local: {
+ absence: function(element, options) {
+ if (!/^\s*$/.test(element.val() || '')) {
+ return options.message;
+ }
+ },
+ presence: function(element, options) {
+ if (/^\s*$/.test(element.val() || '')) {
+ return options.message;
+ }
+ },
+ acceptance: function(element, options) {
+ var ref;
+ switch (element.attr('type')) {
+ case 'checkbox':
+ if (!element.prop('checked')) {
+ return options.message;
+ }
+ break;
+ case 'text':
+ if (element.val() !== (((ref = options.accept) != null ? ref.toString() : void 0) || '1')) {
+ return options.message;
+ }
+ }
+ },
+ format: function(element, options) {
+ var message;
+ message = this.presence(element, options);
+ if (message) {
+ if (options.allow_blank === true) {
+ return;
+ }
+ return message;
+ }
+ if (options["with"] && !new RegExp(options["with"].source, options["with"].options).test(element.val())) {
+ return options.message;
+ }
+ if (options.without && new RegExp(options.without.source, options.without.options).test(element.val())) {
+ return options.message;
+ }
+ },
+ numericality: function(element, options) {
+ var $form, CHECKS, check, checkValue, fn, number_format, operator, val;
+ if (options.allow_blank === true && this.presence(element, {
+ message: options.messages.numericality
+ })) {
+ return;
+ }
+ $form = $(element[0].form);
+ number_format = $form[0].ClientSideValidations.settings.number_format;
+ val = $.trim(element.val()).replace(new RegExp("\\" + number_format.separator, 'g'), '.');
+ if (options.only_integer && !ClientSideValidations.patterns.numericality.only_integer.test(val)) {
+ return options.messages.only_integer;
+ }
+ if (!ClientSideValidations.patterns.numericality["default"].test(val)) {
+ return options.messages.numericality;
+ }
+ CHECKS = {
+ greater_than: '>',
+ greater_than_or_equal_to: '>=',
+ equal_to: '==',
+ less_than: '<',
+ less_than_or_equal_to: '<='
+ };
+ for (check in CHECKS) {
+ operator = CHECKS[check];
+ if (!(options[check] != null)) {
+ continue;
+ }
+ checkValue = !isNaN(parseFloat(options[check])) && isFinite(options[check]) ? options[check] : $form.find("[name*=" + options[check] + "]").length === 1 ? $form.find("[name*=" + options[check] + "]").val() : void 0;
+ if ((checkValue == null) || checkValue === '') {
+ return;
+ }
+ fn = new Function("return " + val + " " + operator + " " + checkValue);
+ if (!fn()) {
+ return options.messages[check];
+ }
+ }
+ if (options.odd && !(parseInt(val, 10) % 2)) {
+ return options.messages.odd;
+ }
+ if (options.even && (parseInt(val, 10) % 2)) {
+ return options.messages.even;
+ }
+ },
+ length: function(element, options) {
+ var CHECKS, blankOptions, check, fn, message, operator, tokenized_length, tokenizer;
+ tokenizer = options.js_tokenizer || "split('')";
+ tokenized_length = new Function('element', "return (element.val()." + tokenizer + " || '').length")(element);
+ CHECKS = {
+ is: '==',
+ minimum: '>=',
+ maximum: '<='
+ };
+ blankOptions = {};
+ blankOptions.message = options.is ? options.messages.is : options.minimum ? options.messages.minimum : void 0;
+ message = this.presence(element, blankOptions);
+ if (message) {
+ if (options.allow_blank === true) {
+ return;
+ }
+ return message;
+ }
+ for (check in CHECKS) {
+ operator = CHECKS[check];
+ if (!options[check]) {
+ continue;
+ }
+ fn = new Function("return " + tokenized_length + " " + operator + " " + options[check]);
+ if (!fn()) {
+ return options.messages[check];
+ }
+ }
+ },
+ exclusion: function(element, options) {
+ var lower, message, option, ref, upper;
+ message = this.presence(element, options);
+ if (message) {
+ if (options.allow_blank === true) {
+ return;
+ }
+ return message;
+ }
+ if (options["in"]) {
+ if (ref = element.val(), indexOf.call((function() {
+ var i, len, ref1, results;
+ ref1 = options["in"];
+ results = [];
+ for (i = 0, len = ref1.length; i < len; i++) {
+ option = ref1[i];
+ results.push(option.toString());
+ }
+ return results;
+ })(), ref) >= 0) {
+ return options.message;
+ }
+ }
+ if (options.range) {
+ lower = options.range[0];
+ upper = options.range[1];
+ if (element.val() >= lower && element.val() <= upper) {
+ return options.message;
+ }
+ }
+ },
+ inclusion: function(element, options) {
+ var lower, message, option, ref, upper;
+ message = this.presence(element, options);
+ if (message) {
+ if (options.allow_blank === true) {
+ return;
+ }
+ return message;
+ }
+ if (options["in"]) {
+ if (ref = element.val(), indexOf.call((function() {
+ var i, len, ref1, results;
+ ref1 = options["in"];
+ results = [];
+ for (i = 0, len = ref1.length; i < len; i++) {
+ option = ref1[i];
+ results.push(option.toString());
+ }
+ return results;
+ })(), ref) >= 0) {
+ return;
+ }
+ return options.message;
+ }
+ if (options.range) {
+ lower = options.range[0];
+ upper = options.range[1];
+ if (element.val() >= lower && element.val() <= upper) {
+ return;
+ }
+ return options.message;
+ }
+ },
+ confirmation: function(element, options) {
+ var confirmation_value, value;
+ value = element.val();
+ confirmation_value = $("#" + (element.attr('id')) + "_confirmation").val();
+ if (!options.case_sensitive) {
+ value = value.toLowerCase();
+ confirmation_value = confirmation_value.toLowerCase();
+ }
+ if (value !== confirmation_value) {
+ return options.message;
+ }
+ },
+ uniqueness: function(element, options) {
+ var form, matches, name, name_prefix, name_suffix, valid, value;
+ name = element.attr('name');
+ if (/_attributes\]\[\d/.test(name)) {
+ matches = name.match(/^(.+_attributes\])\[\d+\](.+)$/);
+ name_prefix = matches[1];
+ name_suffix = matches[2];
+ value = element.val();
+ if (name_prefix && name_suffix) {
+ form = element.closest('form');
+ valid = true;
+ form.find(":input[name^=\"" + name_prefix + "\"][name$=\"" + name_suffix + "\"]").each(function() {
+ if ($(this).attr('name') !== name) {
+ if ($(this).val() === value) {
+ valid = false;
+ return $(this).data('notLocallyUnique', true);
+ } else {
+ if ($(this).data('notLocallyUnique')) {
+ return $(this).removeData('notLocallyUnique').data('changed', true);
+ }
+ }
+ }
+ });
+ if (!valid) {
+ return options.message;
+ }
+ }
+ }
+ }
+ },
+ remote: {}
+ },
+ disable: function(target) {
+ var $target;
+ $target = $(target);
+ $target.off('.ClientSideValidations');
+ if ($target.is('form')) {
+ return ClientSideValidations.disable($target.find(':input'));
+ } else {
+ $target.removeData('valid');
+ $target.removeData('changed');
+ return $target.filter(':input').each(function() {
+ return $(this).removeAttr('data-validate');
+ });
+ }
+ },
+ reset: function(form) {
+ var $form, key;
+ $form = $(form);
+ ClientSideValidations.disable(form);
+ for (key in form.ClientSideValidations.settings.validators) {
+ form.ClientSideValidations.removeError($form.find("[name='" + key + "']"));
+ }
+ return ClientSideValidations.enablers.form(form);
+ }
+ };
+
+ if ((window.Turbolinks != null) && window.Turbolinks.supported) {
+ initializeOnEvent = window.Turbolinks.EVENTS != null ? 'page:change' : 'turbolinks:load';
+ $(document).on(initializeOnEvent, function() {
+ return $(ClientSideValidations.selectors.forms).validate();
+ });
+ } else {
+ $(function() {
+ return $(ClientSideValidations.selectors.forms).validate();
+ });
+ }
+
+ window.ClientSideValidations = ClientSideValidations;
+
+}).call(this);
diff --git a/app/assets/javascripts/reviews.js b/app/assets/javascripts/reviews.js
new file mode 100644
index 0000000000..dee720facd
--- /dev/null
+++ b/app/assets/javascripts/reviews.js
@@ -0,0 +1,2 @@
+// Place all the behaviors and hooks related to the matching controller here.
+// All this logic will automatically be available in application.js.
diff --git a/app/assets/javascripts/sessions.js b/app/assets/javascripts/sessions.js
new file mode 100644
index 0000000000..dee720facd
--- /dev/null
+++ b/app/assets/javascripts/sessions.js
@@ -0,0 +1,2 @@
+// Place all the behaviors and hooks related to the matching controller here.
+// All this logic will automatically be available in application.js.
diff --git a/app/assets/javascripts/users.js b/app/assets/javascripts/users.js
new file mode 100644
index 0000000000..dee720facd
--- /dev/null
+++ b/app/assets/javascripts/users.js
@@ -0,0 +1,2 @@
+// Place all the behaviors and hooks related to the matching controller here.
+// All this logic will automatically be available in application.js.
diff --git a/app/assets/stylesheets/_settings.scss b/app/assets/stylesheets/_settings.scss
new file mode 100644
index 0000000000..87f5909e0c
--- /dev/null
+++ b/app/assets/stylesheets/_settings.scss
@@ -0,0 +1,868 @@
+// Foundation for Sites Settings
+// -----------------------------
+//
+// Table of Contents:
+//
+// 1. Global
+// 2. Breakpoints
+// 3. The Grid
+// 4. Base Typography
+// 5. Typography Helpers
+// 6. Abide
+// 7. Accordion
+// 8. Accordion Menu
+// 9. Badge
+// 10. Breadcrumbs
+// 11. Button
+// 12. Button Group
+// 13. Callout
+// 14. Card
+// 15. Close Button
+// 16. Drilldown
+// 17. Dropdown
+// 18. Dropdown Menu
+// 19. Flexbox Utilities
+// 20. Forms
+// 21. Label
+// 22. Media Object
+// 23. Menu
+// 24. Meter
+// 25. Off-canvas
+// 26. Orbit
+// 27. Pagination
+// 28. Progress Bar
+// 29. Prototype Arrow
+// 30. Prototype Border-Box
+// 31. Prototype Border-None
+// 32. Prototype Bordered
+// 33. Prototype Display
+// 34. Prototype Font-Styling
+// 35. Prototype List-Style-Type
+// 36. Prototype Overflow
+// 37. Prototype Position
+// 38. Prototype Rounded
+// 39. Prototype Separator
+// 40. Prototype Shadow
+// 41. Prototype Sizing
+// 42. Prototype Spacing
+// 43. Prototype Text-Decoration
+// 44. Prototype Text-Transformation
+// 45. Prototype Text-Utilities
+// 46. Responsive Embed
+// 47. Reveal
+// 48. Slider
+// 49. Switch
+// 50. Table
+// 51. Tabs
+// 52. Thumbnail
+// 53. Title Bar
+// 54. Tooltip
+// 55. Top Bar
+// 56. Xy Grid
+
+@import 'util/util';
+
+// 1. Global
+// ---------
+
+$global-font-size: 100%;
+$global-width: rem-calc(1200);
+$global-lineheight: 1.5;
+$foundation-palette: (
+ primary: #1779ba,
+ secondary: #767676,
+ success: #3adb76,
+ warning: #ffae00,
+ alert: #cc4b37,
+);
+$light-gray: #e6e6e6;
+$medium-gray: #cacaca;
+$dark-gray: #8a8a8a;
+$black: #0a0a0a;
+$white: #fefefe;
+$body-background: $white;
+$body-font-color: $black;
+$body-font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif;
+$body-antialiased: true;
+$global-margin: 1rem;
+$global-padding: 1rem;
+$global-position: 1rem;
+$global-weight-normal: normal;
+$global-weight-bold: bold;
+$global-radius: 0;
+$global-menu-padding: 0.7rem 1rem;
+$global-menu-nested-margin: 1rem;
+$global-text-direction: ltr;
+$global-flexbox: true;
+$global-prototype-breakpoints: false;
+$global-button-cursor: auto;
+$global-color-pick-contrast-tolerance: 0;
+$print-transparent-backgrounds: true;
+
+@include add-foundation-colors;
+
+// 2. Breakpoints
+// --------------
+
+$breakpoints: (
+ small: 0,
+ medium: 640px,
+ large: 1024px,
+ xlarge: 1200px,
+ xxlarge: 1440px,
+);
+$print-breakpoint: large;
+$breakpoint-classes: (small medium large);
+
+// 3. The Grid
+// -----------
+
+$grid-row-width: $global-width;
+$grid-column-count: 12;
+$grid-column-gutter: (
+ small: 20px,
+ medium: 30px,
+);
+$grid-column-align-edge: true;
+$grid-column-alias: 'columns';
+$block-grid-max: 8;
+
+// 4. Base Typography
+// ------------------
+
+$header-font-family: $body-font-family;
+$header-font-weight: $global-weight-normal;
+$header-font-style: normal;
+$font-family-monospace: Consolas, 'Liberation Mono', Courier, monospace;
+$header-color: inherit;
+$header-lineheight: 1.4;
+$header-margin-bottom: 0.5rem;
+$header-styles: (
+ small: (
+ 'h1': ('font-size': 24),
+ 'h2': ('font-size': 20),
+ 'h3': ('font-size': 19),
+ 'h4': ('font-size': 18),
+ 'h5': ('font-size': 17),
+ 'h6': ('font-size': 16),
+ ),
+ medium: (
+ 'h1': ('font-size': 48),
+ 'h2': ('font-size': 40),
+ 'h3': ('font-size': 31),
+ 'h4': ('font-size': 25),
+ 'h5': ('font-size': 20),
+ 'h6': ('font-size': 16),
+ ),
+);
+$header-text-rendering: optimizeLegibility;
+$small-font-size: 80%;
+$header-small-font-color: $medium-gray;
+$paragraph-lineheight: 1.6;
+$paragraph-margin-bottom: 1rem;
+$paragraph-text-rendering: optimizeLegibility;
+$code-color: $black;
+$code-font-family: $font-family-monospace;
+$code-font-weight: $global-weight-normal;
+$code-background: $light-gray;
+$code-border: 1px solid $medium-gray;
+$code-padding: rem-calc(2 5 1);
+$anchor-color: $primary-color;
+$anchor-color-hover: scale-color($anchor-color, $lightness: -14%);
+$anchor-text-decoration: none;
+$anchor-text-decoration-hover: none;
+$hr-width: $global-width;
+$hr-border: 1px solid $medium-gray;
+$hr-margin: rem-calc(20) auto;
+$list-lineheight: $paragraph-lineheight;
+$list-margin-bottom: $paragraph-margin-bottom;
+$list-style-type: disc;
+$list-style-position: outside;
+$list-side-margin: 1.25rem;
+$list-nested-side-margin: 1.25rem;
+$defnlist-margin-bottom: 1rem;
+$defnlist-term-weight: $global-weight-bold;
+$defnlist-term-margin-bottom: 0.3rem;
+$blockquote-color: $dark-gray;
+$blockquote-padding: rem-calc(9 20 0 19);
+$blockquote-border: 1px solid $medium-gray;
+$cite-font-size: rem-calc(13);
+$cite-color: $dark-gray;
+$cite-pseudo-content: '\2014 \0020';
+$keystroke-font: $font-family-monospace;
+$keystroke-color: $black;
+$keystroke-background: $light-gray;
+$keystroke-padding: rem-calc(2 4 0);
+$keystroke-radius: $global-radius;
+$abbr-underline: 1px dotted $black;
+
+// 5. Typography Helpers
+// ---------------------
+
+$lead-font-size: $global-font-size * 1.25;
+$lead-lineheight: 1.6;
+$subheader-lineheight: 1.4;
+$subheader-color: $dark-gray;
+$subheader-font-weight: $global-weight-normal;
+$subheader-margin-top: 0.2rem;
+$subheader-margin-bottom: 0.5rem;
+$stat-font-size: 2.5rem;
+
+// 6. Abide
+// --------
+
+$abide-inputs: true;
+$abide-labels: true;
+$input-background-invalid: get-color(alert);
+$form-label-color-invalid: get-color(alert);
+$input-error-color: get-color(alert);
+$input-error-font-size: rem-calc(12);
+$input-error-font-weight: $global-weight-bold;
+
+// 7. Accordion
+// ------------
+
+$accordion-background: $white;
+$accordion-plusminus: true;
+$accordion-title-font-size: rem-calc(12);
+$accordion-item-color: $primary-color;
+$accordion-item-background-hover: $light-gray;
+$accordion-item-padding: 1.25rem 1rem;
+$accordion-content-background: $white;
+$accordion-content-border: 1px solid $light-gray;
+$accordion-content-color: $body-font-color;
+$accordion-content-padding: 1rem;
+
+// 8. Accordion Menu
+// -----------------
+
+$accordionmenu-padding: $global-menu-padding;
+$accordionmenu-nested-margin: $global-menu-nested-margin;
+$accordionmenu-submenu-padding: $accordionmenu-padding;
+$accordionmenu-arrows: true;
+$accordionmenu-arrow-color: $primary-color;
+$accordionmenu-item-background: null;
+$accordionmenu-border: null;
+$accordionmenu-submenu-toggle-background: null;
+$accordion-submenu-toggle-border: $accordionmenu-border;
+$accordionmenu-submenu-toggle-width: 40px;
+$accordionmenu-submenu-toggle-height: $accordionmenu-submenu-toggle-width;
+$accordionmenu-arrow-size: 6px;
+
+// 9. Badge
+// --------
+
+$badge-background: $primary-color;
+$badge-color: $white;
+$badge-color-alt: $black;
+$badge-palette: $foundation-palette;
+$badge-padding: 0.3em;
+$badge-minwidth: 2.1em;
+$badge-font-size: 0.6rem;
+
+// 10. Breadcrumbs
+// ---------------
+
+$breadcrumbs-margin: 0 0 $global-margin 0;
+$breadcrumbs-item-font-size: rem-calc(11);
+$breadcrumbs-item-color: $primary-color;
+$breadcrumbs-item-color-current: $black;
+$breadcrumbs-item-color-disabled: $medium-gray;
+$breadcrumbs-item-margin: 0.75rem;
+$breadcrumbs-item-uppercase: true;
+$breadcrumbs-item-separator: true;
+$breadcrumbs-item-separator-item: '/';
+$breadcrumbs-item-separator-item-rtl: '\\';
+$breadcrumbs-item-separator-color: $medium-gray;
+
+// 11. Button
+// ----------
+
+$button-font-family: inherit;
+$button-padding: 0.85em 1em;
+$button-margin: 0 0 $global-margin 0;
+$button-fill: solid;
+$button-background: $primary-color;
+$button-background-hover: scale-color($button-background, $lightness: -15%);
+$button-color: $white;
+$button-color-alt: $black;
+$button-radius: $global-radius;
+$button-hollow-border-width: 1px;
+$button-sizes: (
+ tiny: 0.6rem,
+ small: 0.75rem,
+ default: 0.9rem,
+ large: 1.25rem,
+);
+$button-palette: $foundation-palette;
+$button-opacity-disabled: 0.25;
+$button-background-hover-lightness: -20%;
+$button-hollow-hover-lightness: -50%;
+$button-transition: background-color 0.25s ease-out, color 0.25s ease-out;
+
+// 12. Button Group
+// ----------------
+
+$buttongroup-margin: 1rem;
+$buttongroup-spacing: 1px;
+$buttongroup-child-selector: '.button';
+$buttongroup-expand-max: 6;
+$buttongroup-radius-on-each: true;
+
+// 13. Callout
+// -----------
+
+$callout-background: $white;
+$callout-background-fade: 85%;
+$callout-border: 1px solid rgba($black, 0.25);
+$callout-margin: 0 0 1rem 0;
+$callout-padding: 1rem;
+$callout-font-color: $body-font-color;
+$callout-font-color-alt: $body-background;
+$callout-radius: $global-radius;
+$callout-link-tint: 30%;
+
+// 14. Card
+// --------
+
+$card-background: $white;
+$card-font-color: $body-font-color;
+$card-divider-background: $light-gray;
+$card-border: 1px solid $light-gray;
+$card-shadow: none;
+$card-border-radius: $global-radius;
+$card-padding: $global-padding;
+$card-margin-bottom: $global-margin;
+
+// 15. Close Button
+// ----------------
+
+$closebutton-position: right top;
+$closebutton-offset-horizontal: (
+ small: 0.66rem,
+ medium: 1rem,
+);
+$closebutton-offset-vertical: (
+ small: 0.33em,
+ medium: 0.5rem,
+);
+$closebutton-size: (
+ small: 1.5em,
+ medium: 2em,
+);
+$closebutton-lineheight: 1;
+$closebutton-color: $dark-gray;
+$closebutton-color-hover: $black;
+
+// 16. Drilldown
+// -------------
+
+$drilldown-transition: transform 0.15s linear;
+$drilldown-arrows: true;
+$drilldown-padding: $global-menu-padding;
+$drilldown-nested-margin: 0;
+$drilldown-background: $white;
+$drilldown-submenu-padding: $drilldown-padding;
+$drilldown-submenu-background: $white;
+$drilldown-arrow-color: $primary-color;
+$drilldown-arrow-size: 6px;
+
+// 17. Dropdown
+// ------------
+
+$dropdown-padding: 1rem;
+$dropdown-background: $body-background;
+$dropdown-border: 1px solid $medium-gray;
+$dropdown-font-size: 1rem;
+$dropdown-width: 300px;
+$dropdown-radius: $global-radius;
+$dropdown-sizes: (
+ tiny: 100px,
+ small: 200px,
+ large: 400px,
+);
+
+// 18. Dropdown Menu
+// -----------------
+
+$dropdownmenu-arrows: true;
+$dropdownmenu-arrow-color: $anchor-color;
+$dropdownmenu-arrow-size: 6px;
+$dropdownmenu-arrow-padding: 1.5rem;
+$dropdownmenu-min-width: 200px;
+$dropdownmenu-background: $white;
+$dropdownmenu-submenu-background: $dropdownmenu-background;
+$dropdownmenu-padding: $global-menu-padding;
+$dropdownmenu-nested-margin: 0;
+$dropdownmenu-submenu-padding: $dropdownmenu-padding;
+$dropdownmenu-border: 1px solid $medium-gray;
+$dropdown-menu-item-color-active: get-color(primary);
+$dropdown-menu-item-background-active: transparent;
+
+// 19. Flexbox Utilities
+// ---------------------
+
+$flex-source-ordering-count: 6;
+$flexbox-responsive-breakpoints: true;
+
+// 20. Forms
+// ---------
+
+$fieldset-border: 1px solid $medium-gray;
+$fieldset-padding: rem-calc(20);
+$fieldset-margin: rem-calc(18 0);
+$legend-padding: rem-calc(0 3);
+$form-spacing: rem-calc(16);
+$helptext-color: $black;
+$helptext-font-size: rem-calc(13);
+$helptext-font-style: italic;
+$input-prefix-color: $black;
+$input-prefix-background: $light-gray;
+$input-prefix-border: 1px solid $medium-gray;
+$input-prefix-padding: 1rem;
+$form-label-color: $black;
+$form-label-font-size: rem-calc(14);
+$form-label-font-weight: $global-weight-normal;
+$form-label-line-height: 1.8;
+$select-background: $white;
+$select-triangle-color: $dark-gray;
+$select-radius: $global-radius;
+$input-color: $black;
+$input-placeholder-color: $medium-gray;
+$input-font-family: inherit;
+$input-font-size: rem-calc(16);
+$input-font-weight: $global-weight-normal;
+$input-line-height: $global-lineheight;
+$input-background: $white;
+$input-background-focus: $white;
+$input-background-disabled: $light-gray;
+$input-border: 1px solid $medium-gray;
+$input-border-focus: 1px solid $dark-gray;
+$input-padding: $form-spacing / 2;
+$input-shadow: inset 0 1px 2px rgba($black, 0.1);
+$input-shadow-focus: 0 0 5px $medium-gray;
+$input-cursor-disabled: not-allowed;
+$input-transition: box-shadow 0.5s, border-color 0.25s ease-in-out;
+$input-number-spinners: true;
+$input-radius: $global-radius;
+$form-button-radius: $global-radius;
+
+// 21. Label
+// ---------
+
+$label-background: $primary-color;
+$label-color: $white;
+$label-color-alt: $black;
+$label-palette: $foundation-palette;
+$label-font-size: 0.8rem;
+$label-padding: 0.33333rem 0.5rem;
+$label-radius: $global-radius;
+
+// 22. Media Object
+// ----------------
+
+$mediaobject-margin-bottom: $global-margin;
+$mediaobject-section-padding: $global-padding;
+$mediaobject-image-width-stacked: 100%;
+
+// 23. Menu
+// --------
+
+$menu-margin: 0;
+$menu-nested-margin: $global-menu-nested-margin;
+$menu-items-padding: $global-menu-padding;
+$menu-simple-margin: 1rem;
+$menu-item-color-active: $white;
+$menu-item-background-active: get-color(primary);
+$menu-icon-spacing: 0.25rem;
+$menu-item-background-hover: $light-gray;
+$menu-state-back-compat: true;
+$menu-centered-back-compat: true;
+$menu-icons-back-compat: true;
+
+// 24. Meter
+// ---------
+
+$meter-height: 1rem;
+$meter-radius: $global-radius;
+$meter-background: $medium-gray;
+$meter-fill-good: $success-color;
+$meter-fill-medium: $warning-color;
+$meter-fill-bad: $alert-color;
+
+// 25. Off-canvas
+// --------------
+
+$offcanvas-sizes: (
+ small: 250px,
+);
+$offcanvas-vertical-sizes: (
+ small: 250px,
+);
+$offcanvas-background: $light-gray;
+$offcanvas-shadow: 0 0 10px rgba($black, 0.7);
+$offcanvas-inner-shadow-size: 20px;
+$offcanvas-inner-shadow-color: rgba($black, 0.25);
+$offcanvas-overlay-zindex: 11;
+$offcanvas-push-zindex: 12;
+$offcanvas-overlap-zindex: 13;
+$offcanvas-reveal-zindex: 12;
+$offcanvas-transition-length: 0.5s;
+$offcanvas-transition-timing: ease;
+$offcanvas-fixed-reveal: true;
+$offcanvas-exit-background: rgba($white, 0.25);
+$maincontent-class: 'off-canvas-content';
+
+// 26. Orbit
+// ---------
+
+$orbit-bullet-background: $medium-gray;
+$orbit-bullet-background-active: #E87A89;
+$orbit-bullet-diameter: 1.2rem;
+$orbit-bullet-margin: 0.1rem;
+$orbit-bullet-margin-top: 0.8rem;
+$orbit-bullet-margin-bottom: 0.8rem;
+$orbit-caption-background: rgba($black, 0.5);
+$orbit-caption-padding: 1rem;
+$orbit-control-background-hover: rgba($black, 0.5);
+$orbit-control-padding: 1rem;
+$orbit-control-zindex: 10;
+
+// 27. Pagination
+// --------------
+
+$pagination-font-size: rem-calc(14);
+$pagination-margin-bottom: $global-margin;
+$pagination-item-color: $black;
+$pagination-item-padding: rem-calc(3 10);
+$pagination-item-spacing: rem-calc(1);
+$pagination-radius: $global-radius;
+$pagination-item-background-hover: $light-gray;
+$pagination-item-background-current: $primary-color;
+$pagination-item-color-current: $white;
+$pagination-item-color-disabled: $medium-gray;
+$pagination-ellipsis-color: $black;
+$pagination-mobile-items: false;
+$pagination-mobile-current-item: false;
+$pagination-arrows: true;
+
+// 28. Progress Bar
+// ----------------
+
+$progress-height: 1rem;
+$progress-background: $medium-gray;
+$progress-margin-bottom: $global-margin;
+$progress-meter-background: $primary-color;
+$progress-radius: $global-radius;
+
+// 29. Prototype Arrow
+// -------------------
+
+$prototype-arrow-directions: (
+ down,
+ up,
+ right,
+ left
+);
+$prototype-arrow-size: 0.4375rem;
+$prototype-arrow-color: $black;
+
+// 30. Prototype Border-Box
+// ------------------------
+
+$prototype-border-box-breakpoints: $global-prototype-breakpoints;
+
+// 31. Prototype Border-None
+// -------------------------
+
+$prototype-border-none-breakpoints: $global-prototype-breakpoints;
+
+// 32. Prototype Bordered
+// ----------------------
+
+$prototype-bordered-breakpoints: $global-prototype-breakpoints;
+$prototype-border-width: rem-calc(1);
+$prototype-border-type: solid;
+$prototype-border-color: $medium-gray;
+
+// 33. Prototype Display
+// ---------------------
+
+$prototype-display-breakpoints: $global-prototype-breakpoints;
+$prototype-display: (
+ inline,
+ inline-block,
+ block,
+ table,
+ table-cell
+);
+
+// 34. Prototype Font-Styling
+// --------------------------
+
+$prototype-font-breakpoints: $global-prototype-breakpoints;
+$prototype-wide-letter-spacing: rem-calc(4);
+$prototype-font-normal: $global-weight-normal;
+$prototype-font-bold: $global-weight-bold;
+
+// 35. Prototype List-Style-Type
+// -----------------------------
+
+$prototype-list-breakpoints: $global-prototype-breakpoints;
+$prototype-style-type-unordered: (
+ disc,
+ circle,
+ square
+);
+$prototype-style-type-ordered: (
+ decimal,
+ lower-alpha,
+ lower-latin,
+ lower-roman,
+ upper-alpha,
+ upper-latin,
+ upper-roman
+);
+
+// 36. Prototype Overflow
+// ----------------------
+
+$prototype-overflow-breakpoints: $global-prototype-breakpoints;
+$prototype-overflow: (
+ visible,
+ hidden,
+ scroll
+);
+
+// 37. Prototype Position
+// ----------------------
+
+$prototype-position-breakpoints: $global-prototype-breakpoints;
+$prototype-position: (
+ static,
+ relative,
+ absolute,
+ fixed
+);
+$prototype-position-z-index: 975;
+
+// 38. Prototype Rounded
+// ---------------------
+
+$prototype-rounded-breakpoints: $global-prototype-breakpoints;
+$prototype-border-radius: rem-calc(3);
+
+// 39. Prototype Separator
+// -----------------------
+
+$prototype-separator-breakpoints: $global-prototype-breakpoints;
+$prototype-separator-align: center;
+$prototype-separator-height: rem-calc(2);
+$prototype-separator-width: 3rem;
+$prototype-separator-background: $primary-color;
+$prototype-separator-margin-top: $global-margin;
+
+// 40. Prototype Shadow
+// --------------------
+
+$prototype-shadow-breakpoints: $global-prototype-breakpoints;
+$prototype-box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),
+ 0 2px 10px 0 rgba(0,0,0,.12);
+
+// 41. Prototype Sizing
+// --------------------
+
+$prototype-sizing-breakpoints: $global-prototype-breakpoints;
+$prototype-sizing: (
+ width,
+ height
+);
+$prototype-sizes: (
+ 25: 25%,
+ 50: 50%,
+ 75: 75%,
+ 100: 100%
+);
+
+// 42. Prototype Spacing
+// ---------------------
+
+$prototype-spacing-breakpoints: $global-prototype-breakpoints;
+$prototype-spacers-count: 3;
+
+// 43. Prototype Text-Decoration
+// -----------------------------
+
+$prototype-decoration-breakpoints: $global-prototype-breakpoints;
+$prototype-text-decoration: (
+ overline,
+ underline,
+ line-through,
+);
+
+// 44. Prototype Text-Transformation
+// ---------------------------------
+
+$prototype-transformation-breakpoints: $global-prototype-breakpoints;
+$prototype-text-transformation: (
+ lowercase,
+ uppercase,
+ capitalize
+);
+
+// 45. Prototype Text-Utilities
+// ----------------------------
+
+$prototype-utilities-breakpoints: $global-prototype-breakpoints;
+$prototype-text-overflow: ellipsis;
+
+// 46. Responsive Embed
+// --------------------
+
+$responsive-embed-margin-bottom: rem-calc(16);
+$responsive-embed-ratios: (
+ default: 4 by 3,
+ widescreen: 16 by 9,
+);
+
+// 47. Reveal
+// ----------
+
+$reveal-background: $white;
+$reveal-width: 600px;
+$reveal-max-width: $global-width;
+$reveal-padding: $global-padding;
+$reveal-border: 1px solid $medium-gray;
+$reveal-radius: $global-radius;
+$reveal-zindex: 1005;
+$reveal-overlay-background: rgba($black, 0.45);
+
+// 48. Slider
+// ----------
+
+$slider-width-vertical: 0.5rem;
+$slider-transition: all 0.2s ease-in-out;
+$slider-height: 0.5rem;
+$slider-background: $light-gray;
+$slider-fill-background: $medium-gray;
+$slider-handle-height: 1.4rem;
+$slider-handle-width: 1.4rem;
+$slider-handle-background: $primary-color;
+$slider-opacity-disabled: 0.25;
+$slider-radius: $global-radius;
+
+// 49. Switch
+// ----------
+
+$switch-background: $medium-gray;
+$switch-background-active: $primary-color;
+$switch-height: 2rem;
+$switch-height-tiny: 1.5rem;
+$switch-height-small: 1.75rem;
+$switch-height-large: 2.5rem;
+$switch-radius: $global-radius;
+$switch-margin: $global-margin;
+$switch-paddle-background: $white;
+$switch-paddle-offset: 0.25rem;
+$switch-paddle-radius: $global-radius;
+$switch-paddle-transition: all 0.25s ease-out;
+
+// 50. Table
+// ---------
+
+$table-background: $white;
+$table-color-scale: 5%;
+$table-border: 1px solid smart-scale($table-background, $table-color-scale);
+$table-padding: rem-calc(8 10 10);
+$table-hover-scale: 2%;
+$table-row-hover: darken($table-background, $table-hover-scale);
+$table-row-stripe-hover: darken($table-background, $table-color-scale + $table-hover-scale);
+$table-is-striped: true;
+$table-striped-background: smart-scale($table-background, $table-color-scale);
+$table-stripe: even;
+$table-head-background: smart-scale($table-background, $table-color-scale / 2);
+$table-head-row-hover: darken($table-head-background, $table-hover-scale);
+$table-foot-background: smart-scale($table-background, $table-color-scale);
+$table-foot-row-hover: darken($table-foot-background, $table-hover-scale);
+$table-head-font-color: $body-font-color;
+$table-foot-font-color: $body-font-color;
+$show-header-for-stacked: false;
+$table-stack-breakpoint: medium;
+
+// 51. Tabs
+// --------
+
+$tab-margin: 0;
+$tab-background: $white;
+$tab-color: $primary-color;
+$tab-background-active: $light-gray;
+$tab-active-color: $primary-color;
+$tab-item-font-size: rem-calc(12);
+$tab-item-background-hover: $white;
+$tab-item-padding: 1.25rem 1.5rem;
+$tab-expand-max: 6;
+$tab-content-background: $white;
+$tab-content-border: $light-gray;
+$tab-content-color: $body-font-color;
+$tab-content-padding: 1rem;
+
+// 52. Thumbnail
+// -------------
+
+$thumbnail-border: solid 4px $white;
+$thumbnail-margin-bottom: $global-margin;
+$thumbnail-shadow: 0 0 0 1px rgba($black, 0.2);
+$thumbnail-shadow-hover: 0 0 6px 1px rgba($primary-color, 0.5);
+$thumbnail-transition: box-shadow 200ms ease-out;
+$thumbnail-radius: $global-radius;
+
+// 53. Title Bar
+// -------------
+
+$titlebar-background: $black;
+$titlebar-color: $white;
+$titlebar-padding: 0.5rem;
+$titlebar-text-font-weight: bold;
+$titlebar-icon-color: $white;
+$titlebar-icon-color-hover: $medium-gray;
+$titlebar-icon-spacing: 0.25rem;
+
+// 54. Tooltip
+// -----------
+
+$has-tip-cursor: help;
+$has-tip-font-weight: $global-weight-bold;
+$has-tip-border-bottom: dotted 1px $dark-gray;
+$tooltip-background-color: $black;
+$tooltip-color: $white;
+$tooltip-padding: 0.75rem;
+$tooltip-max-width: 10rem;
+$tooltip-font-size: $small-font-size;
+$tooltip-pip-width: 0.75rem;
+$tooltip-pip-height: $tooltip-pip-width * 0.866;
+$tooltip-radius: $global-radius;
+
+// 55. Top Bar
+// -----------
+
+$topbar-padding: 0.5rem;
+$topbar-background: $light-gray;
+$topbar-submenu-background: $topbar-background;
+$topbar-title-spacing: 0.5rem 1rem 0.5rem 0;
+$topbar-input-width: 200px;
+$topbar-unstack-breakpoint: medium;
+
+// 56. Xy Grid
+// -----------
+
+$xy-grid: true;
+$grid-container: $global-width;
+$grid-columns: 12;
+$grid-margin-gutters: (
+ small: 20px,
+ medium: 30px
+);
+$grid-padding-gutters: $grid-margin-gutters;
+$grid-container-padding: $grid-padding-gutters;
+$grid-container-max: $global-width;
+$xy-block-grid-max: 8;
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
new file mode 100644
index 0000000000..ffb54a4aa5
--- /dev/null
+++ b/app/assets/stylesheets/application.css
@@ -0,0 +1,443 @@
+/*
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
+ * listed below.
+ *
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
+ * vendor/assets/stylesheets directory can be referenced here using a relative path.
+ *
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
+ * files in this directory. Styles in this file should be added after the last require_* statement.
+ * It is generally better to create a new file per style scope.
+ *= require normalize-rails
+
+ *
+ *= require_tree .
+ *= require_self
+ *= require foundation_and_overrides
+
+ */
+
+@import url('https://fonts.googleapis.com/css?family=Chicle|Dosis:300,400,500,600,700');
+/* font-family: 'Chicle', cursive;
+font-family: 'Dosis', sans-serif; */
+
+/* #1A1F22; brown-black;
+#c17317; brown;
+#E87A89; pink; */
+
+/*--------- global ---------*/
+/********** text **********/
+body, h1, h2, h3, h4, h5, p {
+ font-family: 'Dosis', sans-serif;
+ color: #1A1F22;
+}
+
+h3 {
+ margin-bottom: 1rem;
+ font-weight: bold;
+}
+
+h4 {
+ font-weight: 500;
+ margin: 1rem auto;
+}
+
+p {
+ font-size: 1.2rem;
+}
+
+.color-text {
+ color: green;
+ font-weight: bold;
+}
+
+label {
+ font-size: 1.2rem;
+ font-weight: 500;
+ color: #c17317;
+}
+
+/********** message **********/
+.callout {
+ border: none;
+ text-align: center;
+}
+
+.callout .success {
+ color: green;
+}
+
+.callout .error, .callout .alert {
+ color: red;
+}
+
+.field_with_errors input {
+ margin-bottom: 0;
+}
+
+.message {
+ color: red;
+ margin: 3px auto;
+}
+
+/********** button **********/
+.button {
+ font-family: 'Dosis', sans-serif;
+ font-weight: 500;
+ font-size: 1.2rem;
+ padding: 0.8rem;
+ border-radius: 0.5rem;
+ background-color: #c17317;
+ margin-left: 20px;
+ margin-right: 20px;
+}
+
+.button:hover {
+ background-color: #E87A89;
+}
+
+table .button {
+ margin: 0;
+}
+
+/********** image **********/
+img {
+ height: 300px;
+ margin-bottom: 20px;
+}
+
+/********** list **********/
+ul {
+ padding-left: 0;
+ list-style: none;
+}
+
+/********** table **********/
+thead th, thead td, tfoot th, tfoot td {
+ text-align: center;
+ font-size: 1.2rem;
+}
+
+/********** header **********/
+.page-header {
+ width: 100%;
+ padding: 25px 20px 20px 20px;
+ background: #1A1F22;
+ color: white;
+}
+
+.page-header nav {
+ display: grid;
+ grid-template-columns: 1fr 4fr 3fr;
+ margin-left: 30px;
+ margin-right: 30px;
+}
+
+.page-header h1 {
+ width: 200px;
+}
+
+.page-header h1 a {
+ font-family: 'Chicle', cursive;
+ color: #c17317;
+}
+
+.nav-button {
+ margin: 20px 5px;
+}
+
+.float-right {
+ margin-left: auto;
+ margin-right: 0;
+}
+
+/********** main **********/
+.main-container {
+ padding: 1rem 2rem;
+ text-align: center;
+ min-height: 50rem;
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: center;
+}
+
+/********** boxes **********/
+.search-container, .filter-container, .status-container, .quantity-container {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: space-around;
+}
+
+.search-box, .search-button, .status-box, .status-button, .filter-box, .filter-button, .quantity-box, .quantity-button {
+ display: inline-block;
+}
+
+.form {
+ min-width: 200px;
+}
+
+/********** footer **********/
+footer {
+ text-align: center;
+}
+
+.fureign-keys {
+ height: 30px;
+}
+
+/*--------- home ---------*/
+.orbit-container img {
+ margin-bottom: 0px;
+}
+
+.list-container {
+ display: grid;
+ grid-template-columns: auto 120px;
+}
+
+.product-lists {
+ display: flex;
+ flex-flow: row wrap;
+ justify-content: center;
+}
+
+.product-lists section {
+ width: 350px;
+}
+
+/*--------- all-products ---------*/
+.boxes {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ width: 90%;
+ align-items: center;
+}
+
+.search-container {
+ width: 250px;
+}
+
+.search-box {
+ width: 120px;
+}
+
+.filter-container {
+ width: 150px;
+}
+
+.filter {
+ display: grid;
+ grid-template-columns: 150px 150px;
+ grid-gap: 5px;
+ justify-content: right;
+}
+
+.filter .button {
+ display: none;
+}
+
+.product-list {
+ display: flex;
+ flex-flow: row wrap;
+ width: 100%;
+ justify-content: space-around;
+}
+
+.product-list li {
+ width: 400px;
+ padding-top: 20px;
+}
+
+/*--------- product ---------*/
+.product-container {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-gap: 50px;
+}
+
+.product-details {
+ min-width: 400px;
+}
+
+/********** review **********/
+.review {
+ max-width: 400px;
+}
+
+.new-review select {
+ width: 150px;
+}
+
+/*--------- cart ---------*/
+.cart {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+}
+
+.order-items li {
+ display: grid;
+ grid-template-columns: 3fr 4fr;
+ padding-right: 30px;
+ width: 600px;
+ margin-top: 20px;
+ align-items: center;
+ border-bottom: 1px solid #f1f1f1;
+}
+
+.order-item-photo img {
+ height: 200px;
+}
+
+.quantity-box {
+ width: 80px;
+}
+
+.subtotal-remove-container {
+ width: 300px;
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: center;
+ align-items: center;
+}
+
+.checkout-container {
+ width: 600px;
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: center;
+}
+
+.checkout-form {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ margin-bottom: 10px;
+ border-left: 1px solid #f1f1f1;
+}
+
+.shipping-info, .billing-info {
+ width: 250px;
+ padding-left: 30px;
+}
+
+.cart-button {
+ display: flex;
+ flex-flow: row nowrap;
+}
+
+/*--------- account ---------*/
+.status-container {
+ width: 250px;
+}
+
+/*--------- narrow-screen ---------*/
+@media screen and (max-width: 1050px) {
+ .page-header nav {
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: center;
+ }
+
+ .float-right {
+ display: flex;
+ flex-flow: row nowrap;
+ margin: 0px;
+ }
+
+ .page-header h1 {
+ font-size: 50px;
+ }
+
+ /********** cart **********/
+ .cart {
+ grid-template-columns: 1fr 2fr;
+ }
+
+ .order-items li {
+ display: flex;
+ flex-flow: column nowrap;
+ width: 300px;
+ }
+}
+
+/*--------- mobile-design ---------*/
+@media only screen and (max-width: 600px) {
+
+ /********** global **********/
+ .page-header nav, .nav-button, .float-right, .boxes, .filter, .list-container, .product-container, .cart, .form {
+ display: flex;
+ flex-flow: column nowrap;
+ margin-left: 0;
+ margin-right: 0;
+ }
+
+ .main-container {
+ padding: 0.5rem 0.5rem;
+ }
+
+ h3, h4 {
+ font-size: 25px;
+ margin: 0.5rem auto;
+ }
+
+ .hidden {
+ display: none;
+ }
+
+ /********** home **********/
+ .categories {
+ display: none;
+ }
+
+ /********** all-products **********/
+ .search-container, .filter-container {
+ width: 300px;
+ }
+
+ .filter .button {
+ display: block;
+ }
+
+ /********** cart **********/
+ .cart, .checkout-form {
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: center;
+ }
+
+ .checkout-container {
+ padding-left: 0px;
+ }
+
+ .checkout-form {
+ border: none;
+ }
+ /********** account-page **********/
+ .status-container {
+ width: 100px;
+ }
+
+ /********** order-details **********/
+ table.transpose {
+ width: 390px;
+ font-size: 19px;
+ border: 1px solid #f1f1f1;
+ }
+
+ .transpose thead {
+ float: left;
+ height: 380px;
+ border: none;
+ }
+
+ .transpose tbody {
+ float: right;
+ height: 380px;
+ border: none;
+ }
+
+ .transpose thead th, .transpose tbody td {
+ display: block;
+ }
+
+}
diff --git a/app/assets/stylesheets/browserslist b/app/assets/stylesheets/browserslist
new file mode 100644
index 0000000000..6019618a9a
--- /dev/null
+++ b/app/assets/stylesheets/browserslist
@@ -0,0 +1,4 @@
+last 2 versions
+ie >= 9
+Android >= 2.3
+ios >= 7
diff --git a/app/assets/stylesheets/cart.scss b/app/assets/stylesheets/cart.scss
new file mode 100644
index 0000000000..653388aee1
--- /dev/null
+++ b/app/assets/stylesheets/cart.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the Cart controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/categories.scss b/app/assets/stylesheets/categories.scss
new file mode 100644
index 0000000000..42976cbc11
--- /dev/null
+++ b/app/assets/stylesheets/categories.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the Categories controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/foundation_and_overrides.scss b/app/assets/stylesheets/foundation_and_overrides.scss
new file mode 100644
index 0000000000..ed4c5a0ecf
--- /dev/null
+++ b/app/assets/stylesheets/foundation_and_overrides.scss
@@ -0,0 +1,61 @@
+@charset 'utf-8';
+
+@import 'settings';
+@import 'foundation';
+
+// If you'd like to include motion-ui the foundation-rails gem comes prepackaged with it, uncomment the 3 @imports, if you are not using the gem you need to install the motion-ui sass package.
+//
+// @import 'motion-ui/motion-ui';
+
+// We include everything by default. To slim your CSS, remove components you don't use.
+
+@include foundation-global-styles;
+@include foundation-xy-grid-classes;
+//@include foundation-grid;
+//@include foundation-flex-grid;
+@include foundation-flex-classes;
+@include foundation-typography;
+@include foundation-forms;
+@include foundation-button;
+@include foundation-accordion;
+@include foundation-accordion-menu;
+@include foundation-badge;
+@include foundation-breadcrumbs;
+@include foundation-button-group;
+@include foundation-callout;
+@include foundation-card;
+@include foundation-close-button;
+@include foundation-menu;
+@include foundation-menu-icon;
+@include foundation-drilldown-menu;
+@include foundation-dropdown;
+@include foundation-dropdown-menu;
+@include foundation-responsive-embed;
+@include foundation-label;
+@include foundation-media-object;
+@include foundation-off-canvas;
+@include foundation-orbit;
+@include foundation-pagination;
+@include foundation-progress-bar;
+@include foundation-slider;
+@include foundation-sticky;
+@include foundation-reveal;
+@include foundation-switch;
+@include foundation-table;
+@include foundation-tabs;
+@include foundation-thumbnail;
+@include foundation-title-bar;
+@include foundation-tooltip;
+@include foundation-top-bar;
+@include foundation-visibility-classes;
+@include foundation-float-classes;
+
+// If you'd like to include motion-ui the foundation-rails gem comes prepackaged with it, uncomment the 3 @imports, if you are not using the gem you need to install the motion-ui sass package.
+//
+// @include motion-ui-transitions;
+// @include motion-ui-animations;
+@import 'motion-ui/motion-ui';
+@include motion-ui-transitions;
+@include motion-ui-animations;
+
+
diff --git a/app/assets/stylesheets/homepage.scss b/app/assets/stylesheets/homepage.scss
new file mode 100644
index 0000000000..9027e07a04
--- /dev/null
+++ b/app/assets/stylesheets/homepage.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the homepage controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/order_items.scss b/app/assets/stylesheets/order_items.scss
new file mode 100644
index 0000000000..584862de9b
--- /dev/null
+++ b/app/assets/stylesheets/order_items.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the OrderItems controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/orders.scss b/app/assets/stylesheets/orders.scss
new file mode 100644
index 0000000000..741506954d
--- /dev/null
+++ b/app/assets/stylesheets/orders.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the Orders controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/products.scss b/app/assets/stylesheets/products.scss
new file mode 100644
index 0000000000..bff386e55a
--- /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/assets/stylesheets/reviews.scss b/app/assets/stylesheets/reviews.scss
new file mode 100644
index 0000000000..11bbb12cd5
--- /dev/null
+++ b/app/assets/stylesheets/reviews.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the Reviews controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/sessions.scss b/app/assets/stylesheets/sessions.scss
new file mode 100644
index 0000000000..ccb1ed25b2
--- /dev/null
+++ b/app/assets/stylesheets/sessions.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the Sessions controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/users.scss b/app/assets/stylesheets/users.scss
new file mode 100644
index 0000000000..31a2eacb84
--- /dev/null
+++ b/app/assets/stylesheets/users.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the Users controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb
new file mode 100644
index 0000000000..d672697283
--- /dev/null
+++ b/app/channels/application_cable/channel.rb
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Channel < ActionCable::Channel::Base
+ end
+end
diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb
new file mode 100644
index 0000000000..0ff5442f47
--- /dev/null
+++ b/app/channels/application_cable/connection.rb
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Connection < ActionCable::Connection::Base
+ end
+end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
new file mode 100644
index 0000000000..d88abf2271
--- /dev/null
+++ b/app/controllers/application_controller.rb
@@ -0,0 +1,21 @@
+class ApplicationController < ActionController::Base
+ protect_from_forgery with: :exception
+
+ before_action :find_user
+
+ def find_user
+ @user = User.find_by(id: session[:user_id])
+ end
+
+ def render_404
+ render file: "/public/404.html", status: 404
+ end
+
+ def require_login
+
+ if @user.nil?
+ redirect_to github_login_path
+ end
+ end
+
+end
diff --git a/app/controllers/cart_controller.rb b/app/controllers/cart_controller.rb
new file mode 100644
index 0000000000..a70f12cbf6
--- /dev/null
+++ b/app/controllers/cart_controller.rb
@@ -0,0 +1,167 @@
+
+class CartController < ApplicationController
+
+ def access_cart
+ @action = update_cart_info_path
+ @cart = Order.find_by(id: session[:cart_order_id])
+ if @cart.nil?
+ render :empty_cart and return
+ else
+ render "orders/cart"
+ end
+ end
+
+ def add_to_cart
+ if !session[:cart_order_id].nil?
+ @cart = Order.find_by(id: session[:cart_order_id])
+ end
+ if @cart.nil?
+ @cart = create_cart
+ session[:cart_order_id] = @cart.id
+ end
+ @product = Product.find_by(id: params[:id])
+ if @product.nil?
+ flash[:status] = :failure
+ flash[:result_text] = "That product could not be added to your cart"
+ redirect_to cart_path and return
+ end
+ desired_quantity = total_quantity_requested
+ if desired_quantity > @product.stock
+ flash[:status] = :failure
+ flash[:result_text] = "Not enough inventory on-hand to complete your request."
+ flash[:messages] = @product.errors.messages
+ else
+ if @target_item = @cart.order_items.find_by(product_id: params[:id])
+ @target_item.quantity = desired_quantity
+ @target_item.save
+ else
+ @order_item = OrderItem.create product_id: @product.id, order_id: @cart.id, quantity: desired_quantity, is_shipped: "false"
+ end
+ flash[:status] = :success
+ flash[:result_text] = "Item added to your cart!"
+ end
+ redirect_to cart_path
+ end
+
+ def update_cart_info
+ @cart = Order.find_by(id: session[:cart_order_id])
+ if @cart && @cart.id == session[:cart_order_id]
+ @cart.name = params[:order][:name]
+ @cart.email = params[:order][:email]
+ @cart.street_address = params[:order][:street_address]
+ @cart.city = params[:order][:city]
+ @cart.state = params[:order][:state]
+ @cart.zip = params[:order][:zip]
+ @cart.name_cc = params[:order][:name_cc]
+ @cart.credit_card = params[:order][:credit_card]
+ @cart.expiry = Date.new params[:order]["expiry(1i)"].to_i, params[:order]["expiry(2i)"].to_i, params[:order]["expiry(3i)"].to_i
+ @cart.ccv= params[:order][:ccv]
+ @cart.billing_zip = params[:order][:billing_zip]
+ if @cart.save
+ # redirect_to order_path(@order.id)
+ flash[:status] = :success
+ flash[:result_text] = "Your order information has been successfully updated!"
+ redirect_to cart_path and return
+ else
+ flash[:status] = :failure
+ flash[:result_text] = "We were unable to update your order information."
+ flash[:messages] = @cart.errors.messages
+ redirect_to cart_path and return
+ end
+ else
+ flash[:status] = :failure
+ flash[:result_text] = "We were unable to find your cart."
+ render_404
+ end
+ end
+
+ def update_to_paid
+ @cart = Order.find_by(id: session[:cart_order_id])
+ @cart.confirm
+ if !@cart.save
+ flash[:status] = :failure
+ flash[:result_text] = "We weren't able to process your order. Please double-check the form."
+ flash[:messages] = @cart.errors.messages
+ redirect_to cart_path
+ else
+ flash[:status] = :success
+ flash[:result_text] = "Your order has been submitted!"
+ @order = Order.find_by(id: session[:cart_order_id])
+ session[:cart_order_id] = nil
+ render "orders/confirmation"
+ end
+ end
+
+ def destroy
+ @cart = Order.find_by(id: session[:cart_order_id])
+ if @cart.nil?
+ flash[:status] = :failure
+ flash[:result_text] = "Unable to remove the items from your cart."
+ redirect_to cart_path and return
+ end
+ if @cart.order_items.count > 0
+ @cart.order_items.each do |order_item|
+ order_item.destroy
+ end
+ else
+ flash[:status] = :failure
+ flash[:result_text] = "Your cart was already empty!"
+ end
+ redirect_to cart_path
+ end
+
+ def remove_single_item
+ @cart = Order.find_by(id: session[:cart_order_id])
+ @order_item = OrderItem.find_by(id: params[:id])
+ if @order_item && @cart && (@order_item.order_id == @cart.id)
+ @item_name = @order_item.product.name
+ @order_item.destroy
+ flash[:status] = :success
+ flash[:result_text] = "#{@item_name} removed from your cart!"
+ else
+ flash[:status] = :failure
+ flash[:result_text] = "Unable to remove the items from your cart."
+ if @cart
+ flash[:errors] = @cart.errors.messages
+ end
+ if @order_item
+ flash[:errors] = @order_item.errors.messages
+ end
+ redirect_to cart_path and return
+ end
+ if !(@cart.order_items.count > 0)
+ render :empty_cart and return
+ else
+ redirect_to cart_path
+ end
+ end
+
+private
+
+ def create_cart
+ @cart = Order.new status: "pending"
+ session[:cart_order_id] = @cart.id
+ if @cart.save
+ flash[:status] = :success
+ flash[:result_text] = "Welcome to the Puppsy shopping experience!"
+ else
+ flash[:status] = :failure
+ flash[:result_text] = "We weren't able to create your shopping cart."
+ flash[:messages] = @cart.errors.messages
+ end
+ return @cart
+ end
+
+ def total_quantity_requested
+ @product = Product.find_by(id: params[:id])
+ total_quantity = 1
+ if @product && @cart && (@cart.order_items.count > 0)
+ @cart.order_items.each do |order_item|
+ if order_item.product_id == @product.id
+ total_quantity += order_item.quantity
+ end
+ end
+ end
+ return total_quantity
+ end
+end
diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb
new file mode 100644
index 0000000000..9769ad181e
--- /dev/null
+++ b/app/controllers/categories_controller.rb
@@ -0,0 +1,38 @@
+class CategoriesController < ApplicationController
+ before_action :require_login
+ skip_before_action :require_login, only: [:create]
+
+ def index
+ @categories = Category.all
+ end
+
+ def new
+ @category = Category.new
+ end
+
+ def create
+ category_id = params[:category][:id]
+ if category_id
+ redirect_to category_products_path(category_id)
+ else
+ @category = Category.new(category_params)
+
+ if @category.save
+ flash[:status] = :success
+ flash[:result_text] = "Successfully created a category for #{@category.name}!"
+ redirect_to products_path
+ else
+ flash[:status] = :failure
+ flash[:result_text] = "Could not create this category."
+ flash[:messages] = @category.errors.messages
+ render :new, status: :bad_request
+ end
+ end
+ end
+
+ private
+
+ def category_params
+ params.require(:category).permit(:name)
+ end
+end
diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/app/controllers/homepage_controller.rb b/app/controllers/homepage_controller.rb
new file mode 100644
index 0000000000..ad65e1949c
--- /dev/null
+++ b/app/controllers/homepage_controller.rb
@@ -0,0 +1,10 @@
+class HomepageController < ApplicationController
+ def index
+ @popular_products = Product.top_sellers
+ category_outfit = Category.find_by(name: "Outfit")
+ @outfits = category_outfit.products
+ category_food = Category.find_by(name: "Food")
+ @foods = category_food.products
+ @categories = Category.all
+ end
+end
diff --git a/app/controllers/order_items_controller.rb b/app/controllers/order_items_controller.rb
new file mode 100644
index 0000000000..707c032fa7
--- /dev/null
+++ b/app/controllers/order_items_controller.rb
@@ -0,0 +1,48 @@
+class OrderItemsController < ApplicationController
+ before_action :require_login
+ skip_before_action :require_login, only: [:update]
+
+
+ def update
+ @order_item = OrderItem.find_by(id: params[:id])
+ if @order_item.nil?
+ render_404 and return
+ else
+ @order_item.quantity = params[:order_item][:quantity]
+ if @order_item.save
+ flash[:status] = :success
+ flash[:result_text] = "Quantity updated!"
+ else
+ flash[:status] = :failure
+ flash[:result_text] = "Could not add the desired quantity of this item to the cart."
+ flash[:messages] = @order_item.errors.messages
+ end
+ end
+ redirect_to cart_path
+ end
+
+ def set_status
+ @order_item = OrderItem.find_by(id: params[:id])
+ if @order_item.nil?
+ render_404
+ else
+ @order_item.update(is_shipped: true)
+ is_complete = true
+ @order_item.order.order_items.each do |order_item|
+ if order_item.is_shipped == false
+ is_complete = false
+ end
+ end
+ if is_complete
+ @order_item.order.update(status: "complete")
+ end
+ redirect_back fallback_location: user_path(@order_item.product.user)
+ end
+ end
+
+ private
+
+ def order_item_params
+ params.require(:order_item).permit(:quantity, :is_shipped, :order_id, :product_id)
+ end
+end
diff --git a/app/controllers/orders_controller.rb b/app/controllers/orders_controller.rb
new file mode 100644
index 0000000000..7b73d78118
--- /dev/null
+++ b/app/controllers/orders_controller.rb
@@ -0,0 +1,106 @@
+class OrdersController < ApplicationController
+ before_action :require_login
+ skip_before_action :require_login, only: [:confirmation]
+
+ # def index
+ # @orders = Order.all
+ # end
+
+ # def new
+ # @order = Order.new
+ # end
+
+# #TODO
+# def create
+# @order = Order.new(order_params)
+# if @order.save
+# flash[:status] = :success
+# flash[:result_text] = "Your order has been made - congratulations!"
+# redirect_to order_confirmation_path(@order.id)
+# else
+# flash[:status] = :failure
+# flash[:result_text] = "Something has gone wrong in your orders processing."
+# render :new, status: :bad_request
+# flash[:messages] = @order.errors.messages
+# end
+# end
+
+ def confirmation
+ @order = Order.find_by(id: params[:id])
+ if @order.nil?
+ render_404
+ end
+ end
+
+ def show
+ @order = Order.find_by(id: params[:id])
+ if @order.nil?
+ render_404
+ end
+ end
+
+
+ # def edit
+ # @order = Order.find_by(id: params[:id])
+ # if @order.nil?
+ # render_404
+ # end
+ # end
+
+ def update
+ @order = Order.find_by(id: params[:id])
+ if @order.nil?
+ render_404
+ else
+ @order.update_attributes(order_params)
+ if @order.valid?
+ @order.save
+ redirect_to order_path(@order.id)
+ flash[:status] = :success
+ flash[:result_text] = "#{@order.name} has been updated"
+ else
+ render :show, status: :bad_request
+ flash[:status] = :failure
+ flash[:result_text] = "#{@order.name} update has failed"
+ flash[:messages] = @order.errors.messages
+ end
+ end
+ end
+
+ def cancel
+ @order = Order.find_by(id: params[:id])
+ if @order.nil?
+ render_404
+ else
+ @order.cancel
+ if @order.save
+ flash[:status] = :success
+ flash[:result_text] = "Your order has been cancelled!"
+ end
+ redirect_to products_path
+ end
+ end
+
+
+ # def destroy
+ # @order = Order.find_by(id: params[:id])
+ # if @order.nil?
+ # render_404
+ # else
+ # if @order
+ # @order.order_items.each do |item|
+ # item.destroy
+ # end
+ # @order.destroy
+ # end
+ # end
+ # end
+
+ private
+
+ def order_params
+ params.require(:order).permit(:status,:name,:email,:street_address,:city,:state,:zip,:name_cc,:credit_card,:expiry,:ccv,:billing_zip)
+ end
+
+
+end
diff --git a/app/controllers/products_controller.rb b/app/controllers/products_controller.rb
new file mode 100644
index 0000000000..b47966a138
--- /dev/null
+++ b/app/controllers/products_controller.rb
@@ -0,0 +1,117 @@
+class ProductsController < ApplicationController
+ before_action :require_login
+ skip_before_action :require_login, only: [:index, :show]
+
+
+ def index
+ @category = Category.new
+ @user = User.new
+ if params[:category_id]
+ @current_category = Category.find_by(id: params[:category_id])
+ @products = @current_category.products
+ @current_user = nil
+ elsif params[:user_id]
+ @current_user = User.find_by(id: params[:user_id])
+ @products = @current_user.products
+ @current_category = nil
+ elsif params[:term]
+ @products = Product.search(params[:term])
+ else
+ @products = Product.all
+ end
+ end
+
+ def new
+ @product = Product.new
+ @product.user = User.find(params[:user_id])
+ @action = user_products_path(params[:user_id])
+ end
+
+ def create
+ @product = Product.new(product_params)
+ @product.price = params[:product][:price].to_i * 100
+ @product.user = User.find(session[:user_id])
+ unless @product.user_id == session[:user_id]
+ @product.destroy
+ end
+ if @product.save && ( @product.user_id == session[:user_id] )
+ flash[:status] = :success
+ flash[:result_text] = "#{@product.name} has been successfully created!"
+ redirect_to product_path(@product.id)
+ else
+ flash[:status] = :failure
+ flash[:result_text] = "Could not create this product."
+ flash[:messages] = @product.errors.messages
+ render :new, status: :bad_request
+ if @product
+ @product.destroy
+ end
+ end
+ end
+
+ def show
+ @product = Product.find_by(id: params[:id])
+ if @product.nil?
+ render_404
+ else
+ @review = Review.new
+ @action = product_reviews_path(params[:id])
+ end
+ end
+
+ def edit
+ @product = Product.find_by(id: params[:id])
+ if @product.nil?
+ render_404 and return
+ end
+ if @product.user_id == session[:user_id]
+ @action = product_path(params[:id])
+ else
+ flash[:status] = :failure
+ flash[:result_text] = "Merchants are only allowed to modify their own products"
+ redirect_to root_path
+ end
+ end
+
+ def update
+ @product = Product.find_by(id: params[:id])
+ if @product.nil?
+ render_404 and return
+ end
+ if @product.user_id == session[:user_id]
+ @product.update(product_params)
+ @product.price = params[:product][:price].to_i * 100
+ if @product.save
+ flash[:status] = :success
+ flash[:result_text] = "#{@product.name} has been successfully updated!"
+ redirect_to product_path(@product.id) and return
+ else
+ flash[:status] = :failure
+ flash[:result_text] = "Update failed."
+ flash[:messages] = @product.errors.messages
+ render :edit, status: :bad_request and return
+ end
+ else
+ flash[:status] = :failure
+ flash[:result_text] = "Merchants are only allowed to modify their own products"
+ redirect_to product_path(@product.id)
+ end
+ end
+
+ def set_status
+ @product = Product.find_by(id: params[:id])
+ if @product.nil?
+ render_404
+ else
+ @product.toggle_is_active
+ @product.save
+ redirect_back fallback_location: user_path(@product.user)
+ end
+ end
+
+ private
+
+ def product_params
+ params.require(:product).permit(:name, :is_active, :description, :price, :photo_url, :stock, :user_id, :term, :category_ids => [] )
+ end
+end
diff --git a/app/controllers/reviews_controller.rb b/app/controllers/reviews_controller.rb
new file mode 100644
index 0000000000..d34662b00b
--- /dev/null
+++ b/app/controllers/reviews_controller.rb
@@ -0,0 +1,22 @@
+class ReviewsController < ApplicationController
+
+ def create
+ @review = Review.new(review_params)
+ @review[:product_id] = params[:product_id]
+ if @review.save
+ redirect_to product_path(params[:product_id])
+ flash[:status] = :success
+ flash[:result_text] = "Your comment was saved"
+ else
+ render :new, status: :bad_request
+ flash[:status] = :failure
+ flash[:result_text] = "Your comment was not saved"
+ flash[:messages] = @review.errors.messages
+ end
+ end
+
+ private
+ def review_params
+ params.require(:review).permit(:rating,:content)
+ end
+end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
new file mode 100644
index 0000000000..6ba283dd9d
--- /dev/null
+++ b/app/controllers/sessions_controller.rb
@@ -0,0 +1,52 @@
+
+class SessionsController < ApplicationController
+ before_action :require_login
+ skip_before_action :require_login, only: [:create]
+
+ def new
+ @username = User.new
+ end
+
+ def create
+ auth_hash = request.env['omniauth.auth']
+ if auth_hash[:uid]
+ @user = User.find_by(uid: auth_hash[:uid], provider: 'github')
+ if @user.nil?
+ @user = User.build_from_github(auth_hash)
+ @user.update_image(request.env["omniauth.auth"]["info"]["image"])
+ successful_save = @user.save
+ if successful_save
+ flash[:status] = :success
+ flash[:result_text] = "Successful first login!"
+ else
+ flash[:status] = :failure
+ flash[:result_text] = "An error occurred during User creation."
+ flash[:messages] = @user.errors.messages
+ end
+ else
+ flash[:status] = :success
+ flash[:result_text] = "Logged in successfully"
+ @user.update_image(request.env["omniauth.auth"]["info"]["image"])
+ @user.save
+ end
+ session[:user_id] = @user.id
+ session_test = session[:user_id]
+ else
+ flash[:status] = :failure
+ flash[:result_text] = "Logging in through Github not successful"
+ end
+ redirect_to root_path
+ end
+
+ # def index
+ # @user = User.find(session[:user_id]) # < recalls the value set in a previous request
+ # end
+
+ def destroy
+ session[:user_id] = nil
+ flash[:status] = :success
+ flash[:result_text] = "Successfully logged out"
+ redirect_to root_path
+ end
+
+end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
new file mode 100644
index 0000000000..b8c8e3fea8
--- /dev/null
+++ b/app/controllers/users_controller.rb
@@ -0,0 +1,47 @@
+class UsersController < ApplicationController
+ before_action :require_login
+ skip_before_action :require_login, only: [:create]
+
+ def index
+ @users = User.all
+ end
+
+ def create
+ user_id = params[:user][:id]
+ if user_id
+ redirect_to user_products_path(user_id)
+ else
+ flash[:status] = :failure
+ flash[:result_text] = "Could not create a new user ID."
+ # flash[:messages] = @user.errors.messages
+ end
+ end
+
+ def show
+ @user = User.find_by(id: params[:id])
+ if @user.nil?
+ render_404
+ else
+ @user_products = @user.products
+ # Model method for getting the relevant orders is needed
+ if params[:term]
+ @term = params[:term]
+ @orders = @user.list_orders_by_status(@term)
+ # raise
+ @revenue = @user.total_revenue_by_status(@term)
+ @num_orders = @user.num_orders_by_status(@term)
+ else
+ @orders = @user.list_all_orders
+ @revenue = @user.total_revenue
+ @num_orders = @user.num_orders
+ end
+ end
+ end
+
+ private
+
+ def user_params
+ params.require(:user).permit(:username, :email, :uid, :provider, :term )
+ end
+
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
new file mode 100644
index 0000000000..e8bb1b7a71
--- /dev/null
+++ b/app/helpers/application_helper.rb
@@ -0,0 +1,17 @@
+module ApplicationHelper
+ def price_in_dollar(price)
+ sprintf('%.2f', (price / 100.00))
+ end
+
+ def pretty_date(date)
+ return date.strftime("%b %e, %Y")
+ end
+
+ def discard_day(date)
+ return date.strftime("%m/%Y")
+ end
+
+ # def display_image(photo_url)
+ # [" "].join.html_safe
+ # end
+end
diff --git a/app/helpers/cart_helper.rb b/app/helpers/cart_helper.rb
new file mode 100644
index 0000000000..ac02bcd1bf
--- /dev/null
+++ b/app/helpers/cart_helper.rb
@@ -0,0 +1,2 @@
+module CartHelper
+end
diff --git a/app/helpers/categories_helper.rb b/app/helpers/categories_helper.rb
new file mode 100644
index 0000000000..e06f31554c
--- /dev/null
+++ b/app/helpers/categories_helper.rb
@@ -0,0 +1,2 @@
+module CategoriesHelper
+end
diff --git a/app/helpers/homepage_helper.rb b/app/helpers/homepage_helper.rb
new file mode 100644
index 0000000000..c5bbfe518f
--- /dev/null
+++ b/app/helpers/homepage_helper.rb
@@ -0,0 +1,2 @@
+module HomepageHelper
+end
diff --git a/app/helpers/order_items_helper.rb b/app/helpers/order_items_helper.rb
new file mode 100644
index 0000000000..e197528ae1
--- /dev/null
+++ b/app/helpers/order_items_helper.rb
@@ -0,0 +1,2 @@
+module OrderItemsHelper
+end
diff --git a/app/helpers/orders_helper.rb b/app/helpers/orders_helper.rb
new file mode 100644
index 0000000000..443227fd48
--- /dev/null
+++ b/app/helpers/orders_helper.rb
@@ -0,0 +1,2 @@
+module OrdersHelper
+end
diff --git a/app/helpers/products_helper.rb b/app/helpers/products_helper.rb
new file mode 100644
index 0000000000..ab5c42b325
--- /dev/null
+++ b/app/helpers/products_helper.rb
@@ -0,0 +1,2 @@
+module ProductsHelper
+end
diff --git a/app/helpers/reviews_helper.rb b/app/helpers/reviews_helper.rb
new file mode 100644
index 0000000000..682b7b1abc
--- /dev/null
+++ b/app/helpers/reviews_helper.rb
@@ -0,0 +1,2 @@
+module ReviewsHelper
+end
diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb
new file mode 100644
index 0000000000..309f8b2eb3
--- /dev/null
+++ b/app/helpers/sessions_helper.rb
@@ -0,0 +1,2 @@
+module SessionsHelper
+end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
new file mode 100644
index 0000000000..2310a240d7
--- /dev/null
+++ b/app/helpers/users_helper.rb
@@ -0,0 +1,2 @@
+module UsersHelper
+end
diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb
new file mode 100644
index 0000000000..a009ace51c
--- /dev/null
+++ b/app/jobs/application_job.rb
@@ -0,0 +1,2 @@
+class ApplicationJob < ActiveJob::Base
+end
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
new file mode 100644
index 0000000000..286b2239d1
--- /dev/null
+++ b/app/mailers/application_mailer.rb
@@ -0,0 +1,4 @@
+class ApplicationMailer < ActionMailer::Base
+ default from: 'from@example.com'
+ layout 'mailer'
+end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
new file mode 100644
index 0000000000..10a4cba84d
--- /dev/null
+++ b/app/models/application_record.rb
@@ -0,0 +1,3 @@
+class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+end
diff --git a/app/models/category.rb b/app/models/category.rb
new file mode 100644
index 0000000000..dd01aeaecf
--- /dev/null
+++ b/app/models/category.rb
@@ -0,0 +1,12 @@
+class Category < ApplicationRecord
+ has_and_belongs_to_many :products
+ validates :name, presence: true, uniqueness: true
+ before_validation :capitalize_name
+
+ private
+ def capitalize_name
+ if !name.nil?
+ self.name = name.capitalize
+ end
+ end
+end
diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/app/models/order.rb b/app/models/order.rb
new file mode 100644
index 0000000000..a52202526a
--- /dev/null
+++ b/app/models/order.rb
@@ -0,0 +1,58 @@
+class Order < ApplicationRecord
+ has_many :order_items
+ validates :status, presence: true
+ validates :name, presence: true, length: { minimum: 3 }, unless: :in_cart?
+ validates :email, presence: true, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }, unless: :in_cart?
+ validates :street_address, presence: true, unless: :in_cart?
+ validates :city, presence: true, unless: :in_cart?
+ validates :state, presence: true, unless: :in_cart?
+ validates :zip, presence: true, length: { is: 5 }, unless: :in_cart?
+ validates :name_cc, presence: true, unless: :in_cart?
+ validates :credit_card, presence: true, length: { in: 14..19 }, unless: :in_cart?
+ validates :expiry, presence: true, unless: :in_cart?
+ validates :ccv, presence: true, length: { in: 3..4 }, unless: :in_cart?
+ validates :billing_zip, presence: true, length: { is: 5 }, unless: :in_cart?
+ validate :cc_expiry_cannot_be_in_the_past, unless: :in_cart?
+
+ def cc_expiry_cannot_be_in_the_past
+ if expiry.present? && expiry < Date.today
+ errors.add(:expiry, "can't be in the past")
+ end
+ end
+
+ # validate :check_atleast_one_order_item, unless: :in_cart?
+
+ # def check_atleast_one_order_item
+ # if order_items.count < 1
+ # errors.add(:order, "need atleast one order item")
+ # end
+ # end
+
+ def total_cost
+ order_items.map { |order_item| order_item.subtotal }.sum.round(2)
+ end
+
+ def credit_card_last_digits
+ credit_card[-4..-1]
+ end
+
+ def can_cancel?
+ status == "paid" && !order_items.any? { |i| i.is_shipped }
+ end
+
+ def confirm
+ self.status = "paid"
+ self.order_items.each { |item| item.product.update_attributes({:stock => item.product.stock - item.quantity}) }
+ end
+
+ def cancel
+ self.status = "cancelled"
+ self.order_items.each { |item| item.product.update_attributes({:stock => item.product.stock + item.quantity}) }
+ end
+
+ private
+
+ def in_cart?
+ status == "pending"
+ end
+end
diff --git a/app/models/order_item.rb b/app/models/order_item.rb
new file mode 100644
index 0000000000..c2b1c3d56b
--- /dev/null
+++ b/app/models/order_item.rb
@@ -0,0 +1,29 @@
+class OrderItem < ApplicationRecord
+ belongs_to :order
+ belongs_to :product
+ validates :quantity, presence: true, numericality: { only_integer: true, greater_than: 0 }
+ validate :check_product_is_active, if: :contains_product?
+ validate :check_quantity, if: :contains_product?
+
+ def check_product_is_active
+ if !product.is_active
+ errors.add(:is_active,"product is no longer available")
+ end
+ end
+
+ def check_quantity
+ if quantity.to_i > product.stock
+ errors.add(:stock, "only #{product.stock} items available")
+ end
+ end
+
+ def subtotal
+ ((quantity * product.price)/100.0).round(2)
+ end
+
+ private
+ def contains_product?
+ !product.nil?
+ end
+
+end
diff --git a/app/models/product.rb b/app/models/product.rb
new file mode 100644
index 0000000000..a25627b7de
--- /dev/null
+++ b/app/models/product.rb
@@ -0,0 +1,29 @@
+class Product < ApplicationRecord
+ belongs_to :user
+ has_and_belongs_to_many :categories
+ has_many :reviews
+ has_many :order_items
+ validates :name, presence: true, uniqueness: { case_sensitive: false }
+ validates :price, presence: true, numericality: { only_integer: true, greater_than: 0 }
+ validates :stock, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
+
+ def average_rating
+ (reviews.average(:rating) || 0).round(1)
+ end
+
+ def toggle_is_active
+ self.is_active = !is_active
+ end
+
+ def self.top_sellers(count = 5)
+ sorted_products = self.all.sort_by { |p|
+ p.order_items.map { |i| i.quantity }.sum
+ }.reverse!
+ actual_count = [count, sorted_products.count].min
+ return sorted_products[0...actual_count]
+ end
+
+ def self.search(term)
+ where('lower(name) LIKE ?', "%#{term.downcase}%").order('id DESC')
+ end
+end
diff --git a/app/models/review.rb b/app/models/review.rb
new file mode 100644
index 0000000000..86ef794b29
--- /dev/null
+++ b/app/models/review.rb
@@ -0,0 +1,5 @@
+class Review < ApplicationRecord
+ belongs_to :product
+ validates :rating, presence: true, numericality: { only_integer: true }
+ validates_inclusion_of :rating, in: (1..5)
+end
diff --git a/app/models/user.rb b/app/models/user.rb
new file mode 100644
index 0000000000..d668ea8654
--- /dev/null
+++ b/app/models/user.rb
@@ -0,0 +1,48 @@
+class User < ApplicationRecord
+ has_many :products
+ has_many :order_items, through: :products
+ has_many :orders, through: :order_items
+
+ validates :username, presence: true, uniqueness: true
+ validates :email, presence: true, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }, uniqueness: true
+
+ def self.build_from_github(auth_hash)
+ return User.new(
+ username: auth_hash[:info][:nickname],
+ email: auth_hash[:info][:email],
+ uid: auth_hash[:uid],
+ provider: auth_hash[:provider]
+ )
+ end
+
+ def update_image(image)
+ self.image = image
+ end
+
+ def num_orders
+ order_items.map { |order_item| order_item.order }.uniq.count
+ end
+
+ def total_revenue
+ order_items.map { |order_item| order_item.subtotal }.sum.round(2)
+ end
+
+ def num_orders_by_status(status)
+ order_items.map { |order_item|
+ order_item.order if order_item.order.status == status }.compact.uniq.count
+ end
+
+ def total_revenue_by_status(status)
+ order_items.map { |order_item|
+ order_item.subtotal if order_item.order.status == status }.compact.sum.round(2)
+ end
+
+ def list_all_orders
+ order_items.map { |order_item| order_item.order }.compact.uniq
+ end
+
+ def list_orders_by_status(status)
+ order_items.map { |order_item| order_item.order if order_item.order.status == status }.compact.uniq
+ end
+
+ end
diff --git a/app/views/cart/empty_cart.html.erb b/app/views/cart/empty_cart.html.erb
new file mode 100644
index 0000000000..0dc326393d
--- /dev/null
+++ b/app/views/cart/empty_cart.html.erb
@@ -0,0 +1,3 @@
+
Cart
+
+ Your cart is empty!
diff --git a/app/views/categories/new.html.erb b/app/views/categories/new.html.erb
new file mode 100644
index 0000000000..c6f468d34e
--- /dev/null
+++ b/app/views/categories/new.html.erb
@@ -0,0 +1,10 @@
+Add a New Category
+
+
+ <%= form_for @category, validate: true do |f| %>
+ <%= f.label :category_name %>
+ <%= f.text_field :name %>
+
+ <%= f.submit "Save", class: "button" %>
+ <% end %>
+
diff --git a/app/views/homepage/index.html.erb b/app/views/homepage/index.html.erb
new file mode 100644
index 0000000000..b9ece890bd
--- /dev/null
+++ b/app/views/homepage/index.html.erb
@@ -0,0 +1,86 @@
+Adoptable Puppies
+
+
+
+
+ <%= image_tag "dog-1.jpg", alt: "Dog-1" %>
+ Some introduction for this cute puppy.
+
+
+ <%= image_tag "dog-2.jpg", alt: "Dog-2" %>
+ Some introduction for this cute puppy.
+
+
+ <%= image_tag "dog-3.jpg", alt: "Dog-3" %>
+ Some introduction for this cute puppy.
+
+
+ <%= image_tag "dog-4.jpg", alt: "Dog-4" %>
+ Some introduction for this cute puppy.
+
+
+ <%= image_tag "dog-5.jpg", alt: "Dog-5" %>
+ Some introduction for this cute puppy.
+
+
+
+
+ First slide details. Current Slide
+ Second slide details.
+ Third slide details.
+ Fourth slide details.
+ Fifth slide details.
+
+
+
+
+
+
+ Popular
+
+ <% @popular_products.each do |popular_product| %>
+ <% if popular_product.is_active %>
+ <%= link_to popular_product.name, product_path(popular_product.id) %>
+ <%= link_to image_tag(popular_product.photo_url, alt: popular_product.name), product_path(popular_product.id) %>
+ <%# Code used for http links %>
+ <%# display_image(popular_product.photo_url) %>
+ <% end %>
+ <% end %>
+
+
+
+
+ Outfit
+
+ <% @outfits.each do |outfit| %>
+ <% if outfit.is_active %>
+ <%= link_to outfit.name, product_path(outfit.id) %>
+ <%= link_to image_tag(outfit.photo_url, alt: outfit.name), product_path(outfit.id) %>
+ <% end %>
+ <% end %>
+
+
+
+
+ Food
+
+ <% @foods.each do |food| %>
+ <% if food.is_active %>
+ <%= link_to food.name, product_path(food.id) %>
+ <%= link_to image_tag(food.photo_url, alt: food.name), product_path(food.id) %>
+ <% end %>
+ <% end %>
+
+
+
+
+
+ Categories
+
+ <% @categories.each do |category| %>
+ <%= link_to category.name, category_products_path(category.id) %>
+ <% end %>
+
+
+
+
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
new file mode 100644
index 0000000000..9c153a258a
--- /dev/null
+++ b/app/views/layouts/application.html.erb
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+ Puppsy
+
+ <%= stylesheet_link_tag "application" %>
+ <%= javascript_include_tag "application", 'data-turbolinks-track' => true, 'rails.validations' => true %>
+ <%= csrf_meta_tags %>
+ <%= favicon_link_tag 'favicon.png' %>
+
+
+
+
+
+
+
+
+
+ <% if flash[:result_text] || flash[:status] || flash[:messages] %>
+
+ <% flash.each do |name, message| %>
+ <% if name == "messages" %>
+
+ <% flash[:messages].each do |type, message| %>
+
+ <%= type %>: <%= message[0] %>
+
+ <% end %>
+
+ <% elsif name == "status" %>
+ <% if flash[:status] == "success" %>
+ <%= flash[:result_text] %>
+ <% elsif flash[:status] == "failure" %>
+ <%= flash[:result_text] %>
+ <% end %>
+ <% end %>
+ <% end %>
+
+ <% end %>
+
+
+ <%= yield %>
+
+
+
+
+ <%= image_tag "dog-with-keys.png", alt: "Fur-eign Keys", class: "fureign-keys" %> ©2018 Fur-eign Keys
+
+
+
+
diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb
new file mode 100644
index 0000000000..cbd34d2e9d
--- /dev/null
+++ b/app/views/layouts/mailer.html.erb
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+ <%= yield %>
+
+
diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb
new file mode 100644
index 0000000000..37f0bddbd7
--- /dev/null
+++ b/app/views/layouts/mailer.text.erb
@@ -0,0 +1 @@
+<%= yield %>
diff --git a/app/views/orders/cart.html.erb b/app/views/orders/cart.html.erb
new file mode 100644
index 0000000000..08b4b5f142
--- /dev/null
+++ b/app/views/orders/cart.html.erb
@@ -0,0 +1,99 @@
+Cart
+
+<% if @cart.order_items.count > 0 %>
+
+
+ <% @cart.order_items.each do |order_item| %>
+
+
+ <% if order_item.product.photo_url %>
+ <% if order_item.product.photo_url.length > 0 %>
+ <%= link_to image_tag(order_item.product.photo_url, alt: order_item.product.name), product_path(order_item.product.id) %>
+ <% else %>
+ <%= link_to image_tag('favicon.png', alt: 'No URL in record'), product_path(order_item.product.id) %>
+ <% end %>
+ <% end %>
+
+
+
<%= order_item.product.description %>
+
+ <%= form_for order_item do |f| %>
+
+ <%= f.select :quantity, (1..order_item.product.stock).to_a, :prompt => "#{order_item.quantity}" %>
+
+
+ <%= f.submit "Update", class: "button" %>
+
+ <% end %>
+
+
+
Subtotal: $<%= price_in_dollar(order_item.subtotal) %>
+ <%= button_to "Remove", {:action =>"remove_single_item", :controller => "cart", id: order_item.id }, method: :delete, class: "button" %>
+
+
+
+ <% end %>
+
+
+
+ <%= form_for @cart, url: @action, validate: true do |f| %>
+
+ <%= f.submit "Save Information", class: "button" %>
+ <% end %>
+
+
+ <%= button_to "Empty Cart", {:action =>"destroy", :controller => "cart" }, :data => {:confirm => 'Are you sure you want to delete all your items?'}, method: :delete, class: "button" %>
+ <% if @cart.name && @cart.name.length > 0 %>
+ <%= button_to "PLACE ORDER", {:action =>"update_to_paid", :controller => "cart" }, method: :patch, class: "button" %>
+ <% end %>
+
+
+
+<% else %>
+ Your cart is empty!
+<% end %>
diff --git a/app/views/orders/confirmation.html.erb b/app/views/orders/confirmation.html.erb
new file mode 100644
index 0000000000..b9c1e26ad5
--- /dev/null
+++ b/app/views/orders/confirmation.html.erb
@@ -0,0 +1,47 @@
+Order Confirmation
+
+
+
+
+ Order number
+ Total price
+ Date placed
+ Status
+
+
+
+
+ <%= @order.id %>
+ $<%= @order.total_cost %>
+ <%= pretty_date(@order.updated_at) %>
+
+ <%= @order.status %>
+
+
+
+
+
+<% if @order.can_cancel? %>
+ <%= link_to "Cancel", order_cancel_path(@order.id), method: :put, class: "button" %>
+<% end %>
+
+Order Items
+
+
+
+
+ Item
+ Quantity
+ Subtotal
+
+
+
+ <% @order.order_items.each do |order_item| %>
+
+ <%= link_to order_item.product.name, product_path(order_item.product) %>
+ <%= order_item.quantity %>
+ $<%= order_item.subtotal %>
+
+ <% end %>
+
+
diff --git a/app/views/orders/show.html.erb b/app/views/orders/show.html.erb
new file mode 100644
index 0000000000..37a1381667
--- /dev/null
+++ b/app/views/orders/show.html.erb
@@ -0,0 +1,49 @@
+Order Details
+
+
+
+
+ Order number
+ Total price
+ Date placed
+ Status
+
+
+
+
+ <%= @order.id %>
+ $<%= @order.total_cost %>
+ <%= pretty_date(@order.updated_at) %>
+ <%= @order.status %>
+
+
+
+
+Customer Information
+
+
+
+
+ Name
+ Email
+ Street Address
+ City
+ State
+ Zip Code
+ Last 4 digits of Card
+ Expiration Date
+
+
+
+
+ <%= @order.name %>
+ <%= @order.email %>
+ <%= @order.street_address %>
+ <%= @order.city %>
+ <%= @order.state %>
+ <%= @order.zip %>
+ <%= @order.credit_card_last_digits %>
+ <%= discard_day(@order.expiry) %>
+
+
+
diff --git a/app/views/products/edit.html.erb b/app/views/products/edit.html.erb
new file mode 100644
index 0000000000..ed1fb60d63
--- /dev/null
+++ b/app/views/products/edit.html.erb
@@ -0,0 +1,26 @@
+Update Product
+
+
+ <%= form_for @product, url: @action, validate: true do |f| %>
+
+ <%= f.label :name %>
+ <%= f.text_field :name %>
+
+ <%= f.label :price %>
+ <%= f.text_field :price %>
+
+ <%= f.label :description %>
+ <%= f.text_area :description %>
+
+ <%= f.label :select_a_category %>
+ <%= f.collection_select :category_ids, Category.order(:name), :id, :name, {}, {multiple: true} %>
+
+ <%= f.label :photo_url %>
+ <%= f.text_field :photo_url %>
+
+ <%= f.label :stock %>
+ <%= f.text_field :stock %>
+
+ <%= f.submit "Update", class: "button" %>
+
+<% end %>
diff --git a/app/views/products/index.html.erb b/app/views/products/index.html.erb
new file mode 100644
index 0000000000..cb200c709c
--- /dev/null
+++ b/app/views/products/index.html.erb
@@ -0,0 +1,74 @@
+List of Products
+<% if @current_category %>
+ Category: <%= @current_category.name %>
+<% end %>
+<% if @current_user %>
+ Seller: <%= @current_user.username %>
+<% end %>
+
+
+
+ <%= form_tag(products_path, method: :get) do %>
+
+ <%= text_field_tag :term, nil, placeholder: "🔍" %>
+
+
+ <%= submit_tag 'Search', class: "button" %>
+
+ <% end %>
+
+
+
+ <%= form_for @category do |f| %>
+
+ <%= f.select :id, Category.all.map { |category| [category.name, category.id] }, :prompt => 'Select a category' %>
+
+
+ <%= f.submit "Sumbit", class: "button" %>
+
+ <% end %>
+
+
+ <%= form_for @user do |f| %>
+
+ <%= f.select :id, User.all.map { |user| [user.username, user.id] }, :prompt => 'Select a seller' %>
+
+
+ <%= f.submit "Sumbit", class: "button" %>
+
+ <% end %>
+
+
+
+
+
+ <% if @products == [] %>
+ Nothing has been found.
+ <% else %>
+ <% @products.each do |product| %>
+ <% if product.is_active %>
+
+ <%= product.name %>
+ <% if product.photo_url.nil? %>
+ <%= link_to image_tag('favicon.png', alt: 'No URL in record'), product_path(product.id) %>
+ <% else %>
+ <%= link_to image_tag(product.photo_url, alt: product.name), product_path(product.id) %>
+ <% end %>
+ Price: $<%= price_in_dollar(product.price) %>
+
+ <% product.average_rating.to_i.times do |i| %>
+ ⭐️
+ <% end %>
+
+ <%= product.description %>
+ <% if product.stock > 0 %>
+ <%= product.stock %> in stock
+ <%= button_to "Add to Cart", {:action =>"add_to_cart", :controller => "cart", id: product.id }, method: :post, class: "button" %>
+ <% else %>
+ Out of Stock
+ <% end %>
+
+ <% end %>
+ <% end %>
+ <% end %>
+
diff --git a/app/views/products/new.html.erb b/app/views/products/new.html.erb
new file mode 100644
index 0000000000..d572562282
--- /dev/null
+++ b/app/views/products/new.html.erb
@@ -0,0 +1,25 @@
+Create a New Product
+
+
+ <%= form_for @product, url: @action, validate: true do |f| %>
+ <%= f.label :name %>
+ <%= f.text_field :name %>
+
+ <%= f.label :price %>
+ <%= f.text_field :price, placeholder: "$" %>
+
+ <%= f.label :description %>
+ <%= f.text_area :description %>
+
+ <%= f.label :select_a_category %>
+ <%= f.collection_select :category_ids, Category.order(:name), :id, :name, {}, {multiple: true} %>
+
+ <%= f.label :photo_url %>
+ <%= f.text_field :photo_url %>
+
+ <%= f.label :stock %>
+ <%= f.text_field :stock %>
+
+ <%= f.submit "Save", class: "button" %>
+ <% end %>
+
diff --git a/app/views/products/show.html.erb b/app/views/products/show.html.erb
new file mode 100644
index 0000000000..1881e785f2
--- /dev/null
+++ b/app/views/products/show.html.erb
@@ -0,0 +1,51 @@
+<%= @product.name %>
+by <%= link_to @product.user.username, user_products_path(@product.user.id) %>
+
+<% if @product.is_active %>
+
+
+ <% if @product.photo_url.nil? %>
+ <%= link_to image_tag('favicon.png', alt: 'No URL in record'), product_path(@product.id) %>
+ <% else %>
+ <%= link_to image_tag(@product.photo_url, alt: @product.name), product_path(@product.id) %>
+ <% end %>
+ <%= @product.description %>
+ Price: $<%= price_in_dollar(@product.price) %>
+ <% if @product.stock > 0 %>
+ <%= @product.stock %> in stock
+ <%= button_to "Add to Cart", {:action =>"add_to_cart", :controller => "cart", id: @product.id }, method: :post, class: "button" %>
+ <% else %>
+ Out of Stock
+ <% end %>
+
+
+
+
Write a Review
+
+ <%= form_for @review, url: @action, validate: true do |f| %>
+ <%= f.select :rating, (1..5).to_a, :prompt => 'Select a rating'%>
+
+ <%= f.label :comment %>
+ <%= f.text_area :content %>
+
+ <%= f.submit "Sumbit", class: "button" %>
+ <% end %>
+
+
+
Reviews
+ <% @product.reviews.reverse.each do |review| %>
+
+
+ <% review.rating.times do |i| %>
+ ⭐️
+ <% end %>
+
+ <%= review.content %>
+
+ <% end %>
+
+
+
+<% else %>
+ The product has been discontinued by seller.
+<% end %>
diff --git a/app/views/reviews/new.html.erb b/app/views/reviews/new.html.erb
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb
new file mode 100644
index 0000000000..e71ae7f245
--- /dev/null
+++ b/app/views/users/show.html.erb
@@ -0,0 +1,120 @@
+User Account Page
+
+
+ <% if @user.image %>
+ <%= image_tag @user.image %>
+ <% end %>
+ <% if @user.username %>
+ <%= @user.username %>
+ <% end %>
+
+
+
+ <%= link_to "Add Categories", new_category_path, class: "button" %>
+ <%= link_to "Add Products", new_user_product_path(@user.id), class: "button" %>
+
+
+List of Products
+
+
+
+ Name
+ Price
+ Stock
+ Update
+ Retire
+
+
+
+ <% @user.products.each do |product| %>
+
+ <%= link_to product.name, product_path(product) %>
+ $<%= price_in_dollar(product.price) %>
+ <%= product.stock %>
+
+ <% if product.is_active %>
+ <%= link_to "Update", edit_product_path(product.id), class: "button" %>
+ <% else %>
+ Update
+ <% end %>
+
+
+ <% if product.is_active %>
+ <%= link_to "Retire", product_set_status_path(product.id), method: :put, class: "button" %>
+ <% else %>
+ <%= link_to "Reactivate", product_set_status_path(product.id), method: :put, class: "button" %>
+ <% end %>
+
+
+
+ <% end %>
+
+
+
+List of Orders
+
+
+
+ Order #
+
+
+ <%= form_tag(user_path(@user), method: :get) do %>
+
+ <%= select_tag :term, "pending paid complete cancelled ".html_safe, :prompt => 'Select a status' %>
+
+
+ <%= submit_tag "Submit", class: "button" %>
+
+ <% end %>
+
+
+ Order Items
+ Quantity
+ Subtotal
+ Date Placed
+ Shipping Status
+
+
+
+ <% @orders.each do |order| %>
+ <% order.order_items.each do |order_item| %>
+ <% if order_item.product.user == @user %>
+
+ <%= link_to order.id, order_path(order.id) %>
+ <%= order.status %>
+ <%= link_to order_item.product.name, product_path(order_item.product.id) %>
+ <%= order_item.quantity %>
+ $<%= order_item.subtotal %>
+ <%= pretty_date(order_item.created_at) %>
+
+ <% if order_item.order.status == "pending" || order_item.order.status == "cancelled" %>
+ N/A
+ <% elsif order_item.is_shipped %>
+ Shipped
+ <% else %>
+ <%= link_to "Mark Shipped", order_item_set_status_path(order_item.id), method: :put, class: "button" %>
+ <% end %>
+
+
+ <% end %>
+ <% end %>
+ <% end %>
+
+
+
+Order Statistics
+
+
+
+
+ Number of Orders
+ Total Revenue
+
+
+
+
+ <%= @num_orders %>
+ $<%= @revenue %>
+
+
+
diff --git a/bin/bundle b/bin/bundle
new file mode 100755
index 0000000000..66e9889e8b
--- /dev/null
+++ b/bin/bundle
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+load Gem.bin_path('bundler', 'bundle')
diff --git a/bin/rails b/bin/rails
new file mode 100755
index 0000000000..5badb2fde0
--- /dev/null
+++ b/bin/rails
@@ -0,0 +1,9 @@
+#!/usr/bin/env ruby
+begin
+ load File.expand_path('../spring', __FILE__)
+rescue LoadError => e
+ raise unless e.message.include?('spring')
+end
+APP_PATH = File.expand_path('../config/application', __dir__)
+require_relative '../config/boot'
+require 'rails/commands'
diff --git a/bin/rake b/bin/rake
new file mode 100755
index 0000000000..d87d5f5781
--- /dev/null
+++ b/bin/rake
@@ -0,0 +1,9 @@
+#!/usr/bin/env ruby
+begin
+ load File.expand_path('../spring', __FILE__)
+rescue LoadError => e
+ raise unless e.message.include?('spring')
+end
+require_relative '../config/boot'
+require 'rake'
+Rake.application.run
diff --git a/bin/setup b/bin/setup
new file mode 100755
index 0000000000..78c4e861dc
--- /dev/null
+++ b/bin/setup
@@ -0,0 +1,38 @@
+#!/usr/bin/env ruby
+require 'pathname'
+require 'fileutils'
+include FileUtils
+
+# path to your application root.
+APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+chdir APP_ROOT do
+ # This script is a starting point to setup your application.
+ # Add necessary setup steps to this file.
+
+ puts '== Installing dependencies =='
+ system! 'gem install bundler --conservative'
+ system('bundle check') || system!('bundle install')
+
+ # Install JavaScript dependencies if using Yarn
+ # system('bin/yarn')
+
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?('config/database.yml')
+ # cp 'config/database.yml.sample', 'config/database.yml'
+ # end
+
+ puts "\n== Preparing database =="
+ system! 'bin/rails db:setup'
+
+ puts "\n== Removing old logs and tempfiles =="
+ system! 'bin/rails log:clear tmp:clear'
+
+ puts "\n== Restarting application server =="
+ system! 'bin/rails restart'
+end
diff --git a/bin/spring b/bin/spring
new file mode 100755
index 0000000000..fb2ec2ebb4
--- /dev/null
+++ b/bin/spring
@@ -0,0 +1,17 @@
+#!/usr/bin/env ruby
+
+# This file loads spring without using Bundler, in order to be fast.
+# It gets overwritten when you run the `spring binstub` command.
+
+unless defined?(Spring)
+ require 'rubygems'
+ require 'bundler'
+
+ lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
+ spring = lockfile.specs.detect { |spec| spec.name == "spring" }
+ if spring
+ Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
+ gem 'spring', spring.version
+ require 'spring/binstub'
+ end
+end
diff --git a/bin/update b/bin/update
new file mode 100755
index 0000000000..a8e4462f20
--- /dev/null
+++ b/bin/update
@@ -0,0 +1,29 @@
+#!/usr/bin/env ruby
+require 'pathname'
+require 'fileutils'
+include FileUtils
+
+# path to your application root.
+APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+chdir APP_ROOT do
+ # This script is a way to update your development environment automatically.
+ # Add necessary update steps to this file.
+
+ puts '== Installing dependencies =='
+ system! 'gem install bundler --conservative'
+ system('bundle check') || system!('bundle install')
+
+ puts "\n== Updating database =="
+ system! 'bin/rails db:migrate'
+
+ puts "\n== Removing old logs and tempfiles =="
+ system! 'bin/rails log:clear tmp:clear'
+
+ puts "\n== Restarting application server =="
+ system! 'bin/rails restart'
+end
diff --git a/bin/yarn b/bin/yarn
new file mode 100755
index 0000000000..c2bacef836
--- /dev/null
+++ b/bin/yarn
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+VENDOR_PATH = File.expand_path('..', __dir__)
+Dir.chdir(VENDOR_PATH) do
+ begin
+ exec "yarnpkg #{ARGV.join(" ")}"
+ rescue Errno::ENOENT
+ $stderr.puts "Yarn executable was not detected in the system."
+ $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
+ exit 1
+ end
+end
diff --git a/config.ru b/config.ru
new file mode 100644
index 0000000000..f7ba0b527b
--- /dev/null
+++ b/config.ru
@@ -0,0 +1,5 @@
+# This file is used by Rack-based servers to start the application.
+
+require_relative 'config/environment'
+
+run Rails.application
diff --git a/config/application.rb b/config/application.rb
new file mode 100644
index 0000000000..5f6ca0f9b2
--- /dev/null
+++ b/config/application.rb
@@ -0,0 +1,25 @@
+require_relative 'boot'
+
+require 'rails/all'
+
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(*Rails.groups)
+
+module Betsy
+ class Application < Rails::Application
+ config.generators do |g|
+ # Force new test files to be generated in the minitest-spec style
+ g.test_framework :minitest, spec: true
+
+ # Always use .js files, never .coffee
+ g.javascript_engine :js
+ end
+ # Initialize configuration defaults for originally generated Rails version.
+ config.load_defaults 5.1
+
+ # Settings in config/environments/* take precedence over those specified here.
+ # Application configuration should go into files in config/initializers
+ # -- all .rb files in that directory are automatically loaded.
+ end
+end
diff --git a/config/boot.rb b/config/boot.rb
new file mode 100644
index 0000000000..30f5120df6
--- /dev/null
+++ b/config/boot.rb
@@ -0,0 +1,3 @@
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+
+require 'bundler/setup' # Set up gems listed in the Gemfile.
diff --git a/config/cable.yml b/config/cable.yml
new file mode 100644
index 0000000000..3cba994bb2
--- /dev/null
+++ b/config/cable.yml
@@ -0,0 +1,10 @@
+development:
+ adapter: async
+
+test:
+ adapter: async
+
+production:
+ adapter: redis
+ url: redis://localhost:6379/1
+ channel_prefix: betsy_production
diff --git a/config/database.yml b/config/database.yml
new file mode 100644
index 0000000000..6903bb6083
--- /dev/null
+++ b/config/database.yml
@@ -0,0 +1,85 @@
+# PostgreSQL. Versions 9.1 and up are supported.
+#
+# Install the pg driver:
+# gem install pg
+# On OS X with Homebrew:
+# gem install pg -- --with-pg-config=/usr/local/bin/pg_config
+# On OS X with MacPorts:
+# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
+# On Windows:
+# gem install pg
+# Choose the win32 build.
+# Install PostgreSQL and put its /bin directory on your path.
+#
+# Configure Using Gemfile
+# gem 'pg'
+#
+default: &default
+ adapter: postgresql
+ encoding: unicode
+ # For details on connection pooling, see Rails configuration guide
+ # http://guides.rubyonrails.org/configuring.html#database-pooling
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+
+development:
+ <<: *default
+ database: betsy_development
+
+ # The specified database role being used to connect to postgres.
+ # To create additional roles in postgres see `$ createuser --help`.
+ # When left blank, postgres will use the default role. This is
+ # the same name as the operating system user that initialized the database.
+ #username: betsy
+
+ # The password associated with the postgres role (username).
+ #password:
+
+ # Connect on a TCP socket. Omitted by default since the client uses a
+ # domain socket that doesn't need configuration. Windows does not have
+ # domain sockets, so uncomment these lines.
+ #host: localhost
+
+ # The TCP port the server listens on. Defaults to 5432.
+ # If your server runs on a different port number, change accordingly.
+ #port: 5432
+
+ # Schema search path. The server defaults to $user,public
+ #schema_search_path: myapp,sharedapp,public
+
+ # Minimum log levels, in increasing order:
+ # debug5, debug4, debug3, debug2, debug1,
+ # log, notice, warning, error, fatal, and panic
+ # Defaults to warning.
+ #min_messages: notice
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: betsy_test
+
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%= ENV['DATABASE_URL'] %>
+#
+production:
+ <<: *default
+ database: betsy_production
+ username: betsy
+ password: <%= ENV['BETSY_DATABASE_PASSWORD'] %>
diff --git a/config/environment.rb b/config/environment.rb
new file mode 100644
index 0000000000..426333bb46
--- /dev/null
+++ b/config/environment.rb
@@ -0,0 +1,5 @@
+# Load the Rails application.
+require_relative 'application'
+
+# Initialize the Rails application.
+Rails.application.initialize!
diff --git a/config/environments/development.rb b/config/environments/development.rb
new file mode 100644
index 0000000000..5187e22186
--- /dev/null
+++ b/config/environments/development.rb
@@ -0,0 +1,54 @@
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # In the development environment your application's code is reloaded on
+ # every request. This slows down response time but is perfect for development
+ # since you don't have to restart the web server when you make code changes.
+ config.cache_classes = false
+
+ # Do not eager load code on boot.
+ config.eager_load = false
+
+ # Show full error reports.
+ config.consider_all_requests_local = true
+
+ # Enable/disable caching. By default caching is disabled.
+ if Rails.root.join('tmp/caching-dev.txt').exist?
+ config.action_controller.perform_caching = true
+
+ config.cache_store = :memory_store
+ config.public_file_server.headers = {
+ 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}"
+ }
+ else
+ config.action_controller.perform_caching = false
+
+ config.cache_store = :null_store
+ end
+
+ # Don't care if the mailer can't send.
+ config.action_mailer.raise_delivery_errors = false
+
+ config.action_mailer.perform_caching = false
+
+ # Print deprecation notices to the Rails logger.
+ config.active_support.deprecation = :log
+
+ # Raise an error on page load if there are pending migrations.
+ config.active_record.migration_error = :page_load
+
+ # Debug mode disables concatenation and preprocessing of assets.
+ # This option may cause significant delays in view rendering with a large
+ # number of complex assets.
+ config.assets.debug = true
+
+ # Suppress logger output for asset requests.
+ config.assets.quiet = true
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
+
+ # Use an evented file watcher to asynchronously detect changes in source code,
+ # routes, locales, etc. This feature depends on the listen gem.
+ config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+end
diff --git a/config/environments/production.rb b/config/environments/production.rb
new file mode 100644
index 0000000000..d6547d475a
--- /dev/null
+++ b/config/environments/production.rb
@@ -0,0 +1,91 @@
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # Code is not reloaded between requests.
+ config.cache_classes = true
+
+ # Eager load code on boot. This eager loads most of Rails and
+ # your application in memory, allowing both threaded web servers
+ # and those relying on copy on write to perform better.
+ # Rake tasks automatically ignore this option for performance.
+ config.eager_load = true
+
+ # Full error reports are disabled and caching is turned on.
+ config.consider_all_requests_local = false
+ config.action_controller.perform_caching = true
+
+ # Attempt to read encrypted secrets from `config/secrets.yml.enc`.
+ # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or
+ # `config/secrets.yml.key`.
+ config.read_encrypted_secrets = true
+
+ # Disable serving static files from the `/public` folder by default since
+ # Apache or NGINX already handles this.
+ config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
+
+ # Compress JavaScripts and CSS.
+ config.assets.js_compressor = :uglifier
+ # config.assets.css_compressor = :sass
+
+ # Do not fallback to assets pipeline if a precompiled asset is missed.
+ config.assets.compile = false
+
+ # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
+
+ # Enable serving of images, stylesheets, and JavaScripts from an asset server.
+ # config.action_controller.asset_host = 'http://assets.example.com'
+
+ # Specifies the header that your server uses for sending files.
+ # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
+
+ # Mount Action Cable outside main process or domain
+ # config.action_cable.mount_path = nil
+ # config.action_cable.url = 'wss://example.com/cable'
+ # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
+
+ # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
+ config.force_ssl = true
+
+ # Use the lowest log level to ensure availability of diagnostic information
+ # when problems arise.
+ config.log_level = :debug
+
+ # Prepend all log lines with the following tags.
+ config.log_tags = [ :request_id ]
+
+ # Use a different cache store in production.
+ # config.cache_store = :mem_cache_store
+
+ # Use a real queuing backend for Active Job (and separate queues per environment)
+ # config.active_job.queue_adapter = :resque
+ # config.active_job.queue_name_prefix = "betsy_#{Rails.env}"
+ config.action_mailer.perform_caching = false
+
+ # Ignore bad email addresses and do not raise email delivery errors.
+ # Set this to true and configure the email server for immediate delivery to raise delivery errors.
+ # config.action_mailer.raise_delivery_errors = false
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation cannot be found).
+ config.i18n.fallbacks = true
+
+ # Send deprecation notices to registered listeners.
+ config.active_support.deprecation = :notify
+
+ # Use default logging formatter so that PID and timestamp are not suppressed.
+ config.log_formatter = ::Logger::Formatter.new
+
+ # Use a different logger for distributed setups.
+ # require 'syslog/logger'
+ # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
+
+ if ENV["RAILS_LOG_TO_STDOUT"].present?
+ logger = ActiveSupport::Logger.new(STDOUT)
+ logger.formatter = config.log_formatter
+ config.logger = ActiveSupport::TaggedLogging.new(logger)
+ end
+
+ # Do not dump schema after migrations.
+ config.active_record.dump_schema_after_migration = false
+end
diff --git a/config/environments/test.rb b/config/environments/test.rb
new file mode 100644
index 0000000000..8e5cbde533
--- /dev/null
+++ b/config/environments/test.rb
@@ -0,0 +1,42 @@
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # The test environment is used exclusively to run your application's
+ # test suite. You never need to work with it otherwise. Remember that
+ # your test database is "scratch space" for the test suite and is wiped
+ # and recreated between test runs. Don't rely on the data there!
+ config.cache_classes = true
+
+ # Do not eager load code on boot. This avoids loading your whole application
+ # just for the purpose of running a single test. If you are using a tool that
+ # preloads Rails for running tests, you may have to set it to true.
+ config.eager_load = false
+
+ # Configure public file server for tests with Cache-Control for performance.
+ config.public_file_server.enabled = true
+ config.public_file_server.headers = {
+ 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}"
+ }
+
+ # Show full error reports and disable caching.
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+
+ # Raise exceptions instead of rendering exception templates.
+ config.action_dispatch.show_exceptions = false
+
+ # Disable request forgery protection in test environment.
+ config.action_controller.allow_forgery_protection = false
+ config.action_mailer.perform_caching = false
+
+ # Tell Action Mailer not to deliver emails to the real world.
+ # The :test delivery method accumulates sent emails in the
+ # ActionMailer::Base.deliveries array.
+ config.action_mailer.delivery_method = :test
+
+ # Print deprecation notices to the stderr.
+ config.active_support.deprecation = :stderr
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
+end
diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb
new file mode 100644
index 0000000000..89d2efab2b
--- /dev/null
+++ b/config/initializers/application_controller_renderer.rb
@@ -0,0 +1,8 @@
+# Be sure to restart your server when you modify this file.
+
+# ActiveSupport::Reloader.to_prepare do
+# ApplicationController.renderer.defaults.merge!(
+# http_host: 'example.org',
+# https: false
+# )
+# end
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
new file mode 100644
index 0000000000..332875eb69
--- /dev/null
+++ b/config/initializers/assets.rb
@@ -0,0 +1,14 @@
+# Be sure to restart your server when you modify this file.
+
+# Version of your assets, change this if you want to expire all your assets.
+Rails.application.config.assets.version = '1.0'
+
+# Add additional assets to the asset load path.
+# Rails.application.config.assets.paths << Emoji.images_path
+# Add Yarn node_modules folder to the asset load path.
+Rails.application.config.assets.paths << Rails.root.join('node_modules')
+
+# Precompile additional assets.
+# application.js, application.css, and all non-JS/CSS in the app/assets
+# folder are already added.
+Rails.application.config.assets.precompile += %w( admin.js admin.css )
diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb
new file mode 100644
index 0000000000..59385cdf37
--- /dev/null
+++ b/config/initializers/backtrace_silencers.rb
@@ -0,0 +1,7 @@
+# Be sure to restart your server when you modify this file.
+
+# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
+# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+
+# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
+# Rails.backtrace_cleaner.remove_silencers!
diff --git a/config/initializers/client_side_validations.rb b/config/initializers/client_side_validations.rb
new file mode 100644
index 0000000000..aed6c829c9
--- /dev/null
+++ b/config/initializers/client_side_validations.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+# ClientSideValidations Initializer
+
+# Disabled validators
+# ClientSideValidations::Config.disabled_validators = []
+
+# Uncomment to validate number format with current I18n locale
+# ClientSideValidations::Config.number_format_with_locale = true
+
+# Uncomment the following block if you want each input field to have the validation messages attached.
+#
+# Note: client_side_validation requires the error to be encapsulated within
+#
+#
+ActionView::Base.field_error_proc = proc do |html_tag, instance|
+ if html_tag =~ /^#{html_tag}).html_safe
+ else
+ %(#{html_tag}#{instance.error_message.first}
).html_safe
+ end
+end
diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb
new file mode 100644
index 0000000000..5a6a32d371
--- /dev/null
+++ b/config/initializers/cookies_serializer.rb
@@ -0,0 +1,5 @@
+# Be sure to restart your server when you modify this file.
+
+# Specify a serializer for the signed and encrypted cookie jars.
+# Valid options are :json, :marshal, and :hybrid.
+Rails.application.config.action_dispatch.cookies_serializer = :json
diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb
new file mode 100644
index 0000000000..4a994e1e7b
--- /dev/null
+++ b/config/initializers/filter_parameter_logging.rb
@@ -0,0 +1,4 @@
+# Be sure to restart your server when you modify this file.
+
+# Configure sensitive parameters which will be filtered from the log file.
+Rails.application.config.filter_parameters += [:password]
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
new file mode 100644
index 0000000000..ac033bf9dc
--- /dev/null
+++ b/config/initializers/inflections.rb
@@ -0,0 +1,16 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format. Inflections
+# are locale specific, and you may define rules for as many different
+# locales as you wish. All of these examples are active by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.plural /^(ox)$/i, '\1en'
+# inflect.singular /^(ox)en/i, '\1'
+# inflect.irregular 'person', 'people'
+# inflect.uncountable %w( fish sheep )
+# end
+
+# These inflection rules are supported but not enabled by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.acronym 'RESTful'
+# end
diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb
new file mode 100644
index 0000000000..dc1899682b
--- /dev/null
+++ b/config/initializers/mime_types.rb
@@ -0,0 +1,4 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new mime types for use in respond_to blocks:
+# Mime::Type.register "text/richtext", :rtf
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
new file mode 100644
index 0000000000..300bf0312c
--- /dev/null
+++ b/config/initializers/omniauth.rb
@@ -0,0 +1,4 @@
+
+Rails.application.config.middleware.use OmniAuth::Builder do
+ provider :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"], scope: "user:email"
+end
diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb
new file mode 100644
index 0000000000..bbfc3961bf
--- /dev/null
+++ b/config/initializers/wrap_parameters.rb
@@ -0,0 +1,14 @@
+# Be sure to restart your server when you modify this file.
+
+# This file contains settings for ActionController::ParamsWrapper which
+# is enabled by default.
+
+# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
+ActiveSupport.on_load(:action_controller) do
+ wrap_parameters format: [:json]
+end
+
+# To enable root element in JSON for ActiveRecord objects.
+# ActiveSupport.on_load(:active_record) do
+# self.include_root_in_json = true
+# end
diff --git a/config/locales/en.yml b/config/locales/en.yml
new file mode 100644
index 0000000000..decc5a8573
--- /dev/null
+++ b/config/locales/en.yml
@@ -0,0 +1,33 @@
+# Files in the config/locales directory are used for internationalization
+# and are automatically loaded by Rails. If you want to use locales other
+# than English, add the necessary files in this directory.
+#
+# To use the locales, use `I18n.t`:
+#
+# I18n.t 'hello'
+#
+# In views, this is aliased to just `t`:
+#
+# <%= t('hello') %>
+#
+# To use a different locale, set it with `I18n.locale`:
+#
+# I18n.locale = :es
+#
+# This would use the information in config/locales/es.yml.
+#
+# The following keys must be escaped otherwise they will not be retrieved by
+# the default I18n backend:
+#
+# true, false, on, off, yes, no
+#
+# Instead, surround them with single quotes.
+#
+# en:
+# 'true': 'foo'
+#
+# To learn more, please read the Rails Internationalization guide
+# available at http://guides.rubyonrails.org/i18n.html.
+
+en:
+ hello: "Hello world"
diff --git a/config/puma.rb b/config/puma.rb
new file mode 100644
index 0000000000..1e19380dcb
--- /dev/null
+++ b/config/puma.rb
@@ -0,0 +1,56 @@
+# Puma can serve each request in a thread from an internal thread pool.
+# The `threads` method setting takes two numbers: a minimum and maximum.
+# Any libraries that use thread pools should be configured to match
+# the maximum value specified for Puma. Default is set to 5 threads for minimum
+# and maximum; this matches the default thread size of Active Record.
+#
+threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
+threads threads_count, threads_count
+
+# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
+#
+port ENV.fetch("PORT") { 3000 }
+
+# Specifies the `environment` that Puma will run in.
+#
+environment ENV.fetch("RAILS_ENV") { "development" }
+
+# Specifies the number of `workers` to boot in clustered mode.
+# Workers are forked webserver processes. If using threads and workers together
+# the concurrency of the application would be max `threads` * `workers`.
+# Workers do not work on JRuby or Windows (both of which do not support
+# processes).
+#
+# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
+
+# Use the `preload_app!` method when specifying a `workers` number.
+# This directive tells Puma to first boot the application and load code
+# before forking the application. This takes advantage of Copy On Write
+# process behavior so workers use less memory. If you use this option
+# you need to make sure to reconnect any threads in the `on_worker_boot`
+# block.
+#
+# preload_app!
+
+# If you are preloading your application and using Active Record, it's
+# recommended that you close any connections to the database before workers
+# are forked to prevent connection leakage.
+#
+# before_fork do
+# ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
+# end
+
+# The code in the `on_worker_boot` will be called if you are using
+# clustered mode by specifying a number of `workers`. After each worker
+# process is booted, this block will be run. If you are using the `preload_app!`
+# option, you will want to use this block to reconnect to any threads
+# or connections that may have been created at application boot, as Ruby
+# cannot share connections between processes.
+#
+# on_worker_boot do
+# ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
+# end
+#
+
+# Allow puma to be restarted by `rails restart` command.
+plugin :tmp_restart
diff --git a/config/routes.rb b/config/routes.rb
new file mode 100644
index 0000000000..29c3d9f4cd
--- /dev/null
+++ b/config/routes.rb
@@ -0,0 +1,48 @@
+Rails.application.routes.draw do
+
+ root 'homepage#index'
+
+ get '/homepage', to:'homepage#index', as:'homepage'
+
+ get '/auth/:provider/callback', as: 'auth_callback', to: 'sessions#create'
+
+ get '/auth/github', as: 'github_login'
+
+ delete "/logout", to: "sessions#destroy", as: "logout"
+
+ patch '/cart/place_order', to: 'cart#update_to_paid', as: 'update_to_paid'
+
+ get '/cart', to: 'cart#access_cart', as: "cart"
+
+ patch '/cart', to: 'cart#update_cart_info', as: "update_cart_info"
+
+ delete '/cart/:id/remove_single_item', to:'cart#remove_single_item', as: "remove_single_item"
+
+ delete '/cart/delete', to:'cart#destroy', as: "cart_destroy"
+
+ post '/products/:id/add_to_cart', to: 'cart#add_to_cart', as: 'add_to_cart'
+
+ get 'orders/:id/confirmation', to: 'orders#confirmation', as: 'order_confirmation'
+
+ put 'orders/:id/cancel', to: 'orders#cancel', as: 'order_cancel'
+
+ resources :orders
+ resources :sessions, except: [:destroy]
+
+ put 'order_items/:id/status', to: 'order_items#set_status', as: 'order_item_set_status'
+ resources :order_items, only: [:update]
+
+ resources :categories, except: [:edit, :update, :show, :destroy] do
+ resources :products, only: [:index]
+ end
+
+ resources :users, only: [:index, :show, :create] do
+ resources :products, only: [:index, :new, :create]
+ end
+
+ put 'products/:id/status', to: 'products#set_status', as: 'product_set_status'
+ resources :products, only: [:index, :show, :edit, :update,] do
+ resources :reviews, only: [:create]
+ end
+
+end
diff --git a/config/secrets.yml b/config/secrets.yml
new file mode 100644
index 0000000000..cf40b5fb3b
--- /dev/null
+++ b/config/secrets.yml
@@ -0,0 +1,32 @@
+# Be sure to restart your server when you modify this file.
+
+# Your secret key is used for verifying the integrity of signed cookies.
+# If you change this key, all old signed cookies will become invalid!
+
+# Make sure the secret is at least 30 characters and all random,
+# no regular words or you'll be exposed to dictionary attacks.
+# You can use `rails secret` to generate a secure secret key.
+
+# Make sure the secrets in this file are kept private
+# if you're sharing your code publicly.
+
+# Shared secrets are available across all environments.
+
+# shared:
+# api_key: a1B2c3D4e5F6
+
+# Environmental secrets are only available for that specific environment.
+
+development:
+ secret_key_base: 57b7f9e34c30b320a0b352ca41bb8de56c998d994a9eda99dc29969e911e07b33c22e488bdc5761152d06cd828d8e34e1453e81f1a2e97bf5c5ec1d3148b6a46
+
+test:
+ secret_key_base: 813924d8c777b2c31a4b44a7b62984747eb9d019cd76478f2a479de959393a83b9c6104c037dd59fb61bfd53f863bb5e55d7a0e672ef2a4c6fad1b6e931a38dc
+
+# Do not keep production secrets in the unencrypted secrets file.
+# Instead, either read values from the environment.
+# Or, use `bin/rails secrets:setup` to configure encrypted secrets
+# and move the `production:` environment over there.
+
+production:
+ secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
diff --git a/config/spring.rb b/config/spring.rb
new file mode 100644
index 0000000000..c9119b40c0
--- /dev/null
+++ b/config/spring.rb
@@ -0,0 +1,6 @@
+%w(
+ .ruby-version
+ .rbenv-vars
+ tmp/restart.txt
+ tmp/caching-dev.txt
+).each { |path| Spring.watch(path) }
diff --git a/db/migrate/20180419225136_create_users.rb b/db/migrate/20180419225136_create_users.rb
new file mode 100644
index 0000000000..c9c75408fe
--- /dev/null
+++ b/db/migrate/20180419225136_create_users.rb
@@ -0,0 +1,12 @@
+class CreateUsers < ActiveRecord::Migration[5.1]
+ def change
+ create_table :users do |t|
+ t.string :username
+ t.string :email
+ t.string :uid
+ t.string :provider
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20180419225258_create_categories.rb b/db/migrate/20180419225258_create_categories.rb
new file mode 100644
index 0000000000..5bef4913b8
--- /dev/null
+++ b/db/migrate/20180419225258_create_categories.rb
@@ -0,0 +1,9 @@
+class CreateCategories < ActiveRecord::Migration[5.1]
+ def change
+ create_table :categories do |t|
+ t.string :name
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20180419225515_create_reviews.rb b/db/migrate/20180419225515_create_reviews.rb
new file mode 100644
index 0000000000..f2bb381bc1
--- /dev/null
+++ b/db/migrate/20180419225515_create_reviews.rb
@@ -0,0 +1,10 @@
+class CreateReviews < ActiveRecord::Migration[5.1]
+ def change
+ create_table :reviews do |t|
+ t.integer :rating
+ t.text :content
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20180419225625_create_products.rb b/db/migrate/20180419225625_create_products.rb
new file mode 100644
index 0000000000..75aff20a50
--- /dev/null
+++ b/db/migrate/20180419225625_create_products.rb
@@ -0,0 +1,14 @@
+class CreateProducts < ActiveRecord::Migration[5.1]
+ def change
+ create_table :products do |t|
+ t.string :name
+ t.boolean :is_active
+ t.text :description
+ t.integer :price
+ t.string :photo_url
+ t.integer :stock
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20180419230456_create_orders.rb b/db/migrate/20180419230456_create_orders.rb
new file mode 100644
index 0000000000..6c96976567
--- /dev/null
+++ b/db/migrate/20180419230456_create_orders.rb
@@ -0,0 +1,20 @@
+class CreateOrders < ActiveRecord::Migration[5.1]
+ def change
+ create_table :orders do |t|
+ t.string :status
+ t.string :name
+ t.string :email
+ t.string :street_address
+ t.string :city
+ t.string :state
+ t.string :zip
+ t.string :name_cc
+ t.string :credit_card
+ t.date :expiry
+ t.string :ccv
+ t.string :billing_zip
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20180419230542_create_order_items.rb b/db/migrate/20180419230542_create_order_items.rb
new file mode 100644
index 0000000000..ffa95ac6d5
--- /dev/null
+++ b/db/migrate/20180419230542_create_order_items.rb
@@ -0,0 +1,10 @@
+class CreateOrderItems < ActiveRecord::Migration[5.1]
+ def change
+ create_table :order_items do |t|
+ t.integer :quantity
+ t.boolean :is_shipped
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20180420015008_create_products_categories_join.rb b/db/migrate/20180420015008_create_products_categories_join.rb
new file mode 100644
index 0000000000..a226c63542
--- /dev/null
+++ b/db/migrate/20180420015008_create_products_categories_join.rb
@@ -0,0 +1,8 @@
+class CreateProductsCategoriesJoin < ActiveRecord::Migration[5.1]
+ def change
+ create_table :products_categories_joins do |t|
+ t.belongs_to :product, index: true
+ t.belongs_to :category, index: true
+ end
+ end
+end
diff --git a/db/migrate/20180420171359_add_product_id_and_order_id_to_order_items.rb b/db/migrate/20180420171359_add_product_id_and_order_id_to_order_items.rb
new file mode 100644
index 0000000000..9fa7ccf00a
--- /dev/null
+++ b/db/migrate/20180420171359_add_product_id_and_order_id_to_order_items.rb
@@ -0,0 +1,6 @@
+class AddProductIdAndOrderIdToOrderItems < ActiveRecord::Migration[5.1]
+ def change
+ add_reference :order_items, :order, foreign_key: true
+ add_reference :order_items, :product, foreign_key: true
+ end
+end
diff --git a/db/migrate/20180420171726_add_user_id_to_products.rb b/db/migrate/20180420171726_add_user_id_to_products.rb
new file mode 100644
index 0000000000..e804eae8f7
--- /dev/null
+++ b/db/migrate/20180420171726_add_user_id_to_products.rb
@@ -0,0 +1,5 @@
+class AddUserIdToProducts < ActiveRecord::Migration[5.1]
+ def change
+ add_reference :products, :user, foreign_key: true
+ end
+end
diff --git a/db/migrate/20180420171823_add_product_id_to_reviews.rb b/db/migrate/20180420171823_add_product_id_to_reviews.rb
new file mode 100644
index 0000000000..aa157d4e07
--- /dev/null
+++ b/db/migrate/20180420171823_add_product_id_to_reviews.rb
@@ -0,0 +1,5 @@
+class AddProductIdToReviews < ActiveRecord::Migration[5.1]
+ def change
+ add_reference :reviews, :product, foreign_key: true
+ end
+end
diff --git a/db/migrate/20180420214249_drop_table_products_categories_joins.rb b/db/migrate/20180420214249_drop_table_products_categories_joins.rb
new file mode 100644
index 0000000000..1cbfdf1b75
--- /dev/null
+++ b/db/migrate/20180420214249_drop_table_products_categories_joins.rb
@@ -0,0 +1,13 @@
+class DropTableProductsCategoriesJoins < ActiveRecord::Migration[5.1]
+ def up
+ drop_table :products_categories_joins
+ end
+ def down
+ create_table "products_categories_joins", force: :cascade do |t|
+ t.bigint "product_id"
+ t.bigint "category_id"
+ t.index ["category_id"], name: "index_products_categories_joins_on_category_id"
+ t.index ["product_id"], name: "index_products_categories_joins_on_product_id"
+ end
+ end
+end
diff --git a/db/migrate/20180420215100_create_products_categories.rb b/db/migrate/20180420215100_create_products_categories.rb
new file mode 100644
index 0000000000..81b871228e
--- /dev/null
+++ b/db/migrate/20180420215100_create_products_categories.rb
@@ -0,0 +1,10 @@
+class CreateProductsCategories < ActiveRecord::Migration[5.1]
+ def change
+ create_table :categories_products do |t|
+ t.bigint "product_id"
+ t.bigint "category_id"
+ t.index ["category_id"], name: "index_products_categories_on_category_id"
+ t.index ["product_id"], name: "index_products_categories_on_product_id"
+ end
+ end
+end
diff --git a/db/migrate/20180423045333_change_products_is_active_default.rb b/db/migrate/20180423045333_change_products_is_active_default.rb
new file mode 100644
index 0000000000..7962bc3afe
--- /dev/null
+++ b/db/migrate/20180423045333_change_products_is_active_default.rb
@@ -0,0 +1,5 @@
+class ChangeProductsIsActiveDefault < ActiveRecord::Migration[5.1]
+ def change
+ change_column_default :products, :is_active, from: nil, to: true
+ end
+end
diff --git a/db/migrate/20180425043854_add_image_to_users.rb b/db/migrate/20180425043854_add_image_to_users.rb
new file mode 100644
index 0000000000..be6f98591f
--- /dev/null
+++ b/db/migrate/20180425043854_add_image_to_users.rb
@@ -0,0 +1,5 @@
+class AddImageToUsers < ActiveRecord::Migration[5.1]
+ def change
+ add_column :users, :image, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
new file mode 100644
index 0000000000..1e058f58b1
--- /dev/null
+++ b/db/schema.rb
@@ -0,0 +1,95 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# Note that this schema.rb definition is the authoritative source for your
+# database schema. If you need to create the application database on another
+# system, you should be using db:schema:load, not running all the migrations
+# from scratch. The latter is a flawed and unsustainable approach (the more migrations
+# you'll amass, the slower it'll run and the greater likelihood for issues).
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema.define(version: 20180425043854) do
+
+ # These are extensions that must be enabled in order to support this database
+ enable_extension "plpgsql"
+
+ create_table "categories", force: :cascade do |t|
+ t.string "name"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ create_table "categories_products", force: :cascade do |t|
+ t.bigint "product_id"
+ t.bigint "category_id"
+ t.index ["category_id"], name: "index_products_categories_on_category_id"
+ t.index ["product_id"], name: "index_products_categories_on_product_id"
+ end
+
+ create_table "order_items", force: :cascade do |t|
+ t.integer "quantity"
+ t.boolean "is_shipped"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.bigint "order_id"
+ t.bigint "product_id"
+ t.index ["order_id"], name: "index_order_items_on_order_id"
+ t.index ["product_id"], name: "index_order_items_on_product_id"
+ end
+
+ create_table "orders", force: :cascade do |t|
+ t.string "status"
+ t.string "name"
+ t.string "email"
+ t.string "street_address"
+ t.string "city"
+ t.string "state"
+ t.string "zip"
+ t.string "name_cc"
+ t.string "credit_card"
+ t.date "expiry"
+ t.string "ccv"
+ t.string "billing_zip"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ create_table "products", force: :cascade do |t|
+ t.string "name"
+ t.boolean "is_active", default: true
+ t.text "description"
+ t.integer "price"
+ t.string "photo_url"
+ t.integer "stock"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.bigint "user_id"
+ t.index ["user_id"], name: "index_products_on_user_id"
+ end
+
+ create_table "reviews", force: :cascade do |t|
+ t.integer "rating"
+ t.text "content"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.bigint "product_id"
+ t.index ["product_id"], name: "index_reviews_on_product_id"
+ end
+
+ create_table "users", force: :cascade do |t|
+ t.string "username"
+ t.string "email"
+ t.string "uid"
+ t.string "provider"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.string "image"
+ end
+
+ add_foreign_key "order_items", "orders"
+ add_foreign_key "order_items", "products"
+ add_foreign_key "products", "users"
+ add_foreign_key "reviews", "products"
+end
diff --git a/db/seed-data/categories.csv b/db/seed-data/categories.csv
new file mode 100644
index 0000000000..616118b199
--- /dev/null
+++ b/db/seed-data/categories.csv
@@ -0,0 +1,10 @@
+id,name
+1,food
+2,treats
+3,toys
+4,cleaning
+5,grooming
+6,training
+7,furniture
+8,wellness
+9,outfit
diff --git a/db/seed-data/categories_products.csv b/db/seed-data/categories_products.csv
new file mode 100644
index 0000000000..b0125fd58e
--- /dev/null
+++ b/db/seed-data/categories_products.csv
@@ -0,0 +1,23 @@
+product_id,category_id
+1,1
+1,2
+2,1
+2,2
+3,1
+3,2
+4,2
+4,4
+4,5
+5,4
+6,7
+7,2
+8,9
+9,7
+10,7
+11,9
+12,9
+13,4
+14,7
+15,1
+16,4
+17,9
diff --git a/db/seed-data/orderitems.csv b/db/seed-data/orderitems.csv
new file mode 100644
index 0000000000..ae6eacb6cd
--- /dev/null
+++ b/db/seed-data/orderitems.csv
@@ -0,0 +1,11 @@
+order_id,product_id,quantity,is_shipped
+1,4,2,false
+2,5,1,true
+3,6,1,false
+4,11,1,true
+5,12,1,false
+6,13,1,true
+6,14,1,true
+6,15,1,true
+4,17,1,true
+2,16,1,true
diff --git a/db/seed-data/orders.csv b/db/seed-data/orders.csv
new file mode 100644
index 0000000000..69ba604031
--- /dev/null
+++ b/db/seed-data/orders.csv
@@ -0,0 +1,7 @@
+id,status,name,email,street_address,city,state,zip,name_cc,credit_card,expiry,ccv,billing_zip
+1,pending,John Doe,johndoe@example.com,1215 4th Ave #1050,Seattle,WA,98161,John Doe,"1234567890123456",2018-12-31,1234,98161
+2,complete,Jane Doe,janedoe@example.com,1215 4th Ave #1050,Seattle,WA,98161,Jane Doe,"6543210987654321",2018-12-31,1234,98161
+3,paid,Dilbert who,dilbert@example.com,1215 4th Ave #1050,Fremont,CA,98345,Dilbert who,"1234567891011123",2018-12-31,2345,99078
+4,complete,Indira Kumar,indira@example.com,1233 6th Ave #234,Chicago,IL,98246,Indira Kumar,"1234457891011124",2018-12-31,123,99080
+5,paid,Bruce Lee,bruce@example.com,1280 2nd Ave #234,Chicago,IL,98246,Bruce Lee,"2134457891011124",2018-12-31,213,99080
+6,complete,Marie curie,marie@example.com,1284 3rd Ave #234,Philadelphia,PA,98046,Marie Curie,"9876543219876543",2018-12-31,213,91234
diff --git a/db/seed-data/products.csv b/db/seed-data/products.csv
new file mode 100644
index 0000000000..1749288590
--- /dev/null
+++ b/db/seed-data/products.csv
@@ -0,0 +1,18 @@
+id,name,price,stock,description,photo_url,is_active,user_id
+1,canned food,200,10,BLUE Homestyle Recipe® Adult Dog Food,1.png,true,1
+2,dog food,70,0,Cesar Sunrise® Canine Cuisine Adult Dog Food,2.png,true,2
+3,chewy treat,1299,5,Authority® Chewy Wrap Dog Treat,3.jpg,true,3
+4,dental treat,2799,6,GREENIES® Teenie Dental Dog Treat,4.jpg,true,4
+5,Teether Puppy,499,0,Puppies R Us™ Teether Puppy Flattie Dog Toy - Crinkle (CHARACTER VARIES),5.jpg,true,1
+6,Dog Bed,2299,9,Canine Cushion Orthopedic Fleece Dog Bed,6.jpg,true,2
+7,Dog chewables,2999,1,21st Century™ Essential Pet™ Adult Hip & Joint Ages 5+ Dog Chewables,7.png,true,3
+8,Dog collar,5499,5,Seresto® Flea & Tick Dog Collar,8.png,true,4
+9,Dog Crate,11499,3,Grreat Choice® Dog Crate,9.png,true,1
+10,Dog pads,617,1,Top Paw® X-Large Dog Pads,10.jpg,true,2
+11,Jersey,1287,3,Chicago Cubs MLB Jersey,11.jpg,true,3
+12,Bandana,399,1,Top Paw® Americana Pet Bandana,12.jpg,true,4
+13,Carpet shampoo,1127,5,Nature's Miracle® Pet Carpet Shampoo,13.jpg,true,1
+14,DogBed,5249,4,KONG® Lounger Dog Bed,14.jpg,true,2
+15,Dog Bowl,1500,0,Top Paw® Bone Dog Bowl,15.jpg,true,3
+16,Shampoo,621,3,FURminator® deShedding Ultra Premium Dog Shampoo,16.jpg,true,4
+17,vest,2500,2,Hip Doggie Swiss Alpine Ski Vest,17.jpg,true,4
diff --git a/db/seed-data/reviews.csv b/db/seed-data/reviews.csv
new file mode 100644
index 0000000000..4b6e9f4b7f
--- /dev/null
+++ b/db/seed-data/reviews.csv
@@ -0,0 +1,17 @@
+id,rating,product_id,content
+1,5,1,excellent product
+2,1,1,deceptive product pricing
+3,5,2,My Great Dane loves this food. It took me a little while to find the perfect food for her and this is it! Shes on track with her weight and how fast shes growing. I recommend this for large breed dog owners
+4,4,3,My dog loves these.
+5,4,4,Value for money.
+6,5,6,My question after owning 104 dog beds: can I wash the cover over and over and over and will the interior retain it's density? Yes! Dog loves it because it's cushy and comfy. I love it because my house doesn't smell like dog.
+7,3,7,
+8,4,8,
+9,2,10,Hate the new color - they should go back to the pale beige they used for years.
+10,4,11,Little tight around front leg holes
+11,2,12,
+12,3,13,
+13,3,15,
+14,4,15,
+15,5,16,
+16,5,17,Very Satisfied
diff --git a/db/seed-data/users.csv b/db/seed-data/users.csv
new file mode 100644
index 0000000000..381c221578
--- /dev/null
+++ b/db/seed-data/users.csv
@@ -0,0 +1,5 @@
+id,username,email,uid,provider,image
+1,monalisa,monalisa@example.com,17690741,github,
+2,choricao,choricao@example.com,14342711,github,
+3,selam,selam@example.com,31666742,github,
+4,lasiorhine,lasiorhine@example.com,26230098,github,
diff --git a/db/seeds.rb b/db/seeds.rb
new file mode 100644
index 0000000000..1999e66ebe
--- /dev/null
+++ b/db/seeds.rb
@@ -0,0 +1,195 @@
+# This file should contain all the record creation needed to seed the database with its default values.
+# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
+#
+# Examples:
+#
+# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
+# Character.create(name: 'Luke', movie: movies.first)
+require 'csv'
+
+CATEGORY_FILE = Rails.root.join('db','seed-data', 'categories.csv')
+puts "Loading raw category data from #{CATEGORY_FILE}"
+
+category_failures = []
+CSV.foreach(CATEGORY_FILE, :headers => true) do |row|
+ category = Category.new
+ category.id = row['id']
+ category.name = row['name']
+ puts "Created category: #{category.inspect}"
+ successful = category.save
+ if !successful
+ category_failures << category
+ end
+end
+
+puts "Added #{Category.count} category records"
+puts "#{category_failures.length} categorys failed to save"
+puts
+puts
+
+ORDER_FILE = Rails.root.join('db','seed-data', 'orders.csv')
+puts "Loading raw order data from #{ORDER_FILE}"
+
+
+order_failures = []
+CSV.foreach(ORDER_FILE, :headers => true) do |row|
+ order = Order.new
+ order.id = row['id']
+ order.status = row['status']
+ order.name = row['name']
+ order.email = row['email']
+ order.street_address = row['street_address']
+ order.city = row['city']
+ order.state = row['state']
+ order.zip = row['zip']
+ order.name_cc = row['name_cc']
+ order.credit_card = row['credit_card']
+ order.expiry = row['expiry']
+ order.ccv = row['ccv']
+ order.billing_zip = row['billing_zip']
+ puts "Created order: #{order.inspect}"
+ successful = order.save
+ if !successful
+ order_failures << order
+ print order.errors.messages
+ end
+end
+
+puts "Added #{Order.count} order records"
+puts "#{order_failures.length} orders failed to save"
+p order_failures
+puts
+
+USER_FILE = Rails.root.join('db','seed-data', 'users.csv')
+puts "Loading raw user data from #{USER_FILE}"
+
+user_failures = []
+CSV.foreach(USER_FILE, :headers => true) do |row|
+ user = User.new
+ user.id = row['id']
+ user.username = row['username']
+ user.email = row['email']
+ user.uid = row['uid']
+ user.provider = row['provider']
+ user.image = row['image']
+ puts "Created user: #{user.inspect}"
+ successful = user.save
+ if !successful
+ user_failures << user
+ end
+end
+
+puts "Added #{User.count} user records"
+puts "#{user_failures.length} users failed to save"
+puts
+puts
+
+
+PRODUCT_FILE = Rails.root.join('db','seed-data', 'products.csv')
+puts "Loading raw product data from #{PRODUCT_FILE}"
+
+product_failures = []
+CSV.foreach(PRODUCT_FILE, :headers => true) do |row|
+ product = Product.new
+ product.id = row['id']
+ product.name = row['name']
+ product.price = row['price']
+ product.stock = row['stock']
+ product.description = row['description']
+ product.photo_url = row['photo_url']
+ product.is_active = row['is_active']
+ product.user_id = row['user_id']
+ puts "Created product: #{product.inspect}"
+ successful = product.save
+ if !successful
+ product_failures << product
+ end
+end
+
+puts "Added #{Product.count} product records"
+puts "#{product_failures.length} products failed to save"
+puts
+puts
+
+REVIEW_FILE = Rails.root.join('db','seed-data', 'reviews.csv')
+puts "Loading raw review data from #{REVIEW_FILE}"
+
+review_failures = []
+CSV.foreach(REVIEW_FILE, :headers => true) do |row|
+ review = Review.new
+ review.id = row['id']
+ review.rating = row['rating']
+ review.product_id = row['product_id']
+ review.content = row['content']
+ puts "Created review: #{review.inspect}"
+ successful = review.save
+ if !successful
+ puts review.errors
+ review_failures << review
+ end
+end
+
+puts "Added #{Review.count} review records"
+puts "#{review_failures.length} reviews failed to save"
+p review_failures
+puts
+
+
+OP_FILE = Rails.root.join('db','seed-data', 'orderitems.csv')
+puts "Loading raw order_item data from #{OP_FILE}"
+
+order_item_failures = []
+CSV.foreach(OP_FILE, :headers => true) do |row|
+ order_item = OrderItem.new
+ order_item.order_id = row['order_id']
+ order_item.product_id = row['product_id']
+ order_item.quantity= row['quantity']
+ order_item.is_shipped = row['is_shipped']
+ puts "Created order_item: #{order_item.inspect}"
+ successful = order_item.save
+ if !successful
+ order_item_failures << order_item
+ end
+end
+
+puts "Added #{OrderItem.count} order-item records"
+puts "#{order_item_failures.length} order_item failed to save"
+p order_item_failures
+puts
+
+OP_FILE = Rails.root.join('db','seed-data', 'categories_products.csv')
+puts "Loading raw order_item data from #{OP_FILE}"
+
+category_product_failures = []
+CSV.foreach(OP_FILE, :headers => true) do |row|
+ prd = Product.find(row['product_id'])
+ cat = Category.find(row['category_id'])
+ prd.categories << cat
+end
+
+# c=Category.find(1); p = Product.find(1); p.categories << c
+# c=Category.find(2); p = Product.find(1); p.categories << c
+# c=Category.find(1); p = Product.find(2); p.categories << c
+# c=Category.find(2); p = Product.find(2); p.categories << c
+# c=Category.find(1); p = Product.find(3); p.categories << c
+# c=Category.find(2); p = Product.find(3); p.categories << c
+# c=Category.find(2); p = Product.find(4); p.categories << c
+# c=Category.find(4); p = Product.find(4); p.categories << c
+# c=Category.find(4); p = Product.find(5); p.categories << c
+# c=Category.find(7); p = Product.find(6); p.categories << c
+# c=Category.find(2); p = Product.find(7); p.categories << c
+# c=Category.find(9); p = Product.find(8); p.categories << c
+# c=Category.find(7); p = Product.find(9); p.categories << c
+# c=Category.find(7); p = Product.find(10); p.categories << c
+# c=Category.find(9); p = Product.find(11); p.categories << c
+# c=Category.find(9); p = Product.find(12); p.categories << c
+# c=Category.find(4); p = Product.find(13); p.categories << c
+# c=Category.find(7); p = Product.find(14); p.categories << c
+# c=Category.find(1); p = Product.find(15); p.categories << c
+# c=Category.find(4); p = Product.find(16); p.categories << c
+# c=Category.find(9); p = Product.find(17); p.categories << c
+
+puts "Manually resetting PK sequence on each table"
+ActiveRecord::Base.connection.tables.each do |t|
+ ActiveRecord::Base.connection.reset_pk_sequence!(t)
+end
diff --git a/lib/assets/.keep b/lib/assets/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/lib/tasks/.keep b/lib/tasks/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/log/.keep b/log/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000..f874acf437
--- /dev/null
+++ b/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "betsy",
+ "private": true,
+ "dependencies": {}
+}
diff --git a/public/404.html b/public/404.html
new file mode 100644
index 0000000000..a0df16f88f
--- /dev/null
+++ b/public/404.html
@@ -0,0 +1,72 @@
+
+
+
+ The page you were looking for doesn't exist (404)
+
+
+
+
+
+
+
+
+
The page you were looking for doesn't exist.
+
You may have mistyped the address or the page may have moved.
+
+
If you are the application owner check the logs for more information.
+
+
+
+
diff --git a/public/404.jpg b/public/404.jpg
new file mode 100644
index 0000000000..c9e384daeb
Binary files /dev/null and b/public/404.jpg differ
diff --git a/public/422.html b/public/422.html
new file mode 100644
index 0000000000..c08eac0d1d
--- /dev/null
+++ b/public/422.html
@@ -0,0 +1,67 @@
+
+
+
+ The change you wanted was rejected (422)
+
+
+
+
+
+
+
+
+
The change you wanted was rejected.
+
Maybe you tried to change something you didn't have access to.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/public/500.html b/public/500.html
new file mode 100644
index 0000000000..78a030af22
--- /dev/null
+++ b/public/500.html
@@ -0,0 +1,66 @@
+
+
+
+ We're sorry, but something went wrong (500)
+
+
+
+
+
+
+
+
+
We're sorry, but something went wrong.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000000..37b576a4a0
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1 @@
+# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb
new file mode 100644
index 0000000000..d19212abd5
--- /dev/null
+++ b/test/application_system_test_case.rb
@@ -0,0 +1,5 @@
+require "test_helper"
+
+class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
+end
diff --git a/test/controllers/.keep b/test/controllers/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/controllers/cart_controller_test.rb b/test/controllers/cart_controller_test.rb
new file mode 100644
index 0000000000..7746ca5d40
--- /dev/null
+++ b/test/controllers/cart_controller_test.rb
@@ -0,0 +1,924 @@
+
+
+require "test_helper"
+
+describe CartController do
+
+ before do
+
+ #USER
+ @user_1 = users(:user_1)
+
+ #ORDER TO USE AS TEST CART
+ @cart = orders(:order_5)
+
+ #TEST ORDER
+ @order_1 = orders(:order_1)
+
+ #ORDER_ITEMS ATTACHED TO ORDER 4
+ @item7 = order_items(:order_item_7) #prod 7
+ @item8 = order_items(:order_item_8) #prod 8
+ @item9 = order_items(:order_item_9) #prod 9
+ @item9 = order_items(:order_item_10) #prod 10
+
+ #PRODUCTS
+ @product_1 = products(:product_1)
+ @product_2 = products(:product_2)
+ @product_3 = products(:product_3)
+ @product_4 = products(:product_4)
+ @product_5 = products(:product_5)
+ @product_6 = products(:product_6)
+
+ #PRODUCTS ATTACHED TO ORDER_ITEMS FOR ORDER 4
+ @product_7 = products(:product_7)
+ @product_8 = products(:product_8)
+ @product_9 = products(:product_9)
+ @product_10 = products(:product_10)
+
+ end
+
+ describe "access_cart" do
+
+
+
+
+ it "must succeed" do
+
+ #Act
+ get cart_path
+
+ #Assert
+ must_respond_with :success
+
+ end
+
+ it "finds the correct order by session key if one already exists" do
+
+ #Arrange
+ login(@user_1) # We just need to log in somebody to activate the session. It doesn't matter who, and we won't use user_1's info for anything except validating the arrangement.
+
+ post add_to_cart_path(@product_2.id)
+ session_cart_id = session[:cart_order_id]
+
+ ####Validate Test
+ session[:user_id].must_equal @user_1.id
+ validating_id = Order.find_by(id: session[:cart_order_id]).id
+ validating_id.must_equal Order.last.id
+
+ #Act
+ get cart_path
+
+ #Assert
+ must_respond_with :success
+
+ end
+
+
+ end
+
+ describe "add_to_cart" do
+
+ it "If a cart does not already exist, creates a new instance of Order and assigns its ID to the proper key in session" do
+
+ #Arrange
+ before_count = Order.count
+
+ #Act
+ post add_to_cart_path(@product_2.id) #Note-- does not matter what this product is for purposes of the test-- we just need it to activate the route.
+
+ #Assert
+ validating_id = Order.find_by(id: session[:cart_order_id]).id
+ validating_id.must_equal Order.last.id
+
+ after_count = Order.count
+ (after_count - before_count).must_equal 1
+
+ end
+
+ it "If a cart already exists, finds the corresponding instance of Order according to the key in session, and does not add to the database" do
+
+ #Arrange
+ before_1st_post_count = Order.count
+ post add_to_cart_path(@product_3.id)
+ after_1st_post_count = Order.count
+ initial_session_cart_id = session[:cart_order_id]
+
+ #### Validate the test
+ (after_1st_post_count - before_1st_post_count).must_equal 1
+ initial_session_cart_id.wont_be_nil
+
+ #Act
+ post add_to_cart_path(@product_4.id)
+ after_2nd_post_count = Order.count
+ test_session_cart_id = session[:cart_order_id]
+
+ #Assert
+ (after_1st_post_count - after_2nd_post_count).must_equal 0
+ test_session_cart_id.must_equal initial_session_cart_id
+
+ end
+
+ it "responds with 'failure' if the ID for the product desired is not in the database" do
+
+ #Arrrange
+ @bogus_product_id = 101
+ post add_to_cart_path(@product_5.id) #Note-- does not matter what this product is for purposes of the test-- we just need it to activate the route.
+
+ #Validate test
+ session[:cart_order_id].must_equal Order.last.id
+ Product.find_by(id: @bogus_product_id).must_be_nil
+
+ #Act
+ post add_to_cart_path(@bogus_product_id)
+
+ #Assert
+ flash[:result_text].must_equal "That product could not be added to your cart"
+
+ must_redirect_to cart_path
+
+ end
+
+ it "creates a new order-item instance if the user does not already have an order-item with that product_id in their cart, and assigns the cart's id to its order_id attribute" do
+
+ #Arrange
+ post add_to_cart_path(@product_2.id)
+ cart_order = Order.find_by(id: session[:cart_order_id])
+
+ ###Validate test
+ cart_order.order_items.count.must_equal 1
+ current_product_in_cart = cart_order.order_items.last.product.name
+ added_product_1_order_id = cart_order.order_items.last.order_id
+ added_product_1_order_id.must_equal cart_order.id
+
+ #Act
+ post add_to_cart_path(@product_3.id)
+
+ #Assert
+ cart_order.order_items.count.must_equal 2
+
+ ###The names of the two products will be different
+ new_product_in_cart = cart_order.order_items.last.product.name
+ current_product_in_cart.wont_equal new_product_in_cart
+
+ ### The order_ids of the two order_items will be the same.
+ added_product_2_item_id = cart_order.order_items.last.order_id
+ added_product_2_item_id.must_equal added_product_1_order_id
+
+ end
+
+ it "Increments the existing order-item's quantity by one, if the user already has an order-item with that product id in their cart " do
+
+ #Arrange
+ post add_to_cart_path(@product_6.id)
+ cart_order = Order.find_by(id: session[:cart_order_id])
+
+ ###Validate test
+ cart_order.order_items.count.must_equal 1
+ current_product_in_cart_name = cart_order.order_items.last.product.name
+ current_product_in_cart_quantity = cart_order.order_items.last.quantity
+ current_product_in_cart_quantity.must_equal 1
+
+ #Act
+ post add_to_cart_path(@product_6.id)
+
+ #Assert
+
+ ### No more order items will have been added.
+ cart_order.order_items.count.must_equal 1
+
+ ### The product's name will not have changed.
+ product_after_second_post_name = cart_order.order_items.last.product.name
+ product_after_second_post_name.must_equal current_product_in_cart_name
+
+ ### The quantity will have increased by one.
+ product_after_second_post_quantity = cart_order.order_items.last.quantity
+ (product_after_second_post_quantity - current_product_in_cart_quantity).must_equal 1
+
+ end
+
+ it "responds with 'failure' if there is not enough inventory on-hand to fulfil the user's 'add' request" do
+
+ #Arrange
+ post add_to_cart_path(@product_1.id)
+ cart_order = Order.find_by(id: session[:cart_order_id])
+
+ ###Validate test
+ cart_order.order_items.count.must_equal 1
+ current_product_in_cart_name = cart_order.order_items.last.product.name
+ current_product_in_cart_stock = cart_order.order_items.last.product.stock
+ current_product_in_cart_quantity = cart_order.order_items.last.quantity
+ current_product_in_cart_quantity.must_equal 1
+ current_product_in_cart_stock.must_equal 1
+
+ #Act
+ post add_to_cart_path(@product_1.id)
+
+ #Assert
+
+ ### No more order items will have been added.
+ cart_order.order_items.count.must_equal 1
+
+ ### The quantity will remain the same.
+ product_after_second_post_quantity = cart_order.order_items.last.quantity
+ (product_after_second_post_quantity - current_product_in_cart_quantity).must_equal 0
+
+ ### Appropriate error messages will be given.
+ flash[:result_text].must_equal "Not enough inventory on-hand to complete your request."
+
+ end
+
+ end
+
+ describe "update_cart_info" do
+
+
+ it 'is able to update the current cart' do
+
+ #Arrange
+
+ post add_to_cart_path(@product_1.id)
+ before_cart_order = Order.find_by(id: session[:cart_order_id])
+
+ #Validate the test
+
+ before_cart_order.wont_be_nil
+ before_cart_order.status.must_equal "pending"
+
+ before_cart_order.name.must_be_nil
+ before_cart_order.email.must_be_nil
+ before_cart_order.street_address.must_be_nil
+ before_cart_order.city.must_be_nil
+ before_cart_order.state.must_be_nil
+ before_cart_order.zip.must_be_nil
+ before_cart_order.name_cc.must_be_nil
+ before_cart_order.credit_card.must_be_nil
+ before_cart_order.expiry.must_be_nil
+ before_cart_order.ccv.must_be_nil
+ before_cart_order.billing_zip.must_be_nil
+
+
+
+ #Act
+ patch update_cart_info_path params:{
+
+ order: {
+
+ name: "Order of Operations",
+ email: orders(:order_1).email,
+ street_address: orders(:order_1).street_address,
+ city: orders(:order_1).city,
+ state: orders(:order_1).state,
+ zip: orders(:order_1).zip,
+ name_cc: orders(:order_1).name_cc,
+ credit_card:orders(:order_1).credit_card,
+ "expiry(1i)" => "2019",
+ "expiry(2i)" => "12",
+ "expiry(3i)" => "11",
+ ccv: orders(:order_1).ccv,
+ billing_zip: orders(:order_1).billing_zip
+ }
+ }
+
+ #Assert
+
+ ### Must update the values of the cart's attributes.
+
+ after_cart_order = Order.find_by(id: session[:cart_order_id])
+
+
+ after_cart_order.name.must_equal "Order of Operations"
+ after_cart_order.email.must_equal "customer_1@test.com"
+ after_cart_order.street_address.must_equal "street_address_1"
+ after_cart_order.city.must_equal "city_1"
+ after_cart_order.state.must_equal "state_1"
+ after_cart_order.zip.must_equal "11111"
+ after_cart_order.name_cc.must_equal "name_cc_1"
+ after_cart_order.credit_card.must_equal "1234123412341111"
+ after_cart_order.expiry.to_s.must_equal "2019-12-11"
+ after_cart_order.ccv.must_equal "111"
+ after_cart_order.billing_zip.must_equal "11111"
+
+
+ ###Must provide an appropriate response message
+ flash[:status].must_equal :success
+ flash[:result_text].must_equal "Your order information has been successfully updated!"
+
+ ### Must redirect to cart path.
+
+ must_redirect_to cart_path
+
+ end
+
+ it 'will fail if there is no cart' do
+
+ #Arrange
+
+ #Step 1: Get a session going, with a cart.
+ post add_to_cart_path(@product_1.id)
+ before_cart_order = Order.find_by(id: session[:cart_order_id])
+
+ before_cart_order.wont_be_nil
+
+ #step 2: Prepare the cart for the cart-destruction process.
+
+ patch update_cart_info_path params:{
+
+ order: {
+
+ name: "Order of Operations",
+ email: orders(:order_1).email,
+ street_address: orders(:order_1).street_address,
+ city: orders(:order_1).city,
+ state: orders(:order_1).state,
+ zip: orders(:order_1).zip,
+ name_cc: orders(:order_1).name_cc,
+ credit_card:orders(:order_1).credit_card,
+ "expiry(1i)" => "2019",
+ "expiry(2i)" => "12",
+ "expiry(3i)" => "11",
+ ccv: orders(:order_1).ccv,
+ billing_zip: orders(:order_1).billing_zip
+ }
+ }
+
+ # Step 3: Destroy the cart via the route designated for such things.
+
+ patch update_to_paid_path
+
+ #Now we have a situation where Session is awake, but the cart is gone.
+
+ #Validate the test
+ after_arrange_cart_order = Order.find_by(id: session[:cart_order_id])
+
+ after_arrange_cart_order.must_be_nil
+
+ #Act:
+
+ #Now we try this method without a cart.
+
+ patch update_cart_info_path params:{
+
+ order: {
+
+ name: "Howdy",
+ email: orders(:order_1).email,
+ street_address: orders(:order_1).street_address,
+ city: orders(:order_1).city,
+ state: orders(:order_1).state,
+ zip: orders(:order_1).zip,
+ name_cc: orders(:order_1).name_cc,
+ credit_card:orders(:order_1).credit_card,
+ "expiry(1i)" => "2019",
+ "expiry(2i)" => "12",
+ "expiry(3i)" => "11",
+ ccv: orders(:order_1).ccv,
+ billing_zip: orders(:order_1).billing_zip
+ }
+ }
+
+ #Assert
+
+ ### Must update the values of the cart's attributes.
+
+ after_act_cart_order = Order.find_by(id: session[:cart_order_id])
+
+
+ ###Must provide an appropriate response message
+ flash[:status].must_equal :failure
+ flash[:result_text].must_equal "We were unable to find your cart."
+
+ ### Must redirect to cart path.
+
+ must_respond_with :not_found
+
+
+ end
+ end
+
+ describe "update_to_paid" do
+
+ it "changes the status of an order from 'pending' to 'paid' when that order's attributes are properly populated, removes its ID from session, and redirects to the confirmation page." do
+
+ #Arrange
+ post add_to_cart_path(@product_1.id)
+ cart_order = Order.find_by(id: session[:cart_order_id])
+ cart_order.wont_be_nil
+
+ patch update_cart_info_path(cart_order.id), params: {
+ order: {
+ status:orders(:order_1).status,
+ name: "Hello World!",
+ email: orders(:order_1).email,
+ street_address: orders(:order_1).street_address,
+ city: orders(:order_1).city,
+ state: orders(:order_1).state,
+ zip: orders(:order_1).zip,
+ name_cc: orders(:order_1).name_cc,
+ credit_card: orders(:order_1).credit_card,
+ "expiry(1i)" => "2019",
+ "expiry(2i)" => "12",
+ "expiry(3i)" => "11",
+ ccv: orders(:order_1).ccv,
+ billing_zip: orders(:order_1).billing_zip
+ }
+ }
+
+ our_test_order = Order.find_by(name: "Hello World!")
+ before_status = our_test_order.status
+ #Act
+ patch update_to_paid_path
+
+
+ #Assert
+ ### status must have changed from pending to paid.
+
+ after_test_order = Order.find_by(name: "Hello World!")
+ after_test_order.status.must_equal "paid"
+ after_test_order.status.wont_equal before_status
+
+ ### appropriate flash messages must be given.
+ flash[:status].must_equal :success
+ flash[:result_text].must_equal "Your order has been submitted!"
+
+ ### The cart_order_id from Session must be changed to nil.
+ session[:cart_order_id].must_be_nil
+
+
+ end
+
+ it "will respond with failure when an order does not have all the necessary information in its attributes" do
+
+ #Arrange
+
+ post add_to_cart_path(@product_1.id)
+
+ cart_order = Order.find_by(id: session[:cart_order_id])
+
+
+ ### Validate test
+ cart_order.status.must_equal "pending"
+ before_status = cart_order.status
+
+ #### New order will be missing key information
+ cart_order.name_cc.must_equal nil
+
+ #Act
+ patch update_to_paid_path
+
+ #Assert
+ ### Will display appropriate failure message
+ flash[:result_text].must_equal "We weren't able to process your order. Please double-check the form."
+
+ ### Status will not have changed
+ after_status = cart_order.status
+ after_status.must_equal before_status
+
+ ### The session's cart_order_id key will still be populated with the cart_order's id.
+
+ session[:cart_order_id].must_equal cart_order.id
+
+ ### Proper redirect will happen
+
+ must_redirect_to cart_path
+
+ end
+ end
+
+ describe "destroy" do
+
+ it "When the cart contains multiple items, destroys all the order-items associated with the current cart" do # Remember that empty_cart has been relocated.
+
+ #Arrange
+
+ post add_to_cart_path(@product_1.id)
+ post add_to_cart_path(@product_2.id)
+ cart_order_before = Order.find_by(id: session[:cart_order_id])
+ before_count = cart_order_before.order_items.count
+
+ #Vaidate Test
+ before_count.must_equal 2
+ initial_cart_items = OrderItem.where(order_id: cart_order_before.id)
+ initial_cart_items.count.must_equal 2
+
+ #Act
+
+ delete cart_destroy_path
+
+ #Assert
+
+ cart_order_after = Order.find_by(id: session[:cart_order_id])
+
+ ### The same instance of cart exists before and after the method is called.
+
+ cart_order_before.id.must_equal cart_order_after.id
+
+ ### The method destroys all the items associated with the current cart
+
+ cart_order_after.order_items.count.must_equal 0
+ afterward_cart_items = OrderItem.where(order_id: cart_order_after.id)
+ afterward_cart_items.count.must_equal 0
+
+ ### The method finishes by redirecting to the cart path.
+
+ must_redirect_to cart_path
+
+ end
+
+ it "When the cart contains a single item, destroys all the order-items associated with the current cart, and renders the empty-cart view" do # Remember that empty_cart has been relocated.
+
+ #Arrange
+
+ post add_to_cart_path(@product_3.id)
+ cart_order_before = Order.find_by(id: session[:cart_order_id])
+ before_count = cart_order_before.order_items.count
+
+ #Vaidate Test
+ before_count.must_equal 1
+ initial_cart_items = OrderItem.where(order_id: cart_order_before.id)
+ initial_cart_items.count.must_equal 1
+
+ #Act
+
+ delete cart_destroy_path
+
+ #Assert
+
+ cart_order_after = Order.find_by(id: session[:cart_order_id])
+
+ ### The same instance of cart exists before and after the method is called.
+
+ cart_order_before.id.must_equal cart_order_after.id
+
+ ### The method destroys all the items associated with the current cart
+
+ cart_order_after.order_items.count.must_equal 0
+ afterward_cart_items = OrderItem.where(order_id: cart_order_after.id)
+ afterward_cart_items.count.must_equal 0
+
+ (initial_cart_items.count - afterward_cart_items.count).must_equal 0
+
+ ### The method finishes by redirecting to the cart path.
+
+ must_redirect_to cart_path
+
+
+ end
+
+ it "displays appropriate messages if called when the cart is already empty, and makes no changes to the database." do
+
+ ##Arrange
+ #Step 1: Activate the cart
+ overall_before_count = OrderItem.all.count
+ post add_to_cart_path(@product_3.id)
+ cart_order_before = Order.find_by(id: session[:cart_order_id])
+ before_count = cart_order_before.order_items.count
+
+ #Vaidate Test Pt 1
+ before_count.must_equal 1
+ initial_cart_items = OrderItem.where(order_id: cart_order_before.id)
+ initial_cart_items.count.must_equal 1
+ that_one_item = initial_cart_items.first
+
+ #Step 2: Remove its one item:
+ delete remove_single_item_path(that_one_item.id)
+
+ #Validate Test Pt. 2:
+
+ stage_2_cart_items = OrderItem.where(order_id: cart_order_before.id)
+ stage_2_cart_items.count.must_equal 0
+ OrderItem.find_by(id: that_one_item.id).must_be_nil
+ current_cart = Order.find_by(id: session[:cart_order_id])
+ current_cart.order_items.count.must_equal 0
+
+ #Step 3: Now we have an active-but-empty cart in which to act.
+
+ #ACT:
+
+ delete cart_destroy_path
+
+ #Assert :
+
+ ### Must not reduce the contents of the database
+
+ overall_after_count = OrderItem.all.count
+
+ overall_before_count.must_equal overall_after_count
+
+ ### Must serve appropriate flash messages
+
+ flash[:result_text].must_equal "Your cart was already empty!"
+
+ ### Must redirect to the cart path
+
+ must_redirect_to cart_path
+
+ end
+
+
+ it "When the cart contains items whose quantities are greater than zero, destroys all the order-items associated with the current cart, and renders the empty-cart view" do # Remember that empty_cart has been relocated.
+
+ #Arrange
+ #Step 1: Activate the cart
+ post add_to_cart_path(@product_3.id)
+ post add_to_cart_path(@product_3.id)
+ post add_to_cart_path(@product_4.id)
+ post add_to_cart_path(@product_4.id)
+ cart_order_before = Order.find_by(id: session[:cart_order_id])
+ before_count = cart_order_before.order_items.count
+
+ #Step 2: Vaidate Test
+ before_count.must_equal 2
+ initial_cart_items = OrderItem.where(order_id: cart_order_before.id)
+ initial_cart_items.count.must_equal 2
+ first_item = initial_cart_items.first
+ last_item = initial_cart_items.last
+ first_item.quantity.must_equal 2
+ last_item.quantity.must_equal 2
+
+ #ACT:
+
+ delete cart_destroy_path
+
+ #Assert
+
+ cart_order_after = Order.find_by(id: session[:cart_order_id])
+
+ ### The same instance of cart exists before and after the method is called.
+
+ cart_order_before.id.must_equal cart_order_after.id
+
+ ### The method destroys the earlier-created items, so that no order-items are associated with the current cart any longer.
+
+ OrderItem.find_by(id: first_item.id).must_be_nil
+
+ OrderItem.find_by(id: last_item.id).must_be_nil
+
+ cart_order_after.order_items.count.must_equal 0
+ afterward_cart_items = OrderItem.where(order_id: cart_order_after.id)
+ afterward_cart_items.count.must_equal 0
+
+ ### The method finishes by redirecting to the cart path.
+
+ must_redirect_to cart_path
+
+ end
+
+ it "responds with failure and appropriate error message if there is no identifiable cart" do
+
+ #Act
+
+ delete cart_destroy_path
+
+ #Assert
+
+ ### Must provide appropriate error message
+ flash[:result_text].must_equal "Unable to remove the items from your cart."
+
+ ### Must redirect to the cart_path
+
+ must_redirect_to cart_path
+
+
+ end
+
+ end
+
+ describe "remove_single_item" do
+
+ it "destroys a specified order-item when it is the only item in the cart, and then it renders the :empty_cart view" do
+
+ #Arrange
+ post add_to_cart_path(@product_3.id)
+ cart_order_before = Order.find_by(id: session[:cart_order_id])
+ before_count = cart_order_before.order_items.count
+
+ #Vaidate Test
+ before_count.must_equal 1
+ initial_cart_items = OrderItem.where(order_id: cart_order_before.id)
+ initial_cart_items.count.must_equal 1
+
+ #Act
+ sole_cart_item_id = initial_cart_items.last.id
+
+ delete remove_single_item_path(sole_cart_item_id)
+ cart_order_after = Order.find_by(id: session[:cart_order_id])
+ after_count = cart_order_after.order_items.count
+ afterward_cart_items = OrderItem.where(order_id: cart_order_after.id)
+
+ #Assert
+ ### The same order instance is being used as the cart before and after the method is called.
+
+ cart_order_before.id.must_equal cart_order_after.id
+
+ ###Must remove the item from the cart
+
+ (before_count - after_count).must_equal 1
+
+ ###Must eliminate the item from the database
+
+ afterward_cart_items.count.must_equal 0
+
+ end
+
+ it "destroys a specified order-item when it has a quantity greater than one" do
+
+ #Arrange
+ post add_to_cart_path(@product_3.id)
+ post add_to_cart_path(@product_3.id)
+ cart_order_before = Order.find_by(id: session[:cart_order_id])
+ before_count = cart_order_before.order_items.count
+
+ #Vaidate Test
+ before_count.must_equal 1
+ initial_cart_items = OrderItem.where(order_id: cart_order_before.id)
+ initial_cart_items.count.must_equal 1
+ sole_cart_product_type = initial_cart_items.first
+ number_of_identicals_in_initial_cart = sole_cart_product_type.quantity
+ number_of_identicals_in_initial_cart.must_equal 2
+
+ #Act
+ sole_cart_item_id = sole_cart_product_type.id
+
+ delete remove_single_item_path(sole_cart_item_id)
+
+ cart_order_after = Order.find_by(id: session[:cart_order_id])
+ after_count = cart_order_after.order_items.count
+ afterward_cart_items = OrderItem.where(order_id: cart_order_after.id)
+
+ #Assert
+ #### The same order instance is being used as the cart before and after the method is called.
+
+ cart_order_before.id.must_equal cart_order_after.id
+
+ ####Must remove the item from the cart
+
+ (before_count - after_count).must_equal 1
+
+ ####Must eliminate the item from the database
+
+ afterward_cart_items.count.must_equal 0
+
+
+ end
+
+
+
+ it "destroys the correct item when there are multiple items in the cart, and then redirects to the cart view" do
+
+ #Arrange
+ post add_to_cart_path(@product_3.id)
+ post add_to_cart_path(@product_4.id)
+ cart_order_before = Order.find_by(id: session[:cart_order_id])
+ before_count = cart_order_before.order_items.count
+
+ #Vaidate Test
+ before_count.must_equal 2
+ initial_cart_items = OrderItem.where(order_id: cart_order_before.id)
+ initial_cart_items.count.must_equal 2
+ first_initial_cart_item = initial_cart_items.first
+ last_initial_cart_item = initial_cart_items.last
+ first_initial_cart_item.id.wont_equal last_initial_cart_item.id
+
+ #Act
+ first_initial_item_id = first_initial_cart_item.id
+
+ delete remove_single_item_path(first_initial_item_id)
+
+ cart_order_after = Order.find_by(id: session[:cart_order_id])
+ after_count = cart_order_after.order_items.count
+ afterward_cart_items = OrderItem.where(order_id: cart_order_after.id)
+
+ #Assert
+ ### The same order instance is being used as the cart before and after the method is called.
+
+ cart_order_before.id.must_equal cart_order_after.id
+
+ ###Must remove the item from the cart
+
+ (before_count - after_count).must_equal 1
+
+ ###Must eliminate the item from the database
+
+ afterward_cart_items.count.must_equal 1
+ OrderItem.find_by(id: first_initial_item_id).must_be_nil
+
+ ###The rest of the contents of the cart must remain associated with the cart and in the database.
+
+ afterward_cart_items.where(id: last_initial_cart_item.id).count.must_equal 1
+
+
+ OrderItem.where(id: last_initial_cart_item.id).count.must_equal 1
+
+ ### Will redirect to the cart path
+
+ must_redirect_to cart_path
+
+ end
+
+
+ it "responds with the appropriate failure messages if cart is empty, and makes no changes to the database" do
+
+ #Arrange / Validate the test
+
+ post add_to_cart_path(@product_3.id)
+ delete cart_destroy_path
+
+ cart_order_before = Order.find_by(id: session[:cart_order_id])
+
+ cart_order_before.wont_be_nil
+ cart_order_before.order_items.count.must_equal 0
+
+ total_order_items_before = OrderItem.all
+ item_not_in_cart = total_order_items_before.find_by(id: @item7.id)
+ item_not_in_cart.wont_be_nil
+
+ #Act
+
+ delete remove_single_item_path(@item7.id)
+
+ #Assert
+
+ ### The same order instance will be serving as the cart before and after the method is called.
+
+ cart_order_after = Order.find_by(id: session[:cart_order_id])
+
+ cart_order_after.wont_be_nil
+ cart_order_after.id.must_equal cart_order_before.id
+
+ ### Must provide appropriate error message
+ flash[:result_text].must_equal "Unable to remove the items from your cart."
+
+ ### Nothing will have been removed from the database
+
+ total_order_items_after = OrderItem.all
+
+ total_order_items_after.find_by(id: @item7.id)
+ item_not_in_cart.wont_be_nil
+
+ total_order_items_after.must_equal total_order_items_before
+
+ ### No change will have been made to the contents of the cart.
+
+ cart_order_before.order_items.count.must_equal cart_order_after.order_items.count
+
+ ### Must redirect to the cart_path
+
+ must_redirect_to cart_path
+
+
+ end
+
+
+
+ it "returns failure if cart is not empty, but does not contain the specified item, and remains in the cart view" do
+
+
+ #Arrange
+ post add_to_cart_path(@product_3.id)
+ cart_order_before = Order.find_by(id: session[:cart_order_id])
+ before_count = cart_order_before.order_items.count
+
+ #Vaidate Test
+ before_count.must_equal 1
+ initial_cart_items = OrderItem.where(order_id: cart_order_before.id)
+ initial_cart_items.count.must_equal 1
+
+ bogus_order_item_id = 23
+ OrderItem.find_by(id: bogus_order_item_id).must_be_nil
+
+ #Act
+ sole_cart_item_id = initial_cart_items.last.id
+
+ delete remove_single_item_path(bogus_order_item_id)
+
+
+ cart_order_after = Order.find_by(id: session[:cart_order_id])
+ after_count = cart_order_after.order_items.count
+ afterward_cart_items = OrderItem.where(order_id: cart_order_after.id)
+
+ #Assert
+ ### The same order instance is being used as the cart before and after the method is called.
+
+ cart_order_before.id.must_equal cart_order_after.id
+
+ ###Nothing will be removed from the cart.
+
+ (before_count - after_count).must_equal 0
+
+ ###Nothing will be removed from the database
+
+ afterward_cart_items.count.must_equal 1
+ OrderItem.find_by(id: afterward_cart_items.first.id).wont_be_nil
+
+ #### It will provide appropriate error messages
+
+ flash[:result_text].must_equal "Unable to remove the items from your cart."
+
+ #### It will redirect to the cart path
+
+ must_redirect_to cart_path
+
+ end
+ end
+
+end
diff --git a/test/controllers/categories_controller_test.rb b/test/controllers/categories_controller_test.rb
new file mode 100644
index 0000000000..43a20d05b1
--- /dev/null
+++ b/test/controllers/categories_controller_test.rb
@@ -0,0 +1,40 @@
+require "test_helper"
+
+describe CategoriesController do
+ let(:u) { users(:user_1) }
+
+ # describe "index" do
+ # it "should run successfully" do
+ # get categories_path
+ # must_respond_with :success
+ # end
+ # end
+
+ describe "new" do
+ it "should run successfully" do
+ login(u)
+ get new_category_path
+ must_respond_with :success
+ end
+ end
+
+ describe "create" do
+ it "should redirect if category exists" do
+ category_data = {
+ category: { name: categories(:category_1).name }
+ }
+ post categories_path, params: category_data
+ must_respond_with :bad_request
+ end
+
+ it "should create category" do
+ category_data = {
+ category: { name: "Test category" }
+ }
+ proc {
+ post categories_path, params: category_data
+ }.must_change 'Category.count', 1
+ end
+ end
+
+end
diff --git a/test/controllers/homepage_controller_test.rb b/test/controllers/homepage_controller_test.rb
new file mode 100644
index 0000000000..be39d1ca5e
--- /dev/null
+++ b/test/controllers/homepage_controller_test.rb
@@ -0,0 +1,8 @@
+require "test_helper"
+
+describe HomepageController do
+ it "Index" do
+ get homepage_path
+ must_respond_with :success
+ end
+end
diff --git a/test/controllers/order_items_controller_test.rb b/test/controllers/order_items_controller_test.rb
new file mode 100644
index 0000000000..b18b18024a
--- /dev/null
+++ b/test/controllers/order_items_controller_test.rb
@@ -0,0 +1,91 @@
+require "test_helper"
+
+describe OrderItemsController do
+ before do
+ user = User.new(provider: "github", uid: 13461, username: "test_me", email: "test_user@gmail.com")
+ OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new(mock_auth_hash(user))
+
+ get auth_callback_path(:github)
+
+ end
+
+
+
+ describe 'Update' do
+ it 'should update a current valid order item' do
+
+ proc{
+ patch order_item_path(order_items(:order_item_4)), params:{
+ order_item:{
+ quantity:2,
+ is_shipped:order_items(:order_item_4).is_shipped,
+ product:order_items(:order_item_4).product,
+ order:order_items(:order_item_4).order
+ }
+ }}.wont_change "OrderItem.count"
+ updated_order_item = OrderItem.find_by(id: order_items(:order_item_4).id)
+
+ updated_order_item.id.must_equal order_items(:order_item_4).id
+ updated_order_item.quantity.must_equal 2
+ flash[:result_text].must_equal "Quantity updated!"
+ must_respond_with :redirect
+ must_redirect_to cart_path
+ end
+
+ it 'should return 404 for an order item ID that does not exist' do
+ unavial_product_id = -100001
+ patch order_item_path(unavial_product_id), params:{
+ order_item:{
+ quantity: 1,
+ is_shipped:false,
+ product: "a product",
+ order: "order_50000"
+ }
+ }
+ must_respond_with :not_found
+ end
+
+ it 'will not update an invalid order item' do
+ patch order_item_path(order_items(:order_item_4).id), params:{
+ order_item:{
+ quantity: " ",
+ is_shipped: order_items(:order_item_4).is_shipped,
+ product: order_items(:order_item_4).product,
+ order: order_items(:order_item_4).order
+ }
+ }
+ flash[:result_text].must_equal "Could not add the desired quantity of this item to the cart."
+ end
+ end
+
+ describe 'Set-status' do
+ it 'is able to set the status for a current order' do
+
+ put order_item_set_status_path(order_items(:order_item_1).id), params:{
+ order_item:{
+ quantity: order_items(:order_item_1).quantity,
+ is_shipped:order_items(:order_item_1).is_shipped,
+ product: order_items(:order_item_1).product,
+ order: order_items(:order_item_1).order
+ }
+ }
+ updated_order_item = OrderItem.find_by(id: order_items(:order_item_1).id)
+
+ updated_order_item.order.status.must_equal "complete"
+ must_respond_with :found
+
+ end
+ it 'will render 404 for an order that does not exist' do
+ unavial_order_id = -100001
+ put order_item_set_status_path(unavial_order_id), params:{
+ order_item:{
+ quantity: 1,
+ is_shipped:false,
+ product: "a product",
+ order: "order_50000"
+ }
+ }
+ must_respond_with :not_found
+ end
+ end
+end
diff --git a/test/controllers/orders_controller_test.rb b/test/controllers/orders_controller_test.rb
new file mode 100644
index 0000000000..acba977ab6
--- /dev/null
+++ b/test/controllers/orders_controller_test.rb
@@ -0,0 +1,249 @@
+require "test_helper"
+
+ describe OrdersController do
+ before do
+ user = User.new(provider: "github", uid: 13461, username: "test_me", email: "test_user@gmail.com")
+ OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new(mock_auth_hash(user))
+
+ get auth_callback_path(:github)
+ end
+
+ # describe 'Index' do
+ # it "should get index" do
+ # get orders_path
+ # must_respond_with :success
+ # end
+ # end
+
+#
+ # describe 'New' do
+ # it 'should be able to render a new order form' do
+ #
+ # get new_order_path
+ # must_respond_with :success
+ # end
+ # end
+
+ # describe 'Create' do
+ # it 'should be able to create a new order' do
+ # proc{
+ # post orders_path, params:{
+ # order: {status:orders(:order_2).status,
+ # name: orders(:order_2).name,
+ # email: orders(:order_2).email,
+ # street_address: orders(:order_2).street_address,
+ # city: orders(:order_2).city,
+ # state: orders(:order_2).state,
+ # zip: orders(:order_2).zip,
+ # name_cc: orders(:order_2).name_cc,
+ # credit_card:orders(:order_2).credit_card,
+ # expiry: orders(:order_2).expiry,
+ # ccv: orders(:order_2).ccv ,
+ # billing_zip: orders(:order_2).billing_zip }
+ # }
+ # }.must_change 'Order.count', 1
+ # must_respond_with :redirect
+ # must_redirect_to order_confirmation_path(Order.last.id)
+ #
+ # Order.last.status.must_equal "complete"
+ # Order.last.name.must_equal "name_2"
+ # Order.last.email.must_equal "customer_2@test.com"
+ # Order.last.street_address.must_equal "street_address_2"
+ # Order.last.city.must_equal "city_2"
+ # Order.last.state.must_equal "state_2"
+ # Order.last.zip.must_equal "22222"
+ # Order.last.name_cc.must_equal "name_cc_2"
+ # Order.last.credit_card.must_equal "1234123412342222"
+ # Order.last.expiry.must_equal Order.last.expiry
+ # Order.last.ccv.must_equal "222"
+ # Order.last.billing_zip.must_equal "22222"
+ #
+ # flash[:result_text].must_equal "Your order has been made - congratulations!"
+ #
+ #
+ #
+ # end
+
+ # it 'Invalid Order object should not be posted/created' do
+ # proc{
+ # post orders_path, params:{
+ # order: {status:" ",
+ # email: orders(:order_2).email,
+ # street_address: orders(:order_2).street_address,
+ # city: orders(:order_2).city,
+ # state: orders(:order_2).state,
+ # zip: orders(:order_2).zip,
+ # name_cc: orders(:order_2).name_cc,
+ # credit_card:orders(:order_2).credit_card,
+ # expiry: orders(:order_2).expiry,
+ # ccv: orders(:order_2).ccv ,
+ # billing_zip: orders(:order_2).billing_zip }
+ # }}.must_change 'Order.count', 0
+ #
+ # must_respond_with :bad_request
+ #
+ # flash[:result_text].must_equal "Something has gone wrong in your orders processing."
+ # end
+ #
+ # it 'Will render the current show page for invalid data and return HTTP status code:400 from the server' do
+ # proc{
+ # post orders_path, params:{
+ # order: {status:"",
+ # name: orders(:order_1).name,
+ # email: orders(:order_1).email,
+ # street_address: orders(:order_1).street_address,
+ # city: orders(:order_1).city,
+ # state: orders(:order_1).state,
+ # zip: orders(:order_1).zip,
+ # name_cc: orders(:order_1).name_cc,
+ # credit_card:orders(:order_1).credit_card,
+ # expiry: orders(:order_1).expiry,
+ # ccv: orders(:order_1).ccv ,
+ # billing_zip: orders(:order_1).billing_zip }
+ # }}.must_change 'Order.count', 0
+ #
+ # must_respond_with :bad_request
+ # end
+ # end
+ describe 'Confirmation' do
+ it 'will display a confirmation for an associated Order' do
+ get order_confirmation_path(orders(:order_3).id)
+
+ orders(:order_3).id.must_equal orders(:order_3).id
+ must_respond_with :success
+ end
+
+ it 'will render 404 if order ID does not exist' do
+ non_existant_order = -100001
+ get order_confirmation_path(non_existant_order)
+ must_respond_with :not_found
+ end
+ end
+
+ describe 'Show' do
+ it 'will display an order page' do
+ get order_path(orders(:order_4).id)
+ must_respond_with :success
+ orders(:order_4).id.must_equal orders(:order_4).id
+ end
+
+ it 'will return a 404 page for a request for an order id that does not exist' do
+ non_existant_order = -20000002
+ get order_path(non_existant_order)
+ must_respond_with :not_found
+ end
+ end
+
+# describe 'Edit' do
+# it "will provide the page to allow a user to edit an Order" do
+# get edit_order_path(orders(:order_3).id)
+# must_respond_with :success
+# orders(:order_3).id.must_equal orders(:order_3).id
+# end
+#
+# it 'will render a 404 page for an edit page for a edit page that does not exist' do
+# non_existant_order = -400004
+# get edit_order_path(non_existant_order)
+# must_respond_with :not_found
+# end
+# end
+
+ describe 'Update' do
+ it 'is able to update a current object' do
+ proc{
+ patch order_path(orders(:order_2).id), params:{
+ order: {status:orders(:order_2).status,
+ name: "Hello World!",
+ email: orders(:order_2).email,
+ street_address: orders(:order_2).street_address,
+ city: orders(:order_2).city,
+ state: orders(:order_2).state,
+ zip: orders(:order_2).zip,
+ name_cc: orders(:order_2).name_cc,
+ credit_card:orders(:order_2).credit_card,
+ expiry: orders(:order_2).expiry,
+ ccv: orders(:order_2).ccv ,
+ billing_zip: orders(:order_2).billing_zip }
+ }
+ }.must_change 'Order.count', 0
+
+ updated_user = Order.find_by(id: orders(:order_2).id)
+ updated_user.name.must_equal "Hello World!"
+ updated_user.email.must_equal orders(:order_2).email
+ updated_user.street_address.must_equal orders(:order_2).street_address
+ updated_user.city.must_equal orders(:order_2).city
+ updated_user.state.must_equal orders(:order_2).state
+ updated_user.zip.must_equal orders(:order_2).zip
+ updated_user.name_cc.must_equal orders(:order_2).name_cc
+ updated_user.credit_card.must_equal orders(:order_2).credit_card
+ updated_user.expiry.must_equal orders(:order_2).expiry
+ updated_user.ccv.must_equal orders(:order_2).ccv
+ updated_user.billing_zip.must_equal orders(:order_2).billing_zip
+
+ flash[:result_text].must_equal "Hello World! has been updated"
+ must_respond_with :redirect
+ must_redirect_to order_path(orders(:order_2).id)
+
+ end
+
+ it 'will render 404 page for request to update an order that does not exist' do
+ non_existant_order = -100000001
+ patch order_path(non_existant_order)
+ must_respond_with :not_found
+ end
+
+ it 'will return a bad request for an attempt to update an order with invalid data' do
+ proc{
+ patch order_path(orders(:order_2).id), params:{
+ order: {status:orders(:order_2).status,
+ name: " ",
+ email: orders(:order_2).email,
+ street_address: orders(:order_2).street_address,
+ city: orders(:order_2).city,
+ state: orders(:order_2).state,
+ zip: orders(:order_2).zip,
+ name_cc: orders(:order_2).name_cc,
+ credit_card:orders(:order_2).credit_card,
+ expiry: orders(:order_2).expiry,
+ ccv: orders(:order_2).ccv ,
+ billing_zip: orders(:order_2).billing_zip }
+ }
+ }.must_change 'Order.count', 0
+ flash[:result_text].must_equal ' update has failed'
+ must_respond_with :bad_request
+
+ end
+ end
+
+ describe 'Cancel' do
+ it 'will update the status of a current order to "cancelled"' do
+ put order_cancel_path( orders(:order_2).id )
+ must_respond_with :redirect
+ must_redirect_to products_path
+
+ updated_order = Order.find_by(id: orders(:order_2).id)
+ updated_order.status.must_equal "cancelled"
+ updated_order.id.must_equal orders(:order_2).id
+
+ end
+ it 'will render 404 page for an order that does not exist' do
+ non_existant_order = -1
+ put order_cancel_path(non_existant_order)
+ must_respond_with :not_found
+ end
+ end
+
+ # describe 'Destroy' do
+ # it 'will destroy an order' do
+ # delete order_path(orders(:order_3).id)
+ # must_respond_with :success
+ # end
+ #
+ # it 'will render 404 page for an order that does not exist' do
+ # non_existant_order = -1001
+ # delete order_path(non_existant_order)
+ # must_respond_with :not_found
+ # end
+ # end
+
+end
diff --git a/test/controllers/products_controller_test.rb b/test/controllers/products_controller_test.rb
new file mode 100644
index 0000000000..4f2b1ce979
--- /dev/null
+++ b/test/controllers/products_controller_test.rb
@@ -0,0 +1,381 @@
+require "test_helper"
+
+describe ProductsController do
+
+ describe "index" do
+
+ it 'displays all products' do
+
+ get products_path
+ must_respond_with :success
+
+ end
+
+ it 'succeeds with a category ID as a parameter' do
+
+ @category = categories(:category_1)
+ # Product.all.count.must_be :>, 0
+ # Category.all.count.must_be :>, 0
+
+ get category_products_path(@category.id)
+ must_respond_with :success
+
+ end
+
+ it 'will display all products for a specific user' do
+
+ #Arrange
+ @user_2 = users(:user_2)
+
+ #Act
+ get user_products_path(@user_2.id)
+
+ #Assert
+ must_respond_with :success
+ #how to get the specific category to return.
+ end
+
+ end
+
+ describe 'New' do
+
+ # PASSING
+ it 'will display a new product form' do
+
+ @user_2 = users(:user_2)
+ login(@user_2)
+ get new_user_product_path(users(:user_1).id)
+ must_respond_with :success
+ end
+ end
+
+ describe 'Create' do
+
+ #ALL CREATE PASSING
+
+ before do
+ @user_1 = users(:user_1)
+ end
+
+ it 'will create a new valid product for a user' do
+
+ #Arrange
+
+ login(@user_1)
+ session[:user_id].must_equal @user_1.id
+ before_count = Product.all.count
+
+ #Act
+ post user_products_path(@user_1.id),
+ params: {
+ product: {
+ name: "A Product",
+ is_active: true,
+ description: "ldkfjsldkfj",
+ price: 111,
+ photo_url: nil,
+ stock: 12,
+ user_id: users(:user_1).id
+ }
+ }
+
+ new_product = Product.last
+ after_count = Product.all.count
+ new_product.name.must_equal "A Product"
+ new_product.is_active.must_equal true
+ # new_product.price.must_equal 11
+ #model method multiplies by 100 ?
+ new_product.photo_url.must_equal nil
+ new_product.stock.must_equal 12
+ new_product.user_id.must_equal @user_1.id
+
+
+ flash[:result_text].must_equal "A Product has been successfully created!"
+
+ must_respond_with :redirect
+ must_redirect_to product_path(Product.last.id)
+
+ end
+
+ it 'will not create a product with invalid inputs' do
+
+ #Arrange
+ @user_2 = users(:user_2)
+
+ #Act
+ proc {
+ post user_products_path(users(:user_2).id),
+ params:{
+ product:{ name: "",
+ is_active: products(:product_2).is_active,
+ description: products(:product_2).description,
+ price: products(:product_2).price,
+ photo_url: products(:product_2).photo_url,
+ stock: products(:product_2).stock,
+ user_id: users(:user_2).id}
+ }
+ }.wont_change 'Product.count'
+ # must_respond_with :bad_request
+ end
+
+ it 'will not allow a user to create a product for another user ID' do
+
+ #Arrange
+ login(@user_1)
+ session[:user_id].must_equal @user_1.id
+
+ #Act
+
+ proc {
+ post user_products_path(users(:user_2).id),
+ params:{
+ product:{ name: "",
+ is_active: products(:product_2).is_active,
+ description: products(:product_2).description,
+ price: products(:product_2).price,
+ photo_url: products(:product_2).photo_url,
+ stock: products(:product_2).stock,
+ user_id: users(:user_1).id}
+ }
+ }.wont_change 'Product.count'
+ must_respond_with :bad_request
+
+ end
+ end
+
+ describe 'Show' do
+
+ # ALL SHOW PASSING
+
+ it "will display a product's detail page, even for a logged-out user" do
+
+ get product_path(products(:product_4).id)
+ must_respond_with :success
+
+ #an existing yml object is showing nil?
+ end
+
+ it "will render 404 not found for a product that does not exist, even for a logged-out user" do
+
+ non_existant_id = -100001
+ get product_path(non_existant_id)
+ must_respond_with :not_found
+
+ end
+ end
+
+ describe 'Edit' do
+
+ it 'will provide the populated fields necessary for editing for a logged-in user for their own product' do
+
+ #Passing
+
+ #Arrange
+ @user_1 = users(:user_1)
+ login(@user_1)
+
+ @product_1 = products(:product_1)
+ @product_1.user_id = @user_1.id
+ @product_1.save
+
+ #Validate test
+
+ modified_1 = Product.find_by(user_id: @user_1.id)
+ modified_1.name.must_equal "product_1"
+
+ #Act
+
+ get edit_product_path(@product_1.id)
+ must_respond_with :success
+
+ #update to confirm all fields are passing in correctly#
+ end
+
+ it 'will NOT provide the populated fields necessary for editing for editing a product, even for a logged-in user, for a product that does not belong to that user' do
+
+ #PASSING
+ #Arrange
+ @user_4 = users(:user_4)
+ login(@user_4)
+
+ @user_1 = users(:user_1)
+
+ @product_6 = products(:product_6)
+ @product_6.user_id = @user_1.id
+
+ #Validate test
+ @user_4.id.wont_equal @product_6.user_id
+
+ get edit_product_path(@product_6.id)
+
+ flash[:result_text].must_equal "Merchants are only allowed to modify their own products"
+
+ #update to confirm all fields are passing in correctly#
+ end
+
+ it 'will NOT provide the populated fields necessary for editing for a person who is not logged in' do
+
+ #PASSING
+ #Arrange
+ @user_2 = users(:user_2)
+ login(@user_2)
+
+ delete logout_path
+
+ @product_2 = products(:product_2)
+ @product_2.user_id.must_equal @user_2.id
+
+ modified_2 = Product.find_by(user_id: @user_2.id)
+
+ #Validate test
+ modified_2.id.must_equal @product_2.id
+
+ #Act
+
+ get edit_product_path(@product_2.id)
+
+ #Assert
+ # Provides appropriat error messages
+ must_redirect_to github_login_path
+ end
+
+
+
+ it 'will render 404 not found for a request to edit a product that does not exist, even for a logged in user' do
+
+ #PASSING
+ #Arrange
+ @user_1 = users(:user_1)
+ login(@user_1)
+
+ non_existant_id = 2
+
+ #Validate test
+ Product.find_by(id: non_existant_id).must_be_nil
+
+ #Act
+ get edit_product_path(non_existant_id)
+
+ #Assert
+ must_respond_with :not_found
+ end
+ end
+
+ describe 'Update' do
+
+ it 'will allow a user to update an existing product' do
+
+ #PASSING
+ #Arrange
+
+ @user_1 = users(:user_1)
+ login(@user_1)
+
+ @product_1 = products(:product_1)
+ @product_1.user_id = @user_1.id
+
+
+
+ proc {patch product_path(products(:product_1).id), params:{
+ product:{ name: "new product name",
+ is_active: products(:product_1).is_active,
+ description: products(:product_1).description,
+ price: products(:product_1).price,
+ photo_url: products(:product_1).photo_url,
+ stock: products(:product_1).stock,
+ user_id: users(:user_1).id }
+ }}.wont_change 'Product.count'
+ must_respond_with :redirect
+ must_redirect_to product_path(products(:product_1))
+
+ end
+
+ it 'will render 404 if the product being requested to update does not exist' do
+
+ #PASSING
+ @user_1 = users(:user_1)
+ login(@user_1)
+
+ non_existant_id = -100001
+ patch product_path(non_existant_id)
+ must_respond_with :not_found
+
+ end
+
+ it "will not allow other users to update another user's product" do
+
+ #PASSING
+
+ proc {patch product_path(products(:product_1)), params:{
+ product:{ name: "new product name",
+ is_active: products(:product_2).is_active,
+ description: products(:product_2).description,
+ price: products(:product_2).price,
+ photo_url: products(:product_2).photo_url,
+ stock: products(:product_2).stock,
+ user_id: users(:user_1).id }
+ }}.wont_change 'Product.count'
+ must_respond_with :redirect
+ end
+
+ it "will not allow a user to update a product with invalid data" do
+
+ #PASSING
+
+ @user_1 = users(:user_1)
+ login(@user_1)
+
+ proc {patch product_path(products(:product_1).id), params:{
+ product:{ name: "",
+ is_active: products(:product_1).is_active,
+ description: products(:product_1).description,
+ price: products(:product_1).price,
+ photo_url: products(:product_1).photo_url,
+ stock: products(:product_1).stock,
+ user_id: users(:user_1).id }
+ }}.wont_change 'Product.count'
+
+ must_respond_with :bad_request
+ end
+
+ # it "will not allow a user to modify the amount of stock" do
+ #
+ # end
+
+
+ end
+
+ describe 'Set Status' do
+
+ before do
+
+ @user_1 = users(:user_1)
+ login(@user_1)
+
+ end
+
+ it "an existing product's status can be updated" do
+
+ #PASSING
+
+ login(@user_1)
+
+ @product_1 = products(:product_1)
+
+ put product_set_status_path(@product_1.id)
+
+ must_respond_with :found
+
+ end
+
+ it 'a product that does not exist will be sent a 404' do
+
+ #PASSING
+
+ non_existant_id = -100001
+ put product_set_status_path(non_existant_id)
+ must_respond_with :not_found
+ end
+
+ end
+end
diff --git a/test/controllers/reviews_controller_test.rb b/test/controllers/reviews_controller_test.rb
new file mode 100644
index 0000000000..6b747cf227
--- /dev/null
+++ b/test/controllers/reviews_controller_test.rb
@@ -0,0 +1,41 @@
+require "test_helper"
+
+
+describe ReviewsController do
+ before do
+ user = User.new(provider: "github", uid: 13461, username: "test_me", email: "test_user@gmail.com")
+ OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new(mock_auth_hash(user))
+
+ get auth_callback_path(:github)
+ end
+ describe 'Create' do
+ it 'will create a new review that is valid for a product' do
+
+ proc{
+ post product_reviews_path(products(:product_4).id), params:{
+ review: {rating:reviews(:review_4).rating,
+ content:reviews(:review_4).content,
+ product:reviews(:review_4).product }
+ }
+ }.must_change 'Review.count', 1
+ must_respond_with :redirect
+ updated_review = Review.find_by(id: reviews(:review_4).id)
+ updated_review.rating.must_equal 4
+ updated_review.content.must_equal "content_4"
+ updated_review.product.name.must_equal products(:product_4).name
+ flash[:result_text].must_equal "Your comment was saved"
+ end
+
+ it 'will render a bad request for an invalid review' do
+ proc{
+ post product_reviews_path(products(:product_4).id), params:{
+ review: {rating:"",
+ content:reviews(:review_4).content,
+ product:reviews(:review_4).product }
+ }
+ }.wont_change 'Review.count'
+
+ must_respond_with :bad_request
+ end
+ end
+ end
diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb
new file mode 100644
index 0000000000..c9b3449f57
--- /dev/null
+++ b/test/controllers/sessions_controller_test.rb
@@ -0,0 +1,220 @@
+require "test_helper"
+
+
+describe SessionsController do
+
+ describe "new" do
+
+ it "succeeds" do
+
+ get new_session_path
+ must_respond_with :found
+
+ end
+
+ end
+
+ describe "create" do
+
+ it "logs in an existing user and redirects to the root route, without creating a new database entry" do
+
+ #Arrange
+ start_count = User.count
+ user = users(:user_1)
+ OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new(mock_auth_hash(user))
+
+ #Act
+ get auth_callback_path(:github)
+
+ #Assert
+
+ ### Logs in an existing user:
+ flash[:result_text].must_equal "Logged in successfully"
+ session[:user_id].must_equal user.id
+
+ ### Does not add to the database
+ User.count.must_equal start_count
+
+ ### Redirects to the root path
+ must_redirect_to root_path
+
+ end
+
+ it "creates an account for a new user, logs th new user in, provides appropriate success messages, and redirects to the root route" do
+
+ # Arrange
+
+ start_count = User.count
+ user = User.new(provider: "github", uid: 40420, username: "drywall_bob", email: "bob@drywall.com")
+ initial_user_id = user.id
+ OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new(mock_auth_hash(user))
+
+ ### Validating arrangement:
+ initial_user_id.must_be_nil
+
+ # Act
+ get auth_callback_path(:github)
+ new_user = User.find_by(username: "drywall_bob")
+ session_id_accessible = session[:user_id]
+
+ # Assert
+
+ ### Creates an account
+ User.count.must_equal start_count + 1
+ User.last.must_equal new_user
+ initial_user_id.wont_equal new_user.id
+
+ ### Logs the new user in
+ session[:user_id].must_equal new_user.id
+
+ ### Provides appropriate success messages
+ flash[:result_text].must_equal "Successful first login!"
+
+ ### Redirects to the root path
+ must_redirect_to root_path
+
+ end
+
+ it "does not log in, and provides appropriate failure messages, if given data that Github cannot use for a login" do
+
+ #Arrange
+ start_count = User.count
+ user = User.new(provider: "github", uid: "", username: "Ratso", email: "")
+ OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new(mock_auth_hash(user))
+
+ #Act
+ get auth_callback_path(:github)
+ bogus_user = User.find_by(username: "Ratso")
+ session_id_accessible = session[:user_id]
+
+ #Assert
+
+ ### Does not log in
+ session[:user_id].must_be_nil
+
+ ### Does not add to the database
+ bogus_user.must_be_nil
+ User.count.must_equal start_count
+
+ ### Provides appropriate failure messages
+ flash[:status].must_equal :failure
+
+ ### Redirects to the root path
+ must_redirect_to root_path
+
+ end
+
+ it "does not log in, and provides appropriate failure messages, if unable to get a response from the provider" do
+
+ #Arrange
+ start_count = User.count
+ user = User.new(provider: "github", uid: nil, username: "drywall_bob", email: "bob@drywall.com")
+
+ OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new(mock_auth_hash(user))
+
+ #Act
+ get auth_callback_path(:github)
+ failing_user = User.find_by(username: "drywall_bob")
+ session_id_accessible = session[:user_id]
+
+ #Assert
+
+ ### Does not log in
+ session[:user_id].must_be_nil
+
+ ### Does not add to the database
+ User.count.must_equal start_count
+ failing_user.must_be_nil
+
+
+ ### Provides appropriate failure messages
+ flash[:status].must_equal :failure
+ flash[:result_text].must_equal "Logging in through Github not successful"
+
+ ### Redirects to root path
+ must_redirect_to root_path
+
+ end
+
+ it "if given data that Github can use, but that our database will reject: does not log in, does not add to the database, provides appropriate failure messages, and redirects to root path" do
+
+ #Arrange
+ start_count = User.count
+ user = User.new(provider: "github", uid: 33333334, username: "username_3", email: "goopface@goopersunited.com")
+ OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new(mock_auth_hash(user))
+
+ #Act
+ get auth_callback_path(:github)
+ duplicate_user = User.find_by(email: "goopface@goopersunited.com")
+ session_id_accessible = session[:user_id]
+
+ #Assert
+
+ ### Does Not Log In:
+ session[:user_id].must_be_nil
+
+ ### Does Not Add to the Database:
+ duplicate_user.must_be_nil
+ User.count.must_equal start_count
+
+ ### Provides appropriate failure messages:
+ flash[:status].must_equal :failure
+ flash[:result_text].must_equal "An error occurred during User creation."
+
+ ### Redirects to root_path
+ must_redirect_to root_path
+
+ end
+ end
+
+ # describe "index" do
+ #
+ # it "Can find a user by session id and assign it to an instance variable" do
+ #
+ # #Arrange
+ # user_2 = users(:user_2)
+ # login(user_2)
+ #
+ # #Act
+ # get sessions_path
+ #
+ # #Assert
+ # must_respond_with :success
+ #
+ # end
+ #
+ # it "returns an error if no user is logged in" do
+ #
+ # proc { get sessions_path }.must_raise StandardError
+ #
+ # end
+ #
+ # end
+
+
+ describe "destroy" do
+
+ it "logs out a logged-in user" do
+
+ # Arrange
+ user_2 = users(:user_2)
+ login(user_2)
+
+
+ ###Validate arrangement
+ user_2_id = User.find_by(username: "username_2").id
+ session[:user_id].must_equal user_2_id
+
+ #Act
+ delete logout_path
+
+ #Assert
+ session[:user_id].must_be_nil
+ flash[:status].must_equal :success
+ flash[:result_text].must_equal "Successfully logged out"
+ must_redirect_to root_path
+ end
+
+ end
+
+end
diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb
new file mode 100644
index 0000000000..5c25e248ef
--- /dev/null
+++ b/test/controllers/users_controller_test.rb
@@ -0,0 +1,54 @@
+require "test_helper"
+
+describe UsersController do
+ let(:u) { users(:user_1) }
+
+
+ # describe "index" do
+ # it "should run successfully" do
+ # login(u)
+ # get users_path
+ # must_respond_with :success
+ # end
+ # end
+
+
+ describe "show" do
+ it "should run successfully for valid user" do
+ login(u)
+ get user_path(u.id)
+ must_respond_with :success
+ end
+
+ it "should run successfully for valid user with params" do
+ login(u)
+ get user_path(u.id), params: { term: "pending" }
+ must_respond_with :success
+ end
+
+ it "should render 404 for invalid user" do
+ login(u)
+ get user_path(9)
+ must_respond_with :not_found
+ end
+ end
+
+ describe "create" do
+ it "should run successfully" do
+ user_data = {
+ user: { name: "Test User" }
+ }
+ post users_path, params: user_data
+ must_respond_with :success
+ end
+
+ it "should create user" do
+ user_data = {
+ user: { name: "Test User", id: "test" }
+ }
+ proc {
+ post users_path, params: user_data
+ }.must_change 'User.count', 0
+ end
+ end
+end
diff --git a/test/fixtures/.keep b/test/fixtures/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/fixtures/categories.yml b/test/fixtures/categories.yml
new file mode 100644
index 0000000000..0e0b850558
--- /dev/null
+++ b/test/fixtures/categories.yml
@@ -0,0 +1,25 @@
+category_1:
+ name: Category_1
+category_2:
+ name: Category_2
+category_3:
+ name: Category_3
+category_4:
+ name: Category_4
+category_5:
+ name: Category_5
+category_6:
+ name: Category_6
+category_7:
+ name: Category_7
+category_8:
+ name: Category_8
+category_9:
+ name: Category_9
+category_10:
+ name: Category_10
+Outfit:
+ name: Outfit
+Food:
+ name: Food
+
diff --git a/test/fixtures/categories_products.yml b/test/fixtures/categories_products.yml
new file mode 100644
index 0000000000..a9d3c15f99
--- /dev/null
+++ b/test/fixtures/categories_products.yml
@@ -0,0 +1,6 @@
+categories_products_1:
+ category_id: Outfit
+ product_id: product_1
+categories_products_2:
+ category_id: Food
+ product_id: product_2
diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/fixtures/order_items.yml b/test/fixtures/order_items.yml
new file mode 100644
index 0000000000..ad676edc03
--- /dev/null
+++ b/test/fixtures/order_items.yml
@@ -0,0 +1,50 @@
+order_item_1:
+ quantity: 1
+ is_shipped: false
+ product: product_1
+ order: order_1
+order_item_2:
+ quantity: 2
+ is_shipped: false
+ product: product_2
+ order: order_2
+order_item_3:
+ quantity: 3
+ is_shipped: false
+ product: product_3
+ order: order_2
+order_item_4:
+ quantity: 4
+ is_shipped: false
+ product: product_4
+ order: order_3
+order_item_5:
+ quantity: 5
+ is_shipped: false
+ product: product_5
+ order: order_3
+order_item_6:
+ quantity: 6
+ is_shipped: false
+ product: product_6
+ order: order_3
+order_item_7:
+ quantity: 7
+ is_shipped: false
+ product: product_7
+ order: order_4
+order_item_8:
+ quantity: 8
+ is_shipped: false
+ product: product_8
+ order: order_4
+order_item_9:
+ quantity: 9
+ is_shipped: false
+ product: product_9
+ order: order_4
+order_item_10:
+ quantity: 10
+ is_shipped: false
+ product: product_10
+ order: order_4
diff --git a/test/fixtures/orders.yml b/test/fixtures/orders.yml
new file mode 100644
index 0000000000..a1ab15160c
--- /dev/null
+++ b/test/fixtures/orders.yml
@@ -0,0 +1,65 @@
+order_1:
+ status: pending
+ name: name_1
+ email: customer_1@test.com
+ street_address: street_address_1
+ city: city_1
+ state: state_1
+ zip: 11111
+ name_cc: name_cc_1
+ credit_card: 1234123412341111
+ expiry: 2018-12-31
+ ccv: 111
+ billing_zip: 11111
+order_2:
+ status: complete
+ name: name_2
+ email: customer_2@test.com
+ street_address: street_address_2
+ city: city_2
+ state: state_2
+ zip: 22222
+ name_cc: name_cc_2
+ credit_card: 1234123412342222
+ expiry: 2018-12-31
+ ccv: 222
+ billing_zip: 22222
+order_3:
+ status: paid
+ name: name_3
+ email: customer_3@test.com
+ street_address: street_address_3
+ city: city_3
+ state: state_3
+ zip: 33333
+ name_cc: name_cc_3
+ credit_card: 1234123412343333
+ expiry: 2018-12-31
+ ccv: 333
+ billing_zip: 33333
+order_4:
+ status: pending
+ name: name_4
+ email: customer_4@test.com
+ street_address: street_address_4
+ city: city_4
+ state: state_4
+ zip: 44444
+ name_cc: name_cc_4
+ credit_card: 1234123412344444
+ expiry: 2018-12-31
+ ccv: 444
+ billing_zip: 44444
+order_5:
+ status: pending
+ name: name_5
+ email: customer_5@test.com
+ street_address: street_address_5
+ city: city_5
+ state: state_5
+ zip: 55555
+ name_cc: name_cc_5
+ credit_card: 1234123412345555
+ expiry: 2018-12-31
+ ccv: 555
+ billing_zip: 55555
diff --git a/test/fixtures/products.yml b/test/fixtures/products.yml
new file mode 100644
index 0000000000..cc74fa926e
--- /dev/null
+++ b/test/fixtures/products.yml
@@ -0,0 +1,80 @@
+product_1:
+ name: product_1
+ is_active: true
+ description: description_1
+ price: 1
+ photo_url: https://placebear.com/
+ stock: 1
+ user: user_1
+product_2:
+ name: product_2
+ is_active: true
+ description: description_2
+ price: 2
+ photo_url: https://placebear.com/
+ stock: 2
+ user: user_2
+product_3:
+ name: product_3
+ is_active: true
+ description: description_3
+ price: 3
+ photo_url: https://placebear.com/
+ stock: 3
+ user: user_2
+product_4:
+ name: product_4
+ is_active: true
+ description: description_4
+ price: 4
+ photo_url: https://placebear.com/
+ stock: 4
+ user: user_3
+product_5:
+ name: product_5
+ is_active: true
+ description: description_5
+ price: 5
+ photo_url: https://placebear.com/
+ stock: 5
+ user: user_3
+product_6:
+ name: product_6
+ is_active: true
+ description: description_6
+ price: 6
+ photo_url: https://placebear.com/
+ stock: 6
+ user: user_3
+product_7:
+ name: product_7
+ is_active: true
+ description: description_7
+ price: 7
+ photo_url: https://placebear.com/
+ stock: 7
+ user: user_4
+product_8:
+ name: product_8
+ is_active: true
+ description: description_8
+ price: 8
+ photo_url: https://placebear.com/
+ stock: 8
+ user: user_4
+product_9:
+ name: product_9
+ is_active: true
+ description: description_9
+ price: 9
+ photo_url: https://placebear.com/
+ stock: 9
+ user: user_4
+product_10:
+ name: product_10
+ is_active: true
+ description: description_10
+ price: 10
+ photo_url: https://placebear.com/
+ stock: 10
+ user: user_4
diff --git a/test/fixtures/reviews.yml b/test/fixtures/reviews.yml
new file mode 100644
index 0000000000..39234e4f34
--- /dev/null
+++ b/test/fixtures/reviews.yml
@@ -0,0 +1,40 @@
+review_1:
+ rating: 1
+ content: content_1
+ product: product_1
+review_2:
+ rating: 2
+ content: content_2
+ product: product_2
+review_3:
+ rating: 3
+ content: content_3
+ product: product_3
+review_4:
+ rating: 4
+ content: content_4
+ product: product_4
+review_5:
+ rating: 5
+ content: content_5
+ product: product_5
+review_6:
+ rating: 1
+ content: content_6
+ product: product_6
+review_7:
+ rating: 2
+ content: content_7
+ product: product_7
+review_8:
+ rating: 3
+ content: content_8
+ product: product_8
+review_9:
+ rating: 4
+ content: content_9
+ product: product_9
+review_10:
+ rating: 5
+ content: content_10
+ product: product_10
diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml
new file mode 100644
index 0000000000..aa2e99fac5
--- /dev/null
+++ b/test/fixtures/users.yml
@@ -0,0 +1,20 @@
+user_1:
+ username: username_1
+ email: user_1@test.com
+ uid: 11111111
+ provider: github
+user_2:
+ username: username_2
+ email: user_2@test.com
+ uid: 22222222
+ provider: github
+user_3:
+ username: username_3
+ email: user_3@test.com
+ uid: 33333333
+ provider: github
+user_4:
+ username: username_4
+ email: user_4@test.com
+ uid: 44444444
+ provider: github
diff --git a/test/helpers/.keep b/test/helpers/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/integration/.keep b/test/integration/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/mailers/.keep b/test/mailers/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/models/.keep b/test/models/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/models/category_test.rb b/test/models/category_test.rb
new file mode 100644
index 0000000000..2323ba6003
--- /dev/null
+++ b/test/models/category_test.rb
@@ -0,0 +1,61 @@
+require "test_helper"
+
+describe Category do
+ let(:category) { Category.new }
+ let(:category1) { categories(:category_1) }
+
+ describe "relationships" do
+ it "has list of products" do
+ category1.products.each do |product|
+ product.must_be_instance_of Product
+ product.categories.must_be_instance_of Array
+ product.categories.must_include category1
+ end
+ end
+
+ it "belongs to many products" do
+ Category.first.products << products(:product_1)
+ Category.first.products << products(:product_2)
+ Category.last.products << products(:product_1)
+ Category.first.products.must_equal [products(:product_1),products(:product_2)]
+ Category.last.products.must_equal [products(:product_1)]
+ end
+ end
+
+ describe "validations" do
+ it "has validation for empty parameters" do
+ category.valid?.must_equal false
+ end
+
+ it "has validation for name presence" do
+ category1.name = nil
+ category1.valid?.must_equal false
+ category1.errors.messages.must_include :name
+
+ category1.name = ""
+ category1.valid?.must_equal false
+ category1.errors.messages.must_include :name
+ end
+
+ it "has validation for uniqueness" do
+ category = Category.new(name: category1.name)
+
+ category.valid?.must_equal false
+ category.errors.messages.must_include :name
+ end
+
+ it "has validation for case insensitive uniqueness" do
+ category = Category.new(name: category1.name.upcase)
+ category.valid?.must_equal false
+ category.errors.messages.must_include :name
+
+ category = Category.new(name: category1.name.downcase)
+ category.valid?.must_equal false
+ category.errors.messages.must_include :name
+ end
+
+ it "has validatons" do
+ category1.valid?.must_equal true
+ end
+ end
+end
diff --git a/test/models/order_item_test.rb b/test/models/order_item_test.rb
new file mode 100644
index 0000000000..6f6dbe5e1f
--- /dev/null
+++ b/test/models/order_item_test.rb
@@ -0,0 +1,79 @@
+require "test_helper"
+
+describe OrderItem do
+ let(:order_item) { OrderItem.new }
+ let(:oi) { order_items(:order_item_1) }
+
+
+ describe "relations" do
+ it "has an order" do
+ oi.must_respond_to :order
+ oi.order.must_be_kind_of Order
+ oi.order.must_equal orders(:order_1)
+ end
+ it "has a product" do
+ oi.must_respond_to :product
+ oi.product.must_be_kind_of Product
+ oi.product.must_equal products(:product_1)
+ end
+
+ it "changes in order_id and product_id reflects in order and product" do
+ oi.order_id = orders(:order_2).id
+ oi.order.must_equal orders(:order_2)
+
+ oi.product_id = products(:product_2).id
+ oi.product.must_equal products(:product_2)
+ end
+ end
+
+ describe "validations" do
+
+ it "has validation for empty parameters" do
+ order_item.valid?.must_equal false
+ end
+
+ it "has validation for quantity presence" do
+ oi.quantity = nil
+ oi.valid?.must_equal false
+ oi.errors.messages.must_include :quantity
+
+ oi.quantity = ""
+ oi.valid?.must_equal false
+ oi.errors.messages.must_include :quantity
+ end
+
+ it "has quantity as integer, greater than 0" do
+ ["one", -1, 0].each {|element|
+ order_item.quantity = element
+ order_item.valid?.must_equal false
+ }
+ end
+
+ it "checks product is active" do
+ oi.product.is_active = false
+ oi.valid?.must_equal false
+ oi.errors.messages.must_include :is_active
+ end
+
+ it "checks quantity is available" do
+ oi.product.stock = 0
+ oi.valid?.must_equal false
+ oi.errors.messages.must_include :stock
+
+ order_items(:order_item_6).product.stock = 2
+ order_items(:order_item_6).valid?.must_equal false
+ order_items(:order_item_6).errors.messages.must_include :stock
+ end
+
+ it "checks subtotal" do
+ oi.product.price = 299
+ oi.subtotal.must_equal 2.99
+ oi.product.price = 29
+ oi.subtotal.must_equal 0.29
+ end
+
+ it "must be valid" do
+ oi.valid?.must_equal true
+ end
+ end
+end
diff --git a/test/models/order_test.rb b/test/models/order_test.rb
new file mode 100644
index 0000000000..7d532426f1
--- /dev/null
+++ b/test/models/order_test.rb
@@ -0,0 +1,224 @@
+require "test_helper"
+
+describe Order do
+ let(:order) { Order.new }
+ let(:o) { orders(:order_2) } #status complete
+ let(:o1) { orders(:order_1) } #staus pending
+ let(:o3) { orders(:order_3) } #status paid
+
+ describe "relations" do
+ it "has a list of orderitems" do
+ o.order_items.each do |item|
+ item.must_be_instance_of OrderItem
+ item.order.must_be_instance_of Order
+ item.order.must_equal o
+ end
+ end
+ end
+
+ describe "validations" do
+ it "has validation for empty parameters" do
+ order.valid?.must_equal false
+ end
+
+ it "has validation for status presence" do
+ o.status = nil
+ o.valid?.must_equal false
+ o.errors.messages.must_include :status
+
+ o.status = ""
+ o.valid?.must_equal false
+ o.errors.messages.must_include :status
+ end
+
+ it "has validation for name presence" do
+ o.name = nil
+ o.valid?.must_equal false
+ o.errors.messages.must_include :name
+
+ o.name = ""
+ o.valid?.must_equal false
+ o.errors.messages.must_include :name
+ end
+
+ it "has validation for name length" do
+ o.name = "ni"
+ o.valid?.must_equal false
+ o.errors.messages.must_include :name
+ end
+
+ it "has validation for email presence" do
+ o.email = nil
+ o.valid?.must_equal false
+ o.errors.messages.must_include :email
+
+ o.email = ""
+ o.valid?.must_equal false
+ o.errors.messages.must_include :email
+ end
+
+ it "has validation for email format" do
+ o.email = "bademail@exmple_nodot"
+ o.valid?.must_equal false
+ o.errors.messages.must_include :email
+ end
+
+ it "has validation for street_address presence" do
+ o.street_address = nil
+ o.valid?.must_equal false
+ o.errors.messages.must_include :street_address
+
+ o.street_address = ""
+ o.valid?.must_equal false
+ o.errors.messages.must_include :street_address
+ end
+
+ it "has validation for city presence" do
+ o.city = nil
+ o.valid?.must_equal false
+ o.errors.messages.must_include :city
+
+ o.city = ""
+ o.valid?.must_equal false
+ o.errors.messages.must_include :city
+ end
+
+ it "has validation for state presence" do
+ o.state = nil
+ o.valid?.must_equal false
+ o.errors.messages.must_include :state
+
+ o.state = ""
+ o.valid?.must_equal false
+ o.errors.messages.must_include :state
+ end
+
+ it "has validation for zip presence" do
+ o.zip = nil
+ o.valid?.must_equal false
+ o.errors.messages.must_include :zip
+
+ o.zip = ""
+ o.valid?.must_equal false
+ o.errors.messages.must_include :zip
+ end
+
+ it "has validation for zip length" do
+ [12, 1, 123456].each {|element|
+ o.zip = element
+ o.valid?.must_equal false
+ }
+ end
+
+ it "has validation for name_cc presence" do
+ o.name_cc = nil
+ o.valid?.must_equal false
+ o.errors.messages.must_include :name_cc
+
+ o.name_cc = ""
+ o.valid?.must_equal false
+ o.errors.messages.must_include :name_cc
+ end
+
+ it "has validation for credit_card presence" do
+ o.credit_card = nil
+ o.valid?.must_equal false
+ o.errors.messages.must_include :credit_card
+
+ o.credit_card = ""
+ o.valid?.must_equal false
+ o.errors.messages.must_include :credit_card
+ end
+
+ it "has validation for credit_card length" do
+ [1234567890113, 12345678901112131420].each {|element|
+ o.credit_card = element
+ o.valid?.must_equal false
+ }
+ end
+
+ it "has validation for expiry presence" do
+ o.expiry = nil
+ o.valid?.must_equal false
+ o.errors.messages.must_include :expiry
+
+ o.expiry = ""
+ o.valid?.must_equal false
+ o.errors.messages.must_include :expiry
+ end
+
+ it "has validation for ccv presence" do
+ o.ccv = nil
+ o.valid?.must_equal false
+ o.errors.messages.must_include :ccv
+
+ o.ccv = ""
+ o.valid?.must_equal false
+ o.errors.messages.must_include :ccv
+ end
+
+ it "has validation for ccv length" do
+ [12, 1, 12345].each {|element|
+ o.ccv = element
+ o.valid?.must_equal false
+ }
+ end
+ it "has validation for billing_zip presence" do
+ o.billing_zip = nil
+ o.valid?.must_equal false
+ o.errors.messages.must_include :billing_zip
+
+ o.billing_zip = ""
+ o.valid?.must_equal false
+ o.errors.messages.must_include :billing_zip
+ end
+
+ it "has validation for billing_zip length" do
+ [12, 1, 123456].each {|element|
+ o.billing_zip = element
+ o.valid?.must_equal false
+ }
+ end
+
+ it "has no validation when items are in cart mean status pending" do
+ o.name = ""
+ o.state = nil
+ o.billing_zip = ""
+ o1.valid?.must_equal true
+ end
+
+ it "must be valid" do
+ o.valid?.must_equal true
+ end
+ end
+
+ describe "methods" do
+ it "computes total cost" do
+ o.total_cost.must_equal 0.13
+ end
+
+ it "computes last 4 digits of credit card" do
+ o.credit_card_last_digits.must_equal "2222"
+ o1.credit_card_last_digits.must_equal "1111"
+ end
+
+ it "can cancel" do
+ o1.status = "paid"
+ o1.can_cancel?.must_equal true
+ end
+
+ it "confirms updates status and reduces product stock" do
+ o1.confirm
+ o1.status.must_equal "paid"
+ products(:product_1).stock.must_equal 0
+ end
+
+ it "cancels updates status and increases product stock" do
+ o3.cancel
+ o3.status.must_equal "cancelled"
+ products(:product_4).stock.must_equal 8
+ products(:product_5).stock.must_equal 10
+ products(:product_6).stock.must_equal 12
+ end
+ end
+end
diff --git a/test/models/product_test.rb b/test/models/product_test.rb
new file mode 100644
index 0000000000..4db0db5461
--- /dev/null
+++ b/test/models/product_test.rb
@@ -0,0 +1,158 @@
+require "test_helper"
+
+describe Product do
+ let(:product) { Product.new }
+ let(:p) { products(:product_1)}
+
+ describe "relations" do
+ it "has an user" do
+ p.must_respond_to :user
+ p.user.must_be_kind_of User
+ p.user.must_equal users(:user_1)
+ end
+
+ it "has a list of categories" do
+ p.categories.each do |cat|
+ cat.must_be_instance_of Category
+ cat.products.must_be_instance_of Array
+ cat.products.must_include p
+ end
+ end
+
+ it "belongs to many categories" do
+ Product.first.categories << categories(:category_1)
+ Product.first.categories << categories(:category_2)
+ Product.last.categories << categories(:category_1)
+ Product.first.categories.must_equal [categories(:category_1),categories(:category_2)]
+ Product.last.categories.must_equal [categories(:category_1)]
+ end
+
+ it "has a list of reviews " do
+ p.reviews.each do |review|
+ review.must_be_instance_of Review
+ review.product.must_be_instance_of Product
+ review.product.must_equal p
+ end
+ end
+
+ it "has a list of orderitems " do
+ p.order_items.each do |order_item|
+ order_item.must_be_instance_of OrderItem
+ order_item.product.must_be_instance_of Product
+ order_item.product.must_equal p
+ end
+ end
+
+ end
+
+ describe "validations" do
+ it "has validation for empty parameters" do
+ product.valid?.must_equal false
+ end
+
+ it "has validation for name presence" do
+ p.name = nil
+ p.valid?.must_equal false
+ p.errors.messages.must_include :name
+
+ p.name = ""
+ p.valid?.must_equal false
+ p.errors.messages.must_include :name
+ end
+
+ it "has validation for name uniqueness" do
+ p1 = Product.new({ name: p.name, is_active: true, description: "description_1", price: 1, photo_url: nil, stock: 1,user: users(:user_1) })
+ p1.valid?.must_equal false
+ p1.errors.messages.must_include :name
+ end
+
+ it "has validation for name case insensitive uniqueness" do
+ p1 = Product.new({ name: p.name.downcase, is_active: true, description: "description_1", price: 1, photo_url: nil, stock: 1,user: users(:user_1) })
+ p1.valid?.must_equal false
+ p1.errors.messages.must_include :name
+
+ p1 = Product.new({ name: p.name.upcase, is_active: true, description: "description_1", price: 1, photo_url: nil, stock: 1,user: users(:user_1) })
+ p1.valid?.must_equal false
+ p1.errors.messages.must_include :name
+ end
+
+ it "has validation for price presence" do
+ p.price = nil
+ p.valid?.must_equal false
+ p.errors.messages.must_include :price
+
+ p.price = ""
+ p.valid?.must_equal false
+ p.errors.messages.must_include :price
+ end
+
+ it "has price as integer, greater than 0" do
+ ["one", -1, 0].each {|element|
+ p.price = element
+ p.valid?.must_equal false
+ }
+ end
+
+ it "has validation for stock presence" do
+ p.stock = nil
+ p.valid?.must_equal false
+ p.errors.messages.must_include :stock
+
+ p.stock = ""
+ p.valid?.must_equal false
+ p.errors.messages.must_include :stock
+ end
+
+ it "has stock as integer, greater than or equal to 0" do
+ ["one", -1].each {|element|
+ p.stock = element
+ p.valid?.must_equal false
+ }
+ end
+
+ it "must be valid" do
+ p.valid?.must_equal true
+ end
+
+ it "computes average rating" do
+ r = Review.create({
+ rating: 5, content: "content_5", product: p })
+ p.average_rating.must_equal 3
+ end
+
+ it "computes average rating as 0 when no reviews" do
+ product.average_rating.must_equal 0
+ end
+
+ it "toggle_is_active" do
+ p = Product.new
+ p.toggle_is_active
+ p.is_active.must_equal false
+
+ p.toggle_is_active
+ p.is_active.must_equal true
+ end
+
+ it "gives top sellers" do
+ Product.top_sellers.must_be_instance_of Array
+ Product.top_sellers.count.must_equal 5
+ Product.top_sellers.must_equal [products(:product_10), products(:product_9), products(:product_8), products(:product_7), products(:product_6)]
+ end
+
+ it "gives top sellers by count" do
+ Product.top_sellers(3).must_be_instance_of Array
+ Product.top_sellers(3).count.must_equal 3
+ Product.top_sellers(3).must_equal [products(:product_10), products(:product_9), products(:product_8)]
+ end
+
+ it "search products" do
+ p = Product.search("product_1")
+ p.count.must_equal 2
+ p = Product.search("product_2")
+ p.count.must_equal 1
+ p = Product.search("dog")
+ p.must_equal []
+ end
+
+ end
+ end
diff --git a/test/models/review_test.rb b/test/models/review_test.rb
new file mode 100644
index 0000000000..d4ae4b91f8
--- /dev/null
+++ b/test/models/review_test.rb
@@ -0,0 +1,48 @@
+require "test_helper"
+
+describe Review do
+ let(:review) { Review.new }
+ let(:r) { reviews(:review_1) }
+
+ describe "relationships" do
+ it "has a product" do
+ r.must_respond_to :product
+ r.product.must_be_kind_of Product
+ r.product.must_equal products(:product_1)
+ end
+ end
+
+ describe "validations" do
+ it "has validation for empty parameters" do
+ review.valid?.must_equal false
+ end
+
+ it "has validation for rating presence" do
+ r.rating = nil
+ r.valid?.must_equal false
+ r.errors.messages.must_include :rating
+
+ r.rating = ""
+ r.valid?.must_equal false
+ r.errors.messages.must_include :rating
+ end
+
+ it "has rating as integer, greater than 0" do
+ ["one", -1, 0].each {|element|
+ r.rating = element
+ r.valid?.must_equal false
+ }
+ end
+
+ it "has validation for rating to be between 1..5" do
+ [-1,0,6,22].each {|element|
+ r = Review.new({rating: element, content: "content_1", product: products(:product_1)})
+ r.valid?.must_equal false
+ }
+ end
+
+ it "must be valid" do
+ r.valid?.must_equal true
+ end
+ end
+end
diff --git a/test/models/user_test.rb b/test/models/user_test.rb
new file mode 100644
index 0000000000..e5574f6c5d
--- /dev/null
+++ b/test/models/user_test.rb
@@ -0,0 +1,141 @@
+require "test_helper"
+
+describe User do
+ let(:user) { User.new }
+ let(:u) { users(:user_1) }
+
+ describe "relations" do
+ it "has a list of products" do
+ u.products.each do |product|
+ product.must_be_instance_of Product
+ product.user.must_be_instance_of User
+ product.user.must_equal u
+ end
+ end
+
+ it "has many orderitems through products" do
+ oi = u.order_items
+ orderitems = []
+ oi.each{|item|
+ item.must_be_instance_of OrderItem
+ orderitems << item
+ }
+ u.products.map{|p| p.order_items}.flatten.must_equal orderitems
+ end
+
+ it "returns an empty array if user has no product" do
+ user.products.must_equal []
+ end
+ end
+
+ describe "validations" do
+ it "has validation for empty parameters" do
+ user.valid?.must_equal false
+ end
+
+ it "has validation for username presence" do
+ u.username = nil
+ u.valid?.must_equal false
+ u.errors.messages.must_include :username
+
+ u.username = ""
+ u.valid?.must_equal false
+ u.errors.messages.must_include :username
+ end
+
+ it "has validation for username uniqueness" do
+ u1 = User.new({ username: u.username, email: "aaa@example.com", uid: "11111111", provider: "github" })
+ u1.valid?.must_equal false
+ u1.errors.messages.must_include :username
+ end
+
+ it "has validation for email presence" do
+ u.email = nil
+ u.valid?.must_equal false
+ u.errors.messages.must_include :email
+
+ u.email = ""
+ u.valid?.must_equal false
+ u.errors.messages.must_include :email
+
+ end
+
+ it "has validation for email uniqueness" do
+ u1 = User.new({ username: "test", email: u.email, uid: 11111111, provider: "github" })
+ u1.valid?.must_equal false
+ u1.errors.messages.must_include :email
+ end
+
+ it "has validation for email format" do
+ u1 = User.new({ username: "test", email: "bad email", uid: 11111111, provider: "github" })
+ u1.valid?.must_equal false
+ u1.errors.messages.must_include :email
+
+ u1 = User.new({ username: "test", email: "bad_email@with_no_dot", uid: "11111111", provider: "github" })
+ u1.valid?.must_equal false
+ u1.errors.messages.must_include :email
+ end
+
+ it "must be valid" do
+ u.valid?.must_equal true
+ end
+ end
+
+ describe "methods" do
+ it "builds user from auth_hash" do
+ auth_hash = {
+ info: { nickname: "test", email: "test@example.com" },
+ uid: "11111111",
+ provider: "github"
+ }
+ user = User.build_from_github(auth_hash)
+
+ user.username.must_equal "test"
+ user.email.must_equal "test@example.com"
+ user.uid.must_equal "11111111"
+ user.provider.must_equal "github"
+ end
+
+ it "can create user" do
+ u1 = User.new({ username: "Charles", email: "charles@example.com", uid: "11111111", provider: "github" })
+ u1.save.must_equal true
+ end
+
+ it "computes number of orders" do
+ u.num_orders.must_equal 1
+
+ # user with no order yet
+ u1 = User.create({ username: "test", email: "test@example.com", uid: "11111111", provider: "github" })
+ u1.num_orders.must_equal 0
+ end
+
+ it "computes number of orders by status" do
+ u.num_orders_by_status("pending").must_equal 1
+ u.num_orders_by_status("paid").must_equal 0
+ end
+
+ it "computes total revenue" do
+ u2 = users(:user_2)
+ u2.total_revenue.must_equal 0.13
+ end
+
+ it "computes total revenue by status" do
+ u.total_revenue_by_status("pending").must_equal 0.01
+ u.total_revenue_by_status("complete").must_equal 0
+ end
+
+ it "lists order by status" do
+ u.list_orders_by_status("pending").must_be_instance_of Array
+ u.list_orders_by_status("pending").must_include orders(:order_1)
+ u.list_orders_by_status("pending").count.must_equal 1
+ user.list_orders_by_status("pending").must_equal []
+ end
+
+ it "lists all orders" do
+ u.list_all_orders.must_be_instance_of Array
+ u.list_all_orders.must_include orders(:order_1)
+ u.list_all_orders.count.must_equal 1
+ user.list_all_orders.must_equal []
+ end
+ end
+end
diff --git a/test/system/.keep b/test/system/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/test_helper.rb b/test/test_helper.rb
new file mode 100644
index 0000000000..46c0eedfa0
--- /dev/null
+++ b/test/test_helper.rb
@@ -0,0 +1,48 @@
+require 'simplecov'
+SimpleCov.start 'rails'
+ENV["RAILS_ENV"] = "test"
+require File.expand_path("../../config/environment", __FILE__)
+require "rails/test_help"
+require "minitest/rails"
+require "minitest/reporters" # for Colorized output
+
+# For colorful output!
+Minitest::Reporters.use!(
+ Minitest::Reporters::SpecReporter.new,
+ ENV,
+ Minitest.backtrace_filter
+)
+
+
+# To add Capybara feature tests add `gem "minitest-rails-capybara"`
+# to the test group in the Gemfile and uncomment the following:
+# require "minitest/rails/capybara"
+
+# Uncomment for awesome colorful output
+# require "minitest/pride"
+
+class ActiveSupport::TestCase
+ # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
+ fixtures :all
+
+ def setup
+ OmniAuth.config.test_mode = true
+ end
+
+ def mock_auth_hash(user)
+ return {
+ provider: user.provider,
+ uid: user.uid,
+ info: {
+ email: user.email,
+ nickname: user.username
+ }
+ }
+ end
+
+ def login(user)
+ OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new(mock_auth_hash(user))
+ get auth_callback_path(:github)
+ end
+
+end
diff --git a/tmp/.keep b/tmp/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/vendor/.keep b/vendor/.keep
new file mode 100644
index 0000000000..e69de29bb2