Creating related resources with Tastypie - django

I would like tastypie to create a UserProfileResource as a result of me POSTing to a 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 am getting:
Django Version: 1.4
Exception Type: IntegrityError
Exception Value:
null value in column "user_id" violates not-null constraint
It seems like a chicken and egg scenario. I need the user_id to create the UserProfileResource and I need the profile to create the UserResource. Obviously I am doing something very silly.
Can anyone out there shine a light?
Many thanks
johnoc
I modified 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 am getting :
Django Version: 1.4
Exception Type: DoesNotExist
Which relates to trying to access the User resource in the ORM and it not existing while its creating the related_objects UserProfileResource. Which is correct. The User ORM isnt created until after the related_objects have been created.
Anyone else seen this??

After 2 days I finally managed to save related resources, the problem was that you have to specify both sides of the relation and their related names, in your case it would be something like that:
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)

EDIT #2: Finally figured out how to fix things, but unfortunately it requires a bit of subclassing and overrides. Here's how I got it working:
First, create a new field subclass - I called my 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
Then override the obj_create & save_related functions in your "top" model, or in this case, UserResource. Here's 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'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)
After you add those to your API, everything should work (At least on 0.9.11). The primary part of the fix is the related_obj's weren't be added properly for ToOneField's. My RelatedToOneField subclass implements this check into the field hydrate code.
EDIT: I was wrong again, ToOneField's still don't work in 0.9.12. My gotcha was that there was already a UserProfileResource with the same data I was trying to post in the database. It just grabbed that row and modified it instead of creating something new.
After also spending way too much time on this, it seems that there was a bug for ToOneField's that was fixed in version 0.9.12 (see comments in Pablo's accepted answer for relevant discussion).
If django-tastypie >= 0.9.12, 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'll 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: switched the order of UserResource & UserProfileResource since that made more sense for my mental model.

Related

Django Rest Framework unique together validation with field absent from request

I'm implementing some voting functionality in an application, where a logged-in user specifies a post that they would like to vote for using a payload like this:
{
"post": 1,
"value": 1
}
As you can tell, the a user field is absent - this is because it gets set in my viewset's perform_create method. I've done this to ensure the vote's user gets set server side. This is what the viewset looks like:
class CreateVoteView(generics.CreateAPIView):
permission_classes = (permissions.IsAuthenticated,)
serializer_class = VoteSerializer
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Here is what the model looks like:
class Vote(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='votes', null=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='votes', null=False)
class Values(models.IntegerChoices):
UP = 1, _('Up')
DOWN = -1, _('Down')
value = models.IntegerField(choices=Values.choices, null=False)
class Meta:
unique_together = ('post', 'user')
and finally, the serializer:
class VoteSerializer(serializers.ModelSerializer):
class Meta:
model = Vote
fields = ['post', 'value']
From what I understand, in order for DRF to enforce a unique together validation, both fields (in my case, user and post) must be included in the serializer's fields. As I've mentioned, I'd like to avoid this. Is there any other way of implementing this type of validation logic?
EDIT:
To clarify: the records do not save - I receive this error:
django.db.utils.IntegrityError: (1062, "Duplicate entry '1-3' for key 'api_vote.api_vote_post_id_user_id_73614533_uniq'")
However, my goal is to return a Bad Request instead of an Internal Server Error much like I would when traditionally using a DRF serializer and excluding required fields from a payload.
To output a custom error message due to the IntegrityError, you can override the create method in your serializer:
from django.db import IntegrityError
class VoteSerializer(serializers.ModelSerializer):
class Meta:
model = Vote
fields = ['post', 'value']
def create(self, validated_data):
try:
validated_data['user'] = self.context['request'].user
return super().create(validated_data)
except IntegrityError:
error_msg = {'error': 'IntegrityError message'}
raise serializers.ValidationError(error_msg)
You can try this on your views
try:
MoviesWatchList.objects.create(user=request.user, content=movie)
return response.Response({'message': f'{movie} added in watchlist.'}, status=status.HTTP_201_CREATED)
except:
return response.Response({'message': f'{movie} already added to watchlist.'}, status=status.HTTP_304_NOT_MODIFIED)

