Different logic for serializer on get and create - django

I want to use one serializer in order to create comments and get list of them.
Here is my comment serializer:
class CommentSerializer(serializers.ModelSerializer):
creator = UserBaseSerializer(source='author')
replies = ShortCommentSerializer(many=True, read_only=True)
reply_on = serializers.PrimaryKeyRelatedField(
queryset=Comment.objects.all(),
write_only=True,
allow_null=True,
required=False
)
author = serializers.PrimaryKeyRelatedField(
queryset=get_user_model().objects.all(),
write_only=True
)
class Meta:
model = Comment
fields = ('id', 'text', 'object_id', 'creator', 'replies', 'reply_on',
'author')
extra_kwargs = {
'text': {'required': True},
'object_id': {'read_only': True}
}
def create(self, validated_data):
validated_data.update(
{'content_type': self.context['content_type'],
'object_id': self.context['pk']}
)
return Comment.objects.create(**validated_data)
My Comment model has field author which is FK to User model. On GET method I'm returning creator as NestedSerializer with source='author'. Also I got author field for write only purposes. I'm trying to figure out is it possible to use author field both for read and write.

It sounds like you want a Writable Nested Serializer. You're on the right track by having a defined create, but the linked documentation should hopefully give a good idea on how to implement this. You can avoid having to loop over your writable field since it's not a many relation.

you can try override method get_serializer_class like this:
def get_serializer_class(self):
if self.request.method == 'POST':
return CommentCreateSerializer
return CommentSerializer
In CommentCreateSerializer, you can write author field directly. And CommentSerializer with source='author' only for api get.
Hoop this help

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

required=False not working with update_or_create in ModelSerializer

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.

Change a field in a Django REST Framework ModelSerializer based on the request type?

