Django rest framework - set default serializer for a class - python

In my code, I have a few models with multiple custom properties:
#dataclass
class Value:
amount: float
currency: str
class MyModel(models.Model):
...
#property
def v1(self) -> Value:
...
#property
def v2(self) -> Value:
...
#property
def v3(self) -> Value:
...
An I have the following serializer:
class MyModelBaseSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = [..., "v1", "v2", "v3"]
When serializing any MyModel instance, a TypeError raises: TypeError: Object of type Value is not JSON serializable.
I know this can be solved by explicitly adding v1, v2, v3 as fields inside MyModelBaseSerializer, but I don't want to do that: I have many models with Value properties. I want the default DRF serializer to know how to serialize Value instances.
I tried overriding to_representation, but that didn't seem to work.
I want something similar to overriding the JSONEncoder.default(o) method, but I don't see how to tell DRF which encoder to use.

Add a custom serializer for Value. For example (not tested).
class ValueSerializer(serializers.Serializer):
amount = serializers.FloatField()
currency = serializers.CharField()
class MyModelBaseSerializer(serializers.ModelSerializer):
v1 = ValueSerializer()
v2 = ValueSerializer()
v3 = ValueSerializer()
...
Or, since Value is a dataclass, take a look at https://github.com/oxan/djangorestframework-dataclasses.
EDIT
A quick and dirty DRY approach can be to override build_property_field on the serializer (see docs).
class MyModelBaseSerializer(serializers.ModelSerializer):
def build_property_field(self, field_name, model_class):
if field_name in ["v1", "v2", "v3"]:
return ValueSerializer, {}
return super().build_property_field(field_name, model_class)
Overriding serializer_field_mapping is another option.

Related

How to change serializer field name with python forbidden character

I need to have a serializer that returns data with illegal character inside dictionary key, what I would like is :
class MyModel(models.Model):
my_field = model.TextField()
...
serializer = MySerializer(django_object)
serializer.data
# returns {'my-field' : "value"}
I have tried using serializers.SerializerMethodField but target field name must be Python valid.
class MySerializer(serializers.Serializer):
my-field = serializers.SerializerMethodField(method_name="get_my_field")
# ^ this fail to be interpreted by Python
def get_my_field(self, obj):
return obj.my_field
using source as an argument to any Serializer fails for the same reason.
You can simple override to_representation method. e.g.
class MySerializer(serializers.Serializer):
my_field = serializers.CharField(source="MY-FIELD") # only if need source
def to_representation(self, instance):
data = super().to_representation(instance)
data["MY-FIELD"] = data.pop("my_field", "")
return data

Django models, #property decorator for method

Let's say I have model like this:
class Article(models.Model):
...
def get_route(self):
...
return data
When I want to display value returned by get_route() in admin panel I have to create custom method like this:
def list_display_get_route(self, obj):
return obj.get_route()
list_display = ('list_display_get_route', ...)
I've recently discovered that if I decorate get_route() with #property there's no need to use list_display_get_route() and the following will work:
models.py
#property
def get_route(self):
...
return data
admin.py
list_display = ('get_route', ...)
Is this correct approach? Are there any drawbacks of this solution?
Using get_route in list_display should work even if you don't make it a property.
class Article(models.Model):
def get_route(self):
...
return data
class ArticleAdmin(admin.ModelAdmin):
list_display = ('get_route', ...)
See the list_display docs for the list of value types supported. The fourth example, "a string representing an attribute on the model", is equivalent to your get_route method.
Using the property decorator is fine if you prefer, but in this case it would make more sense to name it route.
#property
def route(self):
...
return data
class ArticleAdmin(admin.ModelAdmin):
list_display = ('route', ...)

Error trying to display the nested representation of a related GenericForeignKey

I want to display the nested representation of a related GenericForeignKey in Django Rest Framework. This is what I have tried:
class ContentRelatedField(serializers.RelatedField):
def to_representation(self, value):
if isinstance(value, Membership):
return MemberSerializer
return None
class ListSerializer(serializers.ModelSerializer):
content_object = ContentRelatedField(read_only=True)
class Meta:
model = User
fields = ('id', 'description', 'content_object')
Which gives the following error:
<class 'app.serializers.MemberSerializer'> is not JSON serializable
You should not only return the class MemberSerializer class but a serialized instance of it. Change this line to:
return MemberSerializer(value).data

