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:
Bid
is considered valid and the object is saved.Listing.current_bid
is updated to equal the new Bid.amount
.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
.
Bid
model, that the value entered into the form for Bid.amount
is greater than Listing.current_bid
? (See Solution Attempts below)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'))])
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."),
})
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 Bid
object did not yet have a reference to Listing
at the time the overridden clean()
method was being invoked.
Therefore, by instantiating the Bid
with a reference to the listing object, I was able to solve the issue:
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 listing to bid on.
listing = Listing.objects.get(pk=listing_id)
# Validate that the new bid is greater than the current highest bid.
if request.method == "POST":
new_bid_form = NewBidForm(request.POST, instance=Bid(listing=listing))
if new_bid_form.is_valid():
new_bid_form.instance.amount = new_bid_form.cleaned_data["amount"]
new_bid_form.instance.owner = request.user
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,
})
For reference, a slight revision to the clean()
method in the 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'))])
def clean(self):
super().clean()
if self.amount <= self.listing.current_bid:
raise ValidationError(
_("Unable to place new bid! Please enter a minimum bid of at least $%(value)s!"),
params={"value":f"{float(self.listing.current_bid) + 1.0:.2f}"})