How to perform aggregated count within a model method

I have a model called DailyLog where a user logs their steps count everyday. Then I have another table called Goal where a user creates a goal using that steps count e.g achieving 10000 steps in 3 days.

Every day the user logs their data with a new step count.

I want to be able to calculate the total steps count within the model itself. Also I’m not quite sure if I’m going about this the right way or if it’s as optimised as it can be so a few pointers or critics will help greatly:

class DailyLog(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    date = models.DateField(auto_now_add=True)
    water_intake = models.FloatField(
        help_text="Water intake in liters",
        validators=[MinValueValidator(0), MaxValueValidator(10), validate_liters],
    )
    steps_count = models.IntegerField()
    blood_pressure = models.CharField(
        max_length=255, validators=[validate_blood_pressure]
    )

    before_breakfast_time = models.TimeField(null=True, blank=True)
    before_breakfast_glucose = models.FloatField(
        null=True,
        blank=True,
        validators=[
            MinValueValidator(70),
            MaxValueValidator(300),
            validate_blood_glucose,
        ],
    )
    before_breakfast_blood_pressure = models.CharField(
        max_length=9, null=True, blank=True, validators=[validate_blood_pressure]
    )

    breakfast_time = models.TimeField()
    breakfast_description = models.TextField()
    breakfast_image = models.ImageField(upload_to="meal_images/", null=True, blank=True)
    breakfast_glucose = models.FloatField(
        validators=[
            MinValueValidator(70),
            MaxValueValidator(300),
            validate_blood_glucose,
        ]
    )
    breakfast_blood_pressure = models.FloatField(validators=[validate_blood_pressure])

    lunch_time = models.TimeField()
    lunch_description = models.TextField()
    lunch_image = models.ImageField(upload_to="meal_images/", null=True, blank=True)
    lunch_glucose = models.FloatField(
        validators=[
            MinValueValidator(70),
            MaxValueValidator(300),
            validate_blood_glucose,
        ]
    )
    lunch_blood_pressure = models.FloatField(validators=[validate_blood_pressure])

    dinner_time = models.TimeField()
    dinner_description = models.TextField()
    dinner_image = models.ImageField(upload_to="meal_images/", null=True, blank=True)
    dinner_glucose = models.FloatField(
        validators=[
            MinValueValidator(70),
            MaxValueValidator(300),
            validate_blood_glucose,
        ]
    )
    dinner_blood_pressure = models.FloatField(validators=[validate_blood_pressure])

    def save(self, *args, **kwargs):
        # Check if another DailyLog entry exists for the same user and date
        if DailyLog.objects.filter(user=self.user, date=self.date).exists():
            raise ValidationError(
                "DailyLog entry already exists for this user and date."
            )

        super().save(*args, **kwargs)
        profile, created = GamificationProfile.objects.get_or_create(user=self.user)
        profile.update_points(500)
        profile.update_streak()
        self.check_goals()

    def check_goals(self):
        goals = Goal.objects.filter(
            user=self.user,
            start_date__lte=self.date,
            end_date__gte=self.date,
            is_active=True,
        )
        total_steps_count = DailyLog.calculate_total_steps(self.user)
        print(total_steps_count)
        for goal in goals:
            if goal.goal_type == "blood_glucose":
                if (
                    (
                        self.before_breakfast_glucose
                        and self.before_breakfast_glucose >= goal.min_value
                        and self.before_breakfast_glucose <= goal.max_value
                    )
                    or (
                        self.breakfast_glucose >= goal.min_value
                        and self.breakfast_glucose <= goal.max_value
                    )
                    or (
                        self.lunch_glucose >= goal.min_value
                        and self.lunch_glucose <= goal.max_value
                    )
                    or (
                        self.dinner_glucose >= goal.min_value
                        and self.dinner_glucose <= goal.max_value
                    )
                ):
                    goal.current_streak += 1
            elif goal.goal_type == "steps_count":
                if total_steps_count >= goal.min_value:
                    goal.current_streak += 1
                    # Check if steps goal is achieved and update is_completed
                    if goal.current_streak >= goal.target_days:
                        goal.is_completed = True
            if goal.current_streak >= goal.target_days:
                goal.is_completed = True
            goal.save()

    @classmethod
    def calculate_total_steps(cls, user):
        return (
            cls.objects.filter(user=user).aggregate(total_steps=Sum("steps_count"))[
                "total_steps"
            ]
            or 0
        )

Welcome @Nneji123 !

Since we’re talking about “code style” issues here, these are all opinions. There are few real rules or requirements.

However, I would say that this function is probably in the wrong location. As a general principle and an issue of style, a model method should only work on one instance of that model, “self”.

Since this is an aggregate of data on a User, I personally would most likely make this a model method in the User model.

Something like this: (Untried, unverified, and may not be syntactically correct)

# In the User model
def calculate_total_steps(self):
    return (
        self.objects.aggregate(total_steps=Sum("dailylog__steps_count", default=0))["total_steps"]
    )

However, if you’re not using a custom User model where it’s easy to add this, you might want to add this to your User’s profile model. Or, if you have a number of methods like this to be added to the User model without needing to change the User model itself, you could create a Proxy model for these methods.

You could also add this as a Manager method on the DailyLog manager. (Because this is a value associated with a user and not an arbitrary set of DailyLog instances, I consider this the least desirable option - but still better than the model method version.)

From the Manager docs:

Adding extra Manager methods is the preferred way to add “table-level” functionality to your models. (For “row-level” functionality – i.e., functions that act on a single instance of a model object – use Model methods, not custom Manager methods.)

As a purely personal side note: I’d think about expanding the validation range of your glucose readings unless you’re working with specific models that only generate readings in that range. I have seen multiple readings taken outside those ranges where the individuals involved are still lucid and functional - especially on the high side.

1 Like