Commit f2b33c51 by Truong Ba Dieu

Basic done

parent da5d87f3
...@@ -56,6 +56,7 @@ gem 'vacuum' ...@@ -56,6 +56,7 @@ gem 'vacuum'
gem "breadcrumbs_on_rails" gem "breadcrumbs_on_rails"
gem 'momentjs-rails', '>= 2.8.1' gem 'momentjs-rails', '>= 2.8.1'
gem 'bootstrap3-datetimepicker-rails', '~> 4.7.14' gem 'bootstrap3-datetimepicker-rails', '~> 4.7.14'
gem 'draper', '~> 1.3'
group :test do group :test do
# gem 'capybara' # Integration test tool to simulate a user on a website. # gem 'capybara' # Integration test tool to simulate a user on a website.
# gem 'capybara_minitest_spec' # MiniTest::Spec expectations for Capybara node matchers. # gem 'capybara_minitest_spec' # MiniTest::Spec expectations for Capybara node matchers.
......
...@@ -76,6 +76,11 @@ GEM ...@@ -76,6 +76,11 @@ GEM
addressable (~> 2.3) addressable (~> 2.3)
multi_json (~> 1.0) multi_json (~> 1.0)
rack (>= 1.3.0) rack (>= 1.3.0)
draper (1.4.0)
actionpack (>= 3.0)
activemodel (>= 3.0)
activesupport (>= 3.0)
request_store (~> 1.0)
email_spec (1.6.0) email_spec (1.6.0)
launchy (~> 2.1) launchy (~> 2.1)
mail (~> 2.2) mail (~> 2.2)
...@@ -156,6 +161,7 @@ GEM ...@@ -156,6 +161,7 @@ GEM
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rake (10.4.2) rake (10.4.2)
rdoc (4.2.0) rdoc (4.2.0)
request_store (1.1.0)
responders (2.1.0) responders (2.1.0)
railties (>= 4.2.0, < 5) railties (>= 4.2.0, < 5)
rspec (3.0.0) rspec (3.0.0)
...@@ -236,6 +242,7 @@ DEPENDENCIES ...@@ -236,6 +242,7 @@ DEPENDENCIES
database_cleaner database_cleaner
devise (~> 3.4.1) devise (~> 3.4.1)
dragonfly dragonfly
draper (~> 1.3)
email_spec email_spec
factory_girl_rails (~> 4.4.1) factory_girl_rails (~> 4.4.1)
faker faker
......
...@@ -14,5 +14,6 @@ ...@@ -14,5 +14,6 @@
//= require jquery_ujs //= require jquery_ujs
//= require turbolinks //= require turbolinks
//= require moment //= require moment
//= require bootstrap-sprockets
//= require bootstrap-datetimepicker //= require bootstrap-datetimepicker
//= require_tree ./components //= require_tree ./components
var EditCart = {
init: function($el){
this.el = $el;
if(this.el.size() > 0){
this.initChangeQuantity();
}
},
initChangeQuantity: function(){
var $this = this;
this.el.find(".item-quantity").on("change", function(e){
var $target = $(e.target);
var price = parseFloat($target.parents("tr").first().find(".item-price").text());
$target.parents("tr").first().find(".item-total").text( ($target.val() * price).toFixed(2) );
$this.changeTotalCart();
});
},
changeTotalCart: function(){
var sum = 0;
this.el.find('.item-total').each(function() {
sum += parseFloat($(this).text());
});
this.el.find(".total-cart").text(sum.toFixed(2));
}
};
$(document).on('ready page:load', function(){
EditCart.init($('#edit-cart'));
});
\ No newline at end of file
$(document).on('ready page:load', function(){
$('.datetimepicker').datetimepicker();
});
$(document).on 'ready page:load', ->
$('.datetimepicker').datetimepicker()
\ No newline at end of file
...@@ -532,9 +532,17 @@ img { ...@@ -532,9 +532,17 @@ img {
position: absolute; position: absolute;
} }
//*********************************************************** //***********************************************************
.btn{
padding: 8px 12px;
}
table{
th, td{
vertical-align: middle!important;
}
}
//*********************************************************** //***********************************************************
// FOOTER // FOOTER
#footer { #footer {
background-color: #222222; background-color: #222222;
......
...@@ -20,7 +20,8 @@ ...@@ -20,7 +20,8 @@
#search-bar{ #search-bar{
margin: 20px 0; margin: 20px 0;
input[type="text"]{ }
height: 35px;
} img{
display: inline-block;
} }
\ No newline at end of file
.recommend{ .recommends{
margin-bottom: 20px;
} }
.products{ .products{
...@@ -9,4 +9,19 @@ ...@@ -9,4 +9,19 @@
.title{ .title{
padding-left: 60px; padding-left: 60px;
} }
}
#product{
img{
float: left;
}
.content{
padding-left: 126px;
}
p{
margin-bottom: 6px;
}
.desc{
clear: both;
}
} }
\ No newline at end of file
...@@ -14,6 +14,10 @@ ...@@ -14,6 +14,10 @@
text-align: center!important; text-align: center!important;
} }
.text-right{
text-align: right!important;
}
.mar-0{ .mar-0{
margin: 0!important; margin: 0!important;
} }
...@@ -22,4 +26,8 @@ ...@@ -22,4 +26,8 @@
} }
.mar-l-0{ .mar-l-0{
margin-left: 0!important; margin-left: 0!important;
}
.w-100{
width: 100px!important;
} }
\ No newline at end of file
...@@ -4,7 +4,9 @@ class ApplicationController < ActionController::Base ...@@ -4,7 +4,9 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception protect_from_forgery with: :exception
layout :detect_layout layout :detect_layout
before_filter :configure_permitted_parameters, if: :devise_controller? before_action :configure_permitted_parameters, if: :devise_controller?
include OrderHelper
def get_categories def get_categories
@categories = Category.all @categories = Category.all
...@@ -22,4 +24,5 @@ class ApplicationController < ActionController::Base ...@@ -22,4 +24,5 @@ class ApplicationController < ActionController::Base
devise_parameter_sanitizer.for(:sign_up).push(:name) devise_parameter_sanitizer.for(:sign_up).push(:name)
devise_parameter_sanitizer.for(:account_update).push(:name) devise_parameter_sanitizer.for(:account_update).push(:name)
end end
end end
class CategoriesController < ApplicationController class CategoriesController < ApplicationController
before_filter :get_categories before_action :get_categories
def show add_breadcrumb "Home", :root_path
params[:per_page] ||= 5
params[:page] ||= 1
def show
@category = Category.find(params[:id]) @category = Category.find(params[:id])
@products = @category.products.page(params[:page]).per(params[:per_page]) @products = @category.products.page(params[:page])
add_breadcrumb @category.name
end end
end end
\ No newline at end of file
class HomeController < ApplicationController class HomeController < ApplicationController
add_breadcrumb "Home", :root_path add_breadcrumb "Home", :root_path
before_filter :get_categories before_action :authenticate_user!, only: [:cart]
before_filter :get_recommend before_action :get_categories
before_action :get_recommend
def index def index
params[:per_page] ||= 5 params[:per_page] ||= 5
...@@ -15,6 +16,10 @@ class HomeController < ApplicationController ...@@ -15,6 +16,10 @@ class HomeController < ApplicationController
@products = Product.search(params) @products = Product.search(params)
end end
def cart
end
private private
def get_recommend def get_recommend
@recommend_products = Product.recommend @recommend_products = Product.recommend
......
class OrdersController < ApplicationController
before_action :authenticate_user!
layout "cart"
def edit
end
def update
current_order.update_attributes(update_order_params)
redirect_to edit_order_path(current_order), notice: "Update cart successfully"
end
def add_to_cart
  • @dieutb Action này liên quan đến cart mà ở đây là OrdersController. Để phân nhiệm vụ rõ ràng hơn thì nên di chuyển action này sang CartsController chẳng hạn

