21

I want my users to enter their birth year. I don't want them to type the same in the form rather select the year from available options. I known that I can do something like this in my model if I needed to date instead of year:

class MyModel(models.Model):

    birthday = models.DateField(null=True, blank=True)

I can do this in forms to let the user choose date from datepicker.

    birthday = forms.fields.DateField(widget=forms.widgets.DateInput(attrs={'type': 'date'}))

For year, I can use a CharField/IntegerField with choices similar to what has been done in this SO answer.

import datetime
YEAR_CHOICES = [(r,r) for r in range(1984, datetime.date.today().year+1)]

year = models.IntegerField(_('year'), choices=YEAR_CHOICES, default=datetime.datetime.now().year)

The problem, however, is that change of current year from say, 2018 to 2019, will not change the available options.

Can you help or provide hints to achieve what I want to do?

inquilabee
  • 713
  • 1
  • 11
  • 23

4 Answers4

36

My initial thought was to write a callable that returns the choices, that will be evaluated for each request.

import datetime

def year_choices():
    return [(r,r) for r in range(1984, datetime.date.today().year+1)]

def current_year():
    return datetime.date.today().year

class MyModel(models.Model):
    year = models.IntegerField(_('year'), choices=year_choices, default=current_year)

However this doesn't work, because Django's check framework doesn't allow the year_choices to be used as the default. Even if you could hack the choices to be generated dynamically, it would have the disadvantage that Django would try to create a migration each year when the choices change.

You can avoid this by generating the choices at the form level instead. You can use validators in the model to prevent invalid data. Note that MaxValueValidator is wrapped in a function max_value_current_year to avoid a new migration every year.

import datetime
from django.core.validators import MaxValueValidator, MinValueValidator

def current_year():
    return datetime.date.today().year

def max_value_current_year(value):
    return MaxValueValidator(current_year())(value)    

class MyModel(models.Model):
    year = models.IntegerField(_('year'), validators=[MinValueValidator(1984), max_value_current_year])

def year_choices():
    return [(r,r) for r in range(1984, datetime.date.today().year+1)]

class MyForm(forms.ModelForm):
    year = forms.TypedChoiceField(coerce=int, choices=year_choices, initial=current_year)
cacti5
  • 2,006
  • 2
  • 25
  • 33
Alasdair
  • 298,606
  • 55
  • 578
  • 516
  • It will also be evaluated every time you run `makemigrations`, -1. – Adam Barnes Mar 01 '18 at 13:57
  • @AdamBarnes good point. Since it's a year field not a day field, you might decide to put up with this, but you could move the validation to the form if you preferred. – Alasdair Mar 01 '18 at 14:06
  • I most certainly _would_ prefer. I would go as far as to say putting it on the model is incorrect, and putting it on the form is correct. +1 now you've added the form example. – Adam Barnes Mar 01 '18 at 14:08
  • Moving validation to the form has its drawbacks -- you can end up with invalid data in your database if you forget to use the form. In this case, you can add validators to the model field and generate the choices in the field. – Alasdair Mar 01 '18 at 14:52
  • I get your point, but I'm tempted to call that argument moot... There are a billion ways I can put invalid data in the database. This is just about only letting the user select certain years of birth, I may want to do something with that in the future which changes it to a value they weren't able to select, for very good reasons. What if a user was born in 1984, for instance? – Adam Barnes Mar 01 '18 at 15:05
  • Validating years can be important, say you're not allowed users under 18. Moving validation from the model to the form increases the risk of forgetting to validate - I think that's a valid consideration, even if there are other ways of avoiding model validation. – Alasdair Mar 01 '18 at 15:17
  • @Alasdair Thanks. The solution worked for me. I just had to remove `default` argument from `TypedChoiceField` since the same raised an error (unexpected argument). – inquilabee Mar 01 '18 at 15:46
  • 1
    @Inquilabi glad it worked for you. It should be `initial` for form fields, not `default`. – Alasdair Mar 01 '18 at 15:55
6

You can put the options(years) in the form, using the IntegerField min_value and max_value. In model you can use the IntegerField without choices.

So, you won't worry about when the year change, because you will only change the options in the form.

If you want to change the year automatically, this could help you: Django forms integerField set max_value on runtime

klassmann
  • 518
  • 10
  • 14
6

For those who don't prefer having long choice fields i.e. in @Alasdair solution imagine having a choice field from 1984 to 2019, such a long choice list. How about having a field you can type the year and still have a way to increment or decrements. To solve this my way, use models.PositiveIntegerField with MinValueValidator and MaxValueValidator as shown below:

import datetime
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models


def current_year():
    return datetime.date.today().year


def max_value_current_year(value):
    return MaxValueValidator(current_year())(value)


class EmployeesTrainings(models.Model):
    name = models.ForeignKey(employee, default='', blank=True, null=True, on_delete=models.CASCADE)
    training_name = models.TextField(max_length=200, default='', blank=False, null=False)
    training_year = models.PositiveIntegerField(
        default=current_year(), validators=[MinValueValidator(1984), max_value_current_year])
Akash Jobanputra
  • 322
  • 1
  • 3
  • 16
Ngatia Frankline
  • 2,897
  • 2
  • 20
  • 19
4

Keypoints of my Approach

  • In the model, the year is an Integerfield.
  • In the form, I used a ChoiceField (to keep the user experience simple by using a Select. I didn't want o keep an open input to avoid years like 1990, or 90.)
  • And to populate the Choices of the Select, a pretty simple script does the job.
    • Datetime gives the updated year.

Here is the code

models.py

year_founded = models.IntegerField(blank=True)

forms.py

from datetime import datetime

year_founded = forms.ChoiceField(
    choices=possible_years(((datetime.now()).year), 1900),
    label='Año de Fundación',
)

script to populate choices in the form

def possible_years(first_year_in_scroll, last_year_in_scroll):
    p_year = []
    for i in range(first_year_in_scroll, last_year_in_scroll, -1):
        p_year_tuple = str(i), i
        p_year.append(p_year_tuple)
    return p_year
Oilycoyote
  • 111
  • 1
  • 3
  • 1
    A list comprehension or generator expression could simplify possible_years(): p_year = [ ( str(i), i ) for i in range(first_year_in_scroll, last_year_in_scroll, -1 ) ] or p_year = tuple( ( ( str(i), i ) for i in range(first_year_in_scroll, last_year_in_scroll, -1 ) ) ) (Programmers reach nirvana when they can accomplish a lot in one line of code.) – Rick Graves Aug 11 '21 at 23:22