ecommerce 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +96 -0
- data/README +256 -0
- data/Rakefile +7 -0
- data/app/controllers/application_controller.rb +6 -0
- data/app/controllers/cart_controller.rb +42 -0
- data/app/controllers/ecommerce_controller.rb +5 -0
- data/app/controllers/payment_notifications_controller.rb +80 -0
- data/app/controllers/photos_controller.rb +19 -0
- data/app/controllers/products_controller.rb +35 -0
- data/app/helpers/application_helper.rb +3 -0
- data/app/helpers/products_helper.rb +8 -0
- data/app/models/cart.rb +108 -0
- data/app/models/cart_item.rb +39 -0
- data/app/models/photo.rb +21 -0
- data/app/models/product.rb +29 -0
- data/app/views/cart/index.haml +80 -0
- data/app/views/layouts/application.html.erb +14 -0
- data/app/views/products/_form.html.haml +45 -0
- data/app/views/products/edit.html.haml +5 -0
- data/app/views/products/index.html.haml +17 -0
- data/app/views/products/new.html.haml +4 -0
- data/app/views/products/show.haml +28 -0
- data/app/views/products/show/_photos.haml +13 -0
- data/config.ru +4 -0
- data/config/application.rb +43 -0
- data/config/boot.rb +13 -0
- data/config/database.yml +22 -0
- data/config/ecommerce.yml +16 -0
- data/config/environment.rb +5 -0
- data/config/environments/development.rb +26 -0
- data/config/environments/production.rb +49 -0
- data/config/environments/test.rb +35 -0
- data/config/initializers/ecommerce-config.rb +7 -0
- data/config/initializers/mime_types.rb +6 -0
- data/config/locales/en.yml +5 -0
- data/config/routes.rb +23 -0
- data/db/seeds.rb +7 -0
- data/doc/README_FOR_APP +2 -0
- data/ecommerce-0.0.1.gem +0 -0
- data/ecommerce.gemspec +24 -0
- data/lib/ecommerce.rb +4 -0
- data/lib/ecommerce/ecommerce.rb +5 -0
- data/lib/ecommerce/engine.rb +25 -0
- data/lib/ecommerce/version.rb +3 -0
- data/lib/generators/ecommerce/USAGE +4 -0
- data/lib/generators/ecommerce/ecommerce_generator.rb +30 -0
- data/lib/generators/ecommerce/templates/migration.rb +59 -0
- data/lib/tasks/.gitkeep +0 -0
- data/models/payment_notification.rb +15 -0
- data/public/404.html +26 -0
- data/public/422.html +26 -0
- data/public/500.html +26 -0
- data/public/favicon.ico +0 -0
- data/public/javascripts/ecommerce.js +10 -0
- data/public/robots.txt +5 -0
- data/public/stylesheets/.gitkeep +0 -0
- data/script/rails +6 -0
- data/spec/spec_helper.rb +27 -0
- data/test/performance/browsing_test.rb +9 -0
- data/test/test_helper.rb +13 -0
- data/vendor/plugins/.gitkeep +0 -0
- metadata +160 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
class PhotosController < InheritedResources::Base
|
2
|
+
def new
|
3
|
+
@product = Product.find_by_permalink(params['product_id']) || Product.find_by_id(params['product_id'])
|
4
|
+
end
|
5
|
+
|
6
|
+
def create
|
7
|
+
@photo = Photo.new(params[:photo])
|
8
|
+
@photo.product_id = params[:product_id]
|
9
|
+
@photo.save!
|
10
|
+
redirect_to :back
|
11
|
+
end
|
12
|
+
|
13
|
+
def destroy
|
14
|
+
photo = Photo.find_by_id(params[:id])
|
15
|
+
photo.destroy
|
16
|
+
redirect_to :back
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class ProductsController < InheritedResources::Base
|
2
|
+
|
3
|
+
unloadable
|
4
|
+
|
5
|
+
def edit
|
6
|
+
@product = Product.find_by_permalink(params['id']) || Product.find_by_id(params['id'])
|
7
|
+
edit!
|
8
|
+
end
|
9
|
+
|
10
|
+
def update
|
11
|
+
@product = Product.find_by_permalink(params['id']) || Product.find_by_id(params['id'])
|
12
|
+
update!
|
13
|
+
end
|
14
|
+
|
15
|
+
def search
|
16
|
+
# http://railscasts.com/episodes/120-thinking-sphinx with will_paginate
|
17
|
+
# @products = Product.search(params[:q], :page => 1, :per_page => 5, :order => :name)
|
18
|
+
|
19
|
+
@products = Product.where('name like ? or tags like ?', "%#{params['q']}%", "%#{params['q']}%").paginate(:per_page => 10, :page => params[:page])
|
20
|
+
|
21
|
+
respond_to do |format|
|
22
|
+
format.html { render :template => 'products/search/index' }
|
23
|
+
format.xml { render :xml => @products }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def by_permalink
|
28
|
+
if @product = Product.find_by_permalink(params['path'])
|
29
|
+
render :template => 'products/show'
|
30
|
+
else
|
31
|
+
render :template => 'public/404.html', :status => '404'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
data/app/models/cart.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
class Cart < ActiveRecord::Base
|
2
|
+
attr_reader :currency
|
3
|
+
include ActionView::Helpers::NumberHelper
|
4
|
+
|
5
|
+
has_many :cart_items
|
6
|
+
# Because 'has_many :cart_items, :as => :items' does not appear to work
|
7
|
+
def items ; self.cart_items ; end
|
8
|
+
|
9
|
+
def purchased?
|
10
|
+
self.purchased_at.present?
|
11
|
+
end
|
12
|
+
|
13
|
+
# Add n number of this product to our cart (addition)
|
14
|
+
def add(product, quantity=1)
|
15
|
+
self.alter_cart_item_quantity(:add, product, quantity.to_i)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Remove a product no matter what the quantity
|
19
|
+
def remove(product)
|
20
|
+
self.alter_cart_item_quantity(:set, product, 0)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Set the total number of this product in our cart to quantity. Setting to 0 will remove the item
|
24
|
+
def set_item_quantity(product, quantity)
|
25
|
+
self.alter_cart_item_quantity(:set, product, quantity)
|
26
|
+
end
|
27
|
+
|
28
|
+
def alter_cart_item_quantity(change_type, product, quantity)
|
29
|
+
item = self.items.detect{|item| item.product_id == product.id}
|
30
|
+
if item
|
31
|
+
if quantity >= 1
|
32
|
+
case change_type
|
33
|
+
when :add then item.increment_quantity(quantity)
|
34
|
+
when :set then item.quantity = quantity
|
35
|
+
end
|
36
|
+
else
|
37
|
+
item.remove_from_cart!
|
38
|
+
end
|
39
|
+
else
|
40
|
+
item = self.items.build(:product_id => product.id, :quantity => quantity)
|
41
|
+
end
|
42
|
+
item.save!
|
43
|
+
end
|
44
|
+
|
45
|
+
def totalf
|
46
|
+
Cart.format_price(self.total())
|
47
|
+
end
|
48
|
+
|
49
|
+
def total
|
50
|
+
self.items.inject(0) { |sum, cart_item| sum + cart_item.price }
|
51
|
+
end
|
52
|
+
|
53
|
+
def shipping_weight
|
54
|
+
self.items.inject(0) { |sum, cart_item| sum + cart_item.shipping_weight }
|
55
|
+
end
|
56
|
+
|
57
|
+
def empty?
|
58
|
+
self.items.empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
def empty!
|
62
|
+
self.items.each{ |cart_item| cart_item.remove_from_cart! }
|
63
|
+
end
|
64
|
+
|
65
|
+
def item_count
|
66
|
+
count = 0
|
67
|
+
self.items.each{|item| count += item.quantity}
|
68
|
+
count
|
69
|
+
end
|
70
|
+
|
71
|
+
# https://cms.paypal.com/cms_content/en_US/files/developer/PP_OrderMgmt_IntegrationGuide.pdf
|
72
|
+
# https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_html_Appx_websitestandard_htmlvariables
|
73
|
+
def paypal_encrypted(return_url, notify_url)
|
74
|
+
values = {
|
75
|
+
:invoice => self.id,
|
76
|
+
:notify_url => notify_url,
|
77
|
+
:business => ECO['paypal']['email'],
|
78
|
+
:cert_id => ECO['paypal']['cert_id'],
|
79
|
+
:cmd => '_cart',
|
80
|
+
:upload => 1,
|
81
|
+
:weight => self.shipping_weight
|
82
|
+
}
|
83
|
+
|
84
|
+
self.items.each_with_index do |item, index|
|
85
|
+
values.merge!({
|
86
|
+
"amount_#{index+1}" => item.unit_price,
|
87
|
+
"item_name_#{index+1}" => item.name,
|
88
|
+
"item_number_#{index+1}" => item.id,
|
89
|
+
"quantity_#{index+1}" => item.quantity
|
90
|
+
})
|
91
|
+
end
|
92
|
+
encrypt_for_paypal(values)
|
93
|
+
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
def encrypt_for_paypal(values)
|
97
|
+
signed = OpenSSL::PKCS7::sign(OpenSSL::X509::Certificate.new(APP_CERT_PEM), OpenSSL::PKey::RSA.new(APP_KEY_PEM, ''), values.map { |k, v| "#{k}=#{v}" }.join("\n"), [], OpenSSL::PKCS7::BINARY)
|
98
|
+
OpenSSL::PKCS7::encrypt([OpenSSL::X509::Certificate.new(PAYPAL_CERT_PEM)], signed.to_der, OpenSSL::Cipher::Cipher::new("DES3"), OpenSSL::PKCS7::BINARY).to_s.gsub("\n", "")
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.format_price(price)
|
102
|
+
# WTF?!
|
103
|
+
# undefined method `number_to_currency' for #<Class:0x370a064
|
104
|
+
# number_to_currency(sprintf("%.02f", price))
|
105
|
+
sprintf("$%.02f", price)
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class CartItem < ActiveRecord::Base
|
2
|
+
belongs_to :cart
|
3
|
+
belongs_to :product
|
4
|
+
|
5
|
+
delegate :name, :to => :product
|
6
|
+
|
7
|
+
def increment_quantity(increment)
|
8
|
+
self.quantity += increment
|
9
|
+
end
|
10
|
+
|
11
|
+
def quantity=(quantity)
|
12
|
+
if quantity < 1
|
13
|
+
self.remove_from_cart!
|
14
|
+
else
|
15
|
+
write_attribute(:quantity, quantity)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def shipping_weight
|
20
|
+
self.product.shipping_weight * self.quantity
|
21
|
+
end
|
22
|
+
|
23
|
+
def remove_from_cart!
|
24
|
+
self.destroy()
|
25
|
+
end
|
26
|
+
|
27
|
+
# Price per unit
|
28
|
+
def unit_price ; self.product.price ; end
|
29
|
+
def unit_pricef ; Cart.format_price(self.unit_price()) ; end
|
30
|
+
|
31
|
+
# Total price
|
32
|
+
def price ; self.product.price * self.quantity ; end
|
33
|
+
def pricef ; Cart.format_price(self.price()) ; end
|
34
|
+
|
35
|
+
# needed for rails magic form_for stuff, so DO NOT change this to some debug/display value
|
36
|
+
def to_s
|
37
|
+
self.product_id
|
38
|
+
end
|
39
|
+
end
|
data/app/models/photo.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
class Photo < ActiveRecord::Base
|
2
|
+
belongs_to :product
|
3
|
+
|
4
|
+
scope :photos, :conditions => "hidden != true"
|
5
|
+
|
6
|
+
NO_IMAGE_PATH = "/assets/not_available/medium/not_found.png"
|
7
|
+
|
8
|
+
has_attached_file :photo,
|
9
|
+
:styles => { :thumb => "95x95#", :large => "450x300" },
|
10
|
+
:default_url => "/assets/not_available/:style/not_found.png",
|
11
|
+
:url => "/assets/photos/:id/:style/:basename.:extension",
|
12
|
+
:path => ":rails_root/public/assets/photos/:id/:style/:basename.:extension"
|
13
|
+
|
14
|
+
validates_attachment_size :photo, :less_than => 1000.kilobytes
|
15
|
+
|
16
|
+
validate :photo, :presence => true, :content_type => ['image/jpeg', 'image/jpg', 'image/gif', 'image/png']
|
17
|
+
|
18
|
+
def alt
|
19
|
+
self.photo_file_name.dasherize.titleize.gsub(/\.(jpeg|jpg|gif|png)$/i,'')
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Product < ActiveRecord::Base
|
2
|
+
include ActionView::Helpers::NumberHelper
|
3
|
+
|
4
|
+
validates_presence_of :name, :permalink
|
5
|
+
|
6
|
+
has_many :photos
|
7
|
+
|
8
|
+
def photos?
|
9
|
+
self.photos.present?
|
10
|
+
end
|
11
|
+
|
12
|
+
def price? ; self.price_exists?(self.price) ; end
|
13
|
+
def pricef ; self.format_price(self.price) ; end
|
14
|
+
|
15
|
+
def sale? ; self.price_exists?(self.sale_price) ; end
|
16
|
+
def sale_pricef ; self.format_price(self.sale_price) ; end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def format_price(price)
|
21
|
+
number_to_currency(sprintf("%.02f", price))
|
22
|
+
end
|
23
|
+
|
24
|
+
def price_exists?(price)
|
25
|
+
price.to_f > 0
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
|
2
|
+
- if @cart.empty?
|
3
|
+
%p#no_items
|
4
|
+
There are no items in your cart.
|
5
|
+
|
6
|
+
- else
|
7
|
+
:css
|
8
|
+
#cartC div.cart_title {
|
9
|
+
font-weight : bold;
|
10
|
+
font-size : 1.1em;
|
11
|
+
}
|
12
|
+
form#cart_purchase input[type="text"] {
|
13
|
+
width : 15px;
|
14
|
+
margin : 0;
|
15
|
+
padding : 2px 4px;
|
16
|
+
}
|
17
|
+
form#cart_purchase hr {
|
18
|
+
background-color : #333;
|
19
|
+
}
|
20
|
+
div#paypal_checkoutC {
|
21
|
+
text-align : center;
|
22
|
+
margin-top : 1em;
|
23
|
+
}
|
24
|
+
div#notesC {
|
25
|
+
margin-top : 4em;
|
26
|
+
color : #666;
|
27
|
+
}
|
28
|
+
%div#cartC.prepend-10.span-28
|
29
|
+
%div.cart_title.cart_quantity.prepend-2.span-6 Quantity
|
30
|
+
%div.cart_title.product_name.span-14 Product Name
|
31
|
+
%div.cart_title.price.span-6.last Price
|
32
|
+
|
33
|
+
= form_tag cart_path, :method => :put, :id => 'cart_purchase' do
|
34
|
+
= hidden_field :quantity_total_update, 1
|
35
|
+
|
36
|
+
- for @cart_item in @cart.items
|
37
|
+
%div.span-28.last.clearfix
|
38
|
+
%div.cart_quantity.prepend-2.span-6
|
39
|
+
= text_field("cart_item[]", 'quantity', :size => 2)
|
40
|
+
|
41
|
+
%div.cart_item_name.span-14
|
42
|
+
= link_to h(@cart_item.name), product_path(@cart_item.product.id)
|
43
|
+
|
44
|
+
%div.cart_price.span-6.last
|
45
|
+
= number_to_currency(@cart_item.price)
|
46
|
+
|
47
|
+
%div.prepend-1.span-26.append-1
|
48
|
+
%hr
|
49
|
+
|
50
|
+
%div#update_quantityC.span-8
|
51
|
+
%button{:type => :submit, :id => 'update_cart'} Update Quantity
|
52
|
+
|
53
|
+
%div.span-10
|
54
|
+
%div.cart_totalC.span-7.last
|
55
|
+
%div.span-4.cart_total_title
|
56
|
+
Cart Total :
|
57
|
+
%div.span-3.last
|
58
|
+
= number_to_currency(@cart.total)
|
59
|
+
|
60
|
+
%div.prepend-10.span-28
|
61
|
+
%div.span-14
|
62
|
+
%div#paypal_checkoutC.span-14.last
|
63
|
+
= form_tag ECO['paypal']['url'], :method => :post, :id => :checkout do
|
64
|
+
= hidden_field_tag :cmd, "_s-xclick"
|
65
|
+
= hidden_field_tag :encrypted, @cart.paypal_encrypted(order_confirmation_path(:paypal), payment_notifications_url(:secret => ECO['paypal']['secret']))
|
66
|
+
|
67
|
+
%button{:type => :submit, :id => 'checkout'}
|
68
|
+
Checkout with
|
69
|
+
%strong PayPal
|
70
|
+
%div#notesC.prepend-10.span-28
|
71
|
+
%h4 What To Expect
|
72
|
+
%ul
|
73
|
+
%li you will be sent to PayPal to complete this transaction
|
74
|
+
%li tax will be applied only to Washington State residents
|
75
|
+
%li= "shipping costs are calculated by your total shipping weight (#{@cart.shipping_weight}lbs)"
|
76
|
+
|
77
|
+
-#
|
78
|
+
- form_tag cart_path(@cart), :method => :delete do
|
79
|
+
= submit_tag 'Empty Cart', :class => 'button'
|
80
|
+
|
@@ -0,0 +1,45 @@
|
|
1
|
+
|
2
|
+
= form_for @product, :html => {:class => 'edit_product scaffold'} do |f|
|
3
|
+
= render :partial => 'shared/user_notice', :locals => {:model_obj => @product}
|
4
|
+
|
5
|
+
%div.formC_700
|
6
|
+
.field
|
7
|
+
= f.label :name, 'Name'
|
8
|
+
= f.text_field :name
|
9
|
+
|
10
|
+
.field
|
11
|
+
= f.label :permalink, 'Permalink'
|
12
|
+
%div.disabled_form_field= @product.permalink
|
13
|
+
|
14
|
+
.field
|
15
|
+
= f.label :descr, 'Description'
|
16
|
+
= f.text_area :descr
|
17
|
+
|
18
|
+
.field
|
19
|
+
= f.label :price
|
20
|
+
= f.text_field :price
|
21
|
+
|
22
|
+
.field
|
23
|
+
= f.label :sale_price
|
24
|
+
= f.text_field :sale_price
|
25
|
+
|
26
|
+
.field
|
27
|
+
= f.label :shipping_weight, "Shipping Weight (lbs)"
|
28
|
+
= f.text_field :shipping_weight
|
29
|
+
|
30
|
+
|
31
|
+
.field
|
32
|
+
= f.label :meta_description, 'Meta Description'
|
33
|
+
= f.text_field :meta_description
|
34
|
+
|
35
|
+
.field
|
36
|
+
= f.label :meta_keywords, 'Meta Keywords'
|
37
|
+
= f.text_field :meta_keywords
|
38
|
+
|
39
|
+
.actions.clearfix
|
40
|
+
= f.submit 'Save Product Information', :class => :button
|
41
|
+
|
42
|
+
%hr
|
43
|
+
= render :partial => 'photos/form'
|
44
|
+
%hr
|
45
|
+
= render :partial => 'photos/destroy'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
%h1 Listing products
|
2
|
+
|
3
|
+
%table
|
4
|
+
%tr
|
5
|
+
%th
|
6
|
+
%th
|
7
|
+
%th
|
8
|
+
|
9
|
+
- @products.each do |product|
|
10
|
+
%tr
|
11
|
+
%td= link_to 'Show', product
|
12
|
+
%td= link_to 'Edit', edit_product_path(product)
|
13
|
+
%td= link_to 'Destroy', product, :confirm => 'Are you sure?', :method => :delete
|
14
|
+
|
15
|
+
%br
|
16
|
+
|
17
|
+
= link_to 'New Product', new_product_path
|