-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use create_if_necessary instead of a simple find_or_initialize #3494
Use create_if_necessary instead of a simple find_or_initialize #3494
Conversation
Please also read #3467. |
Anyway, I was trying to make something similar and the only reason why I don't like this change is that we are writing something into the database on a GET request, which is not ideal (and could be an easy endpoint to target for DoS attacks). I think another valid way to handle this would be redirecting the user to the |
I think it's legitimate to have an empty cart page, and I actually saw it on many stores, usually with links to a product-list or with inline suggestions. If we don't want to create an order upon visiting |
a5839ad
to
689392a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@elia interesting fix, I left a few notes, thank you.
context 'build_order_if_necessary option is true' do | ||
subject { controller.current_order(build_order_if_necessary: true) } | ||
|
||
it 'creates new order' do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since the order is not persisted, IMO this should read it builds a new order
instead.
include Spree::Core::ControllerHelpers::Store | ||
include Spree::Core::ControllerHelpers::Pricing | ||
include Spree::Core::ControllerHelpers::Auth | ||
include Spree::Core::ControllerHelpers::Order |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not evident to me why we need to include all these modules now, can you explain the reason?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously there was the FakesController class that was shared among all controller helpers spec files. During the file loading (but before the specs were ran) each helper was including itself into that class. So, at the time the specs were ready to run, the FakesController class had all the helpers included.
Now, instead, should be more clear on which helpers each helper depends, e.g. the Order
helper needs methods from Store
, Pricing
, and Auth
.
689392a
to
f220c19
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@elia thank you 👍
Restrict the number of options forwarded to #find_order_by_token_or_user since the only one it uses is :lock.
The mutation had no practical purpose and by avoiding it we also save some cpu cycles.
The same result can easily be obtained with the #controller helper. Prior to this change the specs were interdependent and would have worked only when loaded together, hiding the dependency of one helper module on the others.
If the order is being built it should keep setting the last_ip_address on the instance.
This allows to easily build valid orders with all the expected metadata.
Using current_order with the newly introduced `build_if_necessary` option creates orders that are much more complete by adding info about the ip, the creator, and other meta data. It's also more readable.
f220c19
to
030bf6c
Compare
@@ -39,7 +39,7 @@ def update | |||
|
|||
# Shows the current incomplete order from the session | |||
def edit | |||
@order = current_order || Spree::Order.incomplete.find_or_initialize_by(guest_token: cookies.signed[:guest_token]) | |||
@order = current_order(build_order_if_necessary: true) | |||
authorize! :read, @order, cookies.signed[:guest_token] | |||
associate_user |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work, thank you!
Coincidentally, I also researched current_order
behaviour and found some interesting points in legacy code.
- Remove reduntant
find_or_initialize_by
and just build order (you fixed it, great!) - Remove
associate_user
call in OrdersController#edit
That code was added in this commit spree/spree@a3d1f9a
In that version when you call current_order(true)
you create new order without any attributes (without current user, without store_id, without currency, without guest_token and so on, check it https://github.com/spree/spree/blob/a3d1f9acdbd5f0ec6c5eceb07116e30cd953948b/core/lib/spree/core/current_order.rb#L27). The logic of order creation was divided - in one place you create empty order, at another place you set necessary attributes such as user_id via associate_user
call. This mistake is still here for 8 years despite the fact that in current version necessary attributes are set at current_order
.
associate_user
call can be safely removed here.
P.S. associate_user
defined at this concern Spree::Core::ControllerHelpers::Order
and called only at two places - this one (reduntant) and before any action at Spree::CheckoutController
. Perhaps it is also reduntant because guest order associates with current user at Warden's after_set_user hook.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bitberryru thanks for the analysis and the hint!
So basically, if we inline associate_user
inside spree/orders#edit
we get this:
# Shows the current incomplete order from the session
def edit
@order = current_order(build_order_if_necessary: true)
authorize! :read, @order, cookies.signed[:guest_token]
# vvv Inlined version of #associate_user
if try_spree_current_user && (@order.user.blank? || @order.email.blank?)
@order.associate_user!(try_spree_current_user)
end
if params[:id] && @order.number != params[:id]
flash[:error] = t('spree.cannot_edit_orders')
redirect_to cart_path
end
end
The idea is that it's the responsibility of the authentication system to do the association in case the order was created before the login, and we can safely assume that we'll never encounter a persisted order that is authorized (via guest_token) and not already associated to the current user.
Here's the solidus_auth_devise warden callback:
# Merges users orders to their account after sign in and sign up.
Warden::Manager.after_set_user except: :fetch do |user, auth, _opts|
if auth.cookies.signed[:guest_token].present?
if user.is_a?(Spree::User)
Spree::Order.incomplete.where(guest_token: auth.cookies.signed[:guest_token], user_id: nil).each do |order|
order.associate_user!(user)
end
end
end
end
About the checkout controller seems like the code is executed in a different sequence:
class CheckoutController < Spree::StoreController
before_action :load_order # <<<< calls #current_order
around_action :lock_order
before_action :ensure_order_is_not_skipping_states
before_action :ensure_order_not_completed
before_action :ensure_checkout_allowed
before_action :ensure_sufficient_stock_lines
before_action :ensure_valid_state
before_action :associate_user # <<<< calls #associate_user
before_action :check_authorization # <<<< checks against the guest_token
before_action :apply_coupon_code
I'd like to more research to be sure not to break something for some store…
@kennyadsl @bitberryru what about dealing with removing #associate_user
in another PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, another PR is perfect for this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved the discussion in a separate issue: #3559
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Description
Using current_order with create_if_necessary creates order that are
much more complete adding info about the ip, creator and other meta
data that is better to have rather than not. Not to mention that it's
also more readable.
Fixes #3467
I did a historical check and no specific reason for doing it this way came about.
Checklist: