Creating Related Resources with Tastypie

I would like tastypie to create a UserProfileResource as a result of my POSTing for UserResource.

models.py:

class UserProfile(models.Model): home_address = models.TextField() user = models.ForeignKey(User, unique=True) 

resources.py

 class UserProfileResource(ModelResource): home_address = fields.CharField(attribute='home_address') class Meta: queryset = UserProfile.objects.all() resource_name = 'profile' excludes = ['id'] include_resource_uri = False class UserResource(ModelResource): profile = fields.ToOneField(UserProfileResource, 'profile', full=True) class Meta: queryset = User.objects.all() resource_name = 'user' allowed_methods = ['get', 'post', 'delete', 'put'] fields = ['username'] filtering = { 'username': ALL, } 

curl command:

 curl -v -H "Content-Type: application/json" -X POST --data '{"username":"me", "password":"blahblah", "profile":{"home_address":"somewhere"}}' http://127.0.0.1:8000/api/user/ 

But I get:

  Django Version: 1.4 Exception Type: IntegrityError Exception Value: null value in column "user_id" violates not-null constraint 

This is like a chicken and egg scenario. I need a user_id to create a UserProfileResource, and I need a profile to create a UserResource. Obviously, I am doing something very stupid.

Can anyone shine there? Thanks a lot johnoc

I changed my code as Pablo suggested below.

 class UserProfileResource(StssRessource): home_address = fields.CharField(attribute='home_address') user = fields.ToOneField('resources.UserResource', attribute='user', related_name='profile') class Meta: queryset = UserProfile.objects.all() resource_name = 'profile' class UserResource(ModelResource): profile = fields.ToOneField('resources.UserProfileResource', attribute='profile', related_name = 'user', full=True) class Meta: queryset = User.objects.all() resource_name = 'user' 

But I get:

  Django Version: 1.4 Exception Type: DoesNotExist 

Which is associated with an attempt to access a user resource in ORM, and it does not exist when creating the associated_object UserProfileResource. What is right. A custom ORM is not created until related objects are created.

Has anyone else seen this?

+6
source share
2 answers

After 2 days, I managed to save the related resources, the problem was that you need to indicate both sides of the relationship and the names associated with them, in your case it will be something like this:

 class UserProfileResource(ModelResource): home_address = fields.CharField(attribute='home_address') user = fields.ToOneField('path.to.api.UserResource', attribute='user', related_name='profile') #in my case it was a toManyField, I don't know if toOneField works here, you can try toManyField. class UserResource(ModelResource): profile = fields.ToOneField(UserProfileResource, 'profile', related_name='user', full=True) 
+15
source

EDIT # 2: I finally figured out how to fix the situation, but unfortunately, this requires a few subclasses and overrides. This is how I got the job:

First create a new subclass of the class - I named my Associated ToOneField:

 from tastypie.bundle import Bundle from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from tastypie.exceptions import ApiFieldError, NotFound class RelatedToOneField(fields.RelatedField): """ Provides access to related data via foreign key. This subclass requires Django ORM layer to work properly. """ help_text = 'A single related resource. Can be either a URI or set of nested resource data.' def __init__(self, to, attribute, related_name=None, default=fields.NOT_PROVIDED, null=False, blank=False, readonly=False, full=False, unique=False, help_text=None): super(RelatedToOneField, self).__init__( to, attribute, related_name=related_name, default=default, null=null, blank=blank, readonly=readonly, full=full, unique=unique, help_text=help_text ) self.fk_resource = None def dehydrate(self, bundle): try: foreign_obj = getattr(bundle.obj, self.attribute) except ObjectDoesNotExist: foreign_obj = None if not foreign_obj: if not self.null: raise ApiFieldError("The model '%r' has an empty attribute '%s' and doesn't allow a null value." % (bundle.obj, self.attribute)) return None self.fk_resource = self.get_related_resource(foreign_obj) fk_bundle = Bundle(obj=foreign_obj, request=bundle.request) return self.dehydrate_related(fk_bundle, self.fk_resource) def hydrate(self, bundle): value = super(RelatedToOneField, self).hydrate(bundle) if value is None: return value # START OF MODIFIED CONTENT kwargs = { 'request': bundle.request, } if self.related_name: kwargs['related_obj'] = bundle.obj kwargs['related_name'] = self.related_name return self.build_related_resource(value, **kwargs) #return self.build_related_resource(value, request=bundle.request) #END OF MODIFIED CONTENT 

Then override the obj_create and save_related functions in your "top" model, or in this case UserResource. Here are the relevant overrides:

 def obj_create(self, bundle, request=None, **kwargs): """ A ORM-specific implementation of ``obj_create``. """ bundle.obj = self._meta.object_class() for key, value in kwargs.items(): setattr(bundle.obj, key, value) bundle = self.full_hydrate(bundle) # Save the main object. # THIS HAS BEEN MOVED ABOVE self.save_related(). bundle.obj.save() # Save FKs just in case. self.save_related(bundle) # Now pick up the M2M bits. m2m_bundle = self.hydrate_m2m(bundle) self.save_m2m(m2m_bundle) return bundle def save_related(self, bundle): """ Handles the saving of related non-M2M data. Calling assigning ``child.parent = parent`` & then calling ``Child.save`` isn't good enough to make sure the ``parent`` is saved. To get around this, we go through all our related fields & call ``save`` on them if they have related, non-M2M data. M2M data is handled by the ``ModelResource.save_m2m`` method. """ for field_name, field_object in self.fields.items(): if not getattr(field_object, 'is_related', False): continue if getattr(field_object, 'is_m2m', False): continue if not field_object.attribute: continue # Get the object. # THIS HAS BEEN MOVED ABOVE the field_object.blank CHECK try: related_obj = getattr(bundle.obj, field_object.attribute) except ObjectDoesNotExist: related_obj = None # THE 'not related_obj' CHECK HAS BEEN ADDED if field_object.blank and not related_obj: # ADDED continue # Because sometimes it ``None`` & that OK. if related_obj: # THIS HAS BEEN ADDED setattr(related_obj, field_object.related_name, bundle.obj) # ADDED related_obj.save() setattr(bundle.obj, field_object.attribute, related_obj) 

After you add them to your API, everything should work (at least 0.9.11). The main part of the fix - related_obj was not added properly for ToOneField. The RelatedToOneField subclass implements this check in field hydrated code.

EDIT: I made a mistake again, ToOneField still doesn't work in 0.9.12. I realized that there is already a UserProfileResource with the same data that I was trying to publish to the database. He simply grabbed this line and modified it instead of creating something new.


After you spent too much time on this, it seems that there was a bug for ToOneField that was fixed in version 0.9.12 (see comments in Pablo for an answer to the corresponding discussion).

If django-tastypie> = 0.9.12, then the following should work:

 class UserResource(ModelResource): profile = fields.ToOneField('path.to.api.UserProfileResource', 'profile', related_name='user', full=True) class UserProfileResource(ModelResource): home_address = fields.CharField(attribute='home_address') user = fields.ToOneField(UserResource, attribute='user', related_name='profile') 

If django-tastypie <0.9.12, you need to do the following:

 class UserResource(ModelResource): profile = fields.ToOneField('path.to.api.UserProfileResource', 'profile', related_name='user', full=True) class UserProfileResource(ModelResource): home_address = fields.CharField(attribute='home_address') user = fields.ToManyField(UserResource, attribute='user', related_name='profile') 

Note: the order of UserResource and UserProfileResource switched, as this made more sense for my mental model.

0
source

Source: https://habr.com/ru/post/915662/


All Articles