Opened 5 years ago

Closed 5 years ago

#14931 closed (invalid)

models.DateTimeField hard to inherit from if auto_add is used

Reported by: jtheoof Owned by: nobody
Component: Database layer (models, ORM) Version: 1.2
Severity: Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: UI/UX:

Description

Suppose I want to override models.DateTimeField with this model inspired from Snippet [388](http://djangosnippets.org/snippets/388)

import datetime
import logging
import warnings
import pytz

from django.conf import settings
from django.db import models
from django.dispatch import dispatcher
from django.db.models import signals

LOG = logging.getLogger(__name__)

try:
    pytz_exc = (pytz.UnknownTimeZoneError,)
except AttributeError:
    # older versions of pytz
    pytz_exc = (IOError, KeyError)

TIMEZONES = tuple((t, t) for t in pytz.all_timezones)

class UTCDateTimeField(models.DateTimeField):
    
    def __init__(self, *args, **kw):
        self.default_tz = kw.get('default_tz')
        if 'default_tz' in kw:
            del(kw['default_tz'])
        super(UTCDateTimeField, self).__init__(*args, **kw)
        self._name = None
        
    def get_internal_type(self):
        return 'DateTimeField'

    def _model_init(self, signal, sender, instance, **kw):
        setattr(instance, "_%s" % self._name, getattr(instance, self._name))
        setattr(instance, "_%s_utc" % self._name, getattr(instance, "%s_utc" % self._name))
        setattr(instance, "_%s_tz" % self._name, getattr(instance, "%s_tz" % self._name))
        
    def _model_pre_save(self, signal, sender, instance, **kw):
        dt = getattr(instance, self._name)
        utc = getattr(instance, "%s_utc" % self._name)
        tz = getattr(instance, "%s_tz" % self._name)
        if not tz:
            # try to get the default
            if isinstance(self.default_tz, basestring):
                tz = getattr(instance, self.default_tz)
                if isinstance(tz, basestring):
                    # use it as a timezone
                    pass
                elif callable(tz):
                    self.default_tz = tz
                    tz = None
                else:
                    warnings.warn("UTCDateTimeField cannot use default_tz %s in model %s" % (self.default_tz, instance))
                    self.default_tz = lambda: getattr(settings, 'TIME_ZONE', 'UTC')
                    tz = None
            if callable(self.default_tz):
                tz = self.default_tz()
                if not tz or not isinstance(tz, basestring):
                    warnings.warn("UTCDateTimeField cannot use default timezone %s, using Django default" % tz)
                    tz = getattr(settings, 'TIME_ZONE', 'UTC')
            if not tz:
                tz = getattr(settings, 'TIME_ZONE', 'UTC')
            setattr(instance, "%s_tz" % self._name, tz)
        _dt = getattr(instance, "_%s" % self._name)
        _utc = getattr(instance, "_%s_utc" % self._name)
        _tz = getattr(instance, "_%s_tz" % self._name)
        if isinstance(tz, unicode):
            tz = tz.encode('utf8')
        try:
            timezone = pytz.timezone(tz)
        except pytz_exc, e:
            warnings.warn("Error in timezone: %s" % e)
            setattr(instance, "%s_tz" % self._name, 'UTC')
            timezone = pytz.UTC
        if not dt and not utc:
            # do nothing
            return
        elif dt and dt != _dt:
            utc = UTCDateTimeField.utc_from_localtime(dt, timezone)
            setattr(instance, "%s_utc" % self._name, utc)
        elif utc and (utc != _utc or tz != _tz):
            dt = UTCDateTimeField.localtime_from_utc(utc, timezone)
            setattr(instance, "_%s" % self._name, dt)
        elif not dt:
            dt = UTCDateTimeField.localtime_from_utc(utc, timezone)
            setattr(instance, "_%s" % self._name, dt)
        elif not utc:
            utc = UTCDateTimeField.utc_from_localtime(dt, timezone)
            setattr(instance, "%s_utc" % self._name, utc)
        if dt.tzinfo:
            dt = dt.replace(tzinfo=None)
        if utc.tzinfo:
            utc = utc.replace(tzinfo=None)
        setattr(instance, "_%s" % self._name, dt)
        setattr(instance, "_%s_utc" % self._name, utc)
        setattr(instance, "_%s_tz" % self._name, tz)
        setattr(instance, "%s" % self._name, dt)
        setattr(instance, "%s_utc" % self._name, utc)
        
    def _model_post_save(self, signal, sender, instance, **kw):
        setattr(instance, "%s" % self._name, getattr(instance, "_%s" % self._name))
        setattr(instance, "%s_utc" % self._name, getattr(instance, "_%s_utc" % self._name))
        
    
    def contribute_to_class(self, cls, name):
        self._name = name

        super(UTCDateTimeField, self).contribute_to_class(cls, name)
        self.utc_field = models.DateTimeField(null=True, editable=False)
        self.creation_counter = self.utc_field.creation_counter + 1
        models.DateTimeField.contribute_to_class(self.utc_field, cls, '%s_utc' % name)
        self.tz_field = models.CharField(max_length=38, editable=False, choices=TIMEZONES, default=getattr(settings, 'TIME_ZONE', 'UTC'))
        self.creation_counter = self.tz_field.creation_counter + 1
        models.CharField.contribute_to_class(self.tz_field, cls, '%s_tz' % name)
        
        # add the properties for offset-aware datetimes
        def get_timezone(s):
            tz = getattr(s, '%s_tz' % name)
            if isinstance(tz, unicode):
                tz = tz.encode('utf8')
            return pytz.timezone(tz)
        setattr(cls, "%s_timezone" % name, property(get_timezone))
        
        def get_dt_offset_aware(s):
            dt = getattr(s, "%s_utc_offset_aware" % name)
            tz = getattr(s, "%s_timezone" % name)
            return dt.astimezone(tz)
        setattr(cls, "%s_offset_aware" % name, property(get_dt_offset_aware))
        
        def get_utc_offset_aware(s):
            return getattr(s, '%s_utc' % name).replace(tzinfo=pytz.utc)
        setattr(cls, "%s_utc_offset_aware" % name, property(get_utc_offset_aware))
        
        signals.post_init.connect(self._model_init, sender=cls)
        signals.pre_save.connect(self._model_pre_save, sender=cls)
        signals.post_save.connect(self._model_post_save, sender=cls)

    @staticmethod
    def localtime_from_utc(utc, tz):
        dt = utc.replace(tzinfo=pytz.utc)
        return dt.astimezone(tz)
        
    @staticmethod
    def utc_from_localtime(dt, tz):
        return tz.normalize(tz.localize(dt)).astimezone(pytz.utc)

Now with the following model:

class SimpleModel(models.Model):
    
    date_created = UTCDateTimeField(auto_now_add=True)
    #date_created = UTCDateTimeField(default=datetime.datetime.now)


Doing a simple:

    SimpleModel.objects.create()


Will fail because _model_pre_save is called with empty: getattr(instance, self._name) so dt is None
It works if I use the commented line instead

Looking through django's code django.db.models.fields.__init__.py I founds this:

def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs):
    self.auto_now, self.auto_now_add = auto_now, auto_now_add
    #HACKs : auto_now_add/auto_now should be done as a default or a pre_save.
    if auto_now or auto_now_add:
        kwargs['editable'] = False
        kwargs['blank'] = True
    Field.__init__(self, verbose_name, name, **kwargs)


at initialization of DateField, what's up with that?

Change History (1)

comment:1 Changed 5 years ago by russellm

  • Needs documentation unset
  • Needs tests unset
  • Patch needs improvement unset
  • Resolution set to invalid
  • Status changed from new to closed

I don't know. What's up with that?

Closing invalid. Wishing I could close incoherent.

Seriously, I have no idea what you're reporting here, other than "my code doesn't work". If that's what you're reporting, please post to django-users.

If you're reporting a specific problem with the way fields are initialized, or a problem with the way auto_now etc are handled, you need to give specifics as to what you consider the problem to be, explaining the sequence of events that don't make sense, and providing a *simple* example.

Note: See TracTickets for help on using tickets.
Back to Top