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')