DRF - Serializer Multiple Models - json

How can I POST this JSON
{
"campaign": 27,
"campaignName": "Prueba promo",
"promotionType": 999,
"items": [
{ "item_nbr": 1234567890123, "plu": 2},
{ "item_nbr": 12345678901, "plu": 3}
]
}
Currently, I only get this response JSON
{
"items": [],
"campaign": 27,
"campaignName": "Prueba promo",
"promotionType": 999,
"start_date": "2019-03-04T12:02:16.574874-03:00",
"end_date": null,
"active": true
}
How can I do it? I read the DRF documentation but it didn't work, what I'm doing wrong?
here is my code
my models.py
class Item(models.Model):
promocion = models.ForeignKey(Promocion, related_name='items', on_delete=models.CASCADE, null=True)
item_nbr = models.IntegerField(primary_key=True, help_text="Numero de Item")
modular = models.ForeignKey(Modular, on_delete=models.CASCADE, null=True)
price = models.FloatField()
q_min = models.PositiveIntegerField(default=1, help_text="Cantidad mínima")
q_mul = models.PositiveIntegerField(default=1, help_text="Multiplo de cajas cerradas")
vensil1 = models.CharField(max_length=30, help_text="Atributo item relevante")
vensil2 = models.CharField(max_length=30, help_text="Atributo item relevante")
vensil3 = models.CharField(max_length=30, help_text="Atributo item relevante")
FG = "Fleje grande, 1/3 Carta"
FP = "Fleje pequeño 1/6 Carta"
CP = "Carteleria media Carta"
opciones = ((FG, "Fleje grande, 1/3 Carta"),
(FP, "Fleje pequeño 1/6 Carta"),
(CP, "Carteleria media Carta"),)
print_type = models.CharField(choices=opciones, help_text="Fleje a imprimir", max_length=255)
depto = models.IntegerField(default=1, help_text="Departamento")
descri = models.CharField(max_length=100, help_text="Descripción producto")
brand = models.ForeignKey(Brand, on_delete=models.CASCADE, null=True)
vendor_pack = models.IntegerField(default=1)
container = models.CharField(max_length=6, default="MAY")
size = models.CharField(max_length=20, help_text="Tamaño pack")
cont_net = models.FloatField(default=1, help_text="Contenido Neto")
sell_unit = models.CharField(max_length=5, help_text="Unidad de venta")
weight_drain = models.FloatField(default=0, help_text="Peso drenado")
cod_bal = models.IntegerField(null=True, blank=True, help_text="Código balanza")
plu = models.BigIntegerField(help_text="Código de barra")
here are my serializer.py
class ItemPromoSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = ('item_nbr', 'plu')
class PromoSerializer(serializers.ModelSerializer):
items = ItemPromoSerializer(many=True, read_only=True)
#steps = ScalePromoSerializer(many=True)
class Meta:
model = Promocion
fields = ('items', 'campaign', 'campaignName', 'promotionType',
'start_date', 'end_date', 'active')
my viewsets.py
class PromoViewSet(viewsets.ModelViewSet):
queryset = Promocion.objects.all()
serializer_class = PromoSerializer
and my routes.py
router.register(r'promo', PromoViewSet)
I've tried methods to_internal_value() and to_representation() but the result was
"non_field_errors": ["Invalid data. Expected a dictionary, but got list."]

If this issue happens during a POST request, it means that you need to adapt the way you save your data. Django REST Framework doesn't support writing nested objects in DB out of the box.
What I usually do for these use cases is
I don't use viewset for such complex use cases, instead I prefer using CreateAPIView which enables me to use specific serializers to validate inputs and to present data.
I hook in create of CreateAPIView and use with transaction.atomic(): when writing to several tables at the same time to make sure all transactions are invalidated in case an error comes up.
I use 2 serializers one for the parent model and one for the child model.
In your case the code could like this:
serializer.py
class PromoSerializer(serializers.ModelSerializer):
class Meta:
model = Promocion
fields = ('campaign', 'campaignName', 'promotionType', 'start_date', 'end_date', 'active')
class ItemPromoSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = ('item_nbr', 'plu')
viewsets.py
from rest_framework import status
from django.db import transaction
class PromoCreateAPI(CreateAPIView):
queryset = Promocion.objects.all()
serializer_class = PromoSerializer
# We skip perform_create
def create(self, request, *args, ***kwargs):
try:
items_data = request.data.pop('items')
except KeyError:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
with transaction.atomic():
instance = serializer.save()
# Validate each item
for item in items_data:
s = ItemPromoSerializer(data=item)
s.is_valid(raise_exception=True)
s.save(campaign=instance)
headers = self.get_success_headers(serializer.data)
serializer.data['items'] = items_data
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
Of course this code is untested, but I hope it helps you get where you need to.

