英文:
Validating a model based on the value set for a separate model
问题
背景
我正在创建一个电子商务应用程序,允许用户在任何活动商品列表上提交竞标,其中Bid和Listing是**models.py**中不同的Django模型(如下所示)。
对于每个新的出价,出价金额(Bid.amount)是通过模型表单设置的。如果出价金额大于当前出价(Listing.current_bid):
- 出价被视为有效,并保存对象。
Listing.current_bid更新为等于新的Bid.amount。
问题
目前,我的解决方案(请参见下面的**views.py**)实现了期望的结果——它成功地验证并更新每个新的出价,使用与is_valid()的调用连接的条件:
if new_bid_form.is_valid() and new_bid_form.cleaned_data["current_bid"] > previous_bid:
然而,尽管这个设计有效,但它感觉很“hackish”,因为验证逻辑不发生在模型内部。因此,在调用clean()时隐式检查条件,并且我可以预见这会引发以后的问题。相反,我希望在模型内部实现验证逻辑,以便它在Django的内置验证调用时被调用并引发ValidationError。
问题
- 我是否正确地说我的当前“验证”解决方案设计得很差?
- 如果是这样,我如何在
Bid模型内部验证表单中输入的Bid.amount大于Listing.current_bid的值?(请参见下面的解决方案尝试)
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() 函数:为每个列表定义一个视图,并实现一个ModelForm以提交出价。
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": "提交高于当前列表价格的报价"})
}
labels = {
"amount":_("提交报价:")
}
# 获取当前列表的当前出价
listing = Listing.objects.get(pk=listing_id)
current_bid = listing.current_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()
# 将列表的当前出价设置为新的出价金额
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, "出价必须高于当前的最高出价。"),
})
解决方案尝试
到目前为止,我一直无法找到类似的示例,提供了我可以迭代的解决方案的任何提示。从Django文档中看来,覆盖clean()方法可能是一种实现这一目标的方法,但我还没有找到一种方法,在其中一个(未保存的)模型实例依赖于另一个不同模型的实例的情况下执行此操作。
我尝试了在我的Bid模型中使用以下clean()方法的解决方案:
def clean(self):
super().clean()
if self.amount < self.listing.current_bid:
raise ValidationError("出价必须高于之前的最高出价")
然而,这返回了一个错误,我认为这是因为新的Bid尚未保存,因此没有引用.listing:
RelatedObjectDoesNotExist at /listing/1. 出价没有列表。
任何见解将非常感激!
英文:
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!
答案1
得分: 0
对于可能有类似问题的其他人,我成功找到了解决方案(就在我眼前),通过覆盖clean()方法,正如我所猜测的那样。
错误是因为在调用覆盖的clean()方法时,Bid对象尚未引用Listing,因此,通过将Bid实例化为引用列表对象,我能够解决这个问题:
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:")
}
# 获取要竞拍的当前列表。
listing = Listing.objects.get(pk=listing_id)
# 验证新出价是否高于当前最高出价。
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()
# 将列表的当前出价设置为新出价金额。
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,
})
供参考,在Bid模型的clean()方法中略微修订:
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}"})
英文:
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}"})
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论