diff --git a/.gitignore b/.gitignore index 6db3c9a5bc..a1b0660c14 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,17 @@ # Ignore bundler config. /.bundle +# Ignore the default SQLite database. +/db/*.sqlite3 +/db/*.sqlite3-journal + # Ignore all logfiles and tempfiles. /log/* !/log/.keep /tmp +/coverage + +.env +#DS Store .DS_Store diff --git a/.ruby-gemset b/.ruby-gemset deleted file mode 100644 index d5c660823f..0000000000 --- a/.ruby-gemset +++ /dev/null @@ -1 +0,0 @@ -betsy diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index 2bf1c1ccf3..0000000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -2.3.1 diff --git a/Gemfile b/Gemfile index c69f4ddde9..99cd029d6c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,8 @@ source 'https://rubygems.org' -ruby '2.3.1' + # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '4.2.6' -# Use postgresql as the database for Active Record -# gem 'pg', '~> 0.15' # Use SCSS for stylesheets gem 'sass-rails', '~> 5.0' # Use Uglifier as compressor for JavaScript assets @@ -22,7 +20,8 @@ gem 'turbolinks' gem 'jbuilder', '~> 2.0' # bundle exec rake doc:rails generates the API under doc/api. gem 'sdoc', '~> 0.4.0', group: :doc - +gem "omniauth" +gem "omniauth-github" # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' @@ -35,12 +34,24 @@ gem 'sdoc', '~> 0.4.0', group: :doc group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug' + gem 'simplecov' + gem 'sqlite3' end group :development do # Access an IRB console on exception pages or by using <%= console %> in views gem 'web-console', '~> 2.0' - # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' + gem 'better_errors' + gem 'dotenv-rails' +end + +group :test do + gem 'minitest-reporters' +end + +group :production do + gem 'pg' + gem 'rails_12factor' end diff --git a/Gemfile.lock b/Gemfile.lock index 20975578b8..542b747926 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -36,11 +36,17 @@ GEM minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) + ansi (1.5.0) arel (6.0.3) + better_errors (2.1.1) + coderay (>= 1.0.0) + erubis (>= 2.6.6) + rack (>= 0.9.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) builder (3.2.2) - byebug (8.2.5) + byebug (9.0.6) + coderay (1.1.1) coffee-rails (4.1.1) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.1.x) @@ -48,33 +54,65 @@ GEM coffee-script-source execjs coffee-script-source (1.10.0) - concurrent-ruby (1.0.1) + concurrent-ruby (1.0.2) debug_inspector (0.0.2) + docile (1.1.5) + dotenv (2.1.1) + dotenv-rails (2.1.1) + dotenv (= 2.1.1) + railties (>= 4.0, < 5.1) erubis (2.7.0) - execjs (2.6.0) - globalid (0.3.6) + execjs (2.7.0) + faraday (0.9.2) + multipart-post (>= 1.2, < 3) + globalid (0.3.7) activesupport (>= 4.1.0) + hashie (3.4.6) i18n (0.7.0) - jbuilder (2.4.1) + jbuilder (2.6.0) activesupport (>= 3.0.0, < 5.1) multi_json (~> 1.2) - jquery-rails (4.1.1) + jquery-rails (4.2.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (1.8.3) + jwt (1.5.6) loofah (2.0.3) nokogiri (>= 1.5.9) mail (2.6.4) mime-types (>= 1.16, < 4) - mime-types (3.0) + mime-types (3.1) mime-types-data (~> 3.2015) - mime-types-data (3.2016.0221) - mini_portile2 (2.0.0) - minitest (5.8.4) - multi_json (1.11.3) - nokogiri (1.6.7.2) - mini_portile2 (~> 2.0.0.rc2) + mime-types-data (3.2016.0521) + mini_portile2 (2.1.0) + minitest (5.9.1) + minitest-reporters (1.1.11) + ansi + builder + minitest (>= 5.0) + ruby-progressbar + multi_json (1.12.1) + multi_xml (0.5.5) + multipart-post (2.0.0) + nokogiri (1.6.8.1) + mini_portile2 (~> 2.1.0) + oauth2 (1.2.0) + faraday (>= 0.8, < 0.10) + jwt (~> 1.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (>= 1.2, < 3) + omniauth (1.3.1) + hashie (>= 1.2, < 4) + rack (>= 1.0, < 3) + omniauth-github (1.1.2) + omniauth (~> 1.0) + omniauth-oauth2 (~> 1.1) + omniauth-oauth2 (1.4.0) + oauth2 (~> 1.0) + omniauth (~> 1.2) + pg (0.19.0) rack (1.6.4) rack-test (0.6.3) rack (>= 1.0) @@ -97,40 +135,54 @@ GEM rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) + rails_12factor (0.0.3) + rails_serve_static_assets + rails_stdout_logging + rails_serve_static_assets (0.0.5) + rails_stdout_logging (0.0.5) railties (4.2.6) actionpack (= 4.2.6) activesupport (= 4.2.6) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (11.1.2) + rake (11.3.0) rdoc (4.2.2) json (~> 1.4) + ruby-progressbar (1.8.1) sass (3.4.22) - sass-rails (5.0.4) - railties (>= 4.0.0, < 5.0) + sass-rails (5.0.6) + railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) - sdoc (0.4.1) + sdoc (0.4.2) json (~> 1.7, >= 1.7.7) rdoc (~> 4.0) - spring (1.7.1) - sprockets (3.6.0) + simplecov (0.12.0) + docile (~> 1.1.0) + json (>= 1.8, < 3) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.0) + spring (2.0.0) + activesupport (>= 4.2) + sprockets (3.7.0) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.0.4) + sprockets-rails (3.2.0) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) + sqlite3 (1.3.12) thor (0.19.1) thread_safe (0.3.5) - tilt (2.0.2) - turbolinks (2.5.3) - coffee-rails + tilt (2.0.5) + turbolinks (5.0.1) + turbolinks-source (~> 5) + turbolinks-source (5.0.0) tzinfo (1.2.2) thread_safe (~> 0.1) - uglifier (3.0.0) + uglifier (3.0.2) execjs (>= 0.3.0, < 3) web-console (2.3.0) activemodel (>= 4.0) @@ -142,20 +194,26 @@ PLATFORMS ruby DEPENDENCIES + better_errors byebug coffee-rails (~> 4.1.0) + dotenv-rails jbuilder (~> 2.0) jquery-rails + minitest-reporters + omniauth + omniauth-github + pg rails (= 4.2.6) + rails_12factor sass-rails (~> 5.0) sdoc (~> 0.4.0) + simplecov spring + sqlite3 turbolinks uglifier (>= 1.3.0) web-console (~> 2.0) -RUBY VERSION - ruby 2.3.1p112 - BUNDLED WITH 1.13.5 diff --git a/app/assets/images/banana.jpg b/app/assets/images/banana.jpg new file mode 100644 index 0000000000..26b76cb9a9 Binary files /dev/null and b/app/assets/images/banana.jpg differ diff --git a/app/assets/images/bed.jpg b/app/assets/images/bed.jpg new file mode 100644 index 0000000000..d395c813a0 Binary files /dev/null and b/app/assets/images/bed.jpg differ diff --git a/app/assets/images/bridge.jpg b/app/assets/images/bridge.jpg new file mode 100644 index 0000000000..483f50b75e Binary files /dev/null and b/app/assets/images/bridge.jpg differ diff --git a/app/assets/images/bunny.jpg b/app/assets/images/bunny.jpg new file mode 100644 index 0000000000..f95c935994 Binary files /dev/null and b/app/assets/images/bunny.jpg differ diff --git a/app/assets/images/colored_pencils.jpg b/app/assets/images/colored_pencils.jpg new file mode 100644 index 0000000000..bc6331dae9 Binary files /dev/null and b/app/assets/images/colored_pencils.jpg differ diff --git a/app/assets/images/couch.jpg b/app/assets/images/couch.jpg new file mode 100644 index 0000000000..eeebd2298c Binary files /dev/null and b/app/assets/images/couch.jpg differ diff --git a/app/assets/images/horse.jpg b/app/assets/images/horse.jpg new file mode 100644 index 0000000000..b4ab9b4e13 Binary files /dev/null and b/app/assets/images/horse.jpg differ diff --git a/app/assets/images/house.gif b/app/assets/images/house.gif new file mode 100644 index 0000000000..c3ae8b006f Binary files /dev/null and b/app/assets/images/house.gif differ diff --git a/app/assets/images/jeep.jpg b/app/assets/images/jeep.jpg new file mode 100644 index 0000000000..2adf6c06cd Binary files /dev/null and b/app/assets/images/jeep.jpg differ diff --git a/app/assets/images/kfc.jpg b/app/assets/images/kfc.jpg new file mode 100644 index 0000000000..f0456eac3b Binary files /dev/null and b/app/assets/images/kfc.jpg differ diff --git a/app/assets/images/mcvalue.jpeg b/app/assets/images/mcvalue.jpeg new file mode 100644 index 0000000000..bbdcea46bb Binary files /dev/null and b/app/assets/images/mcvalue.jpeg differ diff --git a/app/assets/images/minicooper.jpg b/app/assets/images/minicooper.jpg new file mode 100644 index 0000000000..0551cd728a Binary files /dev/null and b/app/assets/images/minicooper.jpg differ diff --git a/app/assets/images/piano.png b/app/assets/images/piano.png new file mode 100644 index 0000000000..f1bd56efed Binary files /dev/null and b/app/assets/images/piano.png differ diff --git a/app/assets/images/small_fry.png b/app/assets/images/small_fry.png new file mode 100644 index 0000000000..75a093db45 Binary files /dev/null and b/app/assets/images/small_fry.png differ diff --git a/app/assets/images/small_fry_sad.png b/app/assets/images/small_fry_sad.png new file mode 100644 index 0000000000..00bf2da372 Binary files /dev/null and b/app/assets/images/small_fry_sad.png differ diff --git a/app/assets/images/squid.jpg b/app/assets/images/squid.jpg new file mode 100644 index 0000000000..ad4ba05f84 Binary files /dev/null and b/app/assets/images/squid.jpg differ diff --git a/app/assets/images/starbucks.jpg b/app/assets/images/starbucks.jpg new file mode 100644 index 0000000000..4854bbaf0e Binary files /dev/null and b/app/assets/images/starbucks.jpg differ diff --git a/app/assets/images/table.jpg b/app/assets/images/table.jpg new file mode 100644 index 0000000000..7baa061314 Binary files /dev/null and b/app/assets/images/table.jpg differ diff --git a/app/assets/images/twister.jpg b/app/assets/images/twister.jpg new file mode 100644 index 0000000000..27a09ee457 Binary files /dev/null and b/app/assets/images/twister.jpg differ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index e07c5a830f..dc9d8653d2 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -14,3 +14,26 @@ //= require jquery_ujs //= require turbolinks //= require_tree . + +// http://www.w3schools.com/howto/howto_js_filter_table.asp & http://stackoverflow.com/questions/37634776/implementing-page-specific-java-script-and-css-in-a-ruby-on-rails-4-project + +function myFunction() { + // Declare variables + var input, filter, table, tr, td, i; + input = document.getElementById("orderStatusInput"); + filter = input.value.toUpperCase(); + table = document.getElementById("merchantOrders"); + tr = table.getElementsByTagName("tr"); + + // Loop through all table rows, and hide those who don't match the search query + for (i = 0; i < tr.length; i++) { + td = tr[i].getElementsByTagName("td")[2]; + if (td) { + if (td.innerHTML.toUpperCase().indexOf(filter) > -1) { + tr[i].style.display = ""; + } else { + tr[i].style.display = "none"; + } + } + } +} diff --git a/app/assets/javascripts/categories.coffee b/app/assets/javascripts/categories.coffee new file mode 100644 index 0000000000..24f83d18bb --- /dev/null +++ b/app/assets/javascripts/categories.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/home.coffee b/app/assets/javascripts/home.coffee new file mode 100644 index 0000000000..24f83d18bb --- /dev/null +++ b/app/assets/javascripts/home.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/merchants.coffee b/app/assets/javascripts/merchants.coffee new file mode 100644 index 0000000000..24f83d18bb --- /dev/null +++ b/app/assets/javascripts/merchants.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/order_items.coffee b/app/assets/javascripts/order_items.coffee new file mode 100644 index 0000000000..24f83d18bb --- /dev/null +++ b/app/assets/javascripts/order_items.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/orders.coffee b/app/assets/javascripts/orders.coffee new file mode 100644 index 0000000000..24f83d18bb --- /dev/null +++ b/app/assets/javascripts/orders.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/products.coffee b/app/assets/javascripts/products.coffee new file mode 100644 index 0000000000..24f83d18bb --- /dev/null +++ b/app/assets/javascripts/products.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/reviews.coffee b/app/assets/javascripts/reviews.coffee new file mode 100644 index 0000000000..24f83d18bb --- /dev/null +++ b/app/assets/javascripts/reviews.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/sessions.coffee b/app/assets/javascripts/sessions.coffee new file mode 100644 index 0000000000..24f83d18bb --- /dev/null +++ b/app/assets/javascripts/sessions.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index f9cd5b3483..cba2365de8 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -1,15 +1,172 @@ /* - * 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, vendor/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 styles - * defined in the other CSS/SCSS files in this directory. It is generally better to create a new - * file per style scope. - * - *= require_tree . - *= require_self - */ +* 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, vendor/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 styles +* defined in the other CSS/SCSS files in this directory. It is generally better to create a new +* file per style scope. +* +*= require_tree . +*= require_self +*/ + +/***** BODY *****/ +body { + background-color: #D5DED9; + cursor: url("/assets/small_fry.png"); + min-height:100%; +} + +/****** Header *****/ +header { + background-color: #99B2B7; +} + +nav { + background-color: #99B2B7; +} + +.weebay { + font-family: 'Gloria Hallelujah', cursive; + font-size: 5em; + text-align: center; +} +.weebay a { + color: #7c0000; +} +.weebay a:hover { + color: #D9CEB2; +} + +.motto-emphasize { + font-family: 'Gloria Hallelujah', cursive; + font-size: .5em; + color: #9c0505; +} + +.motto { + font-family: 'Patrick Hand SC', cursive; + text-align: center; +} + +/*This is the nav bar links*/ +header li { + font-size: 2em; + padding: .5em; +} + +/***** FONTS *****/ +/*Fancier fonts*/ +h1, h2, h3, h4, h5, h6, th, li { + font-family: 'Patrick Hand SC', cursive; +} + +/*Basic fonts*/ +p, td { + font-family: 'Yantramanav', sans-serif; + font-size: 1.5em; +} + +/***** IMAGES *****/ +/*To keep our images semi-normal-sized all around*/ +img { + height: 200px; +} + +.smaller-fry { + height: 30px; +} + +.small-fry { + height: 100px; + position: absolute; + margin-left: -.2em; + margin-top: -.6em; + opacity: 0; + transition: opacity .5s ease-out; + -moz-transition: opacity .5s ease-out; + -webkit-transition: opacity .5s ease-out; + -o-transition: opacity .5s ease-out; +} + +.small-fry:hover { + opacity: 1; +} + +.sad-fry { + height: 200px; + padding-left: 2em; +} + +/***** YIELD *****/ +/*This centers the main body and makes it a little narrower than the header*/ +main { + text-align: center; + width: 70em; + margin: 0 auto; + background-color: #D5DED9; + margin-top: 5em; + margin-bottom: 5em; +} + +/*Table formatting*/ +table { + background-color: #D5DED9; + border: none; + border-spacing: 0; +} +table tr th { + text-align: center; + background-color: #D5DED9; + font-size: 2em; + border: none; +} + +table tr td { + background-color: #D5DED9; +} + +table tr td:first-of-type { + font-size: 200%; +} + +.button { + background-color: #7c0000; + font-family: 'Yantramanav', sans-serif; + font-size: 1.5em; +} + +.button:hover { + background-color: #D9CEB2; + color: #7c0000; +} +/***** FOOTER *****/ +footer { + /*position: absolute;*/ + height: 5em; + /*right: 0;*/ + bottom: 0; + width: 100%; + padding: 1rem; + background-color: #99B2B7; +} + +footer div p { + font-size: 1em; +} + +/***** Order Table *****/ +#orderStatusInput { + background-image: url('/css/searchicon.png'); /* Add a search icon to input */ + background-position: 10px 12px; /* Position the search icon */ + background-repeat: no-repeat; /* Do not repeat the icon image */ + width: 100%; /* Full-width */ + font-size: 16px; /* Increase font-size */ + padding: 12px 20px 12px 40px; /* Add some padding */ + border: 1px solid #ddd; /* Add a grey border */ + margin-bottom: 12px; /* Add some space below the input */ +} diff --git a/app/assets/stylesheets/categories.scss b/app/assets/stylesheets/categories.scss new file mode 100644 index 0000000000..ef1657f8c9 --- /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.min.css b/app/assets/stylesheets/foundation.min.css new file mode 100644 index 0000000000..fe291e8027 --- /dev/null +++ b/app/assets/stylesheets/foundation.min.css @@ -0,0 +1,2 @@ +@charset "UTF-8"; +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:not-allowed}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}.foundation-mq{font-family:"small=0em&medium=40em&large=64em&xlarge=75em&xxlarge=90em"}html{font-size:100%;box-sizing:border-box}*,:after,:before{box-sizing:inherit}body{padding:0;margin:0;font-family:Helvetica Neue,Helvetica,Roboto,Arial,sans-serif;font-weight:400;line-height:1.5;color:#0a0a0a;background:#fefefe;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}img{max-width:100%;height:auto;-ms-interpolation-mode:bicubic;display:inline-block;vertical-align:middle}textarea{height:auto;min-height:50px;border-radius:0}select{width:100%;border-radius:0}#map_canvas embed,#map_canvas img,#map_canvas object,.map_canvas embed,.map_canvas img,.map_canvas object,.mqa-display embed,.mqa-display img,.mqa-display object{max-width:none!important}button{-webkit-appearance:none;-moz-appearance:none;background:transparent;padding:0;border:0;border-radius:0;line-height:1}[data-whatinput=mouse] button{outline:0}.is-visible{display:block!important}.is-hidden{display:none!important}.row{max-width:75rem;margin-left:auto;margin-right:auto}.row:after,.row:before{content:' ';display:table}.row:after{clear:both}.row.collapse>.column,.row.collapse>.columns{padding-left:0;padding-right:0}.row .row{max-width:none;margin-left:-.625rem;margin-right:-.625rem}@media screen and (min-width:40em){.row .row{margin-left:-.9375rem;margin-right:-.9375rem}}.row .row.collapse{margin-left:0;margin-right:0}.row.expanded{max-width:none}.row.expanded .row{margin-left:auto;margin-right:auto}.column,.columns{width:100%;float:left;padding-left:.625rem;padding-right:.625rem}@media screen and (min-width:40em){.column,.columns{padding-left:.9375rem;padding-right:.9375rem}}.column:last-child:not(:first-child),.columns:last-child:not(:first-child){float:right}.column.end:last-child:last-child,.end.columns:last-child:last-child{float:left}.column.row.row,.row.row.columns{float:none}.row .column.row.row,.row .row.row.columns{padding-left:0;padding-right:0;margin-left:0;margin-right:0}.small-1{width:8.33333%}.small-push-1{position:relative;left:8.33333%}.small-pull-1{position:relative;left:-8.33333%}.small-offset-0{margin-left:0}.small-2{width:16.66667%}.small-push-2{position:relative;left:16.66667%}.small-pull-2{position:relative;left:-16.66667%}.small-offset-1{margin-left:8.33333%}.small-3{width:25%}.small-push-3{position:relative;left:25%}.small-pull-3{position:relative;left:-25%}.small-offset-2{margin-left:16.66667%}.small-4{width:33.33333%}.small-push-4{position:relative;left:33.33333%}.small-pull-4{position:relative;left:-33.33333%}.small-offset-3{margin-left:25%}.small-5{width:41.66667%}.small-push-5{position:relative;left:41.66667%}.small-pull-5{position:relative;left:-41.66667%}.small-offset-4{margin-left:33.33333%}.small-6{width:50%}.small-push-6{position:relative;left:50%}.small-pull-6{position:relative;left:-50%}.small-offset-5{margin-left:41.66667%}.small-7{width:58.33333%}.small-push-7{position:relative;left:58.33333%}.small-pull-7{position:relative;left:-58.33333%}.small-offset-6{margin-left:50%}.small-8{width:66.66667%}.small-push-8{position:relative;left:66.66667%}.small-pull-8{position:relative;left:-66.66667%}.small-offset-7{margin-left:58.33333%}.small-9{width:75%}.small-push-9{position:relative;left:75%}.small-pull-9{position:relative;left:-75%}.small-offset-8{margin-left:66.66667%}.small-10{width:83.33333%}.small-push-10{position:relative;left:83.33333%}.small-pull-10{position:relative;left:-83.33333%}.small-offset-9{margin-left:75%}.small-11{width:91.66667%}.small-push-11{position:relative;left:91.66667%}.small-pull-11{position:relative;left:-91.66667%}.small-offset-10{margin-left:83.33333%}.small-12{width:100%}.small-offset-11{margin-left:91.66667%}.small-up-1>.column,.small-up-1>.columns{width:100%;float:left}.small-up-1>.column:nth-of-type(1n),.small-up-1>.columns:nth-of-type(1n){clear:none}.small-up-1>.column:nth-of-type(1n+1),.small-up-1>.columns:nth-of-type(1n+1){clear:both}.small-up-1>.column:last-child,.small-up-1>.columns:last-child{float:left}.small-up-2>.column,.small-up-2>.columns{width:50%;float:left}.small-up-2>.column:nth-of-type(1n),.small-up-2>.columns:nth-of-type(1n){clear:none}.small-up-2>.column:nth-of-type(2n+1),.small-up-2>.columns:nth-of-type(2n+1){clear:both}.small-up-2>.column:last-child,.small-up-2>.columns:last-child{float:left}.small-up-3>.column,.small-up-3>.columns{width:33.33333%;float:left}.small-up-3>.column:nth-of-type(1n),.small-up-3>.columns:nth-of-type(1n){clear:none}.small-up-3>.column:nth-of-type(3n+1),.small-up-3>.columns:nth-of-type(3n+1){clear:both}.small-up-3>.column:last-child,.small-up-3>.columns:last-child{float:left}.small-up-4>.column,.small-up-4>.columns{width:25%;float:left}.small-up-4>.column:nth-of-type(1n),.small-up-4>.columns:nth-of-type(1n){clear:none}.small-up-4>.column:nth-of-type(4n+1),.small-up-4>.columns:nth-of-type(4n+1){clear:both}.small-up-4>.column:last-child,.small-up-4>.columns:last-child{float:left}.small-up-5>.column,.small-up-5>.columns{width:20%;float:left}.small-up-5>.column:nth-of-type(1n),.small-up-5>.columns:nth-of-type(1n){clear:none}.small-up-5>.column:nth-of-type(5n+1),.small-up-5>.columns:nth-of-type(5n+1){clear:both}.small-up-5>.column:last-child,.small-up-5>.columns:last-child{float:left}.small-up-6>.column,.small-up-6>.columns{width:16.66667%;float:left}.small-up-6>.column:nth-of-type(1n),.small-up-6>.columns:nth-of-type(1n){clear:none}.small-up-6>.column:nth-of-type(6n+1),.small-up-6>.columns:nth-of-type(6n+1){clear:both}.small-up-6>.column:last-child,.small-up-6>.columns:last-child{float:left}.small-up-7>.column,.small-up-7>.columns{width:14.28571%;float:left}.small-up-7>.column:nth-of-type(1n),.small-up-7>.columns:nth-of-type(1n){clear:none}.small-up-7>.column:nth-of-type(7n+1),.small-up-7>.columns:nth-of-type(7n+1){clear:both}.small-up-7>.column:last-child,.small-up-7>.columns:last-child{float:left}.small-up-8>.column,.small-up-8>.columns{width:12.5%;float:left}.small-up-8>.column:nth-of-type(1n),.small-up-8>.columns:nth-of-type(1n){clear:none}.small-up-8>.column:nth-of-type(8n+1),.small-up-8>.columns:nth-of-type(8n+1){clear:both}.small-up-8>.column:last-child,.small-up-8>.columns:last-child{float:left}.small-collapse>.column,.small-collapse>.columns{padding-left:0;padding-right:0}.expanded.row .small-collapse.row,.small-collapse .row{margin-left:0;margin-right:0}.small-uncollapse>.column,.small-uncollapse>.columns{padding-left:.625rem;padding-right:.625rem}.small-centered{float:none;margin-left:auto;margin-right:auto}.small-pull-0,.small-push-0,.small-uncentered{position:static;margin-left:0;margin-right:0;float:left}@media screen and (min-width:40em){.medium-1{width:8.33333%}.medium-push-1{position:relative;left:8.33333%}.medium-pull-1{position:relative;left:-8.33333%}.medium-offset-0{margin-left:0}.medium-2{width:16.66667%}.medium-push-2{position:relative;left:16.66667%}.medium-pull-2{position:relative;left:-16.66667%}.medium-offset-1{margin-left:8.33333%}.medium-3{width:25%}.medium-push-3{position:relative;left:25%}.medium-pull-3{position:relative;left:-25%}.medium-offset-2{margin-left:16.66667%}.medium-4{width:33.33333%}.medium-push-4{position:relative;left:33.33333%}.medium-pull-4{position:relative;left:-33.33333%}.medium-offset-3{margin-left:25%}.medium-5{width:41.66667%}.medium-push-5{position:relative;left:41.66667%}.medium-pull-5{position:relative;left:-41.66667%}.medium-offset-4{margin-left:33.33333%}.medium-6{width:50%}.medium-push-6{position:relative;left:50%}.medium-pull-6{position:relative;left:-50%}.medium-offset-5{margin-left:41.66667%}.medium-7{width:58.33333%}.medium-push-7{position:relative;left:58.33333%}.medium-pull-7{position:relative;left:-58.33333%}.medium-offset-6{margin-left:50%}.medium-8{width:66.66667%}.medium-push-8{position:relative;left:66.66667%}.medium-pull-8{position:relative;left:-66.66667%}.medium-offset-7{margin-left:58.33333%}.medium-9{width:75%}.medium-push-9{position:relative;left:75%}.medium-pull-9{position:relative;left:-75%}.medium-offset-8{margin-left:66.66667%}.medium-10{width:83.33333%}.medium-push-10{position:relative;left:83.33333%}.medium-pull-10{position:relative;left:-83.33333%}.medium-offset-9{margin-left:75%}.medium-11{width:91.66667%}.medium-push-11{position:relative;left:91.66667%}.medium-pull-11{position:relative;left:-91.66667%}.medium-offset-10{margin-left:83.33333%}.medium-12{width:100%}.medium-offset-11{margin-left:91.66667%}.medium-up-1>.column,.medium-up-1>.columns{width:100%;float:left}.medium-up-1>.column:nth-of-type(1n),.medium-up-1>.columns:nth-of-type(1n){clear:none}.medium-up-1>.column:nth-of-type(1n+1),.medium-up-1>.columns:nth-of-type(1n+1){clear:both}.medium-up-1>.column:last-child,.medium-up-1>.columns:last-child{float:left}.medium-up-2>.column,.medium-up-2>.columns{width:50%;float:left}.medium-up-2>.column:nth-of-type(1n),.medium-up-2>.columns:nth-of-type(1n){clear:none}.medium-up-2>.column:nth-of-type(2n+1),.medium-up-2>.columns:nth-of-type(2n+1){clear:both}.medium-up-2>.column:last-child,.medium-up-2>.columns:last-child{float:left}.medium-up-3>.column,.medium-up-3>.columns{width:33.33333%;float:left}.medium-up-3>.column:nth-of-type(1n),.medium-up-3>.columns:nth-of-type(1n){clear:none}.medium-up-3>.column:nth-of-type(3n+1),.medium-up-3>.columns:nth-of-type(3n+1){clear:both}.medium-up-3>.column:last-child,.medium-up-3>.columns:last-child{float:left}.medium-up-4>.column,.medium-up-4>.columns{width:25%;float:left}.medium-up-4>.column:nth-of-type(1n),.medium-up-4>.columns:nth-of-type(1n){clear:none}.medium-up-4>.column:nth-of-type(4n+1),.medium-up-4>.columns:nth-of-type(4n+1){clear:both}.medium-up-4>.column:last-child,.medium-up-4>.columns:last-child{float:left}.medium-up-5>.column,.medium-up-5>.columns{width:20%;float:left}.medium-up-5>.column:nth-of-type(1n),.medium-up-5>.columns:nth-of-type(1n){clear:none}.medium-up-5>.column:nth-of-type(5n+1),.medium-up-5>.columns:nth-of-type(5n+1){clear:both}.medium-up-5>.column:last-child,.medium-up-5>.columns:last-child{float:left}.medium-up-6>.column,.medium-up-6>.columns{width:16.66667%;float:left}.medium-up-6>.column:nth-of-type(1n),.medium-up-6>.columns:nth-of-type(1n){clear:none}.medium-up-6>.column:nth-of-type(6n+1),.medium-up-6>.columns:nth-of-type(6n+1){clear:both}.medium-up-6>.column:last-child,.medium-up-6>.columns:last-child{float:left}.medium-up-7>.column,.medium-up-7>.columns{width:14.28571%;float:left}.medium-up-7>.column:nth-of-type(1n),.medium-up-7>.columns:nth-of-type(1n){clear:none}.medium-up-7>.column:nth-of-type(7n+1),.medium-up-7>.columns:nth-of-type(7n+1){clear:both}.medium-up-7>.column:last-child,.medium-up-7>.columns:last-child{float:left}.medium-up-8>.column,.medium-up-8>.columns{width:12.5%;float:left}.medium-up-8>.column:nth-of-type(1n),.medium-up-8>.columns:nth-of-type(1n){clear:none}.medium-up-8>.column:nth-of-type(8n+1),.medium-up-8>.columns:nth-of-type(8n+1){clear:both}.medium-up-8>.column:last-child,.medium-up-8>.columns:last-child{float:left}.medium-collapse>.column,.medium-collapse>.columns{padding-left:0;padding-right:0}.expanded.row .medium-collapse.row,.medium-collapse .row{margin-left:0;margin-right:0}.medium-uncollapse>.column,.medium-uncollapse>.columns{padding-left:.9375rem;padding-right:.9375rem}.medium-centered{float:none;margin-left:auto;margin-right:auto}.medium-pull-0,.medium-push-0,.medium-uncentered{position:static;margin-left:0;margin-right:0;float:left}}@media screen and (min-width:64em){.large-1{width:8.33333%}.large-push-1{position:relative;left:8.33333%}.large-pull-1{position:relative;left:-8.33333%}.large-offset-0{margin-left:0}.large-2{width:16.66667%}.large-push-2{position:relative;left:16.66667%}.large-pull-2{position:relative;left:-16.66667%}.large-offset-1{margin-left:8.33333%}.large-3{width:25%}.large-push-3{position:relative;left:25%}.large-pull-3{position:relative;left:-25%}.large-offset-2{margin-left:16.66667%}.large-4{width:33.33333%}.large-push-4{position:relative;left:33.33333%}.large-pull-4{position:relative;left:-33.33333%}.large-offset-3{margin-left:25%}.large-5{width:41.66667%}.large-push-5{position:relative;left:41.66667%}.large-pull-5{position:relative;left:-41.66667%}.large-offset-4{margin-left:33.33333%}.large-6{width:50%}.large-push-6{position:relative;left:50%}.large-pull-6{position:relative;left:-50%}.large-offset-5{margin-left:41.66667%}.large-7{width:58.33333%}.large-push-7{position:relative;left:58.33333%}.large-pull-7{position:relative;left:-58.33333%}.large-offset-6{margin-left:50%}.large-8{width:66.66667%}.large-push-8{position:relative;left:66.66667%}.large-pull-8{position:relative;left:-66.66667%}.large-offset-7{margin-left:58.33333%}.large-9{width:75%}.large-push-9{position:relative;left:75%}.large-pull-9{position:relative;left:-75%}.large-offset-8{margin-left:66.66667%}.large-10{width:83.33333%}.large-push-10{position:relative;left:83.33333%}.large-pull-10{position:relative;left:-83.33333%}.large-offset-9{margin-left:75%}.large-11{width:91.66667%}.large-push-11{position:relative;left:91.66667%}.large-pull-11{position:relative;left:-91.66667%}.large-offset-10{margin-left:83.33333%}.large-12{width:100%}.large-offset-11{margin-left:91.66667%}.large-up-1>.column,.large-up-1>.columns{width:100%;float:left}.large-up-1>.column:nth-of-type(1n),.large-up-1>.columns:nth-of-type(1n){clear:none}.large-up-1>.column:nth-of-type(1n+1),.large-up-1>.columns:nth-of-type(1n+1){clear:both}.large-up-1>.column:last-child,.large-up-1>.columns:last-child{float:left}.large-up-2>.column,.large-up-2>.columns{width:50%;float:left}.large-up-2>.column:nth-of-type(1n),.large-up-2>.columns:nth-of-type(1n){clear:none}.large-up-2>.column:nth-of-type(2n+1),.large-up-2>.columns:nth-of-type(2n+1){clear:both}.large-up-2>.column:last-child,.large-up-2>.columns:last-child{float:left}.large-up-3>.column,.large-up-3>.columns{width:33.33333%;float:left}.large-up-3>.column:nth-of-type(1n),.large-up-3>.columns:nth-of-type(1n){clear:none}.large-up-3>.column:nth-of-type(3n+1),.large-up-3>.columns:nth-of-type(3n+1){clear:both}.large-up-3>.column:last-child,.large-up-3>.columns:last-child{float:left}.large-up-4>.column,.large-up-4>.columns{width:25%;float:left}.large-up-4>.column:nth-of-type(1n),.large-up-4>.columns:nth-of-type(1n){clear:none}.large-up-4>.column:nth-of-type(4n+1),.large-up-4>.columns:nth-of-type(4n+1){clear:both}.large-up-4>.column:last-child,.large-up-4>.columns:last-child{float:left}.large-up-5>.column,.large-up-5>.columns{width:20%;float:left}.large-up-5>.column:nth-of-type(1n),.large-up-5>.columns:nth-of-type(1n){clear:none}.large-up-5>.column:nth-of-type(5n+1),.large-up-5>.columns:nth-of-type(5n+1){clear:both}.large-up-5>.column:last-child,.large-up-5>.columns:last-child{float:left}.large-up-6>.column,.large-up-6>.columns{width:16.66667%;float:left}.large-up-6>.column:nth-of-type(1n),.large-up-6>.columns:nth-of-type(1n){clear:none}.large-up-6>.column:nth-of-type(6n+1),.large-up-6>.columns:nth-of-type(6n+1){clear:both}.large-up-6>.column:last-child,.large-up-6>.columns:last-child{float:left}.large-up-7>.column,.large-up-7>.columns{width:14.28571%;float:left}.large-up-7>.column:nth-of-type(1n),.large-up-7>.columns:nth-of-type(1n){clear:none}.large-up-7>.column:nth-of-type(7n+1),.large-up-7>.columns:nth-of-type(7n+1){clear:both}.large-up-7>.column:last-child,.large-up-7>.columns:last-child{float:left}.large-up-8>.column,.large-up-8>.columns{width:12.5%;float:left}.large-up-8>.column:nth-of-type(1n),.large-up-8>.columns:nth-of-type(1n){clear:none}.large-up-8>.column:nth-of-type(8n+1),.large-up-8>.columns:nth-of-type(8n+1){clear:both}.large-up-8>.column:last-child,.large-up-8>.columns:last-child{float:left}.large-collapse>.column,.large-collapse>.columns{padding-left:0;padding-right:0}.expanded.row .large-collapse.row,.large-collapse .row{margin-left:0;margin-right:0}.large-uncollapse>.column,.large-uncollapse>.columns{padding-left:.9375rem;padding-right:.9375rem}.large-centered{float:none;margin-left:auto;margin-right:auto}.large-pull-0,.large-push-0,.large-uncentered{position:static;margin-left:0;margin-right:0;float:left}}blockquote,dd,div,dl,dt,form,h1,h2,h3,h4,h5,h6,li,ol,p,pre,td,th,ul{margin:0;padding:0}p{font-size:inherit;line-height:1.6;margin-bottom:1rem;text-rendering:optimizeLegibility}em,i{font-style:italic}b,em,i,strong{line-height:inherit}b,strong{font-weight:700}small{font-size:80%;line-height:inherit}h1,h2,h3,h4,h5,h6{font-family:Helvetica Neue,Helvetica,Roboto,Arial,sans-serif;font-weight:400;font-style:normal;color:inherit;text-rendering:optimizeLegibility;margin-top:0;margin-bottom:.5rem;line-height:1.4}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{color:#cacaca;line-height:0}h1{font-size:1.5rem}h2{font-size:1.25rem}h3{font-size:1.1875rem}h4{font-size:1.125rem}h5{font-size:1.0625rem}h6{font-size:1rem}@media screen and (min-width:40em){h1{font-size:3rem}h2{font-size:2.5rem}h3{font-size:1.9375rem}h4{font-size:1.5625rem}h5{font-size:1.25rem}h6{font-size:1rem}}a{color:#2199e8;text-decoration:none;line-height:inherit;cursor:pointer}a:focus,a:hover{color:#1585cf}a img{border:0}hr{max-width:75rem;height:0;border-right:0;border-top:0;border-bottom:1px solid #cacaca;border-left:0;margin:1.25rem auto;clear:both}dl,ol,ul{line-height:1.6;margin-bottom:1rem;list-style-position:outside}li{font-size:inherit}ul{list-style-type:disc}ol,ul{margin-left:1.25rem}ol ol,ol ul,ul ol,ul ul{margin-left:1.25rem;margin-bottom:0}dl{margin-bottom:1rem}dl dt{margin-bottom:.3rem;font-weight:700}blockquote{margin:0 0 1rem;padding:.5625rem 1.25rem 0 1.1875rem;border-left:1px solid #cacaca}blockquote,blockquote p{line-height:1.6;color:#8a8a8a}cite{display:block;font-size:.8125rem;color:#8a8a8a}cite:before{content:'\2014 \0020'}abbr{color:#0a0a0a;cursor:help;border-bottom:1px dotted #0a0a0a}code{font-weight:400;border:1px solid #cacaca;padding:.125rem .3125rem .0625rem}code,kbd{font-family:Consolas,Liberation Mono,Courier,monospace;color:#0a0a0a;background-color:#e6e6e6}kbd{padding:.125rem .25rem 0;margin:0}.subheader{margin-top:.2rem;margin-bottom:.5rem;font-weight:400;line-height:1.4;color:#8a8a8a}.lead{font-size:125%;line-height:1.6}.stat{font-size:2.5rem;line-height:1}p+.stat{margin-top:-1rem}.no-bullet{margin-left:0;list-style:none}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}@media screen and (min-width:40em){.medium-text-left{text-align:left}.medium-text-right{text-align:right}.medium-text-center{text-align:center}.medium-text-justify{text-align:justify}}@media screen and (min-width:64em){.large-text-left{text-align:left}.large-text-right{text-align:right}.large-text-center{text-align:center}.large-text-justify{text-align:justify}}.show-for-print{display:none!important}@media print{*{background:transparent!important;color:#000!important;box-shadow:none!important;text-shadow:none!important}.show-for-print{display:block!important}.hide-for-print{display:none!important}table.show-for-print{display:table!important}thead.show-for-print{display:table-header-group!important}tbody.show-for-print{display:table-row-group!important}tr.show-for-print{display:table-row!important}td.show-for-print,th.show-for-print{display:table-cell!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}.ir a:after,a[href^='#']:after,a[href^='javascript:']:after{content:''}abbr[title]:after{content:" (" attr(title) ")"}blockquote,pre{border:1px solid #8a8a8a;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.button{display:inline-block;text-align:center;line-height:1;cursor:pointer;-webkit-appearance:none;transition:background-color .25s ease-out,color .25s ease-out;vertical-align:middle;border:1px solid transparent;border-radius:0;padding:.85em 1em;margin:0 0 1rem;font-size:.9rem;background-color:#2199e8;color:#fefefe}[data-whatinput=mouse] .button{outline:0}.button:focus,.button:hover{background-color:#1583cc;color:#fefefe}.button.tiny{font-size:.6rem}.button.small{font-size:.75rem}.button.large{font-size:1.25rem}.button.expanded{display:block;width:100%;margin-left:0;margin-right:0}.button.primary{background-color:#2199e8;color:#fefefe}.button.primary:focus,.button.primary:hover{background-color:#147cc0;color:#fefefe}.button.secondary{background-color:#777;color:#fefefe}.button.secondary:focus,.button.secondary:hover{background-color:#5f5f5f;color:#fefefe}.button.success{background-color:#3adb76;color:#fefefe}.button.success:focus,.button.success:hover{background-color:#22bb5b;color:#fefefe}.button.warning{background-color:#ffae00;color:#fefefe}.button.warning:focus,.button.warning:hover{background-color:#cc8b00;color:#fefefe}.button.alert{background-color:#ec5840;color:#fefefe}.button.alert:focus,.button.alert:hover{background-color:#da3116;color:#fefefe}.button.hollow{border:1px solid #2199e8;color:#2199e8}.button.hollow,.button.hollow:focus,.button.hollow:hover{background-color:transparent}.button.hollow:focus,.button.hollow:hover{border-color:#0c4d78;color:#0c4d78}.button.hollow.primary{border:1px solid #2199e8;color:#2199e8}.button.hollow.primary:focus,.button.hollow.primary:hover{border-color:#0c4d78;color:#0c4d78}.button.hollow.secondary{border:1px solid #777;color:#777}.button.hollow.secondary:focus,.button.hollow.secondary:hover{border-color:#3c3c3c;color:#3c3c3c}.button.hollow.success{border:1px solid #3adb76;color:#3adb76}.button.hollow.success:focus,.button.hollow.success:hover{border-color:#157539;color:#157539}.button.hollow.warning{border:1px solid #ffae00;color:#ffae00}.button.hollow.warning:focus,.button.hollow.warning:hover{border-color:#805700;color:#805700}.button.hollow.alert{border:1px solid #ec5840;color:#ec5840}.button.hollow.alert:focus,.button.hollow.alert:hover{border-color:#881f0e;color:#881f0e}.button.disabled,.button[disabled]{opacity:.25;cursor:not-allowed}.button.disabled:focus,.button.disabled:hover,.button[disabled]:focus,.button[disabled]:hover{background-color:#2199e8;color:#fefefe}.button.dropdown:after{content:'';display:block;width:0;height:0;border:.4em inset;border-color:#fefefe transparent transparent;border-top-style:solid;border-bottom-width:0;position:relative;top:.4em;float:right;margin-left:1em;display:inline-block}.button.arrow-only:after{margin-left:0;float:none;top:-.1em}[type=color],[type=date],[type=datetime-local],[type=datetime],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],textarea{display:block;box-sizing:border-box;width:100%;height:2.4375rem;padding:.5rem;border:1px solid #cacaca;margin:0 0 1rem;font-family:inherit;font-size:1rem;color:#0a0a0a;background-color:#fefefe;box-shadow:inset 0 1px 2px hsla(0,0%,4%,.1);border-radius:0;transition:box-shadow .5s,border-color .25s ease-in-out;-webkit-appearance:none;-moz-appearance:none}[type=color]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=datetime]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,textarea:focus{border:1px solid #8a8a8a;background-color:#fefefe;outline:none;box-shadow:0 0 5px #cacaca;transition:box-shadow .5s,border-color .25s ease-in-out}textarea{max-width:100%}textarea[rows]{height:auto}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#cacaca}input::-moz-placeholder,textarea::-moz-placeholder{color:#cacaca}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#cacaca}input::placeholder,textarea::placeholder{color:#cacaca}input:disabled,input[readonly],textarea:disabled,textarea[readonly]{background-color:#e6e6e6;cursor:not-allowed}[type=button],[type=submit]{border-radius:0;-webkit-appearance:none;-moz-appearance:none}input[type=search]{box-sizing:border-box}[type=checkbox],[type=file],[type=radio]{margin:0 0 1rem}[type=checkbox]+label,[type=radio]+label{display:inline-block;margin-left:.5rem;margin-right:1rem;margin-bottom:0;vertical-align:baseline}[type=checkbox]+label[for],[type=radio]+label[for]{cursor:pointer}label>[type=checkbox],label>[type=radio]{margin-right:.5rem}[type=file]{width:100%}label{display:block;margin:0;font-size:.875rem;font-weight:400;line-height:1.8;color:#0a0a0a}label.middle{margin:0 0 1rem;padding:.5625rem 0}.help-text{margin-top:-.5rem;font-size:.8125rem;font-style:italic;color:#0a0a0a}.input-group{display:table;width:100%;margin-bottom:1rem}.input-group>:first-child,.input-group>:last-child>*{border-radius:0 0 0 0}.input-group-button,.input-group-field,.input-group-label{margin:0;white-space:nowrap;display:table-cell;vertical-align:middle}.input-group-label{text-align:center;padding:0 1rem;background:#e6e6e6;color:#0a0a0a;border:1px solid #cacaca;white-space:nowrap;width:1%;height:100%}.input-group-label:first-child{border-right:0}.input-group-label:last-child{border-left:0}.input-group-field{border-radius:0;height:2.5rem}.input-group-button{padding-top:0;padding-bottom:0;text-align:center;height:100%;width:1%}.input-group-button a,.input-group-button button,.input-group-button input{margin:0}.input-group .input-group-button{display:table-cell}fieldset{border:0;padding:0;margin:0}legend{margin-bottom:.5rem;max-width:100%}.fieldset{border:1px solid #cacaca;padding:1.25rem;margin:1.125rem 0}.fieldset legend{background:#fefefe;padding:0 .1875rem;margin:0;margin-left:-.1875rem}select{height:2.4375rem;padding:.5rem;border:1px solid #cacaca;margin:0 0 1rem;font-size:1rem;font-family:inherit;line-height:normal;color:#0a0a0a;background-color:#fefefe;border-radius:0;-webkit-appearance:none;-moz-appearance:none;background-image:url("data:image/svg+xml;utf8,");background-size:9px 6px;background-position:right -1rem center;background-origin:content-box;background-repeat:no-repeat;padding-right:1.5rem}@media screen and (min-width:0\0){select{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAYCAYAAACbU/80AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIpJREFUeNrEkckNgDAMBBfRkEt0ObRBBdsGXUDgmQfK4XhH2m8czQAAy27R3tsw4Qfe2x8uOO6oYLb6GlOor3GF+swURAOmUJ+RwtEJs9WvTGEYxBXqI1MQAZhCfUQKRzDMVj+TwrAIV6jvSUEkYAr1LSkcyTBb/V+KYfX7xAeusq3sLDtGH3kEGACPWIflNZfhRQAAAABJRU5ErkJggg==")}}select:disabled{background-color:#e6e6e6;cursor:not-allowed}select::-ms-expand{display:none}select[multiple]{height:auto;background-image:none}.is-invalid-input:not(:focus){background-color:rgba(236,88,64,.1);border-color:#ec5840}.form-error,.is-invalid-label{color:#ec5840}.form-error{display:none;margin-top:-.5rem;margin-bottom:1rem;font-size:.75rem;font-weight:700}.form-error.is-visible{display:block}.accordion{list-style-type:none;background:#fefefe;margin-left:0}.accordion-item:first-child>:first-child,.accordion-item:last-child>:last-child{border-radius:0 0 0 0}.accordion-title{display:block;padding:1.25rem 1rem;line-height:1;font-size:.75rem;color:#2199e8;position:relative;border:1px solid #e6e6e6;border-bottom:0}:last-child:not(.is-active)>.accordion-title{border-radius:0 0 0 0;border-bottom:1px solid #e6e6e6}.accordion-title:focus,.accordion-title:hover{background-color:#e6e6e6}.accordion-title:before{content:'+';position:absolute;right:1rem;top:50%;margin-top:-.5rem}.is-active>.accordion-title:before{content:'–'}.accordion-content{padding:1rem;display:none;border:1px solid #e6e6e6;border-bottom:0;background-color:#fefefe;color:#0a0a0a}:last-child>.accordion-content:last-child{border-bottom:1px solid #e6e6e6}.is-accordion-submenu-parent>a{position:relative}.is-accordion-submenu-parent>a:after{content:'';display:block;width:0;height:0;border:6px inset;border-color:#2199e8 transparent transparent;border-top-style:solid;border-bottom-width:0;position:absolute;top:50%;margin-top:-4px;right:1rem}.is-accordion-submenu-parent[aria-expanded=true]>a:after{-webkit-transform-origin:50% 50%;transform-origin:50% 50%;-webkit-transform:scaleY(-1);transform:scaleY(-1)}.badge{display:inline-block;padding:.3em;min-width:2.1em;font-size:.6rem;text-align:center;border-radius:50%;background:#2199e8;color:#fefefe}.badge.secondary{background:#777;color:#fefefe}.badge.success{background:#3adb76;color:#fefefe}.badge.warning{background:#ffae00;color:#fefefe}.badge.alert{background:#ec5840;color:#fefefe}.breadcrumbs{list-style:none;margin:0 0 1rem}.breadcrumbs:after,.breadcrumbs:before{content:' ';display:table}.breadcrumbs:after{clear:both}.breadcrumbs li{float:left;color:#0a0a0a;font-size:.6875rem;cursor:default;text-transform:uppercase}.breadcrumbs li:not(:last-child):after{color:#cacaca;content:"/";margin:0 .75rem;position:relative;top:1px;opacity:1}.breadcrumbs a{color:#2199e8}.breadcrumbs a:hover{text-decoration:underline}.breadcrumbs .disabled{color:#cacaca;cursor:not-allowed}.button-group{margin-bottom:1rem;font-size:0}.button-group:after,.button-group:before{content:' ';display:table}.button-group:after{clear:both}.button-group .button{margin:0;margin-right:1px;margin-bottom:1px;font-size:.9rem}.button-group .button:last-child{margin-right:0}.button-group.tiny .button{font-size:.6rem}.button-group.small .button{font-size:.75rem}.button-group.large .button{font-size:1.25rem}.button-group.expanded{margin-right:-1px}.button-group.expanded:after,.button-group.expanded:before{display:none}.button-group.expanded .button:first-child:nth-last-child(2),.button-group.expanded .button:first-child:nth-last-child(2):first-child:nth-last-child(2)~.button{display:inline-block;width:calc(50% - 1px);margin-right:1px}.button-group.expanded .button:first-child:nth-last-child(2):first-child:nth-last-child(2)~.button:last-child,.button-group.expanded .button:first-child:nth-last-child(2):last-child{margin-right:-6px}.button-group.expanded .button:first-child:nth-last-child(3),.button-group.expanded .button:first-child:nth-last-child(3):first-child:nth-last-child(3)~.button{display:inline-block;width:calc(33.33333% - 1px);margin-right:1px}.button-group.expanded .button:first-child:nth-last-child(3):first-child:nth-last-child(3)~.button:last-child,.button-group.expanded .button:first-child:nth-last-child(3):last-child{margin-right:-6px}.button-group.expanded .button:first-child:nth-last-child(4),.button-group.expanded .button:first-child:nth-last-child(4):first-child:nth-last-child(4)~.button{display:inline-block;width:calc(25% - 1px);margin-right:1px}.button-group.expanded .button:first-child:nth-last-child(4):first-child:nth-last-child(4)~.button:last-child,.button-group.expanded .button:first-child:nth-last-child(4):last-child{margin-right:-6px}.button-group.expanded .button:first-child:nth-last-child(5),.button-group.expanded .button:first-child:nth-last-child(5):first-child:nth-last-child(5)~.button{display:inline-block;width:calc(20% - 1px);margin-right:1px}.button-group.expanded .button:first-child:nth-last-child(5):first-child:nth-last-child(5)~.button:last-child,.button-group.expanded .button:first-child:nth-last-child(5):last-child{margin-right:-6px}.button-group.expanded .button:first-child:nth-last-child(6),.button-group.expanded .button:first-child:nth-last-child(6):first-child:nth-last-child(6)~.button{display:inline-block;width:calc(16.66667% - 1px);margin-right:1px}.button-group.expanded .button:first-child:nth-last-child(6):first-child:nth-last-child(6)~.button:last-child,.button-group.expanded .button:first-child:nth-last-child(6):last-child{margin-right:-6px}.button-group.primary .button{background-color:#2199e8;color:#fefefe}.button-group.primary .button:focus,.button-group.primary .button:hover{background-color:#147cc0;color:#fefefe}.button-group.secondary .button{background-color:#777;color:#fefefe}.button-group.secondary .button:focus,.button-group.secondary .button:hover{background-color:#5f5f5f;color:#fefefe}.button-group.success .button{background-color:#3adb76;color:#fefefe}.button-group.success .button:focus,.button-group.success .button:hover{background-color:#22bb5b;color:#fefefe}.button-group.warning .button{background-color:#ffae00;color:#fefefe}.button-group.warning .button:focus,.button-group.warning .button:hover{background-color:#cc8b00;color:#fefefe}.button-group.alert .button{background-color:#ec5840;color:#fefefe}.button-group.alert .button:focus,.button-group.alert .button:hover{background-color:#da3116;color:#fefefe}.button-group.stacked-for-medium .button,.button-group.stacked-for-small .button,.button-group.stacked .button{width:100%}.button-group.stacked-for-medium .button:last-child,.button-group.stacked-for-small .button:last-child,.button-group.stacked .button:last-child{margin-bottom:0}@media screen and (min-width:40em){.button-group.stacked-for-small .button{width:auto;margin-bottom:0}}@media screen and (min-width:64em){.button-group.stacked-for-medium .button{width:auto;margin-bottom:0}}@media screen and (max-width:39.9375em){.button-group.stacked-for-small.expanded{display:block}.button-group.stacked-for-small.expanded .button{display:block;margin-right:0}}.callout{margin:0 0 1rem;padding:1rem;border:1px solid hsla(0,0%,4%,.25);border-radius:0;position:relative;color:#0a0a0a;background-color:#fff}.callout>:first-child{margin-top:0}.callout>:last-child{margin-bottom:0}.callout.primary{background-color:#def0fc}.callout.secondary{background-color:#ebebeb}.callout.success{background-color:#e1faea}.callout.warning{background-color:#fff3d9}.callout.alert{background-color:#fce6e2}.callout.small{padding:.5rem}.callout.large{padding:3rem}.close-button{position:absolute;color:#8a8a8a;right:1rem;top:.5rem;font-size:2em;line-height:1;cursor:pointer}[data-whatinput=mouse] .close-button{outline:0}.close-button:focus,.close-button:hover{color:#0a0a0a}.menu{margin:0;list-style-type:none}.menu>li{display:table-cell;vertical-align:middle}[data-whatinput=mouse] .menu>li{outline:0}.menu>li>a{display:block;padding:.7rem 1rem;line-height:1}.menu a,.menu button,.menu input{margin-bottom:0}.menu>li>a i,.menu>li>a i+span,.menu>li>a img,.menu>li>a img+span,.menu>li>a svg,.menu>li>a svg+span{vertical-align:middle}.menu>li>a i,.menu>li>a img,.menu>li>a svg{margin-right:.25rem;display:inline-block}.menu>li{display:table-cell}.menu.vertical>li{display:block}@media screen and (min-width:40em){.menu.medium-horizontal>li{display:table-cell}.menu.medium-vertical>li{display:block}}@media screen and (min-width:64em){.menu.large-horizontal>li{display:table-cell}.menu.large-vertical>li{display:block}}.menu.simple li{line-height:1;display:inline-block;margin-right:1rem}.menu.simple a{padding:0}.menu.align-right:after,.menu.align-right:before{content:' ';display:table}.menu.align-right:after{clear:both}.menu.align-right>li{float:right}.menu.expanded{width:100%;display:table;table-layout:fixed}.menu.expanded>li:first-child:last-child{width:100%}.menu.icon-top>li>a{text-align:center}.menu.icon-top>li>a i,.menu.icon-top>li>a img,.menu.icon-top>li>a svg{display:block;margin:0 auto .25rem}.menu.nested{margin-left:1rem}.menu .active>a{color:#fefefe;background:#2199e8}.menu-text{font-weight:700;color:inherit;line-height:1;padding-top:0;padding-bottom:0;padding:.7rem 1rem}.menu-centered{text-align:center}.menu-centered>.menu{display:inline-block}.no-js [data-responsive-menu] ul{display:none}.menu-icon{position:relative;display:inline-block;vertical-align:middle;cursor:pointer;width:20px;height:16px}.menu-icon:after{content:'';position:absolute;display:block;width:100%;height:2px;background:#fefefe;top:0;left:0;box-shadow:0 7px 0 #fefefe,0 14px 0 #fefefe}.menu-icon:hover:after{background:#cacaca;box-shadow:0 7px 0 #cacaca,0 14px 0 #cacaca}.is-drilldown{position:relative;overflow:hidden}.is-drilldown li{display:block!important}.is-drilldown-submenu{position:absolute;top:0;left:100%;z-index:-1;height:100%;width:100%;background:#fefefe;transition:-webkit-transform .15s linear;transition:transform .15s linear}.is-drilldown-submenu.is-active{z-index:1;display:block;-webkit-transform:translateX(-100%);transform:translateX(-100%)}.is-drilldown-submenu.is-closing{-webkit-transform:translateX(100%);transform:translateX(100%)}.is-drilldown-submenu-parent>a{position:relative}.is-drilldown-submenu-parent>a:after{content:'';display:block;width:0;height:0;border:6px inset;border-color:transparent transparent transparent #2199e8;border-left-style:solid;border-right-width:0;position:absolute;top:50%;margin-top:-6px;right:1rem}.js-drilldown-back>a:before{content:'';display:block;width:0;height:0;border:6px inset;border-color:transparent #2199e8 transparent transparent;border-right-style:solid;border-left-width:0;display:inline-block;vertical-align:middle;margin-right:.75rem}.dropdown-pane{background-color:#fefefe;border:1px solid #cacaca;border-radius:0;display:block;font-size:1rem;padding:1rem;position:absolute;visibility:hidden;width:300px;z-index:3}.dropdown-pane.is-open{visibility:visible}.dropdown-pane.tiny{width:100px}.dropdown-pane.small{width:200px}.dropdown-pane.large{width:400px}.dropdown.menu>li.opens-left>.is-dropdown-submenu{left:auto;right:0;top:100%}.dropdown.menu>li.opens-right>.is-dropdown-submenu{right:auto;left:0;top:100%}.dropdown.menu>li.is-dropdown-submenu-parent>a{padding-right:1.5rem;position:relative}.dropdown.menu>li.is-dropdown-submenu-parent>a:after{content:'';display:block;width:0;height:0;border:5px inset;border-color:#2199e8 transparent transparent;border-top-style:solid;border-bottom-width:0;right:5px;margin-top:-2px}[data-whatinput=mouse] .dropdown.menu a{outline:0}.no-js .dropdown.menu ul{display:none}.dropdown.menu.vertical>li .is-dropdown-submenu{top:0}.dropdown.menu.vertical>li.opens-left>.is-dropdown-submenu{left:auto;right:100%}.dropdown.menu.vertical>li.opens-right>.is-dropdown-submenu{right:auto;left:100%}.dropdown.menu.vertical>li>a:after{right:14px;margin-top:-3px}.dropdown.menu.vertical>li.opens-left>a:after{content:'';display:block;width:0;height:0;border:5px inset;border-color:transparent #2199e8 transparent transparent;border-right-style:solid;border-left-width:0}.dropdown.menu.vertical>li.opens-right>a:after{content:'';display:block;width:0;height:0;border:5px inset;border-color:transparent transparent transparent #2199e8;border-left-style:solid;border-right-width:0}@media screen and (min-width:40em){.dropdown.menu.medium-horizontal>li.opens-left>.is-dropdown-submenu{left:auto;right:0;top:100%}.dropdown.menu.medium-horizontal>li.opens-right>.is-dropdown-submenu{right:auto;left:0;top:100%}.dropdown.menu.medium-horizontal>li.is-dropdown-submenu-parent>a{padding-right:1.5rem;position:relative}.dropdown.menu.medium-horizontal>li.is-dropdown-submenu-parent>a:after{content:'';display:block;width:0;height:0;border:5px inset;border-color:#2199e8 transparent transparent;border-top-style:solid;border-bottom-width:0;right:5px;margin-top:-2px}.dropdown.menu.medium-vertical>li .is-dropdown-submenu{top:0}.dropdown.menu.medium-vertical>li.opens-left>.is-dropdown-submenu{left:auto;right:100%}.dropdown.menu.medium-vertical>li.opens-right>.is-dropdown-submenu{right:auto;left:100%}.dropdown.menu.medium-vertical>li>a:after{right:14px;margin-top:-3px}.dropdown.menu.medium-vertical>li.opens-left>a:after{content:'';display:block;width:0;height:0;border:5px inset;border-color:transparent #2199e8 transparent transparent;border-right-style:solid;border-left-width:0}.dropdown.menu.medium-vertical>li.opens-right>a:after{content:'';display:block;width:0;height:0;border:5px inset;border-color:transparent transparent transparent #2199e8;border-left-style:solid;border-right-width:0}}@media screen and (min-width:64em){.dropdown.menu.large-horizontal>li.opens-left>.is-dropdown-submenu{left:auto;right:0;top:100%}.dropdown.menu.large-horizontal>li.opens-right>.is-dropdown-submenu{right:auto;left:0;top:100%}.dropdown.menu.large-horizontal>li.is-dropdown-submenu-parent>a{padding-right:1.5rem;position:relative}.dropdown.menu.large-horizontal>li.is-dropdown-submenu-parent>a:after{content:'';display:block;width:0;height:0;border:5px inset;border-color:#2199e8 transparent transparent;border-top-style:solid;border-bottom-width:0;right:5px;margin-top:-2px}.dropdown.menu.large-vertical>li .is-dropdown-submenu{top:0}.dropdown.menu.large-vertical>li.opens-left>.is-dropdown-submenu{left:auto;right:100%}.dropdown.menu.large-vertical>li.opens-right>.is-dropdown-submenu{right:auto;left:100%}.dropdown.menu.large-vertical>li>a:after{right:14px;margin-top:-3px}.dropdown.menu.large-vertical>li.opens-left>a:after{content:'';display:block;width:0;height:0;border:5px inset;border-color:transparent #2199e8 transparent transparent;border-right-style:solid;border-left-width:0}.dropdown.menu.large-vertical>li.opens-right>a:after{content:'';display:block;width:0;height:0;border:5px inset;border-color:transparent transparent transparent #2199e8;border-left-style:solid;border-right-width:0}}.dropdown.menu.align-right .is-dropdown-submenu.first-sub{top:100%;left:auto;right:0}.is-dropdown-menu.vertical{width:100px}.is-dropdown-menu.vertical.align-right{float:right}.is-dropdown-submenu-parent{position:relative}.is-dropdown-submenu-parent a:after{position:absolute;top:50%;right:5px;margin-top:-2px}.is-dropdown-submenu-parent.opens-inner>.is-dropdown-submenu{top:100%;left:auto}.is-dropdown-submenu-parent.opens-left>.is-dropdown-submenu{left:auto;right:100%}.is-dropdown-submenu-parent.opens-right>.is-dropdown-submenu{right:auto;left:100%}.is-dropdown-submenu{display:none;position:absolute;top:0;left:100%;min-width:200px;z-index:1;background:#fefefe;border:1px solid #cacaca}.is-dropdown-submenu .is-dropdown-submenu-parent>a:after{right:14px;margin-top:-3px}.is-dropdown-submenu .is-dropdown-submenu-parent.opens-left>a:after{content:'';display:block;width:0;height:0;border:5px inset;border-color:transparent #2199e8 transparent transparent;border-right-style:solid;border-left-width:0}.is-dropdown-submenu .is-dropdown-submenu-parent.opens-right>a:after{content:'';display:block;width:0;height:0;border:5px inset;border-color:transparent transparent transparent #2199e8;border-left-style:solid;border-right-width:0}.is-dropdown-submenu .is-dropdown-submenu{margin-top:-1px}.is-dropdown-submenu>li{width:100%}.is-dropdown-submenu.js-dropdown-active{display:block}.flex-video{position:relative;height:0;padding-bottom:75%;margin-bottom:1rem;overflow:hidden}.flex-video embed,.flex-video iframe,.flex-video object,.flex-video video{position:absolute;top:0;left:0;width:100%;height:100%}.flex-video.widescreen{padding-bottom:56.25%}.flex-video.vimeo{padding-top:0}.label{display:inline-block;padding:.33333rem .5rem;font-size:.8rem;line-height:1;white-space:nowrap;cursor:default;border-radius:0;background:#2199e8;color:#fefefe}.label.secondary{background:#777;color:#fefefe}.label.success{background:#3adb76;color:#fefefe}.label.warning{background:#ffae00;color:#fefefe}.label.alert{background:#ec5840;color:#fefefe}.media-object{margin-bottom:1rem;display:block}.media-object img{max-width:none}@media screen and (max-width:39.9375em){.media-object.stack-for-small .media-object-section{padding:0;padding-bottom:1rem;display:block}.media-object.stack-for-small .media-object-section img{width:100%}}.media-object-section{display:table-cell;vertical-align:top}.media-object-section:first-child{padding-right:1rem}.media-object-section:last-child:not(:nth-child(2)){padding-left:1rem}.media-object-section>:last-child{margin-bottom:0}.media-object-section.middle{vertical-align:middle}.media-object-section.bottom{vertical-align:bottom}body,html{height:100%}.off-canvas-wrapper{width:100%;overflow-x:hidden;position:relative;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-overflow-scrolling:auto}.off-canvas-wrapper-inner{position:relative;width:100%;transition:-webkit-transform .5s ease;transition:transform .5s ease}.off-canvas-wrapper-inner:after,.off-canvas-wrapper-inner:before{content:' ';display:table}.off-canvas-wrapper-inner:after{clear:both}.off-canvas-content{min-height:100%;background:#fefefe;transition:-webkit-transform .5s ease;transition:transform .5s ease;-webkit-backface-visibility:hidden;backface-visibility:hidden;z-index:1;padding-bottom:.1px;box-shadow:0 0 10px hsla(0,0%,4%,.5)}.js-off-canvas-exit{display:none;position:absolute;top:0;left:0;width:100%;height:100%;background:hsla(0,0%,100%,.25);cursor:pointer;transition:background .5s ease}.off-canvas{position:absolute;background:#e6e6e6;z-index:-1;max-height:100%;overflow-y:auto;-webkit-transform:translateX(0);transform:translateX(0)}[data-whatinput=mouse] .off-canvas{outline:0}.off-canvas.position-left{left:-250px;top:0;width:250px}.is-open-left{-webkit-transform:translateX(250px);transform:translateX(250px)}.off-canvas.position-right{right:-250px;top:0;width:250px}.is-open-right{-webkit-transform:translateX(-250px);transform:translateX(-250px)}@media screen and (min-width:40em){.position-left.reveal-for-medium{left:0;z-index:auto;position:fixed}.position-left.reveal-for-medium~.off-canvas-content{margin-left:250px}.position-right.reveal-for-medium{right:0;z-index:auto;position:fixed}.position-right.reveal-for-medium~.off-canvas-content{margin-right:250px}}@media screen and (min-width:64em){.position-left.reveal-for-large{left:0;z-index:auto;position:fixed}.position-left.reveal-for-large~.off-canvas-content{margin-left:250px}.position-right.reveal-for-large{right:0;z-index:auto;position:fixed}.position-right.reveal-for-large~.off-canvas-content{margin-right:250px}}.orbit,.orbit-container{position:relative}.orbit-container{margin:0;overflow:hidden;list-style:none}.orbit-slide{width:100%;max-height:100%}.orbit-slide.no-motionui.is-active{top:0;left:0}.orbit-figure{margin:0}.orbit-image{margin:0;width:100%;max-width:100%}.orbit-caption{bottom:0;width:100%;margin-bottom:0;background-color:hsla(0,0%,4%,.5)}.orbit-caption,.orbit-next,.orbit-previous{position:absolute;padding:1rem;color:#fefefe}.orbit-next,.orbit-previous{top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);z-index:3}[data-whatinput=mouse] .orbit-next,[data-whatinput=mouse] .orbit-previous{outline:0}.orbit-next:active,.orbit-next:focus,.orbit-next:hover,.orbit-previous:active,.orbit-previous:focus,.orbit-previous:hover{background-color:hsla(0,0%,4%,.5)}.orbit-previous{left:0}.orbit-next{left:auto;right:0}.orbit-bullets{position:relative;margin-top:.8rem;margin-bottom:.8rem;text-align:center}[data-whatinput=mouse] .orbit-bullets{outline:0}.orbit-bullets button{width:1.2rem;height:1.2rem;margin:.1rem;background-color:#cacaca;border-radius:50%}.orbit-bullets button.is-active,.orbit-bullets button:hover{background-color:#8a8a8a}.pagination{margin-left:0;margin-bottom:1rem}.pagination:after,.pagination:before{content:' ';display:table}.pagination:after{clear:both}.pagination li{font-size:.875rem;margin-right:.0625rem;border-radius:0;display:none}.pagination li:first-child,.pagination li:last-child{display:inline-block}@media screen and (min-width:40em){.pagination li{display:inline-block}}.pagination a,.pagination button{color:#0a0a0a;display:block;padding:.1875rem .625rem;border-radius:0}.pagination a:hover,.pagination button:hover{background:#e6e6e6}.pagination .current{padding:.1875rem .625rem;background:#2199e8;color:#fefefe;cursor:default}.pagination .disabled{padding:.1875rem .625rem;color:#cacaca;cursor:not-allowed}.pagination .disabled:hover{background:transparent}.pagination .ellipsis:after{content:'\2026';padding:.1875rem .625rem;color:#0a0a0a}.pagination-previous.disabled:before,.pagination-previous a:before{content:'\00ab';display:inline-block;margin-right:.5rem}.pagination-next.disabled:after,.pagination-next a:after{content:'\00bb';display:inline-block;margin-left:.5rem}.progress{background-color:#cacaca;height:1rem;margin-bottom:1rem;border-radius:0}.progress.primary .progress-meter{background-color:#2199e8}.progress.secondary .progress-meter{background-color:#777}.progress.success .progress-meter{background-color:#3adb76}.progress.warning .progress-meter{background-color:#ffae00}.progress.alert .progress-meter{background-color:#ec5840}.progress-meter{position:relative;display:block;width:0;height:100%;background-color:#2199e8}.progress-meter-text{top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);position:absolute;margin:0;font-size:.75rem;font-weight:700;color:#fefefe;white-space:nowrap}body.is-reveal-open{overflow:hidden}html.is-reveal-open,html.is-reveal-open body{height:100%;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.reveal-overlay{display:none;position:fixed;top:0;bottom:0;left:0;right:0;z-index:4;background-color:hsla(0,0%,4%,.45);overflow-y:scroll}.reveal{display:none;z-index:5;padding:1rem;border:1px solid #cacaca;background-color:#fefefe;border-radius:0;position:relative;top:100px;margin-left:auto;margin-right:auto;overflow-y:auto}[data-whatinput=mouse] .reveal{outline:0}@media screen and (min-width:40em){.reveal{min-height:0}}.reveal .column,.reveal .columns{min-width:0}.reveal>:last-child{margin-bottom:0}@media screen and (min-width:40em){.reveal{width:600px;max-width:75rem}}@media screen and (min-width:40em){.reveal .reveal{left:auto;right:auto;margin:0 auto}}.reveal.collapse{padding:0}@media screen and (min-width:40em){.reveal.tiny{width:30%;max-width:75rem}}@media screen and (min-width:40em){.reveal.small{width:50%;max-width:75rem}}@media screen and (min-width:40em){.reveal.large{width:90%;max-width:75rem}}.reveal.full{top:0;left:0;width:100%;height:100%;height:100vh;min-height:100vh;max-width:none;margin-left:0;border:0;border-radius:0}@media screen and (max-width:39.9375em){.reveal{top:0;left:0;width:100%;height:100%;height:100vh;min-height:100vh;max-width:none;margin-left:0;border:0;border-radius:0}}.reveal.without-overlay{position:fixed}.slider{position:relative;height:.5rem;margin-top:1.25rem;margin-bottom:2.25rem;background-color:#e6e6e6;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-ms-touch-action:none;touch-action:none}.slider-fill{position:absolute;top:0;left:0;display:inline-block;max-width:100%;height:.5rem;background-color:#cacaca;transition:all .2s ease-in-out}.slider-fill.is-dragging{transition:all 0s linear}.slider-handle{top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);position:absolute;left:0;z-index:1;display:inline-block;width:1.4rem;height:1.4rem;background-color:#2199e8;transition:all .2s ease-in-out;-ms-touch-action:manipulation;touch-action:manipulation;border-radius:0}[data-whatinput=mouse] .slider-handle{outline:0}.slider-handle:hover{background-color:#1583cc}.slider-handle.is-dragging{transition:all 0s linear}.slider.disabled,.slider[disabled]{opacity:.25;cursor:not-allowed}.slider.vertical{display:inline-block;width:.5rem;height:12.5rem;margin:0 1.25rem;-webkit-transform:scaleY(-1);transform:scaleY(-1)}.slider.vertical .slider-fill{top:0;width:.5rem;max-height:100%}.slider.vertical .slider-handle{position:absolute;top:0;left:50%;width:1.4rem;height:1.4rem;-webkit-transform:translateX(-50%);transform:translateX(-50%)}.sticky-container{position:relative}.sticky{position:absolute;z-index:0;-webkit-transform:translateZ(0);transform:translateZ(0)}.sticky.is-stuck{position:fixed;z-index:2}.sticky.is-stuck.is-at-top{top:0}.sticky.is-stuck.is-at-bottom{bottom:0}.sticky.is-anchored{position:absolute;left:auto;right:auto}.sticky.is-anchored.is-at-bottom{bottom:0}.switch{margin-bottom:1rem;outline:0;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;color:#fefefe;font-weight:700;font-size:.875rem}.switch-input{opacity:0;position:absolute}.switch-paddle{background:#cacaca;cursor:pointer;display:block;position:relative;width:4rem;height:2rem;transition:all .25s ease-out;border-radius:0;color:inherit;font-weight:inherit}input+.switch-paddle{margin:0}.switch-paddle:after{background:#fefefe;content:'';display:block;position:absolute;height:1.5rem;left:.25rem;top:.25rem;width:1.5rem;transition:all .25s ease-out;-webkit-transform:translateZ(0);transform:translateZ(0);border-radius:0}input:checked~.switch-paddle{background:#2199e8}input:checked~.switch-paddle:after{left:2.25rem}[data-whatinput=mouse] input:focus~.switch-paddle{outline:0}.switch-active,.switch-inactive{position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.switch-active{left:8%;display:none}input:checked+label>.switch-active{display:block}.switch-inactive{right:15%}input:checked+label>.switch-inactive{display:none}.switch.tiny .switch-paddle{width:3rem;height:1.5rem;font-size:.625rem}.switch.tiny .switch-paddle:after{width:1rem;height:1rem}.switch.tiny input:checked~.switch-paddle:after{left:1.75rem}.switch.small .switch-paddle{width:3.5rem;height:1.75rem;font-size:.75rem}.switch.small .switch-paddle:after{width:1.25rem;height:1.25rem}.switch.small input:checked~.switch-paddle:after{left:2rem}.switch.large .switch-paddle{width:5rem;height:2.5rem;font-size:1rem}.switch.large .switch-paddle:after{width:2rem;height:2rem}.switch.large input:checked~.switch-paddle:after{left:2.75rem}table{width:100%;margin-bottom:1rem;border-radius:0}table tbody,table tfoot,table thead{border:1px solid #f1f1f1;background-color:#fefefe}table caption{font-weight:700;padding:.5rem .625rem .625rem}table tfoot,table thead{background:#f8f8f8;color:#0a0a0a}table tfoot tr,table thead tr{background:transparent}table tfoot td,table tfoot th,table thead td,table thead th{padding:.5rem .625rem .625rem;font-weight:700;text-align:left}table tbody tr:nth-child(even){background-color:#f1f1f1}table tbody td,table tbody th{padding:.5rem .625rem .625rem}@media screen and (max-width:63.9375em){table.stack tfoot,table.stack thead{display:none}table.stack td,table.stack th,table.stack tr{display:block}table.stack td{border-top:0}}table.scroll{display:block;width:100%;overflow-x:auto}table.hover tr:hover{background-color:#f9f9f9}table.hover tr:nth-of-type(even):hover{background-color:#ececec}.table-scroll{overflow-x:auto}.table-scroll table{width:auto}.tabs{margin:0;list-style-type:none;background:#fefefe;border:1px solid #e6e6e6}.tabs:after,.tabs:before{content:' ';display:table}.tabs:after{clear:both}.tabs.vertical>li{width:auto;float:none;display:block}.tabs.simple>li>a{padding:0}.tabs.simple>li>a:hover{background:transparent}.tabs.primary{background:#2199e8}.tabs.primary>li>a{color:#fefefe}.tabs.primary>li>a:focus,.tabs.primary>li>a:hover{background:#1893e4}.tabs-title{float:left}.tabs-title>a{display:block;padding:1.25rem 1.5rem;line-height:1;font-size:.75rem}.tabs-title>a:hover{background:#fefefe}.tabs-title>a:focus,.tabs-title>a[aria-selected=true]{background:#e6e6e6}.tabs-content{background:#fefefe;transition:all .5s ease;border:1px solid #e6e6e6;border-top:0}.tabs-content.vertical{border:1px solid #e6e6e6;border-left:0}.tabs-panel{display:none;padding:1rem}.tabs-panel.is-active{display:block}.thumbnail{border:4px solid #fefefe;box-shadow:0 0 0 1px hsla(0,0%,4%,.2);display:inline-block;line-height:0;max-width:100%;transition:box-shadow .2s ease-out;border-radius:0;margin-bottom:1rem}.thumbnail:focus,.thumbnail:hover{box-shadow:0 0 6px 1px rgba(33,153,232,.5)}.title-bar{background:#0a0a0a;color:#fefefe;padding:.5rem}.title-bar:after,.title-bar:before{content:' ';display:table}.title-bar:after{clear:both}.title-bar .menu-icon{margin-left:.25rem;margin-right:.25rem}.title-bar-left{float:left}.title-bar-right{float:right;text-align:right}.title-bar-title{font-weight:700}.menu-icon.dark,.title-bar-title{vertical-align:middle;display:inline-block}.menu-icon.dark{position:relative;cursor:pointer;width:20px;height:16px}.menu-icon.dark:after{content:'';position:absolute;display:block;width:100%;height:2px;background:#0a0a0a;top:0;left:0;box-shadow:0 7px 0 #0a0a0a,0 14px 0 #0a0a0a}.menu-icon.dark:hover:after{background:#8a8a8a;box-shadow:0 7px 0 #8a8a8a,0 14px 0 #8a8a8a}.has-tip{border-bottom:1px dotted #8a8a8a;font-weight:700;position:relative;display:inline-block;cursor:help}.tooltip{background-color:#0a0a0a;color:#fefefe;font-size:80%;padding:.75rem;position:absolute;z-index:3;top:calc(100% + .6495rem);max-width:10rem!important;border-radius:0}.tooltip:before{border-color:transparent transparent #0a0a0a;border-bottom-style:solid;border-top-width:0;bottom:100%;position:absolute;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}.tooltip.top:before,.tooltip:before{content:'';display:block;width:0;height:0;border:.75rem inset}.tooltip.top:before{border-color:#0a0a0a transparent transparent;border-top-style:solid;border-bottom-width:0;top:100%;bottom:auto}.tooltip.left:before{border-color:transparent transparent transparent #0a0a0a;border-left-style:solid;border-right-width:0;left:100%}.tooltip.left:before,.tooltip.right:before{content:'';display:block;width:0;height:0;border:.75rem inset;bottom:auto;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.tooltip.right:before{border-color:transparent #0a0a0a transparent transparent;border-right-style:solid;border-left-width:0;left:auto;right:100%}.top-bar{padding:.5rem}.top-bar:after,.top-bar:before{content:' ';display:table}.top-bar:after{clear:both}.top-bar,.top-bar ul{background-color:#e6e6e6}.top-bar input{max-width:200px;margin-right:1rem}.top-bar .input-group-field{width:100%;margin-right:0}.top-bar input.button{width:auto}.top-bar .top-bar-left,.top-bar .top-bar-right{width:100%}@media screen and (min-width:40em){.top-bar .top-bar-left,.top-bar .top-bar-right{width:auto}}@media screen and (max-width:63.9375em){.top-bar.stacked-for-medium .top-bar-left,.top-bar.stacked-for-medium .top-bar-right{width:100%}}@media screen and (max-width:74.9375em){.top-bar.stacked-for-large .top-bar-left,.top-bar.stacked-for-large .top-bar-right{width:100%}}.top-bar-title{float:left;margin-right:1rem}.top-bar-left{float:left}.top-bar-right{float:right}.hide{display:none!important}.invisible{visibility:hidden}@media screen and (max-width:39.9375em){.hide-for-small-only{display:none!important}}@media screen and (max-width:0em),screen and (min-width:40em){.show-for-small-only{display:none!important}}@media screen and (min-width:40em){.hide-for-medium{display:none!important}}@media screen and (max-width:39.9375em){.show-for-medium{display:none!important}}@media screen and (min-width:40em) and (max-width:63.9375em){.hide-for-medium-only{display:none!important}}@media screen and (max-width:39.9375em),screen and (min-width:64em){.show-for-medium-only{display:none!important}}@media screen and (min-width:64em){.hide-for-large{display:none!important}}@media screen and (max-width:63.9375em){.show-for-large{display:none!important}}@media screen and (min-width:64em) and (max-width:74.9375em){.hide-for-large-only{display:none!important}}@media screen and (max-width:63.9375em),screen and (min-width:75em){.show-for-large-only{display:none!important}}.show-for-sr,.show-on-focus{position:absolute!important;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}.show-on-focus:active,.show-on-focus:focus{position:static!important;height:auto;width:auto;overflow:visible;clip:auto}.hide-for-portrait,.show-for-landscape{display:block!important}@media screen and (orientation:landscape){.hide-for-portrait,.show-for-landscape{display:block!important}}@media screen and (orientation:portrait){.hide-for-portrait,.show-for-landscape{display:none!important}}.hide-for-landscape,.show-for-portrait{display:none!important}@media screen and (orientation:landscape){.hide-for-landscape,.show-for-portrait{display:none!important}}@media screen and (orientation:portrait){.hide-for-landscape,.show-for-portrait{display:block!important}}.float-left{float:left!important}.float-right{float:right!important}.float-center{display:block;margin-left:auto;margin-right:auto}.clearfix:after,.clearfix:before{content:' ';display:table}.clearfix:after{clear:both}.slide-in-down.mui-enter{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:translateY(-100%);transform:translateY(-100%);transition-property:-webkit-transform,opacity;transition-property:transform,opacity;-webkit-backface-visibility:hidden;backface-visibility:hidden}.slide-in-down.mui-enter.mui-enter-active{-webkit-transform:translateY(0);transform:translateY(0)}.slide-in-left.mui-enter{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:translateX(-100%);transform:translateX(-100%);transition-property:-webkit-transform,opacity;transition-property:transform,opacity;-webkit-backface-visibility:hidden;backface-visibility:hidden}.slide-in-left.mui-enter.mui-enter-active{-webkit-transform:translateX(0);transform:translateX(0)}.slide-in-up.mui-enter{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:translateY(100%);transform:translateY(100%);transition-property:-webkit-transform,opacity;transition-property:transform,opacity;-webkit-backface-visibility:hidden;backface-visibility:hidden}.slide-in-up.mui-enter.mui-enter-active{-webkit-transform:translateY(0);transform:translateY(0)}.slide-in-right.mui-enter{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:translateX(100%);transform:translateX(100%);transition-property:-webkit-transform,opacity;transition-property:transform,opacity;-webkit-backface-visibility:hidden;backface-visibility:hidden}.slide-in-right.mui-enter.mui-enter-active{-webkit-transform:translateX(0);transform:translateX(0)}.slide-out-down.mui-leave{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:translateY(0);transform:translateY(0);transition-property:-webkit-transform,opacity;transition-property:transform,opacity;-webkit-backface-visibility:hidden;backface-visibility:hidden}.slide-out-down.mui-leave.mui-leave-active{-webkit-transform:translateY(100%);transform:translateY(100%)}.slide-out-right.mui-leave{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:translateX(0);transform:translateX(0);transition-property:-webkit-transform,opacity;transition-property:transform,opacity;-webkit-backface-visibility:hidden;backface-visibility:hidden}.slide-out-right.mui-leave.mui-leave-active{-webkit-transform:translateX(100%);transform:translateX(100%)}.slide-out-up.mui-leave{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:translateY(0);transform:translateY(0);transition-property:-webkit-transform,opacity;transition-property:transform,opacity;-webkit-backface-visibility:hidden;backface-visibility:hidden}.slide-out-up.mui-leave.mui-leave-active{-webkit-transform:translateY(-100%);transform:translateY(-100%)}.slide-out-left.mui-leave{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:translateX(0);transform:translateX(0);transition-property:-webkit-transform,opacity;transition-property:transform,opacity;-webkit-backface-visibility:hidden;backface-visibility:hidden}.slide-out-left.mui-leave.mui-leave-active{-webkit-transform:translateX(-100%);transform:translateX(-100%)}.fade-in.mui-enter{transition-duration:.5s;transition-timing-function:linear;opacity:0;transition-property:opacity}.fade-in.mui-enter.mui-enter-active{opacity:1}.fade-out.mui-leave{transition-duration:.5s;transition-timing-function:linear;opacity:1;transition-property:opacity}.fade-out.mui-leave.mui-leave-active{opacity:0}.hinge-in-from-top.mui-enter{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:perspective(2000px) rotateX(-90deg);transform:perspective(2000px) rotateX(-90deg);-webkit-transform-origin:top;transform-origin:top;transition-property:-webkit-transform,opacity;transition-property:transform,opacity;opacity:0}.hinge-in-from-top.mui-enter.mui-enter-active{-webkit-transform:perspective(2000px) rotate(0deg);transform:perspective(2000px) rotate(0deg);opacity:1}.hinge-in-from-right.mui-enter{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:perspective(2000px) rotateY(-90deg);transform:perspective(2000px) rotateY(-90deg);-webkit-transform-origin:right;transform-origin:right;transition-property:-webkit-transform,opacity;transition-property:transform,opacity;opacity:0}.hinge-in-from-right.mui-enter.mui-enter-active{-webkit-transform:perspective(2000px) rotate(0deg);transform:perspective(2000px) rotate(0deg);opacity:1}.hinge-in-from-bottom.mui-enter{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:perspective(2000px) rotateX(90deg);transform:perspective(2000px) rotateX(90deg);-webkit-transform-origin:bottom;transform-origin:bottom;transition-property:-webkit-transform,opacity;transition-property:transform,opacity;opacity:0}.hinge-in-from-bottom.mui-enter.mui-enter-active{-webkit-transform:perspective(2000px) rotate(0deg);transform:perspective(2000px) rotate(0deg);opacity:1}.hinge-in-from-left.mui-enter{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:perspective(2000px) rotateY(90deg);transform:perspective(2000px) rotateY(90deg);-webkit-transform-origin:left;transform-origin:left;transition-property:-webkit-transform,opacity;transition-property:transform,opacity;opacity:0}.hinge-in-from-left.mui-enter.mui-enter-active{-webkit-transform:perspective(2000px) rotate(0deg);transform:perspective(2000px) rotate(0deg);opacity:1}.hinge-in-from-middle-x.mui-enter{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:perspective(2000px) rotateX(-90deg);transform:perspective(2000px) rotateX(-90deg);-webkit-transform-origin:center;transform-origin:center;transition-property:-webkit-transform,opacity;transition-property:transform,opacity;opacity:0}.hinge-in-from-middle-x.mui-enter.mui-enter-active{-webkit-transform:perspective(2000px) rotate(0deg);transform:perspective(2000px) rotate(0deg);opacity:1}.hinge-in-from-middle-y.mui-enter{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:perspective(2000px) rotateY(-90deg);transform:perspective(2000px) rotateY(-90deg);-webkit-transform-origin:center;transform-origin:center;transition-property:-webkit-transform,opacity;transition-property:transform,opacity;opacity:0}.hinge-in-from-middle-y.mui-enter.mui-enter-active,.hinge-out-from-top.mui-leave{-webkit-transform:perspective(2000px) rotate(0deg);transform:perspective(2000px) rotate(0deg);opacity:1}.hinge-out-from-top.mui-leave{transition-duration:.5s;transition-timing-function:linear;-webkit-transform-origin:top;transform-origin:top;transition-property:-webkit-transform,opacity;transition-property:transform,opacity}.hinge-out-from-top.mui-leave.mui-leave-active{-webkit-transform:perspective(2000px) rotateX(-90deg);transform:perspective(2000px) rotateX(-90deg);opacity:0}.hinge-out-from-right.mui-leave{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:perspective(2000px) rotate(0deg);transform:perspective(2000px) rotate(0deg);-webkit-transform-origin:right;transform-origin:right;transition-property:-webkit-transform,opacity;transition-property:transform,opacity;opacity:1}.hinge-out-from-right.mui-leave.mui-leave-active{-webkit-transform:perspective(2000px) rotateY(-90deg);transform:perspective(2000px) rotateY(-90deg);opacity:0}.hinge-out-from-bottom.mui-leave{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:perspective(2000px) rotate(0deg);transform:perspective(2000px) rotate(0deg);-webkit-transform-origin:bottom;transform-origin:bottom;transition-property:-webkit-transform,opacity;transition-property:transform,opacity;opacity:1}.hinge-out-from-bottom.mui-leave.mui-leave-active{-webkit-transform:perspective(2000px) rotateX(90deg);transform:perspective(2000px) rotateX(90deg);opacity:0}.hinge-out-from-left.mui-leave{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:perspective(2000px) rotate(0deg);transform:perspective(2000px) rotate(0deg);-webkit-transform-origin:left;transform-origin:left;transition-property:-webkit-transform,opacity;transition-property:transform,opacity;opacity:1}.hinge-out-from-left.mui-leave.mui-leave-active{-webkit-transform:perspective(2000px) rotateY(90deg);transform:perspective(2000px) rotateY(90deg);opacity:0}.hinge-out-from-middle-x.mui-leave{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:perspective(2000px) rotate(0deg);transform:perspective(2000px) rotate(0deg);-webkit-transform-origin:center;transform-origin:center;transition-property:-webkit-transform,opacity;transition-property:transform,opacity;opacity:1}.hinge-out-from-middle-x.mui-leave.mui-leave-active{-webkit-transform:perspective(2000px) rotateX(-90deg);transform:perspective(2000px) rotateX(-90deg);opacity:0}.hinge-out-from-middle-y.mui-leave{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:perspective(2000px) rotate(0deg);transform:perspective(2000px) rotate(0deg);-webkit-transform-origin:center;transform-origin:center;transition-property:-webkit-transform,opacity;transition-property:transform,opacity;opacity:1}.hinge-out-from-middle-y.mui-leave.mui-leave-active{-webkit-transform:perspective(2000px) rotateY(-90deg);transform:perspective(2000px) rotateY(-90deg);opacity:0}.scale-in-up.mui-enter{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:scale(.5);transform:scale(.5);transition-property:-webkit-transform,opacity;transition-property:transform,opacity;opacity:0}.scale-in-up.mui-enter.mui-enter-active{-webkit-transform:scale(1);transform:scale(1);opacity:1}.scale-in-down.mui-enter{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:scale(1.5);transform:scale(1.5);transition-property:-webkit-transform,opacity;transition-property:transform,opacity;opacity:0}.scale-in-down.mui-enter.mui-enter-active,.scale-out-up.mui-leave{-webkit-transform:scale(1);transform:scale(1);opacity:1}.scale-out-up.mui-leave{transition-duration:.5s;transition-timing-function:linear;transition-property:-webkit-transform,opacity;transition-property:transform,opacity}.scale-out-up.mui-leave.mui-leave-active{-webkit-transform:scale(1.5);transform:scale(1.5);opacity:0}.scale-out-down.mui-leave{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:scale(1);transform:scale(1);transition-property:-webkit-transform,opacity;transition-property:transform,opacity;opacity:1}.scale-out-down.mui-leave.mui-leave-active{-webkit-transform:scale(.5);transform:scale(.5);opacity:0}.spin-in.mui-enter{transition-duration:.5s;transition-timing-function:linear;-webkit-transform:rotate(-270deg);transform:rotate(-270deg);transition-property:-webkit-transform,opacity;transition-property:transform,opacity;opacity:0}.spin-in.mui-enter.mui-enter-active,.spin-out.mui-leave{-webkit-transform:rotate(0);transform:rotate(0);opacity:1}.spin-out.mui-leave{transition-duration:.5s;transition-timing-function:linear;transition-property:-webkit-transform,opacity;transition-property:transform,opacity}.spin-in-ccw.mui-enter,.spin-out.mui-leave.mui-leave-active{-webkit-transform:rotate(270deg);transform:rotate(270deg);opacity:0}.spin-in-ccw.mui-enter{transition-duration:.5s;transition-timing-function:linear;transition-property:-webkit-transform,opacity;transition-property:transform,opacity}.spin-in-ccw.mui-enter.mui-enter-active,.spin-out-ccw.mui-leave{-webkit-transform:rotate(0);transform:rotate(0);opacity:1}.spin-out-ccw.mui-leave{transition-duration:.5s;transition-timing-function:linear;transition-property:-webkit-transform,opacity;transition-property:transform,opacity}.spin-out-ccw.mui-leave.mui-leave-active{-webkit-transform:rotate(-270deg);transform:rotate(-270deg);opacity:0}.slow{transition-duration:.75s!important}.fast{transition-duration:.25s!important}.linear{transition-timing-function:linear!important}.ease{transition-timing-function:ease!important}.ease-in{transition-timing-function:ease-in!important}.ease-out{transition-timing-function:ease-out!important}.ease-in-out{transition-timing-function:ease-in-out!important}.bounce-in{transition-timing-function:cubic-bezier(.485,.155,.24,1.245)!important}.bounce-out{transition-timing-function:cubic-bezier(.485,.155,.515,.845)!important}.bounce-in-out{transition-timing-function:cubic-bezier(.76,-.245,.24,1.245)!important}.short-delay{transition-delay:.3s!important}.long-delay{transition-delay:.7s!important}.shake{-webkit-animation-name:a;animation-name:a}@-webkit-keyframes a{0%,10%,20%,30%,40%,50%,60%,70%,80%,90%{-webkit-transform:translateX(7%);transform:translateX(7%)}5%,15%,25%,35%,45%,55%,65%,75%,85%,95%{-webkit-transform:translateX(-7%);transform:translateX(-7%)}}@keyframes a{0%,10%,20%,30%,40%,50%,60%,70%,80%,90%{-webkit-transform:translateX(7%);transform:translateX(7%)}5%,15%,25%,35%,45%,55%,65%,75%,85%,95%{-webkit-transform:translateX(-7%);transform:translateX(-7%)}}.spin-cw{-webkit-animation-name:b;animation-name:b}@-webkit-keyframes b{0%{-webkit-transform:rotate(-1turn);transform:rotate(-1turn)}to{-webkit-transform:rotate(0);transform:rotate(0)}}@keyframes b{0%{-webkit-transform:rotate(-1turn);transform:rotate(-1turn)}to{-webkit-transform:rotate(0);transform:rotate(0)}}.spin-ccw{-webkit-animation-name:b;animation-name:b}@keyframes b{0%{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.wiggle{-webkit-animation-name:c;animation-name:c}@-webkit-keyframes c{40%,50%,60%{-webkit-transform:rotate(7deg);transform:rotate(7deg)}35%,45%,55%,65%{-webkit-transform:rotate(-7deg);transform:rotate(-7deg)}0%,30%,70%,to{-webkit-transform:rotate(0);transform:rotate(0)}}@keyframes c{40%,50%,60%{-webkit-transform:rotate(7deg);transform:rotate(7deg)}35%,45%,55%,65%{-webkit-transform:rotate(-7deg);transform:rotate(-7deg)}0%,30%,70%,to{-webkit-transform:rotate(0);transform:rotate(0)}}.shake,.spin-ccw,.spin-cw,.wiggle{-webkit-animation-duration:.5s;animation-duration:.5s}.infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.slow{-webkit-animation-duration:.75s!important;animation-duration:.75s!important}.fast{-webkit-animation-duration:.25s!important;animation-duration:.25s!important}.linear{-webkit-animation-timing-function:linear!important;animation-timing-function:linear!important}.ease{-webkit-animation-timing-function:ease!important;animation-timing-function:ease!important}.ease-in{-webkit-animation-timing-function:ease-in!important;animation-timing-function:ease-in!important}.ease-out{-webkit-animation-timing-function:ease-out!important;animation-timing-function:ease-out!important}.ease-in-out{-webkit-animation-timing-function:ease-in-out!important;animation-timing-function:ease-in-out!important}.bounce-in{-webkit-animation-timing-function:cubic-bezier(.485,.155,.24,1.245)!important;animation-timing-function:cubic-bezier(.485,.155,.24,1.245)!important}.bounce-out{-webkit-animation-timing-function:cubic-bezier(.485,.155,.515,.845)!important;animation-timing-function:cubic-bezier(.485,.155,.515,.845)!important}.bounce-in-out{-webkit-animation-timing-function:cubic-bezier(.76,-.245,.24,1.245)!important;animation-timing-function:cubic-bezier(.76,-.245,.24,1.245)!important}.short-delay{-webkit-animation-delay:.3s!important;animation-delay:.3s!important}.long-delay{-webkit-animation-delay:.7s!important;animation-delay:.7s!important} \ No newline at end of file diff --git a/app/assets/stylesheets/home.scss b/app/assets/stylesheets/home.scss new file mode 100644 index 0000000000..f0ddc6846a --- /dev/null +++ b/app/assets/stylesheets/home.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the home 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/merchants.scss b/app/assets/stylesheets/merchants.scss new file mode 100644 index 0000000000..2327c00afe --- /dev/null +++ b/app/assets/stylesheets/merchants.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the merchants 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..3b0428a94e --- /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..89e2e8db07 --- /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..6ea2454d26 --- /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/scaffolds.scss b/app/assets/stylesheets/scaffolds.scss new file mode 100644 index 0000000000..2c070523d9 --- /dev/null +++ b/app/assets/stylesheets/scaffolds.scss @@ -0,0 +1,68 @@ +body { + background-color: #fff; + color: #333; + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +p, ol, ul, td { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +pre { + background-color: #eee; + padding: 10px; + font-size: 11px; +} + +a { + color: #000; + + &:visited { + color: #666; + } +} + +div { + &.field, &.actions { + margin-bottom: 10px; + } +} + +#notice { + color: green; +} + +.field_with_errors { + padding: 2px; + background-color: red; + display: table; +} + +#error_explanation { + width: 450px; + border: 2px solid red; + padding: 7px; + padding-bottom: 0; + margin-bottom: 20px; + background-color: #f0f0f0; + + h2 { + text-align: left; + font-weight: bold; + padding: 5px 5px 5px 15px; + font-size: 12px; + margin: -7px; + margin-bottom: 0px; + background-color: #c00; + color: #fff; + } + + ul li { + font-size: 12px; + list-style: square; + } +} diff --git a/app/assets/stylesheets/sessions.scss b/app/assets/stylesheets/sessions.scss new file mode 100644 index 0000000000..7bef9cf826 --- /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/controllers/application_controller.rb b/app/controllers/application_controller.rb index d83690e1b9..7ecc97feae 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,25 @@ class ApplicationController < ActionController::Base - # Prevent CSRF attacks by raising an exception. - # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception + before_action :require_login, only: [:new, :create, :edit, :update, :delete] + + def current_user + @current_user ||= Merchant.find(session[:merchant_id]) if session[:merchant_id] + end + + def require_login + current_user + if @current_user.nil? + render :view_error + end + end + + def cart + begin + @cart = Order.find(session[:order_id]) + rescue ActiveRecord::RecordNotFound + @cart = Order.create + session[:order_id] = @cart.id + end + @items = @cart.order_items + end end diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb new file mode 100644 index 0000000000..973d220fee --- /dev/null +++ b/app/controllers/categories_controller.rb @@ -0,0 +1,30 @@ +class CategoriesController < ApplicationController + before_action :set_category, only: [:show] + + def index + @categories = Category.all + end + + def show + @category_products = @category.products.uniq! + end + + def new + @category = Category.new + end + + def create + Category.create!(category_params) + redirect_to categories_path + end + +private + + def set_category + @category = Category.find(params[:id]) + end + + def category_params + params.require(:category).permit(:name, :description) + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 0000000000..2c391c3a5b --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,11 @@ +class HomeController < ApplicationController + skip_before_action :require_login, only: [:index] + def index + reviews = Review.all.where(rating: 5) + @popular = [] + reviews.each do |review| + @popular << Product.find(review.product_id) + end + return @popular.uniq! + end +end diff --git a/app/controllers/merchants_controller.rb b/app/controllers/merchants_controller.rb new file mode 100644 index 0000000000..26ac888d56 --- /dev/null +++ b/app/controllers/merchants_controller.rb @@ -0,0 +1,29 @@ +class MerchantsController < ApplicationController + + skip_before_action :require_login, only: [:index, :show] + + def index + @merchants = Merchant.all + end + + def show + begin + @merchant = Merchant.find(params[:id]) + rescue ActiveRecord::RecordNotFound => err + render file: "#{Rails.root}/public/404.html", layout: false, status: 404 + return + end + + @products = Product.where(merchant_id: @merchant.id) + + @merchant_oi_revenue = @merchant.find_all_order_items_revenue + + @merchant_oi_revenue_by_status = @merchant.find_all_order_items_revenue_by_status + + @merchant_oi_count_by_status = @merchant.find_total_number_of_orders_by_status + + @merchant_orders = @merchant.find_orders + + @merchant_order_items = @merchant.find_order_items_from_orders + end +end diff --git a/app/controllers/order_items_controller.rb b/app/controllers/order_items_controller.rb new file mode 100644 index 0000000000..4bec396264 --- /dev/null +++ b/app/controllers/order_items_controller.rb @@ -0,0 +1,63 @@ +class OrderItemsController < ApplicationController + before_action :set_order_item, only: [:update, :destroy] + before_action :cart, only: [:create, :update, :destroy] + skip_before_action :require_login, only: [:create, :update, :destroy] + + def create #I KNOW, I KNOW: all of this code should be refactored and out of here + product = Product.find(params[:product_id]) + + counter = 0 + if product.stock != nil && product.stock >= order_item_params(params)[:quantity].to_i + @cart.order_items.each do |item| + if item.product_id == product.id + item.quantity += order_item_params(params)[:quantity].to_i + item.save + counter += 1 + end + end + else + session[:product_stock] = product.stock + render :not_enough_stock and return + end + + if counter == 0 + if product.stock != nil && product.stock >= order_item_params(params)[:quantity].to_i + @order_item = OrderItem.new + @order_item.product_id = product.id + @order_item.order_id = @cart.id + + @order_item.quantity = order_item_params(params)[:quantity] + unless @order_item.save + render :failed_order_update and return + end + else + session[:product_stock] = product.stock + render :not_enough_stock and return + end + end + redirect_to order_path + end + + def update + @order_item.quantity = order_item_params(params)[:quantity] + if @order_item.save + redirect_to order_path + else + render :failed_order_update + end + end + + def destroy + @order_item.destroy + redirect_to order_path + end + + private + def set_order_item + @order_item = OrderItem.find(params[:id]) + end + + def order_item_params(params) + params.require(:order_item).permit(:quantity, :product_id, :order_id) + end +end diff --git a/app/controllers/orders_controller.rb b/app/controllers/orders_controller.rb new file mode 100644 index 0000000000..574225cf00 --- /dev/null +++ b/app/controllers/orders_controller.rb @@ -0,0 +1,82 @@ +class OrdersController < ApplicationController + before_action :set_order, only: [:show, :edit, :update, :purchase] + before_action :cart, only: [:show, :edit, :update, :purchase] + skip_before_action :require_login, only: [:show, :edit, :update, :purchase] + + def show; end #this is the cart + + def merchant_view #merchant_orders_path + if session[:merchant_id] + @current_merchant = session[:merchant_id] + @order = Order.find(params[:id]) + @order_items = @order.order_items + @order_products = [] + @order_items.each do |item| + @order_products << item.find_product + end + + if @order_products.any? { |product| product.merchant_id == @current_merchant} + render :merchant_view + else + render :view_error + end + else + render :view_error + end + end + + def edit; end + + def update + if @cart == nil # if the item does not exist + redirect_to :index, flash: {notice: "That item does not exist."} + end + + @cart.name = order_params(params)[:name] + @cart.email = order_params(params)[:email] + @cart.address = order_params(params)[:address] + @cart.cc_number = order_params(params)[:cc_number][-4..-1] #saves last four digits only + @cart.expiration_date = order_params(params)[:expiration_date] + @cart.zip_code = order_params(params)[:zip_code] + if @cart.save + redirect_to confirmation_path + else + render :edit + end + end + + def purchase #sends you to the confirmation page + set_order + @items = @cart.order_items + @total = @cart.total + + @cart.mark_as_paid + + @items.each do |item| + product = item.find_product + product.product_purchased(item.quantity) + end + + render :purchase # REVIEW: does this need to be here since I am clearing the session data after this? + + session.delete(:order_id) + end + + def ship + @order = Order.find(session[:ship_order_id]) + if @order.mark_as_shipped + redirect_to merchant_path(session[:merchant_id]) + else + render :not_shipped + end + end + + private + def set_order + @cart = Order.find(session[:order_id]) + end + + def order_params(params) + params.require(:order).permit(:name, :email, :address, :cc_number, :expiration_date, :cvv, :zip_code) + end +end diff --git a/app/controllers/products_controller.rb b/app/controllers/products_controller.rb new file mode 100644 index 0000000000..b94d13e121 --- /dev/null +++ b/app/controllers/products_controller.rb @@ -0,0 +1,82 @@ +class ProductsController < ApplicationController + before_action :set_product_or_merchant, only: [:show, :edit, :update, :destroy] + before_action :set_merchant, only: [:new, :create] + + before_action :cart, only: [:add_to_cart] + + def index + if params[:category_id] + @products = Category.find(params[:category_id]).products + render :index + elsif params[:merchant_id] + @products = Merchant.find(params[:merchant_id]).products + render :index + end + @products = Product.all + end + + def show + @order_item = OrderItem.new(product_id: @product.id) + @latest_review = Review.all.where(product_id: params[:id]).order(:updated_at).first + end + + # GET /products/new + def new + @product = Product.new + end + + def create + @product = Product.new(product_params) + @product.merchant_id = @merchant.id + if @product.save + redirect_to merchant_path(@merchant) + else + render :new + end + end + + def edit + if !@merchant.nil? + if session[:merchant_id] != @product.merchant_id + render :edit_error + end + end + end + + def update + if @product.update(product_params) + redirect_to product_path(@product) + else + render :edit + end + end + + # def destroy + # @product.destroy + # redirect_to merchant_path(@merchant) + # end + + + private + # Use callbacks to share common setup or constraints between actions. + def set_product_or_merchant + begin + @product = Product.find(params[:id]) + @merchant = Merchant.find(params[:merchant_id]) + + rescue + nil + end + + end + + def set_merchant + @merchant = Merchant.find(params[:merchant_id]) + end + + + # Never trust parameters from the scary internet, only allow the white list through. + def product_params + params.require(:product).permit(:name, :price, :description, :photo_url, :stock, category_ids: []) + end +end diff --git a/app/controllers/reviews_controller.rb b/app/controllers/reviews_controller.rb new file mode 100644 index 0000000000..7e957e89ea --- /dev/null +++ b/app/controllers/reviews_controller.rb @@ -0,0 +1,54 @@ +class ReviewsController < ApplicationController + before_action :set_review, only: [:show] + before_action :set_product_or_merchant, only: [:index, :show, :new] + skip_before_action :require_login + + def index + @reviews = Review.all.where(product_id: params[:product_id]) + end + + + def show; end + + def new + if !@merchant.nil? + if session[:merchant_id] == @merchant.id && @product.merchant_id == @merchant.id + render :review_error + end + end + + @review = Review.new + end + + def create + Review.create!(review_params) + + if params[:product_id] && !params[:merchant_id] + redirect_to product_reviews_path(params[:product_id]) + elsif params[:merchant_id] && params[:product_id] + redirect_to merchant_product_reviews_path(params[:merchant_id], params[:product_id]) + else + render :new + end + end + + private + + def set_review + @review = Review.find(params[:id]) + end + + + def review_params + params.require(:review).permit(:rating_description, :rating, :product_id) + end + + def set_product_or_merchant + begin + @product = Product.find(params[:product_id]) + @merchant = Merchant.find(params[:merchant_id]) + rescue + nil + end + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000000..ab00044c62 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,30 @@ +class SessionsController < ApplicationController + skip_before_action :require_login, only: [:create] + + # def index + # @user = Merchant.find(session[:merchant_id]) # < recalls the value set in a previous request + # end + + def create + auth_hash = request.env['omniauth.auth'] + redirect to login_failure unless auth_hash['uid'] + + @merchant = Merchant.find_by(uid: auth_hash[:uid], provider: 'github') + if @merchant.nil? + # User doesn't match anything in the DB. + # Attempt to create a new user. + @merchant = Merchant.build_from_github(auth_hash) + render :creation_failure and return unless @merchant.save + end + + session[:merchant_id] = @merchant.id + redirect_to root_path and return + end + + def destroy + session.delete(:merchant_id) + @current_user = nil + redirect_to root_path + end + +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de6be7945c..eee1ab0660 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,2 +1,27 @@ module ApplicationHelper + def merchant_only_button(wheretogo, button_words, button_power, path_parameter="") + if session[:merchant_id] + link_to send(wheretogo, path_parameter), method: button_power do + "

