How do I get each distinct 'year' value out of a Django model with a datetime field?
Question:
I can successfully filter by a given year in my Django model, but I’m having trouble finding a way to list valid years so a user can access them.
I have a django model with a defined ‘datetime’ field, oh-so-originally named ‘date’. In my templates, I can successfully access the ‘bar.date.date.year’ field, so I know it exists, but when I try the following function…
blog_years=[]
for entry in blog_entries:
if entry.date.date.year not in blog_years:
blog_years.append(entry.date.date.year)
I’m told that “‘builtin_function_or_method’ object has no attribute ‘year'”
I can only assume I”m tripping over some aspect of Python I’m not familiar with, but I can’t figure out what it is. I’m quite certain it has to be syntactical, but past that…
Answers:
The first .date
accesses a datetime object.
The second .date
is accessing a method on datetime objects that returns a date object but not calling it (this step is unneccessary).
The last part (the way you wrote it) is trying to access the year
attribute of the date method, instead of accessing the year
attribute of the result of the date method call.
Correcting the code to see the difference, it would look like this…
blog_years=[]
for entry in blog_entries:
if entry.date.date().year not in blog_years:
blog_years.append(entry.date.date().year)
But what you should do is more like this…
blog_years=[]
for entry in blog_entries:
if entry.date.year not in blog_years:
blog_years.append(entry.date.year)
since datetime objects have the date attribute as well.
date()
is a method of datetime
, use
blog_years=[]
for entry in blog_entries:
if entry.date.date().year not in blog_years:
blog_years.append(entry.date.date().year)
It might not be what you exactly expects (it can return years without blog posts):
from datetime import date
from django.db.models import Min
def blog_years():
current_year = date.today().year
queryset = Entry.objects.annotate(Min('date')).order_by('date')
if queryset:
oldest_year = queryset[0].date.date().year
else:
oldest_year = current_year
return range(oldest_year, current_year + 1)
A Python set
does not allow duplicates, so if you wanted a list of distinct years:
blog_years = list(set([entry.date.year for entry in blog_entries]))
Or, you could use distinct()
:
blog_years = blog_entries.distinct(entry__date__year).values(entry__date__year)
Of course, adjust the above based on your model.
if you are using postgres, you can do
BlogEntry.objects.extra(select={"year": "EXTRACT(YEAR FROM date)"}).distinct().values_list("year", flat=True)
Django has an elegant and efficient way of doing this. You can check from their docs https://docs.djangoproject.com/en/3.0/ref/models/querysets/#dates
But to go over it
Entry.objects.dates('pub_date', 'year')
This will bring out distinct year values in the query.
from django 1.10 it has become very simple
from django.db.models.functions import ExtractYear
blog_years= blog_entries.annotate(
year=ExtractYear('created_on')
).values_list('year', flat=True)
blog_years = sorted(set(blog_years), reverse=True)
ModelName.objects.dates('column_name', 'year')
I can successfully filter by a given year in my Django model, but I’m having trouble finding a way to list valid years so a user can access them.
I have a django model with a defined ‘datetime’ field, oh-so-originally named ‘date’. In my templates, I can successfully access the ‘bar.date.date.year’ field, so I know it exists, but when I try the following function…
blog_years=[]
for entry in blog_entries:
if entry.date.date.year not in blog_years:
blog_years.append(entry.date.date.year)
I’m told that “‘builtin_function_or_method’ object has no attribute ‘year'”
I can only assume I”m tripping over some aspect of Python I’m not familiar with, but I can’t figure out what it is. I’m quite certain it has to be syntactical, but past that…
The first .date
accesses a datetime object.
The second .date
is accessing a method on datetime objects that returns a date object but not calling it (this step is unneccessary).
The last part (the way you wrote it) is trying to access the year
attribute of the date method, instead of accessing the year
attribute of the result of the date method call.
Correcting the code to see the difference, it would look like this…
blog_years=[]
for entry in blog_entries:
if entry.date.date().year not in blog_years:
blog_years.append(entry.date.date().year)
But what you should do is more like this…
blog_years=[]
for entry in blog_entries:
if entry.date.year not in blog_years:
blog_years.append(entry.date.year)
since datetime objects have the date attribute as well.
date()
is a method of datetime
, use
blog_years=[]
for entry in blog_entries:
if entry.date.date().year not in blog_years:
blog_years.append(entry.date.date().year)
It might not be what you exactly expects (it can return years without blog posts):
from datetime import date
from django.db.models import Min
def blog_years():
current_year = date.today().year
queryset = Entry.objects.annotate(Min('date')).order_by('date')
if queryset:
oldest_year = queryset[0].date.date().year
else:
oldest_year = current_year
return range(oldest_year, current_year + 1)
A Python set
does not allow duplicates, so if you wanted a list of distinct years:
blog_years = list(set([entry.date.year for entry in blog_entries]))
Or, you could use distinct()
:
blog_years = blog_entries.distinct(entry__date__year).values(entry__date__year)
Of course, adjust the above based on your model.
if you are using postgres, you can do
BlogEntry.objects.extra(select={"year": "EXTRACT(YEAR FROM date)"}).distinct().values_list("year", flat=True)
Django has an elegant and efficient way of doing this. You can check from their docs https://docs.djangoproject.com/en/3.0/ref/models/querysets/#dates
But to go over it
Entry.objects.dates('pub_date', 'year')
This will bring out distinct year values in the query.
from django 1.10 it has become very simple
from django.db.models.functions import ExtractYear
blog_years= blog_entries.annotate(
year=ExtractYear('created_on')
).values_list('year', flat=True)
blog_years = sorted(set(blog_years), reverse=True)
ModelName.objects.dates('column_name', 'year')