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",
),
)
Related
I'm a student studying django rest framework
I'm making a simple sns with django rest framework
I need follower-following system. So, I tried to make it but there is some trouble
First this is my user model with AbstractBaseUser and PermissionsMixin
class User(AbstractBaseUser, PermissionsMixin):
user_id = models.CharField(max_length=100, unique=True, primary_key=True)
name = models.CharField(max_length=100)
created_at = models.DateTimeField(auto_now_add=True)
is_staff = models.BooleanField(default=False)
followers = models.ManyToManyField('self', related_name='follower',blank=True)
following = models.ManyToManyField('self', related_name='following',blank=True)
profile_image = models.ImageField(blank=True)
the field followers is people who follows me and following is whom i follow
When i add following with this APIView class
class AddFollower(APIView):
permission_classes = [IsAuthenticated, ]
def post(self, requset, format=None):
user = User.objects.get(user_id=self.request.data.get('user_id'))
follow = User.objects.get(user_id=self.request.data.get('follow'))
user.following.add(follow)
user.save()
follow.followers.add(user)
follow.save()
print(str(user) + ", " + str(follow))
return JsonResponse({'status':status.HTTP_200_OK, 'data':"", 'message':"follow"+str(follow.user_id)})
The user_id is me and the follow is whom i want to follow
I want to add follow to user_id's following field and add user_id to follow's followers field
But it does not work
What i want for result is like this (with user information api)
{
"followers": [],
"following": [
"some user"
],
}
some user's user info
{
"followers": [
"user above"
],
"following": [
],
}
But real result is like this
{
"followers": [
"some user"
],
"following": [
"some user"
],
}
some user's user info
{
"followers": [
"user above"
],
"following": [
"user above"
],
}
this is not what i want
I have no idea with this problem i need some help
Thank you
I would design it in different way.
I would not add the information to the User model but explicitly create another table to store information about "followers" and "following".
Schema of the table would be:
class UserFollowing(models.Model):
user_id = models.ForeignKey("User", related_name="following")
following_user_id = models.ForeignKey("User", related_name="followers")
# You can even add info about when user started following
created = models.DateTimeField(auto_now_add=True)
Now, in your post method implementation, you would do only this:
UserFollowing.objects.create(user_id=user.id,
following_user_id=follow.id)
And then, you can access following and followers easily:
user = User.objects.get(id=1) # it is just example with id 1
user.following.all()
user.followers.all()
And you can then create constraint so user cannot follow the same user twice. But i leave this up to you ( hint: unique_together )
The above solutions are fine and optimal, but I would like to supply a detailed solution for anyone who wants to implement such functionality.
The intermediary Model
from django.contrib.auth import get_user_model
UserModel = get_user_model()
class UserFollowing(models.Model):
user_id = models.ForeignKey(UserModel, related_name="following", on_delete=models.CASCADE)
following_user_id = models.ForeignKey(UserModel, related_name="followers", on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
constraints = [
models.UniqueConstraint(fields=['user_id','following_user_id'], name="unique_followers")
]
ordering = ["-created"]
def __str__(self):
f"{self.user_id} follows {self.following_user_id}"
THE SERIALIZER for follow and unfollow
Your View for follow and unfollow
class UserFollowingViewSet(viewsets.ModelViewSet):
permission_classes = (IsAuthenticatedOrReadOnly,)
serializer_class = UserFollowingSerializer
queryset = models.UserFollowing.objects.all()
Custom FollowersSerializer and FollowingSerializer
class FollowingSerializer(serializers.ModelSerializer):
class Meta:
model = UserFollowing
fields = ("id", "following_user_id", "created")
class FollowersSerializer(serializers.ModelSerializer):
class Meta:
model = UserFollowing
fields = ("id", "user_id", "created")
Your UserSerializer
from django.contrib.auth import get_user_model
User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
following = serializers.SerializerMethodField()
followers = serializers.SerializerMethodField()
class Meta:
model = User
fields = (
"id",
"email",
"username",
"following",
"followers",
)
extra_kwargs = {"password": {"write_only": True}}
def get_following(self, obj):
return FollowingSerializer(obj.following.all(), many=True).data
def get_followers(self, obj):
return FollowersSerializer(obj.followers.all(), many=True).data
models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
followers = models.ManyToManyField('self', symmetrical=False,
blank=True)
def count_followers(self):
return self.followers.count()
def count_following(self):
return User.objects.filter(followers=self).count()
I created a follow and unfollow system similar to Instagram.
Functionality:
If private account enable --> send follow request.
If user in block list --> they can not follow of opposite user.
User can check pending request, sended request list,blocked user
list,etc.
Let's start :
Models.py:
class Profile(models.Model):
user = models.OneToOneField(to = User,on_delete=models.CASCADE,related_name='profile')
.......
private_account = models.BooleanField(default = False)
followers = models.ManyToManyField('self',blank=True,related_name='user_followers',symmetrical=False)
following = models.ManyToManyField('self',blank=True,related_name='user_following',symmetrical=False)
panding_request = models.ManyToManyField('self',blank=True,related_name='pandingRequest',symmetrical=False)
blocked_user = models.ManyToManyField('self',blank=True,related_name='user_blocked',symmetrical=False)
created_date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return '%s' %(self.user)
Here you can follow, unfollow, send follow request, accept follow request,
decline request and you can remove your follower.
From the front end side pass opposite user profile id and type
of action(follow,unfollow..).
views.py:
class FollowUnfollowView(APIView):
permission_classes = [IsAuthenticated]
def current_profile(self):
try:
return Profile.objects.get(user = self.request.user)
except Profile.DoesNotExist:
raise Http404
def other_profile(self,pk):
try:
return Profile.objects.get(id = pk)
except Profile.DoesNotExist:
raise Http404
def post(self, request,format=None):
pk = request.data.get('id') # Here pk is opposite user's profile ID
req_type = request.data.get('type')
current_profile = self.current_profile()
other_profile = self.other_profile(pk)
if req_type == 'follow':
if other_profile.private_account:
other_profile.panding_request.add(current_profile)
return Response({"Requested" : "Follow request has been send!!"},status=status.HTTP_200_OK)
else:
if other_profile.blocked_user.filter(id = current_profile.id).exists():
return Response({"Following Fail" : "You can not follow this profile becuase your ID blocked by this user!!"},status=status.HTTP_400_BAD_REQUEST)
current_profile.following.add(other_profile)
other_profile.followers.add(current_profile)
return Response({"Following" : "Following success!!"},status=status.HTTP_200_OK)
elif req_type == 'accept':
current_profile.followers.add(other_profile)
other_profile.following.add(current_profile)
current_profile.panding_request.remove(other_profile)
return Response({"Accepted" : "Follow request successfuly accespted!!"},status=status.HTTP_200_OK)
elif req_type == 'decline':
current_profile.panding_request.remove(other_profile)
return Response({"Decline" : "Follow request successfully declined!!"},status=status.HTTP_200_OK)
elif req_type == 'unfollow':
current_profile.following.remove(other_profile)
other_profile.followers.remove(current_profile)
return Response({"Unfollow" : "Unfollow success!!"},status=status.HTTP_200_OK)
elif req_type == 'remove': # You can remove your follower
current_profile.followers.remove(other_profile)
other_profile.following.remove(current_profile)
return Response({"Remove Success" : "Successfuly removed your follower!!"},status=status.HTTP_200_OK)
# Here we can fetch followers,following detail and blocked user,pending request,sended request..
def patch(self, request,format=None):
req_type = request.data.get('type')
if req_type == 'follow_detail':
serializer = FollowerSerializer(self.current_profile())
return Response({"data" : serializer.data},status=status.HTTP_200_OK)
elif req_type == 'block_pending':
serializer = BlockPendinSerializer(self.current_profile())
pf = list(Profile.objects.filter(panding_request = self.current_profile().id).values('id','user__username','profile_pic','overall_pr'))
return Response({"data" : serializer.data,"Sended Request" :pf},status=status.HTTP_200_OK)
# You can block and unblock user
def put(self, request,format=None):
pk = request.data.get('id') # Here pk is oppisite user's profile ID
req_type = request.data.get('type')
if req_type == 'block':
self.current_profile().blocked_user.add(self.other_profile(pk))
return Response({"Blocked" : "This user blocked successfuly"},status=status.HTTP_200_OK)
elif req_type == 'unblock':
self.current_profile().blocked_user.remove(self.other_profile(pk))
return Response({"Unblocked" : "This user unblocked successfuly"},status=status.HTTP_200_OK)
serializers.py:
class EachUserSerializer(serializers.ModelSerializer):
username = serializers.CharField(source='user.username')
class Meta:
model = Profile
fields = ('id','username','profile_pic')
read_only_fields = ('id','username','profile_pic')
class FollowerSerializer(serializers.ModelSerializer):
followers = EachUserSerializer(many=True, read_only= True)
following = EachUserSerializer(many=True,read_only=True)
class Meta:
model = Profile
fields = ('followers','following')
read_only_fields = ('followers','following')
class BlockPendinSerializer(serializers.ModelSerializer):
panding_request = EachUserSerializer(many=True, read_only= True)
blocked_user = EachUserSerializer(many=True,read_only=True)
class Meta:
model = Profile
fields = ('panding_request','blocked_user')
read_only_fields = ('panding_request','blocked_user')
urls.py:
from django.urls.conf import path
from .views import *
urlpatterns = [
.....
path('follow_unfollow/',FollowUnfollowView.as_view(),name = "follow_unfollow"),
]
If any doubt of any step please comment. I will briefly describe.
This is how i solved my problem.
there is a good answer above, but someone need a detail for it. so i'm writing this
I removed field followers and following in User model. And Created new model UserFollowing.
You can see this model in Enthusiast Martin's answer. I didn't use any serializer to create object.
Just two views (Follow, UnFollow) were needed.
In Follow View
UserFollowing.objects.create(user_id=user.id, following_user_id=follow.id)
with this we can create Following-Follower relationship.
The way to use this relationship
In User Information View
following_data = UserFollowingSerializer(qs.following.all(), many=True)
followers_data = UserFollowingSerializer(qs.followers.all(), many=True)
return JsonResponse({'status':status.HTTP_200_OK, 'data':{'user':serializer.data, 'following':following_data.data, 'followers':followers_data.data}, "message":"success"})
I usually use JsonResponse for response.
serializer.data and qs are user object
In UserFolowingSerializer
fields = '__all__'
I used this.
Very good answers. For me it would be either #GST Talib's answer or a very similar one without using self
following = models.ManyToManyField('User', blank=True, related_name='followers')
That way when you add user1.following.add(user2) you can see the relationship being reflected in user2.followers
And no need for extra code.
I'm trying to limit the fields list in serializers based on user permissions. I have a generic routine that does it for all serializers. It's working on the parent serializer, but not on a nested serializer.
I have a client model, and a client profile (referred to as "contacts") as shown below. The client profile model is an extension of the user model (one-to-one relationship).
class Client(AddressPhoneModelMixin, DateFieldsModelMixin, models.Model):
name = models.CharField(max_length=100)
status = models.CharField(max_length=25)
class Meta:
permissions = (
# Object-level
('view_all_clients', 'Can view all clients'),
('change_all_clients', 'Can change all clients'),
# Field-level
('view_client_id', 'Can view client ID'),
('view_client_name', 'Can view client name'),
...others omitted...
)
class ClientProfile(models.Model):
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
blank=True,
null=True,
)
client = models.ForeignKey(
Client,
on_delete=models.PROTECT,
related_name='contacts',
)
receive_invoices = models.BooleanField(default=False)
My object-level permission logic is in the list view:
class ClientList(ListAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = ClientSerializer
def get_queryset(self):
user = self.request.user
queryset = None
if user.has_perm('view_client') or user.has_perm('clients.view_all_clients'):
queryset = Client.objects.all().exclude(status__in=['deleted', 'archived'])
if user.has_perm('view_client'): # View only "assigned" clients
if user.type == 'client':
# See if user is a "contact".
queryset = queryset.distinct().filter(contacts__user=self.request.user)
else:
# See if user is assigned to projects for the client(s).
queryset = queryset.distinct().filter(projects__project_users__user=self.request.user)
if queryset is None:
raise PermissionDenied('You do not have permission to view clients.')
return self.get_serializer_class().setup_eager_loading(queryset)
Removing fields from the serializer "fields" property is done in the serializer __init__ method (from examples I found here in SO):
class ClientContactsSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='clients:clientprofile-detail')
user = UserSerializer()
class Meta:
model = ClientProfile
fields = (
'url',
'receive_invoices',
'user',
)
def __init__(self, *args, **kwargs):
super(ClientContactsSerializer, self).__init__(*args, **kwargs)
check_field_permissions(self, 'view')
class ClientSerializer(AddressPhoneSerializerMixin, serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='clients:client-detail')
contacts = ClientContactsSerializer(many=True, read_only=True)
projects = ClientProjectsSerializer(many=True, read_only=True)
class Meta:
model = Client
fields = (
'url',
'id',
'name',
...omitted for brevity...
'contacts',
'projects',
)
def __init__(self, *args, **kwargs):
super(ClientSerializer, self).__init__(*args, **kwargs)
check_field_permissions(self, 'view')
#staticmethod
def setup_eager_loading(queryset):
queryset = queryset.select_related('country')
return queryset.prefetch_related('contacts', 'contacts__user', 'contacts__user__country', 'projects')
And, finally, here's the check_field_permissions function:
def check_field_permissions(serializer, action='view'):
request = serializer.context.get('request', None)
fields = serializer.get_fields()
model = serializer.Meta.model
app_name = model._meta.app_label
model_name = model._meta.model_name
if request is not None and app_name is not None and model_name is not None:
user = request.user
for field_name in fields:
if hasattr(serializer.fields[field_name], 'child'):
continue
if not user.has_perm(app_name + '.' + action + '_' + model_name + '_' + field_name):
serializer.fields.pop(field_name)
Stepping through in debug on a page-load of the Client list, I can see that the above function is invoked first for clientprofile, and request is None. The second time, it is invoked for client, and request is a valid request object.
First question, is __init__ the correct place for limiting the list of fields to be serialized?
Second, how to get the request object in the nested serializer (clientprofile)?
After reading a post by Tom Christie, who is an undisputed authority on DRF, I was able to solve my issue. He pointed out that each nested serializer does, in fact, have the context object (and the request, and the user). You just have to deal with nested serializers in the parent __init__ - not their own __init__.
Here's my revised check_field_permissions() function:
def check_field_permissions(serializer, action='view'):
request = serializer.context.get('request', None)
fields = serializer.get_fields()
model = serializer.Meta.model
app_name = model._meta.app_label
model_name = model._meta.model_name
if request is not None and app_name is not None and model_name is not None:
user = request.user
extra_fields = []
for field_name in fields:
if field_name == 'url':
extra_fields.append(field_name)
continue
if hasattr(serializer.fields[field_name], 'child'):
check_field_permissions(serializer.fields[field_name].child, action)
extra_fields.append(field_name)
continue
if not user.has_perm(app_name + '.' + action + '_' + model_name + '_' + field_name):
serializer.fields.pop(field_name)
# If only "url" and child fields remain, remove all fields.
if len(serializer.fields) == len(extra_fields):
for field_name in extra_fields:
serializer.fields.pop(field_name)
It is now recursive. If it hits a field with a "child" attribute, it knows that's a nested serializer field. It calls itself with that child serializer passed as an arg.
The other change is that __init__ was removed from ClientContactsSerializer, because it doesn't need to call check_field_permissions().
I am building an activity model, somewhat similar to this package. It has an actor, verb and the target.
class Activity(models.Model):
actor_type = models.ForeignKey(ContentType, related_name='actor_type_activities')
actor_id = models.PositiveIntegerField()
actor = GenericForeignKey('actor_type', 'actor_id')
verb = models.CharField(max_length=10)
target_type = models.ForeignKey(ContentType, related_name='target_type_activities')
target_id = models.PositiveIntegerField()
target = GenericForeignKey('target_type', 'target_id')
pub_date = models.DateTimeField(default=timezone.now)
Now whenever a new object of whichever models (Tender, Job and News) is created, a new Activity object is created, with the target being the objects of any of these three models.
eg. user (actor) published (verb) title (target)
class Tender(models.Model):
title = models.CharField(max_length=256)
description = models.TextField()
class Job(models.Model):
title = models.CharField(max_length=256)
qualification = models.CharField(max_length=256)
class News(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
title = models.CharField(max_length=150)
To get this data I am making an API which will get me the required json data. I am using django-rest-framework for this and very new with it.
class ActorSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'email')
class ActivitySerializer(serializers.HyperlinkedModelSerializer):
actor = ActorSerializer()
class Meta:
model = Activity
fields = ('url', 'actor', 'verb', 'pub_date')
In the above serializers, I knew that actor will be the User. And so I used the User model for the ActorSerializer class. But as for the target, it can be any of these three models (News/Job/Tender).
How can I make a serializer (eg. TargetSerialier class) for the ContentType object so that I can use the target in the ActivitySerializer class field?
Okay so answering my own question here. I had some help with zymud's answer. So, apparently in the documentation, there is a way to serialize the Generic relation.
So, all I had to do was create a custom field and associate that field in the serializer itself:
class ActivityObjectRelatedField(serializers.RelatedField):
def to_representation(self, value):
if isinstance(value, User):
return 'User: ' + value.username
elif isinstance(value, News):
return 'News: ' + value.title
elif isinstance(value, Job):
return 'Job: ' + value.title
elif isinstance(value, Tender):
return 'Tender: ' + value.title
raise Exception('Unexpected type of tagged object')
class ActivitySerializer(serializers.HyperlinkedModelSerializer):
actor = ActivityObjectRelatedField(read_only=True)
target = ActivityObjectRelatedField(read_only=True)
class Meta:
model = Activity
fields = ('url', 'actor', 'verb', 'target', 'pub_date')
You can implement custom field for generic key. Example:
from django.core.urlresolvers import resolve
from rest_framework.fields import Field
class GenericRelatedField(Field):
"""
A custom field that expect object URL as input and transforms it
to django model instance.
"""
read_only = False
_default_view_name = '%(model_name)s-detail'
lookup_field = 'pk'
def __init__(self, related_models=(), **kwargs):
super(GenericRelatedField, self).__init__(**kwargs)
# related models - list of models that should be acceptable by
# field. Note that all this models should have corresponding
# endpoint.
self.related_models = related_models
def _get_url_basename(self, obj):
""" Get object URL basename """
format_kwargs = {
'app_label': obj._meta.app_label,
'model_name': obj._meta.object_name.lower()
}
return self._default_view_name % format_kwargs
def _get_request(self):
try:
return self.context['request']
except KeyError:
raise AttributeError('GenericRelatedField have to be initialized with `request` in context')
def to_representation(self, obj):
""" Serializes any object to its URL representation """
kwargs = {self.lookup_field: getattr(obj, self.lookup_field)}
request = self._get_request()
return request.build_absolute_uri(reverse(self._get_url_basename(obj), kwargs=kwargs))
def clear_url(self, url):
""" Removes domain and protocol from url """
if url.startswith('http'):
return '/' + url.split('/', 3)[-1]
return url
def get_model_from_resolve_match(self, match):
queryset = match.func.cls.queryset
if queryset is not None:
return queryset.model
else:
return match.func.cls.model
def instance_from_url(self, url):
url = self.clear_url(url)
match = resolve(url)
model = self.get_model_from_resolve_match(match)
return model.objects.get(**match.kwargs)
def to_internal_value(self, data):
""" Restores model instance from its URL """
if not data:
return None
request = self._get_request()
user = request.user
try:
obj = self.instance_from_url(data)
model = obj.__class__
except (Resolver404, AttributeError, MultipleObjectsReturned, ObjectDoesNotExist):
raise serializers.ValidationError("Can`t restore object from url: %s" % data)
if model not in self.related_models:
raise serializers.ValidationError('%s object does not support such relationship' % str(obj))
return obj
Example of usage:
class ActivitySerializer(serializers.HyperlinkedModelSerializer):
target = GenericRelatedField(related_models=(News, Job, Tender))
...
There is a third party lib as per documentation that did the heavy lifting already:
https://www.django-rest-framework.org/api-guide/relations/#rest-framework-generic-relations
It is pretty neat actually, my serializer class ended up few readable lines:
class ActivityTypeSerializer(serializers.ModelSerializer):
target = GenericRelatedField({
User: UserSerializer(),
Device: DeviceSerializer(),
})
class Meta:
model = Activity
fields = ('target', 'target_id', 'verb', 'target_ct',)
I am using DRF to expose some API endpoints.
# models.py
class Project(models.Model):
...
assigned_to = models.ManyToManyField(
User, default=None, blank=True, null=True
)
# serializers.py
class ProjectSerializer(serializers.ModelSerializer):
assigned_to = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), required=False, many=True)
class Meta:
model = Project
fields = ('id', 'title', 'created_by', 'assigned_to')
# view.py
class ProjectList(generics.ListCreateAPIView):
mode = Project
serializer_class = ProjectSerializer
filter_fields = ('title',)
def post(self, request, format=None):
# get a list of user.id of assigned_to users
assigned_to = [x.get('id') for x in request.DATA.get('assigned_to')]
# create a new project serilaizer
serializer = ProjectSerializer(data={
"title": request.DATA.get('title'),
"created_by": request.user.pk,
"assigned_to": assigned_to,
})
if serializer.is_valid():
serializer.save()
else:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
return Response(serializer.data, status=status.HTTP_201_CREATED)
This all works fine, and I can POST a list of ids for the assigned to field. However, to make this function I had to use PrimaryKeyRelatedField instead of RelatedField. This means that when I do a GET then I only receive the primary keys of the user in the assigned_to field. Is there some way to maintain the current behavior for POST but return the serialized User details for the assigned_to field?
I recently solved this with a subclassed PrimaryKeyRelatedField() which uses the id for input to set the value, but returns a nested value using serializers. Now this may not be 100% what was requested here. The POST, PUT, and PATCH responses will also include the nested representation whereas the question does specify that POST behave exactly as it does with a PrimaryKeyRelatedField.
https://gist.github.com/jmichalicek/f841110a9aa6dbb6f781
class PrimaryKeyInObjectOutRelatedField(PrimaryKeyRelatedField):
"""
Django Rest Framework RelatedField which takes the primary key as input to allow setting relations,
but takes an optional `output_serializer_class` parameter, which if specified, will be used to
serialize the data in responses.
Usage:
class MyModelSerializer(serializers.ModelSerializer):
related_model = PrimaryKeyInObjectOutRelatedField(
queryset=MyOtherModel.objects.all(), output_serializer_class=MyOtherModelSerializer)
class Meta:
model = MyModel
fields = ('related_model', 'id', 'foo', 'bar')
"""
def __init__(self, **kwargs):
self._output_serializer_class = kwargs.pop('output_serializer_class', None)
super(PrimaryKeyInObjectOutRelatedField, self).__init__(**kwargs)
def use_pk_only_optimization(self):
return not bool(self._output_serializer_class)
def to_representation(self, obj):
if self._output_serializer_class:
data = self._output_serializer_class(obj).data
else:
data = super(PrimaryKeyInObjectOutRelatedField, self).to_representation(obj)
return data
You'll need to use a different serializer for POST and GET in that case.
Take a look into overriding the get_serializer_class() method on the view, and switching the serializer that's returned depending on self.request.method.
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.