英文:
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
Bid
is considered valid and the object is saved. Listing.current_bid
is 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
Bid
model, that the value entered into the form forBid.amount
is 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}"})
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论