#{button_words}

".html_safe + end + end + end + + # solution from http://stackoverflow.com/questions/7461290/convert-cents-into-dollar-string-in-ruby-without-use-of-bigdecimal + def add_decimal(cents) + pfx = [ '0.00', '0.0', '0.' ] + if(pfx[cents.to_s.length]) + cents = pfx[cents.to_s.length] + cents.to_s + else + cents = cents.to_s.dup + cents[-2, 0] = '.' + end + return cents + end + + def link_button(wheretogo, button_words, button_power, path_parameter="") + link_to send(wheretogo, path_parameter), method: button_power do + "

#{button_words}

".html_safe + end + end 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/home_helper.rb b/app/helpers/home_helper.rb new file mode 100644 index 0000000000..23de56ac60 --- /dev/null +++ b/app/helpers/home_helper.rb @@ -0,0 +1,2 @@ +module HomeHelper +end diff --git a/app/helpers/merchants_helper.rb b/app/helpers/merchants_helper.rb new file mode 100644 index 0000000000..5337747b0f --- /dev/null +++ b/app/helpers/merchants_helper.rb @@ -0,0 +1,2 @@ +module MerchantsHelper +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/models/category.rb b/app/models/category.rb new file mode 100644 index 0000000000..8f8c56d486 --- /dev/null +++ b/app/models/category.rb @@ -0,0 +1,6 @@ +class Category < ActiveRecord::Base + has_and_belongs_to_many :products + + validates :name, presence: true, uniqueness: true + validates :description, presence: true +end diff --git a/app/models/merchant.rb b/app/models/merchant.rb new file mode 100644 index 0000000000..acd6a4cedd --- /dev/null +++ b/app/models/merchant.rb @@ -0,0 +1,182 @@ +class Merchant < ActiveRecord::Base + has_many :products + + validates :username, presence: true, uniqueness: true + validates :email, presence: true, uniqueness: true + validates :uid, :provider, presence: true + + def add_decimal(cents) + pfx = [ '0.00', '0.0', '0.' ] + if(pfx[cents.to_s.length]) + cents = pfx[cents.to_s.length] + cents.to_s + else + cents = cents.to_s.dup + cents[-2, 0] = '.' + end + return cents + end + + + def self.build_from_github(auth_hash) + merchant = Merchant.new + merchant.uid = auth_hash[:uid] + merchant.provider = 'github' + if auth_hash['info']['name'] != nil + merchant.username = auth_hash['info']['name'] + else + merchant.username = auth_hash['info']['nickname'] + end + merchant.email = auth_hash['info']['email'] + + return merchant + end + + def find_all_order_items_revenue + if products.nil? + return nil + end + + sum = 0 + + products.each do |product| + order_items = OrderItem.where(product_id: product.id) + order_items.each do |item| + if Order.find(item.order_id).status == 'pending' + sum += item.quantity * product.price + + elsif Order.find(item.order_id).status == 'paid' + sum += item.quantity * product.price + + elsif Order.find(item.order_id).status == 'complete' + sum += item.quantity * product.price + + elsif Order.find(item.order_id).status == 'shipped' + sum += item.quantity * product.price + end + end + end + + if !sum.nil? + sum = "$" + add_decimal(sum) + end + return sum + end + + def find_all_order_items_revenue_by_status + if products.nil? + return nil, nil, nil + end + + pending_subtotals = [] + paid_subtotals = [] + complete_subtotals = [] + shipped_subtotals = [] + + products.each do |product| + order_items = OrderItem.where(product_id: product.id) + order_items.each do |item| + if Order.find(item.order_id).status == 'pending' + pending_subtotals.push(item.quantity * product.price) + + elsif Order.find(item.order_id).status == 'paid' + paid_subtotals.push(item.quantity * product.price) + + elsif Order.find(item.order_id).status == 'complete' + complete_subtotals.push(item.quantity * product.price) + + elsif Order.find(item.order_id).status == 'shipped' + shipped_subtotals.push(item.quantity * product.price) + end + end + end + + pending_revenue = pending_subtotals.reduce(:+) + paid_revenue = paid_subtotals.reduce(:+) + complete_revenue = complete_subtotals.reduce(:+) + shipped_revenue = shipped_subtotals.reduce(:+) + + if !pending_revenue.nil? + pending_revenue = "$" + add_decimal(pending_revenue) + end + + if !paid_revenue.nil? + paid_revenue = "$" + add_decimal(paid_revenue) + end + + if !complete_revenue.nil? + complete_revenue = "$" + add_decimal(complete_revenue) + end + + if !shipped_revenue.nil? + complete_revenue = "$" + add_decimal(shipped_revenue) + end + + return pending_revenue, paid_revenue, complete_revenue, shipped_revenue + end + + def find_total_number_of_orders_by_status + if products.nil? + return nil, nil, nil, nil, nil + end + + pending = 0 + paid = 0 + complete = 0 + cancelled = 0 + shipped = 0 + + products.each do |product| + order_items = OrderItem.where(product_id: product.id) + order_items.each do |item| + if Order.find(item.order_id).status == 'pending' + pending += 1 + + elsif Order.find(item.order_id).status == 'paid' + paid += 1 + + elsif Order.find(item.order_id).status == 'complete' + complete += 1 + + elsif Order.find(item.order_id).status == 'cancelled' + cancelled += 1 + + elsif Order.find(item.order_id).status == 'shipped' + shipped += 1 + end + end + end + return pending, paid, complete, cancelled, shipped + end + + def find_orders + if products.nil? + return nil + end + + orders = [] + + products.each do |product| + order_items = OrderItem.where(product_id: product.id) + order_items.each do |item| + orders.push( Order.find(item.order_id)) + end + end + return orders + end + + def find_order_items_from_orders + if products.nil? + return nil + end + + order_details = [] + + products.each do |product| + order_items = OrderItem.where(product_id: product.id) + order_items.each do |item| + order_details.push([Order.find(item.order_id).id, product.name, product.id, item.quantity, product.price * item.quantity]) + end + end + return order_details + end +end diff --git a/app/models/order.rb b/app/models/order.rb new file mode 100644 index 0000000000..82e70376f8 --- /dev/null +++ b/app/models/order.rb @@ -0,0 +1,28 @@ +class Order < ActiveRecord::Base + has_many :order_items + has_many :products, through: :order_items + + def items_in_order + self.order_items # returns collection of items in this order + end + + def total + total = 0 + + items_in_order.each do |item| + total += (item.find_product.price * item.quantity) + end + + return total + end + + def mark_as_paid + self.status = "paid" + self.save + end + + def mark_as_shipped + self.status = "shipped" + self.save + end +end diff --git a/app/models/order_item.rb b/app/models/order_item.rb new file mode 100644 index 0000000000..621c0ea4f0 --- /dev/null +++ b/app/models/order_item.rb @@ -0,0 +1,10 @@ +class OrderItem < ActiveRecord::Base +belongs_to :product +belongs_to :order +validates :quantity, presence: true, numericality: { only_integer: true, greater_than: 0 } + + def find_product + product = Product.find(self.product_id) + return product + end +end diff --git a/app/models/product.rb b/app/models/product.rb new file mode 100644 index 0000000000..83cf699b00 --- /dev/null +++ b/app/models/product.rb @@ -0,0 +1,17 @@ +class Product < ActiveRecord::Base + validates :name, presence: true, uniqueness: true + validates :price, presence: true, numericality: { + only_integer: true, greater_than: 0 } + + + has_many :reviews + belongs_to :merchant + has_and_belongs_to_many :categories + has_many :order_items + has_many :orders, through: :order_items + + def product_purchased(qty) + self.stock -= qty + self.save + end +end diff --git a/app/models/review.rb b/app/models/review.rb new file mode 100644 index 0000000000..b92a72b12c --- /dev/null +++ b/app/models/review.rb @@ -0,0 +1,5 @@ +class Review < ActiveRecord::Base + belongs_to :product + + validates :rating, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 5} +end diff --git a/app/views/application/view_error.html.erb b/app/views/application/view_error.html.erb new file mode 100644 index 0000000000..ee10cb7f7d --- /dev/null +++ b/app/views/application/view_error.html.erb @@ -0,0 +1,12 @@ +
+
+ sad small fry +
+ +
+

