Qrom
Qrom

Reputation: 497

Filtering latest entry and prefetch related in Django

I have been trying to optimize my queries in Django to reduce my postgres database hits, so here are the, simplified for this question, concerned models

Model :

class SentEmail(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    sent_date = models.DateTimeField(default=timezone.now)    
    sender = models.EmailField()


class AppUser(models.Model):
    user= models.OneToOneField(User, primary_key=True)
    is_active= models.BooleanField(default= False)
    expiration_date = models.DateTimeField(default=make_default_expiration_date) #note : 90 days after the creation

And User is the standard Django auth user with the username, password and other stuff. What I want to do, is retrieving the AppUser's various data for active users and the last SentEmail that concerns them. For now, I have this.

View :

active_users = Language_new.objects.filter(expiration_date__gt=timezone.now(),
                                           is_active=True)

And if I want to include the last sent email, I'd do something like this

active_users = Language_new.objects.filter(expiration_date__gt=timezone.now(),
                                           is_active=True).prefetch_related(
                Prefetch(
                    "sentemail",
                    queryset=SentEmail.objects.filter().latest('sent_date'),
                    to_attr="last_sentemail"
                ))

Questions :

Is Django smart enough to make the relation here : queryset=SentEmail.objects.filter().latest('sent_date')? If not, how do I reference each user in the filter()?

Also, as there might not be a "latest" sentemail for every AppUser, how do I handle the DoesNotExist in this kind of query?

Code execution :

Executing this code gets me an AttributeError

AttributeError: 'SentEmail' object has no attribute '_iterable_class'

Pointing on this line : to_attr="last_sentemail". I don't know why.

Ugly solution :

Note that I have a working solution that look like this :

active_users = Language_new.objects.filter(expiration_date__gt=timezone.now(),
                                               is_active=True)

active_users = list(active_users)
for user in active_users:
     last_sent_mail = SentEmail.objects.filter(user=active_user.user)
     if last_sent_mail.exists():
         setattr(user, 'last_sent_mail_date', last_sent_mail.latest('sent_date').sent_date)
     else:
         setattr(user, 'last_sent_mail_date', 'None')

It's working, but I think we all agree that it's not the best one around.

Note : Here I retreive the date of the last sentemail directly, but having the full sentemail or its date both work for me.

Thanks.

Upvotes: 1

Views: 1338

Answers (1)

alfonso.kim
alfonso.kim

Reputation: 2900

try:

class SentEmail(models.Model):
    user = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name='sent_mails'
    )
    sent_date = models.DateTimeField(default=timezone.now)    
    sender = models.EmailField()

and

class AppUser(models.Model):
    user= models.OneToOneField(User, primary_key=True)
    is_active= models.BooleanField(default= False)
    expiration_date = models.DateTimeField(
         default=make_default_expiration_date
    ) #note : 90 days after the creation

    @property
    def last_sent_mail(self):  # or last_sent_mail_date
        sent_mails = self.user.sent_mails.order_by('sent_date')
        return sent_mails[0] if sent_mails.exists() else None

to retrieve the last email sent:

last_mail = user.last_sent_mail
if last_mail:
    print last_mail.sent_date

Upvotes: 1

Related Questions