I am new to django and coding in general. I have this project that I am trying to code thats basically an auctions website.
I am facing some difficulties structuring the models though.
Here are 2 models of all models
class Listing(models.Model):
title = models.CharField(max_length=64)
image = models.URLField(null=True, blank=True)
description = models.CharField(max_length=64)
starting_price = models.DecimalField(max_digits=7, decimal_places=2)
current_price = #highest bid price
bids_count = #count of bids
lister = models.ForeignKey(User, on_delete=models.CASCADE, related_name="listings")
def __str__(self):
return f"Title: {self.title} ({self.id}), Lister: {self.lister.username}"
class Bid(models.Model):
listing = models.ForeignKey(Listing, on_delete=models.CASCADE, related_name="bids")
bidder = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name="bids")
amount = models.DecimalField(max_digits=7, decimal_places=2)
time = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Bid by {self.bidder.username} on {self.listing.title} ({self.listing.id}) for an amount of {self.amount}"
I am trying to let the current price equal to the highest bid of that listing
current_price = Listing.bid.objects.all().aggregate(Max("amount"))
And count the number of bids
bids_count = Listing.bid.count()
I know we cant place querysets inside models fields like I have done but i did it to demonstrate my issue.
Surely there is some way around this but I just cant figure it out.
CodePudding user response:
As you said, you cannot put those fields "as-is" in your model. The easiest/quickest way to solve it would be to use a property:
class Listing(models.Model):
# ... rest of the class
@property
def bids_count(self):
return self.bids.count()
@property
def current_price(self):
return self.bids.all().aggregate(Max("amount"))
# ... rest of the class
now, be aware that this will do fine when you work with a single instance. This will not be performant if you are looping over a list of Listing
instances and display those properties, because it will trigger a new db query each time you access those properties (so these values are fetched in a lazy way)
The best workaround in my opinion would be to use a custom manager, as follow:
class ListingQuerySet(models.QuerySet):
def with_bids_count(self):
return self.annotate(bids_count=Count('bids'))
def with_current_price(self):
return self.annotate(current_price=Subquery(Bid.objects.filter(listing=OuterRef('pk')).annotate(max=Max('amount')).values('max')[:1]))
class Listing(models.Model):
objects = ListingQuerySet.as_manager()
# ... rest of the class
# Use it like this in your code
for listing in Listing.objects.with_bids_count().with_current_price():
print(listing.current_price)
This previous method is more advanced for someone new to Django/coding (especially with the subquery). You'll be able to read more about all this in the documentation:
Note that I didn't try the code