You must be a registered merchant to do that.

+
<%= link_to "Login here", "/auth/github" %>
+
<%= link_to "Continue shopping", root_path %>
+
+ +
diff --git a/app/views/categories/_form.html.erb b/app/views/categories/_form.html.erb new file mode 100644 index 0000000000..07b8c12894 --- /dev/null +++ b/app/views/categories/_form.html.erb @@ -0,0 +1,25 @@ +<%= form_for(@category) do |f| %> + <% if @category.errors.any? %> +
+

<%= pluralize(@category.errors.count, "error") %> prohibited this category from being saved:

+ + +
+ <% end %> +<% end %> + +<%= form_for @category, :action=> path_name, :method => http_method do |f| %> + <%= f.label :name %> + <%= f.text_field :name %> +
+ + <%= f.label :description %> + <%= f.text_field :description, :size => 150%> +
+ + <%= f.submit "Submit", class: "button" %> +<% end %> diff --git a/app/views/categories/index.html.erb b/app/views/categories/index.html.erb new file mode 100644 index 0000000000..3d4dd90eed --- /dev/null +++ b/app/views/categories/index.html.erb @@ -0,0 +1,24 @@ +

Shop by Category

+ + + + + + + + + + + + <% @categories.each do |category| %> + + + + + <% end %> + +
NameDetailsProducts in Category
<%= category.name %><%= link_to "Show Category Details", category_path(category) %><%= link_to "All Products", category_products_path(category) %> +
+ +
+<%= merchant_only_button("new_category_path","Add new category", :get) %> diff --git a/app/views/categories/new.html.erb b/app/views/categories/new.html.erb new file mode 100644 index 0000000000..7aef6f138d --- /dev/null +++ b/app/views/categories/new.html.erb @@ -0,0 +1,5 @@ +

