stall 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +45 -0
- data/app/assets/javascripts/stall.coffee +2 -0
- data/app/assets/javascripts/stall/cart-form.coffee +23 -0
- data/app/controllers/stall/application_controller.rb +10 -2
- data/app/controllers/stall/carts_controller.rb +31 -10
- data/app/controllers/stall/checkout/steps_controller.rb +36 -4
- data/app/controllers/stall/checkouts_controller.rb +12 -3
- data/app/controllers/stall/line_items_controller.rb +3 -3
- data/app/controllers/stall/payments_controller.rb +5 -3
- data/app/helpers/stall/add_to_cart_helper.rb +1 -1
- data/app/helpers/stall/checkout_helper.rb +22 -2
- data/app/helpers/stall/payments_helper.rb +8 -0
- data/app/mailers/stall/admin_mailer.rb +11 -0
- data/app/mailers/stall/base_mailer.rb +17 -0
- data/app/mailers/stall/customer_mailer.rb +13 -0
- data/app/models/address.rb +3 -0
- data/app/models/address_ownership.rb +3 -0
- data/app/models/adjustment.rb +3 -0
- data/app/models/cart.rb +3 -0
- data/app/models/customer.rb +3 -0
- data/app/models/line_item.rb +3 -0
- data/app/models/payment.rb +3 -0
- data/app/models/payment_method.rb +3 -0
- data/app/models/product_list.rb +3 -0
- data/app/models/shipment.rb +3 -0
- data/app/models/shipping_method.rb +3 -0
- data/app/models/stall/models.rb +4 -0
- data/app/models/stall/models/address.rb +45 -0
- data/app/models/stall/models/address_ownership.rb +26 -0
- data/app/models/stall/models/adjustment.rb +49 -0
- data/app/models/stall/models/cart.rb +37 -0
- data/app/models/stall/models/customer.rb +18 -0
- data/app/models/stall/models/line_item.rb +57 -0
- data/app/models/stall/models/payment.rb +33 -0
- data/app/models/stall/models/payment_method.rb +18 -0
- data/app/models/stall/models/product_list.rb +147 -0
- data/app/models/stall/models/shipment.rb +29 -0
- data/app/models/stall/models/shipping_method.rb +16 -0
- data/app/services/stall/add_to_cart_service.rb +11 -1
- data/app/services/stall/cart_update_service.rb +26 -0
- data/app/services/stall/payment_notification_service.rb +23 -9
- data/app/services/stall/shipping_fee_calculator_service.rb +13 -2
- data/app/views/checkout/steps/_informations.html.haml +3 -3
- data/app/views/checkout/steps/_payment.html.haml +25 -4
- data/app/views/checkout/steps/_payment_method.html.haml +1 -3
- data/app/views/checkout/steps/_payment_return.html.haml +6 -0
- data/app/views/checkout/steps/_shipping_method.html.haml +1 -1
- data/app/views/stall/admin_mailer/order_paid_email.html.haml +4 -0
- data/app/views/stall/carts/show.html.haml +36 -8
- data/app/views/stall/customer_mailer/order_paid_email.html.haml +8 -0
- data/app/views/stall/line_items/_added.html.haml +1 -1
- data/app/views/stall/shared/mailers/_address.html.haml +18 -0
- data/app/views/stall/shared/mailers/_cart.html.haml +114 -0
- data/config/locales/stall.fr.yml +90 -8
- data/db/migrate/20160304134849_change_all_json_columns_to_jsonb.rb +18 -0
- data/db/migrate/20160307142924_add_state_to_stall_addresses.rb +5 -0
- data/db/migrate/20160308142713_create_adjustments.rb +16 -0
- data/db/migrate/20160309165136_add_identifier_to_stall_product_lists.rb +5 -0
- data/db/migrate/20160316114649_add_data_to_stall_shipments.rb +5 -0
- data/db/migrate/20160317141632_add_state_to_stall_shipments.rb +5 -0
- data/db/migrate/20160629102943_add_active_to_stall_shipping_methods.rb +11 -0
- data/db/migrate/20160629104617_add_active_to_stall_payment_methods.rb +11 -0
- data/db/migrate/20160705110151_add_locale_to_stall_customers.rb +5 -0
- data/lib/generators/stall/checkout/step/templates/step.html.haml.erb +1 -1
- data/lib/generators/stall/checkout/step/templates/step.rb.erb +10 -2
- data/lib/generators/stall/checkout/wizard/templates/wizard.rb.erb +1 -1
- data/lib/generators/stall/install/templates/initializer.rb +85 -1
- data/lib/generators/stall/model/model_generator.rb +27 -0
- data/lib/generators/stall/service/service_generator.rb +39 -0
- data/lib/generators/stall/service/templates/service.rb.erb +16 -0
- data/lib/generators/stall/shipping/calculator/calculator_generator.rb +17 -0
- data/lib/generators/stall/shipping/calculator/templates/calculator.rb.erb +29 -0
- data/lib/stall.rb +4 -0
- data/lib/stall/addressable.rb +28 -2
- data/lib/stall/cart_helper.rb +84 -0
- data/lib/stall/carts_cleaner.rb +46 -0
- data/lib/stall/checkout.rb +2 -0
- data/lib/stall/checkout/informations_checkout_step.rb +3 -5
- data/lib/stall/checkout/payment_checkout_step.rb +3 -0
- data/lib/stall/checkout/payment_return_checkout_step.rb +11 -0
- data/lib/stall/checkout/shipping_method_checkout_step.rb +2 -1
- data/lib/stall/checkout/step.rb +47 -11
- data/lib/stall/checkout/step_form.rb +71 -0
- data/lib/stall/checkout/wizard.rb +15 -5
- data/lib/stall/config.rb +51 -0
- data/lib/stall/engine.rb +24 -2
- data/lib/stall/payable.rb +26 -0
- data/lib/stall/payments.rb +7 -3
- data/lib/stall/payments/config.rb +37 -0
- data/lib/stall/payments/fake_gateway_payment_notification.rb +34 -0
- data/lib/stall/payments/gateway.rb +19 -12
- data/lib/stall/payments/urls_config.rb +40 -0
- data/lib/stall/priceable.rb +7 -0
- data/lib/stall/rails/currency_helper.rb +1 -1
- data/lib/stall/routes.rb +17 -4
- data/lib/stall/sellable.rb +1 -2
- data/lib/stall/shipping.rb +3 -1
- data/lib/stall/shipping/calculator.rb +36 -3
- data/lib/stall/shipping/config.rb +17 -3
- data/lib/stall/shipping/country_weight_table_calculator.rb +1 -1
- data/lib/stall/shipping/free_shipping_calculator.rb +1 -1
- data/lib/stall/utils.rb +2 -2
- data/lib/stall/utils/config_dsl.rb +5 -1
- data/lib/stall/version.rb +1 -1
- data/lib/tasks/stall_tasks.rake +11 -0
- metadata +73 -15
- data/app/assets/javascripts/stall/application.js +0 -13
- data/app/assets/javascripts/stall/carts.js +0 -2
- data/app/helpers/stall/cart_helper.rb +0 -28
- data/app/models/stall/address.rb +0 -5
- data/app/models/stall/address_ownership.rb +0 -8
- data/app/models/stall/cart.rb +0 -31
- data/app/models/stall/customer.rb +0 -8
- data/app/models/stall/line_item.rb +0 -49
- data/app/models/stall/payment.rb +0 -14
- data/app/models/stall/payment_method.rb +0 -7
- data/app/models/stall/product_list.rb +0 -68
- data/app/models/stall/shipment.rb +0 -15
- data/app/models/stall/shipping_method.rb +0 -5
@@ -0,0 +1,16 @@
|
|
1
|
+
class <%= service_class_name %> < Stall::<%= service_class_name %>
|
2
|
+
# The call call method is the entry point for the service to process the data
|
3
|
+
# it was given at initialization
|
4
|
+
#
|
5
|
+
# Many overrides would want to call `super` to let the service do its
|
6
|
+
# classical job, then do something depending on the success or failure of
|
7
|
+
# the original work. This pattern is shown below.
|
8
|
+
#
|
9
|
+
# def call
|
10
|
+
# if super
|
11
|
+
# # Do something when it worked
|
12
|
+
# else
|
13
|
+
# # Do something when it didn't
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Stall
|
2
|
+
module Shipping
|
3
|
+
class CalculatorGenerator < Rails::Generators::NamedBase
|
4
|
+
source_root File.expand_path('../templates', __FILE__)
|
5
|
+
|
6
|
+
def copy_calculator_template
|
7
|
+
template 'calculator.rb.erb', "lib/#{ file_path }.rb"
|
8
|
+
end
|
9
|
+
|
10
|
+
def register_calculator_in_initializer
|
11
|
+
insert_into_file "config/initializers/stall.rb", after: "Stall.configure do |config|\n" do
|
12
|
+
indent "config.shipping.register_calculator :#{ singular_name }, #{ class_name }\n"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class <%= class_name %> < Stall::Shipping::Calculator
|
2
|
+
# Use this method to handle the related shipping method availibility depending
|
3
|
+
# on the cart data available to the calculator
|
4
|
+
#
|
5
|
+
def available?
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
# Write your logic here and return the price for the current cart
|
10
|
+
#
|
11
|
+
# You can access `cart` and `shipping_method` (the model) here.
|
12
|
+
#
|
13
|
+
def price
|
14
|
+
0
|
15
|
+
end
|
16
|
+
|
17
|
+
# Allows you to override how calculator's eot_price price is calculated
|
18
|
+
#
|
19
|
+
# def eot_price
|
20
|
+
# price / (1 + (vat_rate / 100.0))
|
21
|
+
# end
|
22
|
+
|
23
|
+
# Allows you to override the VAT for the calculator which allows calculating
|
24
|
+
# the EOT price
|
25
|
+
#
|
26
|
+
# def vat_rate
|
27
|
+
# Stall.config.vat_rate
|
28
|
+
# end
|
29
|
+
end
|
data/lib/stall.rb
CHANGED
@@ -5,6 +5,7 @@ require 'request_store'
|
|
5
5
|
require 'haml-rails'
|
6
6
|
require 'simple_form'
|
7
7
|
require 'country_select'
|
8
|
+
require 'cocoon'
|
8
9
|
|
9
10
|
require 'stall/rails/routing_mapper'
|
10
11
|
require 'stall/rails/currency_helper'
|
@@ -15,12 +16,15 @@ module Stall
|
|
15
16
|
|
16
17
|
autoload :Sellable
|
17
18
|
autoload :Addressable
|
19
|
+
autoload :Priceable
|
20
|
+
autoload :Payable
|
18
21
|
|
19
22
|
autoload :Checkout
|
20
23
|
autoload :Shipping
|
21
24
|
autoload :Payments
|
22
25
|
|
23
26
|
autoload :Routes
|
27
|
+
autoload :CartHelper
|
24
28
|
autoload :Config
|
25
29
|
autoload :Utils
|
26
30
|
|
data/lib/stall/addressable.rb
CHANGED
@@ -6,7 +6,9 @@ module Stall
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
8
|
included do
|
9
|
-
has_many :address_ownerships, as: :addressable, dependent: :destroy
|
9
|
+
has_many :address_ownerships, as: :addressable, dependent: :destroy,
|
10
|
+
inverse_of: :addressable
|
11
|
+
|
10
12
|
has_many :addresses, through: :address_ownerships
|
11
13
|
accepts_nested_attributes_for :address_ownerships, allow_destroy: true
|
12
14
|
end
|
@@ -26,7 +28,16 @@ module Stall
|
|
26
28
|
define_method(:"#{ type }_address=") do |address|
|
27
29
|
ownership = address_ownership_for(type) || address_ownerships.build(type => true)
|
28
30
|
ownership.address = address
|
29
|
-
|
31
|
+
instance_variable_set(instance_variable_name, address)
|
32
|
+
end
|
33
|
+
|
34
|
+
define_method(:"build_#{ type }_address") do |attributes = {}|
|
35
|
+
if (ownership = address_ownership_for(type))
|
36
|
+
ownership.send(:"#{ type }=", false)
|
37
|
+
end
|
38
|
+
|
39
|
+
ownership = address_ownerships.build(type => true)
|
40
|
+
address = ownership.build_address(attributes)
|
30
41
|
instance_variable_set(instance_variable_name, address)
|
31
42
|
end
|
32
43
|
|
@@ -39,6 +50,21 @@ module Stall
|
|
39
50
|
instance_variable_set(instance_variable_name, ownership.address)
|
40
51
|
end
|
41
52
|
end
|
53
|
+
|
54
|
+
define_method(:"#{ type }_address_attributes=") do |attributes|
|
55
|
+
attributes.delete(:id)
|
56
|
+
|
57
|
+
ownership = address_ownership_for(type) || address_ownerships.build(type => true)
|
58
|
+
address = ownership.address || ownership.build_address
|
59
|
+
address.assign_attributes(attributes)
|
60
|
+
instance_variable_set(instance_variable_name, address)
|
61
|
+
end
|
62
|
+
|
63
|
+
define_method(:"mark_address_ownership_as_#{ type }") do |ownership|
|
64
|
+
ownership.send(:"mark_as_#{ type }").tap do
|
65
|
+
instance_variable_set(instance_variable_name, ownership.address)
|
66
|
+
end
|
67
|
+
end
|
42
68
|
end
|
43
69
|
end
|
44
70
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Stall
|
2
|
+
module CartHelper
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
if respond_to?(:helper_method)
|
7
|
+
helper_method :current_cart, :current_cart_key
|
8
|
+
end
|
9
|
+
|
10
|
+
if respond_to?(:after_action)
|
11
|
+
after_action :store_cart_to_cookies
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def current_cart
|
16
|
+
RequestStore.store[cart_key] ||= load_current_cart
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def current_cart_key
|
22
|
+
params[:cart_key].try(:to_sym) || :default
|
23
|
+
end
|
24
|
+
|
25
|
+
def load_current_cart(identifier = current_cart_key)
|
26
|
+
if (cart = find_cart(identifier))
|
27
|
+
return prepare_cart(cart)
|
28
|
+
end
|
29
|
+
|
30
|
+
# If no token was stored or the token does not exist anymore, create a
|
31
|
+
# new cart and store the new token
|
32
|
+
#
|
33
|
+
prepare_cart(cart_class.new(identifier: identifier))
|
34
|
+
end
|
35
|
+
|
36
|
+
def prepare_cart(cart)
|
37
|
+
cart.tap do |cart|
|
38
|
+
# Keep track of potential customer locale switching to allow e-mailing
|
39
|
+
# him in his last used locale
|
40
|
+
cart.customer.locale = I18n.locale if cart.customer
|
41
|
+
|
42
|
+
# Only update locale change for existing carts. New carts don't need
|
43
|
+
# to be saved, avoiding each robot or simple visitors to create a
|
44
|
+
# cart on large shops.
|
45
|
+
cart.save unless cart.new_record?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def find_cart(identifier)
|
50
|
+
if (cart_token = cookies.encrypted[cart_key(identifier)])
|
51
|
+
if (current_cart = ProductList.find_by_token(cart_token)) && current_cart.active?
|
52
|
+
return current_cart
|
53
|
+
else
|
54
|
+
# Remove any cart that can't be fetched, either because it's already
|
55
|
+
# paid, or because it was cleaned out
|
56
|
+
remove_cart_from_cookies(identifier)
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def remove_cart_from_cookies(identifier = current_cart_key)
|
63
|
+
cookies.delete(cart_key(identifier))
|
64
|
+
end
|
65
|
+
|
66
|
+
def cart_key(identifier = current_cart_key)
|
67
|
+
['stall', 'cart', identifier.to_s].join('.')
|
68
|
+
end
|
69
|
+
|
70
|
+
def cart_class
|
71
|
+
Cart
|
72
|
+
end
|
73
|
+
|
74
|
+
def store_cart_to_cookies
|
75
|
+
if current_cart.persisted?
|
76
|
+
store_cart_cookie_for(current_cart.identifier, current_cart)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def store_cart_cookie_for(identifier, cart)
|
81
|
+
cookies.encrypted.permanent[cart_key(identifier)] = cart.token
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Stall
|
2
|
+
class CartsCleaner
|
3
|
+
attr_reader :cart_model
|
4
|
+
|
5
|
+
def initialize(cart_model)
|
6
|
+
@cart_model = cart_model
|
7
|
+
end
|
8
|
+
|
9
|
+
def clean!
|
10
|
+
clean_empty_carts
|
11
|
+
clean_aborted_carts
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# Empty carts are cleaned with a .delete_all call for optimization.
|
17
|
+
#
|
18
|
+
# Empty carts should not need to run destroy callbacks, not being related to
|
19
|
+
# any external model.
|
20
|
+
#
|
21
|
+
def clean_empty_carts
|
22
|
+
carts = cart_model.empty.older_than(Stall.config.empty_carts_expires_after.ago)
|
23
|
+
|
24
|
+
log "Cleaning #{ carts.count } empty carts ..."
|
25
|
+
carts.delete_all
|
26
|
+
log "Done."
|
27
|
+
end
|
28
|
+
|
29
|
+
# Unpaid carts have line items and other models related. Since empty carts
|
30
|
+
# are already cleaned, there should not be too many carts to destroy here
|
31
|
+
# and we can safely use .destroy_all to deeply clean carts.
|
32
|
+
#
|
33
|
+
# Note : The given cart model should implement the `.unpaid` method
|
34
|
+
def clean_aborted_carts
|
35
|
+
carts = cart_model.aborted(before: Stall.config.aborted_carts_expires_after.ago)
|
36
|
+
|
37
|
+
log "Cleaning #{ carts.count } aborted carts ..."
|
38
|
+
carts.destroy_all
|
39
|
+
log "Done."
|
40
|
+
end
|
41
|
+
|
42
|
+
def log(*args)
|
43
|
+
puts(*args) unless Rails.env.test?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/stall/checkout.rb
CHANGED
@@ -4,11 +4,13 @@ module Stall
|
|
4
4
|
|
5
5
|
autoload :Wizard
|
6
6
|
autoload :Step
|
7
|
+
autoload :StepForm
|
7
8
|
|
8
9
|
autoload :InformationsCheckoutStep
|
9
10
|
autoload :ShippingMethodCheckoutStep
|
10
11
|
autoload :PaymentMethodCheckoutStep
|
11
12
|
autoload :PaymentCheckoutStep
|
13
|
+
autoload :PaymentReturnCheckoutStep
|
12
14
|
|
13
15
|
class WizardNotFoundError < StandardError; end
|
14
16
|
end
|
@@ -10,7 +10,7 @@ module Stall
|
|
10
10
|
def process
|
11
11
|
cart.assign_attributes(cart_params)
|
12
12
|
process_addresses
|
13
|
-
cart.save
|
13
|
+
cart.save if valid?
|
14
14
|
end
|
15
15
|
|
16
16
|
private
|
@@ -20,10 +20,8 @@ module Stall
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def ensure_address(type)
|
23
|
-
|
24
|
-
|
25
|
-
ownership.build_address
|
26
|
-
end
|
23
|
+
ownership = cart.address_ownership_for(type) || cart.address_ownerships.build(type => true)
|
24
|
+
ownership.address || ownership.build_address
|
27
25
|
end
|
28
26
|
|
29
27
|
def process_addresses
|
data/lib/stall/checkout/step.rb
CHANGED
@@ -3,11 +3,10 @@ module Stall
|
|
3
3
|
class StepNotFoundError < StandardError; end
|
4
4
|
|
5
5
|
class Step
|
6
|
-
attr_reader :cart
|
6
|
+
attr_reader :cart
|
7
7
|
|
8
|
-
def initialize(cart
|
8
|
+
def initialize(cart)
|
9
9
|
@cart = cart
|
10
|
-
@params = params
|
11
10
|
end
|
12
11
|
|
13
12
|
# Allow injecting dependencies on step initialization and accessing
|
@@ -25,32 +24,69 @@ module Stall
|
|
25
24
|
end
|
26
25
|
|
27
26
|
def process
|
28
|
-
|
27
|
+
save
|
29
28
|
end
|
30
29
|
|
31
30
|
def cart_params
|
32
|
-
@cart_params ||= params
|
31
|
+
@cart_params ||= if params[:cart]
|
32
|
+
params.require(:cart).permit!
|
33
|
+
else
|
34
|
+
{}.with_indifferent_access
|
35
|
+
end
|
33
36
|
end
|
34
37
|
|
35
38
|
def skip?
|
36
39
|
false
|
37
40
|
end
|
38
41
|
|
42
|
+
def is?(key)
|
43
|
+
identifier == key
|
44
|
+
end
|
45
|
+
|
46
|
+
def identifier
|
47
|
+
@identifier ||= begin
|
48
|
+
class_name = self.class.name.demodulize
|
49
|
+
class_name.gsub(/CheckoutStep$/, '').underscore.to_sym
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Abstracts the simple case of assigning the submitted parameters to the
|
54
|
+
# cart object, running the step validations and saving the cart
|
55
|
+
def save
|
56
|
+
cart.assign_attributes(cart_params)
|
57
|
+
cart.save if valid?
|
58
|
+
end
|
59
|
+
|
39
60
|
# Handles conversion from an identifier to a checkout step class, allowing
|
40
61
|
# us to specify a list of symbols in our wizard's .step macro
|
41
62
|
#
|
42
63
|
def self.for(identifier)
|
43
64
|
name = identifier.to_s.camelize
|
44
65
|
step_name = [name, 'CheckoutStep'].join
|
66
|
+
# Try loading step from app
|
67
|
+
step = Stall::Utils.try_load_constant(step_name)
|
68
|
+
# Try loading step from stall core or lib if not found in app
|
69
|
+
step = Stall::Utils.try_load_constant(
|
70
|
+
['Stall', 'Checkout', step_name.demodulize].join('::')
|
71
|
+
) unless step
|
72
|
+
|
73
|
+
unless step
|
74
|
+
raise StepNotFoundError,
|
75
|
+
"No checkout step was found for #{ identifier }. You can generate " +
|
76
|
+
"it with `rails g stall:checkout:step #{ identifier }`"
|
77
|
+
end
|
45
78
|
|
46
|
-
step
|
47
|
-
|
79
|
+
step
|
80
|
+
end
|
48
81
|
|
49
|
-
|
82
|
+
def self.validations(&block)
|
83
|
+
return @validations unless block
|
84
|
+
@validations = Stall::Checkout::StepForm.build(&block)
|
85
|
+
end
|
50
86
|
|
51
|
-
|
52
|
-
|
53
|
-
|
87
|
+
def valid?
|
88
|
+
return true unless (validations = self.class.validations)
|
89
|
+
validations.new(cart, self).validate
|
54
90
|
end
|
55
91
|
end
|
56
92
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Stall
|
2
|
+
module Checkout
|
3
|
+
class StepForm
|
4
|
+
include ActiveModel::Validations
|
5
|
+
|
6
|
+
class_attribute :nested_forms
|
7
|
+
|
8
|
+
attr_reader :object, :step
|
9
|
+
|
10
|
+
delegate :errors, to: :object
|
11
|
+
|
12
|
+
def initialize(object, step)
|
13
|
+
@object = object
|
14
|
+
@step = step
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate
|
18
|
+
super && validate_nested_forms
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.nested(type, &block)
|
22
|
+
self.nested_forms ||= {}
|
23
|
+
nested_forms[type] = build(&block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.build(&block)
|
27
|
+
Class.new(StepForm, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def method_missing(method, *args, &block)
|
31
|
+
if object.respond_to?(method, true)
|
32
|
+
object.send(method, *args, &block)
|
33
|
+
elsif step.respond_to?(method, true)
|
34
|
+
step.send(method, *args, &block)
|
35
|
+
else
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Override model name instanciation to add a name, since the form classes
|
41
|
+
# are anonymous, and ActiveModel::Name does not support unnamed classes
|
42
|
+
def model_name
|
43
|
+
@model_name ||= ActiveModel::Name.new(self, nil, object.class.name)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# Validates all registered nested forms
|
49
|
+
#
|
50
|
+
# Note : We use `forms.map.all?` instead if `forms.all?` to ensure
|
51
|
+
# all the validations are called and the iteration does not stop as soon
|
52
|
+
# as a validation fails
|
53
|
+
#
|
54
|
+
def validate_nested_forms
|
55
|
+
# If no nested forms are present in the class, just return true since
|
56
|
+
# no validation should be tested
|
57
|
+
return true unless self.class.nested_forms
|
58
|
+
|
59
|
+
# Run all validations on all nested forms and ensure they're all valid
|
60
|
+
self.class.nested_forms.map do |name, form|
|
61
|
+
if object.respond_to?(name) && (model = object.send(name))
|
62
|
+
Array.wrap(model).map { |m| form.new(m, step).validate }.all?
|
63
|
+
else
|
64
|
+
# Nested validations shouldn't be run on undefined relations
|
65
|
+
true
|
66
|
+
end
|
67
|
+
end.all?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|