Django DRF rename field in serializer but unique_together validation fails

In the serializer used to create a model I want to rename my model field to (field_name)_id so it's clearer for API consumers that this field is an ID field. The model also has a unique_together constraint on some fields. However when validation runs in the serializer, it fails with a KeyError that the field does not exist:
...rest_framework/utils/serializer_helpers.py", line 148, in __getitem__
return self.fields[key]
KeyError: 'question'
Is there a simple way to get this to work? Minimal example code below.
Model
class MyModel(Model):
question = ForeignKey('uppley.Question', null=False, on_delete=PROTECT)
user = ForeignKey('catalystlab.User', null=False, on_delete=PROTECT)
class Meta:
unique_together = ('question', 'user',)
Serializer
class MyCreateSerializer(ModelSerializer):
question_id = PrimaryKeyRelatedField(
write_only=True,
source='question',
queryset=Question.objects.all(),
)
user_id = PrimaryKeyRelatedField(
write_only=True,
source='user',
queryset=User.objects.all(),
)
class Meta:
model = MyModel
fields = ('question_id', 'user_id',)
test.py - test for demonstration purposes
question = QuestionFactory()
user = UserFactory()
data = {
'question_id': question.id,
'user_id': user.id,
}
serializer = MyCreateSerializer(data=data, write_only=True)
is_valid = serializer.is_valid(raise_exception=True) #KeyError exception raised here.
Previously with DRF 3.10.3 this all worked fine, however with 3.11.0 this now throws a KeyError as mentioned above.
What I have tried
Removing the source field on PrimaryKeyRelatedField for user_id and question_id in the Serializer actually results in bypassing the unique_together validation in DRF and the KeyError is avoided. However the validated data is not mapped back to the original field names (user and question). In this case we have to manually change the keys back to their original names before we can create an instance of the Model from the validated data.
Is there a better way to do this?
You can make a custom serializer like :-
class MyCreateSerializer(serializers.Serializer):
question_id = serializers.PrimaryKeyRelatedField(
write_only=True,
queryset=Question.objects.all(),
)
user_id = PrimaryKeyRelatedField(
write_only=True,
queryset=User.objects.all(),
)
and make custom create function in it for creating object. like :-
def create(self, validated_data):
try:
question = validated_data.get('question_id')
user = validated_data.get('user_id')
instance = MyModel.objects.create(question=question, user=user)
except TypeError:
raise TypeError("Something went wrong while creating objects")
return instance

CreateAPIView with Empty Serializer (no field)

I'm implementing 'Follow' CreateAPIView.
FollowCreateAPIView
- gets 'logged-in User' from self.request.user
- gets 'user's id' who 'Logged-in User' wants to follow (from url)
- with the information above, Follow create new data!
Here is urls.py
url(r'^follow/(?P<user_id>\d+)$', FollowCreateAPIView.as_view(), name="user_profile"),
Model
class Follow(models.Model):
follower = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='follower')
following = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='following')
created_at = models.DateTimeField(auto_now_add=True)
CreateAPIView (I can't get user_id from URL; print(following_id) returns None
)
class FollowCreateAPIView(generics.CreateAPIView):
queryset = Follow.objects.all()
serializer_class = FollowCreateSerializer
def perform_create(self, serializer):
following_id = self.request.GET.get('user_id', None)
print(following_id) <<<<- HERE!
following_user = User.objects.filter(id=following_id)
serializer.save(follower=self.request.user, following=following_user)
Serializer
class FollowCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Follow
fields = ()
Could you fix this problem? And this Like feature seems very common. Do you have any better approach? (if you have...)
Thank you so much!
I think you can get the value with kwargs. Also, you can use get instead of filter as id is incremental and unique.
try:
following_user = User.objects.get(id=self.kwargs['user_id'])
except User.DoesNotExist:
raise Http404
Thanks Risadinha for pointing that out.

Per Field Permission in Django REST Framework

