shop_bunny 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ class ApplicationController < ActionController::Base
2
+ protect_from_forgery
3
+ end
@@ -0,0 +1,50 @@
1
+ class CartsController < ApplicationController
2
+ respond_to :html, :json
3
+ before_filter :find_cart
4
+ before_filter :find_item, :only => [:add_item, :remove_item]
5
+
6
+ if ShopBunny.controller_enhancement
7
+ include ShopBunny.controller_enhancement
8
+ end
9
+
10
+ def show
11
+ respond_with @cart
12
+ end
13
+
14
+ def add_item
15
+ @cart.add_item(@item) if @item
16
+ respond_with @cart do |format|
17
+ format.html { redirect_to :action => :show }
18
+ end
19
+ end
20
+
21
+ def remove_item
22
+ @cart.remove_item(@item) if @item
23
+ respond_with @cart do |format|
24
+ format.html { redirect_to :action => :show }
25
+ end
26
+ end
27
+
28
+ def update
29
+ @cart.update_attributes(params[:cart])
30
+ respond_with @cart do |format|
31
+ format.html {
32
+ if @cart.errors.any?
33
+ render 'show'
34
+ else
35
+ # We have to redirect by hand here, because
36
+ # url_for(@cart) returns "/cart.x" when cart is a singular resource
37
+ # (see routes.rb). This is a known limitation of Rails.
38
+ # See https://rails.lighthouseapp.com/projects/8994/tickets/4168
39
+ redirect_to :action => :show
40
+ end
41
+ }
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def find_item
48
+ @item = @cart.item_model.find(params[:item_id])
49
+ end
50
+ end
@@ -0,0 +1,121 @@
1
+ class Cart < ActiveRecord::Base
2
+ attr_accessor :coupon_code
3
+ has_many :cart_items, :dependent => :destroy
4
+ has_many :coupon_uses, :dependent => :destroy
5
+ has_many :coupons, :through => :coupon_uses
6
+ accepts_nested_attributes_for :cart_items, :allow_destroy => true
7
+ before_save :update_coupons
8
+ attr_accessible :coupon_code, :cart_items_attributes
9
+ validate :coupon_code_must_be_valid
10
+
11
+ def items
12
+ self.cart_items
13
+ end
14
+
15
+ def item_count
16
+ self.cart_items.inject(0) {|sum,e| sum += e.quantity}
17
+ end
18
+
19
+ def shipping_costs
20
+ return 0 if coupons.any?(&:removes_shipping_cost)
21
+ shipping_cost_calculator.costs_for(self)
22
+ end
23
+
24
+ # Calculates the sum of all cart_items, excluding the coupons discount!
25
+ def item_sum
26
+ self.cart_items.inject(0) {|sum,e| sum += e.quantity*e.item.price}
27
+ end
28
+
29
+ # Calculates the total sum and applies the coupons discount!
30
+ def total
31
+ sum = item_sum
32
+
33
+ absolute_discount = coupons.sum(:discount_credit)
34
+ relative_discount = coupons.inject(1) {|s,coupon| s * coupon.discount_percentage }
35
+
36
+ sum -= absolute_discount
37
+ sum *= relative_discount
38
+
39
+ sum + shipping_costs
40
+
41
+ [0, sum].max
42
+ end
43
+
44
+ #increases the quantity of an article. creates a new one if it doesn't exist
45
+ def add_item(item,options = {})
46
+ options[:quantity] ||= 1
47
+ cart_item = self.cart_items.find_or_create_by_item_id(item.id)
48
+ cart_item.quantity += options[:quantity]
49
+ cart_item.save!
50
+ self.reload
51
+ cart_item
52
+ end
53
+
54
+ #removes a quantity of an article specified by :article_id, returns nil if no article has been found
55
+ def remove_item(item,options = {})
56
+ cart_item = self.cart_items.find_by_item_id(item)
57
+
58
+ options[:quantity] ||= cart_item.quantity
59
+ if cart_item
60
+ cart_item.quantity -= options[:quantity]
61
+ cart_item.save!
62
+ self.reload
63
+ end
64
+ cart_item
65
+ end
66
+
67
+ #sets the quantity of an article specified by :article_id, returns nil if no article has been found
68
+ def update_item(item,options)
69
+ cart_item = self.cart_items.find_by_item_id(item)
70
+ if cart_item
71
+ cart_item.quantity = options[:quantity]
72
+ cart_item.save!
73
+ self.reload
74
+ end
75
+ cart_item
76
+ end
77
+
78
+ def shipping_cost_calculator
79
+ ShopBunny.shipping_cost_calculator
80
+ end
81
+
82
+ # Returns the class/model of the items you can buy. (Products)
83
+ def item_model
84
+ ShopBunny.item_model_class_name.constantize
85
+ end
86
+
87
+ # Make
88
+ def as_json(options={})
89
+ {
90
+ :cart => attributes.
91
+ merge(
92
+ :cart_items => cart_items.as_json,
93
+ :item_count => item_count,
94
+ :coupons => coupons.as_json,
95
+ :item_sum => item_sum,
96
+ :shipping_costs => shipping_costs,
97
+ :total => total
98
+ )
99
+ }
100
+ end
101
+
102
+ private
103
+
104
+ def update_coupons
105
+ Array(@coupon_code).each { |code|
106
+ coupon = Coupon.find_by_code(code)
107
+ coupons << coupon if coupon
108
+ }
109
+ end
110
+
111
+ def coupon_code_must_be_valid
112
+ Array(@coupon_code).each { |code|
113
+ coupon = Coupon.find_by_code(code)
114
+ if coupon.nil?
115
+ errors.add(:coupon_code, "is unknown")
116
+ elsif coupon.expired?
117
+ errors.add(:coupon_code, "is expired")
118
+ end
119
+ }
120
+ end
121
+ end
@@ -0,0 +1,23 @@
1
+ class CartItem < ActiveRecord::Base
2
+ belongs_to :cart
3
+ belongs_to :item, :class_name => ShopBunny.item_model_class_name
4
+
5
+ validates_presence_of :cart_id
6
+ validates_presence_of :item_id
7
+ validates_numericality_of :quantity
8
+
9
+ before_validation :set_default_quantity
10
+ after_update :destroy_if_empty
11
+ # TODO attr_accessible :quantity
12
+
13
+
14
+ private
15
+
16
+ def set_default_quantity
17
+ self.quantity ||= 0
18
+ end
19
+
20
+ def destroy_if_empty
21
+ self.destroy if quantity.to_i <= 0
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ class Coupon < ActiveRecord::Base
2
+ has_many :coupon_uses, :dependent => :destroy
3
+ has_many :carts, :through => :coupon_uses
4
+
5
+ validates_presence_of :code
6
+ validates_uniqueness_of :code
7
+ validates_presence_of :title
8
+ attr_accessible
9
+
10
+ # TODO Add self destruction when coupon has expired?
11
+
12
+ def expired?
13
+ not_yet_valid? || has_expired?
14
+ end
15
+
16
+ # FIXME rename?
17
+ def not_yet_valid?
18
+ Time.now < self.valid_from if self.valid_from
19
+ end
20
+
21
+ def has_expired?
22
+ Time.now > self.valid_until if self.valid_until
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ class CouponUse < ActiveRecord::Base
2
+ belongs_to :cart
3
+ belongs_to :coupon
4
+
5
+ validates_presence_of :cart_id
6
+ validates_presence_of :coupon_id
7
+ end
@@ -0,0 +1,6 @@
1
+ # An item in the store
2
+ class Item < ActiveRecord::Base
3
+ validates_presence_of :price
4
+ validates_numericality_of :price
5
+ end
6
+
@@ -0,0 +1,83 @@
1
+ <div class="shop">
2
+ <h1>Shop</h1>
3
+ <table>
4
+ <table>
5
+ <tr>
6
+ <th>Item id</th>
7
+ <th>Item price</th>
8
+ <th>Action</th>
9
+ </tr>
10
+ <% @cart.item_model.limit(10).each do |item| %>
11
+ <tr>
12
+ <th><%= item.id %></th>
13
+ <th><%= item.price %></th>
14
+ <th><%= link_to("Add to cart", add_item_to_cart_path(:item_id => item.id)) %></th>
15
+ </tr>
16
+ <% end %>
17
+ </table>
18
+ </table>
19
+ </div>
20
+
21
+ <div class="shopping-cart">
22
+ <div class="items">
23
+ <%= form_for @cart, :url => cart_path do |f| %>
24
+ <h1>Shopping Cart</h1>
25
+ <table>
26
+ <tr>
27
+ <th>Item id</th>
28
+ <th>Item price</th>
29
+ <th>Item quantity</th>
30
+ <th>Remove</th>
31
+ </tr>
32
+
33
+ <%= f.fields_for :cart_items do |cart_item| %>
34
+ <tr>
35
+ <td><%= cart_item.object.item.id %></td>
36
+ <td><%= cart_item.object.item.price %></td>
37
+ <td><%= cart_item.text_field :quantity %></td>
38
+ <td><%= link_to 'Remove', remove_item_from_cart_path(:item_id => cart_item.object.item.id) %></td>
39
+ </tr>
40
+ <% end %>
41
+ </table>
42
+
43
+ <table class="cashpoint">
44
+ <tr>
45
+ <td>Sum: <%= @cart.item_sum %></td>
46
+ </tr>
47
+ <tr>
48
+ <td>Shippin costs: <%= @cart.shipping_costs %></td>
49
+ </tr>
50
+ <tr>
51
+ <td>Total: <%= @cart.total %></td>
52
+ </tr>
53
+ </table>
54
+
55
+ <%= f.submit %>
56
+ <% end %>
57
+ </div>
58
+
59
+ <div class="coupons">
60
+ <%= form_for @cart, :url => cart_path do |f| %>
61
+ <h1>Available coupons</h2>
62
+ <ul>
63
+ <% Coupon.limit(10).all.each do |coupon| %>
64
+ <li><%= coupon.code %></ti>
65
+ <% end %>
66
+ </ul>
67
+
68
+ <h2>Your coupons</h2>
69
+ <ul>
70
+ <% @cart.coupons.each do |coupon| %>
71
+ <li><%= coupon.title %></ti>
72
+ <% end %>
73
+ </ul>
74
+ <p>
75
+ <%# TODO Show error messages %>
76
+ <%= @cart.errors if @cart.errors.any? %>
77
+ </p>
78
+ <%= f.label :coupon_code, 'Got a code?' %>
79
+ <%= f.text_field :coupon_code %>
80
+ <%= f.submit :value => "Add code" %>
81
+ <% end %>
82
+ </div>
83
+ </div>
Binary file
@@ -0,0 +1,14 @@
1
+ class CreateCarts < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :carts do |t|
4
+ t.integer :owner_id
5
+ t.string :type
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+
11
+ def self.down
12
+ drop_table :carts
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ class CreateCartItems < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :cart_items do |t|
4
+ t.integer :cart_id
5
+ t.integer :item_id
6
+ t.integer :quantity
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+
12
+ def self.down
13
+ drop_table :cart_items
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ class CreateItems < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :items do |t|
4
+ t.float :price
5
+ t.string :title
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+
11
+ def self.down
12
+ drop_table :items
13
+ end
14
+ end
@@ -0,0 +1,21 @@
1
+ class CreateCoupons < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :coupons do |t|
4
+ t.boolean :removes_tax, :default => false
5
+ t.float :discount_percentage, :default => 1.0
6
+ t.float :discount_credit, :default => 0.0
7
+ t.boolean :removes_shipping_cost, :default => false
8
+ t.integer :bonus_article_id
9
+ t.date :valid_from
10
+ t.date :valid_until
11
+ t.text :title
12
+ t.string :code
13
+
14
+ t.timestamps
15
+ end
16
+ end
17
+
18
+ def self.down
19
+ drop_table :coupons
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ class CreateCouponUses < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :coupon_uses do |t|
4
+ t.integer :cart_id
5
+ t.integer :coupon_id
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+
11
+ def self.down
12
+ drop_table :coupon_uses
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ class RemoveOwnerIdFromCarts < ActiveRecord::Migration
2
+ def self.up
3
+ remove_column :carts, :owner_id
4
+ end
5
+
6
+ def self.down
7
+ add_column :carts, :owner_id, :integer
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class SetDefaultColumnValueForCartItemsQuantity < ActiveRecord::Migration
2
+ def self.up
3
+ change_column_default :cart_items, :quantity, 0
4
+ end
5
+
6
+ def self.down
7
+ change_column_default :cart_items, :quantity, nil
8
+ end
9
+ end
data/db/schema.rb ADDED
@@ -0,0 +1,57 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # Note that this schema.rb definition is the authoritative source for your
6
+ # database schema. If you need to create the application database on another
7
+ # system, you should be using db:schema:load, not running all the migrations
8
+ # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9
+ # you'll amass, the slower it'll run and the greater likelihood for issues).
10
+ #
11
+ # It's strongly recommended to check this file into your version control system.
12
+
13
+ ActiveRecord::Schema.define(:version => 20100916194407) do
14
+
15
+ create_table "cart_items", :force => true do |t|
16
+ t.integer "cart_id"
17
+ t.integer "item_id"
18
+ t.integer "quantity", :default => 0
19
+ t.datetime "created_at"
20
+ t.datetime "updated_at"
21
+ end
22
+
23
+ create_table "carts", :force => true do |t|
24
+ t.string "type"
25
+ t.datetime "created_at"
26
+ t.datetime "updated_at"
27
+ end
28
+
29
+ create_table "coupon_uses", :force => true do |t|
30
+ t.integer "cart_id"
31
+ t.integer "coupon_id"
32
+ t.datetime "created_at"
33
+ t.datetime "updated_at"
34
+ end
35
+
36
+ create_table "coupons", :force => true do |t|
37
+ t.boolean "removes_tax", :default => false
38
+ t.float "discount_percentage", :default => 1.0
39
+ t.float "discount_credit", :default => 0.0
40
+ t.boolean "removes_shipping_cost", :default => false
41
+ t.integer "bonus_article_id"
42
+ t.date "valid_from"
43
+ t.date "valid_until"
44
+ t.text "title"
45
+ t.string "code"
46
+ t.datetime "created_at"
47
+ t.datetime "updated_at"
48
+ end
49
+
50
+ create_table "items", :force => true do |t|
51
+ t.float "price"
52
+ t.string "title"
53
+ t.datetime "created_at"
54
+ t.datetime "updated_at"
55
+ end
56
+
57
+ end
data/db/seeds.rb ADDED
@@ -0,0 +1,19 @@
1
+ # This file should contain all the record creation needed to seed the database with its default values.
2
+ # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3
+ #
4
+ # Examples:
5
+ #
6
+ # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }])
7
+ # Mayor.create(:name => 'Daley', :city => cities.first)
8
+
9
+ # Dummy data for development
10
+ if Rails.env.development?
11
+ 3.times { |i| Item.create! :price => i }
12
+ 3.times { |i|
13
+ c = Coupon.new
14
+ c.code = "code#{i}"
15
+ c.title = "10% discount coupon #{i}"
16
+ c.discount_percentage = 0.9
17
+ c.save!
18
+ }
19
+ end
data/db/test.sqlite3 ADDED
Binary file
@@ -0,0 +1,13 @@
1
+ module ShopBunny
2
+ class InstallGenerator < Rails::Generators::Base
3
+ source_root File.expand_path("../../../../", __FILE__)
4
+
5
+ def copy_files
6
+ puts Dir['.']
7
+ directory 'db/migrate'
8
+ copy_file 'config/initializers/shop_bunny.rb'
9
+ end
10
+ end
11
+ end
12
+
13
+ # TODO Write a generator test
@@ -0,0 +1,15 @@
1
+ require 'rails'
2
+
3
+ module ShopBunny
4
+ class Engine < Rails::Engine
5
+
6
+ # Config defaults
7
+ config.mount_at = '/'
8
+
9
+ # Check the gem config
10
+ initializer "check config" do |app|
11
+ # make sure mount_at ends with trailing slash
12
+ config.mount_at += '/' unless config.mount_at.last == '/'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ # The default shipping cost calculator
2
+ class ShopBunny::ShippingCostCalculator
3
+ def self.costs_for(cart)
4
+ 8.90
5
+ end
6
+ end
data/lib/shop_bunny.rb ADDED
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+ module ShopBunny
3
+ require 'shop_bunny/engine' if defined?(Rails) && Rails::VERSION::MAJOR == 3
4
+ require 'shop_bunny/shipping_cost_calculator'
5
+
6
+ mattr_accessor :item_model_class_name
7
+ @@item_model_class_name = 'Item'
8
+
9
+ mattr_accessor :shipping_cost_calculator
10
+ @@shipping_cost_calculator = ShopBunny::ShippingCostCalculator
11
+
12
+ mattr_accessor :controller_enhancement
13
+ @@controller_enhancement = nil
14
+
15
+ # This is the default way to setup ShopBunny and is used in the
16
+ # initializer which gets generated by `rails generate shop_bunny:install`
17
+ def self.setup
18
+ yield self
19
+ end
20
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shop_bunny
3
+ version: !ruby/object:Gem::Version
4
+ hash: 5
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 7
9
+ version: "0.7"
10
+ platform: ruby
11
+ authors:
12
+ - kopfmaschine.com
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-09-20 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: A simple shop gem that integrates a cart and coupons functionality
22
+ email:
23
+ - jan@kopfmaschine.com
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - lib/generators/shop_bunny/install_generator.rb
32
+ - lib/shop_bunny.rb
33
+ - lib/shop_bunny/shipping_cost_calculator.rb
34
+ - lib/shop_bunny/engine.rb
35
+ - app/views/carts/show.html.erb
36
+ - app/models/cart.rb
37
+ - app/models/coupon_use.rb
38
+ - app/models/cart_item.rb
39
+ - app/models/coupon.rb
40
+ - app/models/item.rb
41
+ - app/controllers/application_controller.rb
42
+ - app/controllers/carts_controller.rb
43
+ - db/test.sqlite3
44
+ - db/schema.rb
45
+ - db/migrate/20100915162029_remove_owner_id_from_carts.rb
46
+ - db/migrate/20100915091059_create_coupons.rb
47
+ - db/migrate/20100915073016_create_items.rb
48
+ - db/migrate/20100902115627_create_carts.rb
49
+ - db/migrate/20100915093821_create_coupon_uses.rb
50
+ - db/migrate/20100916194407_set_default_column_value_for_cart_items_quantity.rb
51
+ - db/migrate/20100902115757_create_cart_items.rb
52
+ - db/development.sqlite3
53
+ - db/seeds.rb
54
+ has_rdoc: true
55
+ homepage: http://kopfmaschine.com
56
+ licenses: []
57
+
58
+ post_install_message:
59
+ rdoc_options: []
60
+
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ hash: 3
69
+ segments:
70
+ - 0
71
+ version: "0"
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ hash: 23
78
+ segments:
79
+ - 1
80
+ - 3
81
+ - 6
82
+ version: 1.3.6
83
+ requirements: []
84
+
85
+ rubyforge_project:
86
+ rubygems_version: 1.3.7
87
+ signing_key:
88
+ specification_version: 3
89
+ summary: A shop template for your needs
90
+ test_files: []
91
+