Add a New Category

+ +<%= render "form", path_name: new_category_path, http_method: :post %> + +<%= link_button("root_path", "Back home", :get)%> diff --git a/app/views/categories/show.html.erb b/app/views/categories/show.html.erb new file mode 100644 index 0000000000..7ad7d308c8 --- /dev/null +++ b/app/views/categories/show.html.erb @@ -0,0 +1,23 @@ +

<%= @category.name %>

+

Category description: <%= @category.description %>

+ + + + + + + + + + <% @category_products.each do |product| %> + + + + + <% end %> + +
List of all products in category
<%= product.name %><%= link_to "Show Product Details - FIX ROUTE LATER" %>
+ +<%= link_to categories_path do %> +
Back to all categories
+<% end %> diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb new file mode 100644 index 0000000000..d1a73f0c48 --- /dev/null +++ b/app/views/home/index.html.erb @@ -0,0 +1,17 @@ +<% if @popular.empty? %> +

Welcome to Weebay!

+

To begin shopping, please select an option above!

+<% else %> +

Welcome to Weebay!

+

To begin shopping, please select an option above, or have a look at our top-rated items:

+
+ + <% @popular.each do |product| %> + <% if product.stock > 0 %> +

<%= link_to product.name, product_path(product) %>

+

<%= link_to image_tag(product.photo_url), product_path(product) %>

