How to write manager class which use filter field as computed field not as a part of model fields?

Question:

I have a model Student with manager StudentManager as given below. As property gives the last date by adding college_duration in join_date. But when I execute this property computation is working well, but for StudentManager it gives an error. How to write manager class which on the fly computes some field using model fields and which is used to filter records.

The computed field is not in model fields. still, I want that as filter criteria.

class StudentManager(models.Manager):

    def passed_students(self):
        return self.filter(college_end_date__lt=timezone.now())


class Student(models.Model):
   
    join_date = models.DateTimeField(auto_now_add=True)
    college_duration = models.IntegerField(default=4)

    objects = StudentManager()

    @property
    def college_end_date(self):
        last_date = self.join_date + timezone.timedelta(days=self.college_duration)
        return last_date

Error Django gives. when I tried to access Student.objects.passed_students()

django.core.exceptions.FieldError: Cannot resolve keyword 'college_end_date' into field. Choices are: join_date, college_duration
Asked By: Akash Pagar

||

Answers:

Problem

You’re attempting to query on a row that doesn’t exist in the database. Also, Django ORM doesn’t recognize a property as a field to register.

Solution

The direct answer to your question would be to create annotations, which could be subsequently queried off of. However, I would reconsider your table design for Student as it introduces unnecessary complexity and maintenance overhead.

There’s much more framework/db support for start date, end date idiosyncrasy than there is start date, timedelta.

Instead of storing duration, store end_date and calculate duration in a model method. This makes more not only makes more sense as students are generally provided a start date and estimated graduation date rather than duration, but also because it’ll make queries like these much easier.

Example

Querying which students are graduating in 2020.

Students.objects.filter(end_date__year=2020)
Answered By: pygeek

To quote @Willem Van Olsem’s comment:

You don’t. The database does not know anything about properties, etc. So it can not filter on this. You can make use of .annotate(..) to move the logic to the database side.

You can either do the message he shared, or make that a model field that auto calculates.

class StudentManager(models.Manager):

    def passed_students(self):
        return self.filter(college_end_date__lt=timezone.now())


class Student(models.Model):
   
    join_date = models.DateTimeField(auto_now_add=True)
    college_duration = models.IntegerField(default=4)
    college_end_date = models.DateTimeField()

    objects = StudentManager()

    def save(self, *args, **kwargs):
        # Add logic here
        if not self.college_end_date:
            self.college_end_date = self.join_date + timezone.timedelta(days-self.college_duration)
        return super.save(*args, **kwargs)

Now you can search it in the database.

NOTE: This sort of thing is best to do from the start on data you KNOW you’re going to want to filter. If you have pre-existing data, you’ll need to re-save all existing instances.

Answered By: Zack Plauché

Q 1. How alias queries done in Django ORM?

By using the annotate(...)–(Django Doc) or alias(...) (New in Django 3.2) if you’re using the value only as a filter.

Q 2. Why property not accessed in Django managers?

Because the model managers (more accurately, the QuerySet s) are wrapping things that are being done in the database. You can call the model managers as a high-level database wrapper too.

But, the property college_end_date is only defined in your model class and the database is not aware of it, and hence the error.

Q 3. How to write manager to filter records based on the field which is not in models, but can be calculated using fields present in the model?

Using annotate(...) method is the proper Django way of doing so. As a side note, a complex property logic may not be re-create with the annotate(...) method.

In your case, I would change college_duration field from IntegerField(...) to DurationField(...)–(Django Doc) since its make more sense (to me)

Later, update your manager and the properties as,

from django.db import models
from django.utils import timezone


class StudentManager(models.Manager):

    <b>def passed_students(self):
        default_qs = self.get_queryset()
        college_end = models.ExpressionWrapper(
            models.F('join_date') + models.F('college_duration'),
            output_field=models.DateField()
        )
        return default_qs 
            .annotate(college_end=college_end) 
            .filter(college_end__lt=timezone.now().date())</b>


class Student(models.Model):
    join_date = models.DateTimeField()
    college_duration = models.DurationField()

    objects = StudentManager()

    @property
    def college_end_date(self):
        # return date by summing the datetime and timedelta objects
        return <b>(self.join_date + self.college_duration).date()

Note:

  • DurationField(...) will work as expected in PostgreSQL and this implementation will work as-is in PSQL. You may have problems if you are using any other databases, if so, you may need to have a "database function" which operates over the datetime and duration datasets corresponding to your specific database.

  • Personally, I like this solution,

Answered By: JPG