Consider this case where I have a Book and Author model.
serializers.py
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = models.Author
fields = ('id', 'name')
class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer(read_only=True)
class Meta:
model = models.Book
fields = ('id', 'title', 'author')
viewsets.py
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
This works great if I send a GET request for a book. I get an output with a nested serializer containing the book details and the nested author details, which is what I want.
However, when I want to create/update a book, I have to send a POST/PUT/PATCH with the nested details of the author instead of just their id. I want to be able to create/update a book object by specifying a author id and not the entire author object.
So, something where my serializer looks like this for a GET request
class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer(read_only=True)
class Meta:
model = models.Book
fields = ('id', 'title', 'author')
and my serializer looks like this for a POST, PUT, PATCH request
class BookSerializer(serializers.ModelSerializer):
author = PrimaryKeyRelatedField(queryset=Author.objects.all())
class Meta:
model = models.Book
fields = ('id', 'title', 'author')
I also do not want to create two entirely separate serializers for each type of request. I'd like to just modify the author field in the BookSerializer.
Lastly, is there a better way of doing this entire thing?
There is a feature of DRF where you can dynamically change the fields on the serializer http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields
My use case: use slug field on GET so we can see nice rep of a relation, but on POST/PUT switch back to the classic primary key update. Adjust your serializer to something like this:
class FooSerializer(serializers.ModelSerializer):
bar = serializers.SlugRelatedField(slug_field='baz', queryset=models.Bar.objects.all())
class Meta:
model = models.Foo
fields = '__all__'
def __init__(self, *args, **kwargs):
super(FooSerializer, self).__init__(*args, **kwargs)
try:
if self.context['request'].method in ['POST', 'PUT']:
self.fields['bar'] = serializers.PrimaryKeyRelatedField(queryset=models.Bar.objects.all())
except KeyError:
pass
The KeyError is sometimes thrown on code initialisation without a request, possibly unit tests.
Enjoy and use responsibly.
IMHO, multiple serializers are only going to create more and more confusion.
Rather I would prefer below solution:
Don't change your viewset (leave it default)
Add .validate() method in your serializer; along with other required .create or .update() etc. Here, real logic will go in
validate() method. Where based on request type we will be creating
validated_data dict as required by our serializer.
I think this is the cleanest approach.
See my similar problem and solution at DRF: Allow all fields in GET request but restrict POST to just one field
You are looking for the get_serializer_class method on the ViewSet. This allows you to switch on request type for which serializer that you want to use.
from rest_framework import viewsets
class MyModelViewSet(viewsets.ModelViewSet):
model = MyModel
queryset = MyModel.objects.all()
def get_serializer_class(self):
if self.action in ('create', 'update', 'partial_update'):
return MySerializerWithPrimaryKeysForCreatingOrUpdating
else:
return MySerializerWithNestedData
I know it's a little late, but just in case someone else needs it. There are some third party packages for drf that allow dynamic setting of included serializer fields via the request query parameters (listed in the official docs: https://www.django-rest-framework.org/api-guide/serializers/#third-party-packages).
IMO the most complete ones are:
https://github.com/AltSchool/dynamic-rest
https://github.com/rsinger86/drf-flex-fields
where (1) has more features than (2) (maybe too many, depending on what you want to do).
With (2) you can do things such as (extracted from the repo's readme):
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ['name', 'population']
class PersonSerializer(FlexFieldsModelSerializer):
country = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Person
fields = ['id', 'name', 'country', 'occupation']
expandable_fields = {
'country': (CountrySerializer, {'source': 'country', 'fields': ['name']})
}
The default response:
{
"id" : 13322,
"name" : "John Doe",
"country" : 12,
"occupation" : "Programmer"
}
When you do a GET /person/13322?expand=country, the response will change to:
{
"id" : 13322,
"name" : "John Doe",
"country" : {
"name" : "United States"
},
"occupation" : "Programmer",
}
Notice how population was ommitted from the nested country object. This is because fields was set to ['name'] when passed to the embedded CountrySerializer.
This way you can keep your POST requests including just an id, and "expand" GET responses to include more details.
The way I ended up dealing with this problem was having another serializer for when it's a related field.
class HumanSerializer(PersonSerializer):
class Meta:
model = Human
fields = PersonSerializer.Meta.fields + (
'firstname',
'middlename',
'lastname',
'sex',
'date_of_birth',
'balance'
)
read_only_fields = ('name',)
class HumanRelatedSerializer(HumanSerializer):
def to_internal_value(self, data):
return self.Meta.model.objects.get(id=data['id'])
class PhoneNumberSerializer(serializers.ModelSerializer):
contact = HumanRelatedSerializer()
class Meta:
model = PhoneNumber
fields = (
'id',
'contact',
'phone',
'extension'
)
You could do something like this, but for the RelatedSerializer do:
def to_internal_value(self, data):
return self.Meta.model.objects.get(id=data)
Thus, when serializing, you serialize the related object, and when de-serializing, you only need the id to get the related object.

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.

Create Custom Error Messages with Model Forms

I can see how to add an error message to a field when using forms, but what about model form?
This is my test model:
class Author(models.Model):
first_name = models.CharField(max_length=125)
last_name = models.CharField(max_length=125)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
My model form:
class AuthorForm(forms.ModelForm):
class Meta:
model = Author
The error message on the fields: first_name and last_name is:
This field is required
How do I change that in a model form?
For simple cases, you can specify custom error messages
class AuthorForm(forms.ModelForm):
first_name = forms.CharField(error_messages={'required': 'Please let us know what to call you!'})
class Meta:
model = Author
New in Django 1.6:
ModelForm accepts several new Meta options.
Fields included in the localized_fields list will be localized (by setting localize on the form field).
The labels, help_texts and error_messages options may be used to customize the default fields, see Overriding the default fields for details.
From that:
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = ('name', 'title', 'birth_date')
labels = {
'name': _('Writer'),
}
help_texts = {
'name': _('Some useful help text.'),
}
error_messages = {
'name': {
'max_length': _("This writer's name is too long."),
},
}
Related: Django's ModelForm - where is the list of Meta options?
Another easy way of doing this is just override it in init.
class AuthorForm(forms.ModelForm):
class Meta:
model = Author
def __init__(self, *args, **kwargs):
super(AuthorForm, self).__init__(*args, **kwargs)
# add custom error messages
self.fields['name'].error_messages.update({
'required': 'Please let us know what to call you!',
})
I have wondered about this many times as well. That's why I finally wrote a small extension to the ModelForm class, which allows me to set arbitrary field attributes - including the error messages - via the Meta class. The code and explanation can be found here: http://blog.brendel.com/2012/01/django-modelforms-setting-any-field.html
You will be able to do things like this:
class AuthorForm(ExtendedMetaModelForm):
class Meta:
model = Author
field_args = {
"first_name" : {
"error_messages" : {
"required" : "Please let us know what to call you!"
}
}
}
I think that's what you are looking for, right?
the easyest way is to override the clean method:
class AuthorForm(forms.ModelForm):
class Meta:
model = Author
def clean(self):
if self.cleaned_data.get('name')=="":
raise forms.ValidationError('No name!')
return self.cleaned_data
I have a cleaner solution, based on jamesmfriedman's answer.
This solution is even more DRY, especially if you have lots of fields.
custom_errors = {
'required': 'Your custom error message'
}
class AuthorForm(forms.ModelForm):
class Meta:
model = Author
def __init__(self, *args, **kwargs):
super(AuthorForm, self).__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].error_messages = custom_errors
You can easily check and put custom error message by overriding clean()method and using self.add_error(field, message):
def clean(self):
super(PromotionForm, self).clean()
error_message = ''
field = ''
# reusable check
if self.cleaned_data['reusable'] == 0:
error_message = 'reusable should not be zero'
field = 'reusable'
self.add_error(field, error_message)
raise ValidationError(error_message)
return self.cleaned_data