Django Rest Framework: Writable nested serializers with Generic Foreign Key - django

There are examples how to create a writable nested serializer like this and then how to serialize a generic foreign key (here).
But I cannot find how to do both at the same time, i.e how to create a nested writable serializer for a generic foreign key field.
In my models there is a Meeting model with a GenericForeignKey which can be either DailyMeeting or WeeklyMeeting like:
class Meeting(models.Model):
# More fields above
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
recurring_meeting = GenericForeignKey('content_type', 'object_id')
class DailyMeeting(models.Model):
meeting = GenericRelation(Meeting)
# more fields
class WeeklyMeeting(models.Model):
meeting = GenericRelation(Meeting)
# more fields
Then I created a custom field in my serializers.py:
class RecurringMeetingRelatedField(serializers.RelatedField):
def to_representation(self, value):
if isinstance(value, DailyMeeting):
serializer = DailyMeetingSerializer(value)
elif isinstance(value, WeeklyMeeting):
serializer = WeeklyMeetingSerializer(value)
else:
raise Exception('Unexpected type of tagged object')
return serializer.data
class MeetingSerializer(serializers.ModelSerializer):
recurring_meeting = RecurringMeetingRelatedField()
class Meta:
model = Meeting
fields = '__all__'
I am passing a JSON which looks like:
{
"start_time": "2017-11-27T18:50:00",
"end_time": "2017-11-27T21:30:00",
"subject": "Test now",
"moderators": [41],
"recurring_meeting":{
"interval":"daily",
"repetitions": 10,
"weekdays_only": "True"
}
}
But the problem is that I am getting the following error:
AssertionError: Relational field must provide a queryset argument, override get_queryset, or set read_only=True.
Why does the Relational field has to be read_only? If I set it as read_only then it is not passed in the data in the serializer.
And what type of queryset do I have to provide?

You need to implement to_internal_value as well, and you can use just plain Field class.
from rest_framework.fields import Field
class RecurringMeetingRelatedField(Field):
def to_representation(self, value):
if isinstance(value, DailyMeeting):
serializer = DailyMeetingSerializer(value)
elif isinstance(value, WeeklyMeeting):
serializer = WeeklyMeetingSerializer(value)
else:
raise Exception('Unexpected type of tagged object')
return serializer.data
def to_internal_value(self, data):
# you need to pass some identity to figure out which serializer to use
# supose you'll add 'meeting_type' key to your json
meeting_type = data.pop('meeting_type')
if meeting_type == 'daily':
serializer = DailyMeetingSerializer(data)
elif meeting_type == 'weekly':
serializer = WeeklyMeetingSerializer(data)
else:
raise serializers.ValidationError('no meeting_type provided')
if serializer.is_valid():
obj = serializer.save()
else:
raise serializers.ValidationError(serializer.errors)
return obj
If validation went well then you'll get created object in the MeetingSerializer validated data in other case RecurringMeetingRelatedField will raise an exception.

In this case instead of using a RecurringMeetingRelatedField in the Meeting serializer, you could define a nested serializer like this.
class RecurringMeetingSerializer(serializers.Serializer):
interval = serializers.CharField()
repetitions = serializers.IntegerField()
weekdays_only = serializers.BooleanField()
class Meta:
fields = '__all__'
class MeetingSerializer(serializers.ModelSerializer):
recurring_meeting = RecurringMeetingSerializer()
class Meta:
model = Meeting
exclude = ['object_id', 'content_type']
def create(self, validated_data):
recurring_meeting = validated_data.pop('recurring_meeting')
if recurring_meeting['interval'] == 'daily':
instance = DailyMeeting.objects.create(**recurring_meeting)
type = ContentType.objects.get_for_model(instance)
else:
instance = WeeklyMeeting.objects.create(**recurring_meeting)
type = ContentType.objects.get_for_model(instance)
meeting = Meeting.objects.create(content_type=type,
object_id=instance.id)
return meeting

Related

How do I create a serializer that reuses a unique key of my model?

