Reputation: 1042
I have a model (Points) which saves the points based on the purchase made by a user. I have made in such a way that when order is made (orderapi is called), a signal is passed with order instance and points are calculated based on the amount and it is saved using the save method.
Alothugh Order object is created, I dont see the points being saved in the database. I am not sure what is the issue.
My models:
class Order(models.Model):
ORDER_STATUS = (
('To_Ship', 'To Ship',),
('Shipped', 'Shipped',),
('Delivered', 'Delivered',),
('Cancelled', 'Cancelled',),
)
user = models.ForeignKey(User, on_delete=models.CASCADE, blank=True)
order_status = models.CharField(max_length=50,choices=ORDER_STATUS,default='To_Ship')
ordered_date = models.DateTimeField(auto_now_add=True)
ordered = models.BooleanField(default=False)
@property
def total_price(self):
# abc = sum([_.price for _ in self.order_items.all()])
# print(abc)
return sum([_.price for _ in self.order_items.all()])
def __str__(self):
return self.user.email
class Meta:
verbose_name_plural = "Orders"
ordering = ('-id',)
class OrderItem(models.Model):
orderItem_ID = models.CharField(max_length=12, editable=False, default=id_generator)
order = models.ForeignKey(Order,on_delete=models.CASCADE, blank=True,null=True,related_name='order_items')
item = models.ForeignKey(Product, on_delete=models.CASCADE,blank=True, null=True)
order_variants = models.ForeignKey(Variants, on_delete=models.CASCADE,blank=True,null=True)
quantity = models.IntegerField(default=1)
@property
def price(self):
total_item_price = self.quantity * self.order_variants.price
# print(total_item_price)
return total_item_price
class Points(models.Model):
order = models.OneToOneField(Order,on_delete=models.CASCADE,blank=True,null=True)
points_gained = models.IntegerField(default=0)
def collect_points(sender,instance,created,**kwargs):
total_price = instance.total_price
if created:
if total_price <= 10000:
abc = 0.01 * total_price
else:
abc = 0.75 * total_price
return abc
post_save.connect(collect_points,sender=Order)
def save(self,*args,**kwargs):
self.points_gained = self.collect_points()
super(Points, self).save(*args, **kwargs)
I am actually confused here. Can we access property total_price using instance.total_price??
My serializers:
class OrderSerializer(serializers.ModelSerializer):
billing_details = BillingDetailsSerializer()
order_items = OrderItemSerializer(many=True)
user = serializers.PrimaryKeyRelatedField(read_only=True, default=serializers.CurrentUserDefault())
#total_price = serializers.SerializerMethodField(source='get_total_price')
class Meta:
model = Order
fields = ['id','user','ordered_date','order_status', 'ordered', 'order_items','total_price', 'billing_details']
# depth = 1
def create(self, validated_data):
user = self.context['request'].user
if not user.is_seller:
order_items = validated_data.pop('order_items')
billing_details = validated_data.pop('billing_details')
order = Order.objects.create(user=user,**validated_data)
BillingDetails.objects.create(user=user,order=order,**billing_details)
for order_items in order_items:
OrderItem.objects.create(order=order,**order_items)
order.save()
return order
else:
raise serializers.ValidationError("This is not a customer account.Please login as customer.")
My updated code:
class Order(models.Model):
total_price = models.FloatField(blank=True,null=True)
def final_price(self):
return sum([_.price for _ in self.order_items.all()])
def save(self, *args, **kwargs):
self.total_price = self.final_price()
super(Order, self).save(*args, **kwargs)
class Points(models.Model):
order = models.OneToOneField(Order,on_delete=models.CASCADE,blank=True,null=True)
points_gained = models.FloatField(default=0)
def collect_points(sender,instance,created,**kwargs):
total_price = instance.total_price
print(total_price)
if created:
if total_price <= 10000:
abc = 0.01 * total_price
else:
abc = 0.75 * total_price
new_point = Points.objects.create(order=instance, points_gained=abc)
post_save.connect(collect_points,sender=Order)
Focused on this part
Class Order(models.Model):
total_price = models.FloatField(blank=True,null=True)
def final_price(self):
# abc = sum([_.price for _ in self.order_items.all()])
# print(abc)
return sum([_.price for _ in self.order_items.all()])
def save(self, *args, **kwargs):
self.total_price = self.final_price()
super(Order, self).save(*args, **kwargs)
Upvotes: 0
Views: 1999
Reputation: 1594
While you did put it WITHIN your Point
model, it does not affect it. It is linked to the Order
model. If I were to translate your signal in English, it would be: After saving a Order
instance, calculate and return the abc
variable.
At no point whatsoever are we saving or interacting with the Point
model here. So, even though you did override the Point.save()
method, you're not calling it in relation to your signal
If you want to do is to create Point
instance:
def collect_points(sender, instance, created, **kwargs):
total_price = instance.total_price
if created:
if total_price <= 10000:
abc = 0.01 * total_price
else:
abc = 0.75 * total_price
# ---> Now we can create the point
new_point = Points.objects.create(order=instance, points_gained=abc)
post_save.connect(collect_points,sender=Order)
# And no need to override the save() method
So now, our signal means After saving an Order
instance, if it was created, calculate the points and create a Point
instance from this data
Also, here's some additional tips to make your life easier when dealing with signals:
signals.py
file. It's best for long-term management to group your signal in a specific file@receiver(post_save, sender=Order)
apps.py
file, in your app class, override the ready
method to import the signals. The ready
method is automatically triggered when booting the app. So it's like "do this on boot". In our case, we'll be registering the signals on boot. Snippet example:#apps.py
from django.apps import AppConfig
class SecurityConfig(AppConfig):
name = "security"
label = "security"
def ready(self):
"""Imports signals on application start"""
import security.signals
EDIT: Using a class method and a standalone signal
# In models.py
class Points(models.Model):
order = models.OneToOneField(Order,on_delete=models.CASCADE,blank=True,null=True)
points_gained = models.IntegerField(default=0)
@classmethod
def create_point_from_order(cls, order_instance):
"""Creates a Points instance from an order"""
total_price = order_instance.total_price
if total_price <= 10000:
abc = 0.01 * total_price
else:
abc = 0.75 * total_price
return cls.objects.create(order=order_instance, points_gained=abc)
Then you can create a signal on the Order
model which calls that method
# In signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Order, Points
@receiver(post_save, sender=Order)
def create_point_for_order(sender, instance, created, **kwargs):
"""On Order creation, creates a matching Points instance"""
if created:
created_point = Points.create_point_from_order(instance)
Finally, we register that signal by making sure the signals.py
file is called on boot
# In apps.py
from django.apps import AppConfig
class YourAppNameConfig(AppConfig):
name = "your.app.name"
label = "your.app.name"
def ready(self):
import yourappname.signals
That way:
create_point_from_order
methodOrder
signal whose job is to simply call that point creation methodUpvotes: 1
Reputation:
This is one clear example, where signals are not a good fit. Both models are already under your control, you are already overriding the Order save method. There is zero need for this work to use a signal and it only complicates things.
This is the simple approach:
class Order(models.Model):
total_price = models.FloatField(blank=True,null=True)
def final_price(self):
return sum([_.price for _ in self.order_items.all()])
def save(self, force_insert=False, **kwargs):
created = force_insert or not self.pk
self.total_price = self.final_price()
super(Order, self).save(force_insert=force_insert, **kwargs)
if created:
points = Points.calculate_points(self.total_price)
Points.objects.create(order=self, points_gained=points)
class Points(models.Model):
order = models.OneToOneField(
Order,on_delete=models.CASCADE,blank=True,null=True
)
points_gained = models.FloatField(default=0)
@staticmethod
def calculate_points(amount):
return 0.01 * amount if amount <= 10000 else 0.75 * amount
Signals are ways for libraries to inject arbitrary code into a piece of code they control:
# Somebody else's code, you cannot modify
def greeting():
print('Hello ')
send_signal('after_print_hello')
print('!')
Now this code has some way to register with the 'after_print_hello' signal, this is called the receiver:
# Your code
def on_after_print_hello():
print('world')
So in the end this happens:
def greeting():
print('Hello ')
print('world') <-----------\
print('!') |
|
def on_after_print_hello(): |
print('world') -----------/
It seems absolute nonsense to use signals here, if both parts of the code are yours. You can just move the print statement to where you want it.
And if you take away all the fancy registration and receiver matching, this is exactly what is happening with Django signals as well. These post_save
signals are only useful, if you want to add extra steps in models that are not yours, after the models are saved. If you're using them for your own models, you're just moving the print('world')
statement to a different place and use Django to call it for you using a more involved API.
Upvotes: 2