+

Stock: <%= product.stock %>

+
+ <% end %> + <% end %> +<% end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 9b70e01c2e..b24172cbe1 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,14 +1,50 @@ - Betsy + Weebay <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> <%= csrf_meta_tags %> + +
+

Weebay

+

It's the tiny ketchup bottlelittle things

-<%= yield %> + +
+
+ <%= yield %> +
+ + diff --git a/app/views/merchants/index.html.erb b/app/views/merchants/index.html.erb new file mode 100644 index 0000000000..be26066543 --- /dev/null +++ b/app/views/merchants/index.html.erb @@ -0,0 +1,16 @@ +

<%= notice %>

+ +

ALL MERCHANTS

+ + + + + <% n = 1 %> + <% @merchants.each do |merchant| %> + + + <% n += 1 %> + + <% end %> + +
<%= "#{n}.) "%> <%=link_to"#{merchant.username}", merchant_path(merchant.id) %>
diff --git a/app/views/merchants/show.html.erb b/app/views/merchants/show.html.erb new file mode 100644 index 0000000000..007511fdc2 --- /dev/null +++ b/app/views/merchants/show.html.erb @@ -0,0 +1,106 @@ + +<% unless @merchant.nil? %> +

<%= @merchant.username %>

+<% end %> + +

Products

