diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000..3dc55f7803 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..b81ccc7094 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep +.env + +/node_modules +/yarn-error.log + +.byebug_history + +/coverage/ +.DS_Store diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000000..fc0c37073a --- /dev/null +++ b/Gemfile @@ -0,0 +1,76 @@ +source 'https://rubygems.org' + +git_source(:github) do |repo_name| + repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") + "https://github.com/#{repo_name}.git" +end + + +# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' +gem 'rails', '~> 5.1.6' +# Use postgresql as the database for Active Record +gem 'pg', '>= 0.18', '< 2.0' +# Use Puma as the app server +gem 'puma', '~> 3.7' +# Use SCSS for stylesheets +gem 'sass-rails', '~> 5.0' +# Use Uglifier as compressor for JavaScript assets +gem 'uglifier', '>= 1.3.0' +# See https://github.com/rails/execjs#readme for more supported runtimes +# gem 'therubyracer', platforms: :ruby + +# Use CoffeeScript for .coffee assets and views +# gem 'coffee-rails', '~> 4.2' +# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks +gem 'turbolinks', '~> 5' +# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder +gem 'jbuilder', '~> 2.5' +# Use Redis adapter to run Action Cable in production +# gem 'redis', '~> 4.0' +# Use ActiveModel has_secure_password +# gem 'bcrypt', '~> 3.1.7' + +# Use Capistrano for deployment +# gem 'capistrano-rails', group: :development +gem "omniauth" +gem "omniauth-google-oauth2", "~> 0.2.1" + +group :development, :test do + # Call 'byebug' anywhere in the code to stop execution and get a debugger console + gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] + # Adds support for Capybara system testing and selenium driver + gem 'capybara', '~> 2.13' + gem 'selenium-webdriver' +end + +group :development do + # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. + gem 'web-console', '>= 3.3.0' + gem 'listen', '>= 3.0.5', '< 3.2' + # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring + gem 'spring' + gem 'spring-watcher-listen', '~> 2.0.0' +end + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] + +gem 'jquery-turbolinks' +gem 'jquery-rails' +gem 'foundation-rails' +gem 'normalize-rails' +group :development, :test do + gem 'pry-rails' +end + +group :development do + gem 'better_errors' + gem 'binding_of_caller' + gem 'dotenv-rails' +end + +group :test do + gem 'minitest-rails' + gem 'minitest-reporters' + gem 'simplecov', require: false +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000000..4c5f117ca8 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,275 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (5.1.6) + actionpack (= 5.1.6) + nio4r (~> 2.0) + websocket-driver (~> 0.6.1) + actionmailer (5.1.6) + actionpack (= 5.1.6) + actionview (= 5.1.6) + activejob (= 5.1.6) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (5.1.6) + actionview (= 5.1.6) + activesupport (= 5.1.6) + rack (~> 2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.1.6) + activesupport (= 5.1.6) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (5.1.6) + activesupport (= 5.1.6) + globalid (>= 0.3.6) + activemodel (5.1.6) + activesupport (= 5.1.6) + activerecord (5.1.6) + activemodel (= 5.1.6) + activesupport (= 5.1.6) + arel (~> 8.0) + activesupport (5.1.6) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + ansi (1.5.0) + arel (8.0.0) + babel-source (5.8.35) + babel-transpiler (0.7.0) + babel-source (>= 4.0, < 6) + execjs (~> 2.0) + better_errors (2.4.0) + coderay (>= 1.0.0) + erubi (>= 1.0.0) + rack (>= 0.9.0) + bindex (0.5.0) + binding_of_caller (0.8.0) + debug_inspector (>= 0.0.1) + builder (3.2.3) + byebug (10.0.2) + capybara (2.18.0) + addressable + mini_mime (>= 0.1.3) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (>= 2.0, < 4.0) + childprocess (0.9.0) + ffi (~> 1.0, >= 1.0.11) + coderay (1.1.2) + concurrent-ruby (1.0.5) + crass (1.0.4) + debug_inspector (0.0.3) + docile (1.1.5) + dotenv (2.2.2) + dotenv-rails (2.2.2) + dotenv (= 2.2.2) + railties (>= 3.2, < 6.0) + erubi (1.7.1) + execjs (2.7.0) + faraday (0.12.2) + multipart-post (>= 1.2, < 3) + ffi (1.9.23) + foundation-rails (6.4.3.0) + railties (>= 3.1.0) + sass (>= 3.3.0, < 3.5) + sprockets-es6 (>= 0.9.0) + globalid (0.4.1) + activesupport (>= 4.2.0) + hashie (3.5.7) + i18n (1.0.1) + concurrent-ruby (~> 1.0) + jbuilder (2.7.0) + activesupport (>= 4.2.0) + multi_json (>= 1.2) + jquery-rails (4.3.3) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) + jquery-turbolinks (2.1.0) + railties (>= 3.1.0) + turbolinks + json (2.1.0) + jwt (1.5.6) + listen (3.1.5) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + ruby_dep (~> 1.2) + loofah (2.2.2) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.7.0) + mini_mime (>= 0.1.1) + method_source (0.9.0) + mini_mime (1.0.0) + mini_portile2 (2.3.0) + minitest (5.11.3) + minitest-rails (3.0.0) + minitest (~> 5.8) + railties (~> 5.0) + minitest-reporters (1.2.0) + ansi + builder + minitest (>= 5.0) + ruby-progressbar + multi_json (1.13.1) + multi_xml (0.6.0) + multipart-post (2.0.0) + nio4r (2.3.0) + nokogiri (1.8.2) + mini_portile2 (~> 2.3.0) + normalize-rails (4.1.1) + oauth2 (1.4.0) + faraday (>= 0.8, < 0.13) + jwt (~> 1.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (>= 1.2, < 3) + omniauth (1.8.1) + hashie (>= 3.4.6, < 3.6.0) + rack (>= 1.6.2, < 3) + omniauth-google-oauth2 (0.2.10) + addressable (~> 2.3) + jwt (~> 1.0) + multi_json (~> 1.3) + omniauth (>= 1.1.1) + omniauth-oauth2 (~> 1.3.1) + omniauth-oauth2 (1.3.1) + oauth2 (~> 1.0) + omniauth (~> 1.2) + pg (1.0.0) + pry (0.11.3) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + pry-rails (0.3.6) + pry (>= 0.10.4) + public_suffix (3.0.2) + puma (3.11.4) + rack (2.0.4) + rack-test (1.0.0) + rack (>= 1.0, < 3) + rails (5.1.6) + actioncable (= 5.1.6) + actionmailer (= 5.1.6) + actionpack (= 5.1.6) + actionview (= 5.1.6) + activejob (= 5.1.6) + activemodel (= 5.1.6) + activerecord (= 5.1.6) + activesupport (= 5.1.6) + bundler (>= 1.3.0) + railties (= 5.1.6) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) + railties (5.1.6) + actionpack (= 5.1.6) + activesupport (= 5.1.6) + method_source + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + rake (12.3.1) + rb-fsevent (0.10.3) + rb-inotify (0.9.10) + ffi (>= 0.5.0, < 2) + ruby-progressbar (1.9.0) + ruby_dep (1.5.0) + rubyzip (1.2.1) + sass (3.4.25) + sass-rails (5.0.7) + railties (>= 4.0.0, < 6) + sass (~> 3.1) + sprockets (>= 2.8, < 4.0) + sprockets-rails (>= 2.0, < 4.0) + tilt (>= 1.1, < 3) + selenium-webdriver (3.11.0) + childprocess (~> 0.5) + rubyzip (~> 1.2) + simplecov (0.15.1) + docile (~> 1.1.0) + json (>= 1.8, < 3) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.2) + spring (2.0.2) + activesupport (>= 4.2) + spring-watcher-listen (2.0.1) + listen (>= 2.7, < 4.0) + spring (>= 1.2, < 3.0) + sprockets (3.7.1) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-es6 (0.9.2) + babel-source (>= 5.8.11) + babel-transpiler + sprockets (>= 3.0.0) + sprockets-rails (3.2.1) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + thor (0.20.0) + thread_safe (0.3.6) + tilt (2.0.8) + turbolinks (5.1.1) + turbolinks-source (~> 5.1) + turbolinks-source (5.1.0) + tzinfo (1.2.5) + thread_safe (~> 0.1) + uglifier (4.1.9) + execjs (>= 0.3.0, < 3) + web-console (3.6.0) + actionview (>= 5.0) + activemodel (>= 5.0) + bindex (>= 0.4.0) + railties (>= 5.0) + websocket-driver (0.6.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.3) + xpath (3.0.0) + nokogiri (~> 1.8) + +PLATFORMS + ruby + +DEPENDENCIES + better_errors + binding_of_caller + byebug + capybara (~> 2.13) + dotenv-rails + foundation-rails + jbuilder (~> 2.5) + jquery-rails + jquery-turbolinks + listen (>= 3.0.5, < 3.2) + minitest-rails + minitest-reporters + normalize-rails + omniauth + omniauth-google-oauth2 (~> 0.2.1) + pg (>= 0.18, < 2.0) + pry-rails + puma (~> 3.7) + rails (~> 5.1.6) + sass-rails (~> 5.0) + selenium-webdriver + simplecov + spring + spring-watcher-listen (~> 2.0.0) + turbolinks (~> 5) + tzinfo-data + uglifier (>= 1.3.0) + web-console (>= 3.3.0) + +BUNDLED WITH + 1.16.1 diff --git a/README.md b/README.md index 2758975bf5..7db80e4ca1 100644 --- a/README.md +++ b/README.md @@ -1,205 +1,24 @@ -# bEtsy -[Much like other e-commerce platforms](https://www.etsy.com/), your team will make an online store where a wide variety of products can be listed and sold by any user. This project focuses on reinforcing the major components of Rails, model validations, testing, and more complex logic such as user authentication. +# README -This is a [Stage 3](https://github.com/Ada-Developers-Academy/pedagogy/blob/master/rule-of-three.md) project requiring you to expand upon what you have learned in class. +This README would normally document whatever steps are necessary to get the +application up and running. -## Project Learning Goals -- Core comprehension of: - - Routes - - Controllers - - Models - - Views -- User based application logic -- User authentication -- Testing on models and controllers -- Agile practices -- Feature branch management with Git -- Group project ownership +Things you may want to cover: -## Guidelines -- Groups of three or four will collaborate in pairs or individually and will report to their assigned Project Manager (one of the instructors) -- Use a task manager like [Trello](http://trello.com) to track your team's efforts -- Build a logical user-flow that moves across multiple controllers and models -- Use HTML/CSS and Foundation to style your website +* Ruby version -## Getting Started -1. As a group decide on an app name (this may help lead the aesthetic) -1. As a group decide on a team name (this will amuse your instructors) -1. Have one person on your team fork/clone the project master as per usual - 1. Create a new rails app using `rails new .` - 1. Add all other team members as collaborators - 1. Each team member should clone the repo to their computer -1. Figure out your workflow for the project, re: Git and Task management - 1. Do you want to use git branches? Pull requests? - 1. Determine who will be the Stand Up Leader and Task Leader for the first week -1. Create a Trello board to use as a Kanban board and ensure that all team members and instructors have access -1. Review the User Stories below and create Trello tasks to represent them -1. Slack your team name, app name, and link to your Trello board to your Project Manager +* System dependencies -## Expectations -Build an online system for listing, selling, reviewing, and buying a wide variety of products listed by multiple merchants. +* Configuration -### General Requirements -- Unit tests and/or specs for - - Models - - Controllers -- Test code coverage (using SimpleCov - remember me!) - - 90% for all controller and model classes +* Database creation -### User Stories -#### Guest User (Unauthenticated) -As a guest to the website (not signed in) I **can**: +* Database initialization -- Browse all products -- Browse products by category -- Browse products by merchant (users) -- View any individual product with additional details -- Leave a review for a product providing: - - A text review - - A rating out of 5 -- Add in-stock products to my cart -- Remove products from my cart -- Change the quantity of an existing product in my cart -- Purchase the items in my cart, providing: - - Email Address - - Mailing Address - - Name on credit card - - Credit card number - - Credit cart expiration - - Credit Card CVV (security code) - - Billing zip code -- Purchasing an order makes the following changes: - - Reduces the number of inventory for each product - - Changes the order state from "pending" to "paid" - - Clears the current cart -- After purchasing an order, I can view a confirmation screen including: - - Each item in the order with a quantity and line-item subtotal - - A link to the item description page - - Order total price - - Date/time the order was placed - - The current status of the order -- Sign up to be a merchant using OAuth - - Every merchant must have a username -- Sign in to my merchant account using OAuth +* How to run the test suite -As a guest I **cannot**: +* Services (job queues, cache servers, search engines, etc.) -- Add products to the cart that are out of stock -- View any link or page to manage any products -- View any of the account pages +* Deployment instructions -#### Authenticated Users -As a signed-in user, I **can**: - -- Do everything a guest user can do except for sign up and sign in -- Sign out -- Create new categories (categories are shared between all merchants) -- Create a new product providing: - - name - - description - - price - - photo URL - - stock -- Assign my products to any number of categories -- Retire a product from being sold, which hides it from browsing -- View an account page to edit/update my existing products -- View an account page showing my order fulfillment -- On the order fulfillment page: - - Total Revenue - - Total Revenue by status - - Total number of orders by status - - Filter orders displayed by status - - Link to each individual order - - A list of orders including at least one of my products: - - Each order item sold by me with a quantity and line-item subtotal - - A link to the item description page - - DateTime the order was placed - - Link to transition the order item to marked as shipped - - The current status of the order ("pending", "paid", "complete", "cancelled") -- View an individual order to see the user's: - - Name - - Email address - - Mailing address - - Last four digits of their credit card - - Credit card expiration date - -As a signed-in user, I **cannot**: - -- Review my own products -- View order items from a shared order that belong to another merchant -- View another user's private data (i.e. order fulfillment or product management) - -### Validations -Many of our models will have attributes that are required for our application to use and display data consistently. Each model will have attributes with requirements for a valid record. The requirements are summarized below: - -#### Merchant -- Username must be present -- Username must be unique -- Email Address must be present -- Email Address must be unique - -#### Product -- Name must be present -- Name must be unique -- Price must be present -- Price must be a number -- Price must be greater than 0 -- Product must belong to a User - -#### Order -- An Order must have one or more Order Items - -#### OrderItem -- Must belong to a Product -- Must belong to an Order -- Quantity must be present -- Quantity must be an integer -- Quantity must be greater than 0 - -#### Review -- Rating must be present -- Rating must be an integer -- Rating must be between 1 and 5 - -## Submission Guidelines -Your final project must be deployed to [Heroku](http://heroku.com). Your team will open a single pull request for the entire project. There are comprehension questions to answer with your submission that you should complete together as a group. Remember, you can submit a PR and still make some final changes to your code, so don't wait until the last minute. - -## Team Leaders -Each team will have team leaders who are responsible for keeping track of each team member's contributions. Rotate leader roles at the beginning of the second week; every team member should be in at least one leader role during the project. - -- **Stand Up Leader** - - Notifies team members about meeting schedule and ensures that everyone is present and ready - - Takes notes about each person's daily report in Stand Up - - Keeps the meeting moving -- **Task Leader** - - Leads discussion on task assignment and prioritization - - Decide if a task should be completed alone or in a pair - - Assign tasks based on... - - Individual comfort - - Desire - - Ability - - Ensures the Kanban board stays up to date - -## Stand Up Meetings -The Stand Up Leader should determine the daily time for your stand up meeting with the team. Once you come up with a time, confirm with your PM that this time will work for them. - -At the end of each day, your team's assigned Project Manager will review the Trello board to ensure it captures the updates that your team has made throughout the day. - -## Interim Demo -In a real world work environment, a team's success is measured by their product as opposed to each individual's contribution. - -Each team will present their progress and respond to questions from their Project Manager on the first Friday. Every team member will participate in these demos; the PM will ask specific questions regarding -1. The team's progress and plan for completing the project -1. The technical decisions and implementation -1. Every team member's understanding of the underlying technical structures - -## Final Presentation -Each team will present their product in a final presentation to the group on the final Friday. Your presentation should be no more than 7 minutes. The presentation should include every team member and: -- what you learned as individuals and as a group -- a short story-driven demo of interesting features - -## Due Date -This project is due EOD Apr 27 via PR against Ada-C9/betsy. - -## What Instructors Are Looking For -Check out the [feedback template](feedback.md) which lists the items instructors will be looking for as they evaluate your project. +* ... diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000000..e85f913914 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative 'config/application' + +Rails.application.load_tasks diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 0000000000..b16e53d6d5 --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link_directory ../javascripts .js +//= link_directory ../stylesheets .css diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/assets/images/banner_background.png b/app/assets/images/banner_background.png new file mode 100644 index 0000000000..e112f36f4f Binary files /dev/null and b/app/assets/images/banner_background.png differ diff --git a/app/assets/images/grey-lightening-bolt.jpg b/app/assets/images/grey-lightening-bolt.jpg new file mode 100644 index 0000000000..ccb5989ff4 Binary files /dev/null and b/app/assets/images/grey-lightening-bolt.jpg differ diff --git a/app/assets/images/lightening-bolt.png b/app/assets/images/lightening-bolt.png new file mode 100644 index 0000000000..5b90115421 Binary files /dev/null and b/app/assets/images/lightening-bolt.png differ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js new file mode 100644 index 0000000000..f04673be81 --- /dev/null +++ b/app/assets/javascripts/application.js @@ -0,0 +1,20 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's +// vendor/assets/javascripts directory can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// compiled file. JavaScript code in this file should be added after the last require_* statement. +// +// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details +// about supported directives. +//= require jquery + +// +//= require rails-ujs +//= require foundation +//= require turbolinks +//= require_tree . + +$(function(){ $(document).foundation(); }); diff --git a/app/assets/javascripts/cable.js b/app/assets/javascripts/cable.js new file mode 100644 index 0000000000..739aa5f022 --- /dev/null +++ b/app/assets/javascripts/cable.js @@ -0,0 +1,13 @@ +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the `rails generate channel` command. +// +//= require action_cable +//= require_self +//= require_tree ./channels + +(function() { + this.App || (this.App = {}); + + App.cable = ActionCable.createConsumer(); + +}).call(this); diff --git a/app/assets/javascripts/categories.js b/app/assets/javascripts/categories.js new file mode 100644 index 0000000000..dee720facd --- /dev/null +++ b/app/assets/javascripts/categories.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/app/assets/javascripts/channels/.keep b/app/assets/javascripts/channels/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/assets/javascripts/merchants.js b/app/assets/javascripts/merchants.js new file mode 100644 index 0000000000..dee720facd --- /dev/null +++ b/app/assets/javascripts/merchants.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/app/assets/javascripts/orderitems.js b/app/assets/javascripts/orderitems.js new file mode 100644 index 0000000000..dee720facd --- /dev/null +++ b/app/assets/javascripts/orderitems.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/app/assets/javascripts/orders.js b/app/assets/javascripts/orders.js new file mode 100644 index 0000000000..dee720facd --- /dev/null +++ b/app/assets/javascripts/orders.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/app/assets/javascripts/products.js b/app/assets/javascripts/products.js new file mode 100644 index 0000000000..dee720facd --- /dev/null +++ b/app/assets/javascripts/products.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/app/assets/javascripts/reviews.js b/app/assets/javascripts/reviews.js new file mode 100644 index 0000000000..dee720facd --- /dev/null +++ b/app/assets/javascripts/reviews.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/app/assets/javascripts/sessions.js b/app/assets/javascripts/sessions.js new file mode 100644 index 0000000000..dee720facd --- /dev/null +++ b/app/assets/javascripts/sessions.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/app/assets/stylesheets/_settings.scss b/app/assets/stylesheets/_settings.scss new file mode 100644 index 0000000000..26f74f9494 --- /dev/null +++ b/app/assets/stylesheets/_settings.scss @@ -0,0 +1,869 @@ +// Foundation for Sites Settings +// ----------------------------- +// +// Table of Contents: +// +// 1. Global +// 2. Breakpoints +// 3. The Grid +// 4. Base Typography +// 5. Typography Helpers +// 6. Abide +// 7. Accordion +// 8. Accordion Menu +// 9. Badge +// 10. Breadcrumbs +// 11. Button +// 12. Button Group +// 13. Callout +// 14. Card +// 15. Close Button +// 16. Drilldown +// 17. Dropdown +// 18. Dropdown Menu +// 19. Flexbox Utilities +// 20. Forms +// 21. Label +// 22. Media Object +// 23. Menu +// 24. Meter +// 25. Off-canvas +// 26. Orbit +// 27. Pagination +// 28. Progress Bar +// 29. Prototype Arrow +// 30. Prototype Border-Box +// 31. Prototype Border-None +// 32. Prototype Bordered +// 33. Prototype Display +// 34. Prototype Font-Styling +// 35. Prototype List-Style-Type +// 36. Prototype Overflow +// 37. Prototype Position +// 38. Prototype Rounded +// 39. Prototype Separator +// 40. Prototype Shadow +// 41. Prototype Sizing +// 42. Prototype Spacing +// 43. Prototype Text-Decoration +// 44. Prototype Text-Transformation +// 45. Prototype Text-Utilities +// 46. Responsive Embed +// 47. Reveal +// 48. Slider +// 49. Switch +// 50. Table +// 51. Tabs +// 52. Thumbnail +// 53. Title Bar +// 54. Tooltip +// 55. Top Bar +// 56. Xy Grid + +@import 'util/util'; + +// 1. Global +// --------- + +$global-font-size: 100%; +$global-width: rem-calc(1200); +$global-lineheight: 1.5; +$foundation-palette: ( + primary: #1779ba, + secondary: #767676, + success: #3adb76, + warning: #ffae00, + alert: #cc4b37, +); +$light-gray: #e6e6e6; +$medium-gray: #cacaca; +$dark-gray: #8a8a8a; +$black: #0a0a0a; +$white: #fefefe; +$body-background: $white; +$body-font-color: $black; +$body-font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif; +$body-antialiased: true; +$global-margin: 1rem; +$global-padding: 1rem; +$global-position: 1rem; +$global-weight-normal: normal; +$global-weight-bold: bold; +$global-radius: 0; +$global-menu-padding: 0.7rem 1rem; +$global-menu-nested-margin: 1rem; +$global-text-direction: ltr; +$global-flexbox: true; +$global-prototype-breakpoints: false; +$global-button-cursor: auto; +$global-color-pick-contrast-tolerance: 0; +$print-transparent-backgrounds: true; + +@include add-foundation-colors; + +// 2. Breakpoints +// -------------- + +$breakpoints: ( + small: 0, + medium: 640px, + large: 1024px, + xlarge: 1200px, + xxlarge: 1440px, +); +$print-breakpoint: large; +$breakpoint-classes: (small medium large); + +// 3. The Grid +// ----------- + +$grid-row-width: $global-width; +$grid-column-count: 12; +$grid-column-gutter: ( + small: 20px, + medium: 30px, +); +$grid-column-align-edge: true; +$grid-column-alias: 'columns'; +$block-grid-max: 8; + +// 4. Base Typography +// ------------------ + +$header-font-family: $body-font-family; +$header-font-weight: $global-weight-normal; +$header-font-style: normal; +$font-family-monospace: Consolas, 'Liberation Mono', Courier, monospace; +$header-color: inherit; +$header-lineheight: 1.4; +$header-margin-bottom: 0.5rem; +$header-styles: ( + small: ( + 'h1': ('font-size': 24), + 'h2': ('font-size': 20), + 'h3': ('font-size': 19), + 'h4': ('font-size': 18), + 'h5': ('font-size': 17), + 'h6': ('font-size': 16), + ), + medium: ( + 'h1': ('font-size': 48), + 'h2': ('font-size': 40), + 'h3': ('font-size': 31), + 'h4': ('font-size': 25), + 'h5': ('font-size': 20), + 'h6': ('font-size': 16), + ), +); +$header-text-rendering: optimizeLegibility; +$small-font-size: 80%; +$header-small-font-color: $medium-gray; +$paragraph-lineheight: 1.6; +$paragraph-margin-bottom: 1rem; +$paragraph-text-rendering: optimizeLegibility; +$code-color: $black; +$code-font-family: $font-family-monospace; +$code-font-weight: $global-weight-normal; +$code-background: $light-gray; +$code-border: 1px solid $medium-gray; +$code-padding: rem-calc(2 5 1); +$anchor-color: $primary-color; +$anchor-color-hover: scale-color($anchor-color, $lightness: -14%); +$anchor-text-decoration: none; +$anchor-text-decoration-hover: none; +$hr-width: $global-width; +$hr-border: 1px solid $medium-gray; +$hr-margin: rem-calc(20) auto; +$list-lineheight: $paragraph-lineheight; +$list-margin-bottom: $paragraph-margin-bottom; +$list-style-type: disc; +$list-style-position: outside; +$list-side-margin: 1.25rem; +$list-nested-side-margin: 1.25rem; +$defnlist-margin-bottom: 1rem; +$defnlist-term-weight: $global-weight-bold; +$defnlist-term-margin-bottom: 0.3rem; +$blockquote-color: $dark-gray; +$blockquote-padding: rem-calc(9 20 0 19); +$blockquote-border: 1px solid $medium-gray; +$cite-font-size: rem-calc(13); +$cite-color: $dark-gray; +$cite-pseudo-content: '\2014 \0020'; +$keystroke-font: $font-family-monospace; +$keystroke-color: $black; +$keystroke-background: $light-gray; +$keystroke-padding: rem-calc(2 4 0); +$keystroke-radius: $global-radius; +$abbr-underline: 1px dotted $black; + +// 5. Typography Helpers +// --------------------- + +$lead-font-size: $global-font-size * 1.25; +$lead-lineheight: 1.6; +$subheader-lineheight: 1.4; +$subheader-color: $dark-gray; +$subheader-font-weight: $global-weight-normal; +$subheader-margin-top: 0.2rem; +$subheader-margin-bottom: 0.5rem; +$stat-font-size: 2.5rem; + +// 6. Abide +// -------- + +$abide-inputs: true; +$abide-labels: true; +$input-background-invalid: get-color(alert); +$form-label-color-invalid: get-color(alert); +$input-error-color: get-color(alert); +$input-error-font-size: rem-calc(12); +$input-error-font-weight: $global-weight-bold; + +// 7. Accordion +// ------------ + +$accordion-background: $white; +$accordion-plusminus: true; +$accordion-title-font-size: rem-calc(12); +$accordion-item-color: $primary-color; +$accordion-item-background-hover: $light-gray; +$accordion-item-padding: 1.25rem 1rem; +$accordion-content-background: $white; +$accordion-content-border: 1px solid $light-gray; +$accordion-content-color: $body-font-color; +$accordion-content-padding: 1rem; + +// 8. Accordion Menu +// ----------------- + +$accordionmenu-padding: $global-menu-padding; +$accordionmenu-nested-margin: $global-menu-nested-margin; +$accordionmenu-submenu-padding: $accordionmenu-padding; +$accordionmenu-arrows: true; +$accordionmenu-arrow-color: $primary-color; +$accordionmenu-item-background: null; +$accordionmenu-border: null; +$accordionmenu-submenu-toggle-background: null; +$accordion-submenu-toggle-border: $accordionmenu-border; +$accordionmenu-submenu-toggle-width: 40px; +$accordionmenu-submenu-toggle-height: $accordionmenu-submenu-toggle-width; +$accordionmenu-arrow-size: 6px; + +// 9. Badge +// -------- + +$badge-background: $primary-color; +$badge-color: $white; +$badge-color-alt: $black; +$badge-palette: $foundation-palette; +$badge-padding: 0.3em; +$badge-minwidth: 2.1em; +$badge-font-size: 0.6rem; + +// 10. Breadcrumbs +// --------------- + +$breadcrumbs-margin: 0 0 $global-margin 0; +$breadcrumbs-item-font-size: rem-calc(11); +$breadcrumbs-item-color: $primary-color; +$breadcrumbs-item-color-current: $black; +$breadcrumbs-item-color-disabled: $medium-gray; +$breadcrumbs-item-margin: 0.75rem; +$breadcrumbs-item-uppercase: true; +$breadcrumbs-item-separator: true; +$breadcrumbs-item-separator-item: '/'; +$breadcrumbs-item-separator-item-rtl: '\\'; +$breadcrumbs-item-separator-color: $medium-gray; + +// 11. Button +// ---------- + +$button-font-family: inherit; +$button-padding: 0.85em 1em; +$button-margin: 0 0 $global-margin 0; +$button-fill: solid; +$button-background: $primary-color; +$button-background-hover: scale-color($button-background, $lightness: -15%); +$button-color: $white; +$button-color-alt: $black; +$button-radius: $global-radius; +$button-hollow-border-width: 1px; +$button-sizes: ( + tiny: 0.6rem, + small: 0.75rem, + default: 0.9rem, + large: 1.25rem, +); +$button-palette: $foundation-palette; +$button-opacity-disabled: 0.25; +$button-background-hover-lightness: -20%; +$button-hollow-hover-lightness: -50%; +$button-transition: background-color 0.25s ease-out, color 0.25s ease-out; + +// 12. Button Group +// ---------------- + +$buttongroup-margin: 1rem; +$buttongroup-spacing: 1px; +$buttongroup-child-selector: '.button'; +$buttongroup-expand-max: 6; +$buttongroup-radius-on-each: true; + +// 13. Callout +// ----------- + +$callout-background: $white; +$callout-background-fade: 85%; +$callout-border: 1px solid rgba($black, 0.25); +$callout-margin: 0 0 1rem 0; +$callout-padding: 1rem; +$callout-font-color: $body-font-color; +$callout-font-color-alt: $body-background; +$callout-radius: $global-radius; +$callout-link-tint: 30%; + +// 14. Card +// -------- + +$card-background: $white; +$card-font-color: $body-font-color; +$card-divider-background: $light-gray; +$card-border: 1px solid $light-gray; +$card-shadow: none; +$card-border-radius: $global-radius; +$card-padding: $global-padding; +$card-margin-bottom: $global-margin; + +// 15. Close Button +// ---------------- + +$closebutton-position: right top; +$closebutton-offset-horizontal: ( + small: 0.66rem, + medium: 1rem, +); +$closebutton-offset-vertical: ( + small: 0.33em, + medium: 0.5rem, +); +$closebutton-size: ( + small: 1.5em, + medium: 2em, +); +$closebutton-lineheight: 1; +$closebutton-color: $dark-gray; +$closebutton-color-hover: $black; + +// 16. Drilldown +// ------------- + +$drilldown-transition: transform 0.15s linear; +$drilldown-arrows: true; +$drilldown-padding: $global-menu-padding; +$drilldown-nested-margin: 0; +$drilldown-background: $white; +$drilldown-submenu-padding: $drilldown-padding; +$drilldown-submenu-background: $white; +$drilldown-arrow-color: $primary-color; +$drilldown-arrow-size: 6px; + +// 17. Dropdown +// ------------ + +$dropdown-padding: 1rem; +$dropdown-background: $body-background; +$dropdown-border: 1px solid $medium-gray; +$dropdown-font-size: 1rem; +$dropdown-width: 300px; +$dropdown-radius: $global-radius; +$dropdown-sizes: ( + tiny: 100px, + small: 200px, + large: 400px, +); + +// 18. Dropdown Menu +// ----------------- + +$dropdownmenu-arrows: true; +$dropdownmenu-arrow-color: $anchor-color; +$dropdownmenu-arrow-size: 6px; +$dropdownmenu-arrow-padding: 1.5rem; +$dropdownmenu-min-width: 200px; +$dropdownmenu-background: $white; +$dropdownmenu-submenu-background: $dropdownmenu-background; +$dropdownmenu-padding: $global-menu-padding; +$dropdownmenu-nested-margin: 0; +$dropdownmenu-submenu-padding: $dropdownmenu-padding; +$dropdownmenu-border: 1px solid $medium-gray; +$dropdown-menu-item-color-active: get-color(primary); +$dropdown-menu-item-background-active: transparent; + +// 19. Flexbox Utilities +// --------------------- + +$flex-source-ordering-count: 6; +$flexbox-responsive-breakpoints: true; + +// 20. Forms +// --------- + +$fieldset-border: 1px solid $medium-gray; +$fieldset-padding: rem-calc(20); +$fieldset-margin: rem-calc(18 0); +$legend-padding: rem-calc(0 3); +$form-spacing: rem-calc(16); +$helptext-color: $black; +$helptext-font-size: rem-calc(13); +$helptext-font-style: italic; +$input-prefix-color: $black; +$input-prefix-background: $light-gray; +$input-prefix-border: 1px solid $medium-gray; +$input-prefix-padding: 1rem; +$form-label-color: $black; +$form-label-font-size: rem-calc(14); +$form-label-font-weight: $global-weight-normal; +$form-label-line-height: 1.8; +$select-background: $white; +$select-triangle-color: $dark-gray; +$select-radius: $global-radius; +$input-color: $black; +$input-placeholder-color: $medium-gray; +$input-font-family: inherit; +$input-font-size: rem-calc(16); +$input-font-weight: $global-weight-normal; +$input-line-height: $global-lineheight; +$input-background: $white; +$input-background-focus: $white; +$input-background-disabled: $light-gray; +$input-border: 1px solid $medium-gray; +$input-border-focus: 1px solid $dark-gray; +$input-padding: $form-spacing / 2; +$input-shadow: inset 0 1px 2px rgba($black, 0.1); +$input-shadow-focus: 0 0 5px $medium-gray; +$input-cursor-disabled: not-allowed; +$input-transition: box-shadow 0.5s, border-color 0.25s ease-in-out; +$input-number-spinners: true; +$input-radius: $global-radius; +$form-button-radius: $global-radius; + +// 21. Label +// --------- + +$label-background: $primary-color; +$label-color: $white; +$label-color-alt: $black; +$label-palette: $foundation-palette; +$label-font-size: 0.8rem; +$label-padding: 0.33333rem 0.5rem; +$label-radius: $global-radius; + +// 22. Media Object +// ---------------- + +$mediaobject-margin-bottom: $global-margin; +$mediaobject-section-padding: $global-padding; +$mediaobject-image-width-stacked: 100%; + +// 23. Menu +// -------- + +$menu-margin: 0; +$menu-nested-margin: $global-menu-nested-margin; +$menu-items-padding: $global-menu-padding; +$menu-simple-margin: 1rem; +$menu-item-color-active: $white; +$menu-item-background-active: get-color(primary); +$menu-icon-spacing: 0.25rem; +$menu-item-background-hover: $light-gray; +$menu-state-back-compat: true; +$menu-centered-back-compat: true; +$menu-icons-back-compat: true; + +// 24. Meter +// --------- + +$meter-height: 1rem; +$meter-radius: $global-radius; +$meter-background: $medium-gray; +$meter-fill-good: $success-color; +$meter-fill-medium: $warning-color; +$meter-fill-bad: $alert-color; + +// 25. Off-canvas +// -------------- + +$offcanvas-sizes: ( + small: 250px, +); +$offcanvas-vertical-sizes: ( + small: 250px, +); +$offcanvas-background: $light-gray; +$offcanvas-shadow: 0 0 10px rgba($black, 0.7); +$offcanvas-inner-shadow-size: 20px; +$offcanvas-inner-shadow-color: rgba($black, 0.25); +$offcanvas-overlay-zindex: 11; +$offcanvas-push-zindex: 12; +$offcanvas-overlap-zindex: 13; +$offcanvas-reveal-zindex: 12; +$offcanvas-transition-length: 0.5s; +$offcanvas-transition-timing: ease; +$offcanvas-fixed-reveal: true; +$offcanvas-exit-background: rgba($white, 0.25); +$maincontent-class: 'off-canvas-content'; + +// 26. Orbit +// --------- + +$orbit-bullet-background: $medium-gray; +$orbit-bullet-background-active: $dark-gray; +$orbit-bullet-diameter: 1.2rem; +$orbit-bullet-margin: 0.1rem; +$orbit-bullet-margin-top: 0.8rem; +$orbit-bullet-margin-bottom: 0.8rem; +$orbit-caption-background: rgba($black, 0.5); +$orbit-caption-padding: 1rem; +$orbit-control-background-hover: rgba($black, 0.5); +$orbit-control-padding: 1rem; +$orbit-control-zindex: 10; + +// 27. Pagination +// -------------- + +$pagination-font-size: rem-calc(14); +$pagination-margin-bottom: $global-margin; +$pagination-item-color: $black; +$pagination-item-padding: rem-calc(3 10); +$pagination-item-spacing: rem-calc(1); +$pagination-radius: $global-radius; +$pagination-item-background-hover: $light-gray; +$pagination-item-background-current: $primary-color; +$pagination-item-color-current: $white; +$pagination-item-color-disabled: $medium-gray; +$pagination-ellipsis-color: $black; +$pagination-mobile-items: false; +$pagination-mobile-current-item: false; +$pagination-arrows: true; + +// 28. Progress Bar +// ---------------- + +$progress-height: 1rem; +$progress-background: $medium-gray; +$progress-margin-bottom: $global-margin; +$progress-meter-background: $primary-color; +$progress-radius: $global-radius; + +// 29. Prototype Arrow +// ------------------- + +$prototype-arrow-directions: ( + down, + up, + right, + left +); +$prototype-arrow-size: 0.4375rem; +$prototype-arrow-color: $black; + +// 30. Prototype Border-Box +// ------------------------ + +$prototype-border-box-breakpoints: $global-prototype-breakpoints; + +// 31. Prototype Border-None +// ------------------------- + +$prototype-border-none-breakpoints: $global-prototype-breakpoints; + +// 32. Prototype Bordered +// ---------------------- + +$prototype-bordered-breakpoints: $global-prototype-breakpoints; +$prototype-border-width: rem-calc(1); +$prototype-border-type: solid; +$prototype-border-color: $medium-gray; + +// 33. Prototype Display +// --------------------- + +$prototype-display-breakpoints: $global-prototype-breakpoints; +$prototype-display: ( + inline, + inline-block, + block, + table, + table-cell +); + +// 34. Prototype Font-Styling +// -------------------------- + +$prototype-font-breakpoints: $global-prototype-breakpoints; +$prototype-wide-letter-spacing: rem-calc(4); +$prototype-font-normal: $global-weight-normal; +$prototype-font-bold: $global-weight-bold; + +// 35. Prototype List-Style-Type +// ----------------------------- + +$prototype-list-breakpoints: $global-prototype-breakpoints; +$prototype-style-type-unordered: ( + disc, + circle, + square +); +$prototype-style-type-ordered: ( + decimal, + lower-alpha, + lower-latin, + lower-roman, + upper-alpha, + upper-latin, + upper-roman +); + +// 36. Prototype Overflow +// ---------------------- + +$prototype-overflow-breakpoints: $global-prototype-breakpoints; +$prototype-overflow: ( + visible, + hidden, + scroll +); + +// 37. Prototype Position +// ---------------------- + +$prototype-position-breakpoints: $global-prototype-breakpoints; +$prototype-position: ( + static, + relative, + absolute, + fixed +); +$prototype-position-z-index: 975; + +// 38. Prototype Rounded +// --------------------- + +$prototype-rounded-breakpoints: $global-prototype-breakpoints; +$prototype-border-radius: rem-calc(3); + +// 39. Prototype Separator +// ----------------------- + +$prototype-separator-breakpoints: $global-prototype-breakpoints; +$prototype-separator-align: center; +$prototype-separator-height: rem-calc(2); +$prototype-separator-width: 3rem; +$prototype-separator-background: $primary-color; +$prototype-separator-margin-top: $global-margin; + +// 40. Prototype Shadow +// -------------------- + +$prototype-shadow-breakpoints: $global-prototype-breakpoints; +$prototype-box-shadow: 0 2px 5px 0 rgba(0,0,0,.16), + 0 2px 10px 0 rgba(0,0,0,.12); + +// 41. Prototype Sizing +// -------------------- + +$prototype-sizing-breakpoints: $global-prototype-breakpoints; +$prototype-sizing: ( + width, + height +); +$prototype-sizes: ( + 25: 25%, + 50: 50%, + 75: 75%, + 100: 100% +); + +// 42. Prototype Spacing +// --------------------- + +$prototype-spacing-breakpoints: $global-prototype-breakpoints; +$prototype-spacers-count: 3; + +// 43. Prototype Text-Decoration +// ----------------------------- + +$prototype-decoration-breakpoints: $global-prototype-breakpoints; +$prototype-text-decoration: ( + overline, + underline, + line-through, +); + +// 44. Prototype Text-Transformation +// --------------------------------- + +$prototype-transformation-breakpoints: $global-prototype-breakpoints; +$prototype-text-transformation: ( + lowercase, + uppercase, + capitalize +); + +// 45. Prototype Text-Utilities +// ---------------------------- + +$prototype-utilities-breakpoints: $global-prototype-breakpoints; +$prototype-text-overflow: ellipsis; + +// 46. Responsive Embed +// -------------------- + +$responsive-embed-margin-bottom: rem-calc(16); +$responsive-embed-ratios: ( + default: 4 by 3, + widescreen: 16 by 9, +); + +// 47. Reveal +// ---------- + +$reveal-background: $white; +$reveal-width: 600px; +$reveal-max-width: $global-width; +$reveal-padding: $global-padding; +$reveal-border: 1px solid $medium-gray; +$reveal-radius: $global-radius; +$reveal-zindex: 1005; +$reveal-overlay-background: rgba($black, 0.45); + +// 48. Slider +// ---------- + +$slider-width-vertical: 0.5rem; +$slider-transition: all 0.2s ease-in-out; +$slider-height: 0.5rem; +$slider-background: $light-gray; +$slider-fill-background: $medium-gray; +$slider-handle-height: 1.4rem; +$slider-handle-width: 1.4rem; +$slider-handle-background: $primary-color; +$slider-opacity-disabled: 0.25; +$slider-radius: $global-radius; + +// 49. Switch +// ---------- + +$switch-background: $medium-gray; +$switch-background-active: $primary-color; +$switch-height: 2rem; +$switch-height-tiny: 1.5rem; +$switch-height-small: 1.75rem; +$switch-height-large: 2.5rem; +$switch-radius: $global-radius; +$switch-margin: $global-margin; +$switch-paddle-background: $white; +$switch-paddle-offset: 0.25rem; +$switch-paddle-radius: $global-radius; +$switch-paddle-transition: all 0.25s ease-out; + +// 50. Table +// --------- + +$table-background: $white; +$table-color-scale: 5%; +$table-border: 1px solid smart-scale($table-background, $table-color-scale); +$table-padding: rem-calc(8 10 10); +$table-hover-scale: 2%; +$table-row-hover: darken($table-background, $table-hover-scale); +$table-row-stripe-hover: darken($table-background, $table-color-scale + $table-hover-scale); +$table-is-striped: true; +$table-striped-background: smart-scale($table-background, $table-color-scale); +$table-stripe: even; +$table-head-background: smart-scale($table-background, $table-color-scale / 2); +$table-head-row-hover: darken($table-head-background, $table-hover-scale); +$table-foot-background: smart-scale($table-background, $table-color-scale); +$table-foot-row-hover: darken($table-foot-background, $table-hover-scale); +$table-head-font-color: $body-font-color; +$table-foot-font-color: $body-font-color; +$show-header-for-stacked: false; +$table-stack-breakpoint: medium; + +// 51. Tabs +// -------- + +$tab-margin: 0; +$tab-background: $white; +$tab-color: $primary-color; +$tab-background-active: $light-gray; +$tab-active-color: $primary-color; +$tab-item-font-size: rem-calc(12); +$tab-item-background-hover: $white; +$tab-item-padding: 1.25rem 1.5rem; +$tab-expand-max: 6; +$tab-content-background: $white; +$tab-content-border: $light-gray; +$tab-content-color: $body-font-color; +$tab-content-padding: 1rem; + +// 52. Thumbnail +// ------------- + +$thumbnail-border: solid 4px $white; +$thumbnail-margin-bottom: $global-margin; +$thumbnail-shadow: 0 0 0 1px rgba($black, 0.2); +$thumbnail-shadow-hover: 0 0 6px 1px rgba($primary-color, 0.5); +$thumbnail-transition: box-shadow 200ms ease-out; +$thumbnail-radius: $global-radius; + +// 53. Title Bar +// ------------- + +$titlebar-background: $black; +$titlebar-color: $white; +$titlebar-padding: 0.5rem; +$titlebar-text-font-weight: bold; +$titlebar-icon-color: $white; +$titlebar-icon-color-hover: $medium-gray; +$titlebar-icon-spacing: 0.25rem; + +// 54. Tooltip +// ----------- + +$has-tip-cursor: help; +$has-tip-font-weight: $global-weight-bold; +$has-tip-border-bottom: dotted 1px $dark-gray; +$tooltip-background-color: $black; +$tooltip-color: $white; +$tooltip-padding: 0.75rem; +$tooltip-max-width: 10rem; +$tooltip-font-size: $small-font-size; +$tooltip-pip-width: 0.75rem; +$tooltip-pip-height: $tooltip-pip-width * 0.866; +$tooltip-radius: $global-radius; + +// 55. Top Bar +// ----------- + +$topbar-padding: 0.5rem; +$topbar-background: $light-gray; +$topbar-submenu-background: $topbar-background; +$topbar-title-spacing: 0.5rem 1rem 0.5rem 0; +$topbar-input-width: 200px; +$topbar-unstack-breakpoint: medium; + +// 56. Xy Grid +// ----------- + +$xy-grid: true; +$grid-container: $global-width; +$grid-columns: 12; +$grid-margin-gutters: ( + small: 20px, + medium: 30px +); +$grid-padding-gutters: $grid-margin-gutters; +$grid-container-padding: $grid-padding-gutters; +$grid-container-max: $global-width; +$xy-block-grid-max: 8; + diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 0000000000..d97d83a375 --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,225 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's + * vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + *= require normalize-rails + + * + *= require_tree . + *= require_self + *= require foundation_and_overrides + + */ + +* { + font-family: 'Comic Sans MS', sans-serif; +} + +html { + min-height: 100%; +} + +main { + min-height: 80vh; +} + +body > div { + min-height: 100vh; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Bangers', 'Comic Sans MS', sans-serif; +} + +header { + z-index: 1; +} + +nav.top-bar { + background-color: #00ACEE; +} + +nav.top-bar .button, .hollow, nav.top-bar .dropbtn { + color: #FFEE1D; + font-size: 1em; + border: solid 2px #FFEE1D; + margin: 10px 5px; +} + +.navbar { + display: flex; + justify-content: center; + width: 100vw; + z-index: 10; + position: absolute; +} + +.left { + display: flex; +} + +/* Dropdown Button */ +.dropbtn { + color: white; + padding: 10px 16px 10px 16px; + font-size: 16px; + border: none; +} + +/*.dropbtn:hover { + +}*/ + +/* The container
- needed to position the dropdown content */ +.has-dropdown { + margin-top: 3vh; + position: relative; + display: inline-block; +} + +/* Dropdown Content (Hidden by Default) */ +.dropdown { + display: none; + position: absolute; + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 10; + left: -1rem; + top: 2.2rem; +} + +/* Links inside the dropdown */ +.dropdown a { + color: black; + padding: 12px 16px; + text-decoration: none; + display: block; +} + +.dropdown a:hover { + color: white; + border: 1px solid white; +} + +/* Show the dropdown menu on hover */ +.has-dropdown:hover .dropdown { + display: block; + background-color: #EC173A +} + +/* Change the background color of the dropdown button when the dropdown content is shown */ +.has-dropdown:hover .dropbtn { + /*background-color: #3e8e41;*/ + background-color: #00ACEE; + color: #FFEE1D; +} +.dropdown li { + list-style-type: none; + + +} + + + +.right { + display: flex; +} + +/* search bar */ + +.form-inline { + display: flex; + width: 30vw; + justify-content: center; +} + +#search { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + margin-right: 0; +} + +.button { + margin-bottom: 0; +} + +input[name="search"] { + border: none; + margin: 15px 10px; +} + +input[name="search"]::placeholder { + color: #000; +} + +/* main styles */ + +main { + padding: 20px; +} + +/* footer styles */ + +footer { + background-color: rgb(255, 232, 10); + position: relative; + bottom: 0; + width: 100%; + text-align: center; + padding-top: 10px; +} +footer h4 { + color: #EC173A; + text-shadow: 2px 2px 4px #000000; +} + +.body-button { + background-color: #FFEE1D; + color: #00ACEE; + font-weight: bold; + box-shadow: -2px 2px 4px #000000; +} + +.button:hover { + background-color: #00ACEE; + color: #FFEE1D; +} + +.manage-products-button { + float: right; +} + +.orders-title, .products-title { + display: inline; + margin: 50px auto; +} + +.table { + margin: 30px auto; +} + +h3 { + margin: 30px auto; +} + +.status-submit { + margin-left: 20px; + height: 40px; +} + +.by-status { + margin-top: 30px; + width: 200px; +} + +p.copyright { + margin-bottom: 0; + padding-bottom: 10px; +} diff --git a/app/assets/stylesheets/browserslist b/app/assets/stylesheets/browserslist new file mode 100644 index 0000000000..6019618a9a --- /dev/null +++ b/app/assets/stylesheets/browserslist @@ -0,0 +1,4 @@ +last 2 versions +ie >= 9 +Android >= 2.3 +ios >= 7 diff --git a/app/assets/stylesheets/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_and_overrides.scss b/app/assets/stylesheets/foundation_and_overrides.scss new file mode 100644 index 0000000000..ed4c5a0ecf --- /dev/null +++ b/app/assets/stylesheets/foundation_and_overrides.scss @@ -0,0 +1,61 @@ +@charset 'utf-8'; + +@import 'settings'; +@import 'foundation'; + +// If you'd like to include motion-ui the foundation-rails gem comes prepackaged with it, uncomment the 3 @imports, if you are not using the gem you need to install the motion-ui sass package. +// +// @import 'motion-ui/motion-ui'; + +// We include everything by default. To slim your CSS, remove components you don't use. + +@include foundation-global-styles; +@include foundation-xy-grid-classes; +//@include foundation-grid; +//@include foundation-flex-grid; +@include foundation-flex-classes; +@include foundation-typography; +@include foundation-forms; +@include foundation-button; +@include foundation-accordion; +@include foundation-accordion-menu; +@include foundation-badge; +@include foundation-breadcrumbs; +@include foundation-button-group; +@include foundation-callout; +@include foundation-card; +@include foundation-close-button; +@include foundation-menu; +@include foundation-menu-icon; +@include foundation-drilldown-menu; +@include foundation-dropdown; +@include foundation-dropdown-menu; +@include foundation-responsive-embed; +@include foundation-label; +@include foundation-media-object; +@include foundation-off-canvas; +@include foundation-orbit; +@include foundation-pagination; +@include foundation-progress-bar; +@include foundation-slider; +@include foundation-sticky; +@include foundation-reveal; +@include foundation-switch; +@include foundation-table; +@include foundation-tabs; +@include foundation-thumbnail; +@include foundation-title-bar; +@include foundation-tooltip; +@include foundation-top-bar; +@include foundation-visibility-classes; +@include foundation-float-classes; + +// If you'd like to include motion-ui the foundation-rails gem comes prepackaged with it, uncomment the 3 @imports, if you are not using the gem you need to install the motion-ui sass package. +// +// @include motion-ui-transitions; +// @include motion-ui-animations; +@import 'motion-ui/motion-ui'; +@include motion-ui-transitions; +@include motion-ui-animations; + + diff --git a/app/assets/stylesheets/merchants.scss b/app/assets/stylesheets/merchants.scss new file mode 100644 index 0000000000..a58365442e --- /dev/null +++ b/app/assets/stylesheets/merchants.scss @@ -0,0 +1,47 @@ +// 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/ + +h3.merchant { + text-align: center; + font-size: 5em; +} + +section.merchants { + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +.merchant-content { + width: 200px; + height: 100px; + position: relative; + background: #00ACEE; + border-radius: .4em; + color: white; + min-height: 50px; + margin: 50px; + text-align: center; +} + +.merchant-content a { + color: white; + font-size: 1.5em; +} + +.merchant-content:after { + + content: ''; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 0; + border: 27px solid transparent; + border-top-color: #00aabb; + border-bottom: 0; + border-right: 0; + margin-left: -13.5px; + margin-bottom: -27px; +} diff --git a/app/assets/stylesheets/orderitems.scss b/app/assets/stylesheets/orderitems.scss new file mode 100644 index 0000000000..3ae284dd37 --- /dev/null +++ b/app/assets/stylesheets/orderitems.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..41cfb8c845 --- /dev/null +++ b/app/assets/stylesheets/orders.scss @@ -0,0 +1,50 @@ +// 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/ + +.edit_order_item { + display: flex; + justify-content: flex-end; +} + +.viewcart-item { + display: inline; +} +.viewcart-item.button { + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + margin-bottom: 0; +} + +a.viewcart-item { + padding: 0.85em; +} + +#order_item_quantity { + width: 60px; + margin-bottom: 0; +} + +.viewcart-photo { + width: 100px; + height: 100px; +} + +.order-item { + display: grid; + grid-template-columns: 1fr 5fr 1fr 1fr 1fr; + align-items: center; + border-bottom: 1px solid rgb(123, 197, 238); + padding: 5vh; +} + +.checkout.button { + background-color: red; + margin-top: 5vh; +} + +.subtotal { + // text-align: right; + margin-right: 10vw; + margin-top: 5vh; +} diff --git a/app/assets/stylesheets/products.scss b/app/assets/stylesheets/products.scss new file mode 100644 index 0000000000..a30ecb0d79 --- /dev/null +++ b/app/assets/stylesheets/products.scss @@ -0,0 +1,109 @@ +// 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/ + +section.banner { + height: 100vh; + background: + linear-gradient(135deg, #FFEE1D 25%, transparent 25%) -50px 0, + linear-gradient(225deg, #FFEE1D 25%, transparent 25%) -50px 0, + linear-gradient(315deg, #FFEE1D 25%, transparent 25%), + linear-gradient(45deg, #FFEE1D 25%, transparent 25%); + background-size: 100px 100px; + background-color: #00ACEE; + text-align: center; + grid-row: 1 / span 2; +} + +section.banner h1 { + z-index: 2; + font-size: 10em; + padding-top: 200px; + color: #EC173A; + text-shadow: 2px 2px 4px #000000; +} + +/* product cards */ + +.all-products { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + grid-row: 1 / span 1; + grid-column: 2 / span 1; +} + +.all-products > .card { + margin: 5px; + max-width: 400px; + min-width: 400px; + // max-width: 400px; + height: 400px; + border: solid 3px black; + background: #FFEE1D; + background-image: + radial-gradient(#00ACEE 20%, transparent 0), + radial-gradient(#00ACEE 20%, transparent 0); + background-size: 30px 30px; + background-position: 0 0, 15px 15px; +} + + +.all-products > .card img { + display: block; + margin: auto; + max-width: 300px; + height: 350px; +} + +.card-content { + position: relative; + bottom: 0; + background: #EC173A; + text-align: center; + border-top: solid 3px black; +} + +.card-content .content h5 a { + font-family: 'Bangers', 'Comic Sans MS', sans-serif; + color: #00ACEE; + text-shadow: 1px 1px 4px #000000; + font-size: 1.2em; +} + +.product-show { + display: grid; + grid-template-columns: 1fr 2fr; + grid-template-rows: 1fr 1fr; + align-items: center; +} + +.reviews { + grid-row: 2 / span 1; + grid-column: 1 / span 2; + margin: 5vh; + margin-top: 0; +} + +.reviews h4 { + margin-top: 3vh; +} + +.product-photo { + height: 40vh; +} + +.product-info { + margin-left: 3vw; + height: 50vh; +} + +.products-content h2 { + text-align: center; + font-size: 5em; +} + +li { + list-style-type: none; +} 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/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/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 0000000000..d672697283 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 0000000000..0ff5442f47 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000000..16356422f2 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,31 @@ +class ApplicationController < ActionController::Base + protect_from_forgery with: :exception + + before_action :logged_in_merchant + # before_action :set_cart + + + private + # def set_cart + # # will need to clear this somehow + # @cart = Order.where(status: 'pending').last + # end + + def logged_in_merchant + if session[:merchant_id] + @logged_merchant = Merchant.find_by(id: session[:merchant_id]) + end + end + + def render_404 + raise ActionController::RoutingError.new('Not Found') + end + + def require_login + unless @logged_merchant + flash[:error] = 'You must be logged in to do that' + redirect_back(fallback_location: root_path) + end + end + +end diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb new file mode 100644 index 0000000000..4dc91bfd6d --- /dev/null +++ b/app/controllers/categories_controller.rb @@ -0,0 +1,22 @@ +class CategoriesController < ApplicationController + before_action :fix_category + + def create + category = Category.new(name: @category_name) + + if category.save + flash[:success] = 'Added Category' + else + flash[:failure] = 'Unable to add category' + flash[:errors] = category.errors.messages + # make this render new? + end + redirect_back(fallback_location: merchant_products_path(@logged_merchant.id)) + end + + private + + def fix_category + @category_name = params[:name].nil? ? nil : Category.fix_category(params[:name]) + end +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/controllers/merchants_controller.rb b/app/controllers/merchants_controller.rb new file mode 100644 index 0000000000..6d1d618ae0 --- /dev/null +++ b/app/controllers/merchants_controller.rb @@ -0,0 +1,27 @@ +class MerchantsController < ApplicationController + before_action :require_login, except: [:index, :show] + + # for any user + def index + @merchants = Merchant.all + end + + def show + merchant_id = params[:id] + @merchant = Merchant.find_by(id: merchant_id) + @products = Product.where(retired: false, merchant_id: @merchant.id) + end + + # for the merchant + def edit + end + + def update + end + + def manage_products + @products = @logged_merchant.products + end + + def destroy; end # is this necessary? +end diff --git a/app/controllers/orderitems_controller.rb b/app/controllers/orderitems_controller.rb new file mode 100644 index 0000000000..c4aa138332 --- /dev/null +++ b/app/controllers/orderitems_controller.rb @@ -0,0 +1,86 @@ +class OrderitemsController < ApplicationController + before_action :find_order_item, only: [:update, :destroy, :ship, :cancel] + before_action :order_exists?, only: [:create] + + + def create + @orderitem = OrderItem.new(order_item_params) + @orderitem.order_id = session[:cart_id] + # order = Order.find(session[:cart_id]) + + existing_oi = OrderItem.existing_oi?(@orderitem) + unless existing_oi.nil? + @orderitem = OrderItem.aggregate_orderitem(@orderitem, existing_oi) + end + + if @orderitem.save + flash[:success] = "Item added successfully!" + # redirect_back(fallback_location: root_path) + else + flash[:failure] = "Oops! Something went wrong and we couldn't add this item." + # render 'products/show', status: :bad_request + end + redirect_to product_path(@orderitem.product_id) + end + + def update + @orderitem.assign_attributes(order_item_params) + + if @orderitem.save + flash[:success] = "Item updated successfully!" + else + flash[:failure] = "Oops! Something went wrong and we couldn't add this item." + end + redirect_to viewcart_path(session[:cart_id]) + + end + + def destroy + @orderitem.destroy + flash[:success] = 'Item successfully removed' + + redirect_back(fallback_location: root_path) + end + + def ship + @orderitem.assign_attributes(status: "shipped") + if @orderitem.save + flash[:success] = "You have shipped #{@orderitem.product.name} for order #{@orderitem.order.id}." + else + flash[:failure] = "Could not ship item." + end + redirect_to orders_path + end + + def cancel + @orderitem.assign_attributes(status: "cancelled") + if @orderitem.save + flash[:success] = "You have cancelled #{@orderitem.product.name} for order #{@orderitem.order.id}." + else + flash[:failure] = "Could not cancel item." + end + redirect_to orders_path + end + + private + + def order_item_params + params.require(:order_item).permit(:order_id, :product_id, :quantity) + end + + def find_order_item + @orderitem = OrderItem.find_by(id: params[:id]) + unless @orderitem + redirect_to root_path + flash[:failure] = 'Cart item not found' + end + end + + def order_exists? + unless session[:cart_id] + @order = Order.create!(status: 'pending') + session[:cart_id] = @order.id + end + end + +end diff --git a/app/controllers/orders_controller.rb b/app/controllers/orders_controller.rb new file mode 100644 index 0000000000..c399cb0b6e --- /dev/null +++ b/app/controllers/orders_controller.rb @@ -0,0 +1,98 @@ +class OrdersController < ApplicationController + + before_action :order_params, only: [:show, :edit, :update] + + def index + if session[:merchant_id] + @merchant = Merchant.find(session[:merchant_id]) + if params[:order_status_filter] + @orders_and_items = @merchant.merchant_order_items(params[:order_status_filter]) + else + @orders_and_items = @merchant.merchant_order_items("all") + end + else + flash[:failure] = "You must log in as a merchant to see your orders." + # this IS the root path rn so I get a too many redirects error + # redirect_to root_path + end + end + + def show + if session[:merchant_id] && session[:cart_id].nil? + render :show + elsif @order.status != "paid" + flash[:failure] = "Oops! You need to checkout first!" + redirect_to edit_order_path(@order) + elsif session[:cart_id] + if session[:merchant_id] + flash[:failure] = 'Please checkout your cart first' + redirect_to viewcart_path(session[:cart_id]) + end + session[:cart_id] = nil + else + flash[:failure] = "Order not found" + redirect_to root_path + end + end + + # I don't think we really get here anymore + def create + order = Order.new(status: "pending") + + if order.save + flash[:success] = "You have opened up a cart!" + redirect_back(fallback_location: root_path) + else + flash[:failure] = "Could not open up a cart." + redirect_back(fallback_location: root_path) + end + end + # + # non-restful view action for editing order_item quantities + # put customer info form here and edit quantities + def edit; end + + def update + @order.assign_attributes(customer_params) + + if @order.order_items.count > 0 + if @order.save + flash[:success] = "Thank you! Your order has been placed." + @order.update(status: "paid") + + @order.reduce_stock + + redirect_to order_path(@order) + else + flash[:failure] = "The customer information was incomplete." + render :edit, status: :bad_request + end + else + flash[:failure] = "There are no items to check out" + redirect_back(fallback_location: root_path) + end + end + + def viewcart + @order = Order.find_by(id: session[:cart_id]) + unless @order + flash[:error] = 'You do not have any items in your cart' + redirect_back(fallback_location: root_path) + return + end + end + + private + def order_params + @order = Order.find_by(id: params[:id]) + unless @order + redirect_to root_path + flash[:failure] = 'Order not found' + end + end + + def customer_params + return params.require(:order).permit(:customer_name, :customer_email, :credit_card, :cvv, :cc_expiration, :shipping_address, :billing_address) + end + +end diff --git a/app/controllers/products_controller.rb b/app/controllers/products_controller.rb new file mode 100644 index 0000000000..b6fe888bbd --- /dev/null +++ b/app/controllers/products_controller.rb @@ -0,0 +1,91 @@ +class ProductsController < ApplicationController + + before_action :find_product, only: [:show, :edit, :update, :destroy] + + # before_action :require_login, except: [:index] + + def index + @category = Category.find_by(name: params[:category]) + if @category + @products = Product.by_category(@category.name).where(retired: false).where('stock > 0') + elsif params[:search] + @products = Product.find_search(params[:search]).where('stock > 0') + else + @products = Product.where(retired: false).where('stock > 0') + end + end + + def home + @products = index + end + + def new + @product = Product.new + @category = Category.new + end + + def create + @product = Product.new(product_params) + if @product.photo_url.nil? + @product.photo_url = "http://www.equistaff.com/Images/noimageavailable.gif" + end + + if @product.save + flash[:success] = 'Successfully added product' + redirect_to merchant_manage_products_path(session[:merchant_id]) + else + flash.now[:failure] = 'Product not created' + render :new, status: :bad_request + end + end + + def show; end + + def edit; end + + def update + @product.assign_attributes(product_params) + if @product.photo_url.nil? + @product.photo_url = "http://www.equistaff.com/Images/noimageavailable.gif" + end + + if @product.save + flash[:success] = "Successfully updated product #{@product.id}" + redirect_to merchant_manage_products_path(session[:merchant_id]) + else + flash.now[:failure] = 'Product not updated' + render :edit, status: :bad_request + end + end + + def destroy + if @product.merchant_id == @logged_merchant.id + + @product.retired = true + @product.stock_decrement(@product.stock) + if @product.save + flash[:success] = 'Product has been retired' + else + flash[:failure] = 'Could not retire product' + end + else + flash[:failure] = 'You are not authorized to retire this product' + end + redirect_to merchant_manage_products_path(session[:merchant_id]) + end + + private + + def product_params + return params.require(:product).permit(:name, :merchant_id, :stock, :price, :description, :photo_url, :category_ids => []) + end + + def find_product + @product = Product.find_by(id: params[:id]) + unless @product + redirect_to root_path + flash[:failure] = 'Product not found' + end + end + +end diff --git a/app/controllers/reviews_controller.rb b/app/controllers/reviews_controller.rb new file mode 100644 index 0000000000..bfdf7f0353 --- /dev/null +++ b/app/controllers/reviews_controller.rb @@ -0,0 +1,24 @@ +class ReviewsController < ApplicationController + + def create + @review = Review.new(review_params) + + if @review.product.merchant_id == session[:merchant_id] + flash[:failure] = 'You cannot review your own products!' + else + if @review.save + flash[:success] = 'Review submitted' + else + flash[:failure] = 'Review could not be saved' + end + end + + redirect_back(fallback_location: root_path) + end + + private + + def review_params + return params.require(:review).permit(:rating, :product_id, :description) + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000000..f01dfd825a --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,31 @@ +class SessionsController < ApplicationController + + def create + auth_hash = request.env['omniauth.auth'] + + if auth_hash['uid'] + + merchant = Merchant.get_user(auth_hash) + + if merchant.nil? + flash[:error] = "Could not log in because merchant failed validations" + redirect_to root_path + return + end + session[:merchant_id] = merchant.id + flash[:success] = 'Successfully logged in' + redirect_to orders_path + return + else + flash[:error] = 'Could not log in' + end + redirect_to root_path + end + + def destroy + session.delete(:merchant_id) + flash[:success] = "Logged out successfully." + redirect_to root_path + end + +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000000..3e4df32220 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,5 @@ +module ApplicationHelper + def readable_date(date) + ("" + date.strftime("%b %d, %Y") + "").html_safe + 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/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/orderitems_helper.rb b/app/helpers/orderitems_helper.rb new file mode 100644 index 0000000000..c74e407d1c --- /dev/null +++ b/app/helpers/orderitems_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..e135291f68 --- /dev/null +++ b/app/helpers/products_helper.rb @@ -0,0 +1,12 @@ +module ProductsHelper + def star_rating(rating) + stars = "" + rating.times do + stars += "#{ image_tag 'lightening-bolt.png', alt: 'lightening bolt', width: 20, height: 20}" + end + (5 - rating).times do + stars += "#{ image_tag 'grey-lightening-bolt.jpg', alt: 'grey lightening bolt', width: 20, height: 20}" + end + return (stars).html_safe + end +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/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000000..a009ace51c --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000000..286b2239d1 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000000..0e0f662d41 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,4 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + +end diff --git a/app/models/category.rb b/app/models/category.rb new file mode 100644 index 0000000000..cd626ee279 --- /dev/null +++ b/app/models/category.rb @@ -0,0 +1,13 @@ +class Category < ApplicationRecord + has_and_belongs_to_many :products, join_table: :products_categories + + validates :name, presence: true, uniqueness: true + + def self.categories_with_works + return Category.find_by_sql('SELECT DISTINCT categories.name FROM categories INNER JOIN products_categories ON categories.id = products_categories.category_id INNER JOIN products ON products_categories.category_id = products.id ORDER BY categories.name') + end + + def self.fix_category(params) + return params.downcase.pluralize + end +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/models/merchant.rb b/app/models/merchant.rb new file mode 100644 index 0000000000..d1aa409321 --- /dev/null +++ b/app/models/merchant.rb @@ -0,0 +1,81 @@ +class Merchant < ApplicationRecord + has_many :products, dependent: :destroy + has_many :orders, through: :products + + validates :username, presence: true, uniqueness: true + + validates :email, presence: true, uniqueness: true + + def merchant_order_items(status) + + orders_and_items = {} + orders = self.orders + order_ids = orders.map { |order| order.id } + order_ids.each { |order_id| orders_and_items[order_id] = [] } + + order_items = [] + self.products.each { |product| + product.order_items.each { |order_item| + order_items << order_item + } + } + + order_items.each { |order_item| + order_status = Order.find_by(id: order_item.order_id).status + if order_status == status || status == "all" + orders_and_items[order_item.order_id] << order_item + end + } + + orders_and_items.each do |order, order_items| + if order_items.empty? + orders_and_items.delete(order) + end + end + + return orders_and_items + + end # merchant_order_items + + def total_revenue_by(status) + orders_and_items = self.merchant_order_items("all") + total_revenue = 0.00 + orders_and_items.each do |order, order_items| + order_items.each do |order_item| + if Order.find(order).status == status || status == "all" + total_revenue += order_item.subtotal + end + end + end + return total_revenue + end # total_revenue_by + + def count_orders_by(status) + count = 0 + orders = self.orders.distinct + orders.each do |order| + if order.status == status || status == "all" + count += 1 + end + end + return count + end # count_orders_by + + def self.get_user(data_hash) + merchant = Merchant.find_by(uid: data_hash['uid'], provider: data_hash['provider']) + + if merchant.nil? + merchant_data = { + uid: data_hash['uid'], + provider: data_hash['provider'], + username: data_hash['info']['name'], + email: data_hash['info']['email'] + } + + merchant = Merchant.new(merchant_data) + return merchant.save ? merchant : nil + end + return merchant + end + +end diff --git a/app/models/order.rb b/app/models/order.rb new file mode 100644 index 0000000000..ee4567da6f --- /dev/null +++ b/app/models/order.rb @@ -0,0 +1,57 @@ +class Order < ApplicationRecord + has_many :order_items, dependent: :destroy + has_many :products, through: :order_items + + validates :customer_name, presence: true, on: :update + validates :customer_email, presence: true, on: :update + validates :credit_card, presence: true, on: :update + validates :cvv, presence: true, on: :update + validates :cc_expiration, presence: true, on: :update + validates :shipping_address, presence: true, on: :update + validates :billing_address, presence: true, on: :update + + validates :order_items, length: { minimum: 1 }, if: :customer_info? + + def customer_info? + if + self.customer_name.nil? || self.customer_email.nil? || self.credit_card.nil? || self.cvv.nil? || self.cc_expiration.nil? || self.shipping_address.nil? || self.billing_address.nil? + return false + end + return true + end + + def order_total + total = 0.00 + purchased_items = self.order_items.where(order_id: self) + purchased_items.each do |item| + total += item.subtotal + end + return total + end + + def order_status + statuses = self.order_items.map do |item| + item.status + end + if self.status == "pending" || self.status.nil? + self.status = "pending" + elsif statuses.uniq.size <= 1 && statuses.uniq.first == "cancelled" + self.status = "cancelled" + # return "cancelled" + elsif !statuses.include?("pending") + self.status = "completed" + # return "completed" + else + self.status = "paid" + end + self.save + return self.status + end # order_status + + def reduce_stock + self.order_items.each do |item| + item.product.stock_decrement(item.quantity) + end + end + +end diff --git a/app/models/order_item.rb b/app/models/order_item.rb new file mode 100644 index 0000000000..b91b220b11 --- /dev/null +++ b/app/models/order_item.rb @@ -0,0 +1,43 @@ +class OrderItem < ApplicationRecord + belongs_to :product + belongs_to :order + + validates :order_id, presence: true + validates :product_id, presence: true + validates :quantity, presence: true, :numericality => { :greater_than_or_equal_to => 1, only_integer: true } + validates :status, presence: true + validate :available_quantity + + def available_quantity + if product + unless product.stock >= quantity + errors[:quantity] << 'Not enough stock' + end + end + end + + def subtotal + unit_price = self.product.price + quantity = self.quantity + subtotal = (unit_price * quantity).round(2) + return subtotal + end + + def self.existing_oi?(new_oi) + existing_oi = find_by(product_id: new_oi.product_id, order_id: new_oi.order_id) + + return existing_oi.nil? ? nil : existing_oi + end + + + def self.aggregate_orderitem(new_oi, existing_oi) + if existing_oi.product_id == new_oi.product_id + sum = existing_oi.quantity + new_oi.quantity + + unless sum > existing_oi.product.stock # knowing about product quanity + existing_oi.quantity = sum + end + end + return existing_oi + end +end diff --git a/app/models/product.rb b/app/models/product.rb new file mode 100644 index 0000000000..3e1f1bbda4 --- /dev/null +++ b/app/models/product.rb @@ -0,0 +1,24 @@ +class Product < ApplicationRecord + has_and_belongs_to_many :categories, join_table: :products_categories + has_many :reviews, dependent: :destroy + belongs_to :merchant + has_many :order_items, dependent: :destroy + has_many :orders, through: :order_items + accepts_nested_attributes_for :categories + + validates :name, presence: true, uniqueness: true + validates :price, presence: true, numericality: {greater_than: 0} + validates :stock, numericality: { greater_than_or_equal_to: 0 } + + scope :by_category, -> (category_name) { where(retired: false).joins(:categories).merge(Category.where(name: category_name)) } + + def stock_decrement(quantity) + stock = self.stock - quantity + self.update(stock: stock) + end + + def self.find_search(query) + return Product.where('name ILIKE ? OR description ILIKE ?', '%' + query + '%', '%' + query + '%') + end + +end diff --git a/app/models/review.rb b/app/models/review.rb new file mode 100644 index 0000000000..0c8753826d --- /dev/null +++ b/app/models/review.rb @@ -0,0 +1,5 @@ +class Review < ApplicationRecord + belongs_to :product + + validates :rating, presence: true, numericality: { only_integer: true, greater_than: 0, less_than: 6 } +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..253bfd21ef --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,86 @@ + + + + + + + <%= content_for?(:title) ? yield(:title) : "SuperMarket" %> + + <%= stylesheet_link_tag "application" %> + + + <%= javascript_include_tag "application", 'data-turbolinks-track' => true %> + <%= csrf_meta_tags %> + + + +
+ +
+
+ +
+
+ +
+ +
+ <%= flash[:error] if flash.any? %> + <%= flash[:success] if flash.any? %> + <%= flash[:failure] if flash.any? %> +
+ + <%= yield %> +
+ +
+

It's a shop.. it's a store.. It's a SUPERSTORE!

+ +
+
+ + diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000000..cbd34d2e9d --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000000..37f0bddbd7 --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/merchants/index.html.erb b/app/views/merchants/index.html.erb new file mode 100644 index 0000000000..f7142879cb --- /dev/null +++ b/app/views/merchants/index.html.erb @@ -0,0 +1,11 @@ +

Merchants

+ +
+ <% @merchants.each do |merchant| %> +
+
+
<%= link_to merchant.username, merchant_path(merchant) %>
+
+
+ <% end %> +
diff --git a/app/views/merchants/manage_products.html.erb b/app/views/merchants/manage_products.html.erb new file mode 100644 index 0000000000..7911c87238 --- /dev/null +++ b/app/views/merchants/manage_products.html.erb @@ -0,0 +1,23 @@ +

<%= @logged_merchant.username %>

+ +<%= link_to 'Add a New Product or Category', new_merchant_product_path(@logged_merchant), class: 'button body-button manage-products-button' %> + + + + + + + + + + <% @products.each do |product| %> + + + + + + + + <% end %> + +
ProductStockProduct StatusManage Product
<%= link_to product.name, product_path(product.id) %><%= product.stock %><%= product.retired == false ? 'Active' : 'Retired' %><%= link_to 'Edit', edit_merchant_product_path(product.merchant_id, product.id) %><%= link_to 'Retire', merchant_product_path(@logged_merchant.id, product.id), method: :delete %>
diff --git a/app/views/merchants/show.html.erb b/app/views/merchants/show.html.erb new file mode 100644 index 0000000000..04266021cb --- /dev/null +++ b/app/views/merchants/show.html.erb @@ -0,0 +1,27 @@ +<%# should this be an edited products#index? %> + +

<%= @merchant.username %>

+ +<% if @products.empty? %> +
This merchant has no items at this time
+<% else %> +
+ <% @products.each do |product| %> +
+
+ <%= link_to image_tag(product.photo_url, alt: product.name, width: 350, height: 350), product_path(product.id) %> +
+
+
+
+ <%= link_to product.name, product_path(product.id) %> + by <%= link_to product.merchant.username, merchant_path(product.merchant_id)%> +
+ +
+
+
+ <% end %> + +
+<% end %> diff --git a/app/views/orderitems/edit.html.erb b/app/views/orderitems/edit.html.erb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/views/orderitems/new.html.erb b/app/views/orderitems/new.html.erb new file mode 100644 index 0000000000..17a5763d19 --- /dev/null +++ b/app/views/orderitems/new.html.erb @@ -0,0 +1 @@ +

Placeholder - likely won't have this view!

diff --git a/app/views/orderitems/show.html.erb b/app/views/orderitems/show.html.erb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/views/orders/edit.html.erb b/app/views/orders/edit.html.erb new file mode 100644 index 0000000000..de5f320679 --- /dev/null +++ b/app/views/orders/edit.html.erb @@ -0,0 +1,19 @@ +
+ <%= form_for @order, url: order_path, method: :patch do |f| %> + <%= f.label :customer_name %> + <%= f.text_field :customer_name %> + <%= f.label :customer_email %> + <%= f.text_field :customer_email %> + <%= f.label :credit_card %> + <%= f.text_field :credit_card %> + <%= f.label :cvv, "CVV" %> + <%= f.text_field :cvv %> + <%= f.label :cc_expiration, "Credit Card Expiration" %> + <%= f.text_field :cc_expiration%> + <%= f.label :shipping_address %> + <%= f.text_field :shipping_address %> + <%= f.label :billing_address %> + <%= f.text_field :billing_address %> + <%= f.submit "Submit Order", class: "checkout button" %> + <% end %> +
diff --git a/app/views/orders/index.html.erb b/app/views/orders/index.html.erb new file mode 100644 index 0000000000..412d1f7726 --- /dev/null +++ b/app/views/orders/index.html.erb @@ -0,0 +1,168 @@ +<% if @merchant %> + + +

+

Orders Overview

+ <%= link_to 'Manage My Products', merchant_manage_products_path(@merchant), class: 'button body-button manage-products-button' %> +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Pending Orders + + Paid Orders + + Completed Orders + + Cancelled Orders + + All Orders +
+ Revenue + + -- + + <%= number_to_currency(@merchant.total_revenue_by("paid")) %> + + <%= number_to_currency(@merchant.total_revenue_by("completed")) %> + + -- + + <%= number_to_currency(@merchant.total_revenue_by("all")) %> +
+ No. Orders + + <%= @merchant.count_orders_by("pending") %> + + <%= @merchant.count_orders_by("paid") %> + + <%= @merchant.count_orders_by("completed") %> + + <%= @merchant.count_orders_by("cancelled") %> + + <%= @merchant.count_orders_by("all") %> +
+
+ +

+ <%= @merchant.username %>'s orders +

+ +
+ <%= form_tag orders_path, class: "form-inline by-status", method: :get do %> + <%= select_tag :order_status_filter, options_for_select(['all', 'paid', 'pending', 'completed', 'cancelled'], params[:order_status_filter]) %> + <%= submit_tag "Search", class: 'button body-button status-submit' %> + <% end %> + <% if params[:order_status_filter] %> +

<%= params[:order_status_filter].capitalize %> orders:

+ <% else %> +

All orders:

+ <% end %> +
+ +
+ <% if @orders_and_items.empty? %> +

+ <%= @merchant.username %> has no <%= params[:order_status_filter] unless params[:order_status_filter] == 'all' %> orders yet. +

+ <% else %> + + + + + + + + + + + + + + <% @orders_and_items.each do |order, order_items| %> + + + + + <% order_items.each do |order_item| %> + + + + + + + + <% end %> + + <% end %> + +
+ Order + + Order Status + + Product + + Quantity + + Subtotal + + Placed On + + Mark as Shipped + + Cancel +
+ <%= link_to order, order_path(order) %> + + <%= Order.find(order).order_status.capitalize %> + + <%= link_to order_item.product.name, product_path(order_item.product) %> + + <%= order_item.quantity %> + + <%= number_to_currency(order_item.subtotal) %> + + <%= readable_date(order_item.order.updated_at) %> + + <% if order_item.status == "pending" %> + <%= link_to_if Order.find(order).status != "pending", "Ship", ship_path(order_item), method: :patch %> + <% elsif order_item.status == "shipped" %> + Shipped + <% end %> + + <% if order_item.status == "pending" %> + <%= link_to_if Order.find(order).status != "pending", "Cancel", cancel_path(order_item), method: :patch %> + <% elsif order_item.status == "cancelled" %> + Cancelled + <% end %> +
+ + <% end %> + +
+<% end %> diff --git a/app/views/orders/show.html.erb b/app/views/orders/show.html.erb new file mode 100644 index 0000000000..b64e14c0d6 --- /dev/null +++ b/app/views/orders/show.html.erb @@ -0,0 +1,57 @@ +
+

Order Confirmation

+

Order Placed: <%= readable_date(@order.updated_at) %>

+

Order Total: <%= number_to_currency(@order.order_total) %>

+

Order Status: <%= @order.status %>

+
+

Customer Information

+
    +
  • Customer Name: <%= @order.customer_name %>
  • +
  • Customer Email: <%= @order.customer_email %>
  • +
  • Credit Card: *<%= @order.credit_card[-4..-1] %>
  • +
  • Exp Date: <%= @order.cc_expiration %>
  • +
  • Shipping Address: <%= @order.shipping_address %>
  • +
  • Order Status: <%= @order.status %>
  • +
+
+
+ <% if session[:merchant_id] %> +

Purchased Items

+ + + + + + + <% merchants_items = @order.order_items.select {|orderitem| orderitem.product.merchant_id == session[:merchant_id]} %> + <% merchants_items.each do |item| %> + + + + + + + <% end %> + +
ProductQuantityPriceSubtotal
<%= link_to item.product.name, product_path(item.product_id) %><%= item.quantity %><%= number_to_currency(item.product.price) %><%= number_to_currency(item.subtotal) %>
+ <% else %> +

Purchased Items

+ + + + + + + <% @order.order_items.each do |item| %> + + + + + + + <% end %> + +
ProductQuantityPriceSubtotal
<%= link_to item.product.name, product_path(item.product_id) %><%= item.quantity %><%= number_to_currency(item.product.price) %><%= number_to_currency(item.subtotal) %>
+ <% end %> +
+
diff --git a/app/views/orders/viewcart.html.erb b/app/views/orders/viewcart.html.erb new file mode 100644 index 0000000000..c1606d61c1 --- /dev/null +++ b/app/views/orders/viewcart.html.erb @@ -0,0 +1,20 @@ +

Your Cart

+ +
+ <% @order.order_items.each do |item| %> +
+ <%= image_tag item.product.photo_url, class: 'viewcart-photo'%> + <%= item.product.name %> + <%= number_to_currency(item.product.price) %> + <%= form_for item, url: orderitem_path(item), method: :patch do |f| %> + <%= f.select :quantity, (1..item.product.stock).to_a, :selected => item.quantity, class: 'dropdown' %> + <%= f.submit "Update Quantity", class: 'viewcart-item button small body-button' %> + <% end %> + <%= link_to "Remove Item", orderitem_path(item), method: :delete, class: 'viewcart-item', data: { confirm: "With great power comes great responsibility. Are you sure you want to delete #{item.product.name}?"} %> +
+ <% end %> + +
Subtotal: <%= number_to_currency(@order.order_total) %>
+

<%= link_to "Checkout", edit_order_path(@order), class: 'checkout button' %>

+ +
diff --git a/app/views/products/_form.html.erb b/app/views/products/_form.html.erb new file mode 100644 index 0000000000..16381b1d19 --- /dev/null +++ b/app/views/products/_form.html.erb @@ -0,0 +1,39 @@ +

<%= page_title %>

+ +<% if @product %> + <% if @product.errors.any? %> + <% @product.errors.messages.each do |column, message| %> + <%= column %>: <%= message %> + <% end %> + <% end %> +<% end %> +<% if @category %> + <% if @category.errors.any? %> + <%= @category.errors.messages.each do |column, message| %> + <%= column %>: <%= message %> + <% end %> + <% end %> +<% end %> + +<%= form_tag categories_path do %> + <%= label_tag :name, 'New Category' %> + <%= text_field_tag :name, params[:name], required: true %> + <%= button_tag 'Add', class: 'button small body-button' %> +<% end %> + +<%= form_for @product, url: url do |f| %> + <%= f.hidden_field :merchant_id, value: @logged_merchant.id %> + <%= f.label :name, 'Product Name' %> + <%= f.text_field :name %> + <%= f.label :price %> + <%= f.text_field :price %> + <%= f.label :stock %> + <%= f.number_field :stock %> + <%= f.label :categories, 'Categories for this Product' %> + <%= collection_check_boxes :product, :category_ids, Category.all, :id, :name %> + <%= f.label :description, 'Product Description' %> + <%= f.text_area :description %> + <%= f.label :photo_url %> + <%= f.text_field :photo_url %> + <%= f.submit action_name, class: 'button body-button' %> +<% end %> diff --git a/app/views/products/_products.html.erb b/app/views/products/_products.html.erb new file mode 100644 index 0000000000..8efdf40744 --- /dev/null +++ b/app/views/products/_products.html.erb @@ -0,0 +1,31 @@ +
+ +

Products

+ +
+ <% if @products.empty? %> +

There are no products available at this time

+ <% else %> + <% @products.each do |product| %> +
+
+ <%= link_to image_tag(product.photo_url, alt: product.name), product_path(product.id) %> +
+
+
+
+ <%= link_to product.name, product_path(product.id) %> +
+
+ by <%= link_to product.merchant.username, merchant_path(product.merchant_id)%> +
+ +
+
+
+ <% end %> + <% end %> + +
+ +
diff --git a/app/views/products/edit.html.erb b/app/views/products/edit.html.erb new file mode 100644 index 0000000000..7ef84e05ac --- /dev/null +++ b/app/views/products/edit.html.erb @@ -0,0 +1 @@ +<%= render partial: 'form', locals: {action_name: 'Edit Product', url: merchant_product_path, page_title: 'Edit Product'} %> diff --git a/app/views/products/home.html.erb b/app/views/products/home.html.erb new file mode 100644 index 0000000000..213b94da2b --- /dev/null +++ b/app/views/products/home.html.erb @@ -0,0 +1,6 @@ + + +<%= render partial: 'products' %> diff --git a/app/views/products/index.html.erb b/app/views/products/index.html.erb new file mode 100644 index 0000000000..2df3935567 --- /dev/null +++ b/app/views/products/index.html.erb @@ -0,0 +1,5 @@ +<% if @category %> +

<%= @category.name.capitalize %>

+<% end %> + +<%= render partial: 'products' %> diff --git a/app/views/products/new.html.erb b/app/views/products/new.html.erb new file mode 100644 index 0000000000..9065f40335 --- /dev/null +++ b/app/views/products/new.html.erb @@ -0,0 +1 @@ +<%= render partial: 'form', locals: {action_name: 'Create New Product', url: merchant_products_path, page_title: 'Add a New Product'} %> diff --git a/app/views/products/show.html.erb b/app/views/products/show.html.erb new file mode 100644 index 0000000000..c1e77a6e9c --- /dev/null +++ b/app/views/products/show.html.erb @@ -0,0 +1,55 @@ +
+ <%= image_tag(@product.photo_url, alt: @product.name, class: 'product-photo') %> +
+

<%= @product.name %>

+

<%= number_to_currency(@product.price) %>

+

<%= @product.description %>

+

Quantity Available: <%= @product.stock %>

+ + <%= form_for :order_item, url: orderitems_path, method: :post do |f| %> + <%= f.hidden_field :product_id, :value => @product.id%> + <% if @product.stock >= 1 %> + <%= f.label :quantity %> + <%= f.select :quantity, (1..@product.stock).to_a, :selected => 1 %> + <%= f.submit 'Add to Cart', class: 'button' %> + <% end %> + <% end %> + + <%= link_to "See More from this merchant", merchant_path(@product.merchant_id) %> +
+ +
+

Product Reviews

+ + <% if @logged_merchant.nil? || @logged_merchant.id != @product.merchant_id %> +
+
Add a new review
+ <%= form_for :review, url: reviews_path do |f| %> + <%= f.hidden_field :product_id, value: @product.id %> + <%= f.label :rating %> + <%= f.select :rating, (1..5).to_a %> + <%= f.label :description %> + <%= f.text_area :description %> + <%= f.submit 'Submit Review', class: 'button' %> + <% end %> +
+ <% end %> + + +
+ <% product_reviews = Review.where(product_id: @product) %> +
    + <% product_reviews.each do |review| %> +
  • +
      +
    • <%= star_rating(review.rating) %>
    • +
    • Created at: <%= readable_date(review.created_at) %>
    • +
    • <%= review.description %>
    • +
    +
  • + <% end %> +
+
+
+ +
diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 0000000000..66e9889e8b --- /dev/null +++ b/bin/bundle @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +load Gem.bin_path('bundler', 'bundle') diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000000..5badb2fde0 --- /dev/null +++ b/bin/rails @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000000..d87d5f5781 --- /dev/null +++ b/bin/rake @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000000..78c4e861dc --- /dev/null +++ b/bin/setup @@ -0,0 +1,38 @@ +#!/usr/bin/env ruby +require 'pathname' +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a starting point to setup your application. + # Add necessary setup steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # Install JavaScript dependencies if using Yarn + # system('bin/yarn') + + + # puts "\n== Copying sample files ==" + # unless File.exist?('config/database.yml') + # cp 'config/database.yml.sample', 'config/database.yml' + # end + + puts "\n== Preparing database ==" + system! 'bin/rails db:setup' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/bin/spring b/bin/spring new file mode 100755 index 0000000000..fb2ec2ebb4 --- /dev/null +++ b/bin/spring @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby + +# This file loads spring without using Bundler, in order to be fast. +# It gets overwritten when you run the `spring binstub` command. + +unless defined?(Spring) + require 'rubygems' + require 'bundler' + + lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) + spring = lockfile.specs.detect { |spec| spec.name == "spring" } + if spring + Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path + gem 'spring', spring.version + require 'spring/binstub' + end +end diff --git a/bin/update b/bin/update new file mode 100755 index 0000000000..a8e4462f20 --- /dev/null +++ b/bin/update @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +require 'pathname' +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a way to update your development environment automatically. + # Add necessary update steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + puts "\n== Updating database ==" + system! 'bin/rails db:migrate' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/bin/yarn b/bin/yarn new file mode 100755 index 0000000000..c2bacef836 --- /dev/null +++ b/bin/yarn @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +VENDOR_PATH = File.expand_path('..', __dir__) +Dir.chdir(VENDOR_PATH) do + begin + exec "yarnpkg #{ARGV.join(" ")}" + rescue Errno::ENOENT + $stderr.puts "Yarn executable was not detected in the system." + $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" + exit 1 + end +end diff --git a/config.ru b/config.ru new file mode 100644 index 0000000000..f7ba0b527b --- /dev/null +++ b/config.ru @@ -0,0 +1,5 @@ +# This file is used by Rack-based servers to start the application. + +require_relative 'config/environment' + +run Rails.application diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000000..5f6ca0f9b2 --- /dev/null +++ b/config/application.rb @@ -0,0 +1,25 @@ +require_relative 'boot' + +require 'rails/all' + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Betsy + class Application < Rails::Application + config.generators do |g| + # Force new test files to be generated in the minitest-spec style + g.test_framework :minitest, spec: true + + # Always use .js files, never .coffee + g.javascript_engine :js + end + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 5.1 + + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000000..30f5120df6 --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,3 @@ +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'bundler/setup' # Set up gems listed in the Gemfile. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000000..3cba994bb2 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: async + +production: + adapter: redis + url: redis://localhost:6379/1 + channel_prefix: betsy_production diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000000..6903bb6083 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,85 @@ +# PostgreSQL. Versions 9.1 and up are supported. +# +# Install the pg driver: +# gem install pg +# On OS X with Homebrew: +# gem install pg -- --with-pg-config=/usr/local/bin/pg_config +# On OS X with MacPorts: +# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config +# On Windows: +# gem install pg +# Choose the win32 build. +# Install PostgreSQL and put its /bin directory on your path. +# +# Configure Using Gemfile +# gem 'pg' +# +default: &default + adapter: postgresql + encoding: unicode + # For details on connection pooling, see Rails configuration guide + # http://guides.rubyonrails.org/configuring.html#database-pooling + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + +development: + <<: *default + database: betsy_development + + # The specified database role being used to connect to postgres. + # To create additional roles in postgres see `$ createuser --help`. + # When left blank, postgres will use the default role. This is + # the same name as the operating system user that initialized the database. + #username: betsy + + # The password associated with the postgres role (username). + #password: + + # Connect on a TCP socket. Omitted by default since the client uses a + # domain socket that doesn't need configuration. Windows does not have + # domain sockets, so uncomment these lines. + #host: localhost + + # The TCP port the server listens on. Defaults to 5432. + # If your server runs on a different port number, change accordingly. + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # log, notice, warning, error, fatal, and panic + # Defaults to warning. + #min_messages: notice + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: betsy_test + +# As with config/secrets.yml, you never want to store sensitive information, +# like your database password, in your source code. If your source code is +# ever seen by anyone, they now have access to your database. +# +# Instead, provide the password as a unix environment variable when you boot +# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database +# for a full rundown on how to provide these environment variables in a +# production deployment. +# +# On Heroku and other platform providers, you may have a full connection URL +# available as an environment variable. For example: +# +# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" +# +# You can use this database configuration with: +# +# production: +# url: <%= ENV['DATABASE_URL'] %> +# +production: + <<: *default + database: betsy_production + username: betsy + password: <%= ENV['BETSY_DATABASE_PASSWORD'] %> diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000000..426333bb46 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative 'application' + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000000..5187e22186 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,54 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + if Rails.root.join('tmp/caching-dev.txt').exist? + config.action_controller.perform_caching = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. + config.assets.debug = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + config.file_watcher = ActiveSupport::EventedFileUpdateChecker +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000000..9284f84839 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,91 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Attempt to read encrypted secrets from `config/secrets.yml.enc`. + # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or + # `config/secrets.yml.key`. + config.read_encrypted_secrets = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? + + # Compress JavaScripts and CSS. + config.assets.js_compressor = :uglifier + # config.assets.css_compressor = :sass + + # Do not fallback to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = 'http://assets.example.com' + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + + # Mount Action Cable outside main process or domain + # config.action_cable.mount_path = nil + # config.action_cable.url = 'wss://example.com/cable' + # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Use the lowest log level to ensure availability of diagnostic information + # when problems arise. + config.log_level = :debug + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment) + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "betsy_#{Rails.env}" + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners. + config.active_support.deprecation = :notify + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require 'syslog/logger' + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000000..8e5cbde533 --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,42 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true + + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}" + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true +end diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb new file mode 100644 index 0000000000..89d2efab2b --- /dev/null +++ b/config/initializers/application_controller_renderer.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 0000000000..4b828e80cb --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path +# Add Yarn node_modules folder to the asset load path. +Rails.application.config.assets.paths << Rails.root.join('node_modules') + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in the app/assets +# folder are already added. +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb new file mode 100644 index 0000000000..59385cdf37 --- /dev/null +++ b/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers! diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb new file mode 100644 index 0000000000..5a6a32d371 --- /dev/null +++ b/config/initializers/cookies_serializer.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000000..4a994e1e7b --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [:password] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000000..ac033bf9dc --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym 'RESTful' +# end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb new file mode 100644 index 0000000000..dc1899682b --- /dev/null +++ b/config/initializers/mime_types.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb new file mode 100644 index 0000000000..03e7984f03 --- /dev/null +++ b/config/initializers/omniauth.rb @@ -0,0 +1,3 @@ +Rails.application.config.middleware.use OmniAuth::Builder do + provider :google_oauth2, ENV["GOOGLE_CLIENT_ID"], ENV["GOOGLE_CLIENT_SECRET"], scope: "email, profile" +end diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb new file mode 100644 index 0000000000..bbfc3961bf --- /dev/null +++ b/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] +end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000000..decc5a8573 --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,33 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# 'true': 'foo' +# +# To learn more, please read the Rails Internationalization guide +# available at http://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000000..1e19380dcb --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,56 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +# +threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked webserver processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. If you use this option +# you need to make sure to reconnect any threads in the `on_worker_boot` +# block. +# +# preload_app! + +# If you are preloading your application and using Active Record, it's +# recommended that you close any connections to the database before workers +# are forked to prevent connection leakage. +# +# before_fork do +# ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) +# end + +# The code in the `on_worker_boot` will be called if you are using +# clustered mode by specifying a number of `workers`. After each worker +# process is booted, this block will be run. If you are using the `preload_app!` +# option, you will want to use this block to reconnect to any threads +# or connections that may have been created at application boot, as Ruby +# cannot share connections between processes. +# +# on_worker_boot do +# ActiveRecord::Base.establish_connection if defined?(ActiveRecord) +# end +# + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000000..b0fe5f3ce1 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,37 @@ +Rails.application.routes.draw do + # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html + + root 'products#home' + + resources :orders + + resources :merchants, except: [:new, :create] do + resources :orders, only: [:index] + resources :products, except: [:show] + get '/manage_products', to: 'merchants#manage_products', as: 'manage_products' + end + + resources :orderitems, only: [:create, :update, :destroy] + + resources :reviews, only: [:create] + + resources :categories, only: [:create] + + resources :products, only: [:index, :show] + + + get '/:category', to: 'products#index', as: 'category' + + post '/products/categories/new', to: 'products#new_category', as: 'new_category' + + get '/:id/viewcart', to: 'orders#viewcart', as: 'viewcart' + + get "/auth/:provider/callback", to: "sessions#create", as: 'auth_callback' + + delete "/logout", to: "sessions#destroy", as: "logout" + + patch '/order_items/:id/cancel', to: 'orderitems#cancel', as: 'cancel' + patch '/order_items/:id/ship', to: 'orderitems#ship', as: 'ship' + + get '/search', to: 'products#index', as: 'search' +end diff --git a/config/secrets.yml b/config/secrets.yml new file mode 100644 index 0000000000..eb6904ad4b --- /dev/null +++ b/config/secrets.yml @@ -0,0 +1,32 @@ +# Be sure to restart your server when you modify this file. + +# Your secret key is used for verifying the integrity of signed cookies. +# If you change this key, all old signed cookies will become invalid! + +# Make sure the secret is at least 30 characters and all random, +# no regular words or you'll be exposed to dictionary attacks. +# You can use `rails secret` to generate a secure secret key. + +# Make sure the secrets in this file are kept private +# if you're sharing your code publicly. + +# Shared secrets are available across all environments. + +# shared: +# api_key: a1B2c3D4e5F6 + +# Environmental secrets are only available for that specific environment. + +development: + secret_key_base: 34fb4a95a4674d0975fcd131a3f269c2b709575c4bb992502aaa40db62f58292965df4b9433ae4126e12c20dca1fdaa481ba2c7fdebb919d62cd0d983f2741b5 + +test: + secret_key_base: 1d4ffcfb3ee3e78cae04116f4cc76df3eb789be342ea4bb3e321d58145dbc5eb1b508a0f89254b9009063d62b86e856f67214970c6899af60792928ccd69223f + +# Do not keep production secrets in the unencrypted secrets file. +# Instead, either read values from the environment. +# Or, use `bin/rails secrets:setup` to configure encrypted secrets +# and move the `production:` environment over there. + +production: + secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> diff --git a/config/spring.rb b/config/spring.rb new file mode 100644 index 0000000000..c9119b40c0 --- /dev/null +++ b/config/spring.rb @@ -0,0 +1,6 @@ +%w( + .ruby-version + .rbenv-vars + tmp/restart.txt + tmp/caching-dev.txt +).each { |path| Spring.watch(path) } diff --git a/db/migrate/20180419033356_create_merchants.rb b/db/migrate/20180419033356_create_merchants.rb new file mode 100644 index 0000000000..64a57c904c --- /dev/null +++ b/db/migrate/20180419033356_create_merchants.rb @@ -0,0 +1,10 @@ +class CreateMerchants < ActiveRecord::Migration[5.1] + def change + create_table :merchants do |t| + t.string :username + t.string :email + + t.timestamps + end + end +end diff --git a/db/migrate/20180419034041_create_reviews.rb b/db/migrate/20180419034041_create_reviews.rb new file mode 100644 index 0000000000..f369d1c988 --- /dev/null +++ b/db/migrate/20180419034041_create_reviews.rb @@ -0,0 +1,9 @@ +class CreateReviews < ActiveRecord::Migration[5.1] + def change + create_table :reviews do |t| + t.integer :rating + + t.timestamps + end + end +end diff --git a/db/migrate/20180419044712_create_products.rb b/db/migrate/20180419044712_create_products.rb new file mode 100644 index 0000000000..94e1798ebf --- /dev/null +++ b/db/migrate/20180419044712_create_products.rb @@ -0,0 +1,12 @@ +class CreateProducts < ActiveRecord::Migration[5.1] + def change + create_table :products do |t| + t.string :name + t.string :stock + t.float :price + t.text :description + + t.timestamps + end + end +end diff --git a/db/migrate/20180419045117_create_categories.rb b/db/migrate/20180419045117_create_categories.rb new file mode 100644 index 0000000000..5bef4913b8 --- /dev/null +++ b/db/migrate/20180419045117_create_categories.rb @@ -0,0 +1,9 @@ +class CreateCategories < ActiveRecord::Migration[5.1] + def change + create_table :categories do |t| + t.string :name + + t.timestamps + end + end +end diff --git a/db/migrate/20180419045927_create_products_categories.rb b/db/migrate/20180419045927_create_products_categories.rb new file mode 100644 index 0000000000..cb0a2423f0 --- /dev/null +++ b/db/migrate/20180419045927_create_products_categories.rb @@ -0,0 +1,8 @@ +class CreateProductsCategories < ActiveRecord::Migration[5.1] + def change + create_table :products_categories do |t| + t.belongs_to :product, index: true + t.belongs_to :category, index: true + end + end +end diff --git a/db/migrate/20180419050820_create_orders.rb b/db/migrate/20180419050820_create_orders.rb new file mode 100644 index 0000000000..dbe01c2067 --- /dev/null +++ b/db/migrate/20180419050820_create_orders.rb @@ -0,0 +1,16 @@ +class CreateOrders < ActiveRecord::Migration[5.1] + def change + create_table :orders do |t| + t.string :customer_name + t.string :customer_email + t.string :credit_card + t.string :CVV + t.string :CC_expiration + t.string :status + t.string :shipping_address + t.string :billing_address + + t.timestamps + end + end +end diff --git a/db/migrate/20180419053635_create_order_items.rb b/db/migrate/20180419053635_create_order_items.rb new file mode 100644 index 0000000000..1b14bf865b --- /dev/null +++ b/db/migrate/20180419053635_create_order_items.rb @@ -0,0 +1,11 @@ +class CreateOrderItems < ActiveRecord::Migration[5.1] + def change + create_table :order_items do |t| + t.integer :quantity + t.integer :product_id + t.integer :order_id + + t.timestamps + end + end +end diff --git a/db/migrate/20180419171654_add_merchant_id_to_products.rb b/db/migrate/20180419171654_add_merchant_id_to_products.rb new file mode 100644 index 0000000000..45930fd0d9 --- /dev/null +++ b/db/migrate/20180419171654_add_merchant_id_to_products.rb @@ -0,0 +1,5 @@ +class AddMerchantIdToProducts < ActiveRecord::Migration[5.1] + def change + add_reference :products, :merchant, foreign_key: true + end +end diff --git a/db/migrate/20180419171936_product_id_to_reviews.rb b/db/migrate/20180419171936_product_id_to_reviews.rb new file mode 100644 index 0000000000..58e4c7b6dd --- /dev/null +++ b/db/migrate/20180419171936_product_id_to_reviews.rb @@ -0,0 +1,5 @@ +class ProductIdToReviews < ActiveRecord::Migration[5.1] + def change + add_reference :reviews, :product, foreign_key: true + end +end diff --git a/db/migrate/20180419172655_add_product_to_order_item.rb b/db/migrate/20180419172655_add_product_to_order_item.rb new file mode 100644 index 0000000000..3672da38ca --- /dev/null +++ b/db/migrate/20180419172655_add_product_to_order_item.rb @@ -0,0 +1,6 @@ +class AddProductToOrderItem < ActiveRecord::Migration[5.1] + def change + remove_column :order_items, :product_id, :integer + add_reference :order_items, :product, foreign_key: true + end +end diff --git a/db/migrate/20180419210206_add_orderto_order_items.rb b/db/migrate/20180419210206_add_orderto_order_items.rb new file mode 100644 index 0000000000..e85143d2bf --- /dev/null +++ b/db/migrate/20180419210206_add_orderto_order_items.rb @@ -0,0 +1,7 @@ +class AddOrdertoOrderItems < ActiveRecord::Migration[5.1] + def change + remove_column :order_items, :order_id, :integer + + add_reference :order_items, :order, foreign_key: true + end +end diff --git a/db/migrate/20180419213238_add_description_to_review.rb b/db/migrate/20180419213238_add_description_to_review.rb new file mode 100644 index 0000000000..4c7de16c49 --- /dev/null +++ b/db/migrate/20180419213238_add_description_to_review.rb @@ -0,0 +1,5 @@ +class AddDescriptionToReview < ActiveRecord::Migration[5.1] + def change + add_column :reviews, :description, :text + end +end diff --git a/db/migrate/20180420062814_add_status_to_products.rb b/db/migrate/20180420062814_add_status_to_products.rb new file mode 100644 index 0000000000..a12aa899ef --- /dev/null +++ b/db/migrate/20180420062814_add_status_to_products.rb @@ -0,0 +1,5 @@ +class AddStatusToProducts < ActiveRecord::Migration[5.1] + def change + add_column :products, :retired, :boolean, default: false + end +end diff --git a/db/migrate/20180420175356_change_credit_card_columns_to_lower_case.rb b/db/migrate/20180420175356_change_credit_card_columns_to_lower_case.rb new file mode 100644 index 0000000000..beff288d0b --- /dev/null +++ b/db/migrate/20180420175356_change_credit_card_columns_to_lower_case.rb @@ -0,0 +1,6 @@ +class ChangeCreditCardColumnsToLowerCase < ActiveRecord::Migration[5.1] + def change + rename_column :orders, :CVV, :cvv + rename_column :orders, :CC_expiration, :cc_expiration + end +end diff --git a/db/migrate/20180421173034_change_product_stock_to_integer.rb b/db/migrate/20180421173034_change_product_stock_to_integer.rb new file mode 100644 index 0000000000..f375900ea4 --- /dev/null +++ b/db/migrate/20180421173034_change_product_stock_to_integer.rb @@ -0,0 +1,5 @@ +class ChangeProductStockToInteger < ActiveRecord::Migration[5.1] + def change + change_column :products, :stock, :integer, using: 'stock::integer' + end +end diff --git a/db/migrate/20180421225111_modify_merchant_for_authentication.rb b/db/migrate/20180421225111_modify_merchant_for_authentication.rb new file mode 100644 index 0000000000..67523944db --- /dev/null +++ b/db/migrate/20180421225111_modify_merchant_for_authentication.rb @@ -0,0 +1,6 @@ +class ModifyMerchantForAuthentication < ActiveRecord::Migration[5.1] + def change + add_column :merchants, :uid, :integer, options: { null: false } + add_column :merchants, :provider, :string, options: { null: false } + end +end diff --git a/db/migrate/20180421234834_make_merchant_uid_big_int.rb b/db/migrate/20180421234834_make_merchant_uid_big_int.rb new file mode 100644 index 0000000000..5eff310fab --- /dev/null +++ b/db/migrate/20180421234834_make_merchant_uid_big_int.rb @@ -0,0 +1,5 @@ +class MakeMerchantUidBigInt < ActiveRecord::Migration[5.1] + def change + change_column :merchants, :uid, :integer, limit: 8 + end +end diff --git a/db/migrate/20180421235138_make_merchant_uid_bigger.rb b/db/migrate/20180421235138_make_merchant_uid_bigger.rb new file mode 100644 index 0000000000..32e2ef0a16 --- /dev/null +++ b/db/migrate/20180421235138_make_merchant_uid_bigger.rb @@ -0,0 +1,5 @@ +class MakeMerchantUidBigger < ActiveRecord::Migration[5.1] + def change + change_column :merchants, :uid, :bigint + end +end diff --git a/db/migrate/20180421235852_change_uid_to_string.rb b/db/migrate/20180421235852_change_uid_to_string.rb new file mode 100644 index 0000000000..4697ea6efc --- /dev/null +++ b/db/migrate/20180421235852_change_uid_to_string.rb @@ -0,0 +1,5 @@ +class ChangeUidToString < ActiveRecord::Migration[5.1] + def change + change_column :merchants, :uid, :string + end +end diff --git a/db/migrate/20180423172432_add_photo_url_to_products.rb b/db/migrate/20180423172432_add_photo_url_to_products.rb new file mode 100644 index 0000000000..4521172bc9 --- /dev/null +++ b/db/migrate/20180423172432_add_photo_url_to_products.rb @@ -0,0 +1,5 @@ +class AddPhotoUrlToProducts < ActiveRecord::Migration[5.1] + def change + add_column :products, :photo_url, :string + end +end diff --git a/db/migrate/20180424190055_add_status_to_order_item.rb b/db/migrate/20180424190055_add_status_to_order_item.rb new file mode 100644 index 0000000000..93cf2bd147 --- /dev/null +++ b/db/migrate/20180424190055_add_status_to_order_item.rb @@ -0,0 +1,5 @@ +class AddStatusToOrderItem < ActiveRecord::Migration[5.1] + def change + add_column :order_items, :status, :string + end +end diff --git a/db/migrate/20180425060326_add_default_pending_status_to_order_item.rb b/db/migrate/20180425060326_add_default_pending_status_to_order_item.rb new file mode 100644 index 0000000000..6d22b9a3d3 --- /dev/null +++ b/db/migrate/20180425060326_add_default_pending_status_to_order_item.rb @@ -0,0 +1,5 @@ +class AddDefaultPendingStatusToOrderItem < ActiveRecord::Migration[5.1] + def change + change_column :order_items, :status, :string, default: "pending" + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000000..d742a37f9a --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,90 @@ +# 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: 20180425060326) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "categories", force: :cascade do |t| + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "merchants", force: :cascade do |t| + t.string "username" + t.string "email" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "uid" + t.string "provider" + end + + create_table "order_items", force: :cascade do |t| + t.integer "quantity" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "product_id" + t.bigint "order_id" + t.string "status", default: "pending" + t.index ["order_id"], name: "index_order_items_on_order_id" + t.index ["product_id"], name: "index_order_items_on_product_id" + end + + create_table "orders", force: :cascade do |t| + t.string "customer_name" + t.string "customer_email" + t.string "credit_card" + t.string "cvv" + t.string "cc_expiration" + t.string "status" + t.string "shipping_address" + t.string "billing_address" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "products", force: :cascade do |t| + t.string "name" + t.integer "stock" + t.float "price" + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "merchant_id" + t.boolean "retired", default: false + t.string "photo_url" + t.index ["merchant_id"], name: "index_products_on_merchant_id" + end + + create_table "products_categories", force: :cascade do |t| + t.bigint "product_id" + t.bigint "category_id" + t.index ["category_id"], name: "index_products_categories_on_category_id" + t.index ["product_id"], name: "index_products_categories_on_product_id" + end + + create_table "reviews", force: :cascade do |t| + t.integer "rating" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "product_id" + t.text "description" + t.index ["product_id"], name: "index_reviews_on_product_id" + end + + add_foreign_key "order_items", "orders" + add_foreign_key "order_items", "products" + add_foreign_key "products", "merchants" + add_foreign_key "reviews", "products" +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000000..c5cc3df5ee --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,86 @@ +# This file should contain all the record creation needed to seed the database with its default values. +# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). +# +# Examples: +# +# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) +# Character.create(name: 'Luke', movie: movies.first) + +category_names = ['accessories', 'capes', 'stickers', 'utility', 'media'] + +unsaved_categories = [] +category_names.each do |category| + category = Category.new(name: category) + successful = category.save + + if !successful + unsaved_categories << category.name + end +end +puts "#{unsaved_categories.count} categories not saved" + + +merchants = ['wonder pig', 'captain dog', 'professor heggie', 'batcat'] +unsaved_merchants = [] +merchants.each do |name| + merchant = Merchant.new + merchant.username = name + merchant.email = name.gsub(' ', '_').concat('@superpet.com') + merchant.uid = rand(10000..99999).to_s + merchant.provider = 'google_oauth2' + + successful = merchant.save + + if !successful + unsaved_merchants << merchant.name + end +end + +puts "#{unsaved_merchants.count} merchants not saved" +category_hash = {} +Category.all.map {|category| category_hash[category.name] = category} + +product_names = [ + ['Green Lantern', 'https://images-na.ssl-images-amazon.com/images/I/51ByMhmwMWL._SY879_.jpg', [category_hash['utility'],] ], + ['Ms Marvel Stickers', 'https://i.pinimg.com/236x/36/dc/70/36dc7094fef7a1db3596db10eee68a07--ms-marvel-sticker.jpg', [ category_hash['stickers'], category_hash['accessories']] ], + ['Batarang', 'https://www.thinkgeek.com/images/products/zoom/httq_batman_batarang_letter_opener.jpg', [category_hash['utility'], category_hash['accessories']] ], + ['Black Eye Mask', 'http://www.5050factoryoutlet.com/istarimages/mp/395936-10!02-30648_d.jpg', [category_hash['accessories']]], + ['Yellow Cape', 'https://i.pinimg.com/736x/b1/c6/d1/b1c6d16f5d0cc1ea35ff4344f1aaa386--halloween-costumes-for-dogs-pet-costumes.jpg', [category_hash['accessories'], category_hash['capes']]], + ['Entire Captain Planet Series on VHS', 'https://img1.etsystatic.com/142/0/5844803/il_570xN.1130347923_5jxi.jpg', [category_hash['media']]], + ['Storm Wig', 'https://ssli.ebayimg.com/images/g/Mv8AAOSwLN5WiOXY/s-l640.jpg', [category_hash['accessories']]], + ['Black Widdow Utility Cuffs', 'https://img.etsystatic.com/il/42fa43/1103666807/il_340x270.1103666807_fn4o.jpg?version=0', [category_hash['accessories'], category_hash['utility']] ], + ['Red Hair Dye', 'https://www.softsheen-carson.com/~/media/Images/SoftsheenUS/Dark%20and%20Lovely/ALL_GO%20INTENSE%20SPICY%20RED_1000x1000.jpg', [category_hash['accessories']]] +] + +unsaved_products = [] +product_names.each do |product| + prod = Product.new + prod.merchant_id = Merchant.all.sample.id + prod.name = product[0] + prod.stock = rand(1..10) + prod.price = rand(1..100) + prod.photo_url = product[1] + prod.categories = product[2] + + successful = prod.save! + if !successful + unsaved_products << prod.name + end +end +puts "#{unsaved_products.count} products not saved" + +unsaved_orderitems = [] +5.times do + orderitem = OrderItem.new + product = Product.all.sample + orderitem.product_id = product.id + orderitem.quantity = rand(1..product.stock) + orderitem.order_id = Order.create!.id + + successful = orderitem.save! + if !successful + unsaved_orderitems << orderitem + end +end + +puts "#{unsaved_orderitems.count} orderitems not saved" diff --git a/lib/assets/.keep b/lib/assets/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/log/.keep b/log/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/package.json b/package.json new file mode 100644 index 0000000000..f874acf437 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "name": "betsy", + "private": true, + "dependencies": {} +} diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000000..2be3af26fc --- /dev/null +++ b/public/404.html @@ -0,0 +1,67 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
+
+

The page you were looking for doesn't exist.

+

You may have mistyped the address or the page may have moved.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/public/422.html b/public/422.html new file mode 100644 index 0000000000..c08eac0d1d --- /dev/null +++ b/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
+
+

The change you wanted was rejected.

+

Maybe you tried to change something you didn't have access to.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/public/500.html b/public/500.html new file mode 100644 index 0000000000..78a030af22 --- /dev/null +++ b/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
+
+

We're sorry, but something went wrong.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000000..37b576a4a0 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1 @@ +# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb new file mode 100644 index 0000000000..d19212abd5 --- /dev/null +++ b/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/test/controllers/.keep b/test/controllers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/controllers/categories_controller_test.rb b/test/controllers/categories_controller_test.rb new file mode 100644 index 0000000000..e926788b71 --- /dev/null +++ b/test/controllers/categories_controller_test.rb @@ -0,0 +1,29 @@ +require "test_helper" + +describe CategoriesController do + describe'create' do + let(:merchant) { Merchant.first } + before do + login(merchant) + end + + it 'creates a category with valid data' do + old_category_count = Category.count + + post categories_path, params: {name: 'test'} + + must_redirect_to merchant_products_path(merchant.id) + Category.count.must_equal old_category_count + 1 + end + + it 'returns bad_request for bad data' do + old_category_count = Category.count + + post categories_path, params: {name: nil} + + must_respond_with :redirect + Category.count.must_equal old_category_count + end + + end +end diff --git a/test/controllers/merchants_controller_test.rb b/test/controllers/merchants_controller_test.rb new file mode 100644 index 0000000000..2a917e7399 --- /dev/null +++ b/test/controllers/merchants_controller_test.rb @@ -0,0 +1,61 @@ +require "test_helper" + +describe MerchantsController do + describe 'authenticated user' do + before do + @merchant = Merchant.first + login(@merchant) + end + describe 'index' do + it 'returns success with many mearchants' do + get merchants_path + + must_respond_with :success + end + + it 'returns success with no merchants' do + Merchant.destroy_all + + get merchants_path + + must_respond_with :success + end + end + + describe 'show' do + it 'returns success' do + merchant2 = Merchant.last + get merchant_path(merchant2.id) + + must_respond_with :success + end + end + end + + describe 'guest user' do + describe 'index' do + it 'returns success with many mearchants' do + get merchants_path + + must_respond_with :success + end + + it 'returns success with no merchants' do + Merchant.destroy_all + + get merchants_path + + must_respond_with :success + end + end + + describe 'show' do + it 'returns success' do + merchant2 = Merchant.last + get merchant_path(merchant2.id) + + must_respond_with :success + end + end + end +end diff --git a/test/controllers/orderitems_controller_test.rb b/test/controllers/orderitems_controller_test.rb new file mode 100644 index 0000000000..be13cd7b2b --- /dev/null +++ b/test/controllers/orderitems_controller_test.rb @@ -0,0 +1,178 @@ +require "test_helper" + +describe OrderitemsController do + + describe 'create' do + it 'can add a valid orderitem' do + oi_data = { + product_id: Product.first.id, + order_id: Order.first.id, + quantity: 1 + } + + old_oi_count = OrderItem.count + + OrderItem.new(oi_data).must_be :valid? + + post orderitems_path, params: {order_item: oi_data} + + must_respond_with :redirect + + OrderItem.count.must_equal old_oi_count + 1 + + end + + it "won't create an invalid orderitem" do + oi_data = { + product_id: Product.last.id, + quantity: Product.last.stock + 1 + } + + old_oi_count = OrderItem.count + + OrderItem.new(oi_data).wont_be :valid? + + post orderitems_path, params: {order_item: oi_data} + + must_respond_with :redirect + OrderItem.count.must_equal old_oi_count + end + + it 'will update a current order item quantity if adding more items of the same product to the cart' do + product1 = Product.first + orderitem_data = { + product_id: product1.id, + quantity: 1 + } + old_oi_count = OrderItem.count + + post orderitems_path, params: { order_item: orderitem_data } + OrderItem.count.must_equal old_oi_count + 1 + + orderitem = OrderItem.last + post orderitems_path, params: { order_item: orderitem_data } + + orderitem.reload + orderitem.quantity.must_equal 2 + + order = Order.find(orderitem.order_id) + product_ones = order.products.select { |product| product.id == product1.id } + + product_ones.length.must_equal 1 + end + end + + describe 'update' do + it 'updates an existing orderitem with valid data' do + orderitem = { + product_id: Product.first.id, + order_id: Order.first.id, + quantity: 1 + } + post orderitems_path, params: {order_item: orderitem} + + test_io = OrderItem.last + + test_io.assign_attributes(quantity: 2) + test_io.must_be :valid? + + patch orderitem_path(test_io.id), params: { order_item: {quantity: 2} } + + must_redirect_to viewcart_path(session[:cart_id]) + test_io.reload + test_io.quantity.must_equal 2 + + end + + it 'sends does not update a product for invalid data' do + orderitem = { + product_id: Product.first.id, + order_id: Order.first.id, + quantity: 1 + } + post orderitems_path, params: {order_item: orderitem} + + test_io = OrderItem.last + + test_io.assign_attributes(quantity: 0) + test_io.wont_be :valid? + + patch orderitem_path(test_io.id), params: { order_item: {quantity: 0} } + + must_redirect_to viewcart_path(session[:cart_id]) + test_io.reload + test_io.quantity.must_equal 1 + end + + it 'redirects to root for orderitem that dne' do + io_id = OrderItem.last.id + 1 + patch orderitem_path(io_id) + + must_respond_with :redirect + must_redirect_to root_path + end + end + + describe 'destroy' do + it 'responds with success if an orderitem is deleted' do + io_id = OrderItem.first.id + old_oi_count = OrderItem.count + + delete orderitem_path(io_id) + + must_respond_with :redirect + + OrderItem.count.must_equal old_oi_count - 1 + OrderItem.find_by(id: io_id).must_be_nil + end + + it 'redirects to root if orderitem dne' do + io_id = OrderItem.last.id + 1 + old_oi_count = OrderItem.count + + delete orderitem_path(io_id) + + must_redirect_to root_path + OrderItem.count.must_equal old_oi_count + end + end + + describe 'ship' do + it "allows you to ship an item that exists" do + order_item = OrderItem.create(order_id: Order.first.id, product_id: Product.first.id, quantity: 1, status: "pending") + order_item.status.must_equal "pending" + + patch ship_path(order_item) + + OrderItem.last.status.must_equal "shipped" + end + + it "redirects to root for an item that doesn't exist" do + order_item_id = OrderItem.last.id + 1 + order_item = OrderItem.find_by(id: order_item_id) + order_item.must_be :nil? + patch ship_path(order_item_id) + must_redirect_to root_path + end + end # ship + + describe 'cancel' do + it "allows you to cancel an item that exists" do + order_item = OrderItem.create(order_id: Order.first.id, product_id: Product.first.id, quantity: 1, status: "pending") + order_item.status.must_equal "pending" + + patch cancel_path(order_item.id) + + OrderItem.last.status.must_equal "cancelled" + end + + it "responds with not found for an item that doesn't exist" do + order_item_id = OrderItem.last.id + 1 + order_item = OrderItem.find_by(id: order_item_id) + order_item.must_be :nil? + patch cancel_path(order_item_id) + must_redirect_to root_path + end + end # cancel + +end diff --git a/test/controllers/orders_controller_test.rb b/test/controllers/orders_controller_test.rb new file mode 100644 index 0000000000..575473c47c --- /dev/null +++ b/test/controllers/orders_controller_test.rb @@ -0,0 +1,196 @@ +require "test_helper" + +describe OrdersController do + describe "logged in merchant" do + + before do + login(Merchant.first) + @orders = Merchant.first.orders + end + + describe "index" do + + it "sends a success response when there are many orders" do + @orders.count.must_be :>, 0 + get orders_path + must_respond_with :success + end + + it "sends a success response when there are no orders" do + Order.destroy_all + @orders.count.must_equal 0 + get orders_path + must_respond_with :success + end + + end # index + end # logged in merchant + + describe "show" do + + it "sends success if there is an order and status is paid" do + orderitem = {product_id: Product.first.id, quantity: Product.first.stock} + + post orderitems_path, params: {order_item: orderitem} + + order = Order.find(session[:cart_id]) + order.update_attributes( + cc_expiration: '234', + cvv: '2', + customer_name: '23', + customer_email: 'sf', + credit_card: 'asdf', + shipping_address: 'asdf', + billing_address: '234' + ) + order.status = "paid" + order.save! + get order_path(order) + must_respond_with :success + end + + it "redirects to root if the order does not exist" do + order_id = Order.last.id + 1 + get order_path(order_id) + must_redirect_to root_path + end + + it "redirects to edit_path if order status != paid" do + orderitem = {product_id: Product.first.id, quantity: Product.first.stock} + + post orderitems_path, params: {order_item: orderitem} + + order = Order.find(session[:cart_id]) + order.update_attributes( + cc_expiration: '234', + cvv: '2', + customer_name: '23', + customer_email: 'sf', + credit_card: 'asdf', + shipping_address: 'asdf', + billing_address: '234' + ) + order.status = "pending" + order.save! + + get order_path(order) + must_redirect_to edit_order_path(order.id) + + end + + end # show + + describe "create" do + + it 'adds a valid order' do + old_order_count = Order.count + + post orders_path + + must_respond_with :redirect + # must redirect to same path as before + + Order.count.must_equal old_order_count + 1 + Order.last.status.must_equal "pending" + end + + end # create + + describe "update" do + + it "incorporates complete customer information and changes status to paid" do + product = Product.first + orderitem = {product_id: product.id, quantity: product.stock} + post orderitems_path, params: {order_item: orderitem} + + order_id = Order.last.id + + order_data = { + customer_name: "Barry Allen", + customer_email: "run@nike.com", + credit_card: "1123581321345589", + cvv: "890", + cc_expiration: "09/20", + shipping_address: "200 Washington St., Central City, NJ, 23456", + billing_address: "23456" + } + patch order_path(order_id), params: {order: order_data} + + must_redirect_to order_path(order_id) + + Order.last.status.must_equal "paid" + end + + it "does not allow the transaction to go through if there are no items in the cart" do + # create an order item + orderitem = {product_id: Product.first.id, quantity: Product.first.stock} + post orderitems_path, params: {order_item: orderitem} + + # delete the order item + order_id = OrderItem.last.order.id + delete orderitem_path(OrderItem.last.id) + + # send to update the order w customer info + order_data = { + customer_name: "Barry Allen", + customer_email: "run@nike.com", + credit_card: "1123581321345589", + cvv: "890", + cc_expiration: "09/20", + shipping_address: "200 Washington St., Central City, NJ, 23456", + billing_address: "23456" + } + patch order_path(order_id), params: {order: order_data} + + must_respond_with :redirect + Order.last.status.must_equal "pending" + end + + it "does not allow the transaction to go through if the customer data is incomplete" do + # create a new order item + orderitem = {product_id: Product.first.id, quantity: Product.first.stock} + post orderitems_path, params: {order_item: orderitem} + + # update the order with missing fields + order_data = { + customer_name: "Barry Allen", + credit_card: "1123581321345589", + cvv: "890", + shipping_address: "200 Washington St., Central City, NJ, 23456", + billing_address: "23456" + } + patch order_path(session[:cart_id]), params: {order: order_data} + + must_respond_with :bad_request + Order.last.status.must_equal "pending" + # relating the cart/order to the order item + session[:cart_id].must_equal OrderItem.last.order_id + end + + end # update + + describe 'viewcart' do + + it "sends success if the order exists" do + order = Order.first + orderitem_data = { product_id: Product.first.id, quantity: Product.first.stock, order_id: order.id } + + post orderitems_path, params: {order_item: orderitem_data} + + get viewcart_path(order) + must_respond_with :success + end + + it "sends not_found if the order does not exist" do + order_id = Order.last.id + 1 + get viewcart_path(order_id) + must_respond_with :redirect + must_redirect_to root_path + end + + + + end # viewcart + + +end diff --git a/test/controllers/products_controller_test.rb b/test/controllers/products_controller_test.rb new file mode 100644 index 0000000000..75b8c45100 --- /dev/null +++ b/test/controllers/products_controller_test.rb @@ -0,0 +1,228 @@ +require "test_helper" + +describe ProductsController do + describe 'guest user' do + describe 'index' do + it 'succeeds with multiple products for a guest user' do + Product.count.must_be :>, 0 + + get products_path + must_respond_with :success + end + + it 'succeeds with no products for a guest user' do + Product.destroy_all + + Product.count.must_equal 0 + get products_path + must_respond_with :success + end + + it 'succeeds for a specific category' do + category = Category.first + + get category_path(category.name) + must_respond_with :success + end + + it 'succeeds with no categories' do + Category.destroy_all + + get products_path + must_respond_with :success + end + end + + describe 'show' do + it 'succeeds for an existing product' do + product_id = Product.first.id + + get product_path(product_id) + + must_respond_with :success + end + + it 'redirects to root path for a non-existing id' do + product_id = Product.last.id + 1 + + get product_path(product_id) + + must_redirect_to root_path + end + end + end + + describe 'authenticated user' do + let (:product_data) { + { + name: 'Product', + stock: 3, + price: 3.00, + merchant_id: Merchant.first.id + } + } + let(:merchant) { Merchant.first } + + before do + login(merchant) + end + + describe 'index' do + it 'works with no products' do + Product.where(merchant_id: merchant.id).destroy_all + + get merchant_path(merchant.id) + + must_respond_with :success + end + + it 'works with many products' do + products = Product.where(merchant_id: merchant.id) + + products.count.must_be :>, 0 + + get merchant_path(merchant.id) + + must_respond_with :success + end + end + + describe 'new' do + it 'succeeds for an authenticated user' do + get new_merchant_product_path(merchant.id) + + must_respond_with :success + end + end + + describe 'create' do + + it "creates a work with valid data" do + old_product_count = Product.count + + post merchant_products_path(merchant.id), params: { product: product_data } + + must_redirect_to merchant_manage_products_path(session[:merchant_id]) + Product.count.must_equal old_product_count + 1 + end + + it "renders bad_request and does not update the DB for bogus data for an authenticated user" do + product_data[:name] = nil + + old_product_count = Product.count + + post merchant_products_path(merchant.id), params: { product: product_data } + + must_respond_with :bad_request + Product.count.must_equal old_product_count + end + + it 'adds a default photo if none is given' do + post merchant_products_path(merchant.id), params: { product: product_data } + + must_respond_with :redirect + Product.last.photo_url.must_equal "http://www.equistaff.com/Images/noimageavailable.gif" + end + end + + describe 'edit' do + it 'succeeds for an authenticated user' do + product = merchant.products.first + get edit_merchant_product_path(merchant.id, product.id) + + must_respond_with :success + end + end + + describe 'update' do + let (:old_product_count) { Product.count } + + it 'succeeds with good data' do + product = merchant.products.first + old_product_stock = product.stock + + product_data = { + name: product.name, + stock: product.stock - 1, + price: product.price, + merchant_id: product.merchant_id + } + + patch merchant_product_path(merchant.id, product.id), params: { product: product_data } + product.reload + + must_redirect_to merchant_manage_products_path(session[:merchant_id]) + Product.count.must_equal old_product_count + + product.stock.must_equal (old_product_stock - 1) + end + + it 'returns bad_request for bad data' do + product = merchant.products.first + old_product_stock = product.stock + + product_data = { + name: product.name, + stock: nil, + price: product.price, + merchant_id: product.merchant_id + } + + patch merchant_product_path(merchant.id, product.id), params: { product: product_data } + product.reload + + must_respond_with :bad_request + Product.count.must_equal old_product_count + + product.stock.must_equal old_product_stock + end + + it 'adds a default photo if none is given' do + product = merchant.products.first + product_data = { + product_id: product.id, + photo_url: nil, + merchant_id: product.merchant_id, + stock: 2, + price: 2.99 + } + + patch merchant_product_path(merchant.id, product.id), params: { product: product_data } + + must_respond_with :redirect + product.reload + product.photo_url.must_equal "http://www.equistaff.com/Images/noimageavailable.gif" + end + end + + describe 'destroy' do + let (:product) { merchant.products.first } + + it 'retires an existing product' do + delete merchant_product_path(merchant.id, product.id) + + product.reload + product.retired.must_equal true + must_redirect_to merchant_manage_products_path(session[:merchant_id]) + end + + it 'must redirect to root for a non-existing product' do + non_existing_id = Product.last.id + 1 + delete merchant_product_path(merchant.id, non_existing_id) + + must_redirect_to root_path + end + + it 'will not retire a product that is not associated with the merchant' do + product = Merchant.where.not(id: merchant.id).first.products.first + + delete merchant_product_path(merchant.id, product.id) + + product.reload + product.retired.must_equal false + must_redirect_to merchant_manage_products_path(session[:merchant_id]) + end + end + + end +end diff --git a/test/controllers/reviews_controller_test.rb b/test/controllers/reviews_controller_test.rb new file mode 100644 index 0000000000..e48347f596 --- /dev/null +++ b/test/controllers/reviews_controller_test.rb @@ -0,0 +1,53 @@ +require "test_helper" + +describe ReviewsController do + describe 'create' do + before do + @old_review_count = Review.count + end + + it 'is successful with valid data' do + review_data = { + rating: 5, + product_id: Product.first.id + } + + post reviews_path, params: {review: review_data} + + must_respond_with :redirect + Review.count.must_equal @old_review_count + 1 + end + + it 'is unsuccessful with invalid data' do + review_data = { + rating: 7, + product_id: Product.first.id + } + + post reviews_path, params: {review: review_data} + + must_respond_with :redirect + Review.count.must_equal @old_review_count + end + + it "wont' let you review if it's your own product" do + merchant = Merchant.first + merchant.products.length.must_be :>, 0 + login(merchant) + + product = merchant.products.first + + review_data = { + rating: 3, + product_id: product.id + } + post reviews_path, params: {review: review_data} + + must_respond_with :redirect + + Review.count.must_equal @old_review_count + + end + + end +end diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb new file mode 100644 index 0000000000..12ab1f1bbb --- /dev/null +++ b/test/controllers/sessions_controller_test.rb @@ -0,0 +1,34 @@ +require "test_helper" + +describe SessionsController do + describe 'auth_callback' do + it 'logs in an existing user' do + merchant = Merchant.first + + login(merchant) + + must_redirect_to orders_path + session[:merchant_id].must_equal merchant.id + end + + it 'creates a new merchant if one does not already exist' do + merchant_data = { + username: 'Test User', + email: 'test@email.com', + uid: '9999999999999', + provider: 'google_oauth2' + } + + merchant = Merchant.new(merchant_data) + merchant.valid?.must_equal true + + old_merchant_count = Merchant.count + + login(merchant) + must_redirect_to orders_path + + Merchant.count.must_equal old_merchant_count + 1 + session[:merchant_id].must_equal Merchant.last.id + end + end +end diff --git a/test/fixtures/.keep b/test/fixtures/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixtures/categories.yml b/test/fixtures/categories.yml new file mode 100644 index 0000000000..814a8eaf3d --- /dev/null +++ b/test/fixtures/categories.yml @@ -0,0 +1,12 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# + +memorabilia: + name: memorabilia + +awesome: + name: awesome diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixtures/merchants.yml b/test/fixtures/merchants.yml new file mode 100644 index 0000000000..3090fa08bd --- /dev/null +++ b/test/fixtures/merchants.yml @@ -0,0 +1,13 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +merchant_one: + username: merchant_one + email: merchant_one@example.com + uid: 1 + provider: :google_oauth2 + +batambaman: + username: batambaman + email: thebatambaman@gmail.com + uid: 12345 + provider: google_oauth2 diff --git a/test/fixtures/order_items.yml b/test/fixtures/order_items.yml new file mode 100644 index 0000000000..3a6fc88df6 --- /dev/null +++ b/test/fixtures/order_items.yml @@ -0,0 +1,20 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + quantity: 1 + product: batarang + order: one + status: pending + +two: + quantity: 2 + product: lasso + order: two + status: pending + + +three: + quantity: 1 + product: utility_belt + order: one + status: pending diff --git a/test/fixtures/orders.yml b/test/fixtures/orders.yml new file mode 100644 index 0000000000..51a016ce8f --- /dev/null +++ b/test/fixtures/orders.yml @@ -0,0 +1,21 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + customer_name: Peter Parker + customer_email: iluvheros@fan.net + credit_card: 1234567891011121 + cvv: 987 + cc_expiration: 09/35 + status: paid + shipping_address: 1234 Lovelace Ave Seattle WA 98103 + billing_address: 98103 + +two: + customer_name: Jane Foster + customer_email: desertphenom@smartypants.com + credit_card: 9876543210111213 + cvv: 786 + cc_expiration: 03/23 + status: paid + shipping_address: 109 Desert Road Nowhere AZ 29843 + billing_address: 29843 diff --git a/test/fixtures/products.yml b/test/fixtures/products.yml new file mode 100644 index 0000000000..457b2f1d12 --- /dev/null +++ b/test/fixtures/products.yml @@ -0,0 +1,37 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# + +batarang: + name: Batarang + stock: 10 + price: 40.00 + description: The only weapon you'll ever need. + merchant: merchant_one + # categories: + # - memorabilia + # - awesome + photo_url: http://www.equistaff.com/Images/noimageavailable.gif + +lasso: + name: Lasso of Truth + stock: 10 + price: 234.99 + description: Lasso + merchant: merchant_one + # categories: + # - memorabilia + # - awesome + photo_url: http://www.equistaff.com/Images/noimageavailable.gif + + +utility_belt: + name: Utility Belt + stock: 20 + price: 59.99 + description: A fanny pack, but better + merchant: batambaman + photo_url: http://www.equistaff.com/Images/noimageavailable.gif diff --git a/test/fixtures/reviews.yml b/test/fixtures/reviews.yml new file mode 100644 index 0000000000..194299143a --- /dev/null +++ b/test/fixtures/reviews.yml @@ -0,0 +1,9 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + rating: 1 + product: batarang + +two: + rating: 1 + product: lasso diff --git a/test/helpers/.keep b/test/helpers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/integration/.keep b/test/integration/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/mailers/.keep b/test/mailers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/models/.keep b/test/models/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/models/category_test.rb b/test/models/category_test.rb new file mode 100644 index 0000000000..68b8f31e67 --- /dev/null +++ b/test/models/category_test.rb @@ -0,0 +1,80 @@ +require "test_helper" + +describe Category do + describe 'relations' do + it 'connects category and products' do + product = Product.first + + category = Category.new(name: 'Accessories') + category.products.push(product) + + category.valid?.must_equal true + category.products.must_include product + end + end + + describe 'validations' do + it 'cannot be created without a name' do + old_category_count = Category.count + + category = Category.new(name: nil) + + category.valid?.must_equal false + Category.count.must_equal old_category_count + category.errors.must_include :name + end + + it 'can only be created with a unique name' do + old_category_count = Category.count + + category = Category.new(name: Category.first.name) + + category.valid?.must_equal false + Category.count.must_equal old_category_count + category.errors.must_include :name + end + end + + describe 'logic' do + + describe 'categories_with_works' do + it 'only returns categories that have products associated with it' do + categories_with_works = Category.categories_with_works + + categories_with_works.each do |category| + category.products.must_be :>, 0 + end + end + + it 'returns an empty array if there are no categories with products' do + Category.destroy_all + + categories_with_works = Category.categories_with_works + + categories_with_works.must_be_empty + + end + end + + describe 'fix_category' do + it 'downcases and pluralizes the category' do + category1 = 'weapon' + category2 = 'mouse' + + result1 = Category.fix_category(category1) + result2 = Category.fix_category(category2) + + result1.must_equal 'weapons' + result2.must_equal 'mice' + end + + it 'works for an already plural category' do + category1 = 'Toys' + + result1 = Category.fix_category(category1) + + result1.must_equal 'toys' + end + end + end +end diff --git a/test/models/merchant_test.rb b/test/models/merchant_test.rb new file mode 100644 index 0000000000..7b18378e05 --- /dev/null +++ b/test/models/merchant_test.rb @@ -0,0 +1,219 @@ +require "test_helper" + +describe Merchant do + describe "validations" do + + it "must have a unique username" do + # arrange + merchant = Merchant.first + old_merchant_count = Merchant.count + + new_merchant = Merchant.new(username: merchant.username, email: "address@example.com") + + # assumptions + new_merchant.wont_be :valid? + + # act + new_merchant.save + + # assert + Merchant.count.must_equal old_merchant_count + end + + it "must have a unique email" do + # arrange + merchant = Merchant.first + old_merchant_count = Merchant.count + + new_merchant = Merchant.new(username: "example_username", email: merchant.email) + + # assumptions + new_merchant.wont_be :valid? + + # act + new_merchant.save + + # assert + Merchant.count.must_equal old_merchant_count + end + + end # validations + + describe "relations" do + + it "connects product and product_id" do + merchant = Merchant.first + + product = Product.new(name: "Green Lantern Ring", stock: 1, price: 57.00) + + # act + product.merchant = merchant + product.must_be :valid? + + product.save + + merchant.products.must_include product + end + + end # relations + + describe "business logic" do + + describe "#merchant_order_items" do + + before do + @merchant = Merchant.first + @result = @merchant.merchant_order_items("all") + end + + it "must return a hash" do + @result.must_be_kind_of Hash + end + + it "must have keys corresponding to order id" do + orders = @merchant.orders + orders_ids = orders.map { |order| order.id } + @result.keys.must_equal orders_ids + end + + it "must have values of type order_item only for products that belong to the merchant" do + merchant = merchants(:merchant_one) + + result = merchant.merchant_order_items("all").values + result.each do |order_items| + order_items.each do |order_item| + order_item.product.merchant_id.must_equal merchant.id + end + end + + end + + it "must return an empty hash for a merchant with no orders" do + @merchant = Merchant.create(username: "Barry Allen", email: "b@rryfast.com") + @result = @merchant.merchant_order_items("all") + @result.must_be :empty? + end + + end # merchant_order_items + + describe "#total_revenue_by" do + before do + @merchant = Merchant.first + @order = Order.create(status: "completed") + product = Product.create(name: "eye mask", merchant_id: @merchant.id, stock: "20", price: 22.50) + @order_item = OrderItem.create(product_id: product.id, order_id: @order.id, quantity: 1) + end + + it "returns the total revenue for all of a merchant's orders" do + merchant_orders = @merchant.orders + all_orders_total = 0.00 + merchant_orders.each do |order| + order.order_items.each do |order_item| + if order_item.product.merchant_id == @merchant.id + all_orders_total += order_item.subtotal + end + end + end + + result = @merchant.total_revenue_by("all") + result.must_equal all_orders_total + end + + it "returns the total revenue for all of a merchant's orders with status paid" do + merchant_orders = @merchant.orders + paid_orders_total = 0.00 + merchant_orders.each do |order| + order.order_items.each do |order_item| + if order_item.product.merchant_id == @merchant.id && Order.find(order.id).status == "paid" + paid_orders_total += order_item.subtotal + end + end + end + + result = @merchant.total_revenue_by("paid") + result.must_equal paid_orders_total + + end + + it "returns the total revenue for all of a merchant's orders with status completed" do + merchant_orders = @merchant.orders + completed_orders_total = 0.00 + merchant_orders.each do |order| + order.order_items.each do |order_item| + # binding.pry + if order_item.product.merchant_id == @merchant.id && Order.find(order.id).status == "completed" + completed_orders_total += order_item.subtotal + end + end + end + + result = @merchant.total_revenue_by("completed") + result.must_equal completed_orders_total + + end + + + it "returns 0.0 for the total revenue of a merchant with zero orders" do + merchant = Merchant.new + result = merchant.total_revenue_by("all") + result.must_equal 0.0 + end + + end # merchant total_revenue_by + + describe "#count_orders_by" do + before do + @merchant = Merchant.first + end + + it "returns the total number of orders for a merchant" do + num_orders = @merchant.orders.count + result = @merchant.count_orders_by("all") + result.must_equal num_orders + end + + it "returns the total number of pending orders for a merchant" do + pending_orders = @merchant.orders.select do |order| + order.status == "pending" + end + result = @merchant.count_orders_by("pending") + result.must_equal pending_orders.count + end + + it "returns the total number of paid orders for a merchant" do + paid_orders = @merchant.orders.select do |order| + order.status == "paid" + end + result = @merchant.count_orders_by("paid") + result.must_equal paid_orders.count + end + + it "returns the total number of completed orders for a merchant" do + completed_orders = @merchant.orders.select do |order| + order.status == "completed" + end + result = @merchant.count_orders_by("completed") + result.must_equal completed_orders.count + end + + it "returns the total number of cancelled orders for a merchant" do + cancelled_orders = @merchant.orders.select do |order| + order.status == "cancelled" + end + result = @merchant.count_orders_by("cancelled") + result.must_equal cancelled_orders.count + end + + it "returns 0 for a merchant with zero orders" do + merchant = Merchant.new + num_orders = merchant.orders.count + num_orders.must_equal 0 + result = merchant.count_orders_by("all") + result.must_equal num_orders + end + + end # num_orders_by + + end # business logic + +end diff --git a/test/models/order_item_test.rb b/test/models/order_item_test.rb new file mode 100644 index 0000000000..8495048f84 --- /dev/null +++ b/test/models/order_item_test.rb @@ -0,0 +1,165 @@ +require "test_helper" +require 'pry' + +describe OrderItem do + + describe 'validations' do + before do + @product = Product.first + @order = Order.first + @order_item = OrderItem.new(product_id: @product.id, order_id: @order.id, quantity: 1, status: "pending") + end + + it 'is valid with a index_reviews_on_product_id' do + + @order_item.valid?.must_equal true + + end + + it 'is invalid without a product_id' do + + order_item = OrderItem.new(order_id: @order_id, quantity: 1) + + order_item.wont_be :valid? + + end + + it 'is valid with an order_id' do + + @order_item.valid?.must_equal true + + end + + it 'is invalid without an order_id' do + @order_item.valid?.must_equal true + + @order_item.order_id = nil + + @order_item.valid?.must_equal false + end + + it 'is valid with a quantity greater than zero' do + @order_item.quantity = 3 + @order_item.quantity.must_equal 3 + @order_item.valid?.must_equal true + end + + it 'is invalid when the quantity is not an integer' do + @order_item.valid?.must_equal true + + @order_item.quantity = "potatoes" + + @order_item.valid?.must_equal false + + end + + it 'is invalid when the quantity is zero' do + @order_item.valid?.must_equal true + + @order_item.quantity = 0 + + @order_item.valid?.must_equal false + end + + it 'is invalid with a quantity greater than the product stock' do + stock = @product.stock + @order_item.quantity = stock + 1 + @order_item.wont_be :valid? + end + + it 'is invalid without a status' do + @order_item.must_be :valid? + @order_item.status = nil + @order_item.wont_be :valid? + end + + end # validations + + describe "relations" do + before do + @order_item = OrderItem.new + end + + + it "connects product and product id" do + product = Product.first + @order_item.product = product + @order_item.product_id.must_equal product.id + end + + it "connects order and order id" do + order = Order.first + @order_item.order = order + @order_item.order_id.must_equal order.id + end + + end # relations + + describe "business logic" do + + describe "subtotal" do + + it "returns the price of the product times the quantity of the order item" do + product = Product.first + quantity = 2 + order = Order.create + order_item = OrderItem.new(quantity: quantity, product_id: product.id, order_id: order.id, status: "pending") + order_item.must_be :valid? + subtotal = product.price * quantity + result = order_item.subtotal + result.must_equal subtotal + end + + end # subtotal + + describe 'orderitem aggregation' do + it 'will update an order item quantity' do + product1 = Product.first + order = Order.create! + + orderitem1 = OrderItem.create!(product_id: product1.id, quantity: 1, order_id: order.id) + + orderitem2 = OrderItem.new(product_id: orderitem1.product_id, quantity: 1, order_id: order.id) + + # return orderitem2 + orderitem_test = OrderItem.aggregate_orderitem(orderitem2, orderitem1) + + orderitem_test.save + + orderitem1.quantity.must_equal 2 + orderitem_test.id.must_equal orderitem1.id + + product_ones = order.products.select { |product| product.id == product1.id } + + product_ones.length.must_equal 1 + end + end + + describe 'existing_oi?' do + it "will return true if order item's product is already associated with the order" do + product1 = Product.first + order = Order.create! + + orderitem1 = OrderItem.create!(product_id: product1.id, quantity: 1, order_id: order.id) + + orderitem2 = OrderItem.new(product_id: product1.id, quantity: 1, order_id: order.id) + result = OrderItem.existing_oi?(orderitem2) + + result.must_equal orderitem1 + end + + it 'will return nil if that product is not associated with the order' do + product1 = Product.first + order = Order.create! + + OrderItem.create!(product_id: product1.id, quantity: 1, order_id: order.id) + + orderitem2 = OrderItem.new(product_id: Product.last.id, quantity: 1, order_id: order.id) + + result = OrderItem.existing_oi?(orderitem2) + result.must_equal nil + end + end + end # business logic + +end diff --git a/test/models/order_test.rb b/test/models/order_test.rb new file mode 100644 index 0000000000..897fae30a1 --- /dev/null +++ b/test/models/order_test.rb @@ -0,0 +1,140 @@ +require "test_helper" + +describe Order do + describe 'validations' do + + describe 'validations before changing status from pending to paid' do + it 'is valid with one order item' do + order = Order.first + order.order_items.count.must_equal 1 + order.valid?.must_equal true + end + + it 'is can be associated with multiple order items' do + order = Order.create! + OrderItem.create!(quantity: 1, product: Product.last, order: order, status: 'pending') + OrderItem.create!(quantity: 1, product: Product.first, order: order, status: 'pending') + + order.order_items.count.must_be :>, 1 + OrderItem.last.order_id.must_equal order.id + item1 = OrderItem.find(OrderItem.last.id - 1) + item1.order_id.must_equal order.id + end + + it 'is valid with 0 order items' do + order = Order.new + order.order_items.count.must_equal 0 + order.valid?.must_equal true + end + + end # validations before changing status + + end # validations + + describe 'business logic' do + describe 'order_total' do + before do + @order = Order.new + @order.order_items << OrderItem.first + @order.valid?.must_equal true + @order.save + end + it 'returns the correct total when there is one orderitem' do + @order.order_items.length.must_equal 1 + total = @order.order_total + + total.must_equal OrderItem.first.product.price + + end + it 'returns the correct total when there are multiple orderitems' do + @order.order_items << OrderItem.last + + @order.order_items.length.must_equal 2 + + total = @order.order_total + + total.must_equal OrderItem.first.product.price + OrderItem.last.product.price + end + end # order_total + + describe 'order_status' do + + describe 'determines the status of an order based on the status of its order items' do + before do + @order = Order.create + @order_item_one = OrderItem.create(product_id: Product.first.id, order_id: @order.id, quantity: 1, status: "pending") + @order_item_two = OrderItem.create(product_id: Product.last.id, order_id: @order.id, quantity: 1, status: "pending") + @order.assign_attributes(customer_name: "Bob Belcher", customer_email: "burgers_rule@gmail.com", credit_card: "123456789abcdef0", cvv: "789", cc_expiration: "11/18", shipping_address: "345 Main St., Seattle, WA 54321", billing_address: "54321", status: "paid") + @order.save + @order.status.must_equal "paid" + + @order_item_one.update(status: "shipped") + @order.status.must_equal "paid" + @order_item_two.update(status: "cancelled") + + end + + it "has a paid status until none of the order items have a pending status" do + @order.reload + @order.order_status + @order.status.must_equal "completed" + end + + it "has a cancelled status only if all order_items are cancelled" do + @order.reload + @order.order_status + @order.status.must_equal "completed" + + @order_item_one.update(status: "cancelled") + + @order.reload + + @order.order_status + @order.status.must_equal "cancelled" + + end + + end + + end # order_status + + + describe 'reduce_stock' do + it 'reduces the stock for all items by expected amount' do + order = Order.new + order.valid? + order.save + item1 = OrderItem.first + item1.quantity = 5 + item1.save + order.order_items << item1 + item2 = OrderItem.last + item2.quantity = 8 + item2.save + order.order_items << item2 + + order.order_items.length.must_equal 2 + + product1 = item1.product + product1.stock = 12 + + product2 = item2.product + product2.stock = 9 + + order.reduce_stock + + order.reload + + product1.reload + product2.reload + + product1.stock.must_equal 7 + product2.stock.must_equal 1 + + end + end + + + end # business logic + +end # Order diff --git a/test/models/product_test.rb b/test/models/product_test.rb new file mode 100644 index 0000000000..146c88e4e2 --- /dev/null +++ b/test/models/product_test.rb @@ -0,0 +1,190 @@ +require "test_helper" + +describe Product do + describe 'relations' do + let (:product) { Product.new(name: 'Action Figure', stock: 3, price: 2.99, merchant: merchants(:merchant_one)) } + let (:merchant) { Merchant.first } + + it 'has a list of categories' do + product = products(:batarang) + + product.must_respond_to :categories + + product.categories.each do |category| + category.must_be_kind_of Category + end + end + + it 'connects categories and products' do + category = Category.first + + product.categories.push(category) + + product.valid?.must_equal true + + product.categories.must_include category + end + + + it 'connects products and merchants' do + product = Product.new(name: 'Green Hair Dye', stock: 24, price: 8.99, merchant_id: merchant.id) + + product.merchant_id.must_equal merchant.id + end + + it 'connects products and order_items' do + order_item = order_items(:one) + + product = Product.create!(name: 'Green Hair Dye', stock: 24, price: 8.99, merchant_id: merchant.id) + + product.order_items << order_item + product.valid?.must_equal true + product.order_item_ids.must_include order_item.id + end + + it 'connects products and orders' do + product1 = Product.create!(name: 'Green Hair Dye', stock: 24, price: 8.99, merchant_id: merchant.id) + + order = Order.create! + OrderItem.create!(product_id: product1.id, order_id: order.id, quantity: 2, status: 'pending') + + product1.valid?.must_equal true + product1.order_ids.must_include order.id + end + end + + describe 'validations' do + let (:merchant) { Merchant.first } + let (:categories) { [Category.first] } + + it 'cannot be created without a name' do + product_data = { + stock: 1, + price: 1.99, + merchant_id: merchant.id, + categories: categories + } + + product = Product.new(product_data) + + product.valid?.must_equal false + product.errors.messages.must_include :name + end + + it 'must have a unique name' do + product1_name = Product.first.name + + product2_data = { + name: product1_name, + stock: 1, + price: 1.99, + merchant_id: merchant.id, + categories: categories + } + + product = Product.new(product2_data) + + product.valid?.must_equal false + product.errors.messages.must_include :name + end + + it 'cannot be created without a price' do + product_data = { + name: 'product', + stock: 1, + merchant_id: merchant.id, + categories: categories + } + + product = Product.new(product_data) + + product.valid?.must_equal false + product.errors.messages.must_include :price + end + + it 'cannot be created with a price below 0' do + product_data = { + name: 'product', + stock: 1, + price: -2.4, + merchant_id: merchant.id, + categories: categories + } + + product = Product.new(product_data) + product.valid?.must_equal false + product.errors.messages.must_include :price + end + + it 'cannot be created without a stock' do + product_data = { + name: 'product', + price: 2.99, + stock: nil, + merchant_id: merchant.id, + categories: categories + } + + product = Product.new(product_data) + + product.valid?.must_equal false + product.errors.messages.must_include :stock + end + + it 'cannot be created without a merchant ID' do + product_data = { + name: 'product', + stock: 1, + price: 2.4, + categories: categories + } + + product = Product.new(product_data) + product.valid?.must_equal false + product.errors.messages.must_include :merchant + end + + it 'can be created with appropriate data' do + product_data = { + name: 'product', + stock: 1, + price: 2.4, + merchant_id: merchant.id, + categories: categories + } + old_product_count = Product.count + + product = Product.create(product_data) + product.valid?.must_equal true + Product.count.must_equal old_product_count + 1 + end + end + + describe 'scopes' do + describe 'by_category' do + it 'returns only the products of a specific category' do + category = Category.first + + products = Product.by_category(category.name) + + products.each do |product| + product.categories.must_include category + end + end + end + end + + describe 'stock_decrement' do + it 'decreases product stock by the amount ordered' do + product = Product.first + product.stock = 5 + product.save + + product.stock_decrement(3) + product.reload + + product.stock.must_equal 2 + end + end + +end diff --git a/test/models/review_test.rb b/test/models/review_test.rb new file mode 100644 index 0000000000..bbd22e4052 --- /dev/null +++ b/test/models/review_test.rb @@ -0,0 +1,60 @@ +require "test_helper" + +describe Review do + describe "validations" do + before do + @old_review_count = Review.count + @review = Review.new() + @review.product = Product.first + end + + it "must have a rating" do + @review.wont_be :valid? + @review.save + Review.count.must_equal @old_review_count + end + + it "must have a rating that's an integer" do + @review.rating = "a" + @review.wont_be :valid? + @review.save + Review.count.must_equal @old_review_count + end + + it "must have a rating that is greater than 0" do + @review.rating = 0 + @review.wont_be :valid? + @review.save + Review.count.must_equal @old_review_count + end + + it "must have a rating that is less than 6" do + @review.rating = 6 + @review.wont_be :valid? + @review.save + Review.count.must_equal @old_review_count + end + + it "a valid rating can be created" do + @review.rating = 3 + @review.must_be :valid? + @review.save + Review.count.must_equal @old_review_count + 1 + end + + end # validations + + describe "relations" do + + it "connects product and product id" do + review = Review.new(rating: 3) + review.product = Product.first + + review.must_be :valid? + review.save + + review.product_id.must_equal Product.first.id + end + + end # relations +end diff --git a/test/system/.keep b/test/system/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000000..2c394b186a --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,61 @@ +require 'simplecov' +SimpleCov.start 'rails' do + add_filter '/bin/' + add_filter '/db/' + add_filter '/spec/' # for rspec + add_filter '/test/' # for minitest + add_filter '/app/channels' + add_filter '/app/jobs' + add_filter '/app/mailer' +end + +ENV["RAILS_ENV"] = "test" +require File.expand_path("../../config/environment", __FILE__) +require "rails/test_help" +require "minitest/rails" +require "minitest/reporters" # for Colorized output + +# For colorful output! +Minitest::Reporters.use!( + Minitest::Reporters::SpecReporter.new, + ENV, + Minitest.backtrace_filter +) + + +# To add Capybara feature tests add `gem "minitest-rails-capybara"` +# to the test group in the Gemfile and uncomment the following: +# require "minitest/rails/capybara" + +# Uncomment for awesome colorful output +# require "minitest/pride" + +class ActiveSupport::TestCase + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + # Add more helper methods to be used by all tests here... + + def setup + # Once you have enabled test mode, all requests + # to OmniAuth will be short circuited to use the mock authentication hash. + # A request to /auth/provider will redirect immediately to /auth/provider/callback. + OmniAuth.config.test_mode = true + end + + def mock_auth_hash(user) + return { + provider: user.provider, + uid: user.uid, + info: { + email: user.email, + name: user.username + } + } + end + + def login(user) + OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new(mock_auth_hash(user)) + get auth_callback_path(:google_oauth2) + + end +end diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 0000000000..e69de29bb2