Fixed #1443 -- Django's various bits now support dates before 1900. Thanks to SmileyChris, Chris Green, Fredrik Lundh and others for patches and design help

git-svn-id: http://code.djangoproject.com/svn/django/trunk@7946 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Adrian Holovaty 2008-07-18 03:47:27 +00:00
parent f6fafc02c8
commit df2b19cc17
9 changed files with 162 additions and 19 deletions

View File

@ -8,6 +8,7 @@ from django.utils.translation import get_date_formats
from django.utils.encoding import force_unicode from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.views.generic import date_based from django.views.generic import date_based
from django.utils import datetime_safe
class CalendarPlugin(DatabrowsePlugin): class CalendarPlugin(DatabrowsePlugin):
def __init__(self, field_names=None): def __init__(self, field_names=None):
@ -33,12 +34,13 @@ class CalendarPlugin(DatabrowsePlugin):
def urls(self, plugin_name, easy_instance_field): def urls(self, plugin_name, easy_instance_field):
if isinstance(easy_instance_field.field, models.DateField): if isinstance(easy_instance_field.field, models.DateField):
d = easy_instance_field.raw_value
return [mark_safe(u'%s%s/%s/%s/%s/%s/' % ( return [mark_safe(u'%s%s/%s/%s/%s/%s/' % (
easy_instance_field.model.url(), easy_instance_field.model.url(),
plugin_name, easy_instance_field.field.name, plugin_name, easy_instance_field.field.name,
easy_instance_field.raw_value.year, d.year,
easy_instance_field.raw_value.strftime('%b').lower(), datetime_safe.new_date(d).strftime('%b').lower(),
easy_instance_field.raw_value.day))] d.day))]
def model_view(self, request, model_databrowse, url): def model_view(self, request, model_databrowse, url):
self.model, self.site = model_databrowse.model, model_databrowse.site self.model, self.site = model_databrowse.model, model_databrowse.site

View File

@ -8,6 +8,7 @@ except ImportError:
from StringIO import StringIO from StringIO import StringIO
from django.db import models from django.db import models
from django.utils.encoding import smart_str, smart_unicode from django.utils.encoding import smart_str, smart_unicode
from django.utils import datetime_safe
class SerializationError(Exception): class SerializationError(Exception):
"""Something bad happened during serialization.""" """Something bad happened during serialization."""
@ -59,7 +60,8 @@ class Serializer(object):
Convert a field's value to a string. Convert a field's value to a string.
""" """
if isinstance(field, models.DateTimeField): if isinstance(field, models.DateTimeField):
value = getattr(obj, field.name).strftime("%Y-%m-%d %H:%M:%S") d = datetime_safe.new_datetime(getattr(obj, field.name))
value = d.strftime("%Y-%m-%d %H:%M:%S")
else: else:
value = field.flatten_data(follow=None, obj=obj).get(field.name, "") value = field.flatten_data(follow=None, obj=obj).get(field.name, "")
return smart_unicode(value) return smart_unicode(value)

View File

@ -6,6 +6,7 @@ import datetime
from django.utils import simplejson from django.utils import simplejson
from django.core.serializers.python import Serializer as PythonSerializer from django.core.serializers.python import Serializer as PythonSerializer
from django.core.serializers.python import Deserializer as PythonDeserializer from django.core.serializers.python import Deserializer as PythonDeserializer
from django.utils import datetime_safe
try: try:
from cStringIO import StringIO from cStringIO import StringIO
except ImportError: except ImportError:
@ -20,7 +21,7 @@ class Serializer(PythonSerializer):
Convert a queryset to JSON. Convert a queryset to JSON.
""" """
internal_use_only = False internal_use_only = False
def end_serialization(self): def end_serialization(self):
self.options.pop('stream', None) self.options.pop('stream', None)
self.options.pop('fields', None) self.options.pop('fields', None)
@ -51,9 +52,11 @@ class DjangoJSONEncoder(simplejson.JSONEncoder):
def default(self, o): def default(self, o):
if isinstance(o, datetime.datetime): if isinstance(o, datetime.datetime):
return o.strftime("%s %s" % (self.DATE_FORMAT, self.TIME_FORMAT)) d = datetime_safe.new_datetime(o)
return d.strftime("%s %s" % (self.DATE_FORMAT, self.TIME_FORMAT))
elif isinstance(o, datetime.date): elif isinstance(o, datetime.date):
return o.strftime(self.DATE_FORMAT) d = datetime_safe.new_date(o)
return d.strftime(self.DATE_FORMAT)
elif isinstance(o, datetime.time): elif isinstance(o, datetime.time):
return o.strftime(self.TIME_FORMAT) return o.strftime(self.TIME_FORMAT)
elif isinstance(o, decimal.Decimal): elif isinstance(o, decimal.Decimal):

View File

@ -141,10 +141,6 @@ def _isValidDate(date_string):
# Could use time.strptime here and catch errors, but datetime.date below # Could use time.strptime here and catch errors, but datetime.date below
# produces much friendlier error messages. # produces much friendlier error messages.
year, month, day = map(int, date_string.split('-')) year, month, day = map(int, date_string.split('-'))
# This check is needed because strftime is used when saving the date
# value to the database, and strftime requires that the year be >=1900.
if year < 1900:
raise ValidationError, _('Year must be 1900 or later.')
try: try:
date(year, month, day) date(year, month, day)
except ValueError, e: except ValueError, e:
@ -407,12 +403,12 @@ class IsAPowerOf(object):
""" """
Usage: If you create an instance of the IsPowerOf validator: Usage: If you create an instance of the IsPowerOf validator:
v = IsAPowerOf(2) v = IsAPowerOf(2)
The following calls will succeed: The following calls will succeed:
v(4, None) v(4, None)
v(8, None) v(8, None)
v(16, None) v(16, None)
But this call: But this call:
v(17, None) v(17, None)
will raise "django.core.validators.ValidationError: ['This value must be a power of 2.']" will raise "django.core.validators.ValidationError: ['This value must be a power of 2.']"

View File

@ -23,6 +23,7 @@ from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy, ugettext as _ from django.utils.translation import ugettext_lazy, ugettext as _
from django.utils.encoding import smart_unicode, force_unicode, smart_str from django.utils.encoding import smart_unicode, force_unicode, smart_str
from django.utils.maxlength import LegacyMaxlength from django.utils.maxlength import LegacyMaxlength
from django.utils import datetime_safe
class NOT_PROVIDED: class NOT_PROVIDED:
pass pass
@ -557,7 +558,7 @@ class DateField(Field):
if lookup_type in ('range', 'in'): if lookup_type in ('range', 'in'):
value = [smart_unicode(v) for v in value] value = [smart_unicode(v) for v in value]
elif lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte') and hasattr(value, 'strftime'): elif lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte') and hasattr(value, 'strftime'):
value = value.strftime('%Y-%m-%d') value = datetime_safe.new_date(value).strftime('%Y-%m-%d')
else: else:
value = smart_unicode(value) value = smart_unicode(value)
return Field.get_db_prep_lookup(self, lookup_type, value) return Field.get_db_prep_lookup(self, lookup_type, value)
@ -589,7 +590,7 @@ class DateField(Field):
# Casts dates into string format for entry into database. # Casts dates into string format for entry into database.
if value is not None: if value is not None:
try: try:
value = value.strftime('%Y-%m-%d') value = datetime_safe.new_date(value).strftime('%Y-%m-%d')
except AttributeError: except AttributeError:
# If value is already a string it won't have a strftime method, # If value is already a string it won't have a strftime method,
# so we'll just let it pass through. # so we'll just let it pass through.
@ -601,7 +602,11 @@ class DateField(Field):
def flatten_data(self, follow, obj=None): def flatten_data(self, follow, obj=None):
val = self._get_val_from_obj(obj) val = self._get_val_from_obj(obj)
return {self.attname: (val is not None and val.strftime("%Y-%m-%d") or '')} if val is None:
data = ''
else:
data = datetime_safe.new_date(val).strftime("%Y-%m-%d")
return {self.attname: data}
def formfield(self, **kwargs): def formfield(self, **kwargs):
defaults = {'form_class': forms.DateField} defaults = {'form_class': forms.DateField}
@ -668,8 +673,13 @@ class DateTimeField(DateField):
def flatten_data(self,follow, obj = None): def flatten_data(self,follow, obj = None):
val = self._get_val_from_obj(obj) val = self._get_val_from_obj(obj)
date_field, time_field = self.get_manipulator_field_names('') date_field, time_field = self.get_manipulator_field_names('')
return {date_field: (val is not None and val.strftime("%Y-%m-%d") or ''), if val is None:
time_field: (val is not None and val.strftime("%H:%M:%S") or '')} date_data = time_data = ''
else:
d = datetime_safe.new_datetime(val)
date_data = d.strftime('%Y-%m-%d')
time_data = d.strftime('%H:%M:%S')
return {date_field: date_data, time_field: time_data}
def formfield(self, **kwargs): def formfield(self, **kwargs):
defaults = {'form_class': forms.DateTimeField} defaults = {'form_class': forms.DateTimeField}

View File

