Reactoo
Reactoo

Reputation: 1042

Overwriting save method not working in Django Rest Framework

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

Answers (2)

Jordan Kowal
Jordan Kowal

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:

  • Create a signals.py file. It's best for long-term management to group your signal in a specific file
  • Put your signal code there
  • You can use decorators on your functions to make them signals, like @receiver(post_save, sender=Order)
  • In your 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:

  • The logic is still in the model, in the create_point_from_order method
  • That allows us to split logic from signal, and make it re-usable
  • And then we simply register an Order signal whose job is to simply call that point creation method

Upvotes: 1

user1600649
user1600649

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

How do signals work?

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

Related Questions