Please register or sign in to reply
state = CartService.add_product(current_order, params)
if state[0] == true
flash[:notice] = state[1]
else
flash[:error] = state[1]
end
redirect_to product_path(params[:product_id])
end
def show
@order = current_user.orders.find_by_id(params[:id])
redirect_to root_path if @order.blank?
end
def checkout
order_id = current_order.id
state = CartService.checkout(current_order)
if state[0] == true
redirect_to order_path(order_id), notice: state[1]
else
redirect_to root_path(order_id), error: state[1]
end
end
private
def update_order_params
params.require(:order).permit(line_items_attributes:[:id, :quantity])
end
end
class ProductsController < ApplicationController class ProductsController < ApplicationController
before_filter :get_categories before_action :get_categories
add_breadcrumb "Home", :root_path
def show def show
@product = Product.find(params[:id]) @product = Product.find(params[:id])
add_breadcrumb @product.category.try(:name), category_path(@product.category.try(:id))
add_breadcrumb @product.title
end end
def new def new
......
class UserDecorator < Draper::Decorator
include Draper::LazyHelpers
def edit_profile_link
h.content_tag(:a, object.name || object.email, href: edit_user_registration_path)
end
end
\ No newline at end of file
module OrderHelper
def current_order
if current_user.present?
current_user.orders.where(state: Order.states[:cart]).first_or_create
else
nil
end
end
end
class UserMailer < ActionMailer::Base
default from: ENV["default_send_mail"]
add_template_helper(ApplicationHelper)
def success_checkout(current_order)
@user = current_order.user
@order = current_order
mail to: @user.email, subject: "Venshop! Checkout successfully"
end
end
class LineItem < ActiveRecord::Base
belongs_to :order
belongs_to :product
validate :check_product_stock
  • @dieutb Nên có 1 dòng trắng phân tách giữa vùng validations và vùng callbacks

