How to make follower-following system with django model - django

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.

Related

Django Rest Framework: foreign key field is required in viewset or serializer

I am very new to Django Rest Framework (DRF). I have read that viewsets are very good, because it reduces the amount of code, but I found it more complex.
Description:
Imagine that I want to implement a phonebook API, in which we have some users and each of them has it own contacts and each contact can have several phone number. So, I have three models here.
User (Default Django model)
Contact
class Contact(models.Model):
owner = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='contacts'
)
name = models.CharField(
max_length=70
)
description = models.TextField()
Phone Number
class Phones(models.Model):
person = models.ForeignKey(
Contact,
related_name="phones",
on_delete=models.CASCADE,
)
phone_no = models.CharField(
max_length=11,
)
Problem Definition
What I want is to create new contact with the current request.user. So I should have my contact.serializers like the following:
class ContactSerializer(serializers.ModelSerializer):
owner = serializers.SlugRelatedField(queryset=User.objects.all(), slug_field='user')
class Meta:
model = Contact
fields = ['id', 'owner', 'name', 'description']
read_only_fields = ['owner']
and my views is like:
class ContactViewSet(viewsets.ModelViewSet):
queryset = Contact.objects.all()
serializer_class = ContactSerializer
permission_classes = [IsCreator]
def get_permissions(self):
if self.request.method == "GET":
self.permission_classes = [IsCreator, permissions.IsAdminUser,]
if self.request.method == "POST":
self.permission_classes = [permissions.IsAuthenticated,]
if self.request.method == "PUT":
self.permission_classes = [IsCreator]
if self.request.method == "DELETE":
self.permission_classes = []
return super(ContactViewSet, self).get_permissions()
Error Whenever I want to post a new contact using postman, I have pass the name, description and owner and it should automatically detects the owner from the request but it doesn't and I have got the following error:
PS: If it is necessary to checkout the project here is my project link.
What should I do?
The problem was solved after applying the below changes.
apps.contacts.views.py:
add the following method to the body of the class
def perform_create(self, serializer):
return serializer.save(owner = self.request.user)
apps.contacts.serializers.py:
class ContactSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
fields = ['id', 'name', 'description', 'owner']
PS I am also looking for more answers, in this case add your answer.
One thing you could do is add currentuserdefault. like this
class ContactSerializer(serializers.ModelSerializer):
owner = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = Contact
fields = ['id', 'owner', 'name', 'description']
And in your view code remember to pass request in context. Like this
class ContactViewSet(viewsets.ModelViewSet):
queryset = Contact.objects.all()
serializer_class = ContactSerializer
permission_classes = [IsCreator]
def get_serializer_context(self):
context = super().get_serializer_context()
context.update({"request": self.request})
return context

Updating ManyToMany of a custom user DRF best practice

