DRF - Create Serializer: accepting ForeignKey data with Non-Primary Field value - python

I am totally new to Django/DRF, and trying to work with Create/Read/Update of a Model.
Here are the dummy model/serializers:
AddressModel and UserModel:
class AddressModel(models.Model):
name = models.CharField(max_length=255)
street = models.CharField(max_length=255)
class UserModel(models.Model):
email = models.CharField(max_length=255)
address = models.ForeignKey(Address, on_delete=models.PROTECT)
And I have BaseSerializer and WriteSerializer:
class UserBaseSerializer(serializers.ModelSerializer):
email = serializers.CharField(required=True)
address = AddressSerializer()
class Meta:
model = User
fields = ['email', 'address']
class UserWriteSerializer(UserBaseSerializer):
class Meta(UserBaseSerializer.Meta):
read_only_fields = ["created_by"]
def create(self, validated_data):
return super().create(validated_data)
Now the problem is, reading data through BaseSerializer, is working fine, I am able to display User and Address on UI correctly. But having issues with Create/Update.
For creating new user from UI, I have a select dropdown for Address, which has some constant values, these constant values are on UI side, it's not getting fetched from Backend, but backend will have related row in database.
And the issue is, I am not sending primary key of the address, I am sending name field of the address in the post call, so how can I still handle name field on Create serializer to store correct address, and return it in success?
validated_data in create method is not having Address instance. It's omitting that, may be due to non-primary field value.
Solution 1#:
One solution can be to fetch address from backend to display in select dropdown, so that I can send address id in api call.
Solution 2#:
Another solution could be to use serializers.PrimaryKeyRelatedField and convert data using to_internal_values and to_representation_value:
class UserWriteSerializer(UserBaseSerializer):
address = serializers.PrimaryKeyRelatedField(
queryset=Address.objects.all(), required=True
)
def to_internal_value(self, data):
address = Address.objects.filter(name=data.get("name")).first()
data["address"] = address.id
return super().to_internal_value(data)
def to_representation(self, instance):
data = super().to_representation(instance)
data["address"] = AddressSerializer(instance.address).data
return data
Above solution works, but mypy throws error that I am changing data type of address in WriteSerializer, while BaseSerializer has different type.
Is there another way to handle this non-primary field value in WriteSerializer or somewhere else?

This scenario is supported when using the SlugRelatedField. Just specify your name field as the "slug" and ensure it is unique.
class UserWriteSerializer(serializers.ModelSerializer):
address = serializers.SlugRelatedField(
slug_field="name",
queryset=Address.objects.all()
)
class Meta:
model = User
fields = ["id", "address", "email", "..."]
It is a good idea to split serializers, as you have done, for create/update vs. list. Be careful with inheriting, though, since any fields you add in the list view will automatically be editable in the create. Its safer to just make them entirely separate.

Related

Converting fields inside a Serializer in django

