I am new to Django and have a json object whose data has to be split and stored in 3 different Django models. I'm trying to figure out if I'm doing this correctly in using the view file and serializers in Django.
The json object that I receive into the views file looks like below:
[
{
"contact_entry":
{
"employee_name" : "Tom Hanks",
"employee_type" : "Full-time permanent"
},
"address" :
{
"line1": "1435 Manhattan Ave",
"line2": "Apt 123",
"city": "New York",
"state": "New York",
"country":"US",
},
"phone_number" :
{
"work_number": "9901948626"
"home_number": "9908847555"
}
}
]
I have three django models as shown below:
class ContactEntry(models.Model):
employee_name = models.CharField(max_length=128)
employee_type = models.CharField(max_length=128)
class Address(models.Model):
contact_entry = models.ForeignKey(ContactEntry, on_delete=models.CASCADE)
line1 = models.CharField(max_length=128)
line2 = models.CharField(max_length=128)
city = models.CharField(max_length=128)
state = models.CharField(max_length=128)
country = models.CharField(max_length=128)
class PhoneNumber(model.Model):
contact_entry = models.ForeignKey(ContactEntry, on_delete=models.CASCADE)
work_number = models.IntegerField(max_length=10)
home_number = models.IntegerField(max_length=10)
Each of them has a ModelSerializer as shown below:
class ContactEntrySerializer(serializers.ModelSerializer):
class Meta:
model = ContactEntry
fields = '__all__'
class AddressSerializer(serializers.ModelSerializer):
class Meta:
model = Address
fields = '__all__'
class PhoneNumberSerializer(serializers.ModelSerializer):
class Meta:
model = PhoneNumber
fields = '__all__'
I have defined one serializer (inherited from serializers.serializer) that combines the above 3 serializers as shown below:
class CombinedSerializer(serializers.Serializer):
contact_entry = ContactEntrySerializer()
phone_number = PhoneNumberSerializer()
address = AddressSerializer()
The post method in my view is as shown below:
class CombinedContactView(APIView):
def post(self, request, *args, **kwargs):
# Validate JSON data with the serializer
serializer = CombinedSerializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
validated_data = serializer.validated_data.pop()
contact_entry_data = validated_data['contact_entry']
address_data = validated_data['address']
phone_number_data = validated_data['phone_number']
# Create model for contact book entry
contact_entry_model = ContactEntry(**contact_entry_data)
contact_entry_model.save()
# Create model for address
address_data['contact_entry'] = contact_entry_model
address_model = Address(**address_data)
address_model.save()
# Create model for phone_number
phone_number_data['contact_entry'] = contact_entry_model
phone_number_model = PhoneNumber(**phone_number_data)
phone_number_model.save()
return HttpResponse(status=201)
The above code actually runs fine. I see the contact_entry object created and the address and phone_numbers correctly created (with a foreign key relationship to the contact_entry object). However, I'm concerned that I'm doing this in a roundabout way with lots of unnecessary code in my views file. Is there a more straightforward way to do this?
if you change your serializers to these you'll be OK:
class ContactEntrySerializer(serializers.ModelSerializer):
class Meta:
model = ContactEntry
fields = '__all__'
class AddressSerializer(serializers.ModelSerializer):
contact_entry = ContactEntrySerializer(required=False)
class Meta:
model = Address
fields = '__all__'
class PhoneNumberSerializer(serializers.ModelSerializer):
contact_entry = ContactEntrySerializer(required=False)
class Meta:
model = PhoneNumber
fields = '__all__'
just notice that i changed contact_entry on AddressSerializer and PhoneNumberSerializer to required=False so you can pass contact_entry validation and these two serializers. and after it your view do the rest.
Related
Hi I want to deserialize only using 1 field. However, I want to serialize it as an object depending on the model.
Suppose I have:
#models.py
class Product(models.Model):
name = models.CharField()
amount = models.IntegerField()
description = models.TextField()
class Query(models.Model):
name = models.CharField()
product = models.ForeignKey(Product)
...
#serializers.py
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = '__all__'
class QuerySerializer(serializers.ModelSerializer):
product = ProductSerializer()
class Meta:
model = Query
fields = '__all__'
I want to POST/deserialize something like this on the QuerySerializer:
{
"name": "Query 1",
"product": "Banana",
...
}
and I want something like this in return for serializer:
{
"name": "Query 1",
"product": {
"name": "Banana",
"amount": 1,
"description": "Banana description"
}
...
}
I know a way is overriding to_internal_value but I do not like it since it messes up with ValidationErrrors.
I also get this as a result:
{'product': {'non_field_errors':
['Invalid data. Expected a dictionary, but got str.']}}
First of all, make the name field of Product as unique to avoid unnecessary complications.
class Product(models.Model):
name = models.CharField(unique=True)
amount = models.IntegerField()
description = models.TextField()
and change your serializer as,
class QuerySerializer(serializers.ModelSerializer):
product = serializers.CharField(write_only=True)
class Meta:
model = Query
fields = '__all__'
def create(self, validated_data):
product_name = validated_data.pop('product')
product_instance = Product.objects.get(name=product_name)
return Query.objects.create(product=product_instance, **validated_data)
def to_representation(self, instance):
rep = super().to_representation(instance)
rep['product'] = ProductSerializer(instance.product).data
return rep
Reference: DRF: Simple foreign key assignment with nested serializers?
In my models.py there are two models:
class Genre(models.Model):
genre_id = models.CharField(max_length=10)
name = models.CharField(max_length=40)
information = models.CharField(max_length=120)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.CharField(max_length=100)
genre = models.ForeignKey(Genre, on_delete=models.CASCADE)
def __str__(self):
return self.title
They are serialized:
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ('title', 'author', 'genre')
class GenreSerializer(serializers.ModelSerializer):
class Meta:
model = Genre
fields = ('name', 'information')
and ViewSets are created for each:
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
class GenreViewSet(viewsets.ModelViewSet):
queryset = Genre.objects.all()
serializer_class = GenreSerializer
What I'd like to do is:
Sending a POST request to books/ endpoint. Sent data has to contain existing genre ID, it won't be saved to the database otherwise (it's done by default already).
Receiving information from the Genre model as a response.
Let me give a short example:
I'm sending this JSON:
{
"title": "Hercules Poirot",
"author": "Agatha Christie",
"genre": 1
}
Instead of repeated request from above I receive something like this:
{ "genre": "crime story" }
How to do this?
What you can do is add a custom create method within your BookViewSet to override the return statement.
You can take exemple on the default create method which is implemented within CreateModelMixin (https://github.com/encode/django-rest-framework/blob/master/rest_framework/mixins.py#L12)
You could tell how the nested field will work. Like this:
class GenreSerializer(serializers.ModelSerializer):
class Meta:
model = Genre
fields = ('name', 'information')
class BookSerializer(serializers.ModelSerializer):
genre = GenreSerializer(read_only=True)
class Meta:
model = Book
fields = ('title', 'author', 'genre')
Using Django 2.1/Django Rest Framework.
I am receiving the Model object output from DRF instead of the actual fields.
I would like to recieve all the items for both the audio_links and release_artists tables.
Here's what I have.
Output
{
"title": "Attack The Dancefloor Volume Two",
"audiolinks": [
"AudioLinks object (55708)",
"AudioLinks object (55709)",
"AudioLinks object (55710)",
"AudioLinks object (55711)"
],
"releaseartists": [
"ReleaseArtists object (140)",
"ReleaseArtists object (141)"
]
}
models.py
class AudioLinks(models.Model):
release = models.ForeignKey('ReleasesAll', models.DO_NOTHING, db_column='release_id', related_name='audiolinks')
track_number = models.IntegerField()
class Meta:
managed = False
db_table = 'audio_links'
class ReleaseArtists(models.Model):
release = models.ForeignKey('ReleasesAll', models.DO_NOTHING, db_column='release_id', related_name='releaseartists')
artists = models.CharField(max_length=100)
class Meta:
managed = False
db_table = 'release_artists'
views.py
class ListReleaseDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = ReleasesAll.objects.all()
serializer_class = ReleasesSerializer
def get(self, request, *args, **kwargs):
try:
a_release = self.queryset.prefetch_related('releaseartists','audiolinks').get(pk=kwargs['release_id'])
return Response(ReleasesSerializer(a_release).data)
except ReleasesAll.DoesNotExist:
return Response(
data = {
"message": "{} does not exist".format(kwargs["release_id"])
},
status=status.HTTP_404_NOT_FOUND
)
serializers.py
class ReleasesSerializer(serializers.ModelSerializer):
audiolinks = serializers.StringRelatedField(many=True)
releaseartists = serializers.StringRelatedField(many=True)
class Meta:
model = ReleasesAll
fields = ('title','audiolinks','releaseartists')
serializers.stringRelatedField gives the output of the __str__ method defined on the Model.
Hence you are getting this "AudioLinks object (55708)" and ReleaseArtists object (141), which is the default __str__ representation of any model.
In order to get all fields, you must define serializers for every other model that is related to the required fields as such:
class AudioLinksSerializer(serializers.ModelSerializer):
class Meta:
model = AudioLinks
fields = ('__all__')
class ReleaseArtistsSerializer(serializers.ModelSerializer):
audiolinks = serializers.StringRelatedField(many=True)
releaseartists = serializers.StringRelatedField(many=True)
class Meta:
model = ReleaseArtists
fields = ('__all__')
and then:
class ReleasesSerializer(serializers.ModelSerializer):
audiolinks = AudioLinksSerializer(many=True)
releaseartists = ReleaseArtistsSerializer(many=True)
class Meta:
model = ReleasesAll
fields = ('title','audiolinks','releaseartists')
That would do enough for you to get all fields.
***And also as of Django convention, you need to name the models as in singular form i.e, AudioLink, ReleaseArtist. This way you see the model as a single object. And it will be easier when you try to get audiolinks as you will be thinking like "there are many AudioLink objects and I am getting few of them"
I have a Product class that has "source products"; I use a many-to-many field with a through model to represent it (the database tables already exist, and that's the only way I found to configure the models):
class Product(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
product_name = models.TextField()
source_products = models.ManyToManyField('self', symmetrical=False, related_name='derived_products', through='Link', through_fields=('product', 'source'),)
class Meta:
db_table = 'product'
managed = False
class Link(models.Model):
product = models.ForeignKey('Product', models.CASCADE, db_column='uuid', related_name='source_links')
source = models.ForeignKey('Product', models.CASCADE, db_column='source_uuid', related_name='+')
class Meta:
db_table = 'link'
unique_together = (('product', 'source'),)
managed = False
My serializer is dead simple:
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ('uuid', 'product_name', 'source_products', )
A GET request returns:
{
"product_name": "dummy.fra",
"source_products": [
"17b021e7-3d6b-4d29-a80b-895d62710080"
],
"uuid": "48c5a344-877e-4e3f-9a4b-2daa136b68fe"
}
The since ManyToManyFields with a Through Model are read-only, how do I go about to create a product including the link between products? I'd like to send a POST request that follows the same format as the GET response (i.e., a source_products field that lists UUIDs of existing products).
You can use PrimaryKeyRelatedField but you have to write custom create method.
class ProductSerializer(serializers.ModelSerializer):
source_products = serializers.PrimaryKeyRelatedField(many=True, queryset=Products.objects.all())
class Meta:
model = Product
fields = ('uuid', 'product_name', 'source_products', )
def create(self, validated_data):
source_products = validated_data.pop('source_products', [])
product = Product.objects.create(**validated_data)
for source in source_products:
product.source_products.add(source)
return product
Your data will be like this
{
"product_name": "dummy.fra",
"source_products": [
"17b021e7-3d6b-4d29-a80b-895d62710080"
],
"uuid": "48c5a344-877e-4e3f-9a4b-2daa136b68fe"
}
The serializer has to override create() and access the unvalidated GET parameters (self.context['request'].data) directly:
class ProductSerializer(serializers.ModelSerializer):
def create(self, validated_data):
# extract sources data
unvalidated_data = self.context['request'].data
sources_data = unvalidated_data.get('source_products', [])
# save objects
instance = Product.objects.create(**validated_data)
for uuid in sources_data:
source = Product.objects.get(pk=uuid)
instance.source_links.create(source=source)
return instance
class Meta:
model = Product
fields = ('uuid', 'product_name', 'source_products', )
I have the model structure like below
class BaseProduct:
id = models.CharField(max_length=15)
name = models.CharField(max_length=20)
class Product
base_product = ForeigKey(BaseProduct)
name = models.CharField(max_length=20)
class Condition:
category = models.ForeignKey(Product, related_name='allowed_product')
check = models.IntegerField(default=0)
allow = models.PositiveSmallIntegerField(default=1)
The query:
Product.objects.filter(condition__allow=1, condition__check=1)
I want format something like below
Base Product and inside that list of products based on allow and check filter
[
{
"name": "BaseProduct 1",
"products": [
{
"name": "TV",
}, {}, ....
]
},
........
]
Try it, if you use django rest framework
from rest_framework import serializers
from rest_framework.fields import empty
from django.utils.functional import cached_property
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ('name')
class BaseProductSerializer(serializers.ModelSerializer):
products = serializers.SerializerMethodField()
class Meta:
model = BaseProduct
fields = ('name', 'products')
def __init__(self, instance=None, data=empty, **kwargs):
self._condition_allow = kwargs.pop('condition_allow', 1)
super(BaseProductSerializer, self).__init__(instance=None, data=empty, **kwargs)
#cached_property
def _request_data(self):
request = self.context.get('request')
# if POST
# return request.data if request else {}
# if GET params
return request.query_params if request else {}
#cached_property
def _condition(self):
return self._request_data.get('CONDITION_PARAM_NAME')
def get_products(self, obj):
qs = obj.product_set.filter(condition__allow=self._condition_allow, condition__check=1)
serializer = ProductSerializer(qs, many=True)
# ^^^^^
return serializer.data
in view
serialiser(qs, condition_allow=5)
Change your models to have related_name for the foreignkeys to have reverse relationship:
class BaseProduct:
id = models.CharField(max_length=15)
name = models.CharField(max_length=20)
class Product
base_product = ForeigKey(BaseProduct, related_name='products')
name = models.CharField(max_length=20)
class Condition:
category = models.ForeignKey(Product, related_name='conditions')
check = models.IntegerField(default=0)
allow = models.PositiveSmallIntegerField(default=1)
So now you can use it in your serializers:
class BaseProductSerializer:
class Meta:
model = BaseProduct
fields = ('name', 'products',)
class ProductSerializer:
class Meta:
model = Product
fields = ('conditions',)
class ConditionSerializer:
class Meta:
model = Condition
fields = '__all__'
Finally in your views, change this:
Product.objects.filter(condition__allow=1, condition__check=1)
into this:
BaseProduct.objects.filter(products__conditions__allow=1, products__conditions__allow=1)
And hopefully, this should generate JSON data in the format that you asked for.