Tuesday, October 27, 2009

"Beginning Django E-Commerce" Book Release!

Yesterday, Beginning Django E-Commerce was officially released on Amazon, so it's now in stock and available for purchase. I don't know how long it's going to take my publisher to get the "Look Inside!" functionality working, so I figured that I would post the 500 foot view of the table of contents here for those who are interested an overview of the book's contents:

Chapter 1 - Best Laid Plans
Chapter 2 - Creating a Django Site
Chapter 3 - Models for Sale
Chapter 4 - The Shopping Cart
Chapter 5 - Site Checkout & Order
Chapter 6 - Customer Accounts
Chapter 7 - Product Images
Chapter 8 - Implementing Product Search
Chapter 9 - Intelligent Cross-Selling
Chapter 10 - Adding in Ajax
Chapter 11 - Search Engine Optimization
Chapter 12 - Web Security Overview
Chapter 13 - Improving Performance
Chapter 14 - Django Testing
Chapter 15 - Deployment
Chapter 16 - Django on Google App Engine

It doesn't say it anywhere else, but the book uses Django 1.1. For those curious about what I cover in addition to Django: I use MySQL as the database engine, mod_wsgi to integrate Django with Apache and NginX in deployment, Google Keyczar for symmetric encryption of credit card information, Google Checkout and Authorize.Net as payment gateways, and the jQuery JavaScript library to handle the Ajax functionality. The last chapter is dedicated to showing the basics of how to deploy Django projects onto Google App Engine using the App Engine Patch project.

I'd like to express my gratitude to all of those people who purchased the alpha version of the book, and thank those of you that provided your opinions and feedback.

