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:
- 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 theuser_logged_in
signal handler. But this seems to open myself up to "session fixation" attacks? - 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
When you use the database backend for sessions, then calling the
clearsessions
command will end up callingcls.get_model_class().objects.filter(expire_date__lt=timezone.now()).delete()
, which will indeed call thepre_delete
signal. When using the pure cache backend thenclearsessions
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 thepre_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.
You can check how django oscar and oscar api handles their basket/cart, here, using Custom Header Session Middleware.