如何从 Django 中的另一个模型字段更新模型字段?

How can i update the field of a model from another model field in django?

我有两个模型。带有字段 'scores' 的 DepartmentGoal 和带有字段 'scores' 和外国 departmentgoal 的 Task。

我想要的是;如果我将分数分配给 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)

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

之前的回复

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

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

  1. 任务 1 - 目前,分数设置为 7
  2. 任务 2 - 目前,分数设置为 3

现在,如果将 DepartmentGoal 分数更新为 13,如何将更改传播到任务?任务 1 的分数是否增加 2,任务 2 的分数增加 1?每个任务的得分是否增加了相同的数量(在这种情况下意味着每个任务 +1.5)?

因此,仅允许编辑任务分数并将更改传播回 DepartmentGoal,您至少可以确信 DepartmentGoal 分数将准确反映任务总和相关 Task 分数。毕竟,根据您的评论,您同意 DepartmentGoal 分数是一个 计算字段

所以解决方法很简单。您可以覆盖 Task 模型的 save 方法,或使用 post-保存信号。我将使用前一种方法进行演示,但如果您选择使用 post-保存信号,它会很相似。

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 是否已更改,但这需要使用字段跟踪器,例如 [=32] 提供的跟踪器=].

补充说明:

您还可以使用 property 方法,您不需要 运行 任何 post-保存挂钩,但 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. 自动调用
  4. 返回您的 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"})