I've been struggling to understand how one would update a M2M-field in the django rest framework that is between a custom user and some random field. I should mention I'm using Djoser as authentication.
Lets say I have a custom user
Models:
class CustomUser(AbstractUser):
username = None
email = models.EmailField(_('email address'), unique=True)
paying_user = models.BooleanField(default=False)
subscribed_companies = models.ManyToManyField('myapp.Company')
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
objects = UserAccountManager()
def __str__(self):
return f"{self.email}' account"
class Company(models.Model):
name = models.CharField(max_length=150)
def __str__(self):
return self.name
class Meta:
ordering = ['name']
My serializers
Imports - serializers.py:
from django.contrib.auth import get_user_model
from djoser.serializers import UserCreateSerializer
from rest_framework import serializers
from apartments.models.company_model import Company
User = get_user_model()
class UserCreateSerializer(UserCreateSerializer):
class Meta(UserCreateSerializer.Meta):
model = User
fields = ('email','password', 'paying_user', 'subscribed_companies')
class UserCompanyListSerializer(serializers.ModelSerializer):
#Is this a reasonable way to serialize a M2M-field?
subscribed_company_ids = serializers.PrimaryKeyRelatedField(many=True, read_only=False,
queryset=Company.objects.all(), source='subscribed_companies')
class Meta:
model = User
fields = [
'subscribed_company_ids'
]
class CompanySerializer(serializers.ModelSerializer):
class Meta:
model = Company
fields = ('name',)
As you can see, I've attached a M2M-field on the custom user itself, instead of using a OneToOne-field where I store custom data. I'm not sure this is the best way to do it.
The idea is that a user should be able to, on the front end, have a list of companies it wants to subscribe to once they are logged in. That means I'll have many users that can subscribe to many companies.
Where I'm really doubting myself is how I handled the class based views.
Since I can retrieve the ID from request.user.id and since I want to replace the entire list of companies, I don't need the PK which identifies a specific company.
Therefore, in the put method, I removed the PK parameter. This works.
So my question is - Is there a more clean way to do it? Looking at posts at stackoverflow I couldn't find a decent answer that involved authentication. Am I approaching it wrong?
class UserCompanies(APIView):
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def get(self, request):
user_id = request.user.id
instance = CustomUser.objects.get(id=user_id)
serializer = UserCompanyListSerializer(instance)
return Response(serializer.data)
def put(self, request, format=None):
user_id = request.user.id
instance = CustomUser.objects.get(id=user_id)
serializer = UserCompanyListSerializer(instance, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
How a GET request response would look to localhost:8000/usercompanies/:
{
"subscribed_company_ids": [
2,
1,
3
]
}
How a PUT request response would look to localhost:8000/usercompanies/:
{
"subscribed_company_ids": [
2,
1,
3,
5,
4,
]
}
Feedback would be much appreciated, I'm a total DRF newbie.

I created an extra table extra table in one to one relation with User table. how to show phone field in User registration

I am trying to create a simple API to get a user register.
I am using the default User table for authentication purpose, created another table called "phone" with one to one relation with User.
I am trying to add "phone" field just above the password. (I hope the image attached is visible).
**
Serializer.py
class UserRegisterSerializer(serializers.ModelSerializer):
class Meta:
model = UserDetailsModel
fields = ('phone', 'user')
class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(max_length=68, min_length=6, write_only=True)
class Meta:
model = User
fields = ('username','first_name', 'last_name','email','password')
read_only_fields = ('id',)
**
models.py<<
**
class UserDetailsModel(models.Model):
phone = models.IntegerField()
balance = models.DecimalField(max_digits=10, decimal_places=2, default=0)
user = models.OneToOneField(get_user_model(),primary_key='email' , on_delete=models.CASCADE)
def __str__(self):
return str(self.user)
**
views.py
**
class RegisterView(generics.GenericAPIView):
serializer_class = RegisterSerializer
def post(self, request):
user = request.data
serializer = self.serializer_class(data=user)
serializer.is_valid(raise_exception=True)
serializer.save()
user_data = serializer.data
return Response(user_data,status=status.HTTP_201_CREATED)
class DetailsRegisterView(generics.GenericAPIView):
serializer_class = UserRegisterSerializer
def post(self, request):
user = request.data
serializer = self.serializer_class(data=user)
serializer.is_valid(raise_exception=True)
serializer.save()
user_data = serializer.data
return Response(user_data,status=status.HTTP_201_CREATED)
**
urls
**
urlpatterns = [
path('',RegisterView.as_view()),
path('details', DetailsRegisterView.as_view())
]
**
You probably can use source in a serializer with a FK
class RegisterSerializer(...)
...
phone = serializers.CharField(..., source='userdetails.phone')
see also : the doc
I have some doubt in create case, in a update case this code work fine.
see also : How to serialize a relation OneToOne in Django with Rest Framework?
and an other way to resolve your issue : nested serializer
Updated code:
serializers>
from django.contrib.auth.models import User
from django.http import JsonResponse
from rest_framework import serializers
from .models import UserDetailsModel
class RegisterSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('username','first_name', 'last_name','email','password')
read_only_fields = ('id',)
class UserRegisterSerializer(serializers.ModelSerializer):
user = RegisterSerializer(required=True)
class Meta:
model = UserDetailsModel
fields = ('phone','user')
def create(self, validated_data):
user_data = validated_data.pop('user')
user = RegisterSerializer.create(RegisterSerializer(), validated_data=user_data)
data, created = UserDetailsModel.objects.update_or_create(user=user,
phone=validated_data.pop('phone'))
return data
class DetailView(serializers.ModelSerializer):
user = RegisterSerializer(read_only=True)
class Meta:
model = UserDetailsModel
fields = ('user','phone')
Remaining code stays the same.

DRF serializer get liked data with post by request user

I'm making a social app like facebook.
when getting post(at news feed) data I would like to get Boolean if I pressed like about that post.
models.py
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class Post(models.Model):
uploader = models.ForeignKey(User)
likes = models.IntegerField(default=0)
point = models.IntegerField(default=0)
isPointReceived = models.BooleanField(default=False)
content = models.TextField()
uploadedTime = models.DateTimeField(auto_now=True)
# editedTime = models.DateTimeField()
def __str__(self):
return ("[uploader = " + self.uploader.username + "]") + (", [id = " + (str)(self.id) + "]") + ("[content = " + self.content[:50] + "]")
class PostLike(models.Model):
post = models.ForeignKey(Post, related_name='postLikes')
liker = models.ForeignKey(User)
def __str__(self):
return "Like" + "| [Post = " + (str)(self.post) + "]" + ", [Liker = " + self.liker.username + "]"
serializers.py
class PostLikeSerializer(serializers.ModelSerializer):
class Meta:
model = PostLike
fields = '__all__'
class PostListSerializer(serializers.ModelSerializer):
uploader = UserDetailSerializer()
isMine = serializers.SerializerMethodField()
isLiked = serializers.SerializerMethodField()
postComments = PostCommentSerializer(many=True, allow_null=True)
class Meta:
model = Post
fields = ('uploader', 'id', 'likes', 'point', 'isPointReceived', 'content', 'uploadedTime', 'postComments', 'isMine', 'isLiked',)
def get_isMine(self, obj):
requestUser = CurrentUserDefault()
return obj.objects.fileter(uploader=requestUser).exists()
# return obj.filter(uploader=requestUser)
def get_isLiked(self, obj):
requestUser = CurrentUserDefault()
return PostLike.objects.filter(post=obj, liker=requestUser).exists()
#
# try:
# PostLike.objects.get(post=obj, liker=requestUser)
# return True
# except:
# return False
I tried lot of dirty stuffs. but there was no solution..
conclusion:
HTF to get data about, if user has a record of PostLike(post=post, user=user)
how to get user in serializer Class.
or should I approach this in different ways?
like managing liked data in APIView or whatever etc...
help!
========EDITED==========
final code should look like this.
in serialziers.py
class PostListSerializer(serializers.ModelSerializer):
uploader = UserDetailSerializer()
postComments = PostCommentSerializer(many=True, allow_null=True)
postLikes = PostLikeSerializer(many=True, allow_null=True)
postImages = PostImageSerializer(many=True, allow_null=True)
isMine = serializers.SerializerMethodField()
isLiked = serializers.SerializerMethodField()
class Meta:
model = Post
fields = ('uploader', 'id', 'likes', 'content', 'uploadedTime', 'postComments', 'postLikes', 'postImages', 'isMine', 'isLiked',)
def get_isMine(self, obj):
requestUser = self.context['request'].user
return obj.uploader == requestUser
def get_isLiked(self, obj):
requestUser = self.context['request'].user
return PostLike.objects.filter(post=obj, liker=requestUser).exists()
You can get a user from serializer context inside serializer method:
self.context['request'].user
It passed from a method get_serializer_context which originally created in a GenericAPIView:
class GenericAPIView(APIView):
....
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
As you can see you could also get format and view from context. You could also override this method for set additional context.
def get_isLiked(self, obj):
requestUser = self.context['request'].user
return PostLike.objects.filter(post=obj, liker=requestUser).exists()
If results have 12 objects it will execute 12 similar query, which increase increase your response time. I am looking for solution so that I can make it with one query. Don't know how can I do that.
def get_isLiked(self, obj):
requestUser = self.context['request'].user
return PostLike.objects.filter(post=obj, liker=requestUser).exists()
You should to get all PostLike fields in cache and use prefetch_related method in your query set. This thing will check all entries using cache, not multiple queries.
For example:
def get_isLiked(self, obj):
requestUser = self.context['request'].user
likes = PostLike.objects.all()
return likes.filter(post=obj, liker=requestUser).exists()
and somewhere in models or views or managers in "get_queryset" you should add:
queryset.prefetch_related('postlike')

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",
),
)