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.
Related
I have Contact model to list the followers of an User object, I try to filter the contacts of a User but I still could not manage get a correct queryset. My Contact model is simple with two ForeignKey:
class Contact(models.Model):
user_from = models.ForeignKey(User,related_name='rel_from_set', on_delete=models.CASCADE,)
user_to = models.ForeignKey(User,related_name='rel_to_set', on_delete=models.CASCADE,)
def __str__(self):
return '{} follow {}'.format(self.user_from, self.user_to)
I have created serializers for User and Contact:
##Contact Serializer
class ContactsSerializer(serializers.ModelSerializer):
user_from = serializers.StringRelatedField(read_only=True)
user_to = serializers.StringRelatedField(read_only=True)
class Meta:
model = Contact
fields = ["user_from", "user_to"]
##UserSerializer
class UserInformationSerializer(serializers.ModelSerializer):
followers = ContactsSerializer(many=True, read_only=True)
class Meta:
model = User
fields = ['first_name', 'last_name', 'followers']
And try to make a query through views:
class FollowerListView(APIView):
queryset = Contact.objects.all()
serializer_class = ContactsSerializer
lookup_field = "username"
def get(self, request, format=None, slug=None):
kwarg_username = self.kwargs.get("slug")
user = User.objects.filter(is_active=1).filter(username=kwarg_username)
print(user.username)
contacts = Contact.objects.filter(user_to=user.id)
serializer = ContactsSerializer(contacts)
return Response(serializer.data)
Now I get error message:
AttributeError at /api/member/ytsejam/followers/
'QuerySet' object has no attribute 'username'
print(user.username)
If i try print(user) I can see the user an Object.
Can you guide me how to correct?
Thanks
filter will always return a queryset. If you expect to retrieve one single item, use get.
So that it looks like that:
def get(self, request, format=None, slug=None):
kwarg_username = self.kwargs.get("slug")
user = User.objects.filter(is_active=1).get(username=kwarg_username)
print(user.username)
contacts = Contact.objects.filter(user_to=user.id)
serializer = ContactsSerializer(contacts)
return Response(serializer.data)
You could, of course, do this on one take:
User.objects.get(is_active=1, username=kwarg_username)
But beware, if there are two rows in your model that would satisfy this call, Django will throw an error. Best make sure that the username has a unique constraint.
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
I'm implementing some voting functionality in an application, where a logged-in user specifies a post that they would like to vote for using a payload like this:
{
"post": 1,
"value": 1
}
As you can tell, the a user field is absent - this is because it gets set in my viewset's perform_create method. I've done this to ensure the vote's user gets set server side. This is what the viewset looks like:
class CreateVoteView(generics.CreateAPIView):
permission_classes = (permissions.IsAuthenticated,)
serializer_class = VoteSerializer
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Here is what the model looks like:
class Vote(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='votes', null=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='votes', null=False)
class Values(models.IntegerChoices):
UP = 1, _('Up')
DOWN = -1, _('Down')
value = models.IntegerField(choices=Values.choices, null=False)
class Meta:
unique_together = ('post', 'user')
and finally, the serializer:
class VoteSerializer(serializers.ModelSerializer):
class Meta:
model = Vote
fields = ['post', 'value']
From what I understand, in order for DRF to enforce a unique together validation, both fields (in my case, user and post) must be included in the serializer's fields. As I've mentioned, I'd like to avoid this. Is there any other way of implementing this type of validation logic?
EDIT:
To clarify: the records do not save - I receive this error:
django.db.utils.IntegrityError: (1062, "Duplicate entry '1-3' for key 'api_vote.api_vote_post_id_user_id_73614533_uniq'")
However, my goal is to return a Bad Request instead of an Internal Server Error much like I would when traditionally using a DRF serializer and excluding required fields from a payload.
To output a custom error message due to the IntegrityError, you can override the create method in your serializer:
from django.db import IntegrityError
class VoteSerializer(serializers.ModelSerializer):
class Meta:
model = Vote
fields = ['post', 'value']
def create(self, validated_data):
try:
validated_data['user'] = self.context['request'].user
return super().create(validated_data)
except IntegrityError:
error_msg = {'error': 'IntegrityError message'}
raise serializers.ValidationError(error_msg)
You can try this on your views
try:
MoviesWatchList.objects.create(user=request.user, content=movie)
return response.Response({'message': f'{movie} added in watchlist.'}, status=status.HTTP_201_CREATED)
except:
return response.Response({'message': f'{movie} already added to watchlist.'}, status=status.HTTP_304_NOT_MODIFIED)
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
So I have a view that accepts serialized Album object and assign the owner to current user
class AlbumListViewer(APIView):
def post(self, request, format = None):
request.data['user_id'] = request.user.id
serializer = AlbumSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(status.HTTP_201_CREATED)
return Response(status = status.HTTP_400_BAD_REQUEST)
The error I get is
null value in column "user_id" violates not-null constraint
My Serializer looks like this
class AlbumSerializer(serializers.ModelSerializer):
user = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Album
fields = ('id','album_name', 'date_created','user')
And finally my Model looks like this
class Album(models.Model):
user = models.ForeignKey(User)
album_name = models.CharField(max_length=30)
date_created = models.DateTimeField(editable=False, auto_now_add=True)
I have tried assigning a User ID in the JSON data but it is not being recognized by the serializer, anyway to assign this serialized object and give it a owner before saving?
Your have some problems in your AlbumListViewer. Do not try to add to the validatated_data anything. Instead pass on partial=True to the serializer, and after validation, use the save() method to save the missing values.
class AlbumListViewer(APIView):
def post(self, request, format = None):
serializer = AlbumSerializer(data=request.data, partial=True)
if serializer.is_valid():
user = request.user
serializer.save(user=user)
return Response(status=status.HTTP_201_CREATED)
return Response(status = status.HTTP_400_BAD_REQUEST)