I am using Django REST Framework to serialize a Django model. I have a ListCreateAPIView view to list the objects and a RetrieveUpdateDestroyAPIView view to retrieve/update/delete individual objects. The model stores information that the users submit themselves. The information they submit contains some private information and some public information. I want all users to be able to list and retrieve the public information but I want only the owner to list/retrieve/update/delete the private information. Therefore, I need per-field permissions and not object permissions.
The closest suggestion I found was https://groups.google.com/forum/#!topic/django-rest-framework/FUd27n_k3U0 which changes the serializer based on the request type. This won't work for my situation because I don't have the queryset or object at that point to determine if it is owned by the user or not.
Of course, I have my frontend hiding the private information but smart people can still snoop the API requests to get the full objects. If code is needed, I can provide it but my request applies to vanilla Django REST Framework designs.
How about switching serializer class based on user?
In documentation:
http://www.django-rest-framework.org/api-guide/generic-views/#get_serializer_classself
def get_serializer_class(self):
if self.request.user.is_staff:
return FullAccountSerializer
return BasicAccountSerializer
I had a similar problem the other day. Here is my approach:
This is a DRF 2.4 solution.
class PrivateField(serializers.Field):
def field_to_native(self, obj, field_name):
"""
Return null value if request has no access to that field
"""
if obj.created_by == self.context.get('request').user:
return super(PrivateField, self).field_to_native(obj, field_name)
return None
#Usage
class UserInfoSerializer(serializers.ModelSerializer):
private_field1 = PrivateField()
private_field2 = PrivateField()
class Meta:
model = UserInfo
And a DRF 3.x solution:
class PrivateField(serializers.ReadOnlyField):
def get_attribute(self, instance):
"""
Given the *outgoing* object instance, return the primitive value
that should be used for this field.
"""
if instance.created_by == self.context['request'].user:
return super(PrivateField, self).get_attribute(instance)
return None
This time we extend ReadOnlyField only because to_representation is not implemented in the serializers.Field class.
I figured out a way to do it. In the serializer, I have access to both the object and the user making the API request. I can therefore check if the requestor is the owner of the object and return the private information. If they are not, the serializer will return an empty string.
class UserInfoSerializer(serializers.HyperlinkedModelSerializer):
private_field1 = serializers.SerializerMethodField('get_private_field1')
class Meta:
model = UserInfo
fields = (
'id',
'public_field1',
'public_field2',
'private_field1',
)
read_only_fields = ('id')
def get_private_field1(self, obj):
# obj.created_by is the foreign key to the user model
if obj.created_by != self.context['request'].user:
return ""
else:
return obj.private_field1
Here:
-- models.py:
class Article(models.Model):
name = models.CharField(max_length=50, blank=False)
author = models.CharField(max_length=50, blank=True)
def __str__(self):
return u"%s" % self.name
class Meta:
permissions = (
# name
('read_name_article', "Read article's name"),
('change_name_article', "Change article's name"),
# author
('read_author_article', "Read article's author"),
('change_author_article', "Change article's author"),
)
-- serializers.py:
class ArticleSerializer(serializers.ModelSerializer):
class Meta(object):
model = Article
fields = "__all__"
def to_representation(self, request_data):
# get the original representation
ret = super(ArticleSerializer, self).to_representation(request_data)
current_user = self.context['request'].user
for field_name, field_value in sorted(ret.items()):
if not current_user.has_perm(
'app_name.read_{}_article'.format(field_name)
):
ret.pop(field_name) # remove field if it's not permitted
return ret
def to_internal_value(self, request_data):
errors = {}
# get the original representation
ret = super(ArticleSerializer, self).to_internal_value(request_data)
current_user = self.context['request'].user
for field_name, field_value in sorted(ret.items()):
if field_value and not current_user.has_perm(
'app_name.change_{}_article'.format(field_name)
):
errors[field_name] = ["Field not allowed to change"] # throw error if it's not permitted
if errors:
raise ValidationError(errors)
return ret
For a solution that allows both reading and writing, do this:
class PrivateField(serializers.Field):
def get_attribute(self, obj):
# We pass the object instance onto `to_representation`,
# not just the field attribute.
return obj
def to_representation(self, obj):
# for read functionality
if obj.created_by != self.context['request'].user:
return ""
else:
return obj.private_field1
def to_internal_value(self, data):
# for write functionality
# check if data is valid and if not raise ValidationError
class UserInfoSerializer(serializers.HyperlinkedModelSerializer):
private_field1 = PrivateField()
...
See the docs for an example.
This is an old question, but the topic is still relevant.
DRF recommends to create different serializers for different permission. But this approach only works, if you have only a few permissions or groups.
restframework-serializer-permissions is a drop in replacement for drf serializers.
Instead of importing the serializers and fields from drf, you are importing them from serializer_permissions.
Installation:
$ pip install restframework-serializer-permissions
Example Serializers:
# import permissions from rest_framework
from rest_framework.permissions import AllowAny, IsAuthenticated
# import serializers from serializer_permissions instead of rest_framework
from serializer_permissions import serializers
# import you models
from myproject.models import ShoppingItem, ShoppingList
class ShoppingItemSerializer(serializers.ModelSerializer):
item_name = serializers.CharField()
class Meta:
# metaclass as described in drf docs
model = ShoppingItem
fields = ('item_name', )
class ShoppingListSerializer(serializers.ModelSerializer):
# Allow all users to list name
list_name = serializers.CharField(permission_classes=(AllowAny, ))
# Only allow authenticated users to retrieve the comment
list_comment = serializers.CharField(permissions=(IsAuthenticated, ))
# show owner only, when the current user has 'auth.view_user' permission
owner = serializers.CharField(permissions=('auth.view_user', ), hide=True)
# serializer which is only available, when the user is authenticated
items = ShoppingItemSerializer(many=True, permissions=(IsAuthenticated, ), hide=True)
class Meta:
# metaclass as described in drf docs
model = ShoppingItem
fields = ('list_name', 'list_comment', 'owner', 'items', )
Disclosure: I'm the author of this extension
In case you are performing only READ operations, you can just pop the fields in to_representation method of the serializer.
def to_representation(self,instance):
ret = super(YourSerializer,self).to_representation(instance)
fields_to_pop = ['field1','field2','field3']
if instance.created_by != self.context['request'].user.id:
[ret.pop(field,'') for field in fields_to_pop]
return ret
This should be enough to hide sensitive fields.
Just share another possible solution
For example, to make email only show for oneself.
On UserSerializer, add:
email = serializers.SerializerMethodField('get_user_email')
Then implement get_user_email like this:
def get_user_email(self, obj):
user = None
request = self.context.get("request")
if request and hasattr(request, "user"):
user = request.user
return obj.email if user.id == obj.pk else 'HIDDEN'
I solved it using a serializer Mixin:
class FieldPermissionModelSerializerMixin(serializers.ModelSerializer):
"""
A mixin that allows you to specify what fields will be returned based on field level permissions
"""
permission_fields = []
def get_field_names(self, declared_fields, info) -> List:
"""Determine the fields to apply."""
fields = getattr(self.Meta, "fields", [])
for permission_field in self.permission_fields:
app_name = getattr(self.Meta, "model", None)._meta.app_label
permission_name = f"can_view_field_{permission_field}"
full_permission_name = f"{app_name}.{permission_name}"
if self.context["request"].user.has_perm(full_permission_name):
fields.append(permission_field)
return fields
Then you can use this serializer with base fields and permissionable fields.
POSITION_BASE_FIELDS = [
"id",
"name",
"level",
"role",
"sort",
]
POSITION_PERMISSION_FIELDS = ["market_salary", "recommended_rate_per_hour"]
class PositionListSerializer(FieldPermissionModelSerializerMixin):
permission_fields = POSITION_PERMISSION_FIELDS
class Meta:
model = Position
fields = POSITION_BASE_FIELDS + []
This is then based on field level permissions defined on the model.
class Position(models.Model):
name = models.CharField(max_length=255, db_index=True)
level = models.CharField(max_length=255, null=True, blank=True)
sort = models.IntegerField(blank=True, default=0)
market_salary = models.DecimalField(max_digits=19, decimal_places=2, default=0.00)
recommended_rate_per_hour = models.DecimalField(
max_digits=7, decimal_places=2, null=True, blank=True
)
class Meta:
ordering = ["name", "sort"]
unique_together = ("name", "level")
permissions = (
("can_view_field_market_salary", "Can view field: market_salary"),
(
"can_view_field_recommended_rate_per_hour",
"Can view field: recommended_rate_per_hour",
),
)

