Signals

basky emits a number of useful signals at certain points when interacting with the basket.

request_total_for_product

Before a product can be added to the basket we need to be aware of the total price for the item that is being added into the basket. For this reason the request_total_for_product will be sent.

Sends arguments of

  • sender - Basket
  • instance - the users basket instance
  • item - the item added to the basket

Can receive a single argument of total that will override the total from the item and be used as the total for the basketitem.

Adding To Basket

Two signals are provided for listening for products being added to the basket:

pre_add_to_basket()
post_add_to_basket()

They are similar in nature, but they are different. pre_add_to_basket will accept return values that can modify the basketitem being added to the basket whereas post_add_to_basket does not.

pre_add_to_basket

Sends arguments of

  • sender - Basket
  • instance - the users basket instance
  • item - the item added to the basket
  • quantity - quantity of items to be added to the basket

Can optionally receives a dictionary containing the following keys

  • price - Decimal : will override the price of the item
  • description - String: will override the default basketitem description
  • quantity - Integer: will override the quantity of items added to the basket
  • locked - Boolean: if True will prevent the basket line from being editable by the user. Very useful for items added as part of promotions.
  • do_not_add - Boolean: if True will prevent the item from being added to the basket.

If do_not_add is returned by any of your connected receivers and it has a value of True then the item will not be added to basket and the Basket.add() method will return False. This is very useful for performing stock checks or eligibility criteria.

Your receiver functions can return a dictionary that can override the basketitem quantity, description and price. This is done without affecting the actual product instance. This is very useful for running promotions ( 2 for 1, buy one get one free and any other elaborate promotion you can think of).

Note

Be aware that that if you have multiple receivers modifying product information then it will be the last receiver called that overrides the product information. This shouldn’t be an issue, but it is best noted as a possible reason for head scratching.

pre_add_to_basket example

Let’s assume the following product model:

class SimpleProduct(models.Model):
        name = models.CharField(max_length=255)
        price = models.DecimalField(max_digits=8,
                        decimal_places=2)

        def __unicode__(self):
                return u'%s' % self.name

        @property
        def total(self):
                return self.price

and from that model we’ve made a product called “hammer”:

hammer = SimpleProduct(
                        name="Hammer",
                        price="4.99")

and that we’re running a promotion of buy one hammer and get one free. This is very simple to achieve with basky. Here’s a rather crude receiver function:

def example_buy_one_get_one_free(sender, instance, item, quantity, **kwargs):
        # if item is a hammer, insert another hammer
        # with title 'Free Hammer! Buy One Get One Free'
        # and a price of 0.00
        if item.name == "Hammer":
                instance.add(hammer,
                        price="0.00",
                        description="Free Hammer! Buy One Get One Free",
                        locked=True,
                        silent=True)

Note

In this example we’re passing the silent=True argument because we’re adding another item to the basket. By passing silent=True the pre_add_to_basket and post_add_to_basket signals will not be sent. If we didn’t do this we’d end up reaching a maximum recursion error. And that’s just not cricket.

So let’s connect the signal and add something into our basket:

>>> pre_add_to_basket.connect(example_buy_one_get_one_free, dispatch_uid='bogof')
>>> basket.add(hammer)

Now let’s ask the basket what’s in the basket:

>>> basket.basketitem_set.all()
>>> ['<BasketItem: Free Hammer! Buy One Get One Free>', '<BasketItem: Hammer>']

And we’re only charging our customer for one

>>> basket.total_price()
>>> 4.99

That’s only the tip of the iceburg for the pre_add_to_basket and not immediately very useful, but it gives you an idea of where you can go with the pre_add_to_basket signal.

post_add_to_basket

Sends arguments of

  • sender - Basket
  • instance - the users basket instance
  • basketitem - the item added to the basket
  • quantity - quantity of items to be added to the basket

post_add_to_basket example

Let’s assume the following product model:

class SimpleProduct(models.Model):
        name = models.CharField(max_length=255)
        price = models.DecimalField(max_digits=8, decimal_places=2)

        def __unicode__(self):
                return u'%s' % self.name

        @property
        def total(self):
                return self.price

and from that model we’ve saved two products: a hammer and a bag of nails:

hammer = SimpleProduct(
                        name="Hammer",
                        price="4.99")
nails = SimpleProduct(
                        name="Bag of 500 Nails",
                        price="1.99")

