Building a full ecommerce site part 5: testing directing customers to Oscar's thank-you page after payment

This is the story of how I thought I'd spend an hour or so, maybe two, fixing it so that when people press the "Place Order" button on the /preview page, they get sent to the /thank-you page. I started at around 10-11am. I sort of worked it out at around 11.25pm.

For you guys who don't want the guts and gore of the process, though, I'll give you a quickie.

The tl;dr version

In the last episode of this exciting series, we commented out the "Place Order" button and replaced it with a PayPal button. My next objective was to have the PayPal button send customers to Oscar's /thank-you page after payment.

However, I didn't want to fiddle around with PayPal's JavaScript code just yet. I wanted to test if the /thank-you page really worked, under the original conditions, before I added CheckOut Express.

So, I commented out the PayPal button and commented the "Place Order" button back in. Then, I ran the server and clicked on it. Unsurprisingly, it broke. The error: ConnectionRefusedError: [WinError 10061] No connection could be made because the target machine actively refused it.

Long story short, that error occurs because the "Place Order" button is inextricably tied to functions for emails hidden somewhere in the deep recesses of Oscar's views. Add this to your settings.py file to fix it: EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'. With this, the payment email will be printed to console, and you'll be successfully directed to the /thank-you page.

The long story

I'll break it down.

Django packages' convenient urls.py

After working with frameworks/libraries/packages based on Django for a while, I realised that I should always take a look at their urls, to see what pages are already written into them. More often than not, packages like Mezzanine and Oscar are built so adequately that they've already got all the standard pages for their respective industries. You can look into their templates to see them, or you can check out their urls.py to see something like this:

url(r'thank-you/$', self.thankyou_view.as_view(),
name='thank-you'),

Oscar is a wee bit more tricky. It sometimes hides its urls in app.py. This one was in oscar/apps/checkout/app.py.

Anyway, a glance at this particular url() shows that it sends people to http://localhost:8000/thank-you based on self.thankyou_view. It also shows that you can send people to the page using the name 'thank-you'.

So, thought I, it should be very easy to send people to the /thank-you page! All I need to do was find the view that works on the "Place Order" button, and edit it to redirect people to a nice happy thank you.

Overriding Oscar's views

So, I hunted and found this in oscar/apps/checkout/views.py

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())

It's also in the latest documentation.

Note the line I bolded. It mentions the "Place Order" button! So, it must be the one to override.

But, before you go on, you might want to read Part 3 on how to override Oscar's views. Funny how it turns out useful after all. (Or not.)

Anyway, once I'd followed the documentation on overriding views and fiddled through the quirk I mentioned in Part 3, I overrode Oscar's original view with this:

from oscar.apps.checkout.views import *

class PaymentDetailsView(PaymentDetailsView):

def handle_place_order_submission(self, request):
return redirect('thank-you')

I'm going to contract the story here a bit otherwise it'll be super long. First, this didn't work because the 'thank-you' url is in oscar/apps/checkout/app.py. We have to use 'checkout:thank-you' instead.

Second, if you try 'home', it does bring you to the home page. 'checkout:thank-you' breaks too, but I can't remember the error anymore.

