skip to Main Content

I’m building a webshop where we want anonymous users to also be able to add things to their basket. I thought I’d be smart and created a Basket model like this:

class Basket(models.Model):
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.CASCADE)
    session = models.ForeignKey(Session, blank=True, null=True, on_delete=models.CASCADE)

So for logged in users I’d assign the owner field, and for anonymous users I’d assign the session field. Then when the session expires the basket would also be cleaned up, so I wouldn’t end up with a ton of database records for baskets that can never be checked out anymore. So far so good.

When an anonymous user logs in, I want to assign the basket to that user. And this is where the complications start to add up because when a user logs in they get a new session key, which is not accessible in the request. While I could create some kind of middleware that keeps track of the previous session key, the biggest problem is that by the time user_logged_in signal is called, the session record has already been deleted from the database and thus my basket has also been deleted.

I could do a few things, but none seem very appealing:

  1. Override the session’s cycle_key method which is called when logging in. This causes the old session to be deleted. If I stop that from happening, I can just assign the basket to the user in the user_logged_in signal handler. But this seems to open myself up to "session fixation" attacks?
  2. I could remove my Basket.session field, and store the basket ID in the session data instead. The session data should "survive" the key cycle and be available after logging in. The problem with this is that I will definitely end up with a bunch of old baskets for anonymous users from long long ago with long expired sessions.

The solution for cleaning up baskets belonging to expired sessions would be to tap into the pre_delete signal for the session model I guess, and then read the contents of the session using its get_decoded method. If it has a basket ID, then remove the basket (if it doesn’t belong to a user).

Is the pre_delete signal even called when sessions are deleted via the clearsessions management command? Would it kill my server if I add a bunch of logic there?

Am I missing an ever simpler solution?

Also, at the moment I am using the database backend for sessions, mainly because I thought it would be handy to create that session link from the Basket model. But if that field has to go, I might as well swap to another backend, like Redis. And then there’s no pre_delete signal anymore, right? So how would I then know when a session is about to be deleted?

2

Answers


  1. Chosen as BEST ANSWER

    When you use the database backend for sessions, then calling the clearsessions command will end up calling cls.get_model_class().objects.filter(expire_date__lt=timezone.now()).delete(), which will indeed call the pre_delete signal. When using the pure cache backend then clearsessions ends up doing nothing at all. The cached_db backend should also send the signal.

    With that knowledge, I refactored my Basket model to remove its session field, and instead I now store the basket ID inside of the session. And in the pre_delete signal handler I then clean up the basket belonging to the expired session.

    I wrote a more detailed answer here, with code:

    https://www.loopwerk.io/articles/2023/django-sessions-pre-delete/

    Maybe I missed a simpler solution, but this seems to work fine.


  2. You can check how django oscar and oscar api handles their basket/cart, here, using Custom Header Session Middleware.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search