Django RestFramework group by - json

My issue is related to the django-rest-framework and is about how to group elements.
This is my serializers.py
from collaborativeAPP.models import *
from rest_framework import serializers
class VocabSerializer(serializers.ModelSerializer):
term_word = serializers.CharField(source='term.word',read_only=True)
kwdGroup = serializers.StringRelatedField()
class Meta:
model = Vocab
fields = ('id','term_word', 'meaning','kwdGroup')
class TermSerializer(serializers.ModelSerializer):
word = serializers.CharField(read_only=True)
class Meta:
model = Term
fields = ('url', 'word')
The following JSON it's the actual result:
{"results":[
{
"id": 5,
"term_word": "word1",
"meaning": "Text1"
"kwdGroup": "A"
},
{
"id": 6,
"term_word": "word2",
"meaning": "Text2"
"kwdGroup": "A"
},
{
"id": 7,
"term_word": "word3",
"meaning": "Text3"
"kwdGroup": "A"
}
]}
As you can notice kwdGroup is a repetitive element that I which to group.
I would like to group by kwdGroup:
{"A":[
{
"id": 5,
"term_word": "word1",
"meaning": "Text1"
},
{
"id": 6,
"term_word": "word2",
"meaning": "Text2"
},
{
"id": 7,
"term_word": "word3",
"meaning": "Text3"
}
]
}
I'm looking for answers on http://www.django-rest-framework.org/ on the API guide but I'm having difficulties finding an approach to lead with it.
Do you share this same issue? Do you have any suggestions on how can i do this? Do you have any example that deals with element grouping using django-rest-framework?
Thanks in advance.

Let's assume that the kwdGroup field is the relation field to a model called KeyWordGroup.
The default ListSerializer uses the to_representation method to render a list of the serialized objects rest_framework :
class ListSerializer(BaseSerializer):
...
def to_representation(self, data):
"""
List of object instances -> List of dicts of primitive datatypes.
"""
# Dealing with nested relationships, data can be a Manager,
# so, first get a queryset from the Manager if needed
iterable = data.all() if isinstance(data, models.Manager) else data
return [
self.child.to_representation(item) for item in iterable
]
We can modify the ListSerializer to group the results for example:
class VocabListSerializer(serializers.ListSerializer):
def to_representation(self, data):
iterable = data.all() if isinstance(data, models.Manager) else data
return {
kwdGroup: super().to_representation(Vocab.objects.filter(kwdGroup=kwdGroup))
for kwdGroup in KeyWordGroup.objects.all()
}
We can then use the modified VocabListSerializer with the VocabSerializer.
class VocabSerializer(serializers.Serializer):
...
class Meta:
list_serializer_class = VocabListSerializer

One way to achieve this is to use a SerializerMethodField. The below might be slightly different than your use case, but you can adopt accordingly. There are other ways of achieving this as well, including overwriting to_representation methods, but they rely on messing with the inner workings of DRF more than is relevant here.
models.py
class Dictionary(Model):
id = PrimaryKey
class Word(Model):
dictionary = ForeignKey(Dictionary, related_name='words')
word = Charfield()
group = Charfield()
serializers.py
class WordSerializer(serializers.ModelSerializer):
word = serializers.CharField(read_only=True)
class Meta:
model = Word
fields = ('word',)
class DictionarySerializer(serializers.ModelSerializer):
group_a = serializers.SerializerMethodField()
group_b = serializers.SerializerMethodField()
def get_group_a(self, instance):
return WordSerializer(instance.words.filter(group='a'), many=True).data
def get_group_b(self, instance):
return WordSerializer(instance.words.filter(group='b'), many=True).data
class Meta:
model = Dictionary
fields = ('group_a', 'group_b')
An example
>>> my_dictionary = Dictionary.objects.create()
>>> Word.objects.bulk_create(
Word(word='arrow', group='a' dictionary=my_dictionary),
Word(word='apple', group='a' dictionary=my_dictionary),
Word(word='baby', group='b' dictionary=my_dictionary),
Word(word='banana', group='b' dictionary=my_dictionary)
)
>>> serializer = DictionarySerializer(my_dictionary)
>>> print serializer.data
{
'group_a': {
'word': 'arrow',
'word': 'apple'
},
'group_b': {
'word': 'baby',
'word': 'banana'
},
}