I think, around this time I began receiving an error saying PaymentDetailsView does not have the attribute 'object_list'. Either that or that submit() requires 7 positional arguments and none were given. (Something I couldn't figure out and still am dying to know is, how do you get the context of basket details for submit()?)

Hiking the code trail

So, I looked harder at handle_place_order_submission() and saw that it ends with return self.submit(**self.build_submission()). So, I had to hunt submit() down, didn't I? It was also in the PaymentDetailsView class in oscar/apps/checkout/views.py. It's very long but the important bit:

def submit(self, user, basket, shipping_address, shipping_method,  # noqa (too complex (10))
shipping_charge, billing_address, order_total,
payment_kwargs=None, order_kwargs=None):

...

# If all is ok with payment, try and place order
...
try:
return self.handle_order_placement(
order_number, user, basket, shipping_address, shipping_method,
shipping_charge, billing_address, order_total, **order_kwargs)

Specifically, self.handle_order_placement(). So, I had to go look for it, right?

I did, I found it, and it led me to another piece of code. And then it led me to another piece of code. And then I gave up.

I went on GitHub and looked at other people's code. I tried out different permutations of their code, first with handle_place_order_submission(), then with submit(), then with a couple of others.

I was getting super discouraged. Why hadn't anyone mentioned these problems? Surely some others must have been drawn into its cold, bloodless web of intrigue. There was nothing in the documentation and nothing on Stack Overflow about it as far as I searched.

Back at the traceback

And then, at around 11pm, I reset the code to just after I had commented out the PayPal button and commented the "Place Order" button back in. I ran the server. And, I looked at the traceback properly.

It's very long, but here is the more important section. I've annotated it with the thoughts that ran through my mind as I read through it.

 # It starts with Oscar, and here it's at checkout.views. Ok, good.
File "C:\Python3\lib\site-packages\oscar\apps\checkout\views.py", line 631, in submit
shipping_charge, billing_address, order_total, **order_kwargs)
File "C:\Python3\lib\site-packages\oscar\apps\checkout\mixins.py", line 118, in handle_order_placement
return self.handle_successful_order(order)
File "C:\Python3\lib\site-packages\oscar\apps\checkout\mixins.py", line 254, in handle_successful_order
self.send_confirmation_message(order, self.communication_type_code)
File "C:\Python3\lib\site-packages\oscar\apps\checkout\mixins.py", line 292, in send_confirmation_message
event_type, **kwargs)

# What's it doing? Why's it sending messages?
File "C:\Python3\lib\site-packages\oscar\apps\customer\utils.py", line 43, in dispatch_order_messages
dispatched_messages = self.dispatch_user_messages(order.user, messages)

# Email?! Why's it sending email?! How?!
File "C:\Python3\lib\site-packages\oscar\apps\customer\utils.py", line 59, in dispatch_user_messages
dispatched_messages['email'] = self.send_user_email_messages(user, messages)
File "C:\Python3\lib\site-packages\oscar\apps\customer\utils.py", line 94, in send_user_email_messages
email = self.send_email_messages(user.email, messages)
File "C:\Python3\lib\site-packages\oscar\apps\customer\utils.py", line 123, in send_email_messages
email.send()

# Ok, Oscar's relying on Django to send email, I guess?
File "C:\Python3\lib\site-packages\django\core\mail\message.py", line 342, in send
return self.get_connection(fail_silently).send_messages([self])
File "C:\Python3\lib\site-packages\django\core\mail\backends\smtp.py", line 100, in send_messages
new_conn_created = self.open()
File "C:\Python3\lib\site-packages\django\core\mail\backends\smtp.py", line 58, in open
self.connection = connection_class(self.host, self.port, **connection_params)
File "C:\Python3\lib\smtplib.py", line 251, in __init__
(code, msg) = self.connect(host, port)

# How's it doing SMTP and sockets and all that?
# Don't these require my gmail username and password at least?
File "C:\Python3\lib\smtplib.py", line 336, in connect
self.sock = self._get_socket(host, port, self.timeout)
File "C:\Python3\lib\smtplib.py", line 307, in _get_socket
self.source_address)
File "C:\Python3\lib\socket.py", line 722, in create_connection
raise err
File "C:\Python3\lib\socket.py", line 713, in create_connection
sock.connect(sa)
ConnectionRefusedError: [WinError 10061] No connection could be made because the target machine actively refused it

And then I realised, the error isn't explicit but it must indicate something about not being able to send emails.

So, I googled and found this issue on an old discontinued package. mbrochh said way back when, to put this in (local) settings: EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'.

I did. And ta-dah, it worked. Oscar sends emails to customers telling them they'd bought whatever, after they'd paid for it. But it couldn't actually send those emails without a proper email back end set up.

This Django email back end merely prints the emails to console. But it does allow Oscar to proceed as though it'd really sent the emails out.

So, I got to the /thank-you page finally.

Oscar might have its own email back end and I haven't found it yet. I just thought I should write this all down before I forget in case some other noob stumbles into the same terrible mistake.

Lesson learnt: read the traceback first.

Comments

There are currently no comments

New Comment

required

required (not published)

optional

required