Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

django-tastypie: Posting to a Resource having ManytoMany field with through relationship

I'm working on a API for a project and I have a relationship Order/Products through OrderProducts like this:

In models.py

class Product(models.Model):
    ...

class Order(models.Model):
    products = models.ManyToManyField(Product, verbose_name='Products', through='OrderProducts')
    ...

class OrderProducts(models.Model):
    order = models.ForeignKey(Order)
    product = models.ForeignKey(Product)
    ...

Now, when I load an Order through the API I'd like to get the related Products as well, so I tried this (with django-tastypie):

In order/api.py

class OrderResource(ModelResource):
    products = fields.ToManyField('order.api.ProductResource', products, full=True)

    class Meta:
        queryset = Order.objects.all()
        resource_name = 'order'

Everything works for listing Order resources. I get order resources with product data embedded.

The problem is that I am not able to create or edit Order objects using the api. Since I am using a through model in ManytoMany relation, the ManyToManyField(products) does not have the .add() methods. But tastypie is trying to call .add() on the products field in OrderResource when posting/putting data to it.

{"error_message": "'ManyRelatedManager' object has no attribute 'add'", "traceback": "Traceback (most recent call last):\n\n  File \"/Library/Python/2.7/site-packages/tastypie/resources.py\", line 192, in wrapper\n    response = callback(request, *args, **kwargs)\n\n  File \"/Library/Python/2.7/site-packages/tastypie/resources.py\", line 397, in dispatch_list\n    return self.dispatch('list', request, **kwargs)\n\n  File \"/Library/Python/2.7/site-packages/tastypie/resources.py\", line 427, in dispatch\n    response = method(request, **kwargs)\n\n  File \"/Library/Python/2.7/site-packages/tastypie/resources.py\", line 1165, in post_list\n    updated_bundle = self.obj_create(bundle, request=request, **self.remove_api_resource_names(kwargs))\n\n  File \"/Library/Python/2.7/site-packages/tastypie/resources.py\", line 1784, in obj_create\n    self.save_m2m(m2m_bundle)\n\n  File \"/Library/Python/2.7/site-packages/tastypie/resources.py\", line 1954, in save_m2m\n    related_mngr.add(*related_objs)\n\nAttributeError: 'ManyRelatedManager' object has no attribute 'add'\n"}
like image 861
shreyj Avatar asked Dec 07 '12 17:12

shreyj


3 Answers

Since you needed the manytomany field only for listing, a better solution is to add readonly=True on OrderResource's products field. This removes the need of overriding save_m2m method. For completeness:

class OrderResource(ModelResource):
    products = fields.ToManyField('order.api.ProductResource', products, 
                                  readonly=True, full=True)

    class Meta:
        queryset = Order.objects.all()
        resource_name = 'order'
like image 82
TasDiam Avatar answered Nov 09 '22 10:11

TasDiam


The solution lies in overriding the save_m2m() method on the resource. In my case I needed the manytomany field for only listing, so overridden the save_m2m() method to do nothing.

like image 25
shreyj Avatar answered Nov 09 '22 09:11

shreyj


If you are allowed to modify class OrderProducts, adding auto_created = True might solve your problem, i.e.,

class OrderProducts(models.Model): 
    class Meta:
        auto_created = True

If you cannot change class OrderProducts, try the following tastypie patch.

---------------------------- tastypie/resources.py ----------------------------
index 2cd869e..aadf874 100644
@@ -2383,7 +2383,20 @@ class BaseModelResource(Resource):
                     related_resource.save(updated_related_bundle)
                 related_objs.append(updated_related_bundle.obj)

-            related_mngr.add(*related_objs)
+            if hasattr(related_mngr, 'through'):
+                through = getattr(related_mngr, 'through')
+                if not through._meta.auto_created:
+                    for related_obj in related_objs:
+                        args = dict()
+                        args[related_mngr.source_field_name] = bundle.obj
+                        args[related_mngr.target_field_name] = related_obj
+                        through_obj = through(**args)
+                        through_obj.save()
+                else:
+                    related_mngr.add(*related_objs)
+            else:
+                related_mngr.add(*related_objs)

     def detail_uri_kwargs(self, bundle_or_obj):
         """

In Django 1.7, the error message is changed to "Cannot set values on a ManyToManyField which specifies an intermediary model". The solution is the same.

like image 20
Thomas - BeeDesk Avatar answered Nov 09 '22 11:11

Thomas - BeeDesk