Background
I am creating an E-commerce application that allows users to submit bids on any active item listings – where Bid & Listing are distinct Django models in models.py (below).
For each new bid, the bid amount (Bid.amount) is set via a Model Form. If the bid amount is greater than the current bid (Listing.current_bid), then:
- The
Bidis considered valid and the object is saved. Listing.current_bidis updated to equal the newBid.amount.
Issue
Currently, my solution (see views.py below) achieves the desired result – it successfully validates and updates each new bid using a conditional concatenated with the call to is_valid():
if new_bid_form.is_valid() and new_bid_form.cleaned_data["current_bid"] > previous_bid:
However, even though this design works, it feels "hackish", because the validation logic does not occur within the model. Therefore, the condition is not implicitly checked on the call to clean(), and I can foresee that causing issues going forward. Instead, I want to implement the validation logic within the model so that its called with Django's built-in validation and will raise a ValidationError.
Question
- Am I correct in saying that my current "validation" solution is poorly designed?
- If so, how can I validate within the
Bidmodel, that the value entered into the form forBid.amountis greater thanListing.current_bid? (See Solution Attempts below)
models.py
Listing Model
class Listing(models.Model):
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="usernames")
title = models.CharField(max_length=80)
description = models.CharField(max_length=800)
starting_bid = models.DecimalField(max_digits=11, decimal_places=2,validators=[MinValueValidator(Decimal('0.00'))])
current_bid = models.DecimalField(default=0, max_digits=11, decimal_places=2,validators=[MinValueValidator(Decimal('0.00'))])
image = models.URLField(blank=True)
Bid Model
class Bid(models.Model):
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="owners")
listing = models.ForeignKey(Listing, on_delete=models.CASCADE, related_name="listings")
amount = models.DecimalField(max_digits=11, decimal_places=2,validators=[MinValueValidator(Decimal('0.00'))])
views.py
listing() Function: Defines a view for each listing & implements a ModelForm to submit bids.
def listing(request, listing_id):
class NewBidForm(ModelForm):
template_name = "auctions/form_template.html"
class Meta:
model = Bid
fields = ["amount"]
widgets = {
"amount":forms.NumberInput(attrs={"placeholder": "Submit an offer greater than the current listing price"})
}
labels = {
"amount":_("Submit an Offer:")
}
# Get the current bid for the current listing
listing = Listing.objects.get(pk=listing_id)
current_bid = listing.current_bid
# If the submitted form data is valid, update & save new bid
if request.method == "POST":
new_bid_form = NewBidForm(request.POST)
if new_bid_form.is_valid() and new_bid_form.cleaned_data["amount"] > current_bid:
new_bid_form.instance.amount = new_bid_form.cleaned_data["amount"]
new_bid_form.instance.owner = request.user
new_bid_form.instance.listing = listing
new_bid_form.save()
# Set the listing's current bid to the new bid amount
listing.current_bid = new_bid_form.instance.amount
listing.save()
return HttpResponseRedirect(reverse("listing", args=[listing_id]))
else:
return render(request, "auctions/listing.html", {
"listing": listing,
"form": new_bid_form,
"message": messages.add_message(request, messages.ERROR, "Bid must be greater than the current highest bid."),
})
Solution Attempts
So far, I've been unable to find a similar example that provided any hints at a solution that I could iterate on. From the Django documentation, it seems overriding the clean() method might potentially be a way to achieve this, but I have not found a way to do this when one (unsaved) model instance, depends on an instance of another distinct model.
I attempted a solution that used the clean() method below in my Bid model:
def clean(self):
super().clean()
if self.amount < self.listing.current_bid:
raise ValidationError("Bid must be greater than the previous current bid")
However, this returned an error, which I believe is because the new Bid is not yet saved, thus does not have a reference to .listing:
RelatedObjectDoesNotExist at /listing/1. Bid has no listing.
Any insight would be massively appreciated!
For anyone else that might have a similar issue, I was able to find a solution (which was staring me in the face) by overriding the
clean()method as I had guessed.The error was raised because the
Bidobject did not yet have a reference toListingat the time the overriddenclean()method was being invoked.Therefore, by instantiating the
Bidwith a reference to the listing object, I was able to solve the issue:For reference, a slight revision to the
clean()method in theBidmodel: