Django Rest Framework ParentRelatedField metadata choices - python

Hello Everyone!
I have latest django-rest-framework and I'm trying to make serializer show possible choices for each field on OPTIONS request.
Here's part of my model
# models.py
class Task(models.Model):
parent = models.ForeignKey('self',
blank=True, null=True, related_name='child_tasks')
title = models.CharField(max_length=128)
status = models.CharField(max_length=16, choices=STATUS_CHOISES, default='new')
priority = models.CharField(max_length=16, choices=PRIORITY_CHOISES, default='1')
chief = models.ForeignKey('users.SystemUser', related_name='tasks',
blank=True, null=True)
And here's serializer
# serializers.py
class ParentRelatedField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
obj = self.context['view'].get_object()
return Task.objects.exclude(pk=obj.pk)
def get_user_choices():
return tuple([(i.id, i.system_name) for i in SystemUser.objects.all()])
class TaskDetailSerializer(serializers.Serializer):
title = serializers.CharField()
parent = ParentRelatedField(
required=False, allow_null=True
)
status = serializers.ChoiceField(choices=STATUS_CHOISES)
priority = serializers.ChoiceField(choices=PRIORITY_CHOISES)
chief = serializers.ChoiceField(choices=get_user_choices(), required=False)
I achieved that for chief field using get_user_choices function, so i get:
"chief": {
"type": "choice",
"required": false,
"read_only": false,
"label": "Chief",
"choices": [
{
"value": 1,
"display_name": "First User Name"
}
]
}
ParentRelatedField works great for validation, but not for metadata:
"parent": {
"type": "field",
"required": false,
"read_only": false,
"label": "Parent"
}
I can't use ChoiceField with function (like in chief) for that because parent choice must exclude current Task object.

Solved the problem.
Solution was found at drf 3.4 announcement and drf issue comments.
I changed my field to make it more universal
class SelfExcludingRelatedField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
obj = self.context['view'].get_object()
return self.queryset.exclude(pk=obj.pk)
Then wrote custom metadata class (copied from github).
class CustomMetadata(SimpleMetadata):
def get_field_info(self, field):
field_info = super().get_field_info(field)
if (not field_info.get('read_only') and
isinstance(field, SelfExcludingRelatedField) and
hasattr(field, 'choices')):
field_info['choices'] = [
{
'value': choice_value,
'display_name': choice_name
}
for choice_value, choice_name in field.choices.items()
]
return field_info
And added it to settings:
REST_FRAMEWORK = {
'DEFAULT_METADATA_CLASS': 'api_v0.metadata.CustomMetadata',
}

Related

django drf about many-to-many RelatedField custom field