Related

Update multiple model data through one serializer

Please go through the description, I tried to describe everything i've encountered while trying to solve this issue.
I have two models, User and DoctorProfile. User model has OneToOne relation with DoctorProfile. I'm trying to update data of both model through one serializer. I've combined two models into one serilzer like below:
class DoctorProfileFields(serializers.ModelSerializer):
"""this will be used as value of profile key in DoctorProfileSerializer"""
class Meta:
model = DoctorProfile
fields = ('doctor_type', 'title', 'date_of_birth', 'registration_number', 'gender', 'city', 'country', )
class DoctorProfileSerializer(serializers.ModelSerializer):
"""retrieve, update and delete profile"""
profile = DoctorProfileFields(source='*')
class Meta:
model = User
fields = ('name', 'avatar', 'profile', )
#transaction.atomic
def update(self, instance, validated_data):
ModelClass = self.Meta.model
profile = validated_data.pop('profile', {})
ModelClass.objects.filter(id=instance.id).update(**validated_data)
if profile:
DoctorProfile.objects.filter(owner=instance).update(**profile)
new_instance = ModelClass.objects.get(id = instance.id)
return new_instance
When I send request with GET method, the DoctorProfileSerializer returns nested data(Combining two models User and DoctorProfile) in the desired fashion.
But when I try to update both models through this serializer, it returns error saying User has no field named 'doctor_type'.
Let's have a look at the JSON i'm trying to send:
{
"name": "Dr. Strange updated twice",
"profile" : {
"doctor_type": "PSYCHIATRIST"
}
}
Let's have a look at how the serializer is receiving the JSON:
{
"name": "Maruf updated trice",
"doctor_type": "PSYCHIATRIST"
}
Models:
class CustomUser(AbstractBaseUser, PermissionsMixin):
class Types(models.TextChoices):
DOCTOR = "DOCTOR", "Doctor"
PATIENT = "PATIENT", "Patient"
#Type of user
type = models.CharField(_("Type"), max_length=50, choices=Types.choices, null=True, blank=False)
avatar = models.ImageField(upload_to="avatars/", null=True, blank=True)
email = models.EmailField(max_length=255, unique=True)
name = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
objects = CustomBaseUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['name', 'type'] #email is required by default
def get_full_name(self):
return self.name
def __str__(self):
return self.email
class DoctorProfile(models.Model):
"""Model for Doctors profile"""
class DoctorType(models.TextChoices):
"""Doctor will choose profession category from enum"""
PSYCHIATRIST = "PSYCHIATRIST", "Psychiatrist"
PSYCHOLOGIST = "PSYCHOLOGIST", "Psychologist"
DERMATOLOGIST = "DERMATOLOGIST", "Dermatologist"
SEXUAL_HEALTH = "SEXUAL HEALTH", "Sexual health"
GYNECOLOGIST = "GYNECOLOGIST", "Gynecologist"
INTERNAL_MEDICINE = "INTERNAL MEDICINE", "Internal medicine"
DEVELOPMENTAL_THERAPIST = "DEVELOPMENTAL THERAPIST", "Developmental therapist"
owner = models.OneToOneField(
CustomUser,
on_delete=models.CASCADE,
related_name='doctor_profile'
)
doctor_type = models.CharField(
_("Profession Type"),
max_length=70,
choices=DoctorType.choices,
null=True,
blank=False
)
title = models.IntegerField(_('Title'), default=1, choices=TITLES)
date_of_birth = models.DateField(null=True, blank=False)
gender = models.IntegerField(_('Gender'), default=1, choices=GENDERS)
registration_number = models.IntegerField(_('Registration Number'), null=True, blank=False)
city = models.CharField(_('City'), max_length=255, null=True, blank=True)
country = models.CharField(_('Country'), max_length=255, null=True, blank=True)
def __str__(self):
return f'profile-{self.id}-{self.title} {self.owner.get_full_name()}'
How do I know that the serializer is getting wrong JSON? I debugged the validated_data in the DoctorProfileSerializer and it's showing that it's a flat JSON, there's no key named profile.
I'm assuming the problem is with the source that I've added in the DoctorProfileSerializer. But if I don't use the source the get method returns the following error
Got AttributeError when attempting to get a value for field profile on serializer (DoctorProfileSerializer).
Please let me know if it's solvable also if it's a good approach to do it this way?
Ok, sorry if my answer is too long but let me try to answer step by step,
Models:
class DoctorProfile(models.Model):
# everything as it is
# except I feel comfortable using ForeignKey :D
owner = models.ForeignKey(
CustomUser,
on_delete=models.CASCADE,
related_name='doctor_profile'
)
# everything as it is
class CustomUser(AbstractBaseUser, PermissionsMixin):
# as it is
Serializers:
class DoctorProfileSerializer(serializers.ModelSerializer):
"""Serializer for DoctorProfile."""
class Meta(object):
model = DoctorProfile
fields = [
'id',
'doctor_type',
'title',
'date_of_birth',
'registration_number',
'gender',
'city',
'country',
]
read_only_fields = [
'id',
]
class CustomUserSerializer(serializers.ModelSerializer):
"""Serializer for DoctorProfile."""
# here I'm renaming the related object exactly as the
# related name you've provided on model
doctor_profile = DoctorProfileSerializer(many=False)
class Meta(object):
model = CustomUser
fields = [
'name',
'avatar',
'doctor_profile',
]
read_only_fields = [
'id',
]
def update(self, instance, validated_data):
# instance is the current row of CustomUser
# validated_data is the new incoming data
# use validated_data.pop('doctor_profile') to extract
# doctor_profile data and do whatever is needed on
# DoctorProfile model
# compare them and perform your update method
# as you wish on the DoctorProfile model
# object after updating models, you can query the total
# object again before returning if you want
return updated_object
View:
class CustomUserAPIView(RetrieveUpdateAPIView):
"""CustomUserAPIView."""
permission_classes = [IsAuthenticated]
model = CustomUser
serializer_class = CustomUserSerializer
lookup_field = 'id'
#for returning logged in user info only
def get_queryset(self):
return CustomUser.objects.filter(id=self.request.user.id).first()
def update(self, request, *args, **kwargs):
"""Update override."""
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(
instance,
data=request.data,
partial=partial,
)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
custom_user_obj = CustomUser.objects.filter(
id=instance.id,
).first()
serializer = CustomUserSerializer(custom_user_obj)
return Response(serializer.data)
Run the migration and let me know if you are getting the expected output on GET method. For UPDATE method if you face any problem let me know I will update the answer accordingly right away.
For keeping all the Django Rest Framework related docs handy, use this link
https://www.cdrf.co/

