I am building an API using Django Rest Framework for my car-sharing app. I want to let not owner users to have access to update "participants" field in race, so they can join. Other fields should be available only to owner. I was reading about django-guardian, but i don't realy understand how to implement it. Here's my model:
from django.db import models
from django.contrib.auth.models import User
class Race(models.Model):
owner = models.ForeignKey("auth.User", related_name = 'races', on_delete=models.CASCADE)
origin_long = models.DecimalField(max_digits=8, decimal_places=3)
origin_lat = models.DecimalField(max_digits=8, decimal_places=3)
destination_long = models.DecimalField(max_digits=8, decimal_places=3)
destination_lat = models.DecimalField(max_digits=8, decimal_places=3)
start_time = models.TimeField(auto_now=False, auto_now_add=False)
participants = models.ManyToManyField(User,blank=True)
schedule = models.DurationField(blank=True,null=True)
subs = models.ManyToManyField(User, related_name='subs',blank=True)
cost = models.DecimalField(max_digits=5, decimal_places=2)
def __str__(self):
return self.user.get_full_name()
Thank you in advance.
I don't think Django has anyway to have field level permission by default.
But we can tweak and restrict the fields through the serializers.py and views.py .
In views.py
class RaceUpdateView(UpdateAPIView):
lookup_field = 'pk'
serializer_class = RaceUpdateSerializer
queryset = Race.objects.all()
permission_classes = [IsAuthenticated]
model = Race
def put(self, request, pk):
try:
try:
race_obj = self.get_object()
except Exception as error:
context = {'error': "Race Id does not exist", 'success': "false", 'message': 'Race Id does not exist.'}
return Response(context, status=status.HTTP_404_NOT_FOUND)
#I don't know how you are checking owner. So i kept it this way.
if request.user.id != race_obj.owner.id:
#passing the fields which are to be used by the serializer.
serializer = RaceUpdateSerializer(race_obj, data=request.data, partial=True, fields=('participants',))
else:
serializer = RaceUpdateSerializer(race_obj, data=request.data, partial=True)
if serializer.is_valid():
try:
serializer.save()
except Exception as error:
context = {"success": False, "message": "Update Failed. %s" % str(error), "error": str(error)}
return Response(context, status=status.HTTP_400_BAD_REQUEST)
context = {"success": True, "message": "Updated Successful", "error": "", "data": serializer.data}
return Response(context, status=status.HTTP_200_OK)
context = {"success": False, "message": "Updated Failed, Invalid Input Data", "error": str(serializer.errors)}
return Response(context, status=status.HTTP_400_BAD_REQUEST)
except Exception as error:
context = {'error': str(error), 'success': "false", 'message': 'Failed To Update Race.'}
return Response(context, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
In serializers.py
class RaceUpdateSerializer(ModelSerializer):
class Meta:
model = Race
fields = '__all__'
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Instantiate the superclass normally
super(RaceUpdateSerializer, self).__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
This way only the mentioned fields which is called from the views.py will be used while updating.
serializer = RaceUpdateSerializer(race_obj, data=request.data, partial=True, fields=('participants',))
It will achieve the task that you are trying to do.
Note - You can allow multiple fields this way as well
serializer = RaceUpdateSerializer(race_obj, data=request.data, partial=True, fields=('field1','field2'))
Related
I have a model which has specific many to many fields to the user model. Now, to prevent information leaking, I do not want to return the whole related field though the rest framework. But, I want to create some kind of computed field, such that it return True if the requesting user is in the related field, and False otherwise. Is there a way to make this work?
For example, as it stands now, the rest framework will list the users for "user_like" and
"user_bookmark", which I dont want to happen, hence I want to exclude them from the serialized. But I want to have a field, say, named is_liked, which will be true if request.user is in user_like, and false otherwise.
My current setup:
model
class Post(models.Model):
title = models.CharField(max_length=100)
user = models.ForeignKey(User, on_delete=models.CASCADE)
image = models.ImageField(upload_to='dream_photos')
description = models.TextField(max_length=500)
date_added = models.DateTimeField(auto_now_add=True)
user_like = models.ManyToManyField(User, related_name='likes', blank=True)
user_bookmark = models.ManyToManyField(
User, related_name='bookmarks', blank=True)
total_likes = models.PositiveIntegerField(db_index=True, default=0)
tags = TaggableManager()
serialiser
class PostSerializer(TaggitSerializer, serializers.ModelSerializer):
tags = TagListSerializerField()
class Meta:
model = Dream
fields = ('title','user', 'image','description','date_added', 'tags', 'total_likes' )
views
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.prefetch_related('user').all()
serializer_class = PostSerializer
permission_classes = [permissions.IsAuthenticated]
#action(detail=False, methods=['get'], url_path='current-profile', url_name='current-profile')
def current_user_posts(self, request):
# I expected this to add the extra field I required
# But it does not seem to work as expected
queryset = self.get_queryset().filter(user=request.user).annotate(
bookmark=(request.user in "user_bookmark"))
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
Expected behavior when requesting:
{
"id": 1,
"tags": [
"test"
],
"title": "Tets",
"image": "http://127.0.0.1:8000/media/post_photos/photo1648638314.jpeg",
"description": "TEst",
"date_added": "2022-05-20T17:47:55.739431Z",
"total_likes": 0,
"user": 1,
"like": true, // true if current user is in user_like, false otherwise
"bookmark": true // true if current user is in user_bookmark, false otherwise
}
Actual behavior
TypeError: 'in ' requires string as left operand, not SimpleLazyObject
Edit 1:
The answer from here seems to help to resolve the error. Unfortunately, the annotated field does not seem to be returned by the serializer
the edited view:
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.prefetch_related('user').all()
serializer_class = PostSerializer
permission_classes = [permissions.IsAuthenticated]
#action(detail=False, methods=['get'], url_path='current-profile', url_name='current-profile')
def current_user_posts(self, request):
queryset = self.get_queryset().filter(user=request.user).annotate(
bookmark=Exists(Post.user_bookmark.through.objects.filter(
post_id=OuterRef('pk'), user_id=request.user.id))
)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
What you can do is add your custom fields to the serializer with SerializerMethodField and pass the request.user via get_serializer_context in your view. For example:
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.prefetch_related('user').all()
serializer_class = PostSerializer
permission_classes = [permissions.IsAuthenticated]
#action(detail=False, methods=['get'], url_path='current-profile', url_name='current-profile')
def current_user_posts(self, request):
queryset = self.get_queryset().filter(user=request.user)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
def get_serializer_context(self):
context = super(PostViewSet, self).get_serializer_context()
context.update({"request": self.request})
return context
This allows you to sent the request via the context which can be used by the serializer. Now in your serializer you can add this two new fields:
class PostSerializer(TaggitSerializer, serializers.ModelSerializer):
tags = TagListSerializerField()
bookmark = serializers.SerializerMethodField()
like = serializers.SerializerMethodField()
class Meta:
model = Post
fields = ('title','user', 'image','description','date_added', 'tags', 'total_likes', 'bookmark', 'like')
def get_like(self, obj):
return self.context['request'].user in obj.user_like.all()
def get_bookmark(self, obj):
return self.context['request'].user in obj.user_bookmark.all()
I am new to DRF.
While creating the twitter app I faced a problem with serializers. Since the user is required - I need somehow to pass the actual user to the TweetSerializer class. I have tried different methods that did not work.
This is giving me an error
owner = serializers.HiddenField(default=serializers.CurrentUserDefault())
error image
error image continued
also i have tried to se the user by passing it tot he serializer constructor
serializer = TweetSerializer(owner=request.user)
also did not work
class TweetSerializer(serializers.ModelSerializer):
try:
owner = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
except Exception as exception:
print(exception)
class Meta:
model = Tweet
fields = '__all__'
read_only_fields = ['owner']
def validate(self, attrs):
if len(attrs['content']) > MAX_TWEET_LENGTH:
raise serializers.ValidationError("This tweet is too long")
return attrs
class Tweet(models.Model):
id = models.AutoField(primary_key=True)
owner = models.ForeignKey('auth.User', related_name='tweets', on_delete=models.CASCADE)
content = models.TextField(blank=True, null=True)
image = models.FileField(upload_to='images/', blank=True, null=True)
class Meta:
ordering = ['-id']
#api_view(['POST'])
def tweet_create_view(request, *args, **kwargs):
serializer = TweetSerializer(data=request.POST)
user = request.user
if serializer.is_valid():
serializer.save()
else:
print(serializer.errors)
return JsonResponse({}, status=400)
try:
pass
except Exception as e:
print(str(e))
return JsonResponse({}, status=400, safe=False)
return JsonResponse({}, status=201)
The solution was to pass a context as I read from drf documentation for CurrentUserDefault()
#api_view(['POST'])
def tweet_create_view(request, *args, **kwargs):
context = {
"request" : request
}
serializer = TweetSerializer(data=request.POST, context=context)
if serializer.is_valid():
serializer.save()
return JsonResponse({}, status=201)
A default class that can be used to represent the current user. In order to use this, the 'request' must have been provided as part of the context dictionary when instantiating the serializer.
I am trying to write a nested serializer which would add serialize 2 models in the same view. Serialization seems to work fine since changes get reflected in the database but I am not able to get the many-to-many related field data in the response. I have been trying to figure out what the issue might be but still no progress.
Here is my code:
Model
class User(AbstractBaseUser):
AccountName = models.ManyToManyField(Account,
through='User_Account',
through_fields=('user', 'acc'),
related_name='AccountData',
blank=True)
EmailId = models.EmailField(max_length=128, blank=False, null=False)
USERNAME_FIELD = 'EmailId'
REQUIRED_FIELDS = ['AccountName']
class Account(models.Model):
AccountName = models.TextField(max_length=100, blank=False, null=False)
Serializer
class AccountCreationSerializer(ModelSerializer):
class Meta:
model = Account
fields = ["AccountName"]
class SignUpSerializer1(ModelSerializer):
AccountData = AccountCreationSerializer(read_only=True, many=True)
class Meta:
model = User
fields = ['EmailId', 'AccountData', 'password']
extra_kwargs = {'password': {'write_only': True, 'required': True}}
def validate(self, attrs):
attrs = super(SignUpSerializer1, self).validate(attrs=attrs)
attrs.update({"AccountData": self.initial_data.get("AccountData")})
return attrs
def create(self, validated_data):
AccountName_data = validated_data.pop('AccountData')
acc = Account.objects.create(AccountName=AccountName_data)
userAcc = User.objects.create_user(**validated_data)
if acc:
userAcc.AccountName.add(acc)
print("added")
return userAcc
View
class SignUpView(APIView):
serializer_class1 = SignUpSerializer1
def post(self, request, *args, **kwargs):
if request.data['CreateAccount']:
serializer = self.serializer_class1(data=request.data)
is_valid_serializer = serializer.is_valid(raise_exception=True)
if is_valid_serializer:
with transaction.atomic():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
raise Exception("Bad error")
Request1
"EmailId" : "xyz#gmail.com",
"AccountData":{"AccountName":"TestAcc1"},
"CreateAccount": true,
"password" : "xyz"
Response
"EmailId": "xyz#gmail.com",
#After Removing read_only=true from AccountData
Request2
"EmailId" : "xyz#gmail.com",
"AccountData":{"AccountName":"TestAcc1"},
"CreateAccount": true,
"password" : "xyz"
Response
{"AccountData":{"non_field_errors":["Expected a list of items but got type \"dict\"."]}}
Request3
"EmailId" : "xyz#gmail.com",
"AccountData":[{"AccountName":"TestAcc1"}],
"CreateAccount": true,
"password" : "xyz"
Response
Got AttributeError when attempting to get a value for field `AccountData` on serializer `SignUpSerializer1`.
The serializer field might be named incorrectly and not match any attribute or key on the `User` instance.
Original exception text was: 'User' object has no attribute 'AccountData'.
There is no response data fo AccountName in the response. And when I try print(User.objects.get(EmailId = serializer.data['AccountName'])) ====> None.
How should I get the field populated in the correct way in my response?
Thanks!
You need to specify source argument, since model's field called AccountName, not AccountData:
class SignUpSerializer1(ModelSerializer):
AccountData = AccountCreationSerializer(many=True, source="AccountName")
I'm trying to use Django Rest Framework for updating my database using the HTTP PUT, but when on my client I get the error Exception Value: update() missing 1 required positional argument: 'validated_data' and in the python code I get an error that says Validated_data unfilled.
Here is my model code:
nombre = models.CharField(max_length=200)
calle_numero = models.CharField(max_length=200)
zona_residencial = models.ForeignKey(Zona, on_delete=models.CASCADE)
telefono = models.CharField(max_length=20)
numero_habitantes = models.IntegerField()
tipo_residente = models.CharField(max_length=100,
choices=[(tag.value, tag.value) for tag in TipoHabitanteEnum])
codigo_acceso = models.CharField(max_length=6, default="000000")
status_activacion = models.BooleanField(default=False)
class Meta:
verbose_name = 'Residente'
verbose_name_plural = 'Residentes'
def __str__(self):
return self.nombre
here is my serializer code:
class Meta:
model = Residente
fields = '__all__'
And my view (PUT method where the error is) code:
"""
Modifica un residente
"""
try:
id_residente = self.queryset.get(pk=kwargs["pk"])
serializer = ResidenteSerializer
update_residente = serializer.update(id_residente, request.data)
return Response(ResidenteSerializer(update_residente).data)
except Residente.DoesNotExist:
return Response(
data={
"message": "El residente con id {} no existe".format(kwargs["pk"])
},
status=status.HTTP_404_NOT_FOUND
)
in the update_residente = serializer.update(id_residente, request.data) is the validated_data error is and that's why I can't update my database but I don't know how to fix it.
Hope you can help me.
You are not creating the serializer object.
change the code to this:
"""
Modifica un residente
"""
try:
id_residente = self.queryset.get(pk=kwargs["pk"])
serializer = ResidenteSerializer(id_residente, data=request.data)
if serializer.is_valid(raise_exception=False):
update_residente = serializer.save()
return Response(ResidenteSerializer(update_residente).data)
return Response(serializer.errors, status=400)
except Residente.DoesNotExist:
return Response(
data={
"message": "El residente con id {} no existe".format(kwargs["pk"])
},
status=status.HTTP_404_NOT_FOUND
)
Suggestion for simpler code:
id_residente = get_object_or_404(self.queryset, pk=kwargs["pk"])
serializer = ResidenteSerializer(id_residente, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
This code is same as the above but simpler.
use from rest_framework.generics import get_object_or_404 to import the function.
I have a problem with PATCH Null Value in Django Rest Framework with Extend User Model. Please take a look my issues!
Serializers:, Profile is extend User Model
class UserEditSerializer(ModelSerializer):
job_title = serializers.CharField(source='profile.job_title')
class Meta:
model = User
fields = [
'username',
'job_title',
]
def update(self, instance, validated_data):
instance.username = validated_data.get('username', instance.username)
instance.save()
if (validated_data.get('profile') is not None):
profile_data = validated_data.pop('profile')
profile = instance.profile
profile.job_title = profile_data.get('job_title', profile.job_title)
My viewsets:
class UserUpdateAPIView(ReadOnlyModelViewSet):
queryset = User.objects.all()
serializer_class = UserEditSerializer
permission_classes = (IsAuthenticated,)
#detail_route(methods=['PATCH'])
def edit(self, request):
user_obj = User.objects.get(id=request.user.id)
serializer = UserEditSerializer(user_obj, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return JsonResponse({'message': 'Error'}, status=500)
My api patch request to server:
{
"job_title": ""
}
Error:
{
"detail": "Method \"GET\" not allowed."
}
Error Photo