0

我有两个模型。DepartmentGoal 带有字段“scores”,Task 也带有字段“scores”和外文部门目标。

我想要的是;如果我将分数分配给 DepartmentGoal,分配给 Task 的分数应该从 DepartmentGoal 的分数中减去,这意味着 Task 分数的任何实例在总和时应该等于 DepartmentGoal 的单个实例的分数。

我只需要一个关于如何实现这一点的线索。

这是模型

class DepartmentGoal(models.Model):
   name = models.TextField(max_length=150, null=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



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

这里的表格

class DepartmentGoalForm(ModelForm):
    class Meta:
        model = DepartmentGoal
        fields = (         
            ('name'),                      
            ('score'),                      
        )


class TaskForm(ModelForm):
    class Meta:
        model = Task
        fields = [ 
            'departmentgoal', 
            'name', 
            'score',
            ]

这是我的实现

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 save(self, *args, **kwargs):
        goal = DepartmentGoal.objects.get(id=self.departmentgoal.id)
        goal.scores -= self.scores
        goal.save()
        super(Task, self).save(*args, **kwargs)

我现在的问题是,如果部门目标分数用尽,即变为 0,用户仍然可以向任务添加新的任务分数,这会将部门目标分数的值更新为负分数。这是我要防止的行为。如果部门目标分数的值达到零,用户应该无法添加更多任务和任务分数

4

1 回答 1

0

以前的回应

我要解决的方法只是将任务分数公开为可编辑,并在保存或更新Task实例时更新score关联的DepartmentGoal. 我不允许编辑DepartmentGoal分数的原因是因为将更改传播到相关任务会很困难。

想象一下,如果你的DepartmentGoal分数是 10 并且它有两个相关的任务:

  1. 任务 1 - 目前,分数设置为 7
  2. 任务 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

更新的回复

以下是您的要求:

  1. 预先定义 DepartmentGoal 的分数。
  2. 任何具有给定分数的新任务都会降低 DepartmentGoal 的预定义分数。
  3. 一旦用尽了预定义的分数,就不应允许该 DepartmentGoal 的其他任务。
  4. 此外,对任务分数的任何修改都不应导致总任务分数超过预定义分数。

解决方案

  1. 在您的 DepartmentGoal 模型中,定义一个名为score. 这是您预先定义分数的字段,并且是必填字段。
  2. 在您的任务模型中,添加一个clean方法来验证分数。该clean方法将由您的ModelForm.
  3. 回到您的 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"})
于 2021-07-25T05:22:32.017 回答