required=False not working with update_or_create in ModelSerializer - django

I am creating an API where I get some data with user_id field. If given user_id exist in DB it update rest of the data else it will create new data. Here my code:
serializers.py
class DataUpdateSerializer(serializers.ModelSerializer):
user_id = serializers.CharField(max_length=250)
email = serializers.EmailField(required=False)
first_name = serializers.CharField(required=False)
last_name = serializers.CharField(required=False)
class Meta:
model = User
fields = ('first_name', 'last_name', 'email', 'user_id', 'balance', 'device_id', 'platform')
def create(self, validated_data):
data_update, created = User.objects.update_or_create(user_id=validated_data['user_id'],
defaults={
'first_name': validated_data['first_name'],
'last_name': validated_data['last_name'],
'email': validated_data['email'],
'balance': validated_data['balance'],
'device_id': validated_data['device_id'],
'platform': validated_data['platform'],
}
)
return data_update
views.py
class data_update(APIView):
permission_classes = (Check_API_KEY_Auth,)
def post(self, request):
serializer = DataUpdateSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
username = request.data['user_id']
User.objects.filter(user_id=username).update(username=username)
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_204_NO_CONTENT)
email, first_name and last_name are optional and can be blank in DB or in serializer.
Models.py
class User(AbstractUser):
device_id = models.CharField(max_length=250)
balance = models.FloatField()
platform = models.CharField(max_length=250)
user_id = models.CharField(max_length=250, unique=True)
Problem:
Everything is working fine but if I don't pass email, first_name or last_name, it gives error: key_error and if I provide these fields with blank value "", this give error cannot be blank

I see various problems in your code.
In the declaration of CharField in the model, you don't have blank=True. Hence, they simply cannot be blank in the DB, whatever you do above.
Be careful when using a very basic APIView and manipulating the seralizer yourself. Your view is called "Update" but it does both "create" and "update". These are two different things, usually handled by two different routes.
The usage you make of your serializer DataUpdateSerializer(data=request.data) is the usage for a CREATE, not an UPDATE, where you pass the instance of the relevant model object as first argument.
The usage you make of objects.update_or_create must be checked. You basically make a queryset filtering using fields that may or may not be present, whose values may or may not have been changed, and if by any chance that filtering makes any sense, and you get nothing in return, then you create the object...
I would suggest to split the routes. Make a ListAPIView where you can CREATE, and a RetrieveUpdateDestroyAPIView taking the user_id as parameter in your REST route, and simply declare the serializer class in the views. Then, use the create and update methods of the serializer itself. However, if everything is simply declared, you'd rarely need to write anything in these methods.

Related

Single Update and Delete API for two models connected with a OneToOne relationship in Django Rest Framework