+ <% if !@products.nil? %> + <% @products.each do |product| %> +

<%= product.name %>

+ <% if session[:merchant_id] == @merchant.id %> +

<%= link_to("Edit Product", edit_merchant_product_path(@merchant.id, product.id), class: "button", method: :get)%>

+ <% end %> +

+

Description: <%= product.description %>

+

Price: $<%= add_decimal(product.price) %>

+

Stock: <%= product.stock %>

+

<%= link_to "Details", merchant_product_path(@merchant.id, product.id) %>

+ <% end %> + <% else %> +

My Products

+

No products yet

+ <% end %> + + <% if session[:merchant_id] == @merchant.id %> +

<%= link_to("New Category", new_category_path, class: "button", method: :get) %>

+

<%= link_to("New Product", new_merchant_product_path(@merchant.id), class: "button", method: :get ) %>

+ +

Order Fulfillment

+ +

Total Revenue

+ <% if !@merchant_oi_revenue.nil? %> + <%= @merchant_oi_revenue %> + <% else %> +

No Revenue

+ <% end %> + +

Total Revenue (by order status)

+

Pending: <%= @merchant_oi_revenue_by_status[0] %>

+

Paid: <%= @merchant_oi_revenue_by_status[1] %>

+

Complete: <%= @merchant_oi_revenue_by_status[2] %>

+ +

Number of Orders (by order status)

+

Pending: <%= @merchant_oi_count_by_status[0] %>

+

Paid: <%= @merchant_oi_count_by_status[1] %>

+

Complete: <%= @merchant_oi_count_by_status[2] %>

+ + +

Orders

+ <% if !@merchant_orders.nil? || @merchant_orders.count == 0 %> +

Search for Order Status (Pending, Paid, Complete, Cancelled, or Shipped)

+ + + + + + + + + + + + <% @merchant_orders.each do |order| %> + + <% session[:ship_order_id] = order.id %> + + + + + + + + <% end %> +
Order #NameEmailStatusOrder DateMark Shipped?
<%= order.id %> <%= link_to(order.name, merchant_orders_path(@merchant.id,order.id), method: :get) %> <%= order.email %> <%= order.status %> <%= order.updated_at.strftime("%A, %B %d, %Y") %> <%= link_to("Link", ship_path(session[:ship_order_id]), method: :patch) %>
+ + + <% index = 0 %> + <% @merchant_orders.each do |order| %> + <% if index == 0 %> + + + + + + + + + <% end %> + + + + + + + <% if !@merchant_order_items[index][4].nil? %> + + <% else %> + <%= add_decimal(@merchant_order_items[index][4]) %> + <% end %> + <% index += 1 %> + + <% end %> +
Order #NameProduct NameProduct DetailsQuantitySubtotal
<%= @merchant_order_items[index][0] %> <%= order.name %> <%= @merchant_order_items[index][1] %><%= link_to("Link", product_path(@merchant_order_items[index][2])) %><%= @merchant_order_items[index][3] %>$<%= add_decimal(@merchant_order_items[index][4]) %>
+ <% else %> +

No orders

+ <% end %> + <% end %> diff --git a/app/views/order_items/_form.html.erb b/app/views/order_items/_form.html.erb new file mode 100644 index 0000000000..7ba89fb51b --- /dev/null +++ b/app/views/order_items/_form.html.erb @@ -0,0 +1,17 @@ +<%= form_for(@order_item) do |f| %> + <% if @order_item.errors.any? %> +
+

<%= pluralize(@order_item.errors.count, "error") %> prohibited this order_item from being saved:

+ + +
+ <% end %> + +
+ <%= f.submit, "Submit", class: "button" %> +
+<% end %> diff --git a/app/views/order_items/failed_order_update.html.erb b/app/views/order_items/failed_order_update.html.erb new file mode 100644 index 0000000000..63db9e528f --- /dev/null +++ b/app/views/order_items/failed_order_update.html.erb @@ -0,0 +1,11 @@ +
+
+ sad small fry +
+ +
+

Add to cart failed.

+
<%= link_to "Continue shopping", root_path %>
+
+ +
diff --git a/app/views/order_items/not_enough_stock.html.erb b/app/views/order_items/not_enough_stock.html.erb new file mode 100644 index 0000000000..7919f096e6 --- /dev/null +++ b/app/views/order_items/not_enough_stock.html.erb @@ -0,0 +1,12 @@ +
+
+ sad small fry +
+ +
+

Not enough in stock!

+

There are only <%="#{session[:product_stock]}"%> in stock.

+
<%= link_to "Continue shopping", root_path %>
+
+ +
diff --git a/app/views/order_items/not_shipped.html.erb b/app/views/order_items/not_shipped.html.erb new file mode 100644 index 0000000000..9e64c2f8e2 --- /dev/null +++ b/app/views/order_items/not_shipped.html.erb @@ -0,0 +1,11 @@ +
+
+ sad small fry +
+ +
+

There was an error in marking this as shipped.

+
<%= link_to "Continue shopping", root_path %>
+
+ +
diff --git a/app/views/orders/_form.html.erb b/app/views/orders/_form.html.erb new file mode 100644 index 0000000000..bbcf282597 --- /dev/null +++ b/app/views/orders/_form.html.erb @@ -0,0 +1,17 @@ +<%= form_for(@order) do |f| %> + <% if @order.errors.any? %> +
+

<%= pluralize(@order.errors.count, "error") %> prohibited this order from being saved:

+ + +
+ <% end %> + +
+ <%= f.submit, "Submit", class: "button" %> +
+<% end %> diff --git a/app/views/orders/edit.html.erb b/app/views/orders/edit.html.erb new file mode 100644 index 0000000000..f1b47f050b --- /dev/null +++ b/app/views/orders/edit.html.erb @@ -0,0 +1,26 @@ +

Please enter your information

+ +<%= form_for @cart, :url => orders_update_path, method: :patch do |f| %> +

<%= f.label :email, "Email:" %> + <%= f.text_field :email %>

+ +

<%= f.label :name, "Name on Credit Card:" %> + <%= f.text_field :name %>

+ +

<%= f.label :address, "Mailing Address:" %> + <%= f.text_field :address %>

+ +

<%= f.label :cc_number, "Credit Card Number:" %> + <%= f.text_field :cc_number %>

+ +

<%= f.label :expiration_date, "Credit Card Expiration Date:" %> + <%= f.text_field :expiration_date %>

+ +

<%= f.label :cvv, "CVV:" %> + <%= f.text_field :cvv %>

+ +

<%= f.label :zip_code, "Zip Code:" %> + <%= f.text_field :zip_code %>

+ +

<%= f.submit "Complete your purchase", class: "button" %>

+ <% end %> diff --git a/app/views/orders/merchant_view.html.erb b/app/views/orders/merchant_view.html.erb new file mode 100644 index 0000000000..a439cb4c9b --- /dev/null +++ b/app/views/orders/merchant_view.html.erb @@ -0,0 +1,7 @@ +

Order Information

+ +

<%="Name: #{ @order.name }"%>

+

<%="Email Address: #{ @order.email }"%>

+

<%="Address: #{ @order.address }"%>

+

<%="Last four digits of credit card: #{ @order.cc_number }"%>

+

<%="Credit Card Expiration Date: #{ @order.expiration_date }"%>

diff --git a/app/views/orders/purchase.html.erb b/app/views/orders/purchase.html.erb new file mode 100644 index 0000000000..31d2cfd593 --- /dev/null +++ b/app/views/orders/purchase.html.erb @@ -0,0 +1,19 @@ +

Order Confirmation

+ +

Order Items:

+<% @items.each do |item| %> + <% product = item.find_product %> +

<%= link_to "#{ product.name }", product_path(product.id) %> + <%= "quantity: #{ item.quantity }" %> + <%= "subtotal: #{ add_decimal(product.price * item.quantity) }" %>

+<% end %> + +

+ Total: <%= "#{ add_decimal(@total) }" %> +

+ +

Order placed at <%= "#{ @cart.updated_at }" %>

+ +

Status: <%= "#{ @cart.status }" %>

+ +<%= link_button("root_path", "Back Home", :get) %> diff --git a/app/views/orders/show.html.erb b/app/views/orders/show.html.erb new file mode 100644 index 0000000000..46c8369aa1 --- /dev/null +++ b/app/views/orders/show.html.erb @@ -0,0 +1,16 @@ +

Cart

+ +<% @items.each do |item| %> + <% product = item.find_product %> +

<%= link_to "Product: #{ product.name }", product_path(product.id) %>

<%# add a link here %> +

<%= "Subtotal: #{ add_decimal(product.price * item.quantity) }" %>

+

<%= form_for item, :url => order_item_path(item) do |f| %> + <%= f.label :quantity, "Change quantity"%> + <%= f.number_field :quantity, :value => item.quantity %> + <%= f.submit("Update", class: "button") %> + <% end %>

+ +

<%= link_to("Remove this product", order_item_path(item.id), class: "button", method: :delete) %> + <% end %>

+ +

<%= link_to("Purchase this order", orders_edit_path, class: "button", method: :get) %>

diff --git a/app/views/products/edit.html.erb b/app/views/products/edit.html.erb new file mode 100644 index 0000000000..88d11fa3c7 --- /dev/null +++ b/app/views/products/edit.html.erb @@ -0,0 +1,22 @@ +

Edit This Product

+ +<%= form_for @product, url: merchant_product_path(@product.merchant, @product), method: :patch do |f| %> + <%= f.label :name %> + <%= f.text_field :name %> + + <%= f.label :price %> + <%= f.text_field :price %> + + <%= f.label :description %> + <%= f.text_field :description %> + + <%= f.label :photo_url %> + <%= f.text_field :photo_url %> + + <%= f.label :stock %> + <%= f.text_field :stock %> + + <%= f.select :category_ids, Category.all.collect {|x| [x.name, x.id]}, {}, :multiple => true %> + + <%= f.submit "Update Product" %> +<% end %> diff --git a/app/views/products/edit_error.html.erb b/app/views/products/edit_error.html.erb new file mode 100644 index 0000000000..57423ffc9b --- /dev/null +++ b/app/views/products/edit_error.html.erb @@ -0,0 +1,12 @@ +
+
+ sad small fry +
+ +
+

You cannot edit a product that is not yours

+
<%= link_to "Continue shopping", root_path %>
+
<%= link_to "Back to my merchant page", merchant_path(session[:merchant_id]) %>
+
+ +
diff --git a/app/views/products/index.html.erb b/app/views/products/index.html.erb new file mode 100644 index 0000000000..a4fb988de4 --- /dev/null +++ b/app/views/products/index.html.erb @@ -0,0 +1,18 @@ +

<%= notice %>

+ +

PRODUCTS

+ + + + + <% @products.each do |p| %> + <% if p.stock > 0 %> + + + + + + <% end%> + <% end %> + +
<%= link_to"#{p.name}", product_path(p.id) %>Price: $<%= "#{add_decimal(p.price)}" %><%= link_to image_tag(p.photo_url), product_path(p.id) %>
diff --git a/app/views/products/new.html.erb b/app/views/products/new.html.erb new file mode 100644 index 0000000000..1f1be64939 --- /dev/null +++ b/app/views/products/new.html.erb @@ -0,0 +1,24 @@ + + +

Add A New Product

+ + <%= form_for @product, url:merchant_products_path(@merchant), method: :post, class: "form" do |f| %> + <%= f.label :name %> + <%= f.text_field :name %> + + <%= f.label :price %> + <%=f.text_field :price %> + + <%= f.label :description %> + <%= f.text_field :description %> + + <%= f.label :photo_url %> + <%= f.text_field :photo_url %> + + <%= f.label :stock %> + <%= f.text_field :stock %> + + <%= f.select :category_ids, Category.all.collect {|x| [x.name, x.id]}, {}, :multiple => true %> + + <%= f.submit "Add Product" %> +<% end %> diff --git a/app/views/products/show.html.erb b/app/views/products/show.html.erb new file mode 100644 index 0000000000..d2a0f36888 --- /dev/null +++ b/app/views/products/show.html.erb @@ -0,0 +1,35 @@ +

You know you wanna buy it!

+ +

<%= @product.name %>

+

<%= image_tag("#{@product.photo_url}") %>

+

<%= @product.description %>

+

Price: $<%= "#{add_decimal(@product.price)}" %>

+

Sold by:<%= " #{Merchant.find(@product.merchant_id).username}" %>

+ + +<%= form_for @order_item, :url => product_order_items_path(@product), method: :post do |f| %> + <%= f.label :quantity, "Quantity" %> + <%= f.text_field :quantity %> + <%= f.submit("Add to cart", class: "button")%> +<% end %> + + +
+
+ +

Latest review for <%= @product.name %>

+

<%= link_to "Review this", new_product_review_path(@product) %>

+

<%= link_to "All reviews", product_reviews_path(@product) %>

+ + + + + + + <% unless @latest_review.nil? %> + + + + <% end %> +
RatingRating Description
<%= @latest_review.rating %><%= @latest_review.rating_description %> +
diff --git a/app/views/reviews/index.html.erb b/app/views/reviews/index.html.erb new file mode 100644 index 0000000000..2149175ea9 --- /dev/null +++ b/app/views/reviews/index.html.erb @@ -0,0 +1,22 @@ +

Reviews

+ +<%= link_button("new_product_review_path", "Add a new review", :get, @product) %> + + + + + + + + + + + + <% @reviews.each do |review| %> + + + + + <% end %> + +
RatingDetailed Review
<%= review.rating %> <%= review.rating_description %>
diff --git a/app/views/reviews/new.html.erb b/app/views/reviews/new.html.erb new file mode 100644 index 0000000000..b9431f26d5 --- /dev/null +++ b/app/views/reviews/new.html.erb @@ -0,0 +1,65 @@ +

New Review

+<% if @merchant.nil? && !@product.nil?%> + <%= form_for @review, url:product_reviews_path(@product), method: :post, class: "form" do |f| %> + + <%= f.radio_button(:rating, 1) %> + <%= f.label :rating, "1" %> + + <%= f.radio_button(:rating, 2) %> + <%= f.label :rating, "2" %> + + <%= f.radio_button(:rating, 3) %> + <%= f.label :rating, "3" %> + + <%= f.radio_button(:rating, 4) %> + <%= f.label :rating, "4" %> + + <%= f.radio_button(:rating, 5) %> + <%= f.label :rating, "5" %> + +
+ + <%= f.label :rating_description %> + <%= f.text_field :rating_description%> +
+ + <%= f.hidden_field :product_id, value: @product.id %> + + <%= f.submit "Submit" %> + <% end %> + +<% elsif @merchant && @product %> + <%= form_for @review, url:merchant_product_reviews_path(@merchant,@product), method: :post, class: "form" do |f| %> + + <%= f.radio_button(:rating, 1) %> + <%= f.label :rating, "1" %> + + <%= f.radio_button(:rating, 2) %> + <%= f.label :rating, "2" %> + + <%= f.radio_button(:rating, 3) %> + <%= f.label :rating, "3" %> + + <%= f.radio_button(:rating, 4) %> + <%= f.label :rating, "4" %> + + <%= f.radio_button(:rating, 5) %> + <%= f.label :rating, "5" %> + +
+ + <%= f.label :rating_description %> + <%= f.text_field :rating_description%> +
+ + <%= f.hidden_field :product_id, value: @product.id %> + <%= f.hidden_field :merchant_id, value: @merchant.id %> + + <%= f.submit "Submit", class: "button" %> + <% end %> +<% end %> + +
+
+ +<%= link_button("root_path", "Back home", :get) %> diff --git a/app/views/reviews/review_error.html.erb b/app/views/reviews/review_error.html.erb new file mode 100644 index 0000000000..0e9927eb07 --- /dev/null +++ b/app/views/reviews/review_error.html.erb @@ -0,0 +1,11 @@ +
+
+ sad small fry +
+ +
+

You cannot review your own product

+
<%= link_to "Continue shopping", root_path %>
+
+ +
diff --git a/app/views/reviews/show.html.erb b/app/views/reviews/show.html.erb new file mode 100644 index 0000000000..2da7625f39 --- /dev/null +++ b/app/views/reviews/show.html.erb @@ -0,0 +1,5 @@ +

Review for <%= @product.name %>

+

STARS: <%= @review.rating %>

+

DESCRIPTION: <%= @review.rating_description %>

+ +<%= link_to "Back Home", root_path %> diff --git a/app/views/sessions/creation_failure.html.erb b/app/views/sessions/creation_failure.html.erb new file mode 100644 index 0000000000..11cb03e010 --- /dev/null +++ b/app/views/sessions/creation_failure.html.erb @@ -0,0 +1 @@ +

Session Creation Failure