@ -9,6 +9,7 @@ from django.utils.datastructures import DotExpandedDict
from django.utils.text import capfirst from django.utils.text import capfirst
from django.utils.encoding import smart_str from django.utils.encoding import smart_str
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils import datetime_safe
def add_manipulators(sender): def add_manipulators(sender):
cls = sender cls = sender
@ -332,5 +333,6 @@ def manipulator_validator_unique_for_date(from_field, date_field, opts, lookup_t
pass pass
else: else:
format_string = (lookup_type == 'date') and '%B %d, %Y' or '%B %Y' format_string = (lookup_type == 'date') and '%B %d, %Y' or '%B %Y'
date_val = datetime_safe.new_datetime(date_val)
raise validators.ValidationError, "Please enter a different %s. The one you entered is already being used for %s." % \ raise validators.ValidationError, "Please enter a different %s. The one you entered is already being used for %s." % \
(from_field.verbose_name, date_val.strftime(format_string)) (from_field.verbose_name, date_val.strftime(format_string))

View File

@ -15,6 +15,7 @@ from django.utils.html import escape, conditional_escape
from django.utils.translation import ugettext from django.utils.translation import ugettext
from django.utils.encoding import StrAndUnicode, force_unicode from django.utils.encoding import StrAndUnicode, force_unicode
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils import datetime_safe
from util import flatatt from util import flatatt
__all__ = ( __all__ = (
@ -170,6 +171,7 @@ class DateTimeInput(Input):
if value is None: if value is None:
value = '' value = ''
elif hasattr(value, 'strftime'): elif hasattr(value, 'strftime'):
value = datetime_safe.new_datetime(value)
value = value.strftime(self.format) value = value.strftime(self.format)
return super(DateTimeInput, self).render(name, value, attrs) return super(DateTimeInput, self).render(name, value, attrs)

View File

@ -0,0 +1,89 @@
# Python's datetime strftime doesn't handle dates before 1900.
# These classes override date and datetime to support the formatting of a date
# through its full "proleptic Gregorian" date range.
#
# Based on code submitted to comp.lang.python by Andrew Dalke
#
# >>> datetime_safe.date(1850, 8, 2).strftime("%Y/%M/%d was a %A")
# '1850/08/02 was a Friday'
from datetime import date as real_date, datetime as real_datetime
import re
import time
class date(real_date):
def strftime(self, fmt):
return strftime(self, fmt)
class datetime(real_datetime):
def strftime(self, fmt):
return strftime(self, fmt)
def combine(self, date, time):
return datetime(date.year, date.month, date.day, time.hour, time.minute, time.microsecond, time.tzinfo)
def date(self):
return date(self.year, self.month, self.day)
def new_date(d):
"Generate a safe date from a datetime.date object."
return date(d.year, d.month, d.day)
def new_datetime(d):
"""
Generate a safe datetime from a datetime.date or datetime.datetime object.
"""
kw = [d.year, d.month, d.day]
if isinstance(d, real_datetime):
kw.extend([d.hour, d.minute, d.second, d.microsecond, d.tzinfo])
return datetime(*kw)
# This library does not support strftime's "%s" or "%y" format strings.
# Allowed if there's an even number of "%"s because they are escaped.
_illegal_formatting = re.compile(r"((^|[^%])(%%)*%[sy])")
def _findall(text, substr):
# Also finds overlaps
sites = []
i = 0
while 1:
j = text.find(substr, i)
if j == -1:
break
sites.append(j)
i=j+1
return sites
def strftime(dt, fmt):
if dt.year >= 1900:
return super(type(dt), dt).strftime(fmt)
illegal_formatting = _illegal_formatting.search(fmt)
if illegal_formatting:
raise TypeError("strftime of dates before 1900 does not handle" + illegal_formatting.group(0))
year = dt.year
# For every non-leap year century, advance by
# 6 years to get into the 28-year repeat cycle
delta = 2000 - year
off = 6 * (delta // 100 + delta // 400)
year = year + off
# Move to around the year 2000
year = year + ((2000 - year) // 28) * 28
timetuple = dt.timetuple()
s1 = time.strftime(fmt, (year,) + timetuple[1:])
sites1 = _findall(s1, str(year))
s2 = time.strftime(fmt, (year+28,) + timetuple[1:])
sites2 = _findall(s2, str(year+28))
sites = []
for site in sites1:
if site in sites2:
sites.append(site)
s = s1
syear = "%4d" % (dt.year,)
for site in sites:
s = s[:site] + syear + s[site+4:]
return s

View File

@ -0,0 +1,37 @@
r"""
>>> from datetime import date as original_date, datetime as original_datetime
>>> from django.utils.datetime_safe import date, datetime
>>> just_safe = (1900, 1, 1)
>>> just_unsafe = (1899, 12, 31, 23, 59, 59)
>>> really_old = (20, 1, 1)
>>> more_recent = (2006, 1, 1)
>>> original_datetime(*more_recent) == datetime(*more_recent)
True
>>> original_datetime(*really_old) == datetime(*really_old)
True
>>> original_date(*more_recent) == date(*more_recent)
True
>>> original_date(*really_old) == date(*really_old)
True
>>> original_date(*just_safe).strftime('%Y-%m-%d') == date(*just_safe).strftime('%Y-%m-%d')
True
>>> original_datetime(*just_safe).strftime('%Y-%m-%d') == datetime(*just_safe).strftime('%Y-%m-%d')
True
>>> date(*just_unsafe[:3]).strftime('%Y-%m-%d (weekday %w)')
'1899-12-31 (weekday 0)'
>>> date(*just_safe).strftime('%Y-%m-%d (weekday %w)')
'1900-01-01 (weekday 1)'
>>> datetime(*just_unsafe).strftime('%Y-%m-%d %H:%M:%S (weekday %w)')
'1899-12-31 23:59:59 (weekday 0)'
>>> datetime(*just_safe).strftime('%Y-%m-%d %H:%M:%S (weekday %w)')
'1900-01-01 00:00:00 (weekday 1)'
>>> date(*just_safe).strftime('%y') # %y will error before this date
'00'
>>> datetime(*just_safe).strftime('%y')
'00'
"""