I got following models:
class OrderItem(models.Model):
ordered_amount = models.IntegerField(validators=[MinValueValidator(0)])
amount = models.IntegerField(default=0)
order = models.ForeignKey(
Order, on_delete=models.CASCADE, related_name="order_items"
)
class Order(models.Model):
reference = models.CharField(max_length=50)
purchase_order = models.CharField(max_length=15, blank=True, null=True)
I'm now writing a serializer for listing orders. In this OrderSerializer I need to access amount and ordered_amount in the OrderItem class. How do I do this?
This is What I have now:
class AdminOrderListSerializer(serializers.ModelSerializer):
amount = serializers.IntegerField()
ordered_amount = serializers.IntegerField()
class Meta:
model = Order
fields = [
"purchase_order",
"reference",
"amount",
"ordered_amount",
]
# noinspection PyMethodMayBeStatic
def validate_amount(self, order):
if order.order_items.amount:
return order.order_items.amount
return
# noinspection PyMethodMayBeStatic
def validate_ordered_amount(self, order):
if order.order_items.ordered_amount:
return order.order_items.ordered_amount
return
This gives me following error:
AttributeError: Got AttributeError when attempting to get a value for field amount on serializer AdminOrderItemListSerializer.
The serializer field might be named incorrectly and not match any attribute or key on the Order instance.
Original exception text was: 'Order' object has no attribute 'amount'.
There are many ways to that, one of them is SerializerMethodField:
from django.db.models import Sum
class AdminOrderListSerializer(serializers.ModelSerializer):
amount = serializers.SerializerMethodField()
ordered_amount = serializers.SerializerMethodField()
def get_amount(self,obj):
return obj.order_items.aggregate(sum=Sum('amount'))['sum']
def get_ordered_amount(self,obj):
return obj.order_items.aggregate(sum=Sum('order_amount'))['sum']
Optimized solution
Another way of achieving this is to annotate the data to queryset, and access them in serializer. For that, you need to change in view:
class SomeView(ListAPIView):
queryset = Order.objects.annotate(amount=Sum('order_items__amount'),order_amount=Sum('order_items__order_amount'))
This is a optimized solution because it reduces database hits(it only hits once).
Related
I have a TabularInline class, StockPartInlines in the inline form view I am trying to populate with a query that GROUPs based on two fields (container_id, expiration_date) and adds an additional field that is a SUM of an existing field.
class Stock(models.Model):
part = models.ForeignKey(Part,
on_delete=models.PROTECT,
related_name='stock')
container = models.ForeignKey(StorageContainer,
on_delete=models.PROTECT,
related_name='items',
null=True)
quantity_change = models.IntegerField(default=0)
expiration_date = models.DateField(blank=True, null=True)
class PartAdmin(admin.ModelAdmin):
inlines = [StockPartInlines]
class StockPartInlines(admin.TabularInline):
model = Stock
fields = ['container', 'expiration_date', 'quantity']
readonly_fields = ['quantity']
def quantity(self, obj):
return obj._quantity
The equivalent SQL statement would be;
SELECT
part_id,
container_id,
expiration_date,
SUM(quantity_change)
FROM
inventory_stock
WHERE
part_id = part_id
GROUP BY
container_id,
expiration_date;
I have tried to get this to work by overriding the 'get_queryset()' method of StockPartInlines
def get_queryset(self, request):
qs = super(StockPartInlines, self).get_queryset(request)
.values('container','expiration_date')
.order_by('container')
.annotate(_quantity=Sum('quantity_change'))
return qs
This has been returning an error of 'dict' object has no attribute '_meta', traceback. I believe this is because using .values() in a queryset returns a queryset of dicts rather than a queryset of objects.
Is there a different approach I could take to display the results of this query as an inline form in the admin panel?
I have 3 models: Maker, Item and MakerItem that creates the relation between the items and their makers:
class Maker(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=100)
class Item(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=100)
class MakerItem(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
item_id = models.ForeignKey(Item, on_delete=models.CASCADE)
maker_id = models.ForeignKey(Maker, on_delete=models.CASCADE)
the items can have a random amount of makers.
I want to create both the Item and the MakerItem objects at the same time with a single set of data,
for example if a Maker with id = "abcd" already exists, and I go to /item and send a POST request with the following data:
{
"name": "item1",
"makers": [
{
"maker_id": "abcd"
}
]
}
I want the serializer to create the Item object and the MakerItem object.
I have achieved this, with the following setup:
views.py
class ItemListCreate(ListCreateAPIView):
queryset = Item.objects.all()
serializer_class = ItemSerializer
serializers.py
class ItemSerializer(serializers.ModelSerializer):
class MakerItemSerializer(serializers.ModelSerializer):
class Meta:
model = MakerItem
exclude = ['id', 'item_id']
makers = MakerItemSerializer(many=True)
class Meta:
model = Item
fields = ['id', 'name', 'makers']
def create(self, validated_data):
maker_item_data = validated_data.pop('makers')
item_instance = Item.objects.create(**validated_data)
for each in maker_item_data:
MakerItem.objects.create(
item_id=check_instance,
maker_id=each['maker_id']
)
return item_instance
but when Django tries to return the created object, it always gives me the error:
AttributeError at /item/
Got AttributeError when attempting to get a value for field `makers` on serializer `ItemSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `Item` instance.
Original exception text was: 'Item' object has no attribute 'makers'.
What am I doing wrong?
Thanks
EDIT: To clarify, the objects get created and populate the database correctly, but when the browsable API that DRF provides tries to display the created object, it gives me the error above.
Change:
class ItemSerializer(serializers.ModelSerializer):
class MakerItemSerializer(serializers.ModelSerializer):
class Meta:
model = MakerItem
exclude = ['id', 'item_id']
makers = MakerItemSerializer(many=True)
To:
class ItemSerializer(serializers.ModelSerializer):
class MakerItemSerializer(serializers.ModelSerializer):
class Meta:
model = MakerItem
exclude = ['id', 'item_id']
makers = MakerItemSerializer(many=True, source="makeritem_set")
Hope this works!
For clarity, you're attempting to serialise the reverse relationship between MakerItem and Item for this serialiser.
This means that the attribute on your object is automatically set by Django as fieldname_set but you can override this behaviour by setting the related_name kwarg on the field and then makemigrations and migrate it.
In your case you would need to do:
maker_id = models.ForeignKey(Maker, on_delete=models.CASCADE, related_name="maker_items")
And then update the field in the Meta to match the new field name, this way you don't have to manually specify source. Because actually the attribute "makers" is misleading, due to the fact its actually the MakerItem, not the Maker itself.
See https://docs.djangoproject.com/en/3.2/ref/models/relations/ for further details about this behaviour.
This is a model containing some field and a property carbs_g_secured :
Models.py
class Food(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=255)
ingredients_label = models.TextField()
moisture_g = models.DecimalField(max_digits=4, decimal_places=1, blank=True, null=True)
def __str__(self):
return self.name
def carbs_g_secured(self):
# If moisture is missing
if self.carbs_g :
return self.carbs_g,'Carbs provided'
else :
return None, 'Moisture is missing'
I'd like to do some min/max filtering with django-rest-framework on the the carbs_g_secured property. I know I can't do that directly with Django-filters but I try to override the whole thing.
I have the following approach :
Serializers.py
class FoodSerializer(serializers.ModelSerializer):
carbs_g_secured = serializers.ReadOnlyField()
class Meta:
model = Food
fields = '__all__'
Views.py
class FoodFilter(filters.FilterSet):
carbs_g_secured_max = django_filters.NumberFilter(method='MyPropertyFilter')
class Meta:
model = Food
fields = {'name': ['exact', 'in', 'startswith'],
'species':['exact'],
'protein_g':['exact','lte','gte'],
'carbs_g_secured_max':['exact']}
def MyPropertyFilter(self, queryset, name, value):
results = [row for row in queryset if value and row.carbs_g_secured()[0] and row.carbs_g_secured()[0] < value]
return results
Problem is this is returning a list where it's expecting a query set.
web_1 | AssertionError: Expected 'FoodFilter.carbs_g_secured_max' to
return a QuerySet, but got a list instead.
So it seems I'm a bit stuck. I think it's not good practise to transform this list into query set.
What do you guys think ?
Am I heading the good way ? I have 20 others similar properties I want to filter against. I'm even considering converting all of those property into model fields. They are all the results of reasonable calculus made with the original fields from the food model.
How can I successfully manage to do this filtering and deal with the format expectation ?
symbol.py
class Symbol(BaseModel):
name = models.CharField(max_length=30,)
class Meta:
abstract = True
class StockSymbol(Symbol):
market = models.CharField(max_length=10,)
my_daily_price = GenericRelation(MyDailyPrice)
daily_price.py
class DailyPrice(BaseModel):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
class Meta:
abstract = True
class MyDailyPrice(DailyPrice):
open = models.DecimalField(
max_digits=15,
decimal_places=2,
)
What I want to do is,
symbol = StockSymbol.objects.first()
MyDailyPrice.objects.filter(content_object=symbol)
But it occured errors:
FieldError: Field 'content_object' does not generate an automatic reverse relation and therefore cannot be used for reverse querying. If it is a GenericForeignKey, consider adding a GenericRelation.
StockSymbol already has GenericRelation. What's wrong with it?
Or do I have to override ojbect manager?
You can filter with content_type and object_id, instead of content_object.
from django.contrib.admin.options import get_content_type_for_model
symbol = StockSymbol.objects.first()
MyDailyPrice.objects.filter(content_type=get_content_type_for_model(symbol), object_id=symbol.pk)
I wrapped up #Akash's answer in a method to be added to a custom Manager or QuerySet:
def gfks(self, **kwargs):
filters = {}
for field, obj in kwargs.items():
gfk = self.model._meta.get_field(field)
filters[gfk.ct_field] = ContentType.objects.get_for_model( obj )
filters[gfk.fk_field] = obj.pk
return self.filter(**filters)
For example, if you had a model called Comparison with two GFKs called product1 and product2, and added this method, usage would look like:
comp = Comparison.objects.gfks(product1=foo, product2=bar)
Would be nice if Django's contenttypes app provided some similar sugar automatically, but I'll settle for adding this to my BaseQuerySet class in the meantime.
class Product(models.Model):
products = models.CharField(max_length=256)
def __unicode__(self):
return self.products
class PurchaseOrder(models.Model):
product = models.ManyToManyField('Product')
vendor = models.ForeignKey('VendorProfile')
dollar_amount = models.FloatField(verbose_name='Price')
I have that code. Unfortunately, the error comes in admin.py with the ManyToManyField
class PurchaseOrderAdmin(admin.ModelAdmin):
fields = ['product', 'dollar_amount']
list_display = ('product', 'vendor')
The error says:
'PurchaseOrderAdmin.list_display[0]', 'product' is a ManyToManyField
which is not supported.
However, it compiles when I take 'product' out of list_display. So how can I display 'product' in list_display without giving it errors?
edit: Maybe a better question would be how do you display a ManyToManyField in list_display?
You may not be able to do it directly. From the documentation of list_display
ManyToManyField fields aren’t supported, because that would entail
executing a separate SQL statement for each row in the table. If you
want to do this nonetheless, give your model a custom method, and add
that method’s name to list_display. (See below for more on custom
methods in list_display.)
You can do something like this:
class PurchaseOrderAdmin(admin.ModelAdmin):
fields = ['product', 'dollar_amount']
list_display = ('get_products', 'vendor')
def get_products(self, obj):
return "\n".join([p.products for p in obj.product.all()])
OR define a model method, and use that
class PurchaseOrder(models.Model):
product = models.ManyToManyField('Product')
vendor = models.ForeignKey('VendorProfile')
dollar_amount = models.FloatField(verbose_name='Price')
def get_products(self):
return "\n".join([p.products for p in self.product.all()])
and in the admin list_display
list_display = ('get_products', 'vendor')
This way you can do it, kindly checkout the following snippet:
class Categories(models.Model):
""" Base category model class """
title = models.CharField(max_length=100)
description = models.TextField()
parent = models.ManyToManyField('self', default=None, blank=True)
when = models.DateTimeField('date created', auto_now_add=True)
def get_parents(self):
return ",".join([str(p) for p in self.parent.all()])
def __unicode__(self):
return "{0}".format(self.title)
And in your admin.py module call method as follows:
class categories(admin.ModelAdmin):
list_display = ('title', 'get_parents', 'when')
If you want to save extra queries, you can use prefetch_related in the get_queryset method like below:
class PurchaseOrderAdmin(admin.ModelAdmin):
fields = ['product', 'dollar_amount']
list_display = ('get_products', 'vendor')
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.prefetch_related('product')
def get_products(self, obj):
return ",".join([p.products for p in obj.product.all()])
According to the Docs, In this way, there would be just one extra query needed to fetch related Product items of all PurchaseOrder instances instead of needing one query per each PurchaseOrder instance.