@@ -43,7 +43,7 @@
class CheckoutSessionMixin (object ):
"""
Mixin to provide common functionality shared between checkout views.
"""
"""
def dispatch (self , request , * args , ** kwargs ):
self .checkout_session = CheckoutSessionData (request )
@@ -52,9 +52,9 @@ def dispatch(self, request, *args, **kwargs):
def get_shipping_address (self ):
"""
Return the current shipping address for this checkout session.
This could either be a ShippingAddress model which has been
pre-populated (not saved), or a UserAddress model which will
pre-populated (not saved), or a UserAddress model which will
need converting into a ShippingAddress model at submission
"""
addr_data = self .checkout_session .new_shipping_address_fields ()
@@ -70,7 +70,7 @@ def get_shipping_address(self):
# session data that refers to addresses that no longer exist
pass
return None
def get_shipping_method (self , basket = None ):
method = self .checkout_session .shipping_method ()
if method :
@@ -81,7 +81,7 @@ def get_shipping_method(self, basket=None):
# We default to using free shipping
method = Free ()
return method
def get_order_totals (self , basket = None , shipping_method = None , ** kwargs ):
"""
Returns the total for the order with and without tax (as a tuple)
@@ -94,22 +94,22 @@ def get_order_totals(self, basket=None, shipping_method=None, **kwargs):
total_incl_tax = calc .order_total_incl_tax (basket , shipping_method , ** kwargs )
total_excl_tax = calc .order_total_excl_tax (basket , shipping_method , ** kwargs )
return total_incl_tax , total_excl_tax
def get_context_data (self , ** kwargs ):
"""
Assign common template variables to the context.
"""
ctx = super (CheckoutSessionMixin , self ).get_context_data (** kwargs )
ctx ['shipping_address' ] = self .get_shipping_address ()
method = self .get_shipping_method ()
if method :
ctx ['shipping_method' ] = method
ctx ['shipping_total_excl_tax' ] = method .basket_charge_excl_tax ()
ctx ['shipping_total_incl_tax' ] = method .basket_charge_incl_tax ()
ctx ['order_total_incl_tax' ], ctx ['order_total_excl_tax' ] = self .get_order_totals ()
return ctx
@@ -162,19 +162,19 @@ def get_success_url(self):
class ShippingAddressView (CheckoutSessionMixin , FormView ):
"""
Determine the shipping address for the order.
The default behaviour is to display a list of addresses from the users's
address book, from which the user can choose one to be their shipping address.
They can add/edit/delete these USER addresses. This address will be
automatically converted into a SHIPPING address when the user checks out.
Alternatively, the user can enter a SHIPPING address directly which will be
saved in the session and saved as a model when the order is sucessfully submitted.
"""
template_name = 'checkout/shipping_address.html'
form_class = ShippingAddressForm
def get (self , request , * args , ** kwargs ):
# Check that guests have entered an email address
if not request .user .is_authenticated () and not self .checkout_session .get_guest_email ():
@@ -187,6 +187,8 @@ def get(self, request, *args, **kwargs):
messages .info (request , _ ("Your basket does not require a shipping address to be submitted" ))
self .checkout_session .no_shipping_required ()
return HttpResponseRedirect (self .get_success_url ())
else :
self .checkout_session .shipping_required ()
return super (ShippingAddressView , self ).get (request , * args , ** kwargs )
@@ -201,17 +203,17 @@ def does_basket_require_shipping(self, basket):
def get_initial (self ):
return self .checkout_session .new_shipping_address_fields ()
def get_context_data (self , ** kwargs ):
kwargs = super (ShippingAddressView , self ).get_context_data (** kwargs )
if self .request .user .is_authenticated ():
# Look up address book data
kwargs ['addresses' ] = self .get_available_addresses ()
return kwargs
def get_available_addresses (self ):
return UserAddress ._default_manager .filter (user = self .request .user ).order_by ('-is_default_for_shipping' )
def post (self , request , * args , ** kwargs ):
# Check if a shipping address was selected directly (eg no form was filled in)
if self .request .user .is_authenticated and 'address_id' in self .request .POST :
@@ -228,15 +230,15 @@ def post(self, request, *args, **kwargs):
return HttpResponseBadRequest ()
else :
return super (ShippingAddressView , self ).post (request , * args , ** kwargs )
def form_valid (self , form ):
# Store the address details in the session and redirect to next step
self .checkout_session .ship_to_new_address (form .clean ())
return super (ShippingAddressView , self ).form_valid (form )
def get_success_url (self ):
return reverse ('checkout:shipping-method' )
class UserAddressCreateView (CheckoutSessionMixin , CreateView ):
"""
@@ -252,7 +254,7 @@ def get_context_data(self, **kwargs):
kwargs = super (UserAddressCreateView , self ).get_context_data (** kwargs )
kwargs ['form_url' ] = reverse ('checkout:user-address-create' )
return kwargs
def form_valid (self , form ):
self .object = form .save (commit = False )
self .object .user = self .request .user
@@ -263,15 +265,15 @@ def get_success_response(self):
messages .info (self .request , _ ("Address saved" ))
# We redirect back to the shipping address page
return HttpResponseRedirect (reverse ('checkout:shipping-address' ))
class UserAddressUpdateView (CheckoutSessionMixin , UpdateView ):
"""
Update a user address
"""
template_name = 'checkout/user_address_form.html'
form_class = UserAddressForm
def get_queryset (self ):
return UserAddress ._default_manager .filter (user = self .request .user )
@@ -283,8 +285,8 @@ def get_context_data(self, **kwargs):
def get_success_url (self ):
messages .info (self .request , _ ("Address saved" ))
return reverse ('checkout:shipping-address' )
class UserAddressDeleteView (CheckoutSessionMixin , DeleteView ):
"""
Delete an address from a user's addressbook.
@@ -293,31 +295,31 @@ class UserAddressDeleteView(CheckoutSessionMixin, DeleteView):
def get_queryset (self ):
return UserAddress ._default_manager .filter (user = self .request .user )
def get_success_url (self ):
messages .info (self .request , _ ("Address deleted" ))
return reverse ('checkout:shipping-address' )
# ===============
# ===============
# Shipping method
# ===============
# ===============
class ShippingMethodView (CheckoutSessionMixin , TemplateView ):
"""
View for allowing a user to choose a shipping method.
Shipping methods are largely domain-specific and so this view
will commonly need to be subclassed and customised.
The default behaviour is to load all the available shipping methods
using the shipping Repository. If there is only 1, then it is
using the shipping Repository. If there is only 1, then it is
automatically selected. Otherwise, a page is rendered where
the user can choose the appropriate one.
"""
template_name = 'checkout/shipping_methods.html' ;
def get (self , request , * args , ** kwargs ):
# Check that shipping is required at all
if not self .checkout_session .is_shipping_required ():
@@ -354,13 +356,13 @@ def get_available_shipping_methods(self):
"""
Returns all applicable shipping method objects
for a given basket.
"""
"""
# Shipping methods can depend on the user, the contents of the basket
# and the shipping address. I haven't come across a scenario that doesn't
# fit this system.
return Repository ().get_shipping_methods (self .request .user , self .request .basket ,
return Repository ().get_shipping_methods (self .request .user , self .request .basket ,
self .get_shipping_address ())
def post (self , request , * args , ** kwargs ):
# Need to check that this code is valid for this user
method_code = request .POST .get ('method_code' , None )
@@ -376,7 +378,7 @@ def post(self, request, *args, **kwargs):
# and continue to the next step.
self .checkout_session .use_shipping_method (method_code )
return self .get_success_response ()
def get_success_response (self ):
return HttpResponseRedirect (reverse ('checkout:payment-method' ))
@@ -389,11 +391,11 @@ def get_success_response(self):
class PaymentMethodView (CheckoutSessionMixin , TemplateView ):
"""
View for a user to choose which payment method(s) they want to use.
This would include setting allocations if payment is to be split
between multiple sources.
"""
def get (self , request , * args , ** kwargs ):
# Check that shipping address has been completed
if self .checkout_session .is_shipping_required () and not self .checkout_session .is_shipping_address_set ():
@@ -405,7 +407,7 @@ def get(self, request, *args, **kwargs):
return HttpResponseRedirect (reverse ('checkout:shipping-method' ))
return self .get_success_response ()
def get_success_response (self ):
return HttpResponseRedirect (reverse ('checkout:payment-details' ))
@@ -427,50 +429,50 @@ class OrderPlacementMixin(CheckoutSessionMixin):
_payment_events = None
# Default code for the email to send after successful checkout
communication_type_code = 'ORDER_PLACED'
def handle_order_placement (self , order_number , basket , total_incl_tax , total_excl_tax , ** kwargs ):
communication_type_code = 'ORDER_PLACED'
def handle_order_placement (self , order_number , basket , total_incl_tax , total_excl_tax , ** kwargs ):
"""
Write out the order models and return the appropriate HTTP response
We deliberately pass the basket in here as the one tied to the request
isn't necessarily the correct one to use in placing the order. This can
happen when a basket gets frozen.
"""
"""
order = self .place_order (order_number , basket , total_incl_tax , total_excl_tax , ** kwargs )
basket .set_as_submitted ()
return self .handle_successful_order (order )
def add_payment_source (self , source ):
if self ._payment_sources is None :
self ._payment_sources = []
self ._payment_sources .append (source )
self ._payment_sources .append (source )
def add_payment_event (self , event_type_name , amount ):
event_type , n = PaymentEventType .objects .get_or_create (name = event_type_name )
if self ._payment_events is None :
self ._payment_events = []
event = PaymentEvent (event_type = event_type , amount = amount )
self ._payment_events .append (event )
def handle_successful_order (self , order ):
def handle_successful_order (self , order ):
"""
Handle the various steps required after an order has been successfully placed.
Override this view if you want to perform custom actions when an
order is submitted.
"""
"""
# Send confirmation message (normally an email)
self .send_confirmation_message (order )
# Flush all session data
self .checkout_session .flush ()
# Save order id in session so thank-you page can load it
self .request .session ['checkout_order_id' ] = order .id
return HttpResponseRedirect (reverse ('checkout:thank-you' ))
def place_order (self , order_number , basket , total_incl_tax , total_excl_tax , ** kwargs ):
"""
Writes the order out to the DB including the payment models
@@ -501,16 +503,16 @@ def place_order(self, order_number, basket, total_incl_tax, total_excl_tax, **kw
** kwargs )
self .save_payment_details (order )
return order
def create_shipping_address (self ):
"""
Create and returns the shipping address for the current order.
If the shipping address was entered manually, then we simply
write out a ShippingAddress model with the appropriate form data. If
the user is authenticated, then we create a UserAddress from this data
too so it can be re-used in the future.
too so it can be re-used in the future.
If the shipping address was selected from the user's address book,
then we convert the UserAddress to a ShippingAddress.
"""
@@ -527,16 +529,16 @@ def create_shipping_address(self):
else :
raise AttributeError ("No shipping address data found" )
return addr
def create_shipping_address_from_form_fields (self , addr_data ):
"""Creates a shipping address model from the saved form fields"""
shipping_addr = ShippingAddress (** addr_data )
shipping_addr .save ()
shipping_addr .save ()
return shipping_addr
def create_user_address (self , addr_data ):
"""
For signed-in users, we create a user address model which will go
For signed-in users, we create a user address model which will go
into their address book.
"""
if self .request .user .is_authenticated ():
@@ -548,19 +550,19 @@ def create_user_address(self, addr_data):
UserAddress ._default_manager .get (hash = user_addr .generate_hash ())
except ObjectDoesNotExist :
user_addr .save ()
def create_shipping_address_from_user_address (self , addr_id ):
"""Creates a shipping address from a user address"""
address = UserAddress ._default_manager .get (pk = addr_id )
# Increment the number of orders to help determine popularity of orders
# Increment the number of orders to help determine popularity of orders
address .num_orders += 1
address .save ()
shipping_addr = ShippingAddress ()
address .populate_alternative_model (shipping_addr )
shipping_addr .save ()
return shipping_addr
def create_billing_address (self , shipping_address = None ):
"""
Saves any relevant billing data (eg a billing address).
@@ -569,12 +571,12 @@ def create_billing_address(self, shipping_address=None):
def save_payment_details (self , order ):
"""
Saves all payment-related details. This could include a billing
Saves all payment-related details. This could include a billing
address, payment sources and any order payment events.
"""
self .save_payment_events (order )
self .save_payment_sources (order )
def save_payment_events (self , order ):
"""
Saves any relevant payment events for this order
@@ -588,23 +590,23 @@ def save_payment_events(self, order):
def save_payment_sources (self , order ):
"""
Saves any payment sources used in this order.
When the payment sources are created, the order model does not exist and
When the payment sources are created, the order model does not exist and
so they need to have it set before saving.
"""
if not self ._payment_sources :
return
for source in self ._payment_sources :
source .order = order
source .save ()
def get_initial_order_status (self , basket ):
return None
def get_submitted_basket (self ):
basket_id = self .checkout_session .get_submitted_basket_id ()
return Basket ._default_manager .get (pk = basket_id )
def restore_frozen_basket (self ):
"""
Restores a frozen basket as the sole OPEN basket. Note that this also merges
@@ -645,8 +647,8 @@ def send_confirmation_message(self, order, **kwargs):
CommunicationEvent ._default_manager .create (order = order , event_type = event_type )
messages = event_type .get_messages (ctx )
if messages and messages ['body' ]:
logger .info ("Order #%s - sending %s messages" , order .number , code )
if messages and messages ['body' ]:
logger .info ("Order #%s - sending %s messages" , order .number , code )
dispatcher = Dispatcher (logger )
dispatcher .dispatch_order_messages (order , messages , event_type , ** kwargs )
else :
@@ -656,11 +658,11 @@ def send_confirmation_message(self, order, **kwargs):
class PaymentDetailsView (OrderPlacementMixin , TemplateView ):
"""
For taking the details of payment and creating the order
The class is deliberately split into fine-grained methods, responsible for only one
thing. This is to make it easier to subclass and override just one component of
functionality.
Almost all projects will need to subclass and customise this class.
"""
template_name = 'checkout/payment_details.html'
@@ -685,7 +687,7 @@ def get(self, request, *args, **kwargs):
if error_response :
return error_response
return super (PaymentDetailsView , self ).get (request , * args , ** kwargs )
def post (self , request , * args , ** kwargs ):
"""
This method is designed to be overridden by subclasses which will
@@ -745,7 +747,7 @@ def get_default_billing_address(self):
def submit (self , basket , payment_kwargs = None , order_kwargs = None ):
"""
Submit a basket for order placement.
The process runs as follows:
* Generate an order number
* Freeze the basket so it cannot be modified any more.
@@ -772,17 +774,17 @@ def submit(self, basket, payment_kwargs=None, order_kwargs=None):
return HttpResponseRedirect (url )
# We generate the order number first as this will be used
# in payment requests (ie before the order model has been
# in payment requests (ie before the order model has been
# created). We also save it in the session for multi-stage
# checkouts (eg where we redirect to a 3rd party site and place
# the order on a different request).
order_number = self .generate_order_number (basket )
logger .info ("Order #%s: beginning submission process for basket %d" , order_number , basket .id )
self .freeze_basket (basket )
self .checkout_session .set_submitted_basket (basket )
# Handle payment. Any payment problems should be handled by the
# Handle payment. Any payment problems should be handled by the
# handle_payment method raise an exception, which should be caught
# within handle_POST and the appropriate forms redisplayed.
try :
@@ -821,7 +823,7 @@ def submit(self, basket, payment_kwargs=None, order_kwargs=None):
msg = unicode (e )
self .restore_frozen_basket ()
return self .render_to_response (self .get_context_data (error = msg ))
def generate_order_number (self , basket ):
generator = OrderNumberGenerator ()
order_number = generator .order_number (basket )
@@ -834,11 +836,11 @@ def freeze_basket(self, basket):
# need to be "unfrozen". We also store the basket ID in the session
# so the it can be retrieved by multistage checkout processes.
basket .freeze ()
def handle_payment (self , order_number , total , ** kwargs ):
"""
Handle any payment processing.
Handle any payment processing.
This method is designed to be overridden within your project. The
default is to do nothing.
"""
@@ -862,7 +864,7 @@ class ThankYouView(DetailView):
"""
template_name = 'checkout/thank_you.html'
context_object_name = 'order'
def get_object (self ):
# We allow superusers to force an order thankyou page for testing
order = None
@@ -871,11 +873,11 @@ def get_object(self):
order = Order ._default_manager .get (number = self .request .GET ['order_number' ])
elif 'order_id' in self .request .GET :
order = Order ._default_manager .get (id = self .request .GET ['orderid' ])
if not order :
if 'checkout_order_id' in self .request .session :
order = Order ._default_manager .get (pk = self .request .session ['checkout_order_id' ])
else :
raise Http404 (_ ("No order found" ))
return order