Avoid validation of foreign key in django rest framework serializer - python

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.

Related

Catch-all field for unserialisable data of serializer

I have a route where meta-data can be POSTed. If known fields are POSTed, I would like to store them in a structured manner in my DB, only storing unknown fields or fields that fail validation in a JSONField.
Let's assume my model to be:
# models.py
from django.db import models
class MetaData(models.Model):
shipping_address_zip_code = models.CharField(max_length=5, blank=True, null=True)
...
unparseable_info = models.JSONField(blank=True, null=True)
I would like to use the built-in serialisation logic to validate whether a zip_code is valid (5 letters or less). If it is, I would proceed normally and store it in the shipping_address_zip_code field. If it fails validation however, I would like to store it as a key-value-pair in the unparseable_info field and still return a success message to the client calling the route.
I have many more fields and am looking for a generic solution, but only including one field here probably helps in illustrating my problem.
As you are looking for a generic solution, there are a few points that you should consider:
Make sure not to place any model-level validations in your model as you want it to get saved irrespective of the validation status.
Only validate on the serializer-level with custom validation methods.
Make unparseable_info field read-only as it is something we don't want the user to send but receive.
Make use of the errors dictionary provided by the serializer as it gets populated with field-specific errors when we call is_valid.
This is how it might translate into code, inside models.py:
class MetaData(models.Model):
shipping_address_zip_code = models.CharField(blank=True, null=True)
...
unparseable_info = models.JSONField(blank=True, null=True)
then inside serializers.py:
class MetaDataSerializer(serializers.ModelSerializer):
class Meta:
model = MetaData
read_only_fields = ('unparseable_info', )
fields = '__all__'
# Write validators for all of your fields.
finally inside your views.py method, something like this (you can do this inside serializer's save method as well):
meta_data = MetaDataSerializer(data=request.data)
if not meta_data.is_valid():
meta_data.unparseable_info = meta_data.errors
meta_data.save()
# Return meta_data.data in JSONResponse.
You can use Django serializer that store fields that fail validation in JSONField.
Here is an example that worked for me:
from rest_framework import serializers
class MetaDataSerializer(serializers.ModelSerializer):
class Meta:
model = MetaData
fields = 'all'
def validate_shipping_address_zip_code(self, value):
if len(value) > 5:
raise serializers.ValidationError("Zip code must be 5 characters or less.")
return value
def create(self, validated_data):
unparseable_info = {}
for field, value in self.initial_data.items():
try:
validated_data[field] = self.fields[field].run_validation(value)
except serializers.ValidationError as e:
unparseable_info[field] = value
instance = MetaData.objects.create(**validated_data)
if unparseable_info:
instance.unparseable_info = unparseable_info
instance.save()
return instance
def validate_shipping_address_zip_code(self, value):
if value >= 5:
return value
else:
raise serializers.ValidationError("Message Here")
there's much more validators in serializer look into more detail
https://www.django-rest-framework.org/api-guide/serializers/

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')

django rest framework - global default model serializer

In short, I want to have a global default serializer per model. My use case here is to create dynamic serializer- i.e creating ModelSerializer classes on the fly.
class Customer(models.Model):
name = models.CharField(max_length=200)
code = models.CharField(max_length=200)
# many more fields..
class CustomerTicket(models.Model):
customer = models.ForeignKey(Customer)
date = models.DateTimeField(auto_now_add=True)
# more fields..
Customer will be referenced by many other models, and hence it will be serialized as a nested object. I don't want the 'code' field to appear in the output - no matter what it should always be excluded.
Now I'd like to create a function:
def serialize_default(model, fields, queryset):
class S(serializers.ModelSerializer):
class Meta:
model = model
fields = fields
depth = 1
return S(queryset, many=True)
if I serialize CustomerTicket queryset using this function, I will get all the customer fields as a nested object. I know I can override it locally, but I want to define a CustomerSerializer that will be used by default (for the nested Customer here) unless other serializer is specified as a field. How to achieve this?
Would something like that work for you?
class DefaultCustomerSerializer(serializers.ModelSerializer):
# whatever fields you want
class DefaultCustomerSerializerModel(serializers.ModelSerializer):
customer = DefaultCustomerSerializer()
# You can inherit from this to have default customer serializer
# on serializers you want.
class CustomerTicketSerializer(DefaultCustomerSerializerModel):
# Other fields

In a Django serializer, how to set foreign key field based on view argument?

Using the Django REST Framework, I would like to allow users to create and save instances of a Django model through a ListCreateAPIView (via POST). One of the fields (a foreign-key field called domain) shall be determined from a view parameter as defined in urls.py.
Furthermore, the user can modify the model instance later using PUT or PATCH requests to a RetrieveUpdateDestroyAPIView endpoint (using the same serializer). I don't want the user to be able to modify the domain field at this point.
While I have the code for the model and the view / serializer structure ready, I'm not sure how to tell the serializer to determine the value of the domain field based on the view parameter. Here's what I got:
class RRset(models.Model):
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(null=True)
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, related_name='rrsets')
subname = models.CharField(max_length=255, blank=True)
type = models.CharField(max_length=10)
... and a straight-forward ListCreateAPIView:
class RRsetsDetail(generics.ListCreateAPIView):
serializer_class = RRsetSerializer
permission_classes = (permissions.IsAuthenticated,)
def get_queryset(self):
name = self.kwargs['name']
return RRset.objects.filter(domain__name=name, domain__owner=self.request.user.pk)
urls.py contains the following line:
url(r'^domains/(?P<name>[a-zA-Z\.\-_0-9]+)/rrsets/$', RRsetsDetail.as_view(), name='rrsets')
This allows the user to list and create RRset objects using the RRsetsSerializer serializer (the name field is listed for completeness only, but I do not believe it to be important in this context):
class RRsetSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField()
def get_name(self, obj):
return '.'.join(filter(None, [obj.subname, obj.domain.name])) + '.' # returns 'subname.name.'
class Meta:
model = RRset
fields = ('created', 'updated', 'domain', 'name', 'type',)
read_only_fields = ('created', 'updated', 'domain', 'type',)
Questions:
What do I need to modify to have the serializer take the domain name from the view name parameter?
The the serializer's read_only_fields setting prevents the user from modifying the domain field later. However, I'm not sure if this setting somehow interacts with the serializer trying to set a default value (can the serializer write the default value, even if read-only is set)?
To summarize: What I'm looking for is something like a "write-once field with a default value based on a view parameter".
I think you are looking for a HiddenField with a combination of CreateOnlyDefault
HiddenField
A field class that does not take a value based on user input, but instead takes its value from a default value or callable.
CreateOnlyDefault
A default class that can be used to only set a default argument during
create operations. During updates the field is omitted.
It takes a single argument, which is the default value or callable
that should be used during create operations.
And because you want to access the view, you can't just use callable, but you have to use Class-based callable which can have access to a context data.
class DomainDefault(object):
def set_context(self, serializer_field):
view = serializer_field.context['view']
request = serializer_field.context['request']
self.domain = ...#determine the domain based on request+view
def __call__(self):
return self.domain
class RRsetSerializer(serializers.ModelSerializer):
domain = serializers.HiddenField(default=serializers.CreateOnlyDefault(DomainDefault()))

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