I have a project where one model, called Request, has two fields (source, dest) that contain two ids which are not known to the user. However, each one is connected to another model User, who let's say that they have one field, username, which is known to the user.
Now, I want to make a serializer that can take usernames, and convert them into ids. (The opposite was simple to achieve, I just modified the to_representation method.) The problem is that when I send {'source': 'john', 'dest': 'jim'} the serializer does not take these data as valid. This was expected behavior, as it expected ids and got strings (usernames). However, even when I overridden the validate_source, validate_dest and validate methods to actually check that the usernames exist (instead of the ids), I am still getting errors that the serializer expected id but got string.
Are the validate, validate_<field> methods the wrong ones to override in this case?
Should I just convert the usernames into ids inside my view?
is it pythonic and good practice, django-wise, to receive some fields from the user and change them inside the serializer (as I change username into id)?
Current Serializer:
class RequestSerializer(serializers.ModelSerializer):
class Meta:
model = Request
fields = '__all__'
def validate_source(self, value):
username = value.get('username')
if username is None:
raise serializers.ValidationError('`user` field is required ')
return value
def validate_dest(self, value):
username = value.get('username')
if username is None:
raise serializers.ValidationError('`user` field is required ')
return value
def validate(self, attrs):
self.validate_source(attrs['source'])
self.validate_dest(attrs['dest'])
return attrs
def to_representation(self, instance):
# do things
pass
Please notice that this is not the whole functionality of my serializer. To convert from an id to a username I have to check the data of another Model, So I cannot use a SlugRelatedField.
Also, username is not the only item returned by the serializer. It also returns a 'class' field, depending on which group the the user has joined. The user may join more than one group, and each user-group combination has its own id. In the same way, when deserializing the data, I will need to read (1) the username, and then (2) the group, and find the correct id.
Thank you.
You probably can work with a SlugRelatedField [drf-doc]:
from rest_framework import serializers
class MyModelSerializer(serializers.ModelSerializer):
source = serializers.SlugRelatedField(
queryset=User.objects.all(),
slug_field='username',
)
dest = serializers.SlugRelatedField(
queryset=User.objects.all(),
slug_field='username',
)
class Meta:
model = MyModel
fields = ('source', 'dest')
This will return the username of the source and dest field of the model object, and in the opposite direction will fetch the User with the corresponding username.

How to auto populate a read-only serializer field in django rest framework?

