From b2231a28e888946aa14619ad785e5dc7cf8504ba Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 18 Jul 2008 15:47:10 +0000 Subject: [PATCH] newforms-admin: Merged from trunk up to [7947]. git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@7948 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- .../contrib/databrowse/plugins/calendars.py | 8 +- django/core/serializers/base.py | 4 +- django/core/serializers/json.py | 9 +- django/core/validators.py | 10 +-- django/db/backends/mysql/creation.py | 1 - django/db/backends/mysql_old/creation.py | 1 - django/db/backends/oracle/creation.py | 1 - django/db/backends/postgresql/creation.py | 1 - django/db/backends/sqlite3/creation.py | 1 - django/db/models/fields/__init__.py | 23 +++-- django/db/models/manipulators.py | 2 + django/newforms/widgets.py | 2 + django/utils/datetime_safe.py | 89 +++++++++++++++++++ django/utils/tzinfo.py | 14 ++- tests/regressiontests/datetime_safe/tests.py | 37 ++++++++ 15 files changed, 173 insertions(+), 30 deletions(-) create mode 100644 django/utils/datetime_safe.py create mode 100644 tests/regressiontests/datetime_safe/tests.py diff --git a/django/contrib/databrowse/plugins/calendars.py b/django/contrib/databrowse/plugins/calendars.py index a798868b55..a4524e20dd 100644 --- a/django/contrib/databrowse/plugins/calendars.py +++ b/django/contrib/databrowse/plugins/calendars.py @@ -8,6 +8,7 @@ from django.utils.translation import get_date_formats from django.utils.encoding import force_unicode from django.utils.safestring import mark_safe from django.views.generic import date_based +from django.utils import datetime_safe class CalendarPlugin(DatabrowsePlugin): def __init__(self, field_names=None): @@ -33,12 +34,13 @@ class CalendarPlugin(DatabrowsePlugin): def urls(self, plugin_name, easy_instance_field): if isinstance(easy_instance_field.field, models.DateField): + d = easy_instance_field.raw_value return [mark_safe(u'%s%s/%s/%s/%s/%s/' % ( easy_instance_field.model.url(), plugin_name, easy_instance_field.field.name, - easy_instance_field.raw_value.year, - easy_instance_field.raw_value.strftime('%b').lower(), - easy_instance_field.raw_value.day))] + d.year, + datetime_safe.new_date(d).strftime('%b').lower(), + d.day))] def model_view(self, request, model_databrowse, url): self.model, self.site = model_databrowse.model, model_databrowse.site diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index e22a35815b..f6943e543e 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -8,6 +8,7 @@ except ImportError: from StringIO import StringIO from django.db import models from django.utils.encoding import smart_str, smart_unicode +from django.utils import datetime_safe class SerializationError(Exception): """Something bad happened during serialization.""" @@ -59,7 +60,8 @@ class Serializer(object): Convert a field's value to a string. """ 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: value = field.flatten_data(follow=None, obj=obj).get(field.name, "") return smart_unicode(value) diff --git a/django/core/serializers/json.py b/django/core/serializers/json.py index 20797c02f6..a84206a0fe 100644 --- a/django/core/serializers/json.py +++ b/django/core/serializers/json.py @@ -6,6 +6,7 @@ import datetime from django.utils import simplejson from django.core.serializers.python import Serializer as PythonSerializer from django.core.serializers.python import Deserializer as PythonDeserializer +from django.utils import datetime_safe try: from cStringIO import StringIO except ImportError: @@ -20,7 +21,7 @@ class Serializer(PythonSerializer): Convert a queryset to JSON. """ internal_use_only = False - + def end_serialization(self): self.options.pop('stream', None) self.options.pop('fields', None) @@ -51,9 +52,11 @@ class DjangoJSONEncoder(simplejson.JSONEncoder): def default(self, o): 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): - return o.strftime(self.DATE_FORMAT) + d = datetime_safe.new_date(o) + return d.strftime(self.DATE_FORMAT) elif isinstance(o, datetime.time): return o.strftime(self.TIME_FORMAT) elif isinstance(o, decimal.Decimal): diff --git a/django/core/validators.py b/django/core/validators.py index 3ef0adeda6..f94db40c1b 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -141,10 +141,6 @@ def _isValidDate(date_string): # Could use time.strptime here and catch errors, but datetime.date below # produces much friendlier error messages. 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: date(year, month, day) except ValueError, e: @@ -407,12 +403,12 @@ class IsAPowerOf(object): """ Usage: If you create an instance of the IsPowerOf validator: v = IsAPowerOf(2) - + The following calls will succeed: - v(4, None) + v(4, None) v(8, None) v(16, None) - + But this call: v(17, None) will raise "django.core.validators.ValidationError: ['This value must be a power of 2.']" diff --git a/django/db/backends/mysql/creation.py b/django/db/backends/mysql/creation.py index efb351c07e..698b07548d 100644 --- a/django/db/backends/mysql/creation.py +++ b/django/db/backends/mysql/creation.py @@ -13,7 +13,6 @@ DATA_TYPES = { 'FileField': 'varchar(%(max_length)s)', 'FilePathField': 'varchar(%(max_length)s)', 'FloatField': 'double precision', - 'ImageField': 'varchar(%(max_length)s)', 'IntegerField': 'integer', 'IPAddressField': 'char(15)', 'NullBooleanField': 'bool', diff --git a/django/db/backends/mysql_old/creation.py b/django/db/backends/mysql_old/creation.py index efb351c07e..698b07548d 100644 --- a/django/db/backends/mysql_old/creation.py +++ b/django/db/backends/mysql_old/creation.py @@ -13,7 +13,6 @@ DATA_TYPES = { 'FileField': 'varchar(%(max_length)s)', 'FilePathField': 'varchar(%(max_length)s)', 'FloatField': 'double precision', - 'ImageField': 'varchar(%(max_length)s)', 'IntegerField': 'integer', 'IPAddressField': 'char(15)', 'NullBooleanField': 'bool', diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py index 2652aecfc7..2a8badebd5 100644 --- a/django/db/backends/oracle/creation.py +++ b/django/db/backends/oracle/creation.py @@ -20,7 +20,6 @@ DATA_TYPES = { 'FileField': 'NVARCHAR2(%(max_length)s)', 'FilePathField': 'NVARCHAR2(%(max_length)s)', 'FloatField': 'DOUBLE PRECISION', - 'ImageField': 'NVARCHAR2(%(max_length)s)', 'IntegerField': 'NUMBER(11)', 'IPAddressField': 'VARCHAR2(15)', 'NullBooleanField': 'NUMBER(1) CHECK ((%(qn_column)s IN (0,1)) OR (%(qn_column)s IS NULL))', diff --git a/django/db/backends/postgresql/creation.py b/django/db/backends/postgresql/creation.py index b3e374da27..a8877a7d9b 100644 --- a/django/db/backends/postgresql/creation.py +++ b/django/db/backends/postgresql/creation.py @@ -13,7 +13,6 @@ DATA_TYPES = { 'FileField': 'varchar(%(max_length)s)', 'FilePathField': 'varchar(%(max_length)s)', 'FloatField': 'double precision', - 'ImageField': 'varchar(%(max_length)s)', 'IntegerField': 'integer', 'IPAddressField': 'inet', 'NullBooleanField': 'boolean', diff --git a/django/db/backends/sqlite3/creation.py b/django/db/backends/sqlite3/creation.py index 54b75f23be..c1c2b3170d 100644 --- a/django/db/backends/sqlite3/creation.py +++ b/django/db/backends/sqlite3/creation.py @@ -12,7 +12,6 @@ DATA_TYPES = { 'FileField': 'varchar(%(max_length)s)', 'FilePathField': 'varchar(%(max_length)s)', 'FloatField': 'real', - 'ImageField': 'varchar(%(max_length)s)', 'IntegerField': 'integer', 'IPAddressField': 'char(15)', 'NullBooleanField': 'bool', diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index cb20ae51e2..879807d2d2 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -23,6 +23,7 @@ from django.utils.text import capfirst from django.utils.translation import ugettext_lazy, ugettext as _ from django.utils.encoding import smart_unicode, force_unicode, smart_str from django.utils.maxlength import LegacyMaxlength +from django.utils import datetime_safe class NOT_PROVIDED: pass @@ -542,7 +543,7 @@ class DateField(Field): if lookup_type in ('range', 'in'): value = [smart_unicode(v) for v in value] 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: value = smart_unicode(value) return Field.get_db_prep_lookup(self, lookup_type, value) @@ -574,7 +575,7 @@ class DateField(Field): # Casts dates into string format for entry into database. if value is not None: try: - value = value.strftime('%Y-%m-%d') + value = datetime_safe.new_date(value).strftime('%Y-%m-%d') except AttributeError: # If value is already a string it won't have a strftime method, # so we'll just let it pass through. @@ -586,7 +587,11 @@ class DateField(Field): def flatten_data(self, follow, obj=None): 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): defaults = {'form_class': forms.DateField} @@ -653,8 +658,13 @@ class DateTimeField(DateField): def flatten_data(self,follow, obj = None): val = self._get_val_from_obj(obj) date_field, time_field = self.get_manipulator_field_names('') - return {date_field: (val is not None and val.strftime("%Y-%m-%d") or ''), - time_field: (val is not None and val.strftime("%H:%M:%S") or '')} + if val is None: + 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): defaults = {'form_class': forms.DateTimeField} @@ -917,9 +927,6 @@ class ImageField(FileField): if not self.height_field: setattr(cls, 'get_%s_height' % self.name, curry(cls._get_FIELD_height, field=self)) - def get_internal_type(self): - return "ImageField" - def save_file(self, new_data, new_object, original_object, change, rel, save=True): FileField.save_file(self, new_data, new_object, original_object, change, rel, save) # If the image has height and/or width field(s) and they haven't diff --git a/django/db/models/manipulators.py b/django/db/models/manipulators.py index 1b2587940e..4e6ddca26e 100644 --- a/django/db/models/manipulators.py +++ b/django/db/models/manipulators.py @@ -9,6 +9,7 @@ from django.utils.datastructures import DotExpandedDict from django.utils.text import capfirst from django.utils.encoding import smart_str from django.utils.translation import ugettext as _ +from django.utils import datetime_safe def add_manipulators(sender): cls = sender @@ -327,5 +328,6 @@ def manipulator_validator_unique_for_date(from_field, date_field, opts, lookup_t pass else: 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." % \ (from_field.verbose_name, date_val.strftime(format_string)) diff --git a/django/newforms/widgets.py b/django/newforms/widgets.py index e266fe4e1e..2c9f3c2eba 100644 --- a/django/newforms/widgets.py +++ b/django/newforms/widgets.py @@ -15,6 +15,7 @@ from django.utils.html import escape, conditional_escape from django.utils.translation import ugettext from django.utils.encoding import StrAndUnicode, force_unicode from django.utils.safestring import mark_safe +from django.utils import datetime_safe from util import flatatt from urlparse import urljoin @@ -296,6 +297,7 @@ class DateTimeInput(Input): if value is None: value = '' elif hasattr(value, 'strftime'): + value = datetime_safe.new_datetime(value) value = value.strftime(self.format) return super(DateTimeInput, self).render(name, value, attrs) diff --git a/django/utils/datetime_safe.py b/django/utils/datetime_safe.py new file mode 100644 index 0000000000..a048ecd066 --- /dev/null +++ b/django/utils/datetime_safe.py @@ -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 diff --git a/django/utils/tzinfo.py b/django/utils/tzinfo.py index d8bdee390a..9352acd52a 100644 --- a/django/utils/tzinfo.py +++ b/django/utils/tzinfo.py @@ -60,9 +60,17 @@ class LocalTimezone(tzinfo): tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.weekday(), 0, -1) try: stamp = time.mktime(tt) - except OverflowError: - # 32 bit systems can't handle dates after Jan 2038, so we fake it - # in that case (since we only care about the DST flag here). + except (OverflowError, ValueError): + # 32 bit systems can't handle dates after Jan 2038, and certain + # systems can't handle dates before ~1901-12-01: + # + # >>> time.mktime((1900, 1, 13, 0, 0, 0, 0, 0, 0)) + # OverflowError: mktime argument out of range + # >>> time.mktime((1850, 1, 13, 0, 0, 0, 0, 0, 0)) + # ValueError: year out of range + # + # In this case, we fake the date, because we only care about the + # DST flag. tt = (2037,) + tt[1:] stamp = time.mktime(tt) tt = time.localtime(stamp) diff --git a/tests/regressiontests/datetime_safe/tests.py b/tests/regressiontests/datetime_safe/tests.py new file mode 100644 index 0000000000..e1fe2b0f0e --- /dev/null +++ b/tests/regressiontests/datetime_safe/tests.py @@ -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' +"""