To return custom field in DjangoRestFramework after aggregation

There are lot of answers related to custom fields on stackoverflow but when trying with them, I am getting different error, so posting a separate question.
I wanted to return a JSON Response for the following url
urls.py
path('cards/<int:pk>/smarts', smarts.as_view(), name="smarts"),
I will be using below api.py file to return aggregate fields using Transaction model, the query is working fine, I only have to return appropriate response. Here I have one of the fields as Decimal hence tried with DjangoJSONEncoder but got an error.
api.py
class smarts(generics.ListAPIView):
serializer_class = TransactionSerializer
permission_classes = [permissions.IsAuthenticated, TransactionIsOwnerOrNot]
def get_queryset(self):
card = get_object_or_404(self.request.user.cards, pk=self.kwargs['pk'])
qs=card.transactions.values('vendor').annotate(a=Count('pk'),b=Sum('amount')).order_by('-b')
....CODE REQUIRED
return ....
models.py
class Transactions(models.Model):
amount = models.DecimalField(max_digits=19, decimal_places=2)
vendor = models.CharField(max_length=200)
category = models.CharField(max_length=200)
owner = models.ForeignKey(Cards, on_delete=models.CASCADE, related_name="transactions",null=True)
serializer.py
class TransactionSerializer(serializers.ModelSerializer):
class Meta:
model = Transactions
fields = '__all__'
I found the answer after hit and trial, I used custom serializer to return required field.
serializer.py
class SmartSerializer(serializers.Serializer):
vendor = serializers.CharField(max_length=200)
tot = serializers.IntegerField()
tot_amt = serializers.DecimalField(max_digits=19, decimal_places=2)
api.py
class smartstatements(generics.ListAPIView):
permission_classes = [permissions.IsAuthenticated, TransactionIsOwnerOrNot]
serializer_class = SmartSerializer
def get_queryset(self):
card = get_object_or_404(self.request.user.cards, pk=self.kwargs['pk'])
queryset=card.transactions.values('vendor')
.annotate(tot=Count('pk'),tot_amt=Sum('amount'))
.order_by('-tot_amt')
return queryset

Django rest framework charfilter json

I have a filter in django rest charfilterinfilter(field_name= 'genres__name', lookup_expr= 'in').I have in the database is which two categories to approach I through Many To Many did ,But I have when filtrating two categories of this product there are two elements I need only one element
views
class CharFilterInFilter(filters.BaseInFilter, filters.CharFilter):
pass
class ShoppFilter(filters.FilterSet):
price = filters.RangeFilter()
genres = CharFilterInFilter(field_name='genres__name')
title = SearchFilter()
class Meta:
model = smartphone
fields = ['price','genres','title']
class MDShopListView(generics.ListAPIView):
queryset = smartphone.objects.all()
filter_backends = (DjangoFilterBackend,SearchFilter)
search_fields = ['title']
filterset_class = ShoppFilter
def get(self, request):
queryset = self.filter_queryset(self.get_queryset())
serializer=MDShopListSerializer(queryset,many=True)
return Response(serializer.data)
models
genres = models.ManyToManyField(Genre, verbose_name="жанры")
class Genre(models.Model):
[enter image description here][1]
name = models.CharField("Имя", max_length=100)
img json
1: https://i.stack.imgur.com/4WR6L.png
here change and work
queryset = self.filter_queryset(self.get_queryset()).distinct()
class CharFilterInFilter(filters.BaseInFilter, filters.CharFilter):
pass
class ShoppFilter(filters.FilterSet):
price = filters.RangeFilter()
genres = CharFilterInFilter(field_name='genres__name', lookup_expr='in')
title = SearchFilter()
class Meta:
model = smartphone
fields = ['price','genres','title']
class MDShopListView(generics.ListAPIView):
queryset = smartphone.objects.all()
filter_backends = (DjangoFilterBackend,SearchFilter)
search_fields = ['title']
filterset_class = ShoppFilter
def get(self, request):
queryset = self.filter_queryset(self.get_queryset()).distinct()
serializer=MDShopListSerializer(queryset,many=True)
return Response(serializer.data)
This is a usual problem with ManyToMany fields, solution would be to apply a distinct method to the query:
class ShoppFilter(filters.FilterSet):
...your filter definition as it is now
def filter_queryset(self, request, queryset, view):
return super(ShoppFilter, self).filter_queryset(
request, queryset, view
).distinct()

Object of type "" is not JSON serializable

