How to get highest bid in an auction bidding site Django

I have a Django auctions app, which has 3 models: Users, Listings, Bids. When a user tries to place a bid on some listing, I want to check if the new_bid field in Bid model is bigger than start current_bid field in Listing model and if the new_bid is <= 0, it should return a message.

This is what I’ve done so far but when I click on ‘Place bid’ button, it does not implement this, the page doesn’t redirect to an error page if the value is less than the current bid and doesn’t update the bid if the user adds another one.

VIEWS.PY

def listing_detail(request, listing_id):
    try:
        detail = get_object_or_404(Auction, pk=listing_id)
    except Auction.DoesNotExist:
        messages.add_message(request, messages.ERROR, "This is not available") 
        return HttpResponseRedirect(reverse("index"))

    bid_count = Bids.objects.filter(auction=listing_id).count()
    bid_form= BidForm()     

    if request.method == 'POST':
        comment_form = CommentForm(request.POST)

        if comment_form.is_valid():
            new_comment = comment_form.save(commit=False)
            new_comment.auction = detail
            new_comment.save()

            return redirect('listing_detail', listing_id=listing_id)

        else:
            comment_form = CommentForm()
    else:
        comment_form = CommentForm()

    context = {'detail': detail,
                'bid_count': bid_count, 
                'bid_form': bid_form,
                'comment_form': comment_form
                } 

    return render(request, 'auctions/details.html', context)

# create custom 404 page using learning.django

@login_required
def make_bid(request, listing_id):
    if request.method == 'POST':
        form = BidForm(request.POST)
        if form.is_valid():
            each_listing = Auction.objects.get(pk=listing_id)
            highest_bid = Bids.objects.filter(auction_id=listing_id).order_by('-new_bid').first()
            new_bid = form.cleaned_data.get['new_bid']
            if new_bid <= 0:
                return render(request, 'auctions/details.html', 
                    {"message": "Input an amount greater than 0"})  
                
                # messages.add_message(request, messages.SUCCESS, "Input an amount greater than 0") 
            elif new_bid <= highest_bid.new_bid:
                return HttpResponseRedirect(reverse("index"))
                messages.add_message(request, messages.ERROR, "Amount is low, please increase the bid") 
            else:
                highest_bid = Bids(each_listing=each_listing, user=request.user, new_bid=new_bid)
                highest_bid.save()

                each_listing.current_bid = new_bid
                each_listing.save()

                return HttpResponseRedirect(reverse("index"))
                messages.add_message(request, messages.SUCCESS, "Your bid is currently the highest") 
        else:
            form = BidForm()
            return render(request, 'auctions/details.html', {'bidForm': form})  
                
    else:
        form = BidForm()
        return render(request, 'auctions/details.html', {'bidForm': form})  

MODELS.PY

