carter 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Louis Gillies
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # carter
2
+
3
+ A simple shopping cart for ruby.
4
+
5
+ [Carter]("http://en.wikipedia.org/wiki/Carter_(name)/") - "transports goods by cart"
6
+
7
+ * Anything can be added to the cart using
8
+ `acts_as_cartable`
9
+ * Any page can load a cart from session.
10
+ * Can be included without a need for a complete e-commerce solution.
11
+
12
+ ## Contributing to carter
13
+
14
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
15
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
16
+ * Fork the project
17
+ * Start a feature/bugfix branch
18
+ * Commit and push until you are happy with your contribution
19
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
20
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
21
+
22
+ ## Copyright
23
+
24
+ Copyright (c) 2011 Louis Gillies. See LICENSE.txt for
25
+ further details.
26
+
@@ -0,0 +1,34 @@
1
+ class CartItemsController < ApplicationController
2
+ load_cart_and_cartable
3
+ before_filter :find_cart_item, :only => [:update, :destroy]
4
+
5
+ # RESTful equiv of add
6
+ def create
7
+ persist_cart if @cart.new_record?
8
+ @cart.add_item(find_cartable)
9
+ end
10
+
11
+ def update
12
+ if @cart_item.update_attributes(params[:cart_item])
13
+ flash[:notice] = t(:cart_updated)
14
+ redirect_to cart_path(cart)
15
+ else
16
+ flash[:notice] = t(:cart_update_failed)
17
+ end
18
+ end
19
+
20
+ def destroy
21
+ if @cart_item.destroy
22
+ flash[:notice] = t(:cart_item_removed)
23
+ redirect_to cart_path(@cart)
24
+ else
25
+ flash[:notice] = t(:cart_update_failed)
26
+ end
27
+ end
28
+
29
+ protected
30
+
31
+ def find_cart_item
32
+ @cart_item = @cart.cart_items.find(params[:id])
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ class CartsController < ApplicationController
2
+ load_cart_and_cartable
3
+
4
+ def show
5
+
6
+ end
7
+
8
+ def destroy
9
+ cart.destroy
10
+ end
11
+ end
@@ -0,0 +1,34 @@
1
+ class Cart < ActiveRecord::Base
2
+ belongs_to :shopper, :polymorphic => true # for extra persistance
3
+ has_many :cart_items, :dependent => :destroy
4
+
5
+ include Carter::Cart
6
+ extend Carter::ActiveRecord::Extensions
7
+
8
+ attr_accessor :gateway_response
9
+
10
+ def add_item(cartable, quantity = 1, owner=nil)
11
+ existing_cart_item = cart_item_for_cartable_and_owner(cartable, owner)
12
+ Cart.transaction do
13
+ if existing_cart_item.blank?
14
+ cart_items.create!(:cartable => cartable, :name => cartable.cartable_name, :price => cartable.cartable_price, :quantity => quantity, :owner => owner)
15
+ else
16
+ if cartable.allow_multiples?
17
+ existing_cart_item.update_attributes(:quantity => existing_cart_item.quantity + quantity)
18
+ else
19
+ raise Carter::MultipleCartItemsNotAllowed, "#{cartable.cartable_name} is already in your basket"
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ # TODO cache this value
26
+ def total
27
+ cart_items.reload.map.sum(&:total_price).to_money
28
+ end
29
+
30
+ def empty?
31
+ cart_items.blank?
32
+ end
33
+
34
+ end
@@ -0,0 +1,40 @@
1
+ class CartItem < ActiveRecord::Base
2
+ belongs_to :cart
3
+ belongs_to :cartable, :polymorphic => true
4
+ belongs_to :owner, :polymorphic => true
5
+ delegate :shopper, :to => :cart
6
+
7
+ extend Carter::ActiveRecord::Extensions
8
+ include Carter::StateMachine::CartItem
9
+
10
+ after_update :check_quantity
11
+
12
+ # Match a cart_item by owner and cartable
13
+ named_scope :for_cartable, lambda { |cartable|
14
+ { :conditions => { :cartable_type => cartable.class.to_s, :cartable_id => cartable.id } }
15
+ }
16
+
17
+ named_scope :for_owner, lambda {|owner|
18
+ { :conditions => { :owner_id => (owner ? owner.id : nil), :owner_type => (owner ? owner.class.to_s : nil) } }
19
+ }
20
+
21
+ named_scope :for_cartable_and_owner, lambda {|cartable, owner|
22
+ { :conditions => for_owner(owner).proxy_options[:conditions].merge!(for_cartable(cartable).proxy_options[:conditions])}
23
+ }
24
+
25
+ money_composed_column :total_price,
26
+ :mapping => [[:price, :cents], [:quantity]],
27
+ :constructor => Proc.new{|value, quantity| Money.new(value.to_i * quantity.to_i)}
28
+
29
+ money_composed_column :price
30
+
31
+ def refresh
32
+ update_attributes :price => cartable.cartable_price, :name => cartable.cartable_name
33
+ end
34
+
35
+ protected
36
+
37
+ def check_quantity
38
+ self.destroy if self.quantity < 1
39
+ end
40
+ end
File without changes
@@ -0,0 +1,21 @@
1
+ class CarterGenerator < Rails::Generator::Base
2
+ default_options :skip_migration => false
3
+
4
+ def manifest
5
+ record do |m|
6
+ if !options[:skip_migration] && defined?(ActiveRecord)
7
+ m.migration_template "migration.rb", 'db/migrate',
8
+ :migration_file_name => "create_carter"
9
+ end
10
+ end
11
+ end
12
+
13
+ protected
14
+
15
+ def add_options!(opt)
16
+ opt.separator ''
17
+ opt.separator 'Options:'
18
+ opt.on("--skip-migration", "Don't generate a migration") { |v| options[:skip_migration] = v }
19
+ end
20
+
21
+ end
@@ -0,0 +1,20 @@
1
+ class CarterInstallGenerator < Rails::Generator::Base
2
+ default_options :skip_migration => false
3
+
4
+ def copy_controller_file
5
+ copy_file "cart_items_controller.rb", "app/controllers"
6
+ copy_file "carts_controller.rb", "app/controllers"
7
+ end
8
+
9
+ def copy_view_file
10
+ copy_file "carts/show.html.erb", "app/views/carts"
11
+ end
12
+ protected
13
+
14
+ def add_options!(opt)
15
+ opt.separator ''
16
+ opt.separator 'Options:'
17
+ opt.on("--skip-migration", "Don't generate a migration") { |v| options[:skip_migration] = v }
18
+ end
19
+
20
+ end
@@ -0,0 +1,37 @@
1
+ class CreateCarter < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :carts do |t|
4
+ t.string :session_id, :state
5
+ t.belongs_to :shopper, :polymorphic => true
6
+ t.timestamps
7
+ end
8
+
9
+ add_index :carts, :shopper_id
10
+ add_index :carts, :state
11
+ add_index :carts, :shopper_type
12
+
13
+ create_table :cart_items do |t|
14
+ t.string :name, :state
15
+ t.belongs_to :cartable, :polymorphic => true
16
+ t.belongs_to :owner, :polymorphic => true
17
+ t.belongs_to :cart
18
+ t.column :price, :float, :default => "0.00"
19
+ t.column :quantity, :integer
20
+ t.timestamps
21
+ end
22
+
23
+ add_index :cart_items, :name
24
+ add_index :cart_items, :state
25
+ add_index :cart_items, :cartable_id
26
+ add_index :cart_items, :cart_id
27
+ add_index :cart_items, :cartable_type
28
+ add_index :cart_items, :owner_id
29
+ add_index :cart_items, :owner_type
30
+ end
31
+
32
+ def self.down
33
+ drop_table :carts
34
+ drop_table :cart_items
35
+ end
36
+ end
37
+
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'rails/init.rb'
data/lib/carter.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'carter/cartable'
2
+ require 'carter/initializer'
3
+ require 'carter/cart'
4
+ require 'carter/controller_additions'
5
+ require 'carter/controller_resource'
6
+ require 'carter/state_machine'
7
+ require 'carter/errors'
8
+ require 'carter/active_record/extensions'
@@ -0,0 +1,22 @@
1
+ module Carter
2
+ module ActiveRecord
3
+ module Extensions
4
+ def money_converter
5
+ Proc.new { |value| value.respond_to?(:to_money) ? value.to_money : Money.empty }
6
+ end
7
+
8
+ def money_constructor
9
+ Proc.new { |value| value.respond_to?(:to_money) && !value.blank? ? Money.new(value) : Money.empty }
10
+ end
11
+
12
+ def money_composed_column(*args)
13
+ options = args.extract_options!
14
+ args.each do |column_name|
15
+ composed_options = {:class_name => '::Money', :mapping => ["#{column_name}", "cents"],
16
+ :converter => money_converter, :constructor => money_constructor}.update(options)
17
+ composed_of column_name, composed_options
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,66 @@
1
+ module Carter
2
+ module Cart
3
+
4
+ def self.included(base)
5
+ base.send :include, InstanceMethods
6
+ base.send :include, Carter::StateMachine::Cart
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ module InstanceMethods
11
+ def cartables
12
+ Carter.settings.cartables.inject([]) do |result, cartable_type|
13
+ result.concat self.send(cartable_type.downcase.pluralize)
14
+ result
15
+ end
16
+ end
17
+
18
+ def on_checkout
19
+ if Carter.settings.on_checkout.is_a?(Proc)
20
+ Carter.settings.on_checkout.call(self)
21
+ end
22
+ end
23
+
24
+ # On success from checkout call the add event on each cart item.
25
+ def on_success
26
+ self.cart_items.each{|cart_item| cart_item.add_to_owner }
27
+ end
28
+
29
+ def on_failed
30
+ if Carter.settings.on_failed.is_a?(Proc)
31
+ Carter.settings.on_failed.call(self)
32
+ end
33
+ end
34
+
35
+ def in_cart?(cartable, owner=nil)
36
+ cart_item_for_cartable_and_owner(cartable, owner).nil?
37
+ end
38
+
39
+ def cart_item_for_cartable_and_owner(cartable, owner)
40
+ cart_items.for_cartable_and_owner(cartable, owner).first
41
+ end
42
+
43
+ def refresh
44
+ cart_items.each{|cart_item| cart_item.refresh }
45
+ end
46
+ end
47
+
48
+ module ClassMethods
49
+
50
+ def remove_carts(days=7, state=:active)
51
+ expiry_date = (Time.now.midnight - days.days.to_i)
52
+ count = 0
53
+ self.with_state(state).find_in_batches(:conditions => ["updated_at < ?", expiry_date]) do |batch|
54
+ count += self.delete(batch.map &:id)
55
+ end
56
+ count
57
+ end
58
+
59
+ # TODO doesn't work for polymorphic assoc
60
+ def add_cart_association(klass)
61
+ klass_key = klass.name.downcase.to_sym
62
+ self.has_many klass_key.to_s.pluralize.to_sym, :through => :cart_items, :source => :cartable, :source_type => klass_key.to_s
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,60 @@
1
+ module Carter
2
+ module ActiveRecord #:nodoc:
3
+ module Cartable #:nodoc:
4
+
5
+ # Options
6
+ # acts_as_cartable :name => "name_column", :price => "price_column", :unique => true / false
7
+ # :unique => true # this allows only one item of this type in the cart.
8
+ def acts_as_cartable(options = {})
9
+ configuration = { :name => "name", :price => "price", :unique => false }
10
+ configuration.update(options) if options.is_a?(Hash)
11
+ @cartable_configuration = configuration
12
+ has_many :cart_items, :as => :cartable
13
+ has_many :carts, :through => :cart_items
14
+ register_cartable(self)
15
+ include InstanceMethods
16
+ true
17
+ end
18
+
19
+ def cartable_configuration
20
+ @cartable_configuration ||= {}
21
+ end
22
+
23
+ protected
24
+
25
+ def register_cartable(klass)
26
+ Carter.settings.cartables = (Carter.settings.cartables << klass.name).uniq
27
+ # ::Cart.add_cart_association(klass)
28
+ end
29
+ end
30
+
31
+ module InstanceMethods
32
+
33
+ def cartable_price
34
+ self.send(cartable_configuration_value_by_key(:price))
35
+ end
36
+
37
+ def cartable_name
38
+ self.send(cartable_configuration_value_by_key(:name))
39
+ end
40
+
41
+ def in_cart?(cart, owner=nil)
42
+ !cart.cart_item_for_cartable_and_owner(self, owner).nil?
43
+ end
44
+
45
+ def allow_multiples?
46
+ !cartable_configuration_value_by_key(:unique)
47
+ end
48
+
49
+ def after_purchase_method
50
+ cartable_configuration_value_by_key :after_purchase_method
51
+ end
52
+
53
+ private
54
+
55
+ def cartable_configuration_value_by_key(key)
56
+ self.class.cartable_configuration[key]
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,86 @@
1
+ module Carter
2
+
3
+ # This module is automatically included into all controllers.
4
+
5
+ module ControllerAdditions
6
+ module ClassMethods
7
+
8
+ def has_cart(*args)
9
+ ControllerResource.add_before_filter(self, :load_cart, *args)
10
+ include InstanceMethods
11
+ end
12
+
13
+ def has_cart_for_checkout(*args)
14
+ ControllerResource.add_before_filter(self, :load_cart_for_checkout, *args)
15
+ include InstanceMethods
16
+ end
17
+
18
+ end
19
+
20
+ def self.included(base)
21
+ base.extend ClassMethods
22
+ base.class_inheritable_accessor :checking_out
23
+ base.class_inheritable_accessor :shopping
24
+ base.helper_method :cart, :has_cart?, :shopper
25
+ end
26
+
27
+ module InstanceMethods
28
+ def cart
29
+ @cart
30
+ end
31
+
32
+ def shopper
33
+ @shopper
34
+ end
35
+
36
+ def has_cart?
37
+ !@cart.nil?
38
+ end
39
+
40
+ def redirect_to_continue_shopping_or_default(default)
41
+ redirect_to continue_shopping_or_default_url(default)
42
+ session[:continue_shopping_url] = nil
43
+ end
44
+
45
+ def continue_shopping_or_default_url(default)
46
+ session[:continue_shopping_url] || default || root_url
47
+ end
48
+
49
+ # to stop the has_cart method called from super class from setting the continue_shopping_url when checking out.
50
+ def checking_out?
51
+ checking_out == true
52
+ end
53
+
54
+ def shopping?
55
+ shopping == true
56
+ end
57
+
58
+ protected
59
+
60
+ def find_cartable(cartable_id=params[:cartable_id])
61
+ @cartable = cartable_class.find_by_id(cartable_id)
62
+ end
63
+
64
+ def cartable_class(cartable_type=params[:cartable_type])
65
+ cartable_type.constantize
66
+ end
67
+
68
+ def add_cartable_to_cart(cartable, quantity = 1, owner = nil)
69
+ persist_cart
70
+ cart.add_item(cartable, quantity, owner)
71
+ end
72
+
73
+ def persist_cart
74
+ cart.save
75
+ session[:cart_id] = cart.id
76
+ end
77
+ end
78
+ end
79
+
80
+ end
81
+
82
+ if defined? ActionController
83
+ ActionController::Base.class_eval do
84
+ include Carter::ControllerAdditions
85
+ end
86
+ end