Related

How to modify a many-to-many collection using django rest framework

I am trying to create an endpoint where, having a User entity, I can add / remove existing Group entities to user.groups many-to-many field. But when I try to do it, django-rest-framework tries to create new group objects instead of finding existing ones.
I have defined two serializers where UserSerializer has a nested GroupSerializer:
class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = Group
fields = ['id', 'name']
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'groups']
groups = GroupSerializer(many=True)
def update(self, instance, validated_data):
data = validated_data.copy()
groups = data.pop('groups', [])
for key, val in data.items():
setattr(instance, key, val)
instance.groups.clear()
for group in groups:
instance.groups.add(group)
return instance
def create(self, validated_data):
data = validated_data.copy()
groups = data.pop('groups', [])
instance = self.Meta.model.objects.create(**data)
for group in groups:
instance.groups.add(group)
return instance
When I send a JSON through a PUT REST call (from django-rest-framework web interface):
{
"id": 6,
"username": "user5#example.com",
"email": "user5#example.com",
"groups": [
{
"id": 1,
"name": "AAA"
}
]
}
I expect serializer to find the Group with given id and add it to User. But instead, it tries to create a new user group and fails with duplicate key error:
{
"groups": [
{
"name": [
"group with this name already exists."
]
}
]
}
I searched over the internet and debugged myself and found no solution to this use case.
The create and update methods inside UserSerializerclass are never reached.
Edit: as asked, here are my views and urls:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all().order_by('-date_joined')
serializer_class = UserSerializer
class GroupViewSet(viewsets.ModelViewSet):
queryset = Group.objects.all()
serializer_class = GroupSerializer
Urls:
router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'groups', views.GroupViewSet)
urlpatterns = [
path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
This seems to be the validation error due to a nested serializer model that contains unique constraint, see this post. According to the article, DRF did not handle this condition since it's hard to realize if the serializer is a nested serializer within another one. And that's why the create() and update() never been reached since the validation is done before calling them.
The way to work around this is to remove the uniqueness validator manually in GroupSerializer as follow:
class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = Group
fields = ['id', 'name']
extra_kwargs = {
'name': {'validators': []},
}
BTW, there are some points that can be improved or should be corrected in your update() and create() code. Firstly, you didn't do instance.save() so the instance won't be update after the whole process done. Second, the groups are just a list of dictionary, and you should not add object that way. The following are the modification based on your OP code:
def update(self, instance, validated_data):
data = validated_data.copy()
groups = data.pop('groups', [])
for key, val in data.items():
setattr(instance, key, val)
instance.save() # This will indeed update DB values
group_ids = [g['id'] for g in groups]
instance.groups.clear()
instance.groups.add(*group_ids) # Add all groups once. Also you can replace these two lines with
# instance.groups.set(group_ids)
return instance
Though am late, here is how I did it, adding to Tsang-Yi Shen answer. However this worked for me because I was using django-role-permissions https://django-role-permissions.readthedocs.io/en/stable/setup.html
For the group serializer am only interested in the name.
from rolepermissions.roles import assign_role
class GroupModelSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField()
class Meta:
model = Group
fields = ['name']
extra_kwargs = {
'name': {'validators': []},
}
def get_name(self, obj):
"""
This method modifies the way the name field is returned in a get request.
"""
return [group.name for group in obj.objects.all()]
In the UserSerializer, I modify the groups field to be returned as a list of groups a user belongs to, and received as a list instead of an OrderedDict in the JSON payload for creating a User, which can look something like this:
{
"email": "testuser#example.com",
"username": "testuser",
"name": "Test User",
"password": "password123",
"groups": ["doctor", "nurse", ]
}
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
groups = ListField(required=False, default=[], write_only=True)
user_groups = serializers.SerializerMethodField(read_only=True)
class Meta:
model = User
fields = ["id", "email", "username", "name", "password", "groups", "user_groups"]
depth = 2
def get_user_groups(self, obj):
"""
This method modifies the way the `groups` field is returned in a get request.
"""
return [group.name for group in obj.groups.all()]
def create(self, validated_data):
password = validated_data.pop('password')
gropus_to_add_user = validated_data.pop("groups")
user = User(**validated_data)
user.set_password(password)
user.save()
for group_name in groups_to_add_user:
# The assign_role function adds the user to a group.
# In this case group and roles may mean the same thing.
assign_role(user, group_name)
return user
You will have something like this when creating a user, here am using Swagger:

how to design the serializer in django rest framework for the Json response?

Hi am new to django rest framework.Currently am working on a thing which is contains a json response like this :
{
"students": [
{
"first_name": <str: student first name>,
"last_name": <str: student last name>,
"unique_id": <str: student unique id>,
"current_teachers": [
{
"first_name": <str: teacher first name>,
"last_name": <str: teacher last name>
},
...
]
},
...more...
]
}
This is my serializer:
class StudentFilterSerializer(serializers.ModelSerializer):
class Meta:
model = Student
fields = ("first_name", "last_name", "unique_id",)
class StudentFilterTeacherSerializer(serializers.ModelSerializer):
first_name = serializers.CharField(source='teacher.first_name')
last_name = serializers.CharField(source='teacher.last_name')
class Meta:
model = TeacherClass
fields = ("teacher","first_name","last_name")
class FilterStudentsSerializer(serializers.Serializer):
students = StudentFilterSerializer(many=True)
current_teachers = StudentFilterTeacherSerializer(many=True, required=False)
Now how to edit my serializer to achieve the json response.
Cause this is my current json structure:
serializer = FilterStudentsSerializer()
serializer.data
{'students': [], 'current_teachers': []}
It would be easier to provide correct answer if we could see how your models are implemented. Without knowing the fields connection, I will still try
Here's two kind of solutions:
First:
class StudentFilterTeacherSerializer(serializers.ModelSerializer):
first_name = serializers.CharField(source='teacher.first_name')
last_name = serializers.CharField(source='teacher.last_name')
class Meta:
model = TeacherClass
fields = ("first_name","last_name")
class StudentFilterSerializer(serializers.ModelSerializer):
current_teachers = SerializerMethodField(required=False)
class Meta:
model = Student
fields = ("first_name", "last_name", "unique_id", "current_teachers")
def current_teachers(self, student):
# Get all teachers for this student
# I don't know how your models are created but you'll get the idea
# assuming you have "related_name" setup between student and teachers
teachers = student.teachers.all()
return StudentFilterTeacherSerializer(teachers, many=True).data
and second (probably not the one you want, still for your knowledge):
Note: This will return all fields for teachers.
class StudentFilterSerializer(serializers.ModelSerializer):
class Meta:
model = Student
fields = ("first_name", "last_name", "unique_id",)
# if you somehow have teachers field in 'student' model then you can also do
depth = 1
Happy to help.
Let me know if it works or you don't understand anything.

DRF - Serializer Multiple Models

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.

DRF Create queryset by adding foreign key queryset into it

I want to get foreign data(Child model) into Parent Model queryset.
I am able to add foreign data into Parent data but it is JSON. I want to convert that JSON into queryset because get_queryset() returns model/queryset.
I googled a lot but unable to find anything helpful.
class Parent(models.Model):
parent_name = models.TextField()
child = models.ForeignKey(Child, related_name="+", on_delete=models.CASCADE, null=True)
class Child(models.Model):
child_name = models.TextField()
class ParentViewSet(viewsets.ModelViewSet):
queryset = Parent.objects.all()
serializer_class = ParentInfoSerializer
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ['parent_name']
def get_queryset(self):
response = []
parent_name = self.request.GET.getlist("parent_name")[0]#
parent_queryset =
Parent.objects.filter(parent_name=parent_name)
for par_queryset in parent_queryset:
parent_result = self.serializer_class(phy_queryset).data
child_id = physician_info_result["child"]
child_instance = Child.objects.get(pk=child_id)
child_result = ChildSerializer(child_instance).data
parent_result["child"] = child_result
response.append(parent_result)
return response
URL -
http://localhost:9000/api/parent?parent_name=xyz
Response output is:
[{
"parent_name": "xyz",
"child": [{
"id": 1
"child_name": "abc"
}]
}]
But above output it is JSON which I don't want. I want output into queryset/model.
NOTE: Queryset output should contain foreign queryset.
You could just replace the default field for child to child serializer and it'll work like you want.
class ParentSerializer(models.Model):
child = ChildSerializer()
class Meta:
models = Parent
fields = ('parent_name', 'child')
And you also don't need to override the get_queryset method.

Django Rest Framework: Deserializing and get the primary key from validated_data

I defined a nested model Product as follow. Each Product can belong to a lot of Productlist.
class Product(models.Model):
product_id = models.AutoField(primary_key=True)
product_name = models.CharField(max_length=50)
class Productlist(models.Model):
productlist_id = models.AutoField(primary_key=True)
productlist_name = models.CharField(max_length=50)
product = models.ManyToManyField(Product, related_name='productlists')
The corresponding serializers are:
class ProductlistSerializer(serializers.ModelSerializer):
class Meta:
model = Productlist
fields = ('productlist_id', 'productlist_name',)
class ProductSerializer(serializers.ModelSerializer):
productlists = ProductlistSerializer(many=True, required=False)
class Meta:
model = Product
fields = ('product_id', 'product_name', 'product lists')
def create(self, validated_data):
#some codes
When I POST a new Product (url(r'^api/products/$', views.ProductEnum.as_view()), I would like to update the product lists for adding the new product to the corresponding product lists. The JSON file I prefer to use is:
{
"product_name": "product1"
"productlist": [
{
"productlist_id": 1,
"productlist_name": "list1",
},
{
"productlist_id": 2,
"productlist_name": list2"
}
]
}
The problem is that I cannot get the productlist_id from validated_data. In Django Rest Framework, you always need to call to_internal_value() for deserializing data and generate validated_data. After some degugging, I checked the code of DRF and find the following snippets in to_internal_value():
def to_internal_value(self, data):
"""
Dict of native values <- Dict of primitive datatypes.
"""
if not isinstance(data, dict):
message = self.error_messages['invalid'].format(
datatype=type(data).__name__
)
raise ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: [message]
})
ret = OrderedDict()
errors = OrderedDict()
fields = [
field for field in self.fields.values()
if (not field.read_only) or (field.default is not empty)
]
for field in fields:
validate_method = getattr(self, 'validate_' + field.field_name, None)
primitive_value = field.get_value(data)
try:
validated_value = field.run_validation(primitive_value)
if validate_method is not None:
validated_value = validate_method(validated_value)
except ValidationError as exc:
errors[field.field_name] = exc.detail
except DjangoValidationError as exc:
errors[field.field_name] = list(exc.messages)
except SkipField:
pass
else:
set_value(ret, field.source_attrs, validated_value)
if errors:
raise ValidationError(errors)
return ret
Please notice the to_internal_value's fields has ignored the IntegerField(read_only=True) for it cannot satisfy the following condition:
fields = [
field for field in self.fields.values()
if (not field.read_only) or (field.default is not empty)
]
So the validated_data will just have the following data:
{
"product_name": "product1"
"productlist": [
{
"productlist_name": "list1",
},
{
"productlist_name": list2"
}
]
}
How could I get the primary key of product list? Thanks in advance!
After some digging, I found that the read_only fields are only for output presentation. You can find the similar question on the offcial github link of Django REST Framework.
So the solution is overriding the read_only field in the serializer as follow:
class ProductlistSerializer(serializers.ModelSerializer):
productlist_id = serializers.IntegerField(read_only=False)
class Meta:
model = Productlist
fields = ('productlist_id', 'productlist_name',)