| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658 |
- import six
- import logging
-
- from django import http
- from django.contrib import messages
- from django.contrib.auth import login
- from django.core.urlresolvers import reverse, reverse_lazy
- from django.utils.translation import ugettext as _
- from django.views import generic
-
- from oscar.core.loading import get_model
- from oscar.apps.shipping.methods import NoShippingRequired
- from oscar.core.loading import get_class, get_classes
- from . import signals
-
- ShippingAddressForm, GatewayForm \
- = get_classes('checkout.forms', ['ShippingAddressForm', 'GatewayForm'])
- OrderCreator = get_class('order.utils', 'OrderCreator')
- UserAddressForm = get_class('address.forms', 'UserAddressForm')
- Repository = get_class('shipping.repository', 'Repository')
- AccountAuthView = get_class('customer.views', 'AccountAuthView')
- RedirectRequired, UnableToTakePayment, PaymentError \
- = get_classes('payment.exceptions', ['RedirectRequired',
- 'UnableToTakePayment',
- 'PaymentError'])
- UnableToPlaceOrder = get_class('order.exceptions', 'UnableToPlaceOrder')
- OrderPlacementMixin = get_class('checkout.mixins', 'OrderPlacementMixin')
- CheckoutSessionMixin = get_class('checkout.session', 'CheckoutSessionMixin')
-
- Order = get_model('order', 'Order')
- ShippingAddress = get_model('order', 'ShippingAddress')
- CommunicationEvent = get_model('order', 'CommunicationEvent')
- PaymentEventType = get_model('order', 'PaymentEventType')
- PaymentEvent = get_model('order', 'PaymentEvent')
- UserAddress = get_model('address', 'UserAddress')
- Basket = get_model('basket', 'Basket')
- Email = get_model('customer', 'Email')
- CommunicationEventType = get_model('customer', 'CommunicationEventType')
-
- # Standard logger for checkout events
- logger = logging.getLogger('oscar.checkout')
-
-
- class IndexView(CheckoutSessionMixin, generic.FormView):
- """
- First page of the checkout. We prompt user to either sign in, or
- to proceed as a guest (where we still collect their email address).
- """
- template_name = 'checkout/gateway.html'
- form_class = GatewayForm
- success_url = reverse_lazy('checkout:shipping-address')
- pre_conditions = (
- 'check_basket_is_not_empty',
- 'check_basket_is_valid')
-
- def get(self, request, *args, **kwargs):
- # We redirect immediately to shipping address stage if the user is
- # signed in.
- if request.user.is_authenticated():
- # We raise a signal to indicate that the user has entered the
- # checkout process so analytics tools can track this event.
- signals.start_checkout.send_robust(
- sender=self, request=request)
- return self.get_success_response()
- return super(IndexView, self).get(request, *args, **kwargs)
-
- def get_form_kwargs(self):
- kwargs = super(IndexView, self).get_form_kwargs()
- email = self.checkout_session.get_guest_email()
- if email:
- kwargs['initial'] = {
- 'username': email,
- }
- return kwargs
-
- def form_valid(self, form):
- if form.is_guest_checkout() or form.is_new_account_checkout():
- email = form.cleaned_data['username']
- self.checkout_session.set_guest_email(email)
-
- # We raise a signal to indicate that the user has entered the
- # checkout process by specifying an email address.
- signals.start_checkout.send_robust(
- sender=self, request=self.request, email=email)
-
- if form.is_new_account_checkout():
- messages.info(
- self.request,
- _("Create your account and then you will be redirected "
- "back to the checkout process"))
- self.success_url = "%s?next=%s&email=%s" % (
- reverse('customer:register'),
- reverse('checkout:shipping-address'),
- email
- )
- else:
- user = form.get_user()
- login(self.request, user)
-
- # We raise a signal to indicate that the user has entered the
- # checkout process.
- signals.start_checkout.send_robust(
- sender=self, request=self.request)
-
- return self.get_success_response()
-
- def get_success_response(self):
- return http.HttpResponseRedirect(self.get_success_url())
-
-
- # ================
- # SHIPPING ADDRESS
- # ================
-
-
- class ShippingAddressView(CheckoutSessionMixin, generic.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 later saved as ShippingAddress model when the
- order is sucessfully submitted.
- """
- template_name = 'checkout/shipping_address.html'
- form_class = ShippingAddressForm
- success_url = reverse_lazy('checkout:shipping-method')
- pre_conditions = ('check_basket_is_not_empty',
- 'check_basket_is_valid',
- 'check_user_email_is_captured',
- 'check_basket_requires_shipping')
-
- def get_initial(self):
- return self.checkout_session.new_shipping_address_fields()
-
- def get_context_data(self, **kwargs):
- ctx = super(ShippingAddressView, self).get_context_data(**kwargs)
- if self.request.user.is_authenticated():
- # Look up address book data
- ctx['addresses'] = self.get_available_addresses()
- return ctx
-
- def get_available_addresses(self):
- # Include only addresses where the country is flagged as valid for
- # shipping. Also, use ordering to ensure the default address comes
- # first.
- return self.request.user.addresses.filter(
- country__is_shipping_country=True).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:
- address = UserAddress._default_manager.get(
- pk=self.request.POST['address_id'], user=self.request.user)
- action = self.request.POST.get('action', None)
- if action == 'ship_to':
- # User has selected a previous address to ship to
- self.checkout_session.ship_to_user_address(address)
- return self.get_success_response()
- elif action == 'delete':
- # Delete the selected address
- address.delete()
- messages.info(self.request, _("Address deleted from your"
- " address book"))
- return self.get_success_response()
- else:
- return http.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
- address_fields = dict(
- (k, v) for (k, v) in form.instance.__dict__.items()
- if not k.startswith('_'))
- self.checkout_session.ship_to_new_address(address_fields)
- return super(ShippingAddressView, self).form_valid(form)
-
- def get_success_response(self):
- return http.HttpResponseRedirect(self.get_success_url())
-
-
- class UserAddressUpdateView(CheckoutSessionMixin, generic.UpdateView):
- """
- Update a user address
- """
- template_name = 'checkout/user_address_form.html'
- form_class = UserAddressForm
-
- def get_queryset(self):
- return self.request.user.addresses.all()
-
- def get_form_kwargs(self):
- kwargs = super(UserAddressUpdateView, self).get_form_kwargs()
- kwargs['user'] = self.request.user
- return kwargs
-
- def get_success_url(self):
- messages.info(self.request, _("Address saved"))
- return reverse('checkout:shipping-address')
-
-
- class UserAddressDeleteView(CheckoutSessionMixin, generic.DeleteView):
- """
- Delete an address from a user's addressbook.
- """
- template_name = 'checkout/user_address_delete.html'
-
- def get_queryset(self):
- return self.request.user.addresses.all()
-
- def get_success_url(self):
- messages.info(self.request, _("Address deleted"))
- return reverse('checkout:shipping-address')
-
-
- # ===============
- # Shipping method
- # ===============
-
-
- class ShippingMethodView(CheckoutSessionMixin, generic.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
- automatically selected. Otherwise, a page is rendered where
- the user can choose the appropriate one.
- """
- template_name = 'checkout/shipping_methods.html'
- pre_conditions = ('check_basket_is_not_empty',
- 'check_basket_is_valid',
- 'check_user_email_is_captured', )
-
- def get(self, request, *args, **kwargs):
- # These pre-conditions can't easily be factored out into the normal
- # pre-conditions as they do more than run a test and then raise an
- # exception if it fails.
-
- # Check that shipping is required at all
- if not request.basket.is_shipping_required():
- # No shipping required - we store a special code to indicate so.
- self.checkout_session.use_shipping_method(
- NoShippingRequired().code)
- return self.get_success_response()
-
- # Check that shipping address has been completed
- if not self.checkout_session.is_shipping_address_set():
- messages.error(request, _("Please choose a shipping address"))
- return http.HttpResponseRedirect(
- reverse('checkout:shipping-address'))
-
- # Save shipping methods as instance var as we need them both here
- # and when setting the context vars.
- self._methods = self.get_available_shipping_methods()
- if len(self._methods) == 0:
- # No shipping methods available for given address
- messages.warning(request, _(
- "Shipping is unavailable for your chosen address - please "
- "choose another"))
- return http.HttpResponseRedirect(
- reverse('checkout:shipping-address'))
- elif len(self._methods) == 1:
- # Only one shipping method - set this and redirect onto the next
- # step
- self.checkout_session.use_shipping_method(self._methods[0].code)
- return self.get_success_response()
-
- # Must be more than one available shipping method, we present them to
- # the user to make a choice.
- return super(ShippingMethodView, self).get(request, *args, **kwargs)
-
- def get_context_data(self, **kwargs):
- kwargs = super(ShippingMethodView, self).get_context_data(**kwargs)
- kwargs['methods'] = self._methods
- return kwargs
-
- 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(
- user=self.request.user, basket=self.request.basket,
- shipping_addr=self.get_shipping_address(self.request.basket),
- request=self.request)
-
- 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)
- is_valid = False
- for method in self.get_available_shipping_methods():
- if method.code == method_code:
- is_valid = True
- if not is_valid:
- messages.error(request, _("Your submitted shipping method is not"
- " permitted"))
- return http.HttpResponseRedirect(
- reverse('checkout:shipping-method'))
-
- # Save the code for the chosen shipping method in the session
- # 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 http.HttpResponseRedirect(reverse('checkout:payment-method'))
-
-
- # ==============
- # Payment method
- # ==============
-
-
- class PaymentMethodView(CheckoutSessionMixin, generic.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. It's not the place for entering sensitive details
- like bankcard numbers though - that belongs on the payment details view.
- """
- pre_conditions = (
- 'check_basket_is_not_empty',
- 'check_basket_is_valid',
- 'check_user_email_is_captured',
- 'check_shipping_data_is_captured')
-
- def get(self, request, *args, **kwargs):
- # By default we redirect straight onto the payment details view. Shops
- # that require a choice of payment method may want to override this
- # method to implement their specific logic.
- return self.get_success_response()
-
- def get_success_response(self):
- return http.HttpResponseRedirect(reverse('checkout:payment-details'))
-
-
- # ================
- # Order submission
- # ================
-
-
- class PaymentDetailsView(OrderPlacementMixin, generic.TemplateView):
- """
- For taking the details of payment and creating the order.
-
- This view class is used by two separate URLs: 'payment-details' and
- 'preview'. The `preview` class attribute is used to distinguish which is
- being used.
-
- If sensitive details are required (eg a bankcard), then the payment details
- view should submit to the preview URL and a custom implementation of
- `validate_payment_submission` should be provided.
-
- - If the form data is valid, then the preview template can be rendered with
- the payment-details forms re-rendered within a hidden div so they can be
- re-submitted when the 'place order' button is clicked. This avoids having
- to write sensitive data to disk anywhere during the process. This can be
- done by calling `render_preview`, passing in the extra template context
- vars.
-
- - If the form data is invalid, then the payment details templates needs to
- be re-rendered with the relevant error messages. This can be done by
- calling `render_payment_details`, passing in the form instances to pass
- to the templates.
-
- 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.
-
- All projects will need to subclass and customise this class as no payment
- is taken by default.
- """
- template_name = 'checkout/payment_details.html'
- template_name_preview = 'checkout/preview.html'
-
- pre_conditions = (
- 'check_basket_is_not_empty',
- 'check_basket_is_valid',
- 'check_user_email_is_captured',
- 'check_shipping_data_is_captured')
-
- # If preview=True, then we render a preview template that shows all order
- # details ready for submission.
- preview = False
-
- def get_preconditions(self, request):
- if not self.preview:
- return self.pre_conditions
- # The preview view needs to ensure payment information has been
- # correctly captured.
- return self.pre_conditions + (
- 'check_payment_data_is_captured',)
-
- def get_template_names(self):
- return [self.template_name_preview] if self.preview else [
- self.template_name]
-
- def post(self, request, *args, **kwargs):
- # Posting to payment-details isn't the right thing to do. Form
- # submissions should use the preview URL.
- if not self.preview:
- return http.HttpBadRequest()
-
- # We use a custom parameter to indicate if this is an attempt to place
- # an order (normally from the preview page). Without this, we assume a
- # payment form is being submitted from the payment details view. In
- # this case, the form needs validating and the order preview shown.
- if request.POST.get('action', '') == 'place_order':
- return self.handle_place_order_submission(request)
- return self.handle_payment_details_submission(request)
-
- def handle_place_order_submission(self, request):
- """
- Handle a request to place an order.
-
- This method is normally called after the customer has clicked "place
- order" on the preview page. It's responsible for (re-)validating any
- form information then building the submission dict to pass to the
- `submit` method.
-
- If forms are submitted on your payment details view, you should
- override this method to ensure they are valid before extracting their
- data into the submission dict and passing it onto `submit`.
- """
- return self.submit(**self.build_submission())
-
- def handle_payment_details_submission(self, request):
- """
- Handle a request to submit payment details.
-
- This method will need to be overridden by projects that require forms
- to be submitted on the payment details view. The new version of this
- method should validate the submitted form data and:
-
- - If the form data is valid, show the preview view with the forms
- re-rendered in the page
- - If the form data is invalid, show the payment details view with
- the form errors showing.
-
- """
- # No form data to validate by default, so we simply render the preview
- # page. If validating form data and it's invalid, then call the
- # render_payment_details view.
- return self.render_preview(request)
-
- def render_preview(self, request, **kwargs):
- """
- Show a preview of the order.
-
- If sensitive data was submitted on the payment details page, you will
- need to pass it back to the view here so it can be stored in hidden
- form inputs. This avoids ever writing the sensitive data to disk.
- """
- self.preview = True
- ctx = self.get_context_data(**kwargs)
- return self.render_to_response(ctx)
-
- def render_payment_details(self, request, **kwargs):
- """
- Show the payment details page
-
- This method is useful if the submission from the payment details view
- is invalid and needs to be re-rendered with form errors showing.
- """
- self.preview = False
- ctx = self.get_context_data(**kwargs)
- return self.render_to_response(ctx)
-
- def get_default_billing_address(self):
- """
- Return default billing address for user
-
- This is useful when the payment details view includes a billing address
- form - you can use this helper method to prepopulate the form.
-
- Note, this isn't used in core oscar as there is no billing address form
- by default.
- """
- if not self.request.user.is_authenticated():
- return None
- try:
- return self.request.user.addresses.get(is_default_for_billing=True)
- except UserAddress.DoesNotExist:
- return None
-
- def submit(self, user, basket, shipping_address, shipping_method, # noqa (too complex (10))
- order_total, 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 (important when
- redirecting the user to another site for payment as it prevents the
- basket being manipulated during the payment process).
- * Attempt to take payment for the order
- - If payment is successful, place the order
- - If a redirect is required (eg PayPal, 3DSecure), redirect
- - If payment is unsuccessful, show an appropriate error message
-
- :basket: The basket to submit.
- :payment_kwargs: Additional kwargs to pass to the handle_payment method
- :order_kwargs: Additional kwargs to pass to the place_order method
- """
- if payment_kwargs is None:
- payment_kwargs = {}
- if order_kwargs is None:
- order_kwargs = {}
-
- # Taxes must be known at this point
- assert basket.is_tax_known, (
- "Basket tax must be set before a user can place an order")
- assert shipping_method.is_tax_known, (
- "Shipping method tax must be set before a user can place an order")
-
- # We generate the order number first as this will be used
- # 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)
- self.checkout_session.set_order_number(order_number)
- logger.info("Order #%s: beginning submission process for basket #%d",
- order_number, basket.id)
-
- # Freeze the basket so it cannot be manipulated while the customer is
- # completing payment on a 3rd party site. Also, store a reference to
- # the basket in the session so that we know which basket to thaw if we
- # get an unsuccessful payment response when redirecting to a 3rd party
- # site.
- self.freeze_basket(basket)
- self.checkout_session.set_submitted_basket(basket)
-
- # We define a general error message for when an unanticipated payment
- # error occurs.
- error_msg = _("A problem occurred while processing payment for this "
- "order - no payment has been taken. Please "
- "contact customer services if this problem persists")
-
- signals.pre_payment.send_robust(sender=self, view=self)
-
- try:
- self.handle_payment(order_number, order_total, **payment_kwargs)
- except RedirectRequired as e:
- # Redirect required (eg PayPal, 3DS)
- logger.info("Order #%s: redirecting to %s", order_number, e.url)
- return http.HttpResponseRedirect(e.url)
- except UnableToTakePayment as e:
- # Something went wrong with payment but in an anticipated way. Eg
- # their bankcard has expired, wrong card number - that kind of
- # thing. This type of exception is supposed to set a friendly error
- # message that makes sense to the customer.
- msg = six.text_type(e)
- logger.warning(
- "Order #%s: unable to take payment (%s) - restoring basket",
- order_number, msg)
- self.restore_frozen_basket()
-
- # We assume that the details submitted on the payment details view
- # were invalid (eg expired bankcard).
- return self.render_payment_details(
- self.request, error=msg, **payment_kwargs)
- except PaymentError as e:
- # A general payment error - Something went wrong which wasn't
- # anticipated. Eg, the payment gateway is down (it happens), your
- # credentials are wrong - that king of thing.
- # It makes sense to configure the checkout logger to
- # mail admins on an error as this issue warrants some further
- # investigation.
- msg = six.text_type(e)
- logger.error("Order #%s: payment error (%s)", order_number, msg,
- exc_info=True)
- self.restore_frozen_basket()
- return self.render_preview(
- self.request, error=error_msg, **payment_kwargs)
- except Exception as e:
- # Unhandled exception - hopefully, you will only ever see this in
- # development...
- logger.error(
- "Order #%s: unhandled exception while taking payment (%s)",
- order_number, e, exc_info=True)
- self.restore_frozen_basket()
- return self.render_preview(
- self.request, error=error_msg, **payment_kwargs)
-
- signals.post_payment.send_robust(sender=self, view=self)
-
- # If all is ok with payment, try and place order
- logger.info("Order #%s: payment successful, placing order",
- order_number)
- try:
- return self.handle_order_placement(
- order_number, user, basket, shipping_address, shipping_method,
- order_total, **order_kwargs)
- except UnableToPlaceOrder as e:
- # It's possible that something will go wrong while trying to
- # actually place an order. Not a good situation to be in as a
- # payment transaction may already have taken place, but needs
- # to be handled gracefully.
- msg = six.text_type(e)
- logger.error("Order #%s: unable to place order - %s",
- order_number, msg, exc_info=True)
- self.restore_frozen_basket()
- return self.render_preview(
- self.request, error=msg, **payment_kwargs)
-
-
- # =========
- # Thank you
- # =========
-
-
- class ThankYouView(generic.DetailView):
- """
- Displays the 'thank you' page which summarises the order just submitted.
- """
- template_name = 'checkout/thank_you.html'
- context_object_name = 'order'
-
- def get_object(self):
- # We allow superusers to force an order thank-you page for testing
- order = None
- if self.request.user.is_superuser:
- if 'order_number' in self.request.GET:
- 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['order_id'])
-
- 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 http.Http404(_("No order found"))
-
- return order
|