Satchmo Signals and Real-Time Price Changes
If there is one thing a developer using Satchmo in a project is going to want to master, that one thing should be signals. Django has offered all of us a fantastic framework for notifying other bits of code that something important has happened, and that they should respond. This is the kind of feature that, in my opinion, really makes Django one of the killer frameworks that have emerged for web application development, and a real pleasure to work with. After programming on ASP, .NET, and now Django, I am very impressed with Django's right balance of a powerful internal event system that is also simple to use and extend for one's own needs. Thankfully, the Satchmo developers have put the Django signals framework to good use, creating a number of well-placed signals that those of using Satchmo can leverage to almost infinitely expand Satchmo to be exactly what we need it to be.
So, if we want to be able to sell items through our store at both US and international prices, we need a way to ensure that we determine what price needs to be used when the item is added, and ensure that we are adding each item at the correct price every time. We took care of the first requirement in the previous article, with our listener on Satchmo's cart_add_view signal. The second task must be completed each time an item is added to the cart. Luckily, the Satchmo signals documentation is much better than it was when we began our work, and if you'll review them, you'll find that satchmo_cart_details_query is the best signal to use -- it is sent each time an item is added to cart and provides a details argument that can be used to notify Satchmo of a price_change value that will be used to update the price of each item in the cart.
We already connected our intl_price_check listener to the satchmo_cart_details_query signal in the previous article if you need to review that. In this article, we are going to focus our attention on the intl_price_check listener and how we price the items at each invocation of Satchmo's cart.add() method. So, let's take a look at our listener:
# listeners.py
def intl_price_check(sender, product, details, request, **kwargs):
'''
Listener connected to satchmo_cart_details_query() signal
First check for US or international customer
Then provide appropriate price_us or price_intl value
'''
from product.models import Product
from app.models import Volume, Issue, Article
from app.utils import is_us_customer, get_custom_product
cart = sender
prod = Product.objects.select_related().get(id=product.id)
prod = get_custom_product(prod)
# Get price_level for custom products
if isinstance(prod,Volume):
prod = prod.volume.price_level
elif isinstance(prod,Issue):
prod = prod.issue.price_level
else:
prod = prod.article.price_level
# Return proper price
if is_us_customer(cart, request):
details.append({
'name':'order_type',
'value':'US',
'sort_order':'0',
'price_change':prod.price_us
})
else:
details.append({
'name':'order_type',
'value':'INTL',
'sort_order':'0',
'price_change':prod.price_intl
})
Let's briefly go over what's happening. Our intl_price_check listener is defined as expecting the sender, product, and details arguments, as well as any other **kwargs, passed by the satchmo_cart_details_query. The Satchmo signals documentation explains all of the parameters passed by the signal, though you can probably guess at some of them by reading the code above -- sender is the Cart object, product is the Product instance being added to the cart, and details is a list of dictionaries that map to Satchmo's CartItemDetails model. The key attribute we are interested in is the price_change attribute, which allows us to provide Satchmo the proper US or international price for every product. You'll also see that we are again calling on our is_us_customer() utility method here to decide which price to use when adding an item to the cart.
A brief explanation would likely be helpful here to further understand how this all comes together. If you recall from the previous article, a Satchmo Product does not have a standard price value. Satchmo handles prices via a separate Price model that is queried via Product.slug for template display and adding items to the cart. Lucky for us, this means that every Product added to our Satchmo catalog via our earlier model inheritance work is essentially saved with a price of $0.00. So, as you can see in our listener, we don't actually have to perform any price calculations at this point. Once we know which of our custom products are being added to the cart, we can simply pass the appropriate price_level value as the details.price_change attribute from our listener. And, just to remind you, details is a list of dictionaries -- we use the append method to pass the details list a new dictionary that will be saved as a CartItemDetails instance for the current CartItem.
Another important piece of this code to point out is the call to another utility function -- get_custom_product(). If you notice in the line before calling get_custom_product(), we have set prod = Product.objects.select_related().get(id=product.id). First off, recall that product is passed to our listener from the satchmo_cart_details_query signal, and is an instance of the product a customer is adding to his or her cart. We are playing matters a bit on the safe side here by re-querying that same Product with a select_related() added because our custom Volume, Issue, and Article products are considered by Django to be related models due to model inheritance, and it is on each of those models that our CustomPrices model has a ForeignKey. So, the easiest way to retrieve the US and international prices is by means of a related lookup for product.custom_product.price_level.price_us or product.custom_product.price_level.price_intl respectively. Our get_custom_product() utility function looks like this:
# utils.py
def get_custom_product(product):
"""
We are not saving prices as Satchmo expects
Find out which custom product we're adding
"""
from django.core.exceptions import ObjectDoesNotExist
for attr in ('volume','issue','article'):
try:
return getattr(product,attr)
except ObjectDoesNotExist:
pass
raise TypeError("Custom Product not found.")
In short, this is merely a helper function to return which of our custom product attributes are found in the given Product added to the cart. Then we perform a simple isinstance() check to determine which of our custom products the customer added to the cart, as is shown in the listener code above. Doing so allows us to ensure we can always grab the price_level for each of our custom product models via Django's related lookup notation.
US and International Pricing
That pretty much wraps up all the code we need to enable Satchmo to intelligently add items to our cart in real-time with custom US and international price levels. If, by chance, you've actually followed along with any of the coding on your own, you should now see your Satchmo store freshly extended with dual-pricing abilities that weren't "in the box". Additionally, we have further achieved our goal of providing our Satchmo store this functionality without touching a line of Satchmo code -- so we can continue to update Satchmo trunk without fear that our custom code is going to break (well ... as long as the devs don't drastically alter something in the signals). Of course, we would be remiss if we did not mention that deciding to take your own store in such a direction is going to require you to make some changes to Satchmo's templates, of course. The templates that ship with Satchmo are, naturally, designed for the default pricing behavior found in the Price model. However, if you are selling items at different prices for different customers in such a way as we have explained here, you can always do something like this in your templates:
${{ product.custom_product.price_level.price_us}} US
${{ product.custom_product.price_level.price_intl}} INTL
How far you want to go with Satchmo is entirely up to you, but we can definitely guarantee that the more you learn about Satchmo signals, the more mileage you are likely to get out of the project in your own stores. As usual, feel free to leave any comments with suggestions, corrections, or any of your thoughts on the matter. Thanks.