I'm using Python 3.7, Django 2.2, the Django rest framework, and pytest. I have the following model, in which I want to re-use an existing model if it exists by its unique key ...
class CoopTypeManager(models.Manager):
def get_by_natural_key(self, name):
return self.get_or_create(name=name)[0]
class CoopType(models.Model):
name = models.CharField(max_length=200, null=False, unique=True)
objects = CoopTypeManager()
Then I have created the below serializer to generate this model from REST data
class CoopTypeSerializer(serializers.ModelSerializer):
class Meta:
model = CoopType
fields = ['id', 'name']
def create(self, validated_data):
"""
Create and return a new `CoopType` instance, given the validated data.
"""
return CoopType.objects.get_or_create(**validated_data)
def update(self, instance, validated_data):
"""
Update and return an existing `CoopType` instance, given the validated data.
"""
instance.name = validated_data.get('name', instance.name)
instance.save()
return instance
However, when I run the below test in which I intentionally use a name that is taken
#pytest.mark.django_db
def test_coop_type_create_with_existing(self):
""" Test coop type serizlizer model if there is already a coop type by that name """
coop_type = CoopTypeFactory()
serializer_data = {
"name": coop_type.name,
}
serializer = CoopTypeSerializer(data=serializer_data)
serializer.is_valid()
print(serializer.errors)
assert serializer.is_valid(), serializer.errors
result = serializer.save()
assert result.name == name
I get the below error
python manage.py test --settings=directory.test_settings
... ----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/davea/Documents/workspace/chicommons/maps/web/tests/test_serializers.py", line 46, in test_coop_type_create_with_existing
assert serializer.is_valid(), serializer.errors
AssertionError: {'name': [ErrorDetail(string='coop type with this name already exists.', code='unique')]}
How do I construct my serializer so that I can create my model if its unique key doesn't exist, or re-use it if it does?
Edit: Here's the GitHub link ...
https://github.com/chicommons/maps/tree/master/web
DRF validates the uniqueness of each field if is declared with unique=True in the model, so you have to change the model as following if you want to keep your unique contraint for the name field:
class CoopType(models.Model):
name = models.CharField(max_length=200, null=False)
objects = CoopTypeManager()
class Meta:
# Creates a new unique constraint with the `name` field
constraints = [models.UniqueConstraint(fields=['name'], name='coop_type_unq')]
Also, you have to change your serializer, if you're using a ViewSet with the default behavior, you only need to add a custom validation in the serializer.
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from .models import CoopType
class CoopTypeSerializer(serializers.ModelSerializer):
default_error_messages = {'name_exists': 'The name already exists'}
class Meta:
model = CoopType
fields = ['id', 'name']
def validate(self, attrs):
validated_attrs = super().validate(attrs)
errors = {}
# check if the new `name` doesn't exist for other db record, this is only for updates
if (
self.instance # the instance to be updated
and 'name' in validated_attrs # if name is in the attributes
and self.instance.name != validated_attrs['name'] # if the name is updated
):
if (
CoopType.objects.filter(name=validated_attrs['name'])
.exclude(id=self.instance.id)
.exists()
):
errors['name'] = self.error_messages['name_exists']
if errors:
raise ValidationError(errors)
return validated_attrs
def create(self, validated_data):
# get_or_create returns a tuple with (instance, boolean). The boolean is True if a new instance was created and False otherwise
return CoopType.objects.get_or_create(**validated_data)[0]
The update method was removed because is not needed.
Finally, the tests:
class FactoryTest(TestCase):
def test_coop_type_create_with_existing(self):
""" Test coop type serializer model if there is already a coop type by that name """
coop_type = CoopTypeFactory()
serializer_data = {
"name": coop_type.name,
}
# Creation
serializer = CoopTypeSerializer(data=serializer_data)
serializer.is_valid()
self.assertTrue(serializer.is_valid(), serializer.errors)
result = serializer.save()
assert result.name == serializer_data['name']
# update with no changes
serializer = CoopTypeSerializer(coop_type, data=serializer_data)
serializer.is_valid()
serializer.save()
self.assertTrue(serializer.is_valid(), serializer.errors)
# update with the name changed
serializer = CoopTypeSerializer(coop_type, data={'name': 'testname'})
serializer.is_valid()
serializer.save()
self.assertTrue(serializer.is_valid(), serializer.errors)
coop_type.refresh_from_db()
self.assertEqual(coop_type.name, 'testname')
When you are using unique=True key in model, Serializer will automaticly add unique validator to that field.
It’s enough to cancel the uniqueness check by writting your own name field directly in serializer to prevent your curent error:
class Ser(serializers.ModelSerializer):
name = serializers.CharField() # no unique validation here
class Meta:
model = CoopType
fields = ['id', 'name']
def create(self, validated_data):
return CoopType.objects.get_or_create(**validated_data)
Be carefull: get_or_create in create method will return tuple, not instance.
Ok, now imagine you will call it with id field too so you really need an update method.
Then you can make the following hack in validate method (maybe it's dirty, but it will work):
class Ser(serializers.ModelSerializer):
# no `read_only` option (default for primary keys in `ModelSerializer`)
id = serializers.IntegerField(required=False)
# no unique validators in charfield
name = serializers.CharField()
class Meta:
model = CoopType
fields = ["id", "name"]
def validate(self, attrs):
attrs = super().validate(attrs)
if "id" in attrs:
try:
self.instance = CoopType.objects.get(name=attrs["name"])
except CoopType.DoesNotExist:
pass
# to prevent manual changing ids in database
del attrs["id"]
return attrs
def create(self, validated_data):
return CoopType.objects.get_or_create(**validated_data)
def update(self, instance, validated_data):
# you can delete that method, it will be called anyway from parent class
return super().update(instance, validated_data)
The save method on the serializer checks if the field self.instance is null or not. If there is an non-empty self.instance, it will call the update method; else - create method.
So if CoopType with name from your serializer_data dictionary exists - update method will be called. In other case you will see create method call.
My suggestion is to not use a ModelSerializer but instead use a vanilla serializer.
class CoopTypeSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(max_length=200, required=True, allow_blank=False)
def create(self, validated_data):
"""
Create and return a new `CoopType` instance, given the validated data.
"""
return CoopType.objects.get_or_create(**validated_data)[0]
def update(self, instance, validated_data):
"""
Update and return an existing `CoopType` instance, given the validated data.
"""
instance.name = validated_data.get('name', instance.name)
instance.save()
return instance

How to pass request to a Field Serializer in Rest Framework?

Hi I am using a Field Serializer to be able to serialize a PK field then deserialize it as object. Inside the serializer is a SerializerMethodField to build a custom url. It works when I use the itself from serializing its own record. However when I use it to a different serializer as a FieldSerializer, the request object is not passed.
class TelemetryFileSerializer(serializers.ModelSerializer):
telemetry_type = serializers.SlugRelatedField(
slug_field='name', queryset=TelemetryFileType.objects.all())
receiving_station = serializers.SlugRelatedField(
required=False, slug_field='name', queryset=ReceivingStation.objects.all())
link = serializers.SerializerMethodField()
class Meta:
model = TelemetryFile
fields = '__all__'
def get_link(self, object):
request = self.context.get('request')
print(self.context) # request is not passed here from RawImageSerializer/TelemetryFileField
return request.build_absolute_uri('/data_management/telemetry_files/{}'.format(object.id))
class TelemetryFileField(serializers.PrimaryKeyRelatedField):
def to_representation(self, value):
pk = super(TelemetryFileField, self).to_representation(value)
item = TelemetryFile.objects.get(pk=pk)
serializer = TelemetryFileSerializer(item)
return serializer.data
class RawImageSerializer(serializers.ModelSerializer):
from_telemetry_file = TelemetryFileField(queryset=TelemetryFile.objects.all())
link = serializers.SerializerMethodField()
I want to pass a request of itself to be able to create a url of it.
This is the returned when I use the RawImageSerializer:
AttributeError: 'NoneType' object has no attribute
'build_absolute_uri'
There must be a way to pass request from serializer to another...
I am not sure if this is the correct solution but adding this solved my problem...
class TelemetryFileField(serializers.PrimaryKeyRelatedField):
def to_representation(self, value):
print("value", self.context)
pk = super(TelemetryFileField, self).to_representation(value)
item = TelemetryFile.objects.get(pk=pk)
serializer = TelemetryFileSerializer(item, context=self.context) # context was added
return serializer.data

Django rest framework "Var must be a User Instance" vs "Object of type 'type' is not JSON serializable"

I'm trying to serialize a model having a foreign key "User".
The concerned view snippet is:
data = JSONParser().parse(request)
serializer = SiteSerializer(data=data)
if serializer.is_valid():
userid = data['supervisor']
user = User.objects.get(id=userid).__dict__ ## tried case I
user = User.objects.get(id=userid) ## tried case II
serializer.save(supervisor=user)
return JsonResponse(serializer.data, status=201)
The serializer is as :
class SiteSerializer(serializers.ModelSerializer):
supervisor = serializers.RelatedField(source='User', read_only=True)
class Meta:
model = Site
fields = ('sitename', 'start_date', 'supervisor')
The model is :
class Site(models.Model):
sitename=models.CharField(max_length=255)
start_date=models.DateTimeField
supervisor=models.ForeignKey(User,on_delete=models.PROTECT)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return "{}".format(self.sitename)
When I pass supervisor object it says that object of type "Type" is not serializable and when I pass supervisor as dictionary it says that the dict variable supervisor must be a User instance.
How do I sort this out and proceed??
DRF handles most of the data parsing itself and the parsed data can be found in request.data attribute.
# serializers.py
class SiteSerializer(serializers.ModelSerializer):
supervisor = serializers.RelatedField(read_only=True) # remove "source argument "
class Meta:
model = Site
fields = ('sitename', 'start_date', 'supervisor')
#views.py
from rest_framework.response import Response # use DRF's response class
class Foo(APIView):
...
def post(self, request, *args, **kwargs):
serializer = SiteSerializer(data=request.data) # use "request.data"
if serializer.is_valid():
serializer.save(supervisor=request.user) # pass user instance directly to the serializer/model saving flow using "request.user"
return Response(serializer.data)
else:
return Response(serializer.errors) # show validation errors if any

Django REST Framework: create related model from POSTed data

Using the following models:
class Ticket(models.Model):
[some irrelevant fields]
class TicketComment(models.Model):
text = models.TextField()
creator = models.CharField(max_length=255)
ticket = models.ForeignKey(Ticket, models.CASCADE, related_name='comments')
I created the following serializers:
class TicketSerializer(serializers.ModelSerializer):
[irrelevant]
class TicketCommentSerializer(serializers.ModelSerializer):
class Meta:
model = TicketComment
fields = '__all__'
def create(self, validated_data):
return TicketComment.objects.create(**validated_data)
A view:
class TicketCommentView(APIView):
lookup_url_kwarg = 'ticket_id'
def post(self, request, ticket_id):
data = request.data
data['creator'] = 'joe'
try:
data['ticket'] = Ticket.objects.get(pk=ticket_id)
except Ticket.DoesNotExist:
raise NotFound('Ticket {} does not exist.'.format(ticket_id))
serializer = TicketCommentSerializer(data=data)
serializer.is_valid(raise_exception=True)
comment = serializer.save()
return Response(comment, status=HTTP_201_CREATED)
And URL pattern:
urlpatterns = [
path('ticket/<int:ticket_id>/comment', TicketCommentView.as_view()),
]
However, when trying to POST the data {"text": "test"}, it fails with:
"ticket": ["Incorrect type. Expected pk value, received Ticket."]
If I change the view to pass the ticket_id integer instead of the ticket instance, it complains about duplicate keys:
django.db.utils.IntegrityError: duplicate key value violates unique constraint "ticketcomment_pkey"
DETAIL: Key (id)=(41993) already exists.
How can I create a resource and attach it to an existing related object?
Instead of passing ticket as serializer data, pass it to serializer's save method directly(related part of docs):
def post(self, request, ticket_id):
data = request.data
try:
ticket = Ticket.objects.get(pk=ticket_id)
except Ticket.DoesNotExist:
raise NotFound('Ticket {} does not exist.'.format(ticket_id))
serializer = TicketCommentSerializer(data=data)
serializer.is_valid(raise_exception=True)
comment = serializer.save(ticket=ticket, creator='joe')
return Response(comment, status=HTTP_201_CREATED)
Note in TicketCommentSerializer you should leave only text field:
class TicketCommentSerializer(serializers.ModelSerializer):
class Meta:
model = TicketComment
fields = ['text']
First you should not include id in serializer, cause mostly it will be a auto increment value. That is why you are getting an Integrity Error.

django-rest-framework serializer for ContentType object

I am building an activity model, somewhat similar to this package. It has an actor, verb and the target.
class Activity(models.Model):
actor_type = models.ForeignKey(ContentType, related_name='actor_type_activities')
actor_id = models.PositiveIntegerField()
actor = GenericForeignKey('actor_type', 'actor_id')
verb = models.CharField(max_length=10)
target_type = models.ForeignKey(ContentType, related_name='target_type_activities')
target_id = models.PositiveIntegerField()
target = GenericForeignKey('target_type', 'target_id')
pub_date = models.DateTimeField(default=timezone.now)
Now whenever a new object of whichever models (Tender, Job and News) is created, a new Activity object is created, with the target being the objects of any of these three models.
eg. user (actor) published (verb) title (target)
class Tender(models.Model):
title = models.CharField(max_length=256)
description = models.TextField()
class Job(models.Model):
title = models.CharField(max_length=256)
qualification = models.CharField(max_length=256)
class News(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
title = models.CharField(max_length=150)
To get this data I am making an API which will get me the required json data. I am using django-rest-framework for this and very new with it.
class ActorSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'email')
class ActivitySerializer(serializers.HyperlinkedModelSerializer):
actor = ActorSerializer()
class Meta:
model = Activity
fields = ('url', 'actor', 'verb', 'pub_date')
In the above serializers, I knew that actor will be the User. And so I used the User model for the ActorSerializer class. But as for the target, it can be any of these three models (News/Job/Tender).
How can I make a serializer (eg. TargetSerialier class) for the ContentType object so that I can use the target in the ActivitySerializer class field?
Okay so answering my own question here. I had some help with zymud's answer. So, apparently in the documentation, there is a way to serialize the Generic relation.
So, all I had to do was create a custom field and associate that field in the serializer itself:
class ActivityObjectRelatedField(serializers.RelatedField):
def to_representation(self, value):
if isinstance(value, User):
return 'User: ' + value.username
elif isinstance(value, News):
return 'News: ' + value.title
elif isinstance(value, Job):
return 'Job: ' + value.title
elif isinstance(value, Tender):
return 'Tender: ' + value.title
raise Exception('Unexpected type of tagged object')
class ActivitySerializer(serializers.HyperlinkedModelSerializer):
actor = ActivityObjectRelatedField(read_only=True)
target = ActivityObjectRelatedField(read_only=True)
class Meta:
model = Activity
fields = ('url', 'actor', 'verb', 'target', 'pub_date')
You can implement custom field for generic key. Example:
from django.core.urlresolvers import resolve
from rest_framework.fields import Field
class GenericRelatedField(Field):
"""
A custom field that expect object URL as input and transforms it
to django model instance.
"""
read_only = False
_default_view_name = '%(model_name)s-detail'
lookup_field = 'pk'
def __init__(self, related_models=(), **kwargs):
super(GenericRelatedField, self).__init__(**kwargs)
# related models - list of models that should be acceptable by
# field. Note that all this models should have corresponding
# endpoint.
self.related_models = related_models
def _get_url_basename(self, obj):
""" Get object URL basename """
format_kwargs = {
'app_label': obj._meta.app_label,
'model_name': obj._meta.object_name.lower()
}
return self._default_view_name % format_kwargs
def _get_request(self):
try:
return self.context['request']
except KeyError:
raise AttributeError('GenericRelatedField have to be initialized with `request` in context')
def to_representation(self, obj):
""" Serializes any object to its URL representation """
kwargs = {self.lookup_field: getattr(obj, self.lookup_field)}
request = self._get_request()
return request.build_absolute_uri(reverse(self._get_url_basename(obj), kwargs=kwargs))
def clear_url(self, url):
""" Removes domain and protocol from url """
if url.startswith('http'):
return '/' + url.split('/', 3)[-1]
return url
def get_model_from_resolve_match(self, match):
queryset = match.func.cls.queryset
if queryset is not None:
return queryset.model
else:
return match.func.cls.model
def instance_from_url(self, url):
url = self.clear_url(url)
match = resolve(url)
model = self.get_model_from_resolve_match(match)
return model.objects.get(**match.kwargs)
def to_internal_value(self, data):
""" Restores model instance from its URL """
if not data:
return None
request = self._get_request()
user = request.user
try:
obj = self.instance_from_url(data)
model = obj.__class__
except (Resolver404, AttributeError, MultipleObjectsReturned, ObjectDoesNotExist):
raise serializers.ValidationError("Can`t restore object from url: %s" % data)
if model not in self.related_models:
raise serializers.ValidationError('%s object does not support such relationship' % str(obj))
return obj
Example of usage:
class ActivitySerializer(serializers.HyperlinkedModelSerializer):
target = GenericRelatedField(related_models=(News, Job, Tender))
...
There is a third party lib as per documentation that did the heavy lifting already:
https://www.django-rest-framework.org/api-guide/relations/#rest-framework-generic-relations
It is pretty neat actually, my serializer class ended up few readable lines:
class ActivityTypeSerializer(serializers.ModelSerializer):
target = GenericRelatedField({
User: UserSerializer(),
Device: DeviceSerializer(),
})
class Meta:
model = Activity
fields = ('target', 'target_id', 'verb', 'target_ct',)