I have a question regarding django rest framework.
Most of the time, I have a serializer which has some read-only fields. For example, consider this simple model below:
class PersonalMessage(models.Model):
sender = models.ForeignKey(User, related_name="sent_messages", ...)
recipient = models.ForeignKey(User, related_name="recieved_messages", ...)
text = models.CharField(...)
def __str__(self) -> str:
return f"{self.text} (sender={self.sender})"
In this model, the value of sender and recipient should be automatically provided by the application itself and the user shouldn't be able to edit those fields. Alright, now take a look at this serializer:
class PersonalMessageSerializer(serializers.ModelSerializer):
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')
It perfectly prevents users from setting an arbitrary value on the sender and recipient fields. But the problem is, when these fields are marked as read-only in the serializer, the serializer will completely ignore all the values that are passed into the constructor for these fields. So when I try to create a model, no values would be set for these fields:
PersonalMessageSerializer(data={**request.data, 'sender': ..., 'recipient': ...) # Won't work
What's the best way to prevent users from setting an arbitrary value and at the same time auto-populate those restricted fields in django rest framework?
Depending on how you get those two objects, you can use the serializer's save method to pass them, and they will automatically be applied to the object you are saving:
sender = User.objects.first()
recipient = User.objects.last()
serializer = PersonalMessageSerializer(data=request.data)
message = serializer.save(sender=sender, recipient=recipient)
The kwargs should match the field names in your model for this to work. For reference, have a look here
You able to override the serializer context like this;
PersonalMessageSerializer(data={**request.data, context={'sender': sender, 'recipent': recipent})
and catch the context inside serializer.
class PersonalMessageSerializer(serializers.ModelSerializer):
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')
def validate(self, attrs):
attrs = super().validate(attrs)
attrs['sender'] = self.context['sender']
attrs['recipent'] = self.context['recipent']
return attrs
now serializer.validated_data it must returns sender and recipent.
From the question it is not possible to understand what field(s) of the relationship with sender and recipient you want to interact with, but a general answer can be found in the Serializer relations section of Django REST documentation.
Long story short, if you want to interact with one field only, you can use SlugRelatedField, which lets you interact with the target of the relationship using only one of its fields.
If it just the id, you can use PrimaryKeyRelatedField.
If you want to interact with more than one field, the way to go is Nested Relationships. Here you can specify a custom serializer for the target relationship, but you will have to override the create() method in your PersonalMessageSerializer to create the object from your relationship, as nested serializers are read-only by default.
So this is how you can make set a default on create but read only after in DRF. Although in this solution it wont actually be readonly, it's writable, but you now have explicit control on what the logged in user can write, which is the ultimate goal
Given the model
class PersonalMessage(models.Model):
sender = models.ForeignKey(User,...)
recipient = models.ForeignKey(User,..)
text = models.CharField(...)
You would first create your own custom default (I will show an example for only one field)
# Note DRF already has a CurrentUserDefault you can also use
class CurrentSenderDefault:
requires_context = True
def __call__(self, serializer_field):
return serializer_field.context['request'].user
def __repr__(self):
return '%s()' % self.__class__.__name__
Next you make your own field, that knows whats up with the filter.
This queryset prevents people from setting a value they are not allowed to. which is exactly what you want
class SenderField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
user = self.context['request'].user
if user:
queryset = User.objects.filter(id=user.id)
else:
queryset = User.objects.none()
return queryset
Finally on the serialiser you go
class PersonalMessageSerializer(serializers.ModelSerializer):
sender = SenderField(default=CurrentSenderDefault())
recipient = ...
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')

Avoid validation of foreign key in django rest framework serializer

I write an API for the following models:
class TemplateProjectGroup(models.Model):
pass
class TemplateProject(models.Model):
name = models.CharField(max_length=255, unique=True)
description = models.CharField(max_length=1024, blank=True)
group = models.ForeignKey(TemplateProjectGroup, on_delete=models.CASCADE)
project = models.ForeignKey(Project, on_delete=models.CASCADE)
avatar_url = models.URLField(max_length=1024, blank=True)
The logic is following: User can create an instance of TemplateProject with non existed group field. So if group is not existed, it should be created with a specific ID. So, I have this serializer:
class TemplateProjectSerializer(serializers.ModelSerializer):
def create(self, validated_data):
template_project_group_id = validated_data.pop('group')
project = validated_data.pop('project')
group, _ = models.TemplateProjectGroup.objects.get_or_create(id=template_project_group_id)
template_project = models.TemplateProject.objects.create(**validated_data, group_id=group.id, project_id=project.id)
return template_project
def update(self, instance, validated_data):
template_project_group_id = validated_data.pop('group')
group, _ = models.TemplateProjectGroup.objects.get_or_create(id=template_project_group_id)
instance.save()
instance.update(**validated_data, group=group)
return instance
class Meta:
model = models.TemplateProject
fields = ('name', 'description', 'group', 'project', 'avatar_url')
and the view:
class TemplateProjectsView(generics.ListCreateAPIView):
pagination_class = None
serializer_class = serializers.TemplateProjectSerializer
def get_queryset(self):
return models.TemplateProject.objects.all()
It works well, when I try to retrieve list of objects, but I cannot create an object using this API, because I get following error:
Invalid pk "1" - object does not exist.
So, before creating an object, a validation is applied for all fields, and serializer cannot serialize this integer into an object because this object, which is referenced by foreign key, does not exist. I wrote a method validate_group(self, value), but exception raises before the execution point arrives this method. The more close point I could put a break in a debugger is method is_valid(self, raise_exception=False). I could create missing objects there, but I think, that would be a bad practice because this method actually doesn't has an aim for validating or preparing data.
How to properly create an object before it passes all validations?
One possible options is, define group explicitly as an integer field. This way, group field will not be tried to be validated as a TemplateProjectGroup instance.
class TemplateProjectSerializer(serializers.ModelSerializer):
group = serializers.IntegerField(source='group.id')
...
With this setup, you can get group id like this in create or update method of the serializer:
template_project_group_id = validated_data.pop('group').get('id')
Another option is, you could get or create a group instance in the view, by getting group id from the request, and then always pass an existing group id to the serializer, and expect an existing group id in the serializer. This would mean moving some of the validation logic to the view (you'd need to check at least if an integer is supplied for group field), but you wouldn't need to tweak your serializer.

Django auto save m2m relationship in form using through table

I have a m2m relationship between Servers and Products in Django with a through table called ServerProducts.
class ServerProduct(TimeStampedModel):
# Additional fields may be required in the future
server = models.ForeignKey('Server', on_delete=models.CASCADE)
product = models.ForeignKey('Product', on_delete=models.CASCADE)
class Server(TimeStampedModel):
name = models.CharField(max_length=35)
# ...
products = models.ManyToManyField('Product', through='ServerProduct',
related_name='products', blank=True)
class Product(TimeStampedModel):
name = models.CharField(max_length=45, unique=True)
# ...
servers = models.ManyToManyField(
'Server', through='ServerProduct', related_name='servers')
In my view I have a form which allows users to create a Server and select from a list of all products for the Server to be associted with.
In order to create the ServerProduct objects (for the through table) on each save I have to write the following code inside save().
class ServerForm(forms.ModelForm):
class Meta:
model = Server
fields = '__all__'
def save(self, commit=True):
instance = super(ServerForm, self).save(commit=False)
instance.save()
if instance.products.count():
instance.products.clear()
for product in self.cleaned_data['products']:
ServerProduct.objects.create(server=instance, product=product)
return instance
I want to be able to reuse the form for both Create and Update views. Hence why I have to check if the current Server is associated with any products, and then do instance.products.clear(). To make sure it removes any previous products if they get deselected by a user.
This entire process feels unecessary, especially when I've read a lot about Django's built-in form.save_m2m() method. My question is, is there a simpler way do achieve what I'm doing using Django built-in's?

Can't disable ForeignKey referential integrity check in Django 1.9

I have a model with two entities, Person and Code. Person is referenced by Code twice, a Person can be either the user of the code or the approver.
What I want to achieve is the following:
if the user provides an existing Person.cusman, no further action is needed.
if the user provides an unknown Person.cusman, a helper code looks up other attributes of the Person (from an external database), and creates a new Person entity.
I have implemented a function triggered by pre_save signal, which creates the missing Person on the fly. It works fine as long as I use python manage.py shell to create a Code with nonexistent Person.
However, when I try to add a new Code using the admin form or a CreateView descendant I always get the following validation error on the HTML form:
Select a valid choice. That choice is not one of the available choices.
Obviously there's a validation happening between clicking on the Save button and the Code.save() method, but I can't figure out which is it. Can you help me which method should I override to accept invalid foreign keys until pre_save creates the referenced entity?
models.py
class Person(models.Model):
cusman = models.CharField(
max_length=10,
primary_key=True)
name = models.CharField(max_length=30)
email = models.EmailField()
def __unicode__(self):
return u'{0} ({1})'.format(self.name, self.cusman)
class Code(models.Model):
user = models.ForeignKey(
Person,
on_delete=models.PROTECT,
db_constraint=False)
approver = models.ForeignKey(
Person,
on_delete=models.PROTECT,
related_name='approves',
db_constraint=False)
signals.py
#receiver(pre_save, sender=Code)
def create_referenced_person(sender, instance, **kwargs):
def create_person_if_doesnt_exist(cusman):
try:
Person = Person.objects.get(pk=cusman)
except Person.DoesNotExist:
Person = Person()
cr = CusmanResolver()
Person_details = cr.get_person_details(cusman)
Person.cusman = Person_details['cusman']
Person.name = Person_details['name']
Person.email = Person_details['email']
Person.save()
create_Person_if_doesnt_exist(instance.user_id)
create_Person_if_doesnt_exist(instance.approver_id)
views.py
class CodeAddForm(ModelForm):
class Meta:
model = Code
fields = [
'user',
'approver',
]
widgets = {
'user': TextInput,
'approver': TextInput
}
class CodeAddView(generic.CreateView):
template_name = 'teladm/code_add.html'
form_class = CodeAddForm
You misunderstood one thing: You shouldn't use TextField to populate ForeignKey, because django foreign keys are populated using dropdown/radio button to refer to the id of the object in another model. The error you got means you provided wrong information that doesn't match any id in another model(Person in your case).
What you can do is: not using ModelForm but Form. You might have some extra work to do after you call form.is_valid(), but at least you could code up your logic however you want.

Categories

Resources