I have two models: FacebookAccount and Person. There's a OneToOne relationship like so:
class FacebookAccount(models.Model):
person = models.OneToOneField(Person, related_name='facebook')
name = models.CharField(max_length=50, unique=True)
page_id = models.CharField(max_length=100, blank=True)
I created a PersonSerializer which has a facebook field set to a FacebookSerializer I created:
class FacebookSerializer(serializers.ModelSerializer):
class Meta:
model = FacebookAccount
fields = ('name', 'page_id',)
class PersonSerializer(serializers.ModelSerializer):
facebook = FacebookSerializer(required=False)
class Meta:
model = Person
fields = ('id', 'name', 'facebook',)
I then created a view to create a new Person along with a new FacebookAccount instance for a POST request:
class PersonCreate(APIView):
def post(self, request):
# Checking for something here, doesn't affect anything
if 'token' in request.DATA:
serializer = PersonSerializer(data=request.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)
This works fine and well when my POST data has a facebook object with a unique name attribute in it. However, what if the facebook information is optional? I set "required=False" in the facebook field on the PersonSerializer to account for this, but I get a 400 error when my POST data does not contain a facebook object. The error says that the "name" field for facebook is required.
If I set "blank=True" on the name field in the FacebookAccount model and don't provide and facebook info in my POST data, a blank FacebookAccount record is created which is expected but not desired.
Does anyone know how to fix this? Thanks.
edit:
I tried overriding the save_object method on the PersonSerializer, but it looks like that method isn't called. I tried working my way back through the methods that are called, and it looks like get_field is the last one:
class PersonSerializer(serializers.ModelSerializer):
facebook = FacebookSerializer(required=False)
def get_default_fields(self):
field = super(PersonSerializer, self).get_default_fields()
print field
return field
def get_pk_field(self, model_field):
print 'MF'
mf = super(PersonSerializer, self).get_pk_field(model_field)
print mf
return mf
def get_field(self, model_field):
print 'FIELD'
mf = super(PersonSerializer, self).get_field(model_field)
print mf
return mf
# not called
def get_validation_exclusions(self, instance=None):
print '**Exclusions'
exclusions = super(PersonSerializer, self).get_validation_exclusions(instance=None)
print exclusions
return exclusions
# not called
def save_object(self, obj, **kwargs):
if isinstance(obj, FacebookAccount):
print 'HELLO'
else:
print '*NONONO'
return super(PersonSerializer, self).save_object(obj, **kwargs)
class Meta:
model = Person
fields = ('id', 'name', 'stripped_name', 'source', 'facebook',)
Simply pass allow_null=True when declaring the nested serializer in the parent serializer, like this:
class FacebookSerializer(serializers.ModelSerializer):
class Meta:
model = FacebookAccount
fields = ('name', 'page_id',)
class PersonSerializer(serializers.ModelSerializer):
facebook = FacebookSerializer(allow_null=True, required=False)
class Meta:
model = Person
fields = ('id', 'name', 'facebook',)
Well, that is the limitation of django-restframework when specifying reverse relation as a field, because it creates a list from the fields specified , required=false only directs the serializer to save the default null value for that related field, to resolve it you can override save_object method of serializer and remove facebook field if its null.
Sorry for late reply, i have somewhat similar models and it is working, by overriding save_object method, the is_valid() must be returning false (have you checked it), anyway saving objects using a serializer is for very basic use cases thus limited, best way would be to save those objects explicitly in the view.
Related
I am trying to check if the user id not equal to 1 then he should not be able to update few fields. I tried something similar to the following code but it did not work because of the following issues
self.user.id don't actually return the user I need to get the authenticated user in different why?
the def function maybe should have a different name like update?
also the general way maybe wrong?
class ForAdmins(serializers.ModelSerializer)):
class Meta:
model = User
fields = '__all__'
class ForUsers(serializers.ModelSerializer)):
class Meta:
read_only_fields = ['email','is_role_veryfied','is_email_veryfied']
model = User
fields = '__all__'
class UsersSerializer(QueryFieldsMixin, serializers.ModelSerializer):
def customize_read_only(self, instance, validated_data):
if (self.user.id==1):
return ForAdmins
else:
return ForUsers
class Meta:
# read_only_fields = ['username']
model = User
fields = '__all__'
You can make the decision which serializer you want to pass from your views
or
you can do it inside modelSerializer update method.
for getting user from Serializer class Try:
request = self.context.get('request', None)
if request:
user = request.user
for getting user from View class Try:
user = self.request.user
I'm using Django 2.x and Django REST Framework.
I have a model with contact as a foreign key
class AmountGiven(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
contact = models.ForeignKey(Contact, on_delete=models.PROTECT)
amount = models.FloatField(help_text='Amount given to the contact')
given_date = models.DateField(default=timezone.now)
created = models.DateTimeField(auto_now=True)
and serializer like
class AmountGivenSerializer(serializers.ModelSerializer):
mode_of_payment = serializers.PrimaryKeyRelatedField(queryset=ModeOfPayment.objects.all())
contact_detail = ContactSerializer(source='contact', read_only=True)
contact = serializers.PrimaryKeyRelatedField(queryset=Contact.objects.all())
class Meta:
model = AmountGiven
depth = 1
fields = (
'id', 'contact', 'contact_detail', 'amount', 'given_date', 'created'
)
contact field is required while creating a new record. But I do not want contact to be modified once it is created.
But when I send only amount with PUT method it says
{
"contact": [
"This field is required."
]
}
And when I use PATCH method, it works fine but if passing some other value for contact, it is updating contact as well.
I want to make contact field not-required while updating existing record. And even if it is passed, use the earlier one instead of setting the new data.
Trial 2
I tried overriding the contact field in the request to the previously stored value so that in case if changed contact is passed or no contact is passed, it will save earlier one.
So, in the viewset add the function
def update(self, request, *args, **kwargs):
obj = self.get_object()
request.data['contact'] = obj.contact_id
return super().update(request, *args, **kwargs)
But this is giving error as
This QueryDict instance is immutable
Use __init__ method of serializer to make it read when object is being updated:
class AmountGivenSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
"""If object is being updated don't allow contact to be changed."""
super().__init__(*args, **kwargs)
if self.instance is not None:
self.fields.get('parent').read_only = True
# self.fields.pop('parent') # or remove the field
mode_of_payment = serializers.PrimaryKeyRelatedField(queryset=ModeOfPayment.objects.all())
contact_detail = ContactSerializer(source='contact', read_only=True)
contact = serializers.PrimaryKeyRelatedField(queryset=Contact.objects.all())
class Meta:
model = AmountGiven
depth = 1
fields = (
'id', 'contact', 'contact_detail', 'amount', 'given_date', 'created'
)
Using self.context['view'].action is not recommended as it will not work when using the serializer out of DRF, eg. in normal Django views. It's best to use self.instance as it will work in every situation.
If your viewset is a ModelViewSet, you can overwrite the perform_update hook (because ModelViewSet inherits from GenericAPIView (take a look at "Save and deletion hooks"). You can access the old contact using the serializer's instance field:
class MyViewSet(viewsets.ModelViewSet):
# ... other stuff
def perform_update(self, serializer):
serializer.save(contact=serializer.instance.contact)
So you will have to provide a contact, but no matter which contact you provide, it will always use the old saved contact when updating.
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.
I have a contact form on a site that is posting to a CreateAPIView to create a new instance of a model (that is eventually emailed to the admin). On my serializer I have a honeypot field to help reject spam.
The model:
class Message(models.Model):
name = ...
message = ...
and serializer:
class MessageSerializer(serializers.ModelSerializer):
# Honeypot field
url = serializers.CharField(allow_blank=True, required=False)
class Meta:
model = Message
fields = '__all__'
def validate_url(self, value):
if value and len(value) > 0:
raise serializers.ValidationError('Spam')
return value
and view:
class MessageView(generics.CreateAPIView):
''' Create a new contact form message. '''
serializer_class = MessageSerializer
My problem is that as it stands, when I post to this view, I get the error:
TypeError: Got a TypeError when calling Message.objects.create(). This may be because you have a writable field on the serializer class that is not a valid argument to Message.objects.create(). You may need to make the field read-only, or override the MessageSerializer.create() method to handle this correctly.
so obviously the seriazlier is attempting to save the url field to the model in CreateApiView.perform_create()
I tried adding read_only to the serializer field, but this means that the url_validate method is skipped altogether.
How can I keep the field on the serializer until validation has occurred, removing it before the serializer.save() is called in perform_create()?
you can do this overriding the create method like:
class MessageSerializer(serializers.ModelSerializer):
# Honeypot field
url = serializers.CharField(allow_blank=True, required=False)
class Meta:
model = Message
fields = '__all__'
def validate_url(self, value):
if value and len(value) > 0:
raise serializers.ValidationError('Spam')
return value
def create(self, validated_data):
data = validated_data.pop('url')
return Message.objects.create(**data)
OK, I didn't read the error correctly. As it clearly says:
override the MessageSerializer.create() method to handle this correctly.
I was looking at overwriting the CreateAPIView.create() method which didn't make sense.
This works:
class MessageSerializer(serializers.ModelSerializer):
# Honeypot field
url = serializers.CharField(allow_blank=True, required=False)
class Meta:
model = Message
fields = '__all__'
def validate_url(self, value):
if value and len(value) > 0:
raise serializers.ValidationError('Error')
return value
def create(self, validated_data):
if "url" in validated_data:
del validated_data["url"]
return Message.objects.create(**validated_data)
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.