############################### #### CREATING THE STORE APP ### ###############################
#### INITIAL SETUP ### # (1) Create App
$ rails _3.2.6_ new depot
# (2) Create Scaffold for Products Controller/Product Model
$ rails generate scaffold Product title:string description:text image_url:string price:decimal
# (3) To refine the database model for Product, go to the migration file (edit this stuff with activerecord syntax) # (4) Apply changes to model
$ rake db:migrate # (the products table is added to the database defined by the development section of the database.yml file)
# (5) To test:
$ rake test
# (6) Import seed data to work with
# go to seeds.rb in db # enter text data script # To populate your products table with test data, run the following command: $ rake db:seed
# (7) Style the data!
# go to stylesheet generated with products controller (products.css.scss in the directory app/assets/stylesheets.) # add css # define the products class used by this stylesheet (the application.html.erb file) # edit the index.html.erb file in the views >products # to show alternating colors for rows: <tr class="<%= cycle('list_line_odd', 'list_line_even') %>"> # confirm delete pop-up: # <td class="list_actions"> # <%= link_to 'Show', product %><br/> # <%= link_to 'Edit', edit_product_path(product) %><br/> # <%= link_to 'Destroy', product, method: :delete, # data: { confirm: 'Are you sure?' } %> # </td>
# (8) Config for Github
$ depot> git config --global --add user.name "SamanthaRadocchia" $ depot> git config --global --add user.email samantha.radocchia@gmail.com # You can verify the configuration with the following command: $ depot> git config --global --list
#### VALIDATION #### # (1) Add validation to model layer (in app/models/product.rb)
# Go to model for Class Product (in app/models/product.rb) # VERIFY THAT ALL TEXT FIELDS CONTAIN SOMETHING - use presence validates :title, :description, :image_url, presence: true (add this to the class Product) # VERIFY THAT PRICE IS VALID NUMBER - use numericality and :greater_than_or_equal_to option a value of 0.01 validates :price, numericality: {greater_than_or_equal_to: 0.01} # VERIFY THAT EACH PRODUCT HAS UNIQUE TITLE - use uniqueness validates :title, uniqueness: true # VERIFY THAT URL ENTERED FOR THE IMAGE IS VALID - use format validates :image_url, allow_blank: true, format: { with: %r{\.(gif|jpg|png)\Z}i, message: 'must be a URL for GIF, JPG or PNG image.' }
# (2) Set up Unit tests in test > functional > products_controller_test.rb and test >unit > product_test.rb
#### CATALOG DISPLAY #### # (1) Create consumer-facing controller
# (The Products controller is used by the seller to administer the Depot application.) # (The Store controller is used by the paying customer to view products.) # Create the store controller with the following command: $ rails generate controller Store index
# (2) Make Store the root URL for the site (so the customer arrives and sees the catalog)
# go to config/routes.rb and add the following: root to: 'store#index', as: 'store' # delete the public/index.html by entering: $ rm public/index.html
# (3) Display a Simple List of Products in the database
# go to store_controller.rb and add the following to index: class StoreController < ApplicationController def index @products = Product.order(:title) end end
# (4) Write the Store view template (found in index.html.erb in app/views/store) # (5) Add a stylesheet (found in app/assets/stylesheets/store.css.scss) # (6) Add a PAGE LAYOUT
# application.html.erb --> this file is the layout used for all views for all controllers that don’t otherwise provide a layout, we can change the look and feel of the entire site by editing just one file # Update application.html.erb (DEFAUL PAGE LAYOUT) to include a banner and a sidebar # About YIELD (When we invoke yield, Rails automatically substitutes in the page-specific content) # to make this work, rename application.css to application.css.scss and add default css
# (7) Format the Price using the helper method, number_to_currency()
number_to_currency(product.price) # Go to app/views/store/index.html.erb # <span class="price"><%= number_to_currency(product.price) %></span> # this will add a dollar sign to the price
#### CART CREATION #### # Our application will need to keep track of all the items added to the cart by the buyer. # To do that, we’ll keep a cart in the database and store its unique identifier, cart.id, in the session. # Every time a request comes in, we can recover the identity from the session and use it to find the cart in the database. # (1) Create a Cart Controller
$ rails generate scaffold cart
# (2) Remember to run rake:db migrate
$ rake db:migrate
# (3) Store the id of the cart in the session by indexing it with the symbol :cart_id
# Go to app/controllers/application_controller.rb and do this: class ApplicationController < ActionController::Base protect_from_forgery private def current_cart Cart.find(session[:cart_id]) rescue ActiveRecord::RecordNotFound cart = Cart.create session[:cart_id] = cart.id cart end end
# (4) Generate Models and populate the migrations to create the corresponding tables such that a CART CONTAINS A SET OF PRODUCTS
# To create tables for product/cart run the following command $ rails generate scaffold line_item product_id:integer cart_id:integer # Migrate $ rake db: migrate # The database now has a place to store the relationships between line items, carts, and products.
# (5) Specify that the cart can have many items
# Go to the models > cart.rb and add the following: has_many :line_items, dependent: :destroy # in place of the attr accessor
# (6) Create a reverse link from line_items.rb to cart.rb such that it belongs to a cart and a product
# Go to models > line_items.rb and add the following to class LineItem: belongs_to :product belongs_to :cart
# (7) Edit the model product.rb to include:
has_many :line_items before_destroy :ensure_not_referenced_by_any_line_item
# (8) Add an “Add to Cart” button to each product
# To do this, you want to use the create action provided by the scaffold generator to CREATE a LINEITEM # Essentially you are creating a new object, LineItem, when you add to the cart # To do so, go here: app/controllers/line_items_controller.rb # We will use the button_to() method button_to() # Add the following to app/views/store/index.html.erb # <%= button_to 'Add to Cart', line_items_path(product_id: product) %>
# (9) Format the “Add to Cart” button such that it is displayed next to price, not below it (make the button inline)
# app/assets/stylesheets/store.css.scss
# (10) modify the create() method in the line items controller to expect a product id as a form parameter.
# app/controllers/line_items_controller.rb def create @cart = current_cart product = Product.find(params[:product_id]) @line_item = @cart.line_items.build @line_item.product = product respond_to do |format| if @line_item.save format.html { redirect_to @line_item.cart, notice: 'Line item was successfully created.' } format.json { render json: @line_item, status: :created, location: @line_item } else format.html { render action: "new" } format.json { render json: @line_item.errors, status: :unprocessable_entity } end end end
#### CREATE A SMARTER CART #### # (1) Associating a count with each product in the cart
# Add a new column, "quantity" to the table line_items (# add_XXX_to_TABLE) $ rails generate migration add_quantity_to_line_items quantity:integer # Add default value for this column in the migration file add_column :line_items, :quantity, :integer, default: 1 # Migrate $ rake db:migrate
# (2) Create method to check whether our list of items already includes the product we’re adding if it does, it bumps the quantity, and if it doesn’t, it builds a new LineItem:
# Go to app/models/cart.rb # Create an add_product method for class Cart def add_product(product_id) # Active Record Method find_by_product_id() (start with find_by and end by column_name) current_item = line_items.find_by_product_id(product_id) if current_item current_item.quantity += 1 else current_item = line_items.build(product_id: product_id) end current_item end # Also need to edit line item controller to make use of this method # Go to app/controllers/line_items_controller.rb and add the following: def create @cart = current_cart product = Product.find(params[:product_id]) @line_item = @cart.add_product(product.id) @line_item.product = product respond_to do |format| if @line_item.save format.html { redirect_to @line_item.cart, notice: 'Line item was successfully created.' } format.json { render json: @line_item, status: :created, location: @line_item } else format.html { render action: "new" } format.json { render json: @line_item.errors, status: :unprocessable_entity } end end end
# (3) Update Cart View to display products added to it
# go here: app/views/carts/show.html.erb # add the following erb: <% if notice %> <p id="notice"><%= notice %></p> <% end %> <h2>Your Pragmatic Cart</h2> <ul> <% @cart.line_items.each do |item| %> <li><%= item.product.title %></li> <% end %> </ul>
# (4) Combine items in cart
# run a migration $ rails generate migration combine_items_in_cart # go to depot_g/db/migrate/20110711000005_combine_items_in_cart.rb and put crazy code in there class CombineItemsInCart < ActiveRecord::Migration def up # replace multiple items for a single product in a cart with a single item Cart.all.each do |cart| # count the number of each product in the cart sums = cart.line_items.group(:product_id).sum(:quantity) sums.each do |product_id, quantity| if quantity > 1 # remove individual items cart.line_items.where(product_id: product_id).delete_all # replace with a single item item = cart.line_items.build(product_id: product_id) item.quantity = quantity item.save! end end end end def down # split items with quantity>1 into multiple items LineItem.where("quantity>1").each do |line_item| # add individual items line_item.quantity.times do LineItem.create cart_id: line_item.cart_id, product_id: line_item.product_id end # remove original item line_item.destroy end end end # run rake db:migrate $ rake db:migrate # edit app/views/carts/show.html.erb to show the quantity <li><%= item.quantity %> × <%= item.product.title %></li>
# (5) To empty cart items
# First add a button the cart view (app/views/carts/show.html.erb) # Add this code # <%= button_to 'Empty cart', @cart, method: :delete, # data: { confirm: 'Are you sure?' } %> # Update Controller's destroy method (app/controllers/carts_controller.rb) # Add this code def destroy @cart = current_cart @cart.destroy session[:cart_id] = nil respond_to do |format| format.html { redirect_to store_url, notice: 'Your cart is currently empty' } format.json { head :no_content } end end
# (6) Tidy up the cart CSS (instead of li use a table) and CALCULATE TOTAL
# Calculate total price in cart # add total_price method to line_items model (app/models/line_item.rb) def total_price product.price * quantity end # add new total_price method to cart model (app/models/cart.rb) def total_price line_items.to_a.sum { |item| item.total_price } end # Tweak css to display total (app/assets/stylesheets/carts.css.scss) .carts { .item_price, .total_line { text-align: right; } .total_line .total_cell { font-weight: bold; border-top: 1px solid #595; } }
#### ERRORS AND ERROR REPORTING #### # (1) Intercept Bad Carts and Report on the Problem
# Go here: app/controllers/carts_controller.rb and add the following to the show method: def show begin @cart = Cart.find(params[:id]) rescue ActiveRecord::RecordNotFound logger.error "Attempt to access invalid cart #{params[:id]}" redirect_to store_url, notice: 'Invalid cart' else respond_to do |format| format.html # show.html.erb format.json { render json: @cart } end end end # Now the customer will just be redirected back to the catalog page and not see errors # To see errors on the dev side go to development.log in the log directory