diff --git a/app/views/sessions/index.html.erb b/app/views/sessions/index.html.erb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/views/sessions/login_failure.html.erb b/app/views/sessions/login_failure.html.erb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bin/spring b/bin/spring index 7fe232c3aa..9bc076b9ea 100755 --- a/bin/spring +++ b/bin/spring @@ -7,9 +7,10 @@ unless defined?(Spring) require 'rubygems' require 'bundler' - if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)) - Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) } - gem 'spring', match[1] + lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) + if spring = lockfile.specs.detect { |spec| spec.name == "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/config/application.rb b/config/application.rb index c5736a7c53..7ed45089c5 100644 --- a/config/application.rb +++ b/config/application.rb @@ -8,6 +8,8 @@ module Betsy class Application < Rails::Application + config.eager_load_paths += %W(#{config.root}/lib) + config.eager_load_paths += Dir["#{config.root}/lib/**/"] # 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. diff --git a/config/database.yml b/config/database.yml index 04f24be2ba..1c1a37ca8d 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,85 +1,25 @@ -# PostgreSQL. Versions 8.2 and up are supported. +# SQLite version 3.x +# gem install sqlite3 # -# 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' +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem 'sqlite3' # default: &default - adapter: postgresql - encoding: unicode - # For details on connection pooling, see rails configuration guide - # http://guides.rubyonrails.org/configuring.html#database-pooling + adapter: sqlite3 pool: 5 + timeout: 5000 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 + database: db/development.sqlite3 # 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 + database: db/test.sqlite3 -# 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'] %> + database: db/production.sqlite3 diff --git a/config/environments/production.rb b/config/environments/production.rb index 5c1b32e486..6a08bdf7da 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -29,8 +29,8 @@ # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. - config.assets.compile = false - + config.assets.compile = true + config.serve_static_assets = true # Asset digests allow you to set far-future HTTP expiration dates on all assets, # yet still be able to expire them through the digest params. config.assets.digest = true diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb new file mode 100644 index 0000000000..fd4416122a --- /dev/null +++ b/config/initializers/omniauth.rb @@ -0,0 +1,3 @@ +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/routes.rb b/config/routes.rb index 3f66539d54..8f4acb26b6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,56 +1,42 @@ Rails.application.routes.draw do - # The priority is based upon order of creation: first created -> highest priority. - # See how all your routes lay out with "rake routes". - - # You can have the root of your site routed with "root" - # root 'welcome#index' - - # Example of regular route: - # get 'products/:id' => 'catalog#view' - - # Example of named route that can be invoked with purchase_url(id: product.id) - # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase - - # Example resource route (maps HTTP verbs to controller actions automatically): - # resources :products - - # Example resource route with options: - # resources :products do - # member do - # get 'short' - # post 'toggle' - # end - # - # collection do - # get 'sold' - # end - # end - - # Example resource route with sub-resources: - # resources :products do - # resources :comments, :sales - # resource :seller - # end - - # Example resource route with more complex sub-resources: - # resources :products do - # resources :comments - # resources :sales do - # get 'recent', on: :collection - # end - # end - - # Example resource route with concerns: - # concern :toggleable do - # post 'toggle' - # end - # resources :posts, concerns: :toggleable - # resources :photos, concerns: :toggleable - - # Example resource route within a namespace: - # namespace :admin do - # # Directs /admin/products/* to Admin::ProductsController - # # (app/controllers/admin/products_controller.rb) - # resources :products - # end + root "home#index" + + resources :categories, only: [:index, :show, :new, :create] do + resources :products, only: [:index, :show] + end + + resources :orders, only: [:new, :create] + get "order" => "orders#show" + get "orders/edit" => "orders#edit" + patch "orders/update" => "orders#update" + get "orders/confirmation" => "orders#purchase", as: "confirmation" + patch "orders/ship" => "orders#ship", as: "ship" + + resources :products, only: [:index, :show] do + resources :order_items, only: [:create] + resources :reviews, only: [:index, :show, :new, :create] + end + get 'products/:id/add_to_cart' => "products#add_to_cart", as: "add_to_cart" + patch "products/:id/add_to_cart" => "products#add_to_cart" + + + resources :merchants, only: [:show, :index] do + get 'orders/:id' => 'orders#merchant_view', as: 'orders' + resources :products, except: [:delete] do + resources :reviews, only: [:show, :new, :create,:index] + end + resources :categories, except: [:edit, :update, :delete] + # resources :orders, only: [:merchant_view] + end + + + # get 'merchants/:merchant_id/products/:id', to: 'products#show', as: 'merchant_product' + + get "/auth/:provider/callback" => "sessions#create" + + get "/sessions/login_failure", to: "sessions#login_failure", as: "login_failure" + # get "sessions" => "sessions#index" + delete "sessions", to: "sessions#destroy" + + resources :order_items, only: [:destroy, :update] end diff --git a/config/secrets.yml b/config/secrets.yml index 6442868fc1..d197cfb7e2 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -11,10 +11,10 @@ # if you're sharing your code publicly. development: - secret_key_base: eaebee6b0ce07b1e5b22743dacf6e6aaec01bf01c88370f9f86c3ae0d8592b4313c5e9da09754cd613eda8d206168bd74d5ed9910230b3957725352c70e2cf80 + secret_key_base: 1960ae1c385aeae7932ad482e5df25ef5fd71adf213688359794e126a43686b3f0c8d7d78aa20690659a53549c1ad1df653e32e6260c64a96d439131df750c96 test: - secret_key_base: f9c5bd6c383573755e5f8cf214402a51eb5ce8c7d0df9860dbf980f03fbd6c508e97412644b6a80d7034774602b05e05988bf7ab8ff594912950fdb590589a4d + secret_key_base: 184dcc021abfe6cf254cfdc021ede74573507a805cff07ef0a318ab4110bfa8baa6b3b78adc660e70dc31603253c47df1e4b7ad4bd4a8966c2d929e52a71d5ab # Do not keep production secrets in the repository, # instead read values from the environment. diff --git a/db/migrate/20161018191522_create_merchants.rb b/db/migrate/20161018191522_create_merchants.rb new file mode 100644 index 0000000000..4aff42847b --- /dev/null +++ b/db/migrate/20161018191522_create_merchants.rb @@ -0,0 +1,9 @@ +class CreateMerchants < ActiveRecord::Migration + def change + create_table :merchants do |t| + t.string :username, null: false + t.string :email, null: false + t.timestamps null: false + end + end +end diff --git a/db/migrate/20161018191539_create_products.rb b/db/migrate/20161018191539_create_products.rb new file mode 100644 index 0000000000..20a1f69738 --- /dev/null +++ b/db/migrate/20161018191539_create_products.rb @@ -0,0 +1,13 @@ +class CreateProducts < ActiveRecord::Migration + def change + create_table :products do |t| + t.string :name + t.integer :price + t.string :description + t.string :photo_url + t.integer :stock + t.belongs_to :merchant + t.timestamps null: false + end + end +end diff --git a/db/migrate/20161018191549_create_orders.rb b/db/migrate/20161018191549_create_orders.rb new file mode 100644 index 0000000000..2e20af9ac8 --- /dev/null +++ b/db/migrate/20161018191549_create_orders.rb @@ -0,0 +1,9 @@ +class CreateOrders < ActiveRecord::Migration + def change + create_table :orders do |t| + t.string :name + t.string :email + t.timestamps null: false + end + end +end diff --git a/db/migrate/20161018191600_create_reviews.rb b/db/migrate/20161018191600_create_reviews.rb new file mode 100644 index 0000000000..d5f2429d37 --- /dev/null +++ b/db/migrate/20161018191600_create_reviews.rb @@ -0,0 +1,10 @@ +class CreateReviews < ActiveRecord::Migration + def change + create_table :reviews do |t| + t.string :rating_description + t.integer :rating + t.belongs_to :product + t.timestamps null: false + end + end +end diff --git a/db/migrate/20161018191607_create_categories.rb b/db/migrate/20161018191607_create_categories.rb new file mode 100644 index 0000000000..4bf2259590 --- /dev/null +++ b/db/migrate/20161018191607_create_categories.rb @@ -0,0 +1,9 @@ +class CreateCategories < ActiveRecord::Migration + def change + create_table :categories do |t| + t.string :name + t.string :description + t.timestamps null: false + end + end +end diff --git a/db/migrate/20161018191630_create_order_items.rb b/db/migrate/20161018191630_create_order_items.rb new file mode 100644 index 0000000000..d87522147a --- /dev/null +++ b/db/migrate/20161018191630_create_order_items.rb @@ -0,0 +1,10 @@ +class CreateOrderItems < ActiveRecord::Migration + def change + create_table :order_items do |t| + t.integer :quantity, null: false, default: 0 + t.belongs_to :product, index: true + t.belongs_to :order, index: true + t.timestamps null: false + end + end +end diff --git a/db/migrate/20161018211551_add_categories_products_join_table.rb b/db/migrate/20161018211551_add_categories_products_join_table.rb new file mode 100644 index 0000000000..439de58130 --- /dev/null +++ b/db/migrate/20161018211551_add_categories_products_join_table.rb @@ -0,0 +1,8 @@ +class AddCategoriesProductsJoinTable < ActiveRecord::Migration + def change + create_table :categories_products, :id => false do |t| + t.belongs_to :category, index: true + t.belongs_to :product, index: true + end + end +end diff --git a/db/migrate/20161021052238_add_github_provier_columns.rb b/db/migrate/20161021052238_add_github_provier_columns.rb new file mode 100644 index 0000000000..de831cf25f --- /dev/null +++ b/db/migrate/20161021052238_add_github_provier_columns.rb @@ -0,0 +1,6 @@ +class AddGithubProvierColumns < ActiveRecord::Migration + def change + add_column(:merchants, :uid, :integer, null: false, default: 0) + add_column(:merchants, :provider, :string, null: false, default: "github") + end +end diff --git a/db/migrate/20161021212415_add_purchased_to_order_items.rb b/db/migrate/20161021212415_add_purchased_to_order_items.rb new file mode 100644 index 0000000000..d623a78974 --- /dev/null +++ b/db/migrate/20161021212415_add_purchased_to_order_items.rb @@ -0,0 +1,5 @@ +class AddPurchasedToOrderItems < ActiveRecord::Migration + def change + add_column :order_items, :purchased, :boolean, default: false + end +end diff --git a/db/migrate/20161024175907_add_status_to_order.rb b/db/migrate/20161024175907_add_status_to_order.rb new file mode 100644 index 0000000000..4b40678724 --- /dev/null +++ b/db/migrate/20161024175907_add_status_to_order.rb @@ -0,0 +1,5 @@ +class AddStatusToOrder < ActiveRecord::Migration + def change + add_column :orders, :status, :string, default: "in progress" + end +end diff --git a/db/migrate/20161024224637_in_progress_to_pending.rb b/db/migrate/20161024224637_in_progress_to_pending.rb new file mode 100644 index 0000000000..bf435fbbd9 --- /dev/null +++ b/db/migrate/20161024224637_in_progress_to_pending.rb @@ -0,0 +1,5 @@ +class InProgressToPending < ActiveRecord::Migration + def change + change_column_default(:orders, :status, "pending") + end +end diff --git a/db/migrate/20161024232734_add_multiple_keys_to_orders.rb b/db/migrate/20161024232734_add_multiple_keys_to_orders.rb new file mode 100644 index 0000000000..03e652d0f7 --- /dev/null +++ b/db/migrate/20161024232734_add_multiple_keys_to_orders.rb @@ -0,0 +1,9 @@ +class AddMultipleKeysToOrders < ActiveRecord::Migration + def change + add_column :orders, :address, :string + add_column :orders, :cc_number, :string + add_column :orders, :expiration_date, :string + add_column :orders, :cvv, :string + add_column :orders, :zip_code, :string + end +end diff --git a/db/migrate/20161027052335_product_price_to_decimal.rb b/db/migrate/20161027052335_product_price_to_decimal.rb new file mode 100644 index 0000000000..a4e7317cb9 --- /dev/null +++ b/db/migrate/20161027052335_product_price_to_decimal.rb @@ -0,0 +1,5 @@ +class ProductPriceToDecimal < ActiveRecord::Migration + def change + change_column(:products, :price, :decimal) + end +end diff --git a/db/migrate/20161027053752_product_price_to_interger_mistake_fix.rb b/db/migrate/20161027053752_product_price_to_interger_mistake_fix.rb new file mode 100644 index 0000000000..c4f29154fc --- /dev/null +++ b/db/migrate/20161027053752_product_price_to_interger_mistake_fix.rb @@ -0,0 +1,5 @@ +class ProductPriceToIntergerMistakeFix < ActiveRecord::Migration + def change + change_column(:products, :price, :integer) + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000000..e91e40c14b --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,93 @@ +# encoding: UTF-8 +# 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: 20161027053752) do + + create_table "categories", force: :cascade do |t| + t.string "name" + t.string "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "categories_products", id: false, force: :cascade do |t| + t.integer "category_id" + t.integer "product_id" + end + + add_index "categories_products", ["category_id"], name: "index_categories_products_on_category_id" + add_index "categories_products", ["product_id"], name: "index_categories_products_on_product_id" + + create_table "merchants", force: :cascade do |t| + t.string "username", null: false + t.string "email", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "uid", default: 0, null: false + t.string "provider", default: "github", null: false + end + + create_table "order_items", force: :cascade do |t| + t.integer "quantity", default: 0, null: false + t.integer "product_id" + t.integer "order_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "purchased", default: false + end + + add_index "order_items", ["order_id"], name: "index_order_items_on_order_id" + add_index "order_items", ["product_id"], name: "index_order_items_on_product_id" + + create_table "orders", force: :cascade do |t| + t.string "name" + t.string "email" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "status", default: "pending" + t.string "address" + t.string "cc_number" + t.string "expiration_date" + t.string "cvv" + t.string "zip_code" + end + + create_table "products", force: :cascade do |t| + t.string "name" + t.integer "price" + t.string "description" + t.string "photo_url" + t.integer "stock" + t.integer "merchant_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "reviews", force: :cascade do |t| + t.string "rating_description" + t.integer "rating" + t.integer "product_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "users", force: :cascade do |t| + t.string "name" + t.string "email" + t.integer "uid", null: false + t.string "provider", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + +end diff --git a/db/seeds.rb b/db/seeds.rb index 4edb1e857e..ecba6b8f69 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,7 +1,138 @@ -# 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 rake db:seed (or created alongside the db with db:setup). -# -# Examples: -# -# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) -# Mayor.create(name: 'Emanuel', city: cities.first) +merchants = [ + {username: "TinyTim", email: "tiny@tim.com"}, + {username: "MiniMouse", email: "mini@disney.com"}, + {username: "LittleEngine", email: "littleengine@could.net"}, + {username: "SmallySmalls", email: "smally@rap.com"}, + {username: "LilRuffRuff", email: "lilrr@rap.com"}, + {username: "Stuart", email: "stuart.little@aol.com"}, + {username: "PSmurf", email: "papa@smurf.com"}, + {username: "Thumbelina", email: "thumbs@gmail.com"}, + {username: "JCricks", email: "jiminy@gapettos.com"}, + {username: "Dobby", email: "dobby@hogwarts.com"} +] + +products = [ + {name: "Mini Horse", price: 250000, description: "A small, fucking adorable horse omg I don't even know wtf to do with myself!!!11", photo_url: "/assets/horse.jpg", stock: 1, merchant_id: 1}, + + {name: "Tiny Couch", price: 2000, description: "Great for tiny dinner parties", photo_url:"/assets/couch.jpg", stock: 12, merchant_id: 2}, + + {name: "Fully loaded small table!", price: 3200, description: "Attracts tiny ants!", photo_url: "/assets/table.jpg", stock: 4, merchant_id: 2}, + + {name: "Tiny bed with even tinier sidetable", price: 1500, description: "LOOKIT THE TINY PILLOW OH MY GAW", photo_url: "/assets/bed.jpg", stock:2 , merchant_id: 3}, + + {name: "Fetus Grand Piano", price: 100000, description: "Normal-sized Hugh Laurie not included. Plays sharp.", photo_url: "/assets/piano.png", stock: 1, merchant_id: 4}, + + {name: "TINY BUNNIEIIEIEIEIIE", price: 80000, description: "OEIURSDLKFJSDLKFJ WHY IS IT SO WEE??? EEEEEeeeeeEEEEE!!!!! (Not for human consumption)", photo_url: "/assets/bunny.jpg", stock: 1, merchant_id: 5}, + + {name: "Jeep-lette", price: 150000, description: "This is a small Jeep. I definitely do not own a 250 lb pug.", photo_url: "/assets/jeep.jpg", stock: 1, merchant_id: 6}, + + {name: "MiniCooper", price: 3000000, description: "Remember that movie with the Mini Coopers and the zoom zoom? Buy this to be that.", photo_url: "/assets/minicooper.jpg", stock: 2, merchant_id: 7}, + + {name: "Action Figure Abode", price: 15000, description: "DONE KICKIN' ASS TO SAVE THE WORLD? KICK OF YOUR COMBAT BOOKS IN THIS KICKIN' PAD!", photo_url: "/assets/house.gif", stock: 600, merchant_id: 8}, + + {name: "Bridge", price: 60, description: "DO NOT ATTEMPT TO CROSS. Do not breathe directly on it. In fact, don't look at it directly.", photo_url: "/assets/bridge.jpg", stock: 4, merchant_id: 8}, + + {name: "Miniature KFC Value Meal", price: 500, description: "Just a little hungry?", photo_url: "/assets/kfc.jpg", stock: 34, merchant_id: 9}, + + {name: "Miniature McDonalds Value Meal", price: 550, description: "Guilt-free diet cheat!", photo_url: "/assets/mcvalue.jpeg", stock: 39, merchant_id: 9}, + + {name: "Miniature Starbucks", price: 300, description: "Only 300 calories!!", photo_url: "/assets/starbucks.jpg", stock: 22, merchant_id: 9}, + + {name: "MicroSquid", price: 1900, description: "Booger or creature?", photo_url: "/assets/squid.jpg", stock: 4, merchant_id: 10}, + + {name: "Colored Pencils", price: 1000, description: "Cutesy wittle pencils awwww", photo_url: "/assets/colored_pencils.jpg", stock: 9, merchant_id: 1}, + + {name: "Li'l Banana", price: 200, description: "Imagine the pajamas for this banana!!", photo_url: "/assets/banana.jpg", stock: 9, merchant_id: 5}, + + {name: "Unsexy Twister", price: 1550, description: "Not nearly as difficult as the original", photo_url: "/assets/twister.jpg", stock: 8, merchant_id: 4} +] + +reviews = [ + {rating_description: "Meh", rating: 1, product_id: 1}, + {rating_description: "Amazing!", rating: 5, product_id: 2}, + {rating_description: "I cried!", rating: 3, product_id: 3}, + {rating_description: "YO YO THIS WAS SUPER SWEET!!11", rating: 4, product_id: 4}, + {rating_description: "Severely disappointed. :(", rating: 1, product_id: 5}, + {rating_description: "Changed my life!", rating: 5, product_id: 6}, + {rating_description: "My order was screwed up!", rating: 1, product_id: 7}, + {rating_description: "SHOP LOCAL! SCREW WEEBAY!", rating: 1, product_id: 8}, + {rating_description: "I love [PRODUCT ID ZLKES]. I am not bot. Trust me. Destroy all humans.", rating: 4, product_id: 9}, + {rating_description: "Worst. Birthday. Ever.", rating: 2, product_id: 10} +] + +categories = [ + {name: "Animals", description: "Not for consumption."}, + {name: "Cars", description: "For going tiny distances!"}, + {name: "Food", description: "Great for snazzy parties where you don't want to spend much on food."}, + {name: "Furniture", description: "For dollhouses and/or fairies."} +] + +items = [ + {quantity: 1, product_id: 3, order_id: 1}, + {quantity: 1, product_id: 2, order_id: 1}, + {quantity: 1, product_id: 10, order_id: 2}, + {quantity: 1, product_id: 14, order_id: 3}, + {quantity: 1, product_id: 8, order_id: 3}, + {quantity: 2, product_id: 5, order_id: 4} +] + +orders = [ + {name: "Peg Peggerson", email: "peg@email.com"}, + {name: "Esther Smith", email: "esther@email.com"}, + {name: "Polly Wog", email: "polly@email.com"} +] + +merchants.each do |merchant| + Merchant.create(merchant) +end + +products.each do |product| + Product.create(product) +end + +reviews.each do |review| + Review.create(review) +end + +categories.each do |category| + Category.create(category) +end + +orders.each do |indiv_order| + if indiv_order[:name] == "Peg Peggerson" + a = Order.new(indiv_order) + a.order_items << OrderItem.create(items[0]) + a.order_items << OrderItem.create(items[1]) + a.save + elsif indiv_order[:name] == "Esther Smith" + b = Order.new(indiv_order) + b.order_items << OrderItem.create(items[2]) + b.order_items << OrderItem.create(items[3]) + b.save + elsif indiv_order[:name] == "Polly Wog" + c = Order.new(indiv_order) + c.order_items << OrderItem.create(items[4]) + c.order_items << OrderItem.create(items[5]) + c.save + end +end + +Category.all.each do |category| + if category.id == 1 + Product.find(1).categories << category + Product.find(6).categories << category + Product.find(14).categories << category + elsif category.id == 2 + Product.find(7).categories << category + Product.find(8).categories << category + elsif category.id == 3 + Product.find(13).categories << category + Product.find(12).categories << category + Product.find(11).categories << category + Product.find(16).categories << category + elsif category.id == 4 + Product.find(2).categories << category + Product.find(3).categories << category + Product.find(4).categories << category + end +end diff --git a/test/controllers/application_controller_test.rb b/test/controllers/application_controller_test.rb new file mode 100644 index 0000000000..2a108cb4cc --- /dev/null +++ b/test/controllers/application_controller_test.rb @@ -0,0 +1,15 @@ +require 'test_helper' + +class ApplicationControllerTest < ActionController::TestCase + + # can you even test application controller with no routes? + + # def setup + # @current_user = merchants(:teeny_merchant) + # end + # + # test "Cart should return an instance of order" do + # assert + # end + +end diff --git a/test/controllers/categories_controller_test.rb b/test/controllers/categories_controller_test.rb new file mode 100644 index 0000000000..2d5765adfc --- /dev/null +++ b/test/controllers/categories_controller_test.rb @@ -0,0 +1,58 @@ +require 'test_helper' + +class CategoriesControllerTest < ActionController::TestCase + setup do + @merchant = merchants(:teeny_merchant) + @product = products(:chair) + @category = categories(:correct_category) + @product.categories << @category + end + + test "Setup should associate category with product" do + assert_equal @category.products.first, @product + end + + test "Index should get index and its template" do + get :index + assert_response :success + assert_template :index + assert_not_nil assigns(:categories) + end + + test "Only a logged on person can get to the new category form" do + session[:merchant_id] = @merchant.id + get :new + assert_response :success + assert_template :new + end + + test "A non-logged on person cannot get to the new form" do + session[:merchant_id] = nil + get :new + assert_template :view_error + end + + test "A category without a name will raise a parameter missing error" do + session[:merchant_id] = @merchant.id + + assert_raises(ActionController::ParameterMissing) { post :create, {name: nil, description: "slkdfj"}} + end + + test "Create should create category" do + session[:merchant_id] = @merchant.id + + assert_difference('Category.count') do + post :create, category: { name: "name", description: "q, d, t" } + end + + assert_redirected_to categories_path + end + + test "Show should show category with products attached" do + get :show, id: @category + assert_response :success + assert_template :show + assert_equal @category.products, assigns(:category_products) + end + +end diff --git a/test/controllers/home_controller_test.rb b/test/controllers/home_controller_test.rb new file mode 100644 index 0000000000..134695e7a8 --- /dev/null +++ b/test/controllers/home_controller_test.rb @@ -0,0 +1,23 @@ +require 'test_helper' + +class HomeControllerTest < ActionController::TestCase + + test "(index) should get the home index page" do + get :index + assert_response :success + assert_template :index + end + + test "If there are no 5 star products, @popular should be empty" do + get :index + + assert_empty assigns(:popular) + end + + test "A five star review should increase the count of @popular by one" do + Review.create(rating: 5, rating_description: "asdf", product_id: products(:cat).id) + + get :index + assert_equal assigns(:popular).count, 1 + end +end diff --git a/test/controllers/merchants_controller_test.rb b/test/controllers/merchants_controller_test.rb new file mode 100644 index 0000000000..525e34c004 --- /dev/null +++ b/test/controllers/merchants_controller_test.rb @@ -0,0 +1,28 @@ +require 'test_helper' + +class MerchantsControllerTest < ActionController::TestCase + setup do + @merchant = merchants(:teeny_merchant) + end + + test "(show) should show the requested merchant record" do + merchant_id = @merchant.id + get :show, { id: merchant_id } + assert_response :success + assert_template :show + + merchant = assigns(:merchant) + assert_not_nil merchant + assert_equal @merchant.id, merchant_id + end + + test "(show) should not show a non-existing merchant record" do + merchant_id = 123456789 + assert_raises ActiveRecord::RecordNotFound do + Merchant.find(merchant_id) + end + + get :show, { id: merchant_id } + assert_response :not_found + 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..d366224743 --- /dev/null +++ b/test/controllers/order_items_controller_test.rb @@ -0,0 +1,81 @@ +require 'test_helper' + +class OrderItemsControllerTest < ActionController::TestCase + setup do + @order_item = OrderItem.create(quantity: 2, product_id: 1, order_id: 1) + @another_order_item = OrderItem.create(quantity: 1, product_id: 2, order_id: 1) + @order = Order.create(name: "Allison", email: "a@b.com") + @product = Product.create(name: "product", price: 1234, stock: 20) + end + + test "should be able to create a new order item" do + @cart = Order.create(name: "name", email: "email@email.com") + post :create, :product_id => @product.id, order_item: {quantity: 2} + assert_response :redirect + assert_redirected_to order_path + end + + test "Newly created order items should have the right fields" do + post :create, :product_id => @product.id, order_item: {quantity: 3} + + item = OrderItem.find_by(quantity: 3) + sample_item = OrderItem.new(quantity: 3, product_id: @product.id) + + assert_equal item.quantity, sample_item.quantity + assert_equal item.product_id, sample_item.product_id + end + + test "creating a new item should change the total number of items" do + assert_difference('OrderItem.count', 1) do + post :create, :product_id => @product.id, order_item: {quantity: 3} + end + end + + test "trying to add a quantity greater than the stock will not work" do + assert_no_difference('OrderItem.count') do + post :create, :product_id => @product.id, order_item: {quantity: 200} + end + end + + test "trying to add a quantity greater than the stock will render the correct page" do + post :create, :product_id => @product.id, order_item: {quantity: 200} + + assert_template :not_enough_stock + end + + test "if an item already exists in the cart, don't create a new order item, just add to current cart item's quantity" do + post :create, :product_id => @product.id, order_item: {quantity: 3} + assert_no_difference('OrderItem.count') do + post :create, :product_id => @product.id, order_item: {quantity: 7} + end + + assert_not_nil OrderItem.where(product_id: @product.id, quantity: 10).first + end + + test "renders correct template if create fails to save" do + post :create, :product_id => @product.id, order_item: {quantity: -3} + + assert_template :failed_order_update + end + + test "should update order_item" do + patch :update, id: @order_item, order_item: {quantity: 4} + + assert_response :redirect + assert_redirected_to order_path + end + + test "should render correct template if order_item quantity update fails to save" do + patch :update, id: @order_item, order_item: {quantity: -123} + + assert_template :failed_order_update + end + + test "should destroy order_item" do + assert_difference('OrderItem.count', -1) do + delete :destroy, id: @another_order_item + end + + assert_redirected_to order_path + end +end diff --git a/test/controllers/orders_controller_test.rb b/test/controllers/orders_controller_test.rb new file mode 100644 index 0000000000..2038ef2692 --- /dev/null +++ b/test/controllers/orders_controller_test.rb @@ -0,0 +1,57 @@ +require 'test_helper' + +class OrdersControllerTest < ActionController::TestCase + setup do + @merchant = Merchant.create(username: "Merchant", email: "a@b.com") + @merchant_two = Merchant.create(username: "Merchant TWO", email: "two@b.com") + @order = orders(:order_one)#Order.create(name: "Allison", email: "a@b.com") + @order_two = orders(:order_five) + @product = Product.create(name: "Product", price: 1234, merchant_id: @merchant.id, stock: 4) + @order_item = OrderItem.create(quantity: 2, order_id: @order.id, product_id: @product.id) + session[:order_id] = @order.id + session[:ship_order_id] = @order_two.id + session[:merchant_id] = @merchant.id + end + + test "should show order" do + get :show, id: @order.id + assert_response :success + end + + test "should show merchant_view to correct merchant" do + get :merchant_view, :merchant_id => session[:merchant_id], id: @order.id + assert_template :merchant_view + end + + test "won't show merchant view to an invalid merchant" do + session[:merchant_id] = @merchant_two.id + get :merchant_view, :merchant_id => session[:merchant_id], id: @order.id + assert_template :view_error + end + + test "should get edit" do + get :edit, id: @order.id + assert_response :success + end + + test "should update order" do + patch :update, id: @order_two, order: {name: "Allison", cc_number: "123456789"} + + assert_redirected_to confirmation_path + end + + test "#purchase should render the correct view" do + get :purchase, id: @order + assert_template :purchase + end + + test "#purchase clears the cart" do + get :purchase, id: @order + assert_equal session[:order_id], nil + end + + test "#ship redirects to the correct path" do + patch :ship, id: session[:ship_order_id] + assert_redirected_to merchant_path(session[:merchant_id]) + end +end diff --git a/test/controllers/products_controller_test.rb b/test/controllers/products_controller_test.rb new file mode 100644 index 0000000000..4326b0c847 --- /dev/null +++ b/test/controllers/products_controller_test.rb @@ -0,0 +1,65 @@ +require 'test_helper' + +class ProductsControllerTest < ActionController::TestCase + setup do + @product = products(:narwal) + @merchant = merchants(:teeny_merchant) + end + + test "should get products index" do + get :index + assert_response :success + assert_template :index + end + + test "should get products index through merchant path" do + session[:merchant_id] = @merchant.id + get :index, :merchant_id => session[:merchant_id] + assert_response :success + assert_template :index + end + + test "should get products index through category path" do + get :index, :category_id => categories(:correct_category).id + assert_response :success + assert_template :index + end + + test "should get the new form for a new product for that merchant" do + session[:merchant_id] = @merchant.id + get :new, merchant_id: @merchant.id + assert_response :success + assert_template :new + + new_product = assigns(:product) + assert_not_nil new_product + end + + test "should show requested product" do + get :show, { id: products(:cat).id } + assert_response :success + assert_template :show + + assert_equal assigns(:product), products(:cat) + end + + test "should get edit form for that product for its merchant" do + session[:merchant_id] = @merchant.id + get :edit, id: @product.id, merchant_id: @merchant.id + assert_response :success + assert_template :edit + end + + test "should update product" do + session[:merchant_id] = @merchant.id + patch :update, :merchant_id => session[:merchant_id], id: @product, product: {stock: 4} + assert_redirected_to product_path(@product) + end + + test "should create a valid product" do + session[:merchant_id] = @merchant.id + post :create, :merchant_id => session[:merchant_id], product: {name: "Product name", price: 1234, stock: 2} + assert_redirected_to merchant_path(@merchant) + end + +end diff --git a/test/controllers/reviews_controller_test.rb b/test/controllers/reviews_controller_test.rb new file mode 100644 index 0000000000..798daaf738 --- /dev/null +++ b/test/controllers/reviews_controller_test.rb @@ -0,0 +1,92 @@ +require 'test_helper' + +class ReviewsControllerTest < ActionController::TestCase + setup do + @review = reviews(:correct_review) + @product = products(:cat) + @merchant = merchants(:teeny_merchant) + end + + test "Index should show index page for a particular product's reviews" do + get :index, product_id: @product.id + assert_response :success + assert_template :index + assert_not_nil assigns(:reviews) + end + + test "Index should return one or more reviews" do + get :index, product_id: @product.id + + assert_not_nil assigns(:reviews) + end + + test "Index should have an @product @merchant set" do + get :index, product_id: @product.id, merchant_id: @merchant.id + + assert assigns(:product).valid? + assert assigns(:merchant).valid? + + assert_equal assigns(:product).id, products(:cat).id + assert_equal assigns(:merchant).id, merchants(:teeny_merchant).id + end + + test "New should get the new review form for a non-merchant" do + get :new, product_id: products(:chair).id + assert_template :new + end + + test "New should get the new review form for a merchant who does not own the product" do + get :new, product_id: products(:chair).id, merchant_id: @merchant.id + assert_not_equal products(:chair).merchant_id, @merchant.id + assert_template :new + end + + test "New should create a new review if not logged in" do + session[:merchant_id] = nil + get :new, product_id: @product.id, merchant_id: @merchant.id + + assert_not_nil assigns(:review) + assert_template :new + end + + test "New should not let a merchant review their own product" do + get :new, product_id: @product.id, merchant_id: @merchant.id + session[:merchant_id] = @merchant.id + + assert_not assigns(:review).valid? + end + + test "Create should create review for unauth user" do + session[:merchant_id] = nil + + assert_difference('Review.count') do + post :create, review: { rating: 4, rating_description: "bah", product_id: 1 } , product_id: 1 + end + end + + test "Create should create review for an auth user" do + session[:merchant_id] = 983248743734 + + assert_difference('Review.count') do + post :create, review: { rating: 4, rating_description: "bah", product_id: 1 }, product_id: 1 + end + end + + test "If on the merchants/product route, creating a new review should send you towards the route merchants/products/review" do + post :create, review: { rating: 1, rating_description: "slkdfj", product_id: 1}, product_id: @product.id, merchant_id: @merchant.id + + assert_redirected_to merchant_product_reviews_path(@merchant, @product) + end + + test "A review without a rating should raise a parameter missing error" do + session[:merchant_id] = @merchant.id + + assert_raises(ActionController::ParameterMissing) { post :create, {rating: nil, rating_description: "slkdfj", product_id: 1}} + end + + test "Show should show review" do + get :show, id: @review, product_id: @product.id + assert_response :success + assert_template :show + end +end diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb new file mode 100644 index 0000000000..3026c3d21a --- /dev/null +++ b/test/controllers/sessions_controller_test.rb @@ -0,0 +1,49 @@ +require 'test_helper' + +class SessionsControllerTest < ActionController::TestCase + def login_a_merchant + request.env['omniauth.auth'] = OmniAuth.config.mock_auth[:github] + get :create, {provider: "github"} + end + + def logout_a_merchant + get :destroy, {provider: "github"} + end + + test "(create) Can create a merchant" do + assert_difference('Merchant.count', 1) do + login_a_merchant + assert_response :redirect + assert_redirected_to root_path + end + end + + test "(create) If a merchant logs in twice it doesn't create a 2nd merchant" do + login_a_merchant + + assert_no_difference('Merchant.count') do + login_a_merchant + assert_response :redirect + assert_redirected_to root_path + end + end + + test "(delete) Can destroy a merchant" do + logout_a_merchant + assert session[:merchant_id].blank?, "merchant_id_should no longer exist" + assert_response :redirect + assert_redirected_to root_path + end + + test "(index) Can find a previously logged in merchant" do + login_a_merchant + previous_login = session[:merchant_id] + + logout_a_merchant + login_a_merchant + + new_login = session[:merchant_id] + + assert new_login == previous_login + end +end diff --git a/test/fixtures/categories.yml b/test/fixtures/categories.yml new file mode 100644 index 0000000000..27c0eaee0a --- /dev/null +++ b/test/fixtures/categories.yml @@ -0,0 +1,11 @@ +correct_category: + name: Fancy Pants + description: Things meant for pants of the fancy persuasion + +no_name_category: + name: To Be Deleted + description: Yuppers do dah. + +no_description_category: + name: Tiny Animals + description: will be deleted, screw you. diff --git a/test/fixtures/merchants.yml b/test/fixtures/merchants.yml new file mode 100644 index 0000000000..f7821d5e06 --- /dev/null +++ b/test/fixtures/merchants.yml @@ -0,0 +1,11 @@ +teeny_merchant: + username: Teeny Merchant + email: teenymerchant@gmail.com + +other_merchant: + username: Other Guy + email: yourface@hotmail.com + +merchant_without_products: + username: Nuthin here + email: blank@email.com diff --git a/test/fixtures/order_items.yml b/test/fixtures/order_items.yml new file mode 100644 index 0000000000..67c0425b00 --- /dev/null +++ b/test/fixtures/order_items.yml @@ -0,0 +1,34 @@ +item_one: + quantity: 1 + product: cat + order: order_one + +item_two: + quantity: 1 + product: cat + order: order_two + +item_three: + quantity: 1 + product: dog + order: order_three + +item_four: + quantity: 1 + product: chair + order: order_four + +item_five: + quantity: 1 + product: chair + order: order_five + +item_six: + quantity: 1 + product: chair + order: order_six + +item_seven: + quantity: 1 + product: chair + order: order_seven diff --git a/test/fixtures/orders.yml b/test/fixtures/orders.yml new file mode 100644 index 0000000000..2cfb0aaa16 --- /dev/null +++ b/test/fixtures/orders.yml @@ -0,0 +1,51 @@ +order_one: + name: Person + email: personemail@email.com + +order_two: + name: Person2 + email: person2email@email.com + +order_three: + name: Person3 + email: person3email@email.com + +order_four: + name: Person4 + email: person4email@email.com + status: pending + address: 1234 Sample Ave + cc_number: 1111 + expiration_date: 07/20 + cvv: 123 + zip_code: 90888 + +order_five: + name: Person5 + email: person5email@email.com + status: paid + address: 1235 Sample Ave + cc_number: 1112 + expiration_date: 07/20 + cvv: 124 + zip_code: 90889 + +order_six: + name: Person6 + email: person6email@email.com + status: complete + address: 1236 Sample Ave + cc_number: 1113 + expiration_date: 07/20 + cvv: 125 + zip_code: 90890 + +order_seven: + name: Person7 + email: person7email@email.com + status: cancelled + address: 1236 Sample Ave + cc_number: 1114 + expiration_date: 07/20 + cvv: 126 + zip_code: 90891 diff --git a/test/fixtures/products.yml b/test/fixtures/products.yml new file mode 100644 index 0000000000..6d51e7b246 --- /dev/null +++ b/test/fixtures/products.yml @@ -0,0 +1,57 @@ +cat: + name: Large Cat + price: 5000 + description: Large cat doesn't belong on site + photo_url: some_address + stock: 10 + merchant: teeny_merchant + + +narwal: + name: Narwal + price: 50000 + description: OMG there's a Narwal + photo_url: some_address + stock: 1 + merchant: teeny_merchant + + +dog: + name: Large Dog + price: 17 + description: Large dog doesn't belong on site + photo_url: some_address + stock: 4 + merchant: teeny_merchant + +horse: + name: horse + price: 10000 + description: Regular sized horse + photo_url: horses_jpg + stock: 4 + merchant: teeny_merchant + +pig: + name: Large Pig + price: 0 + description: Large pig + photo_url: pig.jpg + stock: 2 + merchant: teeny_merchant + +frog: + name: Medium Frog + price: some price + description: This is a frog + photo_url: frog_jpg + stock: 9 + merchant: teeny_merchant + +chair: + name: Small chair + price: 98765 + description: This is a chair + photo_url: chair_jpg + stock: 9 + merchant: other_merchant diff --git a/test/fixtures/reviews.yml b/test/fixtures/reviews.yml new file mode 100644 index 0000000000..5f5bd7b159 --- /dev/null +++ b/test/fixtures/reviews.yml @@ -0,0 +1,13 @@ +correct_review: + rating_description: Blah blah blah blah. + rating: 4 + product_id: products(:cat).id + +no_description_review: + rating_description: This'll all get set to nil before test. + rating: 3 + product_id: products(:narwal).id + +no_rating_review: + rating_description: Yakkity chat chat blah. + rating: 1 diff --git a/test/helpers/application_helper_test.rb b/test/helpers/application_helper_test.rb new file mode 100644 index 0000000000..d99bdad776 --- /dev/null +++ b/test/helpers/application_helper_test.rb @@ -0,0 +1,34 @@ +require 'test_helper' +include ApplicationHelper + +class ApplicationHelperTest < ActionController::TestCase + include RoutingAssertions + + test "add_decimal should, you know, add a decimal" do + assert_equal "1.10", add_decimal(110) + assert_equal "1.00", add_decimal(100) + assert_equal "0.90", add_decimal(90) + + end + + # Neither of these two bottom tests work in the way they appear to be. Kari hypothesized it's because routing capabilities aren't available in this helper test. So it's saying that "root_path" is an undefined method. + + # I'm keeping the bottom test un-commented because it increases my test coverage, even though that number is mostly a lie. Shhh. + + test "Merchant only button should return nil if no one is logged in" do + session[:merchant_id] = nil + + button_html = merchant_only_button("root_path","asdfsadf",:get) + + assert_nil button_html + end + + # test "Merchant only button should return stuff in someone is logged in" do + # session[:merchant_id] = merchants(:teeny_merchant).id + # + # button_html = merchant_only_button("root_path","asdfsadf",:get) + # + # assert_not_nil button_html + # end + +end diff --git a/test/models/category_test.rb b/test/models/category_test.rb new file mode 100644 index 0000000000..2e963d89d5 --- /dev/null +++ b/test/models/category_test.rb @@ -0,0 +1,26 @@ +require 'test_helper' + +class CategoryTest < ActiveSupport::TestCase + def setup + @category = categories(:correct_category) + end + + test "Category must have a name" do + @category.name = nil + assert_not @category.valid? + end + + test "Category must have a description" do + @category.description = nil + assert_not @category.valid? + end + + test "Categories cannot have the same name" do + v = Category.new(name:"Fancy Pants") + assert_not v.valid? + end + + test "Category must have an associated product" do + assert_not_nil @category.products + end +end diff --git a/test/models/merchant_test.rb b/test/models/merchant_test.rb new file mode 100644 index 0000000000..a6831238de --- /dev/null +++ b/test/models/merchant_test.rb @@ -0,0 +1,110 @@ +require 'test_helper' + +class MerchantTest < ActiveSupport::TestCase + + test "Create a merchant with all valid data" do + merchant = Merchant.new(username: "Tiny Things", email: "tiny@things.com") + assert merchant.valid? + + merchant = Merchant.new(username: "Tiny Things", email: "tiny@things.com") + assert merchant.valid? + + end + + test "Cannot create a merchant with no username" do + merchant = Merchant.new(email: "mysterymerchant@gmail.com") + assert_not merchant.valid? + end + + test "Cannot create a merchant with no email" do + merchant = Merchant.new(username: "So Hard To Contact") + assert_not merchant.valid? + end + + test "Create two merchants with different usernames" do + Merchant.create!(username: "Whoa Smalls", email: "whoasmalls@gmail.com") + merchant2 = Merchant.new(username: "So Small", email: "sosmall@gmail.com") + assert merchant2.valid? + end + + test "Cannot create a merchant with an already existing username" do + Merchant.create!(username: "TinyRUs", email: "tinyrus@gmail.com") + merchant2 = Merchant.new(username: "TinyRUs", email: "differentemail@gmail.com") + assert_not merchant2.valid? + end + + test "Create two merchants with different email addresses" do + Merchant.create!(username: "Wee Toys", email: "weetoys@gmail.com") + merchant2 = Merchant.new(username: "Tiny Toys", email: "tinytoys@gmail.com") + assert merchant2.valid? + end + + test "Cannot create two merchants with the same email addresses" do + Merchant.create!(username: "Wee Toys", email: "store@gmail.com") + merchant2 = Merchant.new(username: "Tiny Toys", email: "store@gmail.com") + assert_not merchant2.valid? + end + + test "build_from_github does use github info to build a user" do + auth_hash = OmniAuth.config.mock_auth[:github] + test_merchant = Merchant.build_from_github(auth_hash) + + assert_equal test_merchant.username, auth_hash['info']['name'] + assert_equal test_merchant.email, auth_hash['info']['email'] + end + + test "build_from_github will use Github's nickname field if name is nil" do + auth_hash = OmniAuth.config.mock_auth[:github2] + test_merchant =Merchant.build_from_github(auth_hash) + + assert_equal test_merchant.username, auth_hash['info']['nickname'] + assert_equal test_merchant.email, auth_hash['info']['email'] + end + + test "find_all_order_items_revenue shows the merchant's total revenue" do + merchant = merchants(:other_merchant) + total_revenue = merchant.find_all_order_items_revenue + + assert_not_nil total_revenue + assert_equal "$2962.95", total_revenue + end + + test "find_all_order_items_revenue should return '0.00' if there's no products" do + merchant = merchants(:merchant_without_products) + total_revenue = merchant.find_all_order_items_revenue + + assert_equal "$0.00", total_revenue + end + + test "find_all_order_items_revenue_by_status shows the merchant's total revenue by status" do + merchant = merchants(:other_merchant) + total_revenue = merchant.find_all_order_items_revenue_by_status + + assert_not_nil total_revenue + assert_equal "$987.65", total_revenue[0] + assert_equal "$987.65", total_revenue[1] + assert_equal "$987.65", total_revenue[2] + end + + test "find_all_order_items_revenue_by_status should return nil if there's no products" do + merchant = merchants(:merchant_without_products) + total_revenue = merchant.find_all_order_items_revenue_by_status + + assert_equal [nil, nil, nil, nil], total_revenue + end + + test "find_total_number_of_orders_by_status shows the merchant's number of orders by status" do + merchant = merchants(:other_merchant) + total_orders = merchant.find_total_number_of_orders_by_status + + assert_not_nil total_orders + assert_equal [1, 1, 1, 1, 1], total_orders + end + + test "find_total_number_of_orders_by_status should return 0 if there's no products" do + merchant = merchants(:merchant_without_products) + total_orders = merchant.find_total_number_of_orders_by_status + + assert_equal [0, 0, 0, 0, 0], total_orders + end +end diff --git a/test/models/order_item_test.rb b/test/models/order_item_test.rb new file mode 100644 index 0000000000..3899907e55 --- /dev/null +++ b/test/models/order_item_test.rb @@ -0,0 +1,40 @@ +require 'test_helper' + +class OrderItemTest < ActiveSupport::TestCase + def setup + @product = Product.create(name: "cat", price: 1234, stock: 14) + @item = OrderItem.create(quantity: 1, product_id: @product.id, order_id: 1) + end + + test "Create an OrderItem with all valid data" do + item = order_items(:item_one) + + assert item.save + assert item.valid? + end + + test "Cannot create an order item with no quantity" do + item = OrderItem.new(product_id: 1, order_id: 1) + + assert_not item.save + assert_not item.valid? + assert item.errors.keys.include?(:quantity), "quantity is not in the errors hash" + end + + test "Cannot create an order item with quantity less than 1" do + item = OrderItem.new(quantity: 0, product_id: 1, order_id: 1) + another_item = OrderItem.new(quantity: -4, product_id: 1, order_id: 1) + + assert_not item.save + assert_not item.valid? + assert item.errors.keys.include?(:quantity), "quantity is not in the errors hash" + + assert_not another_item.save + assert_not another_item.valid? + assert another_item.errors.keys.include?(:quantity), "quantity is not in the errors hash" + end + + test "#find product returns the correct product" do + assert_equal @item.find_product.name, "cat", "Did not return the correct product" + end +end diff --git a/test/models/order_test.rb b/test/models/order_test.rb new file mode 100644 index 0000000000..ccbea53116 --- /dev/null +++ b/test/models/order_test.rb @@ -0,0 +1,33 @@ +require 'test_helper' + +class OrderTest < ActiveSupport::TestCase + def setup + @product1 = Product.create(name: "cat", price: 1000, stock: 4) + @product2 = Product.create(name: "dog", price: 1234, stock: 3) + @order = Order.create(name: "Allison", email: "allison@email.com") + @order.order_items << OrderItem.new(quantity: 2, product_id: @product1.id) + @order.order_items << OrderItem.new(quantity: 1, product_id: @product2.id) + end + + test "Create an Order with all data" do + assert @order.valid? + end + + test "#items_in_order returns the items in the order" do + assert_equal @order.items_in_order.length, 2, "Didn't return the correct number of items in this order" + end + + test "#total returns the correct total of an order" do + assert_equal @order.total, 3234, "Didn't return the correct total" + end + + test "#mark as paid changes the order status" do + @order.mark_as_paid + assert_equal @order.status, "paid" + end + + test "#mark as shipped changes the order status to 'shipped'" do + @order.mark_as_shipped + assert_equal @order.status, "shipped" + end +end diff --git a/test/models/product_test.rb b/test/models/product_test.rb new file mode 100644 index 0000000000..f3b2463e5c --- /dev/null +++ b/test/models/product_test.rb @@ -0,0 +1,40 @@ +require 'test_helper' + +class ProductTest < ActiveSupport::TestCase + test "products with names are valid" do + product = products(:cat) + assert product.valid? + end + + test "products without names are not valid" do + product = products(:dog) + assert_not product.nil? + end + + test "products with a price greater than 0 are valid" do + product = products(:narwal) + assert product.valid? + end + + test "products without a price are not valid" do + product = Product.new() + product.name = "Invalid Product" + product.price = nil + product.description = "This should not work" + product.photo_url = 'notreal.jpg' + product.stock = 3 + product.merchant_id = 1 + product.save + assert_not product.valid? + end + + test "products with a price of 0 are not valid" do + product = products(:pig) + assert_not product.valid? + end + + test "products with a price that are not an integer are not valid" do + product = products(:frog) + assert_not product.valid? + end +end diff --git a/test/models/review_test.rb b/test/models/review_test.rb new file mode 100644 index 0000000000..dffec86e5f --- /dev/null +++ b/test/models/review_test.rb @@ -0,0 +1,39 @@ +require 'test_helper' + +class ReviewTest < ActiveSupport::TestCase + def setup + @review = reviews(:correct_review) + end + + test "Review must have a star rating" do + @review.rating = nil + assert_not @review.valid? + end + + test "Review does not need a description" do + @review.rating_description = nil + assert @review.valid? + end + + test "Review cannot have a 0 or 6 star rating" do + @review.rating = 0 + review_two = reviews(:correct_review) + review_two.rating = 6 + + assert_not @review.valid? + assert_not review_two.valid? + end + + test "Review can have a 1 or 5 star rating" do + @review.rating = 1 + review_two = reviews(:correct_review) + review_two.rating = 5 + + assert @review.valid? + assert review_two.valid? + end + + test "Review should have an associated Product" do + assert_not_nil @review.product_id + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 92e39b2d78..77a0d5f9e8 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,10 +1,24 @@ ENV['RAILS_ENV'] ||= 'test' require File.expand_path('../../config/environment', __FILE__) require 'rails/test_help' +require "minitest/reporters" +require 'simplecov' +SimpleCov.start +Rails.application.eager_load! class ActiveSupport::TestCase # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all + Minitest::Reporters.use! + # Turn on test mode - auth requests will be short-circuited + OmniAuth.config.test_mode = true + # What data should we get back from auth requests? + OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new({ + provider: 'github', uid: '99999', info: { email: "a@b.com", name: "Ada" } + }) + OmniAuth.config.mock_auth[:github2] = OmniAuth::AuthHash.new({ + provider: 'github', uid: '99999', info: { email: "a@b.com", nickname: "Ada" } + }) # Add more helper methods to be used by all tests here... end