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": "test1_user#hotmail.co.uk"
}
],
"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')
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__"
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.
Related
I've a model:
class ListingPrice(Timestamps):
price = models.ForeignKey("Price", on_delete=models.CASCADE)
location = models.ForeignKey("location", on_delete=models.CASCADE)
class Meta:
unique_together = ["price", "location"]
class Price(Timestamps):
package = models.ForeignKey("products.Package", on_delete=models.CASCADE)
locations = models.ManyToManyField("location", through="ListingPrice")
price = models.DecimalField(max_digits=11, decimal_places=3)
with a serializer:
class LocationSerializer(serializers.ModelSerializer):
name = LocalizedField()
class Meta:
model = location
fields = ['id', 'name']
class PriceSerializer(serializers.ModelSerializer):
locations = LocationSerializer(many=True, read_only=True)
class Meta:
model = Price
fields = ['package', 'locations', 'price']
def create(self, validated_data):
print("validated_data, validated_data)
and viewset:
class PriceViewSet(ModelViewSet):
queryset = Price.objects.all()
serializer_class = PriceSerializer
ordering = ['id']
permissions = {
"GET": ["view_minimum_listing_price", ],
"POST": ["add_minimum_listing_price", ],
'PUT': ['update_minimum_listing_price', ],
'DELETE': ['delete_minimum_listing_price', ],
}
In testing I'mm using the following:
data = {
"price": 12,
"package": self.package.id,
"is_enabled": False,
"location": self.location
}
response = self.client.post(path=self.url, data=data, format='json')
locations doesn't appear in validated_data?
How to get it to assign locations to the instance with post requests?
I also tried to send it with as ids list, but non works. I only field price, package, is_enabled in validated}_data, but location doesn't appear!
read_only=True means the field will be neglected in request body and will only appear in response body
locations = LocationSerializer(many=True, read_only=True)
so, remove it and you can access locations in validated_data
I have included the serializer as well as the views.py, I am new so it might be possible that i made a very silly mistake here, which I am not able to figure out, please review this code and make me understand how to resolve this issue.
serializer.py
class ServiceSerializer(serializers.RelatedField):
def to_representation(self, value):
return value.name
class Meta:
model = Service
fields = ('name')
class SaloonSerializer(serializers.Serializer):
services = ServiceSerializer(read_only = True, many=True)
class Meta:
model = Saloon
fields = (
'name', 'services'
)
Here in the field of SaloonSerializers I have tried multiple things like only name field but still if get just one output which i have attache at the end of this post.
views.py
#api_view(['GET'])
def saloon_list(request):
if request.method=="GET":
saloons = Saloon.objects.all()
serializer = SaloonSerializer(saloons, many=True)
return Response(serializer.data)
models.py
class Service(models.Model):
name = models.CharField(max_length=200, blank=False)
price = models.IntegerField(blank=False)
time = models.IntegerField(blank=False)
def __str__(self):
return self.name
class Saloon(models.Model):
name = models.CharField(max_length = 200, blank=False)
services = models.ManyToManyField(Service)
def __str__(self):
return self.name
output:
[
{
"services": [
"Cutting",
"Shaving",
"Eye Brow",
"Waxing",
"Facial massage"
]
},
{
"services": [
"Cutting",
"Shaving",
"Facial massage"
]
}
]
You need to work with a ModelSerializer: a simple serializer does not care about a Meta, and will only serialize items for which you have specified a serializer field yourself.
You thus should rewrite this to:
# ModelSerializer ↓
class SaloonSerializer(serializers.ModelSerializer):
services = ServiceSerializer(read_only = True, many=True)
class Meta:
model = Saloon
fields = (
'name', 'services'
)
I have created model with many to many relationship and I have join table when I keep additional variable for it:
class BorderStatus(models.Model):
STATUS_CHOICES = [("OP", "OPEN"), ("SEMI", "CAUTION"), ("CLOSED", "CLOSED")]
origin_country = models.ForeignKey(OriginCountry, on_delete=models.CASCADE, default="0")
destination = models.ForeignKey(Country, on_delete=models.CASCADE, default="0")
status = models.CharField(max_length=6, choices=STATUS_CHOICES, default="CLOSED")
extra = 1
class Meta:
unique_together = [("destination", "origin_country")]
verbose_name_plural = "Border Statuses"
def __str__(self):
return (
f"{self.origin_country.origin_country.name} -> {self.destination.name}"
f" ({self.status})"
)
Other models:
# Create your models here.
class Country(models.Model):
name = models.CharField(max_length=100, unique=True, verbose_name='Country')
class Meta:
verbose_name_plural = "Countries"
def __str__(self):
return self.name
class OriginCountry(models.Model):
origin_country = models.ForeignKey(
Country, related_name="origins", on_delete=models.CASCADE
)
destinations = models.ManyToManyField(
Country, related_name="destinations", through="BorderStatus"
)
class Meta:
verbose_name_plural = "Origin Countries"
def __str__(self):
return self.origin_country.name
Here is my serializer for the endpoint:
class BorderStatusEditorSerializer(serializers.ModelSerializer):
"""Create serializer for editing single connection based on origin and destination name- to change status"""
origin_country = serializers.StringRelatedField(read_only=True)
destination = serializers.StringRelatedField(read_only=True)
class Meta:
model = BorderStatus
fields = ('origin_country', 'destination', 'status')
And my endpoint:
class BorderStatusViewSet(viewsets.ModelViewSet):
queryset = BorderStatus.objects.all()
serializer_class = BorderStatusEditorSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields=('origin_country','destination')
The problem Im having is that I cant create any new combination for the BorderStatus model in this serializer via post request.
If I remove the lines:
origin_country = serializers.StringRelatedField(read_only=True)
destination = serializers.StringRelatedField(read_only=True)
Then the form will work, but then I wont have the string representation of those variables, instead I get IDs.
Is there any way to allow request to accept origin_country and destination while being related fields?
EDIT:
To clarify how OriginCountry works, it is has a nested field:
[{ "id": 1
"origin_country": "Canada",
"dest_country": [
{
"id": 1,
"name": "France",
"status": "CLOSED"
},
{
"id": 2,
"name": "Canada",
"status": "OP"
}
]
},
]
You can try to override perform_create method of the viewset to make the necessary adjustments on-the-fly when new entry is posted:
class BorderStatusViewSet(viewsets.ModelViewSet):
queryset = BorderStatus.objects.all()
serializer_class = BorderStatusEditorSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields=('origin_country','destination')
def perform_create(self, serializer):
origin_country, _ = models.Country.get_or_create(name=self.request.data.get('origin_country')
destination, _ = models.Country.get_or_create(name=self.request.data.get('destination')
return serializer.save(origin_country=origin_country, destination=destination)
Maybe you will also need to adjust your serializer to have:
class CountrySerializer(serializers.ModelSerializer):
class Meta:
model = Country
fields = ['name']
class BorderStatusEditorSerializer(serializers.ModelSerializer):
origin_country = CountrySerializer()
destination = CountrySerializer()
...
Yes, I will try to give this combination.
You get this error because of Incorrect Type exception. Django checks data type validation on the serializer. For example here your dest_country returns a list of dicts but in your model it is a primary key (pk)
That's why on post django says : pk value expected, list received
But you can solve this error by using two different serializers (one to post another by default)
1. Create two different serializers
class BorderStatusEditorSerializer(serializers.ModelSerializer):
"""The First serialiser by default"""
origin_country = serializers.StringRelatedField(read_only=True)
destination = serializers.StringRelatedField(read_only=True)
class Meta:
model = BorderStatus
fields = ('origin_country', 'destination', 'status')
class BorderStatusEditorCreateSerializer(serializers.ModelSerializer):
"""The Second serialiser for create"""
class Meta:
model = BorderStatus
fields = ('origin_country', 'destination', 'status')
2.Add get_serializer_class method to for Viewset
class BorderStatusViewSet(viewsets.ModelViewSet):
queryset = BorderStatus.objects.all()
filter_backends = (DjangoFilterBackend,)
filter_fields=('origin_country','destination')
serializer_classes = {
'create': BorderStatusEditorCreateSerializer, # serializer used on post
}
default_serializer_class = BorderStatusEditorSerializer # Your default serializer
def get_serializer_class(self):
return self.serializer_classes.get(self.action, self.default_serializer_class)
I have two models with a many to many relation and
I am trying to send nested data to my API. Unfortunately it only gives me back an empty array.
This is what I am trying:
my models:
class Building(models.Model):
name = models.CharField(max_length=120, null=True, blank=True)
net_leased_area = models.FloatField(null=True, blank=True)
class BuildingGroup(models.Model):
description = models.CharField(max_length=500, null=True, blank=True)
buildings = models.ManyToManyField(Building, default=None, blank=True)
My generic API view:
class BuildingGroupCreateAPIView(CreateAPIView):
queryset = BuildingGroup.objects.all()
serializer_class = BuildingGroupSerializer
My serializer:
class BuildingGroupSerializer(serializers.ModelSerializer):
buildings = BuildingSerializer(many=True)
class Meta:
model = BuildingGroup
fields = (
'description',
'buildings',
)
def create(self, validated_data):
buildings_data = validated_data.pop('buildings')
building_group = BuildingGroup.objects.create(**validated_data)
for building_data in buildings_data:
Building.objects.create(building_group=building_group, **building_data)
return building_group
When I send data it returns this:
{"description":"Test Description API","buildings":[]}
In the array I would like to have my array of dictionaries.
I am trying to follow the REST documentation here when I am overwriting the create method to send a nested object. (https://www.django-rest-framework.org/api-guide/relations/#writable-nested-serializers) and I thought I am doing this correctly, but epic fail.
I send data with request with my custom method like this:
test_api_local(method="post", data={
"description": "Test Description API",
"buildings": [{'name' : 'Testname'}, .... ],
})
Any help is very appreciated. Thanks so much!!
EDIT: When I try to test it on the REST view it tells me:
TypeError: 'building_group' is an invalid keyword argument for this function
EDIT2: Here is my view:
class BuildingGroupCreateAPIView(CreateAPIView):
queryset = BuildingGroup.objects.all()
serializer_class = BuildingGroupSerializer
def create(self, request, *args, **kwargs):
serializer = BuildingGroupSerializer(data=self.request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
You have to explicitly get or create the Building instances, depending upon passed id's inside the payload data, then add them to BuildingGroup instance.
class NestedBuildingSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
class Meta:
model = Building
fields = '__all__'
class BuildingGroupSerializer(serializers.ModelSerializer):
buildings = NestedBuildingSerializer(many=True)
class Meta:
model = BuildingGroup
fields = (
'description',
'buildings',
)
def create(self, validated_data):
buildings_data = validated_data.pop('buildings')
building_group = BuildingGroup.objects.create(**validated_data)
buildings = [] # it will contains list of Building model instance
for building_data in buildings_data:
building_id = building_data.pop('id', None)
building, _ = Building.objects.get_or_create(id=building_id,
defaults=building_data)
buildings.append(building)
# add all passed instances of Building model to BuildingGroup instance
building_group.buildings.add(*buildings)
return building_group
class BuildingGroupView(ListAPIView, CreateAPIView):
queryset = BuildingGroup.objects.all()
serializer_class = BuildingGroupSerializer
## Assume you add your views like this in urls.py
urlpatterns = [
.....
path('building-groups', views.BuildingGroupView.as_view(),
name='building-group'),
.....
]
On calling endpoint /building-groups as POST method with payload like this:
{
"description": "here description",
"buildings": [
{
"id": 1, # existing building of id 1
"name": "name of building 1",
"net_leased_area": 1800
},
{
# a new building will gets create
"name": "name of building 2",
"net_leased_area": 1800
}
]
}
Then , it will return response like this:-
{
"description": "here description",
"buildings": [
{
"id": 1,
"name": "name of building 1",
"net_leased_area": 1800
},
{
"id": 2
"name": "name of building 2",
"net_leased_area": 1800
}
]
}
Learn about M2M relationship
and, .get_or_create()
Note: BuildingSerializer and NestedBuildingSerializer are both different. Don't mix them up.
I'm getting a Json response to save it in my database, I need to get the items in line_items object. My Serializer and View works fine if I remove the line_items attribute in the model, but when I try to get that object and save it in the database nothing happens. Maybe I'm missing something in my serializer?
Json Structure:
{
"id": 123456,
"email": "jon#doe.ca",
"created_at": "2017-03-29T15:56:48-04:00",
"line_items": [
{
"id": 56789,
"title": "Aviator sunglasses",
"quantity": 1
},
{
"id": 98765,
"title": "Mid-century lounger",
"quantity": 1
}
]
}
My model:
class Line(models.Model):
title = models.CharField(max_length=100)
quantity = models.IntegerField()
class Order(models.Model):
name = models.CharField(max_length=255)
created_at = models.DateTimeField()
total_price = models.DecimalField(max_digits=6,decimal_places=2)
line_items = models.ForeignKey(Line)
My Serializer:
class OrderSerializer(ModelSerializer):
class Meta:
model = Order
fields = '__all__'
My View:
#api_view(['POST'])
def orders(request):
if request.method == 'POST':
json_str = json.dumps(request.data)
resp = json.loads(json_str)
serializer = OrderSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
You have a list in the field-key line_items in your response, as per your models you can't accommodate the data in the tables, what you need is ManyToMany relation between Order and Line,
class Order(models.Model):
name = models.CharField(max_length=255)
created_at = models.DateTimeField()
total_price = models.DecimalField(max_digits=6,decimal_places=2)
line_items = models.ManyToManyField(Line)
and in your serializer,
class OrderSerializer(ModelSerializer):
class Meta:
model = Order
fields = [f.name for f in model._meta.fields] + ['line_items']
def create(self, validated_data):
line_items = validated_data.pop('line_items')
instance = super(OrderSerializer, self).create(validated_data)
for item in line_items:
instance.line_items.add(item['id'])
instance.save()
return instance
Just add depth=1 in your serializer. It will do-
class OrderSerializer(ModelSerializer):
class Meta:
depth = 1
fields = '__all__'
I think you should add an explicit line_items field to OrderSerializer. Like this:
class OrderSerializer(ModelSerializer):
line_items = LineSerializer(many=True)
class Meta:
model = Order
fields = '__all__'