Django TastyPie PUT Nested User Model Correctly

I'm trying to go with-the-grain using Django TastyPie to update my models. I have an Identity model, acting as a wrapper around default Django user model:
class Identity(ProfileBase):
user = models.OneToOneField(User, related_name='identity')
avatar = models.ImageField(upload_to=avatar_upload_path, blank=True,
null=True)
I have my UserResource:
class UserResource(ModelResource):
class Meta:
resource_name = 'user'
queryset = User.objects.all()
fields = ['email', 'first_name', 'last_name']
include_resource_uri = False
And I have my IdentityResource:
class IdentityResource(ModelResource):
user = fields.ToOneField(UserResource, 'user', full=True)
class Meta:
resource_name = 'identity'
queryset = Identity.objects.select_related()
fields = ['user', 'avatar']
always_return_data = True
include_resource_uri = False
authentication = OAuthTokenAuthentication()
authorization = Authorization()
I'm currently successfully updating first_name, last_name using the ModelResource obj_update method within IdentityResource:
def obj_update(self, bundle, request, **kwargs):
print 'updating object'
bundle = self.full_hydrate(bundle)
bundle.obj.user = request.user
user = bundle.data['user']
bundle.obj.user.first_name = user['first_name']
bundle.obj.user.last_name = user['last_name']
return super(IdentityResource, self).obj_update(bundle, request, user=request.user)
I want to make a PUT request and optionally update any field on the user or identity models (first_name, last_name on user, or the avatar field on identity). I would rather not have to manually access each field from the bundle data and set them on models manually, as I have done above.
How can I do this naturally in TastyPie? Can someone explain a better approach to solving this problem? Any direction is GREATLY appreciated. :)
Here's my shot at providing an answer that attempts to leverage Tastypie as much as possible.
It is a little more generic than the OP's request (it will update any user, not just the one logged in). In the real world you would probably want to add some sort of authentication/authorization.
from tastypie.resources import ModelResource
from tastypie.authorization import Authorization
from django.contrib.auth.models import User
from myapp.account.models import Identity
class IdentityResource(ModelResource):
class Meta:
queryset = Identity.objects.all()
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
allowed_list_methods = ['get']
allowed_detail_methods = ['get','put']
authorization = Authorization()
def dehydrate(self, bundle):
identity_bundle = self.build_identity_bundle(bundle)
identity_bundle = IdentityResource().full_dehydrate(identity_bundle)
return identity_bundle
def obj_update(self, bundle, request, **kwargs):
user_bundle = super(UserResource, self).obj_update(bundle, request, **kwargs)
identity_bundle = self.build_identity_bundle(user_bundle)
IdentityResource().obj_update(identity_bundle, request)
return user_bundle
def build_identity_bundle(self, user_bundle):
identity_bundle = IdentityResource().build_bundle(
obj=user_bundle.obj.get_profile(),
data=user_bundle.data
)
return identity_bundle
What the example supports is:
GET a flattened User+Identity resource
PUT a flattened User+Identity resource, updating both models
You would want to register the UserResource in the API, and probably not the IdentityResource.
You could do something like this.
# Find all properties in user model.
properties = [prop for prop in bunder.obj.user if not prop.startswith('__')]
bundle_user = bundle.data['user']
# Find the property in bundle user and set it back on user if it exists.
for property in properties:
if property in bundle_user:
setattr(bundle.obj.user, property, bundle_user[property])
Maybe I'm missing the point but did you try a PATCH-method request? Tastypie will take all the sent attributes and update them in the database leaving all not-send attributes untouched.