I am trying to create an API and pass the context data in Response but I am getting the error:
Object of type TakenQuiz is not JSON serializable
Below is the code:
taken_quizzes = quiz.taken_quizzes.select_related('supplier__user').order_by('-date')
total_taken_quizzes = taken_quizzes.count()
quiz_score = quiz.taken_quizzes.aggregate(average_score=Avg('score'))
least_bid = quiz.taken_quizzes.aggregate(least_bid=Min('least_bid'))
extra_context = {'taken_quizzes': taken_quizzes,
'total_taken_quizzes': total_taken_quizzes,
'quiz_score': quiz_score, 'least_bid': least_bid, 'matching_bids': matching_bids,
'driver_num': driver_num, 'lat_lon_orig': lat_lon_orig, 'lat_lon_dest': lat_lon_dest,
'user_pass': user_pass, 'username': username, 'password': password, }
print("extra content is ", extra_context)
return Response(extra_context)
Here is the context data:
extra content is {'taken_quizzes': <QuerySet [<TakenQuiz: TakenQuiz object (1)>]>, 'total_taken_quizzes': 1, 'quiz_score': {'average_score': 0.0}, 'least_bid': {'least_bid': 899}, 'matching_bids': [], 'driver_
num': 0, 'lat_lon_orig': '36.1629343, -95.9913076', 'lat_lon_dest': '36.1629343, -95.9913076', 'user_pass': ('jkcekc', 'efce'), 'username': 'efw', 'password': 'sdf'}
The error I believe is because of the queryset in the extra_context, How do I resolve this ?
I tried json.dumps but it still doesn't work
Serializer.py
class takenquizSerializer(serializers.ModelSerializer):
class Meta:
model = TakenQuiz
fields = "__all__"
Models.py
class TakenQuiz(models.Model):
supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE, related_name='taken_quizzes')
quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE, related_name='taken_quizzes')
score = models.FloatField()
date = models.DateTimeField(auto_now_add=True)
least_bid = models.IntegerField(default=0)
confirmed = models.CharField(max_length=100, default='Not Confirmed')
UPDATE
taken_quizzes = quiz.taken_quizzes.select_related('supplier__user').order_by('-date')
taken_quizzs = takenquizSerializer(taken_quizzes).data
You need to serialize taken_quizzes objects either via some serializer or calling
".values()" and specifying the required key, if any (otherwise it will give all values of the model as a dictionary)
{
'taken_quizes': TakenQuizSerializer(taken_quizzes, many=True).data,
# or
'taken_quizzes': taken_quizzes.values(),
....
}
as ruhaib mentioned you need to serialize the data. If i dont want to define special serializers for models this is what I do.
from django.core import serializers
taken_quizzes=....
data=serializers.serialize('json',taken_quizzes)
you can do this before you populate extra_content with some data.

depth = 1 doesn't work properly and it's saves Null in ManyToManyField and ForeignKey fields in Django Rest Framework

after adding depth = 1 doesn't work properly
=> models.py file
class State(models.Model):
state_name = models.CharField(max_length = 30, unique=True)
def __unicode__(self):
return str(self.state_name)
class City(models.Model):
state = models.ForeignKey(State, related_name='state_city')
city_name = models.CharField(max_length = 30)
def __unicode__(self):
return str(self.city_name)
class Meta:
ordering = ('city_name',)
unique_together = ('state', 'city_name',)
class Snippet(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100, blank=True, default='')
code = models.TextField()
linenos = models.BooleanField(default=False)
owner = models.ForeignKey('auth.User', related_name='snippets')
state = models.ForeignKey(State,blank=True,null=True)
city = models.ManyToManyField(City)
=> serializers.py file
class StateSerializer(serializers.ModelSerializer):
class Meta:
model = State
class CitySerializer(serializers.ModelSerializer):
state_name = serializers.CharField(source='state.state_name', read_only=True)
class Meta:
model = City
class SnippetSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username', read_only=True)
class Meta:
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'owner', 'state', 'city')
depth = 1
I have added ForeignKey and ManyToManyField fields in state and city respectively. It doesn't save values in SnippetSerializer while added depth = 1 in Meta Class (it saves Null value in state and city fields). When I add depth = 1 JSON showing related fields as it should be but it doesn't work properly while add new Snippet. Without depth = 1 it works fine.
I have complex database where tables has many ManyToMany and ForeignKey related fields. Please give me suggestion so I can get related data in JSON.
I have djangorestframework-3.1.2 version. I have used latest version too but same problem. please give me solution and thanks in advance.
I faced the same problem and managed to solve it. Since the problem is with the depth, I just change the depth value in the init method.
class SnippetSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username', read_only=True)
class Meta:
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'owner', 'state', 'city')
depth = 1
def __init__(self, *args, **kwargs):
super(SnippetSerializer, self).__init__(*args, **kwargs)
request = self.context.get('request')
if request and request.method=='POST':
self.Meta.depth = 0
else:
self.Meta.depth = 1
In the code above, I changed the depth dynamically according to what type of request that I made.
But, this is the workaround that I found myself, I'm not sure if this is the best practice but it solve the problem with just little modification.
depth is only for representation (http://www.django-rest-framework.org/api-guide/serializers/#specifying-nested-serialization). If you want to create/update the related fields too you have to overwrite the create/update methods in the serializer (http://www.django-rest-framework.org/api-guide/relations/#writable-nested-serializers).
Greetings.