I've looked extensively on here and probably exhausted all the answers and still haven't found a solution to my particular problem, which is to make an API that update/delete from both models, and I am getting the following error:
The .update()method does not support writable nested fields by default. Write an explicit.update()method for serializeruser_profile.serializers.UserSerializer, or set read_only=True on nested serializer fields.
In this particular instance this happens when I try to update a field from the user_profile model
I have separated my Django project into several apps/folders with each model being in its own folder.
I have a user app and a user_profile app each with their own models.
the user model is basically an AbstractUser sitting in its own app
the user_profile model is as follows:
class UserProfile(models.Model):
user = models.OneToOneField(to=User, on_delete=models.CASCADE, related_name='userprofile')
location = models.CharField(blank=True, max_length=30)
created_time = models.DateTimeField(auto_now_add=True)
updated_time = models.DateTimeField(auto_now=True)
The serializers are as follows:
class UserProfileCrudSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = ('location', 'created_time', 'updated_time')
class UserSerializer(serializers.ModelSerializer):
profile = UserProfileCrudSerializer(source='userprofile', many=False)
class Meta:
model = User
fields = ('username', 'email', 'first_name', 'last_name', 'profile')
def update(self, instance, validated_data):
userprofile_serializer = self.fields['profile']
userprofile_instance = instance.userprofile
userprofile_data = validated_data.pop('userprofile', {})
userprofile_serializer.update(userprofile_instance, userprofile_data)
instance = super().update(instance, validated_data)
return instance
and my view is:
class RetrieveUpdateView(RetrieveUpdateAPIView):
serializer_class = UserSerializer
queryset = User.objects.all()
def get_object(self):
return self.request.user
when I do a GET I am getting the following response without any problems:
{
"username": "blue",
"email": "bluebear#bluebear.com",
"first_name": "Blue",
"last_name": "Bear",
"profile": {
"location": "London",
"created_time": "2023-02-03T00:39:15.149924Z",
"updated_time": "2023-02-03T00:39:15.149924Z"
}
}
and I do a patch request like this:
{
"profile": {
"location": "Paris"
}
}
The way the code is now I have no issue updating username, email, first_name, and last_name which come from the AbstractUser but I am getting the above error when I try to patch the location which is in the UserProfile model.
I've looked at many similar solutions online, but none that pertain to my particular situation.
The .update() method does not support writable nested fields by default. Write an explicit .update() method for serializeruser_profile.serializers.UserSerializer, or set read_only=True on nested serializer fields.
It already shows in the message, you need to explicitly write the update method for the writable nested serializer which is documented here https://www.django-rest-framework.org/topics/writable-nested-serializers/ or you can use another module that is also referred to in the docs https://github.com/beda-software/drf-writable-nested.
Your approach is correct already but there is some typo and wrong indentation in your code:
class UserSerializer(serializers.ModelSerializer):
profile = UserProfileCrudSerializer(source='userprofile', many=False)
class Meta:
model = User
fields = ('username', 'email', 'first_name', 'last_name', 'profile')
def update(self, instance, validated_data):
# update is a method of serializer not serializer.Meta
userprofile_serializer = self.fields['profile']
userprofile_instance = instance.userprofile
# should be 'profile' here instead of 'userprofile' as you defined in serializer
userprofile_data = validated_data.pop('profile', {})
userprofile_serializer.update(userprofile_instance, userprofile_data)
instance = super().update(instance, validated_data)
return instance

prevent user creating if another create unsuccessful django

Probably there is the answer to this question, but I couldn't find it.
How to prevent User - from django.contrib.auth.models import User, creation if userProfileSerializer creating is unsuccessful. I saw a database transaction is an option but it says
While the simplicity of this transaction model is appealing, it also
makes it inefficient when traffic increases. Opening a transaction for
every view has some overhead. The impact on performance depends on the
query patterns of your application and on how well your database
handles locking.
#api_view(['POST'])
#permission_classes([AllowAny])
def register(request):
'''
Registers user to the server. Input should be in the format:
{"username": "username", "password": "1234abcd"}
'''
# Put the data from the request into the serializer
serializer = CreateUserSerializer(data=request.data)
# Validate the data
if serializer.is_valid():
# If it is valid, save the data (creates a user).
serializer.save()
userProfileSerializer = UserProfileSerializer(data=request.data)
userProfileSerializer.context['user_id'] = serializer.data['id']
userProfileSerializer.is_valid(raise_exception=True)
userProfileSerializer.save()
Serializer classes
class CreateUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'email', 'password', 'first_name', 'last_name',)
extra_kwargs = {
'password': {'write_only': True}
}
def create(self, validated_data):
user = User.objects.create_user(**validated_data)
return user
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = ('title', 'organization', 'user_id')
def create(self, validated_data):
user_id = self.context["user_id"]
user_profile = UserProfile(**validated_data, user_id=user_id)
user_profile.save()
return user_profile
You can do the following:
check that UserProfileSerializer is valid, as you doing already: userProfileSerializer.is_valid(raise_exception=True)
save user serializer as usual after UserProfileSerializer is validated: serializer.save()
Their are some ways,
use transaction.atomic, which you know.
delete the created user after userProfileSerializer unsuccessfull.
Not a way but a trick, Validate both serializers first, confirming everything is all right, then save the user, add user id in profile_serializer, then save the profile_serializer at last. This avoids the transaction.
But it is just a trick; Use transaction as your use case requires it. Its the best way.

Django Rest Framework unique together validation with field absent from request

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

Django DRF rename field in serializer but unique_together validation fails

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

Django Rest Framework return nested object using PrimaryKeyRelatedField

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.