And that we have a special offer of a free bag of nails with every hammer. To do this we’re going to attach a rather crude receiver function:

def free_nails_with_every_hammer(sender, instance, basketitem, **kwargs):
        # if item being added is a hammer, add some free nails!
        if basketitem.content_object.title == 'Hammer':
                nails = SimpleProduct.objects.get(title="Bag of 500 Nails")
                instance.add(nails,
                        price=Decimal("0.00"),
                        description="Free nails with every hammer",
                        silent=True,
                        locked=True)

So let’s connect the signal and add something into our basket:

>>> pre_add_to_basket.connect(free_nails_with_every_hammer, dispatch_uid='freenails')
>>> basket.add(hammer)

We now have two items:

>>> basket.basketitem_set.all()
>>> ['<BasketItem: Hammer>', '<BasketItem: Free nails with every hammer>']

And the customer is paying for one, the hammer.

>>> basket.total_price()
>>> 4.99

Removing From Basket

Two signals are provided for listening for products being removed to the basket:

pre_remove_from_basket()
post_remove_from_basket()

pre_remove_from_basket

Sends arguments of

  • sender - Basket
  • instance - the users basket instance
  • basketitem - the basketitem to be removed from the basket

Can optionally receives a dictionary containing the following keys

  • do_not_remove - Boolean : if True the item will not be removed from the basket.

This signal is sent directly before the item is to be removed from the users basket. If any of the connected receivers return do_not_remove = True then the item will not be removed from the basket.

post_remove_from_basket

Sends arguments of

  • sender - Basket
  • instance - the users basket instance
  • basketitem - the basketitem to be removed from the basket

This signal is sent directly after the item is to be removed from the users basket. It cannot modify the removing of the item from the basket in anyway.

post_remove_from_basket example

Let’s assume we were selling individually numbered products such as collectable figurines. With our collectable figurines we can only ever sell one of each number. For instance, we have to make sure that when someone adds figurine number 23 to their basket, then nobody else can add number 23 to their basket. To this we might have the following model:

class DistastefullCollectableFigurine(models.Model):
        AVAILABLE = 1
        RESERVED = 2
        SOLD = 3
        STATUS_CHOICES = (
                (AVAILABLE, 'Available'),
                (RESERVED, 'Reserved'),
                (SOLD, 'Sold'),
        )
        name = models.CharField(max_length=255)
        number = models.PositiveIntegerField()
        price = models.DecimalField(
                        max_digits=8,
                        decimal_places=2)
        status = models.PositiveIntegerField(
                        default=1,
                        choices=STATUS_CHOICES )

        def __unicode__(self):
                return u'%s' % self.name

        @property
        def total(self):
                return self.price

Firstly we’re going to need a receiver to listen for the post_add_to_basket signal that will change the status of the instance to DistastefullCollectableFigurine.RESERVED when it is placed into someones basket. It might look like this:

def take_off_sale(sender, instance, **kwargs):
        item = kwargs['basketitem'].content_object
        item.status = item.RESERVED
        item.save()

At this point it would be trivial to create a custom manager method on your product model to only return items that had a status of DistastefullCollectableFigurine.AVILABLE, but that’s outside the scope of this example.

Now we have to make sure that if someone has a DistastefullCollectableFigurine in their basket and they decide that they’d rather not buy it then we can use the post_remove_from_basket signal to connect a receiver that will change the items status back to DistastefullCollectableFigurine.AVAILABLE:

def put_back_on_sale(sender, instance, **kwargs):
        item = kwargs['basketitem'].content_object
        item.status = item.AVAILABLE
        item.save()

All you’d have to do now is connect them:

post_add_to_basket.connect(take_off_sale)
post_remove_from_basket.connect(put_back_on_sale)

And without too much hassle you can now handle numbered products with basky.

Other Signals

basket_is_now_not_empty

Sends arguments of

  • sender - Basket
  • instance - the users basket instance

This signal is sent immediately after the post_add_to_basket signal if the users basket had no items in it previous to adding the current item. It cannot modify the contents of the basket in any way.

Note

This signal will not be sent if you’ve passed silent=True to the add method on the basket

basket_is_now_empty

Sends arguments of

  • sender - Basket
  • instance - the users basket instance

This signal is sent immediately after the post_remove_from_basket signal if the users basket had items in it previous to removing the current item. It cannot modify the contents of the basket in any way.

Note

This signal will not be sent if you’ve passed silent=True to the add method on the basket