我要解决的方法只是将任务分数公开为可编辑,并在保存或更新Task
实例时更新score
关联的DepartmentGoal
. 我不允许编辑DepartmentGoal
分数的原因是因为将更改传播到相关任务会很困难。
想象一下,如果你的DepartmentGoal
分数是 10 并且它有两个相关的任务:
- 任务 1 - 目前,分数设置为 7
- 任务 2 - 目前,分数设置为 3
现在,如果您将DepartmentGoal
分数更新为 13,您如何将更改传播到任务?任务 1 的分数增加 2,任务 2 的分数增加 1?每个任务的分数是否增加了相等的数量(在这种情况下,这意味着每个任务+1.5)?
因此,仅允许编辑任务分数并将更改传播回DepartmentGoal
,您至少可以确信DepartmentGoal
分数将准确反映相关Task
分数的总和。毕竟,根据您的评论,您同意DepartmentGoal
分数是一个计算字段。
所以解决方案非常简单。您可以覆盖Task
模型的save
方法,或使用保存后信号。我将使用前一种方法进行演示,但如果您选择使用保存后信号,它会是相似的。
class Task(models.Model):
name = models.CharField(max_length=300, null=True)
departmentgoal = models.ForeignKey(
DepartmentGoal,
on_delete=models.CASCADE,
related_name='departgoal',
null=True,
blank=True)
score = models.IntegerField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True, null=True)
updated_at = models.DateTimeField(auto_now=True, null=True)
def __str__(self):
return self.name
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# post-save, update the associated score of the `DepartmentGoal`
# 1. grab associated `DepartmentGoal`
goal = DepartmentGoal.objects.get(id=self.departmentgoal.id)
# 2. sum all task scores of the `DeparmentGoal`
# Note: I'm using `departgoal` which is the `related_name` you set on
# the foreign key. I would rename this to `tasks` since the `related_name`
# is the reference back to the model from the foreign key.
sum = goal.departgoal.values("departmentgoal") \
.annotate(total=models.Sum("score")) \
.values_list("total", flat=True)[0]
# 3. update score of `DepartmentGoal` with the calculated `sum`
goal.score = sum
goal.save(update_fields=["score"])
这只是一个最小的可行示例。显然,可以对 post-save 钩子进行一些优化,例如检查score
任务的 是否已更改,但这需要使用诸如django-model-utils提供的字段跟踪器。
附加说明:
您也可以使用该property
方法,您不需要运行任何保存后挂钩,而是在调用属性属性时让 python 计算分数的总和。这样做的好处是您在保存任务后不需要进行任何计算(因此是性能优化)。但是,缺点是您将无法在 django 查询集中使用属性,因为查询集使用字段,而不是属性。
class DepartmentGoal(models.Model):
name = models.TextField(max_length=150, null=True)
created_at = models.DateTimeField(auto_now_add=True, null=True)
updated_at = models.DateTimeField(auto_now=True, null=True)
def __str__(self):
return self.name
@property
def score(self):
if self.departgoal.count() > 0:
return (
self.departgoal.values("departmentgoal")
.annotate(total=models.Sum("score"))
.values_list("total", flat=True)[0]
)
return 0
更新的回复
以下是您的要求:
- 预先定义 DepartmentGoal 的分数。
- 任何具有给定分数的新任务都会降低 DepartmentGoal 的预定义分数。
- 一旦用尽了预定义的分数,就不应允许该 DepartmentGoal 的其他任务。
- 此外,对任务分数的任何修改都不应导致总任务分数超过预定义分数。
解决方案
- 在您的 DepartmentGoal 模型中,定义一个名为
score
. 这是您预先定义分数的字段,并且是必填字段。
- 在您的任务模型中,添加一个
clean
方法来验证分数。该clean
方法将由您的ModelForm
.
- 回到您的 DepartmentGoal 模型,添加一个
clean
方法来验证分数,以防用户计划修改目标的分数。如果目标已经有关联的任务,这可以确保分数不会低于任务的总和。
from django.core.exceptions import ValidationError
class DepartmentGoal(models.Model):
name = models.TextField(max_length=150, null=True)
score = models.IntegerField() # 1
created_at = models.DateTimeField(auto_now_add=True, null=True)
updated_at = models.DateTimeField(auto_now=True, null=True)
def __str__(self):
return self.name
# 3
def clean(self):
# calculate all contributed scores from tasks
if self.departgoal.count() > 0:
task_scores = self.departgoal.values("departmentgoal") \
.annotate(total=models.Sum("score")) \
.values_list("total", flat=True)[0]
else:
task_scores = 0
# is new score lower than `task_scores`
if self.score < task_scores:
raise ValidationError({"score": "Score not enough"})
class Task(models.Model):
name = models.CharField(max_length=300, null=True)
departmentgoal = models.ForeignKey(
DepartmentGoal,
on_delete=models.CASCADE,
related_name='departgoal',
null=True,
blank=True)
score = models.IntegerField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True, null=True)
updated_at = models.DateTimeField(auto_now=True, null=True)
def __str__(self):
return self.name
# 2
def clean(self):
# calculate contributed scores from other tasks
other_tasks = Task.objects.exclude(pk=self.pk) \
.filter(departmentgoal=self.departmentgoal)
if other_tasks.count() > 0:
contributed = (
other_tasks.values("departmentgoal")
.annotate(total=models.Sum("score"))
.values_list("total", flat=True)[0]
)
else:
contributed = 0
# is the sum of this task's score and `contributed`
# greater than DeparmentGoal's `score`
if self.score and self.score + contributed > self.departmentgoal.score:
raise ValidationError({"score": "Score is too much"})