Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Date Conveniences (Validation, Display, etc) for Partial Dates in Django

I am trying to store dates in my database that often lack the month and day. My current approach (there seem to be many different ways of doing this) is to use three fields for this:

dob_year
dob_month
dob_day

I'd like to get as many of the benefits of DateFields that I can. The most important one I can think of now is validation. Valid:

2010
2010,02
2010,02,28

Invalid:

2010,02,30

There's also the problem of converting ints into a human readable form in the templates. It's great to be able to say:

<p>{{my_date|date:"%Y"}}</p>

But with this, I'll have to do something very strange because I'll need it to support partial dates and regular ones. I'm thinking I might be able to accomplish this with a @property method, but I haven't sorted this out yet either.

I'm sure there are other conveniences I'm giving up too (like date widgets in admin). Ideas here are welcome too since I know partial dates are a common issue, but I'm mostly concerned with validation at the moment.

Update: A friend on Twitter pointed out that using three fields for this creates horrible date queries. Do not use three fields for partial dates unless you want to think about how to handle queries like "between July of 2011 and June of 2012".

like image 865
mlissner Avatar asked May 08 '15 23:05

mlissner


1 Answers

Store dates in database as text is a bad practice. It is not portable, not django query api friendly, and not index friendly.

You can store all date in database even not all date is needed. For example, Oracle stores date and time in dates even you only need the date.

Then you can use a DateField + "ChoiceField" to store partialdate and relevant part information. Ex: 2015-02-01 truncate at month level. 2015-02-28 Full date. 2015-01-01 truncate at year level. Code:

class Item(models.Model):
    PARTIAL_YEAR='%Y'
    PARTIAL_MONTH='%Y-%m'
    PARTIAL_DAY='%Y-%m-%d'
    PARTIAL_CHOICES = (
      (PARTIAL_YEAR, 'Year'),
      (PARTIAL_MONTH, 'Month'),
      (PARTIAL_DAY, 'Day'),
    )
    partial_date_date = models.DateField()
    partial_date_part = models.CharField('Date part', 
                                          choices=PARTIAL_CHOICES,  
                                          max_length=10, )

Validate Both fields will be validated by each own widget. You can add a form clean (Cleaning and validating fields that depend on each other) or model clean validation level.

Quering Easy to make conditioned queries using Q objects:

q_is_year   = q( partial_date_part = Item.PARTIAL_YEAR )
q_by_year   = q( partial_date_date__year = 2015 )

q_is_month  = q( partial_date_part = Item.PARTIAL_MONTH )
q_by_month  = q( partial_date_date__year = 2105 ) 
q_by_month &= q( partial_date_date__month = 2 ) 

qs = Item.objects.filter( q_is_year&q_by_year | q_is_month&q_by_month )

Render display In order to render in template:

<p>{{item.partial_date_date|date:item.partial_date_part}}</p>

Render form To render form controls you can use JavaScript to change UI and help user with data entry:

  date type: (*) Year    ( ) Month     ( ) Day
  date:      [ change form widget dynamically  ]

You can send 3 widgets controls to form and show just one at a time. Change visibility on changing radio date type selection. I use MultiWidget.

EDITED 13 August 2015 with all sample code:

models.py

from django.db import models
from datetime import date

class Item(models.Model):
    PARTIAL_YEAR='%Y'
    PARTIAL_MONTH='%Y-%m'
    PARTIAL_DAY='%Y-%m-%d'
    PARTIAL_CHOICES = (
      (PARTIAL_YEAR, 'Year'),
      (PARTIAL_MONTH, 'Month'),
      (PARTIAL_DAY, 'Day'),
    )
    partial_date_part = models.CharField('Date part', 
                                          choices=PARTIAL_CHOICES,  
                                          max_length=10, )
    partial_date_date = models.DateField()
    some_comment = models.CharField('Comment', max_length=100, )

    def save(self, *args, **kwargs):
        if self.partial_date_part==self.PARTIAL_YEAR:
            self.partial_date_date = date( self.partial_date_date.year, 1, 1 )
        elif self.partial_date_part==self.PARTIAL_MONTH:
            self.partial_date_date = date( self.partial_date_date.year, 
                                          self.partial_date_date.month, 1 )  
        super(Item, self).save(*args, **kwargs)    

forms.py

from django import forms
from django.forms import widgets
from datetime import date

class DateSelectorWidget(widgets.MultiWidget):
    def __init__(self, attrs=None):
        days = [(d, d) for d in range(1,32)]
        months = [(m, m) for m in range(1,13)]
        years = [(year, year) for year in (2011, 2012, 2013)]
        _widgets = (
            widgets.Select(attrs=attrs, choices=days),
            widgets.Select(attrs=attrs, choices=months),
            widgets.Select(attrs=attrs, choices=years),
        )
        super(DateSelectorWidget, self).__init__(_widgets, attrs)

    def decompress(self, value):
        if value:
            return [value.day, value.month, value.year]
        return [None, None, None]

    def format_output(self, rendered_widgets):
        return ''.join(rendered_widgets)

    def value_from_datadict(self, data, files, name):
        datelist = [
            widget.value_from_datadict(data, files, name + '_%s' % i)
            for i, widget in enumerate(self.widgets)]
        D = date(
                day=int(datelist[0]),month=int(datelist[1]),year=int(datelist[2]),
            )
        return D

class ItemForm(forms.Form):
    partial_date_part = forms.CharField(widget=forms.RadioSelect)
    partial_date_date = DateSelectorWidget( )

view.py

from django.http import HttpResponseRedirect
from django.views.generic import View
from models import Item
from django.forms.models import modelform_factory
from .forms import DateSelectorWidget
from django import forms
from django.forms import widgets    

class MyDatAppView(View):
    form_class = modelform_factory(Item ,
                                   exclude=[], 
                                   widgets={ 'partial_date_date': 
                                              DateSelectorWidget() ,})
    initial = {'some_comment': '-*-', }
    template_name = 'form.html'

    def get(self, request, *args, **kwargs):
        form = self.form_class(initial=self.initial)
        return render(request, self.template_name, {'form': form})

    def post(self, request, *args, **kwargs):
        form = self.form_class(request.POST)
        if form.is_valid():
            m=form.save()
            return HttpResponseRedirect('/')

        return render(request, self.template_name, {'form': form})

You should add javascript on template to hide/show date part fields when selected part changes.

like image 167
dani herrera Avatar answered Sep 30 '22 02:09

dani herrera