29 comments:

  1. Why do you use a model for cart items instead of using the session for storing cart items and the session-id as the cart id?

    ReplyDelete
  2. That's a really great question. I actually considered both of those options when doing the site, but opted not to just to keep the two separate.

    First, using a model for cart items allows you the flexibility to store any other information along with the cart item, besides just the product ID. (I didn't get any more creative than a "date_added" field.) I felt there would also be better performance if the cart items had their own model linked to the product table via a product_id foreign key field. Otherwise, you'd have to fetch the list of product IDs from the session and then retrieve the Product instances from those IDs, which seemed like a slower approach than doing the join. (I didn't actually benchmark test that theory, though.)

    Also, some businesses might be interested in analyzing shopping cart items, say, to look at the characteristics of abandoned shopping carts or those that converted. If you store the cart items in the session, then in order to analyze the cart data, you have a big mess to query from. It's not impossible, but I felt it made more sense to keep it separate from the actual django_session table.

    To the second question: the session ID could be used as a cart identifier, since cart IDs just need to be random and unique to that customer's session. I opted not to use the session ID as the cart ID just to avoid any potential confusion. It could be done, but I didn't want to give readers the impression that the cart_id field was a foreign key to the session table. It made much more sense to generate my own unique identifier just because the session ID is used by Django for its own purposes, and I felt that keeping the cart item's identifier decoupled from Django's internals was a better design decision.

    Of course, this is just one geek's opinion. It doesn't mean that I'm right, or that the approach you proposed is wrong. I just wanted to keep my example as flexible and configurable as I could.

    ReplyDelete
  3. Thank you for your answer.

    In chapter 5 section "Building XML document in Python" it might be easier to use django templates instead of buildng the xml document by hand.

    ReplyDelete
  4. "...slower approach then doing the join." I agree with this argument although I think that you are making number of queries instead of doing a join.

    Your code:
    def get_cart_items(request):
    return CartItem.objects.filter(cart_id=_cart_id(request))

    then later you access item.product, making a separate query for each product.

    instead of maybe using object.select_related() ?

    ReplyDelete
  5. Hi Pablo,

    There are some XML purists who will tell you that you should never, ever build XML documents by hand, and that you should rely only on libraries for building them. I don't necessarily agree with this 100% (in Chapter 11, I create the complex "Google Base" XML product feed using a template), but since the example was (relatively) simple, I wrote Chapter 5 with them in mind.

    Using object.select_related() to handle the querying between the shopping cart items and the products themselves is a really good idea, and a great thing to point out. I think it's well worth testing if you're out there in development land. I'll re-examine my code to see if it can be improved.

    As of now, here is the select_related() docs, for those interesting in playing around:

    http://docs.djangoproject.com/en/dev/ref/models/querysets/#id4

    ReplyDelete
  6. A small bug:
    In www.django-ecommerce.com go to a product and click "Add to cart" you are now in the cart page.
    Click the browser back button to go back to the product page and try to click on the "Add to cart" button again. You get the "Cookies must be enabled."

    This is because you check for the test-cookie and then delete it. Maybe you can use the session cookie?

    ReplyDelete
  7. Hi Pablo,

    A very interesting bug! That was also mentioned on the errata page. I played around with it and you're absolutely right, it's because I call delete_test_cookie(). That was a "best practice" move that came from the Django docs. I didn't find that error case in my own testing.

    If this is an issue, I don't see any harm in just leaving the test cookie, without deleting it. Customers are unlikely to add an item to the cart and then disable their cookies.

    Thanks!

    ReplyDelete
  8. In add_to_cart function you are using:
    postdata=request.POST.copy()
    productslug=postdata.get('product_slug','')
    ...

    The only reason I see to use copy() is if you want to modify the post data, usually it's for pre-processing a form. In this case I don't think it is needed.

    p.s. is this the right place to post this stuf?

    ReplyDelete
  9. In you implementation of search you only get products that match all the search terms in on e of the fields. For example 'ruby java' will return nothing. Maybe it's a good thing.

    In the search app in the product function, why do you use the following line instead of just returning the products?
    results['products'] = products

    This line shouldn't be inside the for loop of words.

    ReplyDelete
  10. Hi Pablo,

    You're right, it's perfectly safe to use access, modify, and use the request.POST dictionary directly. I just chose to copy it in my examples because the performance penalty was likely to be negligible and it's consistent with those times when readers will be pre-processing a form.

    The fact that the search function is returning a dictionary called "results" with a "products" entry of Product instances is a vestige of earlier code. Originally, I was going to have two sections on the search results page: one for products and one for categories. (So a search for "guitar" would return both guitars and the guitar categories.) I decided to skip the category results, but the "results" dictionary remains. And you're right, it updates the results dictionary for each word, not just once, when the for loop has completed.

    You're welcome to post on here. Sorry if my responses are ever less than timely! I haven't signed up for email notification when a new comment gets posted.

    ReplyDelete
  11. Hi Jim,

    One thing I have been curious to know how to implement is also returning search results for products whose category/ies also match the query.

    How would one approach this?

    ReplyDelete
  12. Hi Stephen,

    That's a good question. I'm assuming that you'd want to match products where the search text matches the name of the category? If that's the case, I think you would just add one more 'Q' object to the search filter. For example:

    products = products.filter(Q(name__icontains=word) |
    Q(description__icontains=word) |
    Q(sku__iexact=word) |
    Q(brand__icontains=word) |
    Q(meta_description__icontains=word) |
    Q(meta_keywords__icontains=word) |
    Q(categories__name__icontains=word))

    Adding that last line should achieve what you want. Before doing this, I strongly urge you to read about select_related():

    http://docs.djangoproject.com/en/dev/ref/models/querysets/#id4

    Using select_related() follow foreign keys when it fetches those instances. In this example, in order to optimize the query being made, you might want to change the line above where we get all product instances from:

    products = Product.active.all()

    to this:

    products = Product.active.select_related().all()

    This may be result in a slightly larger query up-front to get the categories with each product, but will prevent a subsequent sub-query to the databse to check the category name for each word in the search text. (Just check the django.db.connection.queries list to make sure you're not hitting the database with too many queries.)

    ReplyDelete
  13. Thanks for the tips Jim. I think I'll definitely be looking to implement select_related() going forward.

    ReplyDelete
  14. Hi, I've just taken your source code for Chapter 16 - run the server - tried to add flatpages. It errors with the following:



    Using the URLconf defined in urls, Django tried these URL patterns, in this order:

    1. ^account/login/$
    2. ^account/logout/$
    3. ^admin/(.*)
    4. ^ ^$
    5. ^ ^category/(?P.+)/$
    6. ^ ^product/(?P.+)/$
    7. ^cart/
    8. ^account/register/$
    9. ^person/
    10. ^account/

    The current URL, about/, didn't match any of these.

    Is there a setting I'm missing to get flatpages working on Google App Engine.

    ReplyDelete
  15. Hi T&W,

    I added flatpages to my own project and briefly deployed it to the App Engine. I couldn't get that error.

    I'd suggest you post your question to the App Engine Patch Google group and see if anyone else there has encountered this error, or search for possible solutions:

    http://groups.google.com/group/app-engine-patch

    Hope it helps!

    ReplyDelete
  16. Hello, I am currently scheduling your book but I have a error when I give click on the google checkout button I get an error:
    AttributeError at / cart /
    Text instance has no attribute 'tagName'
    Exception Value: Text instance has no attribute 'tagName'
    ecomstore / checkout / google_checkout.py in _parse_google_checkout_response, line 41
    and Line 41 is:
    def _parse_google_checkout_response (response_xml):
    REDIRECT_URL =''
    xml_doc = minidom.parseString (response_xml)
    root = xml_doc.documentElement
    node = root.childNodes [1]
    node.tagName if == 'redirect-url': # Line 41
    REDIRECT_URL = node.firstChild.data
    node.tagName if == 'error-message':
    raise RuntimeError (node.firstChild.data)
    return REDIRECT_URL

    you think this error is caused because I need your help

    ReplyDelete
  17. Hi Juan,

    It sounds like the Google Checkout XML isn't formatted the way our code is expecting. Take this line:

    node = root.childNodes[1]

    We expect 'node' will be an instance of xml.dom.minidom.Element. In your case, it isn't. I can't tell you exactly what's wrong, but here's how I would troubleshoot - output the following items as variables so you can view them, either in a template or a debugger, and see if they help you:

    1. xml_doc.toxml()
    See if the XML is structured the way you'd expect. I'm betting that the XML contains a single error message node. root.childNodes[1] expects the second child node of the root element to be an element named 'redirect-url'. My guess is that there may be only a single child of the root element, containing an error message.

    2. help(node) # see what 'node' is an instance of

    3. dir(node) # see what calls you can make on 'node' to see if that offers any clues.

    I'm betting it'll be #1. I hope that helps you sort it out!

    ReplyDelete
  18. I did what you suggest on step #1 above, assign xml_doc.toxml() a variable named 'abc'. Here is the content of 'abc' get from TraceBack. It looks weird and like a normal html page than a xml content.
    ===============
    http://dpaste.com/hold/511272/

    ReplyDelete
  19. Hi ColdZero,

    I'm not sure where that response is coming from. You're right, it's just an HTML document, and I've never gotten anything like that in the response at that step. It also looks like it's returning a page for a mobile device. Any reason it would be doing that?

    ReplyDelete
  20. Hi ColdZero and Jim,
    I'm getting the same type of reply from Google...an HTML document for a mobile device instead of XML. Did you guys every figure out what was going on?

    ReplyDelete
  21. Hello James,

    excuse me for my late reply, but I assume it's better ever than never. :-)

    Two years ago I played a bit with Django and wanted to use django-cms, but its development somehow stalled. Satchmo was/is overkill for our needs and LFS was at the beginning...I went back to PHP world and now I'm finishing my site with Concrete5 CMS using their ecommerce module.

    There are several things I do not like about Concrete5 and considering to use SilverStripe and their Sapphire framework which is now heading towards 3.0 release.

    Considering I'm not full-fledged web developer and will soon start to write desktop application using Python+Qt, I'm thinking that it would be easier for me to stay & master one language (Python) instead of jumping between Python & PHP.

    What do you think?

    That's why I think to try once more with Django, especially seeing that Django-cms is in a good shape now and they're developing django-shop (https://www.django-shop.org/) which should probably play very well with the django-cms.

    So, although I have to finish our small company site selling 10s of 'products' (services) and create a small site for non-profit organization soon (probably with PHP apps), I'm thinking about the future and to start learning Django to be able to deploy django-cms, django-shop etc. so I wonder:

    a) do you think your book is still relevant considering it covers Django-1.1 and

    b) is it still useful learning experience considering we'd need to write our own custom payment module for our local IPSP using form-based API (kind of paypal-like one)?

    In that case, I'd skip over PHP & SilverStripe and focus on (slowly) learning Django to be able to realize everything using Python...


    Sincerely,
    Gour

    ReplyDelete
  22. I am getting Text instance has no attribute 'tagName' does anyone know what this is from, Django 1.3 is my version, when I inspect the xml it's actually HTML coming back I am not sure why or where this is coming from, can anyone lend a hand?

    ReplyDelete
  23. More information...

    u'....(more code removed here for brevity)...

    Pretty strange how it is coming back as a mobile doctype, what the?

    ReplyDelete
  24. I figured out the problem, google changed their v2 URL:

    OLD - GOOGLE_CHECKOUT_URL = 'https://sandbox.google.com/checkout/api/v2/merchantCheckout/Merchant/' + GOOGLE_CHECKOUT_MERCHANT_ID

    NEW - GOOGLE_CHECKOUT_URL = 'https://sandbox.google.com/checkout/api/checkout/v2/merchantCheckout/Merchant/' + GOOGLE_CHECKOUT_MERCHANT_ID


    I hope this helps anyone else solve their problem if this happens to come up, I had to dig into the docs for their API to figure it out, I have no idea why they'd implement such a large breaking change but nonetheless; problem solved.

    ReplyDelete
  25. Having the same issues with Text instance has no attribute 'tagName'

    ReplyDelete
  26. Kinda late (I started with the book now).

    The problem with the XML stuff is the uri of the service, now is:

    https://sandbox.google.com/checkout/api/checkout/v2/merchantCheckout/Merchant/ + ID

    ReplyDelete
  27. Hi Jim,

    Thanks for the great book. I was getting the same error as jUaN, ColdZero and Armen B until I changed the GOOGLE_CHECKOUT_URL from 'https://sandbox.google.com/checkout/api/v2/merchantCheckout/Merchant/' + GOOGLE_CHECKOUT_MERCHANT_ID to 'https://sandbox.google.com/checkout/api/checkout/v2/merchantCheckout/Merchant/' + GOOGLE_CHECKOUT_MERCHANT_ID.

    Hope this helps anyone else with this problem.

    ReplyDelete
  28. Thanks Trevor and co - I was pulling out my hair with the "Text instance has no attribute 'tagName'" issue

    ReplyDelete
  29. I just want to say thank for your great book, it allowed me to get a better understand of django. so with it i get a way to implement everything front chapter 1 to 15 and everything is perfect. but i want to tell that couple of things are changed with django 1.5 so we have sometime to do a little research to get things works.
    i would like to know which book can you recommend to learn about google apps engine? before to be able to understand chapter 16.
    thanks again !!!!!

    ReplyDelete