shop_bunny 0.7

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.
@@ -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
+