class Auction(models.Model):
    title = models.CharField(max_length=25)
    description = models.TextField()
    current_bid = models.IntegerField(null=False, blank=False)
    image_url = models.URLField(verbose_name="URL", max_length=255, unique=True, null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    category = models.ForeignKey(Category, max_length=12, null=True, blank=True, on_delete=models.CASCADE)

    def __str__(self):
        return self.title

    class Meta:
        ordering = ['-created_at']

class Bids(models.Model):
    auction = models.ForeignKey(Auction, on_delete=models.CASCADE, related_name='bidding')
    user = models.ForeignKey(User, on_delete=models.PROTECT, related_name='bidding')
    new_bid = models.DecimalField(max_digits=8, decimal_places=2)
    # new_bid = MoneyField(max_digits=10, decimal_places=2, null=False, blank=False, default_currency='USD')
    done_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['auction', '-new_bid']

FORMS.PY

class AuctionForm(forms.ModelForm):
    
    class Meta:
        model = Auction
        fields = ['title', 'description', 'current_bid', 'image_url', 'category']

        widgets = {
            'title': forms.TextInput(attrs={'class': 'form-control'}),
            'description': forms.Textarea(attrs={'class': 'form-control'}),
            'current_bid': forms.NumberInput(attrs={'class': 'form-control'}),
            'image_url': forms.URLInput(attrs={'class': 'form-control'}),
            'category': forms.Select(attrs={'class': 'form-control'})
        }

class BidForm(forms.ModelForm):

    class Meta:
        model = Bids
        fields = ['new_bid']
        labels = {
            'new_bid': ('Bid'),

DETAILS.HTML

<form action="{% url 'listing_detail' detail.id %}" method="post">
    {% csrf_token %}
    {{ bid_form }}
    <input type="submit" class="btn btn-primary btn-block mt-3" value="Place bid">
</form>

You are returning from your view before setting the message. The message never gets a chance to be set.

Additionally, you may want to perform your checks in the Forms clean_new_bid and clean methods rather than in the is_valid method of the view. The intent of the is_valid method is that you’ve already determined that all the data being submitted is “good”.

See the docs for Form and field validation.

1 Like

Also see Validators, specifically MinValueValidator.

1 Like

Thank you for the link, I have researched on it and now I understand why it is better to do the validation check within forms. I’ve been able to implement the below but I the validation error does not get triggered and the amount does not update nor does it show (the new bid amount that should show the current highest bid)

from .models import Auction, Bids, Comment
from django import forms
from django.core.exceptions import ValidationError

class BidForm(forms.ModelForm):

    class Meta:
        model = Bids
        fields = ['new_bid']
        labels = {
            'new_bid': ('Bid'),
        }

    def clean_new_bid(self):
        errors={}
        new_bid = self.cleaned_data('new_bid')

        if new_bid <= 0:
            errors.append(ValidationError("Input an amount greater than 0"))

        if new_bid <= Auction.current_bid:
            errors.append(ValidationError("Amount is low, please increase the bid"))
        
        if errors:
            raise ValidationError(errors)
            
        else:
            Auction.current_bid = new_bid

        return new_bid

details.html

                            <p>{{ detail.description }}</p>
                            <hr>
                            <p>${{ detail.new_bid }}</p>
                            <p>{{ bid_count }}</p>
                            <hr>

                            <form action="{% url 'listing_detail' detail.id %}" method="post">
                                {% csrf_token %}
                                {{ bid_form }}
                                <input type="submit" class="btn btn-primary btn-block mt-3" value="Place bid">
                            </form>

views.py

def make_bid(request, listing_id):
    if request.method == 'POST':
        bid_form = BidForm(request.POST)
        if bid_form.is_valid():
            new_bid = Bids.objects.filter(listing_id=listing_id).order_by('-new_bid').first()
            new_bid.save()
            messages.success(request, "Your bid is currently the highest")
            return HttpResponseRedirect(reverse("index"))

        else:
            bid_form = BidForm()
            return render(request, 'auctions/details.html', {'bid_form': bid_form})  
                
    else:
        bid_form = BidForm()
        return render(request, 'auctions/details.html', {'bid_form': bid_form})  

You’re saving the Bids object already in the database, not the bid submitted through the form (bid_form)

In the else-clause of the is_valid test, you have:

If bid_form is invalid, you’re creating a new instance of bid_form, instead of rendering the instance of the form that has the error messages in it.

In your clean function, you have

Which defines errors as a dict. However, you’re trying to add errors using:

which isn’t going to work

Then these lines:

don’t make any sense, since Auction is a model class, not an instance of that model. You probably want to retrieve the appropriate instance of the Auction model and compare (or set) it.

1 Like

I have been able to update the below and make the changes but still the same, the bid is not going through and I’m not receiving any validation error

class BidForm(forms.ModelForm):

    class Meta:
        model = Bids
        fields = ['new_bid']
        labels = {
            'new_bid': ('Bid'),
        }

    def clean(self):
        current_bid = self.cleaned_data('current_bid')
        new_bid = self.cleaned_data('new_bid')

        errors= []

        if new_bid <= 0:
            errors.append(ValidationError("Input an amount greater than 0"))

        if new_bid <= current_bid:
            errors.append(ValidationError("Amount is low, please increase the bid"))
        
        if errors:
            raise ValidationError(errors)
@login_required
def make_bid(request):
    if request.method == 'POST':
        bid_form = BidForm(request.POST)
        if bid_form.is_valid():
            bid_form.save()

            messages.success(request, "Your bid is currently the highest")
            return HttpResponseRedirect(reverse("index"))

        else:
            context = {
                'bid_form': bid_form
                }
            
            return render(request, 'auctions/details.html', context)  
                
    return render(request, 'auctions/details.html', {'bid_form': BidForm()})  

Question: How are you running this?
If you’re using runserver, I’d be expecting you to be seeing some error messages.

I’m using runserver with VScode and it’s not showing any error message

The error messages would be in the “Terminal” tab at the bottom, where it shows the requests and responses.

This is what is shown.

Performing system checks...

System check identified no issues (0 silenced).
March 06, 2022 - 14:59:56
Django version 4.0.2, using settings 'commerce.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

Yes, that’s the proper window.

Now, when you try the form, you should see the HTTP requests, and the errors that should be reported when you submit the form.

This is what it shows

Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
[06/Mar/2022 15:15:58] "GET / HTTP/1.1" 200 13965
[06/Mar/2022 15:16:06] "GET /detail/2/ HTTP/1.1" 200 15908
[06/Mar/2022 15:16:11] "POST /detail/2/ HTTP/1.1" 200 15908

Something’s “out of sync” here.

I can’t recreate the behavior you’re reporting here from the information posted to this point.

When I copy your code into my test environment, the errors I expect to see, show up. You’ve got at least three errors in your clean method. (Review the examples at: Cleaning and validating fields that depend on each other) That these errors aren’t showing up are a sign of a more fundamental issue.

I’m not sure how to proceed at this point, without having access to your complete project. There’s something wrong, but I can’t tell what it is. You’re either not running the code you think you’re running, or you’re not creating the instance of either the form or view that you’re posting here.

Yes, I could point out the specific errors you have, but ultimately, that doesn’t help you if you’re not running the right code.

Yikes, urrrrmmm, whooshhhh.

I’m checking the link you sent, please let me know if these are the three errors that is causing this because I’m running the code shown and I really don’t know what is causing this.

    def clean(self):
        cleaned_data = super().clean()
        current_bid = cleaned_data.get('current_bid')
        new_bid = cleaned_data.get('new_bid')

        if new_bid <= 0 and new_bid <= current_bid:
            raise ValidationError("Amount is low, please increase the bid")

But, is the views.py looking ‘ok’ and is it possible that the entire issue is coming from the forms.py

Superficially, the view looks fine. But the behavior of the view is dependent upon the behavior of the form.

The form.is_valid method calls the clean method on the form. If the clean method has errors, they would be reported in the console session where runserver is being run.

If you’re not getting errors reported, then you’re not running that clean method. If you’re not running the clean method, then you’re not calling is_valid on the form - which means you’re not running the code you think you’re running.

I’m confused how that is possible if I’m running ‘python manage.py runserver’.

Could there be something I’m missing right now?

Possibly. But like I said earlier, short of having access to your complete project, I’d have no way of knowing what that “something” is.
The runserver command isn’t some kind of magic. It’s just another python script that initializes the Django environment and starts an HTTP server. (It can be found in django.core.management.commands.runserver, and it’s only about 165 lines of code.)
How it behaves in the VSCode environment is governed largely by your VSCode launch.json file along with how Django is configured with your settings.py file, among other things like your project’s file and directory structure.

There are lots of pieces to this puzzle, all interrelated.

1 Like

Thank you, I’d keep trying

Hi Ken, could you please check what I’ve done so far,

VIEWS.PY

@login_required
def make_bid(request, listing_id):
    auction = Auction.objects.get(pk=listing_id)
    user = request.user
    if request.method == 'POST':
        bid_form = BidForm(request.POST)
        if bid_form.is_valid(): 
            new_bid = request.POST['new_bid']
            current_price = Bids.objects.create(
                listing_id = listing_id,
                user = user,
                new_bid = new_bid
            )
            messages.success(request, 'Successfully added your bid')
            return HttpResponseRedirect(reverse("listing_detail", args=(listing_id,)))


        else:
            bid_form = BidForm(request.POST) 
            return render(request, 'auctions/details.html', {"bid_form": bid_form})  
                
    return render(request, 'auctions/details.html', bid_form = BidForm()) 

FORMS.PY

class BidForm(forms.ModelForm):

    class Meta:
        model = Bids
        fields = ['new_bid']
        labels = {
            'new_bid': ('Bid'),
        }


    def clean_new_bid(self):
        new_bid = self.cleaned_data['new_bid']
        current_bid = self.cleaned_data['current_bid']


        if new_bid <= current_bid:
            error = ValidationError("New bid must be greater than the previous bid")
            self.add_error('new_bid', error)
        return new_bid

DETAILS.HTML

                            <p>{{ detail.description }}</p>
                            <hr>
                            <p>Current price: ${{detail.current_price}}</p>

                            <form action="{% url 'make_bid' detail.id %}" method="post">
                                {% csrf_token %}
                                {{ form.non_field_errors }}
                                {{ form.errors }}
                                {{ bid_form }}
                                <input type="submit" class="btn btn-primary btn-block mt-3" value="Place bid">
                            </form>

I’m having this error

KeyError at /make_bid/2

‘current_bid’

Request Method: POST
Request URL: http://127.0.0.1:8000/make_bid/2

I’m sure its because I’m trying to compare two different models, but don’t know a better way to do this. Could you please direct me?

The issue here is that you’re trying to retrieve current_bid from the form - but that data element isn’t in the form. (There is no field named current_bid in BidForm, and there’s no field named current_bid in Bids.)

You can’t find current_bid in the form, you’ll need to query it from the database.

What query could you write to obtain the current_bid?