2012-05-14 5 views
6

Vorrei che tastypie crei una UserProfileResource come risultato di POST a una risorsa utente.Creazione di risorse correlate con Tastypie

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, 
       } 

comando ricciolo:

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/ 

Ma io sono sempre:

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

Sembra uno scenario di uova e galline. Ho bisogno di user_id per creare UserProfileResource e ho bisogno del profilo per creare UserResource. Ovviamente sto facendo qualcosa di molto sciocco.

Qualcuno può illuminare una luce? Molte grazie johnoc

ho modificato il mio codice come Pablo suggerito di seguito.

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' 

Ma sto ricevendo:

Django Version: 1.4 
Exception Type: DoesNotExist 

che si riferisce al tentativo di accedere alla risorsa utente nella ORM e non esistente mentre la sua creazione del UserProfileResource related_objects. Che è corretto. L'ORM utente non viene creato fino a dopo la creazione dell'oggetto related_objects.

Qualcun altro ha visto questo ??

+0

Ho lo stesso problema qui. – Pablo

risposta

15

Dopo 2 giorni sono finalmente riuscito a risparmiare risorse correlate, il problema è che bisogna specificare entrambi i lati della relazione ei loro nomi relativi, nel tuo caso sarebbe qualcosa di simile:

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) 
+1

Grazie Pablo, sono stati 2 giorni anche per me! Molto apprezzato. – johnoc

+1

In realtà avrei parlato troppo presto. Sembrava funzionare quando ho eseguito un post. Ma quando ho provato di nuovo ho ottenuto quanto segue: Il campo 'utente' non ha dati e non consente un valore nullo. – johnoc

+3

Per chiarire, ho funzionato ora ma ho dovuto rendere l'utente archiviato un "ToManyField" (come suggeriva Pablo nel suo commento). E ho anche dovuto aggiungere "null = True" al campo del profilo. Altrimenti le operazioni GET non funzionerebbero. Comunque va tutto meglio adesso. Grazie per tutto il tuo aiuto Pablo! – johnoc

0

EDIT # 2: Finalmente ho capito come sistemare le cose, ma sfortunatamente richiede un po 'di sottoclassi e override. Ecco come ho ottenuto che funziona:

In primo luogo, creare una nuova sottoclasse campo - ho chiamato il mio RelatedToOneField:

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's 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 

poi sovrascrivere l'obj_create & funzioni save_related nel modello "top", o in questo caso, UserResource. Ecco le sostituzioni rilevanti:

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's ``None`` & that's 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) 

Dopo aver aggiunto quelli al vostro API, tutto dovrebbe funzionare (almeno su 0.9.11). La parte principale della correzione è che related_obj's non è stato aggiunto correttamente per ToOneField. La sottoclasse My RelatedToOneField implementa questo controllo nel codice del campo idrato.

MODIFICA: Ho sbagliato ancora, ToOneField continua a non funzionare in 0.9.12. Il mio trucco era che esisteva già una UserProfileResource con gli stessi dati che stavo cercando di pubblicare nel database.Ha appena afferrato quella riga e l'ha modificata invece di creare qualcosa di nuovo.


Dopo aver trascorso anche modo troppo tempo su questo, sembra che ci fosse un bug per ToOneField di che è stato risolto nella versione 0.9.12 (vedi commenti in risposta accettato di Pablo per la discussione del caso).

Se django-tastypie> = 0.9.12, il seguente dovrebbe funzionare:

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') 

se django-tastypie < 0.9.12, è necessario effettuare le seguenti operazioni:

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') 

Nota: è stato cambiato l'ordine di UserResource & UserProfileResource poiché ciò aveva più senso per il mio modello mentale.