I'm trying to make an image upload through a REST API from a mobile client. I've managed to implement it using an multipart request to the REST endpoint, but when I try to update the image, the request is not handled correctly because of the constraints on the OneToOneField.
This is how I implemented the API:
models.py
class Hotel(models.Model):
name = models.CharField(max_length=500, null=False, blank=False)
address = models.CharField(max_length=500, null=False, blank=False)
rating = models.FloatField()
owner = models.CharField(max_length=200, null=False)
class Meta:
ordering = ['name']
class HotelPhoto(models.Model):
photo = models.ImageField(upload_to='hotel_photos', null=True)
hotel = models.OneToOneField(Hotel, on_delete=models.CASCADE, primary_key=True)
views.py
class HotelPhotoUpload(APIView):
parser_classes = [FormParser, MultiPartParser]
def post(self, request):
photo_serializer = HotelPhotoSerializer(data=request.data,
context={'request': request})
if photo_serializer.is_valid():
photo_serializer.save()
return Response(photo_serializer.data,
status=status.HTTP_201_CREATED)
else:
logger.error(f'Error uploading image: {photo_serializer.errors}')
return Response(photo_serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
serializers.py
class HotelSerializer(serializers.HyperlinkedModelSerializer):
photo = serializers.ImageField(source='hotelphoto.photo', read_only=True)
class Meta:
model = Hotel
fields = ['url', 'id', 'name', 'address', 'rating', 'owner', 'photo']
class HotelPhotoSerializer(serializers.ModelSerializer):
photo_url = serializers.SerializerMethodField()
class Meta:
model = HotelPhoto
fields = ['hotel', 'photo', 'photo_url']
def get_photo_url(self, obj):
return self.context['request'].build_absolute_uri(obj.photo.url)
This is the error I'm getting:
Error uploading image: {'hotel': [ErrorDetail(string='hotel photo with this hotel already exists.', code='unique')]}
Bad Request: /hotels/photo/upload/
I understand this is due to the constraint on the OneToOneField since I've have already uploaded a photo, but how should I do the request in order to just update the HotelPhoto.photo field?
What I've tried
Implementing a put method on the HotelPhotoUpload view with partial=True on the serializer, but it gave the same error.
I thought about overwriting the validate method on the serializer, but I don't know if I need to validate anything on the photo itself. I was hoping the framework would handle this for me.
Thought about merging the HotelPhoto and Hotel models, but that would require a big refactor of other code.
EDIT:
I'm currently using django 3.0.2.
Following the answer by neferpitou, I've managed to get it working after these minor changes:
serializers.py
# Didn't change the HotelSerializer
class HotelPhotoSerializer(serializers.ModelSerializer):
hotel = serializers.PrimaryKeyRelatedField(
many=False,
queryset=Hotel.objects.all())
class Meta:
model = HotelPhoto
fields = ['hotel', 'photo']
def create(self, validated_data):
# Instead of creating a new HotelPhoto instance
# changed the photo field from the Hotel instance
hotel = validated_data.get('hotel')
photo = validated_data.get('photo')
hotel.hotelphoto.photo.save(photo.name, photo)
hotel.save()
return hotel.hotelphoto
views.py
class HotelPhotoUpload(APIView):
parser_classes = [FormParser, MultiPartParser]
def post(self, request):
# I'm already sending the hotel id on the POST request
photo_serializer = HotelPhotoSerializer(data=request.data)
if photo_serializer.is_valid():
photo_serializer.save()
return Response(photo_serializer.data,
status=status.HTTP_201_CREATED)
else:
logger.error(f'Error uploading image: {photo_serializer.errors}')
return Response(photo_serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
One thing that I forgot to mention is that I'm always sending a request (either POST or PUT) to the hotels endpoint (which uses the HotelSerializer) before uploading a photo. So I'm not expecting to have problems on the photo/upload endpoint due to an inexistent hotel.
mobile client
Unfortunaly can't post the Multipart POST request content because it's huge. But here is the client method implementation using Retrofit 2.5.0.
// Sends the hotel id and the entire content of the photo file.
#Multipart
#POST(UPLOAD_ENDPOINT)
fun uploadPhoto(#Part("hotel") hotelId: RequestBody,
#Part photo: MultipartBody.Part) : Call<UploadResult>
companion object {
const val UPLOAD_ENDPOINT = "hotels/photo/upload/"
}
Foreignkey and OneToOneField can be serialized in the same manner.
Here is your
views.py
class HotelPhotoUpload(APIView):
# parser_classes = [FormParser, MultiPartParser]
def post(self, request):
hotel = Hotel.objects.get(name=request.data.get('hotel'))
request.data['hotel'] = hotel.id
photo_serializer = HotelPhotoSerializer(data=request.data)
# print(photo_serializer)
if photo_serializer.is_valid():
photo_serializer.save()
return Response(photo_serializer.data,
status=status.HTTP_201_CREATED)
else:
# logger.error(f'Error uploading image: {photo_serializer.errors}')
return Response(photo_serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
serializers.py
class HotelSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Hotel
fields = '__all__'
class HotelPhotoSerializer(serializers.ModelSerializer):
hotel = serializers.PrimaryKeyRelatedField(many=False, queryset=Hotel.objects.all())
class Meta:
model = HotelPhoto
fields = ['hotel', 'photo',]
def create(self, validated_data):
hotel_photo = HotelPhoto.objects.create(**validated_data)
hotel_photo.save()
return hotel_photo
I am confused why there are extra fields in your HotelSerializer, so I have trimmed it down. If you have specific use case for those feel free to modify in your code. And there are no primary key in your Hotel model, so it will create id field by default and I am assuming every hotel name in unique.
Postman request:
Hotel Data From Admin Section:
Related
I have imported User model and customized it a/c to my need and make OneToOne Relation with UserProfileModel Model. While retrieving data I got this error.
"The serializer field might be named incorrectly and not match any attribute or key on the AnonymousUser instance.
Original exception text was: 'AnonymousUser' object has no attribute 'gender'."
My Model is :
class UserProfileModel(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True, related_name='userprofilemodel')
gender = models.CharField(max_length=20)
locality = models.CharField(max_length=70)
city = models.CharField(max_length=70)
address = models.TextField(max_length=300)
pin = models.IntegerField()
state = models.CharField(max_length=70)
profile_image = models.FileField(upload_to='user_profile_image', blank=True)
My Serializer looks like:
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model= User
fields = ['id', 'name' , 'email','mobile',]
class UserProfileModelSerializer(serializers.ModelSerializer):
user = serializers.StringRelatedField(many=True, read_only=True)
class Meta:
model= UserProfileModel
fields = ['user','gender' , 'locality','city','address','pin','state','profile_image', ]
My view looks like:
class UserProfileDataView(APIView):
def get(self, request, format=None):
# user = UserProfileModel.objects.all()
serializer = UserProfileModelSerializer(request.user)
return Response(serializer.data, status=status.HTTP_200_OK)
I want to retrieve profile data of the logged user using UserProfileModel Model
Your first issue in that you are passing a User instance to the UserProfileModelSerializer, which is expecting a UserProfileModel instance. To fix this you need to change:
serializer = UserProfileModelSerializer(request.user)
to
serializer = UserProfileModelSerializer(request.user.userprofilemodel)
where userprofilemodel is the related_name you have set on the user field in your UserProfileModel.
Second issue is, as Mohamed Beltagy said, you're allowing anyone to access the view, including unauthenticated users. Django rest framework has a built in mixin that you can use to restrict access to authenticated users only (https://www.django-rest-framework.org/api-guide/permissions/#isauthenticated).
from rest_framework.permissions import IsAuthenticated
class UserProfileDataView(APIView):
permission_classes = [IsAuthenticated]
the problem here is you are passing an anonymous user which has no profile ( you permit non-authenticated users access this view)
def get(self, request, format=None):
# user = UserProfileModel.objects.all()
if request.user.is_authenticated:
serializer = UserProfileModelSerializer(request.user)
return Response(serializer.data, status=status.HTTP_200_OK)
else:
return Response(status=status.HTTP_401_UNAUTHORIZED)
everyone, I am absolutely a beginner in DjangoRestFramework. I have confusion to deal with relationships in DRF. How do I save foreign keys data using APIView?
models
class User(AbstractUser):
email = models.EmailField(max_length=255, unique=True)
is_client = models.BooleanField(default=False)
is_professional = models.BooleanField(default=False)
class Client(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='client')
##
class Professionals(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='professional')
##
class HireProfessional(models.Model):
client = models.ForeignKey(Client, related_name='user', on_delete=models.CASCADE)
professional = models.ForeignKey(Professionals, on_delete=models.CASCADE, related_name="professsional")
hire_start_date_time = models.DateTimeField(default=timezone.now)
Serializers
class ProfessionalSerializer(serializers.ModelSerializer):
profilePicture = serializers.ImageField(allow_empty_file=True, use_url='professional/profiles', required=False)
skill = SkillSerializer(many=True,read_only=True)
class Meta:
model = Professionals
fields = fields = ['first_name', 'last_name', 'profilePicture', 'profession', 'phone_number', 'experience', 'skill', 'charge_fee', 'about_me']
class ClientSerializer(serializers.ModelSerializer):
class Meta:
model = Client
fields = ['user_id', 'first_name', 'last_name', 'phone_number', 'profilePicture']
class UserSerializer(serializers.ModelSerializer):
client = ClientSerializer()
professional = ProfessionalSerializer()
class Meta:
model = User
fields = ('email', 'username', 'is_client', 'is_professional', 'client', 'professional')
class HireProfessionalSerializer(serializers.ModelSerializer):
client = ClientSerializer()
professional = professionalSerializer()
class Meta:
model = HireProfessional
fields = ['id','client', 'professional', 'hire_start_date_time']
views ##Edited
class HireProfessionalCreateApp(APIView):
permission_classes = (IsAuthenticated, IsClientUser,)
def current_user(self):
user = self.request.user.client
return user
def post(self, request, username, format=None):
try:
professional = Professionals.objects.get(user__username=username)
# print('hello', professional.user_id)
data = request.data
data['client'] = self.current_user()
data['professional'] = professional.user_id
serializer = HireProfessionalSerializer(data=data)
data = {}
if serializer.is_valid():
hire = serializer.save()
hire.save()
return JsonResponse ({
"message":"professional hired success.",
# "remaning_time":remaning_datetime,
"success" : True,
"result" : serializer.data,
"status" : status.HTTP_201_CREATED
})
else:
data = serializer.errors
print(data)
return Response(serializer.data)
except Professionals.DoesNotExist:
return JsonResponse ({"status":status.HTTP_404_NOT_FOUND, 'message':'professional does not exists'})
This is a hiring app.
Client able to hire to professional
client=logged in user
professional=passed id or username through URL
like: path('hire-professional/<professional id or username>', views)
Does anybody have any idea? how to solve it.
Consider using a ModelViewSet instead of an APIView since you're directly modifying the HireProfessional model. Additionally, the model uses ForeignKey fields for Client and Professional, you do not need to include their respective serializers in HireProfessionalSerializer.
Implementing just the ModelViewSet (and adding it to the urls.py using a router) will mean that a user can select the client & professional themself, which means we're not done yet. It is recommended to use a router in DRF instead of adding all the views to urls.py manually.
You can use the ModelViewSet to override the perform_create and perform_update functions in which you autofill serializer fields. :
default create function
def perform_create(self, serializer):
serializer.save()
autofill example
def perform_create(self, serializer):
# client is derived from logged in user
client = Client.objects.get(user=self.request.user)
# Autofill FK Client.
serializer.save(client=client)
Now the client is autofilled, but the professional isn't yet. If you want to also autofill the professional you will have to look into using a nested router since you want to retrieve the PK(id) from the URL. If you do so your URL would look something like this:
url='/professionals/{professionals_pk}/hire/'
I hope this gives you an idea of where to start. If you have any questions do let me know.
I'm implementing some voting functionality in an application, where a logged-in user specifies a post that they would like to vote for using a payload like this:
{
"post": 1,
"value": 1
}
As you can tell, the a user field is absent - this is because it gets set in my viewset's perform_create method. I've done this to ensure the vote's user gets set server side. This is what the viewset looks like:
class CreateVoteView(generics.CreateAPIView):
permission_classes = (permissions.IsAuthenticated,)
serializer_class = VoteSerializer
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Here is what the model looks like:
class Vote(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='votes', null=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='votes', null=False)
class Values(models.IntegerChoices):
UP = 1, _('Up')
DOWN = -1, _('Down')
value = models.IntegerField(choices=Values.choices, null=False)
class Meta:
unique_together = ('post', 'user')
and finally, the serializer:
class VoteSerializer(serializers.ModelSerializer):
class Meta:
model = Vote
fields = ['post', 'value']
From what I understand, in order for DRF to enforce a unique together validation, both fields (in my case, user and post) must be included in the serializer's fields. As I've mentioned, I'd like to avoid this. Is there any other way of implementing this type of validation logic?
EDIT:
To clarify: the records do not save - I receive this error:
django.db.utils.IntegrityError: (1062, "Duplicate entry '1-3' for key 'api_vote.api_vote_post_id_user_id_73614533_uniq'")
However, my goal is to return a Bad Request instead of an Internal Server Error much like I would when traditionally using a DRF serializer and excluding required fields from a payload.
To output a custom error message due to the IntegrityError, you can override the create method in your serializer:
from django.db import IntegrityError
class VoteSerializer(serializers.ModelSerializer):
class Meta:
model = Vote
fields = ['post', 'value']
def create(self, validated_data):
try:
validated_data['user'] = self.context['request'].user
return super().create(validated_data)
except IntegrityError:
error_msg = {'error': 'IntegrityError message'}
raise serializers.ValidationError(error_msg)
You can try this on your views
try:
MoviesWatchList.objects.create(user=request.user, content=movie)
return response.Response({'message': f'{movie} added in watchlist.'}, status=status.HTTP_201_CREATED)
except:
return response.Response({'message': f'{movie} already added to watchlist.'}, status=status.HTTP_304_NOT_MODIFIED)
models.py
class UserContentItem(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=200)
created_date = models.DateTimeField(default=timezone.now)
views.py
class UserContentItemView(APIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly, )
def post(self, request, format=None):
data = request.data
data['owner'] = request.user.id
serializer = UserContentItemSerializer(data=data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializers.py
class UserContentItemSerializer(serializers.ModelSerializer):
class Meta:
model = UserContentItem
fields = ('id', 'owner', 'title', 'created_date')
I am building an API with Django Rest Framework and simple jwt. I want to allow authenticated users to POST a new UserContentItem that has a FK dependency on the User but the User is not part of the POST payload. The only way I've been able to figure out how to do this is as above, adding the request.user.id to the request data before passing it to the serializer. Is there a better way to serialize the UserContentItem and achieve the same goal?
I think you can try like this using CurrentUserDefault:
class UserContentItemSerializer(serializers.ModelSerializer):
owner = serializers.PrimaryKeyRelatedField(
read_only=True,
default=serializers.CurrentUserDefault()
)
class Meta:
model = UserContentItem
fields = ('id', 'owner', 'title', 'created_date')
And in view, pass request as context to serializer:
serializer = UserContentItemSerializer(data=data,context={'request':request})
Also you don't need to pass user id with data.
Your approach is good even you can follow these
serializer.save(owner=self.request.user)
I am using the django rest framework and I have a very simple model of Posts for a particular user which I have serialised in the following manner.
Serializers.py
class PostSerializer(serializers.HyperlinkedModelSerializer):
image = serializers.ImageField(max_length=None, use_url=True)
question = serializers.CharField(required=False)
ayes = serializers.CharField(required=False)
nays = serializers.CharField(required=False)
neutrals = serializers.CharField(required=False)
class Meta:
model = Posts
fields = ('user','question', 'image','ayes', 'nays', 'neutrals')
My models.py is as follows
class Posts(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
question = models.TextField(max_length=500, blank=True)
image = models.ImageField('optionalImage', upload_to='images/posts/', default='/images/posts/blank.png')
ayes = models.TextField(max_length=200, default=0)
nays = models.TextField(max_length=200, default=0)
neutrals = models.TextField(max_length=200, default=0)
When I tried posting to this I kept getting NOT NULL Integrity constraint error of user_id. Hence I added context={'request': request}) to the serializer which ends up giving me the following error:
Could not resolve URL for hyperlinked relationship using view name "user-detail". You may have failed to include the related model in your API, or incorrectly configured the lookup_field attribute on this field.
My views.py is as follows:
views.py
#permission_classes((IsAuthenticated, ))
class PostsView(generics.ListCreateAPIView):
queryset = Posts.objects.all()
serializer_class = PostSerializer
def get(self, request, format=None):
snippets = Posts.objects.filter(pk=request.user.id)
serializer = PostSerializer(snippets, many=True,context={'request': request})
return Response(serializer.data)
def post(self, request, format=None):
posts = PostSerializer(data=request.data,context={'request': request})
if posts.is_valid():
posts.save()
return Response("YOLO", status=status.HTTP_201_CREATED)
else:
return Response(posts.errors, status=status.HTTP_400_BAD_REQUEST)
All other fields of mine have posted correctly when I set default=0 in my model for user. I am unable to submit the foreign key user which needs to be saved on every post. What am I doing wrong here? Am I following the correct method?
Since you don't want to send your user, you should remove it from the serializer's field.
Next, you want to set the post's user to the current user. To achieve that, you want to pass the request.user to the serializer's data by changing the save method to:
posts.save(user=request.user)
It's explained in the documentation and in the tutorial