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
Related
I've got established a relationship between 2 models: Order and OrderLine. I've created serializers for both of them following the DRF documentation, yet when printing the serializer.data the nested objects don't show.
Here are my models:
class Order(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
session_id = models.CharField(max_length=256)
class OrderLine(models.Model):
order_id = models.ForeignKey(Order, on_delete=models.DO_NOTHING)
product_id = models.ForeignKey(Product, on_delete=models.DO_NOTHING)
price = models.DecimalField(decimal_places=2, max_digits=20)
quantity = models.IntegerField()
total = models.DecimalField(decimal_places=2, max_digits=20)
created_at = models.DateField(auto_now_add=True)
updated_at = models.DateField(auto_now=True)
These are the serializers:
from rest_framework import serializers
from .models import Order, OrderLine
class OrderLineSerializer(serializers.ModelSerializer):
"""
OrderLine serializer
"""
class Meta:
model = OrderLine
fields = ['product_id', 'price', 'quantity', 'total']
class OrderSerializer(serializers.ModelSerializer):
"""
Order serializer
"""
items = OrderLineSerializer(many=True, read_only=True)
class Meta:
model = Order
fields = ['session_id', 'subtotal', 'total', 'items']
read_only_fields = ['id']
and this is the view:
class OrderAPIViewSet(viewsets.ViewSet):
def create(self, request):
order = Order.objects.create(session_id=request.data['session_id'])
for item in request.data['items']:
product = Product.objects.get(pk=item['product_id'])
total = Decimal(item['price'] * item['quantity'])
OrderLine.objects.create(
order_id=order,
product_id=product,
price=Decimal(item['price']),
quantity=item['quantity'],
total=total
)
serializer = OrderSerializer(instance=order)
print("HERE")
print(serializer.data)
return Response(status=status.HTTP_200_OK)
From my REST client I'm posting the following object:
{
"session_id":uuid,
"items": [
{
"product_id": product.id,
"price": 5.80,
"quantity": 2,
}
]
}
but when the print statement in the view is executed this is what's being printed:
{
"session_id":"4def7bdb-dedb-46aa-9c70-1d9e4f522149",
"subtotal":"0.00",
"total":"0.00"
}
notice the the items subresource is not being included.
What am i missing?
Because you passed read_only=True parameter to OrderLineSerializer. When your serializer's builtin method to_internal_value() run, clear data not including in your fields list. For more details: read_only
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
My goal is to include the name attribute of the Brand model a Product references through models.ForeignKey, when I make a get request for products. Exactly what this piece of code returns in the python shell:
Product.objects.all().values('name', 'brand__name',)
returns this:
[
{'name': 'B-1', 'brand__name': 'B-brand'},
{'name': 'B-2', 'brand__name': 'B-brand'},
{'name': 'C-1', 'brand__name': 'C-brand'}
]
Im already using django-filters to filter my get requests.
Models:
class Brand(models.Model):
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class Product(models.Model):
name = models.CharField(max_length=255)
brand = models.ForeignKey(Brand, on_delete=models.CASCADE, default=None)
def __str__(self):
return self.name
Serializers:
class BrandSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Brand
fields = ('id', 'url', 'name')
class ProductSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Product
fields = ('id', 'url', 'name', 'brand')
Filter:
class ProductFilter(filters.FilterSet):
name = filters.CharFilter(lookup_expr = 'icontains')
brand__name = filters.CharFilter(lookup_expr = 'icontains')
class Meta:
model = Product
fields = ('name' 'brand__name',)
View:
class ProductView(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filterset_class = ProductFilter
with brand__name in the filterSet, i am able to reference the name of the brand model a certain product is referencing and retrieve it. My goal is to also include that same name of the Brand along with the attributes of a product when I make the get request, which currently only yields the url/reference of the brand (along with all other atributes of Product).
If you want to return as a flat dictionary, you can do like this.
class ProductSerializer(serializers.HyperlinkedModelSerializer):
brand_name = serializer.CharField(source="brand__name")
class Meta:
model = Product
fields = ('id', 'url', 'sku', 'name', 'brand_name', 'price')
Ive solved my own issue, by defining brand as brandSerializer in ProductSerializer, i was able to return the whole brand object along with the product info, just like this:
class ProductSerializer(serializers.HyperlinkedModelSerializer):
brand = BrandSerializer()
class Meta:
model = Product
fields = ('id', 'url', 'sku', 'name', 'brand', 'price')
There are two Django models - ClientCompany & Proposal and the foreign key of ClientCompany is within the Proposal model. In Proposal how do I display the name of the ClientCompany instead of the foreign key id?
models.py:
class ClientCompany(models.Model):
name = models.CharField("Client Name", max_length=255)
abn_acn = models.BigIntegerField("ABN / ACN")
def __str__(self):
return self.name
class Proposal(models.Model):
proj_name = models.CharField("Project Name", max_length=255)
loc_state = models.CharField(
max_length=3,
)
proj_type = models.CharField(
max_length=30,
)
prop_status = models.CharField(
max_length=20,
)
client = models.ForeignKey(ClientCompany, on_delete=models.CASCADE)
oneic = models.ForeignKey(
User, on_delete=models.CASCADE, related_name='main_engineer')
twoic = models.ForeignKey(
User, on_delete=models.CASCADE, related_name='second_engineer')
created_at = models.DateTimeField(default=datetime.now)
def __str__(self):
return self.proj_name
serializers.py:
class ClientSerializer(serializers.ModelSerializer):
class Meta:
model = ClientCompany
fields = ('id', 'name', 'abn_acn')
class ProposalSerializer(serializers.ModelSerializer):
class Meta:
model = Proposal
fields = ('id', 'proj_name', 'loc_state', 'proj_type', 'prop_status', 'client', 'oneic', 'twoic',)
queryset api.py:
class ProposalViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticated, ]
queryset = Proposal.objects.all()
serializer_class = ProposalSerializer
currentlyshows the client foreign key id
I've been stuck on this, tried to apply the existing solutions recommended for similar problems but had no luck... if someone can tell me what I'm missing - thanks
I found this worked in the end by adding the serializers.SlugRelatedField line in serializers.py:
class ProposalSerializer(serializers.ModelSerializer):
client = serializers.SlugRelatedField(slug_field="name", read_only=True)
class Meta:
model = Proposal
fields = ('id', 'proj_name', 'loc_state', 'proj_type', 'prop_status',
'client', 'oneic', 'twoic',)
Update your serializer like this:
class ProposalSerializer(serializers.ModelSerializer):
client = ClientSerializer()
class Meta:
model = Proposal
fields = ('id', 'proj_name', 'loc_state', 'proj_type', 'prop_status', 'client', 'oneic', 'twoic',)
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', )