Saturday, July 13, 2013

Django's DateTimeField auto_now* Option Caveats

I'm new to Python and Django and had a run in with DateTimeField's auto_now and auto_now_new. I'm importing data from an existing database and there currently is a column called upd_stamp which I've renamed to modified. Looking through the documentation, I was happy that I could just add auto_now to the options to get the behavior I wanted without any additional code. Well it didn't and I'll explain why as well as the solution I came up with (thanks to a bit of Goggling and Stack Overflow).

created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)

The problem I ran into was that I wanted to keep the legacy timestamp from the old database when I loaded the record for the first time then have the auto_now kick in. The problem is that auto_now is very stupid (as it should be) and doesn't understand the concept of insert or update and whether your new record is coming in with pre-populated DateTimeField attribute. It gladly throws away any value away and always replaces the value with the current time. I didn't notice it on first import but soon realized auto_now wouldn't meet my needs.

After a quick search, I found this Stack Overflow question that had the perfect fix for my problem. It suggested implementing my own save that would allow me to pass in values for created or modified but would fall back to the current time. I changed the code a bit because I didn't need a created timestamp for my model, but this should give you an idea.

Note: you have to remove any additional values from kwargs before passing to the parent class or super.save will complain that created and modified aren't valid arguments.

import datetime

class User(models.Model):
    created  = models.DateTimeField(editable=False)
    modified = models.DateTimeField()

   def save(self, *args, **kwargs):

        # original Code: called datetime.datetime.today()
        #                multiple places.  Modified kwargs
        #
        # created = kwargs.pop('created', None)
        # if created:
        #     self.created = created
        # elif not self.id:
        #     self.created = datetime.datetime.today()
        # 
        # modified = kwargs.pop('modified', None)
        # if modified:
        #     self.modified = modified
        # else:
        #     self.modified = datetime.datetime.today()

        # Edit #1: Changed example to more efficient 
        #          version from blog comments.
        #          single today()
        # 
        # today = datetime.datetime.today()
        #
        # if not self.id
        #     self.created = kwargs.pop('created', today)
        # elif kwargs.get('created', None)
        #     self.created = kwargs.pop('created')
        #
        # self.modified = kwargs.pop('modified', today)


        # Edit #2: no longer pass arguments via save
        #          was orignially how I wanted to solve
        #          the problem and how I felt auto_now
        #          should have worked.  Thanks to
        #          indosauros from reddit. :)
        #
        #          On inital creation of the object
        #          only set today if values are not
        #          already populated.  auto_now*
        #          wipes them out regardless

        today = datetime.datetime.today()

        if not self.id:
            if not self.created:
                self.created = today
            if not self.modified:
                self.modified = today
        else:
            self.modified = today

        super(User, self).save(*args, **kwargs)

If you see any glaring python or style related issues, please let me know. I'm still finding my way pythonwise and would love any best practices feedback.

4 comments:

  1. Instead of using None as the default arg for kwargs.pop just use datetime.datetime.today()!

    today = datetime.datetime.today()
    self.created = kwargs.pop('modified',today)
    self.modified = kwargs.pop('modified',today)

    note I use the same value of today for both created and modified. If you call datetime.datetime.today() each for created and modified you may get slightly different values.




    ReplyDelete
  2. Aw, perfect. I think because python is so foreign to me, I'm not seeing those simple patterns. Thanks! Small bug that created gets set with today if or if created is passed in then it can be overridden anytime.

    today = datetime.datetime.today()

    if not self.id:
    self.created = kwargs.pop('created', today)
    elif kwargs.get('created',None)
    self.created = kwargs.pop('created')

    self.modified = kwargs.pop('modified', today)

    Thanks for taking time to respond.

    ReplyDelete
  3. Hello, but what to do when you use diferrent field as primary_key and you don't have access to self.id ?

    At this moment I do:

    try:
    p = Product.objects.get(reference=self.reference)
    self.date_created = p.date_created
    except self.DoesNotExist:
    pass

    but I'm not sure if this is the correct method

    ReplyDelete