I want to modify a foreign key value when its deleted from the database. So I looked upon the doc and used on_delete=models.SET(foo) method. https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.SET
This is my model definition
class OrderLine(models.Model):
product = models.ForeignKey(Product, on_delete=models.SET(getDuplicateProduct), null=True)
quantity = models.PositiveSmallIntegerField(default=1)
finalPricePerUnit = models.PositiveIntegerField()
order = models.ForeignKey(Order, on_delete=models.PROTECT)
dateCreated = models.DateTimeField(auto_now=False, auto_now_add=True)
And this is my method which is called on delete
def getDuplicateProduct(orderline):
productToDelete = orderline.product
# some logic to generate duplicate copy and returning it
However the problem here is that I can't pass argument to this method which is why I cannot know which product was deleted. I also tried using signals as specified in this answer django model on_delete pass self to models.SET()
I also tried using signals, but that also didn't work. I can't seem to find out a proper solution for this. Let me know if someone has an idea on how to achieve this.
EDIT
This is the code I am using in signals
@receiver(pre_delete, sender=Product)
def getDuplicateProduct(sender, **kwargs):
product = kwargs['instance']
orderlines = product.orderline_set.all()
#further processing
Now the problem is that django tries to delete my orderlines as well (as default on_delete is set to cascade). And if I set the on_Delete to SET_NULL, it sets the foreign key to null.
EDIT -2 Here is the code I am using
@receiver(pre_delete, sender=Product)
def getDuplicateProduct(sender, **kwargs):
product = kwargs['instance']
orderlines = product.orderline_set.all()
product.name = product.name + ' ' + product.get_type_display()
newProduct = deepcopy(product)
newProduct.name = product.name + ' ' + product.get_type_display()
newProduct.pk=None
newProduct.id=None
newProduct.save()
product.duplicateProductId = newProduct.id
product.old_orderlines = orderlines
product.save()
@receiver(post_delete, sender=Product)
def handlePostDelete(sender, **kwargs):
product = kwargs['instance']
newProduct = Product.objects.get(id=product.duplicateProductId)
for orderline in product.old_orderlines:
orderline.product = newProduct
orderline.save()
EDIT-3 Posting full implementation for completeness.
@receiver(pre_delete, sender=Product)
def handlePreDelete(sender, **kwargs):
product = kwargs['instance']
orderlines = product.orderline_set.all()
shouldCreate=False
for orderline in orderlines:
if orderline.order.status>1:
shouldCreate=True
product.shouldCreate = shouldCreate
if shouldCreate:
product.old_orderlines = orderlines
product.save()
else:
product.save()
return None
@receiver(post_delete, sender=Product)
def handlePostDelete(sender, **kwargs):
product = kwargs['instance']
shouldCreate = product.shouldCreate
if shouldCreate:
newProduct = deepcopy(product)
newProduct.name = product.name + ' ' + product.get_type_display()
newProduct.pk=None
newProduct.id=None
newProduct.save()
# Do whatever you want with product.old_orderlines
for orderline in product.old_orderlines:
orderline.product = newProduct
orderline.save()
Signals are the right way to do this.
You can get the OrderLine
from the signal receiver:
@receiver(pre_delete, sender=Product)
def getDuplicateProduct(sender, **kwargs):
product = kwargs['instance']
orderlines = product.orderline_set.all()
# orderlines contains all the OrderLines foreign keyed to the product.
orderlines
is a queryset which you can iterate over or update in bulk.
EDIT
Turns out the approach suggested above will not work, because by the time the pre_delete
signal has fired Django has already determined which related models it needs to process with on_delete
, and will overwrite these changes.
This approach will work, though it is a bit clunky:
First, in a pre_delete
receiver:
from copy import copy
@receiver(pre_delete, sender=Product)
def handlePreDelete(sender, **kwargs):
product = kwargs['instance']
# Store the OrderLines as a property of the object
# Have to copy it otherwise it will be empty later
product.old_orderlines = copy(product.orderline_set.all())
Then, in a post_delete
receiver:
@receiver(post_delete, sender=Product)
def handlePostDelete(sender, **kwargs):
product = kwargs['instance']
# Do whatever you want with product.old_orderlines
for line in product.old_orderlines:
# ...
In between these two events, Django will have performed SET_NULL
(or whatever you configured) on the OrderLines.