Django REST Framework validate slug field

I have defined a serializer like this:
class ActivitySerializer(serializers.ModelSerializer):
activity_project = serializers.SlugRelatedField(queryset=Project.objects.all(), slug_field='project_name')
activity_tags = serializers.SlugRelatedField(queryset=Tag.objects.all(), slug_field='tag_name', many=True)
class Meta:
model = Activity
fields = ('activity_name', 'activity_description', 'activity_status', 'activity_completion_percent', 'activity_due_date', 'activity_project', 'activity_tags',)
Now if I insert an activity_tag that does not exist in the database, I get a validation error"
{
"activity_tags": [
"Object with tag_name=test does not exist."
]
}
I would like to create a validation method that adds the tag in the database if it does not exist.
I have tried using the
def validate(self, attrs):
....
method, but apparently for a slug field there is a method that is called before this one.
Can someone point me to the right method I should use? Would this method be called in the corresponding view?
I think you would need to create a nested serializer for this to work. This is totally untested and off the top of my head, but maybe something like this:
class ActivityTagFieldSerializer(serializer.ModelSerializer):
tag_name = serializers.SlugField()
class Meta:
model = Tag
fields = ('tag_name')
class ActivitySerializer(serializer.ModelSerializer):
activity_tags = ActivityTagFieldSerializer(many=True)
class Meta:
model = Activity
fields = ('activity_tags', 'activity_project', ...)
def create(self, validated_data):
tags = validated_data.pop('activity_tags')
activity = Activity.objects.create(**validated_data)
for tag in tags:
try:
tag_to_add = Tag.objects.get(**tag)
except:
tag_to_add = Tag.objects.create(**tag)
activity.activity_tags.add(tag_to_add)
return activity
Check the API guide for writable nested serializers
I managed to do this by subclassing SlugRelatedField and overriding "to_internal_value" method. In the original implementation this method tries to get an object from the queryset, and if an object doesn't exist it fails the validation. So instead of calling "get" method, I'm calling "get_or_create":
class CustomSlugRelatedField(serializers.SlugRelatedField):
def to_internal_value(self, data):
try:
obj, created = self.get_queryset().get_or_create(**{self.slug_field: data})
return obj
except (TypeError, ValueError):
self.fail('invalid')

Django #property calculating a model field: FieldError: Cannot resolve keyword

I'm following the method used by #Yauhen Yakimovich in this question:
do properties work on django model fields?
To have a model field that is a calculation of a different model.
The Problem:
FieldError: Cannot resolve keyword 'rating' into field. Choices are: _rating
The rating model field inst correctly hidden and overridden by my rating property causing an error when I try to access it.
My model:
class Restaurant(models.Model):
...
...
#property
def rating(self):
from django.db.models import Avg
return Review.objects.filter(restaurant=self.id).aggregate(Avg('rating'))['rating__avg']
Model in Yauhen's answer:
class MyModel(models.Model):
__foo = models.CharField(max_length = 20, db_column='foo')
bar = models.CharField(max_length = 20)
#property
def foo(self):
if self.bar:
return self.bar
else:
return self.__foo
#foo.setter
def foo(self, value):
self.__foo = value
Any ideas on how to correctly hid the rating field and define the #property technique?
Solved by using sorted()
I was using a query with order_by() to call rating. order_by() is at the database level and doesnt know about my property. Soultion, use Python to sort instead:
sorted(Restaurant.objects.filter(category=category[0]), key=lambda x: x.rating, reverse=True)[:5]
If you encounter a similar error check through your views for anything that might be calling the property. Properties will no longer work at the datatbase level.
Change this line:
self._rating = Review.objects.filter(restaurant=self.id).aggregate(Avg('rating'))['rating__avg']
into this one:
self._rating = Review.objects.filter(restaurant=self.id).aggregate(Avg('_rating'))['_rating__avg']
(notice change of reference in query from rating and rating__avg to _rating and _rating__avg)

Categories