Please register or sign in to reply
after_save :trigger_recalculate
before_destroy :trigger_calculate_order
def trigger_recalculate
  • @dieutb Hàm này có được dùng ở bên ngoài ko?

    Nếu chỉ dùng bên trong class LineItem thì đặt vào vùng private để cho dễ kiểm soát

Please register or sign in to reply
order.recalculate
change_square = quantity - quantity_was
product.substract_stock(change_square)
end
def check_product_stock
change_square = quantity - quantity_was
if change_square > product.stock
self.errors.add(:quantity, "Product #{product.title} is not enough in stock")
return false
end
end
end
class Order < ActiveRecord::Base
enum state: [:card, :checkout]
has_many :line_items, dependent: :destroy
belongs_to :user
accepts_nested_attributes_for :line_items
def recalculate
count = line_items.sum(:quantity)
total_price = 0
line_items.each do |item|
Please register or sign in to reply
total_price += item.quantity*item.price
end
self.update_columns(item_count: count, item_total: total_price)
  • @dieutb Vì hàm update_columns skip validations và callbacks nên tốt hơn là dùng hàm update. Trừ khi có mục đích là skip

    Có nhiều chỗ dùng hàm này

    Edited
Please register or sign in to reply
Please register or sign in to reply
end
def checkout
pid = generate_token
self.update_columns(state: Order.states[:checkout], pid: pid)
end
private
def generate_token
loop do
random_token = SecureRandom.urlsafe_base64(nil, false)
break random_token unless Order.exists?(pid: random_token)
end
end
end
...@@ -6,15 +6,23 @@ class Product < ActiveRecord::Base ...@@ -6,15 +6,23 @@ class Product < ActiveRecord::Base
# default 'public/images/product_default.jpg' # default 'public/images/product_default.jpg'
end end
paginates_per 5
validates :pid, :title, :price, :category_id, :image, :stock, :release_date, :public_date, presence: true validates :pid, :title, :price, :category_id, :image, :stock, :release_date, :public_date, presence: true
validates :pid, uniqueness: true validates :pid, uniqueness: true
scope :recommend, -> {where(recommend: true)} scope :recommend, -> { where(recommend: true) }
def self.search(params) def self.search(params)
params[:per_page] ||= 5 Product.order("release_date DESC").page(params[:page])
Please register or sign in to reply
params[:page] ||= 1 end
def can_buy?(quantity)
stock >= quantity.to_i
end
Product.order("release_date DESC").page(params[:page]).per(params[:per_page]) def substract_stock(change)
self.update_columns(stock: stock - change)
end end
end end
...@@ -5,6 +5,7 @@ class User < ActiveRecord::Base ...@@ -5,6 +5,7 @@ class User < ActiveRecord::Base
:recoverable, :rememberable, :trackable, :validatable :recoverable, :rememberable, :trackable, :validatable
has_many :products has_many :products
has_many :orders, dependent: :destroy
validates :name, presence: true validates :name, presence: true
end end
class CartService
def self.add_product(current_order, params)
params[:quantity] ||= 1
product = Product.find(params[:product_id])
if product.can_buy?(params[:quantity])
Please register or sign in to reply
line_item = current_order.line_items.where(product_id: product.id).first_or_create
line_item.quantity += params[:quantity].to_i
line_item.price = product.price
if line_item.save
return true, "Add to cart successfully"
else
return false, "Something went wrong"
end
else
return false, "Product is not enough in stock"
end
end
def self.checkout(current_order)
if current_order.line_items.blank?
return false, "Your cart is empty."
else
current_order.checkout
UserMailer.success_checkout(current_order).deliver
return true, "Checkout successfully"
end
end
def self.update_cart(current_order, params)
if current_order.update_attributes(params)
return true, "Update cart successfully"
else
return false, "Something went wrong"
end
end
end
\ No newline at end of file
#categories #categories
- if @products.present? - if @products.present?
.products .products
- @products.each do |product| = render partial: "products/item", collection: @products, as: :product
= render "products/item", product: product
.text-center .text-center
= paginate @products = paginate @products
\ No newline at end of file
- if @recommend_products.present? - if @recommend_products.present?
h3 Recommend Products
.row.recommends .row.recommends
- @recommend_products.each do |product| = render partial: "products/recommend_item", collection: @recommend_products, as: :product
= render "products/recommend_item", product: product hr
- if @products.present? - if @products.present?
.products .products
- @products.each do |product| h3 Products
= render "products/item", product: product = render partial: "products/item", collection: @products, as: :product
.text-center .text-center
= paginate @products = paginate @products
\ No newline at end of file
- if @products.present? - if @products.present?
.products .products
- @products.each do |product| = render partial: "products/item", collection: @products, as: :product
= render "products/item", product: product
.text-center .text-center
= paginate @products = paginate @products
\ No newline at end of file
...@@ -18,7 +18,9 @@ nav.navbar.navbar-inverse ...@@ -18,7 +18,9 @@ nav.navbar.navbar-inverse
ul.nav.navbar-nav.navbar-right ul.nav.navbar-nav.navbar-right
- if current_user.present? - if current_user.present?
li li
= link_to (current_user.name || current_user.email), edit_user_registration_path = link_to "Cart (#{current_order.item_count})", edit_order_path(current_order)
li
= current_user.decorate.edit_profile_link
li li
= link_to "Logout", destroy_user_session_path, method: 'delete' = link_to "Logout", destroy_user_session_path, method: 'delete'
- else - else
......
doctype html
html
head
meta[name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"]
title
= @title || "Venshop"
= favicon_link_tag 'favicon.ico'
= favicon_link_tag 'apple-touch-icon.png', rel: 'apple-touch-icon', type: 'image/png'
= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true
= javascript_include_tag "application", "data-turbolinks-track" => true
= csrf_meta_tags
body
#wrap
header
= render 'layouts/navigation'
.container
.row
.col-sm-12.text-center
= render 'shared/messages'
= render 'shared/search_bar'
= render_breadcrumbs :separator => ' / '
.row.mar-top-20
.col-sm-12
= yield
.clearfix
= render 'layouts/footer'
...@@ -14,6 +14,12 @@ html ...@@ -14,6 +14,12 @@ html
header header
= render 'layouts/navigation' = render 'layouts/navigation'
.container .container
.row
.col-sm-12.text-center
= render 'shared/messages'
= render 'shared/search_bar'
= render_breadcrumbs :separator => ' / '
.row.mar-top-20 .row.mar-top-20
.col-sm-12 .col-sm-12
= yield = yield
......
#edit-cart
- if current_order.line_items.blank?
.text-center
'You cart is empty.
= link_to "Continue shopping", root_path
- else
= form_for current_order, html:{ class: "form-horizontal" } do |f|
#error_explanation
- current_order.line_items.each do |i|
- if i.errors.any?
ul
- i.errors.full_messages.each do |msg|
li= msg
.table-responsive
table.table
thead
tr
th #
th Title
th.w-100 Quantity
th Price
th Total
th
tbody
= f.fields_for :line_items do |i|
tr
td= i.index+1
td= i.object.product.title
td= i.number_field :quantity, class: "form-control item-quantity", min: 1
td
span.item-price= i.object.price
span= i.object.product.currency
td
span.item-total= i.object.price * i.object.quantity
span= i.object.product.currency
td
tr
td(colspan=4)
td
b
span.total-cart= current_order.item_total
span= current_order.line_items.first.try(:product).try(:currency)
td
= f.submit "Update Cart", class: "btn btn-primary"
tr
td(colspan=5)
td
.text-center
= link_to "Checkout", checkout_orders_path, class: "btn btn-success btn-lg"
br
i
'(You must update change before checkout cart)
\ No newline at end of file
#order
h1= "Order ##{@order.pid}"
.table-responsive
table.table
thead
tr
th #
th Title
th.w-100 Quantity
th Price
th Total
tbody
- @order.line_items.each_with_index do |item, index|
tr
td= index+1
td= item.product.title
td= item.quantity
td= currency_number(item.price, item.product.currency)
td= currency_number(item.price * item.quantity, item.product.currency)
tr
td(colspan=4)
td
b
'Total:
= currency_number(@order.item_total, @order.line_items.first.try(:product).try(:currency))
tr
td(colspan=5)
td
\ No newline at end of file
.col-sm-4 .col-sm-4.text-center
h4= product.title = link_to product.title, product_path(product)
= image_tag product.image.thumb('71x38>') br
\ No newline at end of file = image_tag product.image.thumb('71x38>').url
\ No newline at end of file
#product
h4= @product.title
= image_tag @product.image.thumb("110x86#").url
.content
p= "Item ID: #{@product.pid}"
p= "Price #{currency_number(@product.price, @product.currency)}"
p= "Release Date: #{@product.release_date.strftime('%d/%m/%Y')}"
p= "Public Date: #{@product.public_date.strftime('%d/%m/%Y')}"
p= "Author: #{@product.author}"
p= "Publisher: #{@product.publisher}"
p= "Studio: #{@product.studio}"
p= "Stock: #{@product.stock}"
= form_tag add_to_cart_orders_path, class: "form-inline" do |f|
.form-group
= number_field_tag :quantity, 1, class: "form-control"
= hidden_field_tag :product_id, @product.id
= submit_tag "Add to cart", class: "btn btn-primary"
- flash.each do |name, msg|
- if msg.is_a?(String)
.alert(class="alert-#{name == 'notice' ? 'success' : 'danger'}")
button.close(data-dismiss="alert" aria-hidden="true")&times;
= content_tag :div, msg, :id => "flash_#{name}"
h3 Congratulation!!! You have checkout successfully.
b= "Your order pid: ##{@order.pid}"
.table-responsive
table.table
thead
tr
th #
th Title
th.w-100 Quantity
th Price
th Total
tbody
- @order.line_items.each_with_index do |item, index|
tr
td= index+1
td= item.product.title
td= item.quantity
td= currency_number(item.price, item.product.currency)
td= currency_number(item.price * item.quantity, item.product.currency)
tr
td(colspan=4)
td
b
'Total:
= currency_number(@order.item_total, @order.line_items.first.try(:product).try(:currency))
tr
td(colspan=5)
td
\ No newline at end of file
aws_access_key_id: "AKIAJ77C4CTZOP7TUVWQ" aws_access_key_id: "AKIAJ77C4CTZOP7TUVWQ"
aws_secret_access_key: "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" aws_secret_access_key: "cYJYb/MLGV0M6oi1+DjlliL1cfxmh78tKXnT6ZmX"
associate_tag: "zigexn6400-22" associate_tag: "zigexn6400-22"
default_send_mail: "no-reply@venshop.com"
\ No newline at end of file
...@@ -38,4 +38,14 @@ Rails.application.configure do ...@@ -38,4 +38,14 @@ Rails.application.configure do
# Raises error for missing translations # Raises error for missing translations
# config.action_view.raise_on_missing_translations = true # config.action_view.raise_on_missing_translations = true
config.action_mailer.default_url_options = { :host => "localhost:3000" }
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: 'smtp.mandrillapp.com',
port: 587,
domain: 'localhost:3000',
user_name: 'debug.com.ca@gmail.com',
password: 'NDGwmV0ahGerm4nFUFPSxA',
authentication: 'plain',
enable_starttls_auto: true }
end end
...@@ -9,7 +9,12 @@ Rails.application.routes.draw do ...@@ -9,7 +9,12 @@ Rails.application.routes.draw do
resources :categories, :only => [:show] resources :categories, :only => [:show]
resources :products, :only => [:new, :create, :show] resources :products, :only => [:new, :create, :show]
resources :orders, :only => [:edit, :update, :show] do
collection do
get "checkout" => "orders#checkout", as: :checkout
post "add_to_cart" => "orders#add_to_cart", as: :add_to_cart
end
end
# Example of regular route: # Example of regular route:
# get 'products/:id' => 'catalog#view' # get 'products/:id' => 'catalog#view'
......
class CreateOrders < ActiveRecord::Migration
def change
create_table :orders do |t|
t.string :pid
t.integer :item_count, default: 0
t.float :item_total, default: 0
t.integer :state, default: 1
t.datetime :completed_at
t.integer :user_id
t.timestamps null: false
end
end
end
class CreateLineItems < ActiveRecord::Migration
def change
create_table :line_items do |t|
t.integer :product_id
t.integer :order_id
t.integer :quantity, default: 0
t.float :price
end
end
end
...@@ -11,12 +11,30 @@ ...@@ -11,12 +11,30 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20150715091023) do ActiveRecord::Schema.define(version: 20150715093529) do
create_table "categories", force: :cascade do |t| create_table "categories", force: :cascade do |t|
t.string "name", limit: 255 t.string "name", limit: 255
end end
create_table "line_items", force: :cascade do |t|
t.integer "product_id", limit: 4
t.integer "order_id", limit: 4
t.integer "quantity", limit: 4, default: 0
t.float "price", limit: 24
end
create_table "orders", force: :cascade do |t|
t.string "pid", limit: 255
t.integer "item_count", limit: 4, default: 0
t.float "item_total", limit: 24, default: 0.0
t.integer "state", limit: 4, default: 1
t.datetime "completed_at"
t.integer "user_id", limit: 4
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "products", force: :cascade do |t| create_table "products", force: :cascade do |t|
t.string "pid", limit: 255 t.string "pid", limit: 255
t.string "title", limit: 1024, default: "", null: false t.string "title", limit: 1024, default: "", null: false
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment