Django REST Framework - pass related field in POST data - django

I'm using Django 2.0 and Django REST Framework
I have a model to store contact's address like
class ContactAddress(models.Model):
contact = models.ForeignKey(Contact, on_delete=models.CASCADE)
city = models.CharField(max_length=250)
postal = models.CharField(max_length=6)
state = models.ForeignKey(State, on_delete=models.PROTECT, blank=True)
I have two more models to store state and country
class Country(models.Model):
name = models.CharField(max_length=100, unique=True, verbose_name='Country Name')
class State(models.Model):
country = models.ForeignKey(Country, on_delete=models.PROTECT)
name = models.CharField(max_length=100, verbose_name='State Name')
I want to pass state field with POST data while creating a new address record
app/serializers.py
class ContactAddressSerializer(serializers.ModelSerializer):
class Meta:
model = ContactAddress
depth = 2
fields = (
'id', 'city', 'postal', 'state'
)
def create(self, validated_data):
print(validated_data)
and my POST data
[
{
"city": "City Name",
"postal": "110011",
"state": "Bihar"
}
]
But there is no state data in validated data. Printing validate_data return
def create(self, validated_data):
print(validated_data)
{'city': 'City Name', 'postal': '110011', 'contact': <Contact: contact_object>}
How can I pass POST data for related field?

state property has foreign key to another model. If you want to use state property in serializer class, cannot behave like simple property
First, write a serializer for state model, then use this serializer class in contact address class
class StateSerializer(serializers.ModelSerializer):
class meta:
Fields = ('name')
class ContactAddressSerializer(serializers.ModelSerializer):
state = StateSerializer()
class Meta:
model = ContactAddress
depth = 2
fields = (
'id', 'city', 'postal', 'state'
)
def create(self, validated_data):
print(validated_data)

Related

Dynamic Serialization for related fields

I have the following models:
class Country(models.Model):
"""
Country model
"""
# With name and isoCode (charfield)
...
class State(models.Model):
"""
State model
"""
# With name and isoCode (charfield)
country = models.ForeignKey(Country, on_delete=models.CASCADE)
...
class City(models.Model):
"""
City model
"""
name = models.CharField(max_length=32)
state = models.ForeignKey(State, on_delete=models.CASCADE)
country = models.ForeignKey(Country, on_delete=models.CASCADE)
...
And UserLocation referenced by:
class Location(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="location")
country = models.ForeignKey(Country, on_delete=models.CASCADE)
state = models.ForeignKey(State, on_delete=models.CASCADE)
city = models.ForeignKey(City, on_delete=models.CASCADE)
How do I build a serializer that creates UserLocation, as well as return the info in JSON?
I have tried
class LocationSerializer(serializers.ModelSerializer):
country = serializers.SlugRelatedField(slug_field="isoCode", queryset=Country.objects.all())
state = serializers.SlugRelatedField(slug_field="isoCode", queryset=State.objects.filter(country__isoCode=country))
city = serializers.SlugRelatedField(slug_field="name", queryset=City.objects.all())
class Meta:
model = Location
fields = ["user", "country", "state", "city"]
But it does not work, it gives the error
{"state":["Object with isoCode=BC does not exist."],...
How does one create a dynamic linked serializer? Or how does one work around this?
I think you want following serializer
class LocationSerializer(serializers.ModelSerializer):
country = serializers.ReadOnlyField(source="country.isocode")
state = serializers.ReadOnlyField(source="state.isocode")
city = serializers.ReadOnlyField(source="city.name")
class Meta:
model = Location
fields = ["user", "country", "state", "city"]
However, it seems your model design is incorrect.
You don't need to have country, state foreign keys in Location model, since City model has state, State model has country.
For future users. I found that
class LocationSerializer(serializers.ModelSerializer):
'''Used to serialize user location'''
country = serializers.SlugRelatedField(slug_field="isoCode", queryset=Country.objects.all(), required=True)
class Meta:
model = Location
fields = ["user", "country"]
extra_kwargs = {"user": { "validators":[UniqueValidator(queryset=Location.objects.all(), message=_("User already has a location. Please update user location. Hint: use PUT request"))]}}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if "country" in kwargs['context']['request'].data and len(Country.objects.get(isoCode=self.initial_data["country"]).state_set.all()) > 0:
self.fields["state"] = serializers.SlugRelatedField(slug_field="isoCode", queryset=State.objects.filter(country__isoCode=self.initial_data["country"]), required=True)
if "state" in kwargs['context']['request'].data and len(State.objects.get(isoCode=self.initial_data["state"], country__isoCode=self.initial_data["country"]).city_set.all()) > 0:
self.fields["city"] = serializers.SlugRelatedField(slug_field="name", queryset=City.objects.filter(state__isoCode=self.initial_data["state"],country__isoCode=self.initial_data["country"]), required=True)
Seemed to work for my case

django-rest-framework access field inside serializer

So I have a model like this
class DataSheet(BaseModel):
"""
Represents a single dataSheet.
dataSheets have their own model at the core. Model data is added to
the dataSheets in the form of separate records.
"""
class Meta:
verbose_name = 'datasheet'
verbose_name_plural = 'datasheets'
ordering = ['position', 'cluster']
required_db_features = {
'supports_deferrable_unique_constraints',
}
constraints = [
models.UniqueConstraint(
fields=['position', 'cluster'],
name='deferrable_unique_datasheet_position',
deferrable=models.Deferrable.DEFERRED
)
]
def __str__(self):
return self.name
objects = managers.DataSheetsManager()
positions = managers.PositionalManager()
position = models.PositiveSmallIntegerField(db_index=True, editable=True)
name = models.CharField(max_length=100, validators=[MinLengthValidator(2)], db_index=True)
description = models.CharField(max_length=1024, null=True, blank=True, db_index=True)
owner = models.ForeignKey('api_backend.Member', on_delete=models.CASCADE, db_index=True, editable=False)
fields = models.ManyToManyField('api_backend.Field')
overwrites = models.ManyToManyField('api_backend.RoleOverwrite')
parent = models.ForeignKey('api_backend.Category', on_delete=models.CASCADE, null=True, blank=True)
cluster = models.ForeignKey('api_backend.Cluster', on_delete=models.CASCADE, editable=False)
REQUIRED_FIELDS = [name, owner, cluster]
and a serializer like this
class DataSheetSerializer(serializers.ModelSerializer):
"""
A serialized DataSheet Object.
Datasheets have their own:
- array of fields
- array of role-overwrites
"""
def get_fields(self):
fields = super(DataSheetSerializer, self).get_fields()
fields['parent'].queryset = self.cluster.categories.all()
return fields
class Meta:
model = DataSheet
read_only_fields = ['position']
fields = '__all__'
# need to make sure that the parent category of the datasheet
# belongs to the datasheet's cluster only.
fields = partial.PartialFieldSerializer(many=True, read_only=True)
overwrites = partial.PartialOverWriteSerializer(many=True, read_only=True)
the thing is, I want to access the serializer model's cluster field inside of the get_fields method. However, I couldn't do the same. Can someone help me?
I've seen other answers involving initial_data, but that doesn't work here.
fields['parent'].queryset = self.cluster.categories.all()
cluster is an unresolved reference here.
self in get_fields is DataSheetSerializer instance not DataSheet model instance. hence it should not have cluster property. you can not access model DataSheet instance in get_fields as it gets fields from class DataSheet not from its instance. you can validate the field like
class DataSheetSerializer(serializers.ModelSerializer):
# ... other code
def validate(self, data):
parent = data.get('parent')
# check if parent is valid i.e in queryset
# if yes return data
# else raise serializers.validationError

AttributeError at /api/module/list/

I want many to many fields to be displayed in module serializer instead of id, these are my serializers
class TrainerSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', ]
class ModuleSerializer(serializers.ModelSerializer):
trainer = serializers.CharField(source='trainer.username')
class Meta:
model = Module
fields = ['id', 'title', 'duration', 'trainer',
'publish_choice']
class Trainer(models.Model):
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
def __str__(self):
return str(self.user)
class Meta:
ordering = ['pk']
class Module(models.Model):
title = models.CharField(max_length=80, unique=True)
duration = models.IntegerField(verbose_name='Duration in Days/ Weeks', blank=True, null=True)
trainer = models.ManyToManyField(Trainer, blank=True)
detail = models.TextField(verbose_name='Program Details', blank=True, null=True)
notify = models.BooleanField(default=False)
publish_choice = models.CharField(verbose_name='Publish/ Draft',
max_length=80, choices=PUBLISH_CHOICES, default='publish')
and this is the error message
Got AttributeError when attempting to get a value for field trainer on serializer ModuleSerializer.
The serializer field might be named incorrectly and not match any attribute or key on the Module instance.
Original exception text was: 'ManyRelatedManager' object has no attribute 'username'.
We have a depth parameter in the serializer MetaClass. we can make use of it like below. depth=1 will retrieve all fields of a relation.
class ModuleSerializer(serializers.ModelSerializer):
class Meta:
model = Module
fields = ['id', 'title', 'duration', 'trainer', 'publish_choice']
depth = 1
for reference DRF-Documentation on serializers
Its raise exception because serializers.CharField(source='trainer.username') not match ManyRelatedManager in model trainer = models.ManyToManyField(Trainer, blank=True).
If you want get all username instead of id, you can try add Custom type serialzier like this:
class ModuleSerializer(serializers.ModelSerializer):
trainer = serializers.SerializerMethodField()
def get_trainer(self, obj):
results = []
for item in obj.trainers.all():
results.append(item.username)
return results
class Meta:
model = Module
fields = ['id', 'title', 'duration', 'trainer', 'publish_choice']
trainer will return array of username relation with Module

Django: error using get_or_create and unique_together

I'm creating an API which list and save transactions. My Transactions Model has a FK to a Category model. My goal when creating a Transaction is to also create the Category if the category is new.
The first time I create the transaction it successfully creates the new category and transaction. The next time I attempt the create a transaction with the existing category, I get an error that the category already exists. In my Transaction serializer I added a create method that should be using get_or_create for the category. However I'm still getting an error on my unique fields. My expectation is that it would be returning the existing Category.
It seems like it's throwing the error before it gets to the create method in the Transaction serializer before it has a chance to use get_or_create.
Models:
class Category(models.Model):
name = models.CharField(max_length=128, null=False)
owner = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
created_time = models.DateTimeField(auto_now_add=True)
modified_time = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ('name', 'owner')
class Transaction(models.Model):
date = models.DateField()
payee = models.CharField(max_length=256)
category = models.ForeignKey(Category,
related_name='category',
on_delete=models.CASCADE)
amount = MoneyField(max_digits=19,
decimal_places=2,
default_currency='USD')
created_time = models.DateTimeField(db_index=True,
auto_now_add=True)
modified_time = models.DateTimeField(auto_now=True)
Serializers:
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('id', 'name', 'owner', 'created_time', 'modified_time')
class TransactionSerializer(serializers.ModelSerializer):
balance = serializers.DecimalField(
decimal_places=2, max_digits=19, read_only=True)
category = CategorySerializer(many=False, read_only=False)
class Meta:
model = Transaction
fields = ('id', 'date', 'payee', 'category',
'amount', 'balance', 'created_time', 'modified_time',
'is_cleared', 'paid_or_deposited')
def create(self, validated_data):
category_data = validated_data.pop('category')
category, created = Category.objects.get_or_create(**category_data)
transaction = Transaction.objects.create(category=category,
**validated_data)
return transaction
POST:
{
"date": "2018-12-19",
"payee": "Test",
"category": {"owner": 1, "name": "TEST"},
"amount": "-134"
}
Error:
{
"category": {
"non_field_errors": [
"The fields name, owner must make a unique set."
]
}
}
You're right about not reaching your create() method.
This happens because ModelSerializer by default creates validators based on your model Meta.unique_together value: https://www.django-rest-framework.org/api-guide/serializers/#modelserializer
Simplest way to disable this type of validators is to override get_unique_together_validators for your serializer:
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('id', 'name', 'owner', 'created_time', 'modified_time')
def get_unique_together_validators(self):
return []
Another solution, which is cleaner is to override Meta.validations of your CategorySerializer*:
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('id', 'name', 'owner', 'created_time', 'modified_time')
validators = []
* be aware that this will disable serializer validators unique_for_date, unique_for_month and unique_for_year that come from model

How can I make a writable ManyToManyField with a Through Model in Django Rest Framework?

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', )