Search code examples
pythondjangopostgresqldjango-admindjango-orm

django orm filter datetime by integer of days


I have a model called with AccessDuration with two important fields are duration (int), and lab_start_date (datetime). bassed on both fields, I want to check whether the access duration is expired or yet. It's done by using model functions.

class AccessDuration(models.Model):
    ....
    duration = models.PositiveIntegerField(default=30, help_text=_('In days'))
    lab_start_date = models.DateTimeField(verbose_name=('Start Date'), null=True)

    @property
    def expiration_date(self) -> Union[timezone.datetime, None]:
        if self.lab_start_date:
            return self.lab_start_date + timezone.timedelta(days=self.duration)
        return None

    @property
    def is_expired(self) -> bool:
        """ to check whether duration already expired or yet """
        if self.expiration_date:
            return timezone.now() > self.expiration_date
        return False

But, I need this filter to can be use in my django admin page. I have tried with this, but seems still doesn't working:

from django.db import models
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone

from .models import AccessDuration


class AccessListFilter(admin.SimpleListFilter):
    title = _('Is Expired')
    parameter_name = 'is_expired'

    def lookups(self, request, model_admin):
        return [
            ('true', _('True')),
            ('false', _('False'))
        ]

    def queryset(self, request, queryset):
        value = self.value()
        expiration_date = models.ExpressionWrapper(
            models.F('lab_start_date') + (timezone.timedelta(days=1) * models.F('duration')),
            output_field=models.DateTimeField()
        )
        if value == 'true':
            return queryset.annotate(expiration_date=expiration_date)\
                           .filter(expiration_date__lt=timezone.now())
        elif value == 'false':
            return queryset.annotate(expiration_date=expiration_date)\
                           .filter(expiration_date__gte=timezone.now())
        return queryset


class AccessAdmin(admin.ModelAdmin):
    list_filter = (AccessListFilter, 'lab_start_date')


admin.site.register(AccessDuration, AccessAdmin)

The error I found;

error


After solved by @William, I did some modification on manager;

class AccessDurationQuerySet(models.QuerySet):

    def filter_expiration(self, is_expired: bool = False):
        """
        To filter whether AccessDuration already expired or yet.
        don't use the same `expiration_date` as expression key,
        because it will clash with `AccessDuration.expiration_date` property.
        Issue: https://stackoverflow.com/q/69012110/6396981
        """
        kwargs = {'_expiration_date__gte': Now()}
        if is_expired:
            del kwargs['_expiration_date__gte']
            kwargs['_expiration_date__lt'] = Now()

        return self.all().annotate(
            _expiration_date=models.ExpressionWrapper(
                models.F('lab_start_date') + (timezone.timedelta(days=1) * models.F('duration')),
                output_field=models.DateTimeField()
            )
        ).filter(**kwargs)


class AccessDurationManager(
    models.Manager.from_queryset(AccessDurationQuerySet),
    DefaultManager
):

    def published(self):
        return super().published().filter(
            status=AccessDurationStatusChoices.active
        )


class AccessDuration(models.Model):
    ....
    objects = AccessDurationManager()

Then in our ORM;

AccessDuration.objects.filter_expiration(is_expired=True)

Or in our admin.py;

class ExpirationListFilter(admin.SimpleListFilter):
    title = _('Is Expired')
    parameter_name = 'is_expired'

    def lookups(self, request, model_admin):
        return [
            (True, True),
            (False, False)
        ]

    def queryset(self, request, queryset):
        value = str(self.value()).lower()
        if value == 'true':
            return queryset.filter_expiration(is_expired=True)
        elif value == 'false':
            return queryset.filter_expiration(is_expired=False)
        return queryset


class AccessAdmin(admin.ModelAdmin):
    ...
    list_filter = (ExpirationListFilter, ...)

New Update:

class AccessDurationQuerySet(models.QuerySet):
    def filter(self, *args, **kwargs):
        return (
            self.all()
            .annotate(
                _expiration_date=models.ExpressionWrapper(
                    models.F("lab_start_date")
                    + (timezone.timedelta(days=1) * models.F("duration")),
                    output_field=models.DateTimeField(),
                ),
                is_expired=models.Case(
                    models.When(_expiration_date__lt=Now(), then=True),
                    default=False,
                    output_field=models.BooleanField(),
                ),
            )
            .filter(*args, **kwargs)
        )

Then in ORM:

AccessDuration.objects.filter(is_expired=True)

Solution

  • You should use another name that expiration_date to annotate. For example:

    from django.db.models.functions import Now
    
    if value == 'true':
        return queryset.annotate(
            _expiration_date=expiration_date
        ).filter(_expiration_date__lt=Now())
    elif value == 'false':
        return queryset.annotate(
            _expiration_date=expiration_date
        ).filter(_expiration_date__gte=Now())