5

In my django application I have a ManytoMany relationship between Orders and Packages. An order can have multiple packages. I want to know about the update and create methods

Models.py

class Package(models.Model):

    prod_name = models.CharField(max_length=255, default=0)
    quantity = models.IntegerField(default=0)
    unit_price = models.IntegerField(default=0)

class Orders(models.Model):

    order_id = models.CharField(max_length=255, default=0)
    package = models.ManyToManyField(Package)
    is_cod = models.BooleanField(default=False)

Serializers.py

class PackageSerializer(serializers.ModelSerializer):
    class Meta:
        model = Package
        fields = "__all__"

class OrderSerializer(serializers.ModelSerializer):
    package = PackageSerializer(many=True)

    class Meta:
        model = Orders
        fields = "__all__"

Views.py

class OrdersCreateAPIView(generics.CreateAPIView):
    permission_classes = (permissions.IsAuthenticated,)
    serializer_class = OrderSerializer

    def post(self, request):

        serializer = OrderSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Is that sufficient to handle the related data? I am trying to understand ManytoMany relationship both in Django as well as DRF so please explain if I need to change the Models or views in anyway

Update:

I have updated my serializer as well as view in order to create manytomany related objectslike this:

class OrderSerializer(serializers.ModelSerializer):
    package = PackageSerializer(many=True)

    class Meta:
        model = Orders
        fields = "__all__"

    def create(self, validated_data):
        package_data = validated_data.pop('package')
        pkgs = []
        order = Orders.objects.create(**validated_data)
        for i in package_data:
            try:
                p = Package.objects.create(**i)
                pkgs.append(p)
            except:
                pass
        order.package.set(pkgs)
        return order

Views.py

class OrdersCreateAPIView(CreateAPIView):
    permission_classes = (permissions.IsAuthenticated,)
    serializer_class = OrderSerializer

    def perform_create(self,serializer):
        serializer.save(owner=self.request.user)

However I am still unclear about overriding the update method of RetrieveUpdateDestroyAPIView. Also, Is the above method is the right method to store M2M related objects ?

Please help with the update part of the serializer, I understand that I have to pass the query in the serializer

Rahul Sharma
  • 2,187
  • 6
  • 31
  • 76
  • Tip: You don't have to override the **`post(....)`**, DRF will handle it for you :) – JPG May 01 '20 at 06:58
  • DRF works seamlessly with FK and M2M relationship *if you are sending `PK` values* and it is the default behavior. In your case, you need to override the **`create()`** and **`update()`** methods of serializer – JPG May 01 '20 at 07:00
  • @ArakkalAbu I don't have to create post function ? Post is defaultly handled by the `serializers` ? And if it does, wouldn't it create the `object` so why do I have to override the create function? – Rahul Sharma May 01 '20 at 07:05
  • here the `post()` method creates a new object in DB (ideally) which is handled internally by **`generics.CreateAPIView`**, not by Serializer. – JPG May 01 '20 at 07:37
  • thanks for the info, could you kindly also help me with overriding the create function – Rahul Sharma May 01 '20 at 07:44
  • I hope you will find plenty of resources on the internet related to that, use search word something like *"writable nested serializer DRF"*. – JPG May 01 '20 at 07:47
  • 1
    https://www.django-rest-framework.org/api-guide/relations/#writable-nested-serializers – JPG May 01 '20 at 07:47
  • @ArakkalAbu I have updated the question, I am able to create the m2m relationship.. please have a look – Rahul Sharma May 06 '20 at 07:06
  • can you add the sample create payload as well as the update payload? – JPG May 06 '20 at 18:26
  • @ArakkalAbu I have update the `create` but I am not sure how to handle the `update` part – Rahul Sharma May 07 '20 at 06:31

1 Answers1

22

Working codebase

#serializers.py
class PackageSerializer(serializers.ModelSerializer):
    id = serializers.IntegerField()

    class Meta:
        model = Package
        fields = "__all__"


