Reputation: 439
I've been developing my own API for a Kanban-Style Project Board. I have attached a UML diagram to show how the "boards" application is organised.
My Application's Model UML Diagram
My problem is that when I want to create a new Card I want to be able to create the card with a list of Labels by Primary Keys passed in the POST parameters, like so:
{
"title": "Test Card",
"description": "This is a Test Card!",
"created_by": 1,
"labels": [1,2]
}
Another requirement I have is that I would like to retrieve the serialized labels as part of the card object, like so:
{
"id": 1,
"board": 1,
"title": "Some Card",
"description": "The description of Some Card.",
"created_by": 1,
"assignees": [
{
"id": 1,
"username": "test1",
"email": "[email protected]"
}
],
"labels": [
{
"id": 1,
"board": 1,
"title": "Pink Label",
"color": "#f442cb"
}
],
"comment_set": []
}
I am going to assume that to achieve this difference in POST and GET functionality I am going to have to have 2 different serializers?
However, the main question of this post has to do with the creation logic from the POST data as mentioned above. I keep getting errors like this:
{
"labels": [
{
"non_field_errors": [
"Invalid data. Expected a dictionary, but got int."
]
},
{
"non_field_errors": [
"Invalid data. Expected a dictionary, but got int."
]
}
]
}
I have tried many different combinations of DRF Serializers in my CardSerializer but always end up with error messages that have the same format as above: "Expected but got ". Any help or pointers, even someone telling me that this is bad REST design for example, would be greatly appreciated! :)
EDIT: I should add that in the case I change the CardSerializer labels field from a LabelSerializer to a PrimaryKeyRelatedField (as the comment in the code shows) I receive the following error:
Direct assignment to the forward side of a many-to-many set is prohibited. Use labels.set() instead.
Here are the relevant parts of my source code:
models.py
class Card(models.Model):
"""Represents a card."""
# Parent
board = models.ForeignKey(Board, on_delete=models.CASCADE)
column = models.ForeignKey(Column, on_delete=models.CASCADE, null=True)
# Fields
title = models.CharField(max_length=255, null=False)
description = models.TextField()
assignees = models.ManyToManyField(User, blank=True, related_name='card_assignees')
labels = models.ManyToManyField(Label, blank=True, related_name='card_labels')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(blank=True, null=True) # Blank for django-admin
created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='card_created_by')
views.py
class CardList(generics.ListCreateAPIView):
queryset = Card.objects.all()
serializer_class = CardSerializer
def get_queryset(self):
columns = Column.objects.filter(board_id=self.kwargs['board_pk'])
queryset = Card.objects.filter(column__in=columns)
return queryset
def post(self, request, *args, **kwargs):
board = Board.objects.get(pk=kwargs['board_pk'])
post_data = {
'title': request.data.get('title'),
'description': request.data.get('description'),
'created_by': request.data.get('created_by'),
'assignees': request.data.get('assignees'),
'labels': request.data.get('labels'),
}
serializer = CardSerializer(data=post_data, context={'board': board})
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status.HTTP_201_CREATED)
return Response(serializer.errors, status.HTTP_400_BAD_REQUEST)
serializers.py
class UserSerializer(serializers.ModelSerializer):
"""Serializer to map the User instance to JSON."""
class Meta:
model = User
fields = ('id', 'username', 'email')
class CommentSerializer(serializers.ModelSerializer):
"""Serializer to map the Comment instance to JSON."""
class Meta:
model = Comment
fields = '__all__'
class LabelSerializer(serializers.ModelSerializer):
"""Serializer to map the Label instance to JSON."""
class Meta:
model = Label
fields = ('id', 'board', 'title', 'color')
class CardSerializer(serializers.ModelSerializer):
"""Serializer to map the Card instance to JSON."""
assignees = UserSerializer(many=True, read_only=True)
labels = LabelSerializer(many=True)
comment_set = CommentSerializer(many=True, read_only=True)
# assignees = PrimaryKeyRelatedField(many=True, read_only=True)
# labels = PrimaryKeyRelatedField(many=True, queryset=Label.objects.all())
def create(self, validated_data):
board = self.context['board']
card = Card.objects.create(
board=board,
**validated_data
)
return card
class Meta:
model = Card
fields = ('id', 'board', 'title', 'description', 'created_by', 'assignees', 'labels', 'comment_set')
read_only_fields = ('id', 'board')
Upvotes: 5
Views: 3483
Reputation: 2337
If anyone stuck on the same problem, then here is the solution. I think you need to create the two serializer classes, one for the get
request and another for the post
request. And call the required serializer from viewset
as below,
class MyModelViewSet(viewsets.MyModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer # default serializer, you can change this to MyModelListSerializer as well
action_serializers = {
'list': MyModelListSerializer, # get request serializer
'create': MyModelCreateSerializer # post request serializer
}
def get_serializer_class(self):
if hasattr(self, 'action_serializers'):
return self.action_serializers.get(self.action, self.serializer_class)
return super(MyModelViewSet, self).get_serializer_class()
here is the example of the MyModelListSerializer
and MyModelCreateSerializer
,
# Used for the get request
class MyModelListSerializer(serializers.ModelSerializer):
assignees = AssigneesSerializer(read_only=True, many=True)
labels = LabelsSerializer(read_only=True, many=True)
class Meta:
model = MyModel
fields = '__all__'
# Used for the post request
class MyModelCreateSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = "__all__"
Upvotes: 2
Reputation: 439
I managed to find a solution, however this may not conform to best practice. If anyone could clarify this that would be great. However, for now:
I changed the create function in the CardSerializer to the following:
def create(self, validated_data):
board = self.context['board']
labels_data = validated_data.pop('labels')
card = Card.objects.create(
board=board,
**validated_data
)
card.labels.set(labels_data)
return card
The card.labels.set(labels_data) line means I bypass the following error message:
Direct assignment to the forward side of a many-to-many set is prohibited.
Which is why I am uncertain if it is the "correct" thing to do but it seems to work for now.
Upvotes: 3