I use the latest version of The Django REST Framework,
and The table in model is many-to-many related
My current model code looks like this:
model.py
class LvsDeploy(models.Model):
MASTER = 'MASTER'
BACKUP = 'BACKUP'
ROLE_CHOICES = (
(MASTER, 'MASTER'),
(BACKUP, 'BACKUP')
)
id = models.AutoField(primary_key=True)
cluster_name = models.CharField(max_length=30)
dip = models.CharField(max_length=15, verbose_name='DR IP')
role = models.CharField(max_length=10, choices=ROLE_CHOICES, verbose_name="role")
class LoadBalance(models.Model):
id = models.AutoField(primary_key=True)
lb_name = models.CharField(max_length=30, verbose_name='load balance')
cluster = models.ManyToManyField('LvsDeploy',related_name='cluster')
vip = models.CharField(max_length=50, verbose_name='VIP')
port = models.IntegerField(verbose_name='port')
def __str__(self):
return self.lb_name
My serializer code looks like this:
serializers.py
class LvsDeployReadOnly(serializers.ModelSerializer):
class Meta:
model = LvsDeploy
fields = '__all__'
class LoadBalanceSerializer(serializers.ModelSerializer):
cluster_name = serializers.RelatedField(source='cluster.name',read_only=True)
cluster = LvsDeployReadOnly(many=True)
##rs = RSInfoSerializer(many=True, read_only=True)
class Meta:
model = LoadBalance
fields = '__all__'
##extra_fields = ['rs','cluster','cluster_name']
extra_fields = ['rs','cluster','cluster_name']
My views code:
class BalanceList(views.APIView):
def get(self, request):
queryset = LoadBalance.objects.all()
serializer = LoadBalanceSerializer(queryset, many=True)
print(serializer.data)
return Response(serializer.data)
and request actual output:
[
{
"id": 2,
"cluster_name": null,
"cluster": [
{
"id": 1,
"cluster_name": "lvs_sz01",
"dip": "1.1.1.6",
"role": "BACKUP",
},
{
"id": 2,
"cluster_name": "lvs_sz01",
"dip": "1.1.1.5",
"role": "BACKUP",
}
],
"lb_name": "lb001",
"vip": "1.1.1.1",
"port": 80,
}
]
But the cluster_name filed value is same in the dictionary of lists .
I want the output to look like this:
[
{
"id": 2,
"cluster_name": "lvs_sz01",
"cluster": [
{
"dip": "1.1.1.6",
"role": "BACKUP",
},
{
"dip": "1.1.1.5",
"role": "BACKUP",
}
],
"lb_name": "lb001",
"vip": "1.1.1.1",
"port": 80,
}
]
How should I change it? Can you help me ?
I solved it myself with the following method:
class LoadBalanceSerializer(serializers.ModelSerializer):
cluster_name = serializers.SerializerMethodField()
cluster = LvsDeployReadOnly(many=True, read_only=True)
rs = RSInfoSerializer(many=True, read_only=True)
class Meta:
model = LoadBalance
fields = '__all__'
extra_fields = ['rs','cluster','cluster_name']
def get_cluster_name(self,obj):
print(type(obj.lb_name))
lvs_cluster = obj.cluster.all()
cluster_name = [ i.cluster_name for i in lvs_cluster ][0]
print(cluster_name)
return cluster_name
Thanks!

DRF Serializer How do I serialize my data and display

I have following Serializer I am facing problem with Json with serializing. I have user named daniel james and he have multiple subject like maths science I am providing nested serializer to fill all subject but based on subject users name also repeats below is more specific qsn
This is my model.
class Result(BaseModel):
semester_choices = (
('first', 'First'),
('second', 'Second'),
('third', 'Third'),
('fourth', 'Fourth'),
('fifth', 'Fifth'),
('sixth', 'Sixth'),
('seventh', 'Seventh'),
('eight', 'Eight'),
('all', 'All'),
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
registration_number = models.CharField(null=True, blank=True, max_length=20)
semester = models.CharField(max_length=50, choices=semester_choices, null=True, blank=True)
gpa = models.CharField(max_length=20)
subject = models.CharField(max_length=50)
serializers.py
class ResultSerializer(serializers.ModelSerializer):
class Meta:
model = Result
fields = '__all__'
class ListResultSerializer(ResultSerializer):
user = serializers.CharField()
semester = serializers.CharField()
subject = serializers.SerializerMethodField()
class Meta(ResultSerializer.Meta):
fields = (
'user',
'semester',
'subject',
)
def get_subject(self, instance):
return SubjectSerializer(instance).data
This is my SubjectSerializer
class SubjectSerializer(ResultSerializer):
class Meta(ResultSerializer.Meta):
fields = (
'gpa',
'subject'
)
And In my views.py I have done like this.
class ListResultView(rest_generics.ListAPIView, UserMixin):
serializer_class = serializers.ListResultSerializer
permission_classes = (AllowAny,)
def get_object(self):
return self.get_user()
def get_queryset(self):
return usecases.ListResultUseCase(
user=self.get_user()
).execute()
I use usecases.py to filter the data here is further code
class ListResultUseCase:
def __init__(self, user: User):
self._user = user
def execute(self):
self._factory()
return self._result
def _factory(self):
self._result = Result.objects.filter(user=self._user)
Now this is the Json I am getting right now from above code.
[
{
"user": "daniel james",
"semester": "first",
"subject": {
"gpa": "a+",
"subject": "maths"
}
},
{
"user": "daniel james",
"semester": "first",
"subject": {
"gpa": "A",
"subject": "data structures"
}
}
]
I want my json to be in this format
[
{
"user": "daniel james",
"semester": "first",
"subject": [
{
"gpa": "a+",
"subject": "maths"
},
{
"gpa": "A",
"subject": "data structures"
}
]
}
]
Any help in serializer?
You could create a seperate serializer to use with the nested serializer.
class SubjectNestedSerializer(serializers.ModelSerializer):
class Meta:
model = Subject
exclude = ['user']
P.S
Try to change your model like this
class Semester(BaseModel):
semester_choices = (
('first', 'First'),
('second', 'Second'),
('third', 'Third'),
('fourth', 'Fourth'),
('fifth', 'Fifth'),
('sixth', 'Sixth'),
('seventh', 'Seventh'),
('eight', 'Eight'),
('all', 'All'),
)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='semesters')
registration_number = models.CharField(null=True, blank=True, max_length=20)
semester = models.CharField(max_length=50, choices=semester_choices, null=True, blank=True)
class Subject(BaseModel):
semester = models.ForeignKey(Semester, on_delete=models.CASCADE, related_name='semester_subjects')
gpa = models.CharField(max_length=20)
subject = models.CharField(max_length=50)
You could create a Serializer for your user, and adding a method field that retrieve all the subjects of the user like this.
def get_subjects(self,instace):
subjects = Subject.objects.filter(user=instance)
return SubjectSerializer(subjects, many=True)
What you want is an aggregation of existing data to have a more meaningful result for frontend. I'd rather generating my own response than using a serializer for this.
Here is a simple example for you.
Let's say you want results for multiple users. (It's even easier if you want for one user)
def get_item_with_attribute_from_list(items, attribute, attribute_value):
"""
This is just a utility function that I use occasionally
"""
if items is None or len(items) == 0:
return None
if isinstance(items[0], dict):
for item in items:
if item.get(attribute) == attribute_value:
return item
else:
for item in items:
if getattr(item, attribute, None) == attribute_value:
return item
results = Result.objects.all().select_related('user')
response = []
for result in results:
user_dict = get_item_with_attribute_from_list(response, "user", result.user.name)
subject = {"gpa": "A", "subject": "data structures"}
if user_dict:
semester_list = user_dict['semesters']
semester = get_item_with_attribute_from_list(semester_list, 'semester_name', result.semester)
if semester:
semester['subjects'].append(subject)
else:
semester_list.append({'subjects': [subject]})
else:
response.append({'user': result.user.name, 'semesters': [{'semester_name': result.semester, 'subjects': [subject]}]})
Resulting JSON will be something like this.
[
{
"user": "daniel james",
"semesters": [
{
"semester_name": "first",
"subjects": [
{
"gpa": "a+",
"subject": "maths"
},
{
"gpa": "A",
"subject": "data structures"
}
]
},
{
"semester_name": "second",
"subjects": [
{
"gpa": "b+",
"subject": "geometry"
},
{
"gpa": "C",
"subject": "chemistry"
}
]
}
]
},
... # Other user's data
]
This way, you'll have more structured data, you'll be able to calculate SPA's for each semester easily.

Django Rest framework cant join tables

I am trying to fetch related data from a parent table using an API. I am trying to get the details from the operator table which has a one-to-one field with the user table.
After going through various answers I understood how to join tables but due to some reason I am unable to fetch the user data
serializer.py
class OpDetailsSerializer(DynamicFieldsModelSerializer):
user = UserSerializer(source="operator_details",many=False, read_only=True)
print(user.data)
class Meta:
model = OperatorDetails
fields = ('gst_no','pan','user')
models.py
class OperatorDetails(models.Model):
operator=models.OneToOneField(settings.AUTH_USER_MODEL,related_name="operator_details",on_delete=models.CASCADE,primary_key=True)
pan = models.CharField(max_length=10, unique=True, null=True,blank=True)
gst_no = models.CharField(max_length=15, unique=True,null=True,blank=True)
def __str__(self):
return str(self.operator)
views.py
def view_operator_info(request):
fields = ('operator','pan','gst_no','user')
operator = OperatorDetails.objects.get(operator__id=request.user.id)
serializer = OpDetailsSerializer(operator,fields=fields)
content = {
"status": True,
"response": status.HTTP_200_OK,
"message": "Operator details",
"data":serializer.data
}
return Response(content)
Actual Output
{
"status": true,
"response": 200,
"message": "Operator details",
"data": {
"gst_no": "test",
"pan": "test"
}
}
expected Output
{
"status": true,
"response": 200,
"message": "Operator details",
"data": {
"gst_no": "test",
"pan": "test",
"user":{
"email":.....
//data from user table
}
}
}
can anyone help. Thanks in advance
Your field name has to match the field on our model the other option is to use the source parameter.
class OpDetailsSerializer(DynamicFieldsModelSerializer):
user = UserSerializer(source="operator", many=False, read_only=True)
print(user.data)
class Meta:
model = OperatorDetails
fields = ('gst_no','pan','operator')
class OperatorDetails(models.Model):
operator = models.OneToOneField(settings.AUTH_USER_MODEL,related_name="operator_details",on_delete=models.CASCADE,primary_key=True)
pan = models.CharField(max_length=10, unique=True, null=True,blank=True)
gst_no = models.CharField(max_length=15, unique=True,null=True,blank=True)
def __str__(self):
return str(self.operator)
def user(Self):
return {
'email':self.operator.email,
'firstname':self.first_name,
'lastname':self.last_name
}
Add the method user in your model OperatorDetails and try it.
These thing helps me lot for making custom objects in my projects and share your review after you try these so i can can also take note.

How to create multiple model instances without duplicated writable nested serializer in Django REST Framework?

