Converting fields inside a Serializer in django - python

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.

Related

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.

DRF - Raise Exception if any defined field is None

I need to serialize model to JSON. Then send this JSON to one API.
But this API requires some fields to be not None.
I have a list of these fields. In this case, let's say it's just ['telephone'] but it can be much more.
For example:
class UserSerializer(serializers.ModelSerializer):
telephone = serializers.CharField(source='userprofile.telephone')
class Meta:
model = User
fields = ['first_name','last_name','telephone']
Serialization:
>>> UserSerializer(user).data
>>> {'first_name':'Michael','last_name':'Jackson','telephone':None}
Since API requires some fields like telephone, I want UserSerializer to raise ValidationError when the required field is None.
So in this case I couldn't serialize user because telephone is None.
I tried many things including adding required=True to the telephone but nothing works.
Is there a way to validate serialized data? Note that I'm not talking about deserialization.
Why validation not working?
The validation process undergoes only while Deserialization process (input is a dict like object) and you are trying a Serialization process. In the case of Serialization, DRF assumes the given object is a valid one and hence it doesn't require a validation.
Source DRF-serializers
How can we make this happen?
Method-1
Make your user object to a user_data (dict object) and pass it to the serializer and run the validation.
user = User.objects.get(id=1)
dict_user_data = {"first_name": user.first_name, "last_name": user.last_name, "telephone": user.userprofile.telephone}
user_serializer = UserSerializer(data=dict_user_data)
user_serializer.is_valid(True)
user_serializer.data
Method-2
Override the to_representation() method
class UserSerializer(serializers.ModelSerializer):
telephone = serializers.CharField(source='userprofile.telephone')
class Meta:
model = User
fields = ['first_name', 'last_name', 'telephone']
def to_representation(self, instance):
data = super().to_representation(instance)
for field, value in data.items():
if value is None:
raise SomeExceptionHere({field: "can't be None"})
return data
You don't need to. DRF serializers can do that right out the box. If a field is setted to be null=False or required=True, just do this.
data = UserSerializer(data=user_data)
data.is_valid(raise_exception=True
And that is it. A 400 error will be raised.
If you want, you can tweak the error message:
data = UserSerializer(data=user_data)
try:
data.is_valid(raise_exception=True)
except ValidationError as e:
return HttpResponse(status_code=400, content=e)
What you want is not to validate data for de-serialization, but to validate for serialization. Expressions like required=True are all used to validate data for de-serialization. Serialization is handled in to_representation method of Serializer class (which is the base for ModelSerializer)
What you can do is, override to_representation, and have the method raise an exception if a required field value is None.
You may need to further subclass default DRF classes like Field (for a serializer field) and use your custom classes to be able to provide your functionality in a systematic manner. You do not want to rely on required=True for this, because it is used for another purpose already (for de-serialization validation).
What I suggest is, subclass Field class, add a property like "required_for_read", and define your serializer field with this property, using your custom field class. Then, in your overridden to_representation method, look for this property, and raise an exception if the field has this property as True but its value is None

Django Tastypie: permissions for ManyToManyFields

It's possible give delete permissions only for a m2m field of my model?
Let's think in:
class Site(models.Model):
name = models.CharField(max_length=50)
favourited_by = models.ManyToManyField(User)
If i write this ModelResource:
class SiteResource(ModelResource):
class Meta:
queryset = Site.objects.all()
resource_name = 'Site'
allowed_methods = ['post', 'get', 'delete']
I'm giving delete permissions for the whole model, but i only want to be able to delete entries from "favourited_by" field. There's some way to achieve this?
Doing a delete on that endpoint, by definition, should delete the whole object.
But if you are looking for a custom behaviour, then you could override obj_delete with something like:
def obj_delete(self, bundle, **kwargs):
# Get the object and raise NotFound if couldn't find
try:
bundle.obj = self.obj_get(bundle=bundle, **kwargs)
except ObjectDoesNotExist:
raise NotFound("A model instance matching the provided arguments could not be found.")
#Here you should do any kind of validation before deleting
# And then remove using the parameter you passed (for example user_id)
bundle.obj.favourited_by.remove(User.objects.get(id=bundle.data['user_id'])) #pass in the user_id
return

Django REST Framework - Serializing optional fields

I have an object that has optional fields. I have defined my serializer this way:
class ProductSerializer(serializers.Serializer):
code = serializers.Field(source="Code")
classification = serializers.CharField(source="Classification", required=False)
I thought required=False would do the job of bypassing the field if it doesn't exist. However, it is mentioned in the documentation that this affects deserialization rather than serialization.
I'm getting the following error:
'Product' object has no attribute 'Classification'
Which is happening when I try to access .data of the serialized instance. (Doesn't this mean it's deserialization that's raising this?)
This happens for instances that do not have Classification. If I omit Classification from the serializer class it works just fine.
How do I correctly do this? Serialize an object with optional fields, that is.
Django REST Framework 3.0+
Dynamic fields now supported, see http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields -- this approach defines all of the fields in the serializer, and then allows you to selectively remove the ones you don't want.
Or you could also do something like this for a Model Serializer, where you mess around with Meta.fields in the serializer init:
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ('code',)
def __init__(self, *args, **kwargs):
if SHOW_CLASSIFICATION: # add logic here for optional viewing
self.Meta.fields = list(self.Meta.fields)
self.Meta.fields.append('classification')
super(ProductSerializer, self).__init__(*args, **kwargs)
You'd have to ask Tom though if this is the "correct way" since it may not fit in with the long term plan.
Django REST Framework < 3.0
Try something like this:
class ProductSerializer(serializers.Serializer):
...
classification = serializers.SerializerMethodField('get_classification')
def get_classification(self, obj):
return getattr(obj, 'classification', None)
Multiple Serializers
Another approach would be to create multiple serializers with different sets of fields. One serializer inherits from another and adds additional fields. Then you can choose the appropriate serializer in the view with the get_serializer_class method. Here's an actual example of how I use this approach to call different serializers to present different user data if the user object is the same as the request user.
def get_serializer_class(self):
""" An authenticated user looking at their own user object gets more data """
if self.get_object() == self.request.user:
return SelfUserSerializer
return UserSerializer
Removing fields from representation
Another approach that I've used in security contexts is to remove fields in the to_representation method. Define a method like
def remove_fields_from_representation(self, representation, remove_fields):
""" Removes fields from representation of instance. Call from
.to_representation() to apply field-level security.
* remove_fields: a list of fields to remove
"""
for remove_field in remove_fields:
try:
representation.pop(remove_field)
except KeyError:
# Ignore missing key -- a child serializer could inherit a "to_representation" method
# from its parent serializer that applies security to a field not present on
# the child serializer.
pass
and then in your serializer, call that method like
def to_representation(self, instance):
""" Apply field level security by removing fields for unauthorized users"""
representation = super(ProductSerializer, self).to_representation(instance)
if not permission_granted: # REPLACE WITH PERMISSION LOGIC
remove_fields = ('classification', )
self.remove_fields_from_representation(representation, remove_fields)
return representation
This approach is straightforward and flexible, but it comes at the cost of serializing fields that are sometimes not displayed. But that's probably okay.
The method describe below did the work for me.
Pretty simple,easy and worked for me.
DRF version used = djangorestframework (3.1.0)
class test(serializers.Serializer):
id= serializers.IntegerField()
name=serializers.CharField(required=False,default='some_default_value')
The serializers are deliberately designed to use a fixed set of fields so you wouldn't easily be able to optionally drop out one of the keys.
You could use a SerializerMethodField to either return the field value or None if the field doesn't exist, or you could not use serializers at all and simply write a view that returns the response directly.
Update for REST framework 3.0 serializer.fields can be modified on an instantiated serializer. When dynamic serializer classes are required I'd probably suggest altering the fields in a custom Serializer.__init__() method.
The serializers Charfield method has a property allow_blank
By default it is set to False.
Setting it to True will allow you to mark the field as optional during "serialization".
This is the code that you should write
classification = serializers.CharField(source="Classification", allow_blank=True)
Note: required property is used for deserialization.
DynamicSerializer for DRF 3, which allows dynamicly specifying which fields will be used in serializer, which will be excluded, and optionally which will become required!
Create Mixin
class DynamicSerializerMixin:
"""
A Serializer that takes an additional `fields` argument that
controls which fields should be used.
"""
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop("fields", None)
excluded_fields = kwargs.pop("excluded_fields", None)
required_fields = kwargs.pop("required_fields", None)
# Instantiate the superclass normally
super().__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
if isinstance(fields, dict):
for field, config in fields.items():
set_attrs(self.fields[field], config)
if excluded_fields is not None:
# Drop any fields that are not specified in the `fields` argument.
for field_name in excluded_fields:
self.fields.pop(field_name)
if required_fields is not None:
for field_name in required_fields:
self.fields[field_name].required = True
Initialize/adjust your serializer by adding DynamicSerializerMixin to inheritence
class UserProfileSerializer(DynamicSerializerMixin, serializers.ModelSerializer):
class Meta:
model = User
fields = (
"id",
'first_name', 'last_name'
"email",
"is_staff",
)
Use it :)
class RoleInvitationSerializer(serializers.ModelSerializer):
invited_by = UserProfileSerializer(fields=['id', 'first_name', 'last_name'])
or in action apis
#action(detail=True, serializer_class=YourSerialzierClass)
def teams_roles(self, request, pk=None):
user = self.get_object()
queryset = user.roles.all()
serializer = self.get_serializer(queryset, many=True, excluded_fields=['user'])
return Response(data=serializer.data)
For this purpose the serializers have the partial argument. If when the serializer is initialized you can pass partial=True. If you are using generics or mixins you can overrider the get_serializer function as follows:
def get_serializer(self, *args, **kwargs):
kwargs['partial'] = True
return super(YOUR_CLASS, self).get_serializer(*args, **kwargs)
And that will do the trick.
Note: This allows all fields to be optional and not only a specific one. If you want only specifics, you can override the method (i.e. update) and add validations of existence for various fields.
What has worked well for me is to set the serializer like so:
classification = serializers.CharField(max_length=20, allow_blank=True, default=None)
From the "it's a terrible hack relying on specific implementation details of both DRF and Django, but it works (at least for now)" files, here's the approach I used to include some additional debugging data in the response from a "create" method implementation on a serializer:
def create(self, validated_data)
# Actual model instance creation happens here...
self.fields["debug_info"] = serializers.DictField(read_only=True)
my_model.debug_info = extra_data
return my_model
This is a temporary approach that lets me use the browsable API to display some of the raw response data received from a particular remote service during the creation process. In the future, I'm inclined to keep this capability, but hide it behind a "report debugging info" flag in the creation request rather than returning the lower level info by default.

Categories

Resources