ecommerce 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|