I have two related models: Product and ProductDescription. In 1 submit action user able to insert a new Product with multiple descriptions depend on the available languages. I use writable nested serializer to insert into Product and ProductDescription simultaneously. I do it by overriding create function in ProductDescriptionSerializer class, it works. However, I can only insert 1 ProductDescription at a time.
Then I tried to use this answer to create multiple model instances at once. The problem is it also creates the same Product twice instead of using the newly created Product Id to insert the next ProductDescription.
My models.py:
class Product(models.Model, ProductStatus):
product_code = models.CharField(max_length=6)
color = models.ForeignKey(ColorParent, on_delete=models.SET_NULL, null=True)
collection = models.ForeignKey(ProductCollection, on_delete=models.SET_NULL, null=True)
video = models.URLField(verbose_name='Video URL', max_length=250, null=True, blank=True)
status = models.CharField(max_length=20, choices=ProductStatus.status, default=ProductStatus.active)
class ProductDescription(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
language = models.ForeignKey(Language, on_delete=models.CASCADE)
description = models.TextField(max_length=500, null=True, blank=True)
def __str__(self):
return '%s - %s' % (self.product, self.language)
My serializers.py:
class CustomRelatedField(serializers.RelatedField):
def display_value(self, instance):
return instance
def to_representation(self, value):
return str(value)
def to_internal_value(self, data):
model = self.queryset.model
return model.objects.get(id=data)
class ProductSerializer(serializers.ModelSerializer):
collection = CustomRelatedField(queryset=ProductCollection.objects.all(), many=False)
color = CustomRelatedField(queryset=ColorParent.objects.all(), many=False)
class Meta:
model = Product
fields = ['id', 'product_code', 'collection', 'color', 'video', 'status']
class ProductDescriptionSerializer(serializers.ModelSerializer):
product = ProductSerializer()
language = CustomRelatedField(many=False, queryset=Language.objects.all())
class Meta:
model = ProductDescription
fields = ['id', 'product', 'language', 'description']
def to_representation(self, instance):
data = super().to_representation(instance)
if self.context['request'].method == 'GET':
data['product'] = instance.product.product_code
return data
return Serializer.to_representation(self, instance)
# The `.create()` method does not support writable nested fields by default.
def create(self, validated_data):
# create product data for Product model.
product_data = validated_data.pop('product')
product = Product.objects.create(**product_data)
# create ProductDescription and set product FK.
product_description = ProductDescription.objects.create(product=product, **validated_data)
# return ProductDescription instance.
return product_description
My views.py:
class CreateListModelMixin(object):
def get_serializer(self, *args, **kwargs):
if isinstance(kwargs.get('data', {}), list):
kwargs['many'] = True
return super(CreateListModelMixin, self).get_serializer(*args, **kwargs)
class ProductDescriptionView(CreateListModelMixin, viewsets.ModelViewSet):
permission_classes = [permissions.DjangoModelPermissions]
queryset = ProductDescription.objects.all()
serializer_class = ProductDescriptionSerializer
http_method_names = ['get', 'head', 'post', 'put', 'patch', 'delete']
The JSON format I use to POST data:
[
{
"product": {
"product_code": "BQ1080",
"collection": 5,
"color": 7,
"video": "https://www.youtube.com/watch?v=",
"status": "Continue"
},
"language": 1,
"description": "English description."
},
{
"product": {
"product_code": "BQ1080",
"collection": 5,
"color": 7,
"video": "https://www.youtube.com/watch?v=",
"status": "Continue"
},
"language": 2,
"description": "Vietnamese description."
}
]
It creates a duplicate Product in Product List:
[
{
"id": 26,
"product_code": "BQ1080",
"collection": 5,
"color": 7,
"video": "https://www.youtube.com/watch?v=",
"status": "Continue"
},
{
"id": 27,
"product_code": "BQ1080",
"collection": 5,
"color": 7,
"video": "https://www.youtube.com/watch?v=",
"status": "Continue"
}
]
The ProductDescription datas are correct though:
[
{
"id": 5,
"product": "BQ1080",
"language": "English",
"description": "English description."
},
{
"id": 6,
"product": "BQ1080",
"language": "Vietnam",
"description": "Vietnamese description."
}
]
To avoid duplicate product you can use get_or_create() method:
class ProductDescriptionSerializer(serializers.ModelSerializer):
...
def create(self, validated_data):
# create product data for Product model.
product_data = validated_data.pop('product')
product_code = product_data.pop("product_code")
product, _ = Product.objects.get_or_create(product_code=product_code, defaults=product_data)
# create ProductDescription and set product FK.
product_description = ProductDescription.objects.create(product=product, **validated_data)
# return ProductDescription instance.
return product_description
Note that get_or_create is prone to race condition. So if two same requests came to you service at the same time you may still have duplicate products.
I think you need to override your ProductSerializer's create method. Maybe you can try like this:
class ProductSerializer(serializers.ModelSerializer):
collection = CustomRelatedField(queryset=ProductCollection.objects.all(), many=False)
color = CustomRelatedField(queryset=ColorParent.objects.all(), many=False)
def create(self, validated_data):
instance, _ = Product.objects.get_or_create(**validated_data)
return instance
class Meta:
model = Product
fields = ['id', 'product_code', 'collection', 'color', 'video', 'status']
So that, first it will try to get if the Product exists, else create the instance(hence reducing duplicate entry).
ForeignKey not for this job you should use ManyToManyField

Get full fields in nested model. Django Rest Framework

I have 2 model classes:
class CustomUser(AbstractUser):
username = models.CharField(max_length=30, unique=True)
tags = models.ManyToManyField('events.Tag', related_name='user_tag', blank=True)
class Tag(models.Model):
name = models.CharField(unique=True, max_length=50)
And serializers:
class UserSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True)
class Meta:
...
class TagSerializer(serializers.ModelSerializer):
class Meta:
lookup_field = 'name'
model = Tag
fields = ('id', 'name')
When I do a get query I get something like this:
"data": {
"type": "CustomUser",
"id": "6",
"attributes": {
"username": "mercer",
},
"relationships": {
"tags": {
"data": [
{
"type": "Tag",
"id": "1"
}
]
},
}
}
What I want is to get Tag 'name' field in user representation:
"type": "Tag",
"id": "1",
"name":"name"
And I want to make patch query for adding tag to user.
I can use SerializerMethodField(), but this way I will not able to add tags
The problem was with settings of rest framework. I wrote there custom json render classes and recived this form of output. Now i removed it and everything is fine.

Categories