class OrderSerializer(serializers.ModelSerializer):
    package = PackageSerializer(many=True)

    def get_or_create_packages(self, packages):
        package_ids = []
        for package in packages:
            package_instance, created = Package.objects.get_or_create(pk=package.get('id'), defaults=package)
            package_ids.append(package_instance.pk)
        return package_ids

    def create_or_update_packages(self, packages):
        package_ids = []
        for package in packages:
            package_instance, created = Package.objects.update_or_create(pk=package.get('id'), defaults=package)
            package_ids.append(package_instance.pk)
        return package_ids

    def create(self, validated_data):
        package = validated_data.pop('package', [])
        order = Orders.objects.create(**validated_data)
        order.package.set(self.get_or_create_packages(package))
        return order

    def update(self, instance, validated_data):
        package = validated_data.pop('package', [])
        instance.package.set(self.create_or_update_packages(package))
        fields = ['order_id', 'is_cod']
        for field in fields:
            try:
                setattr(instance, field, validated_data[field])
            except KeyError:  # validated_data may not contain all fields during HTTP PATCH
                pass
        instance.save()
        return instance

    class Meta:
        model = Orders
        fields = "__all__"

#views.py
class OrderViewSet(viewsets.ModelViewSet):
    serializer_class = OrderSerializer
    queryset = Orders.objects.all()

Register this view with the help of DefaultRouter as,

from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r'order', OrderViewSet, basename='order')
urlpatterns = [

              ] + router.urls

Thus you will get basic CRUD end-points as described in that table (see the DefaultRouter ref).

Let your order list end-point be /foo-bar/order/

  1. HTTP POST to /foo-bar/order/ to create a new instance
  2. HTTP PUT or HTTP PATCH to /foo-bar/order/<ORDER_PK>/ to update the content

Note

In this case, you should pass the id value of package if you wish to map an existing package relation with the Order

References

  1. DRF ModelVieSet
  2. Django get_or_create(...)
  3. Django create_or_update(...)
  4. Django M2M set(...)
  5. DRF DefaultRouter

UPDATE-1

You can wire-up the view like this

urlpatterns = [
    path('foo/order/', OrderViewSet.as_view({'post': 'create'})),  # create new Order instance
    path('foo/order/<int:pk>/', OrderViewSet.as_view({'patch': 'partial_update'})),  # update Order instance

]

Note: This supports only HTTP POST and HTTP PATCH

JPG
  • 82,442
  • 19
  • 127
  • 206
  • 1
    How would I pass the `pk` in the Views such that I only update the selected Record? – Rahul Sharma May 07 '20 at 08:36
  • @RahulSharma I have updated the answer. Please do check it – JPG May 07 '20 at 08:47
  • The code is very well written and very helpful but I am trying to learn the basics thus I would Like to do it with URL as `foo-bar//, OrdersUpdate.as_view())`. Please show me that way, Also if you could simplify the Update, it'd be very much helpful – Rahul Sharma May 07 '20 at 09:13
  • 1
    Regarding the URL pattern, check `UPDATE-1` section. Regarding the `update(...)` method breakdown, I don't this it's a complicated piece of code. Go through it, everything in the code snippet is already in Python (so in Django) and you will get easy references to all those methods/functions/classes. – JPG May 07 '20 at 12:03
  • 1
    Thanks :) I have some stupid questions so I'll ask.. Would the `Update` will handle if I delete the `Packages` ? And, why did you write `package_instance, created ` but appended only `package_instance.pk` ? – Rahul Sharma May 07 '20 at 16:49
  • What do you mean by *delete*? Deleting a `Package` entry from Database? Have you check the doc of `update_or_create(...)` and `get_or_create(...)`? – JPG May 07 '20 at 16:52
  • By delete I meant If I am editing the `Order` and delete a particular `Package` object. For e.g. let's say `Order` object has two `packages` i.e. Foo and Bar and I delete Bar from frontend and call the request – Rahul Sharma May 07 '20 at 16:57
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/213351/discussion-between-arakkal-abu-and-rahul-sharma). – JPG May 07 '20 at 16:58
  • This doesn't work, serializer tries to validate the data before hand (django 3+) – E.Serra Aug 11 '21 at 12:03
  • You may have problem with unique field in nested model. Read https://stackoverflow.com/a/36334825/3139228 in addition to this answer. – Serhii Kushchenko Apr 12 '22 at 10:40