stall 0.1.1 → 0.1.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.
- 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
|