Fixed #7560 -- Moved a lot of the value conversion preparation for

loading/saving interactions with the databases into django.db.backend. This
helps external db backend writers and removes a bunch of database-specific
if-tests in django.db.models.fields.

Great work from Leo Soto.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@8131 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick 2008-07-29 05:09:29 +00:00
parent 7bc728c826
commit b3b71a0922
11 changed files with 318 additions and 135 deletions

View File

@ -5,6 +5,9 @@ except ImportError:
# Import copy of _thread_local.py from Python 2.4
from django.utils._threading_local import local
from django.db.backends import util
from django.utils import datetime_safe
class BaseDatabaseWrapper(local):
"""
Represents a database connection.
@ -36,12 +39,13 @@ class BaseDatabaseWrapper(local):
return cursor
def make_debug_cursor(self, cursor):
from django.db.backends import util
return util.CursorDebugWrapper(cursor, self)
class BaseDatabaseFeatures(object):
allows_group_by_ordinal = True
inline_fk_references = True
# True if django.db.backend.utils.typecast_timestamp is used on values
# returned from dates() calls.
needs_datetime_string_cast = True
supports_constraints = True
supports_tablespaces = False
@ -49,10 +53,7 @@ class BaseDatabaseFeatures(object):
uses_custom_query_class = False
empty_fetchmany_value = []
update_can_self_select = True
supports_usecs = True
time_field_needs_date = False
interprets_empty_strings_as_nulls = False
date_field_supports_time_value = True
can_use_chunked_reads = True
class BaseDatabaseOperations(object):
@ -263,3 +264,64 @@ class BaseDatabaseOperations(object):
"""Prepares a value for use in a LIKE query."""
from django.utils.encoding import smart_unicode
return smart_unicode(x).replace("\\", "\\\\").replace("%", "\%").replace("_", "\_")
def value_to_db_date(self, value):
"""
Transform a date value to an object compatible with what is expected
by the backend driver for date columns.
"""
if value is None:
return None
return datetime_safe.new_date(value).strftime('%Y-%m-%d')
def value_to_db_datetime(self, value):
"""
Transform a datetime value to an object compatible with what is expected
by the backend driver for date columns.
"""
if value is None:
return None
return unicode(value)
def value_to_db_time(self, value):
"""
Transform a datetime value to an object compatible with what is expected
by the backend driver for date columns.
"""
if value is None:
return None
return unicode(value)
def value_to_db_decimal(self, value, max_digits, decimal_places):
"""
Transform a decimal.Decimal value to an object compatible with what is
expected by the backend driver for decimal (numeric) columns.
"""
if value is None:
return None
return util.format_number(value, max_digits, decimal_places)
def year_lookup_bounds(self, value):
"""
Returns a two-elements list with the lower and upper bound to be used
with a BETWEEN operator to query a field value using a year lookup
`value` is an int, containing the looked-up year.
"""
first = '%s-01-01 00:00:00'
second = '%s-12-31 23:59:59.999999'
return [first % value, second % value]
def year_lookup_bounds_for_date_field(self, value):
"""
Returns a two-elements list with the lower and upper bound to be used
with a BETWEEN operator to query a DateField value using a year lookup
`value` is an int, containing the looked-up year.
By default, it just calls `self.year_lookup_bounds`. Some backends need
this hook because on their DB date fields can't be compared to values
which include a time part.
"""
return self.year_lookup_bounds(value)

View File

@ -63,7 +63,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
inline_fk_references = False
empty_fetchmany_value = ()
update_can_self_select = False
supports_usecs = False
class DatabaseOperations(BaseDatabaseOperations):
def date_extract_sql(self, lookup_type, field_name):
@ -124,6 +123,24 @@ class DatabaseOperations(BaseDatabaseOperations):
else:
return []
def value_to_db_datetime(self, value):
# MySQL doesn't support microseconds
if value is None:
return None
return unicode(value.replace(microsecond=0))
def value_to_db_time(self, value):
# MySQL doesn't support microseconds
if value is None:
return None
return unicode(value.replace(microsecond=0))
def year_lookup_bounds(self, value):
# Again, no microseconds
first = '%s-01-01 00:00:00'
second = '%s-12-31 23:59:59.99'
return [first % value, second % value]
class DatabaseWrapper(BaseDatabaseWrapper):
features = DatabaseFeatures()
ops = DatabaseOperations()

View File

@ -5,6 +5,8 @@ Requires cx_Oracle: http://www.python.net/crew/atuining/cx_Oracle/
"""
import os
import datetime
import time
from django.db.backends import BaseDatabaseWrapper, BaseDatabaseFeatures, BaseDatabaseOperations, util
from django.db.backends.oracle import query
@ -28,9 +30,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
supports_tablespaces = True
uses_case_insensitive_names = True
uses_custom_query_class = True
time_field_needs_date = True
interprets_empty_strings_as_nulls = True
date_field_supports_time_value = False
class DatabaseOperations(BaseDatabaseOperations):
def autoinc_sql(self, table, column):
@ -180,6 +180,21 @@ class DatabaseOperations(BaseDatabaseOperations):
def tablespace_sql(self, tablespace, inline=False):
return "%sTABLESPACE %s" % ((inline and "USING INDEX " or ""), self.quote_name(tablespace))
def value_to_db_time(self, value):
if value is None:
return None
if isinstance(value, basestring):
return datetime.datetime(*(time.strptime(value, '%H:%M:%S')[:6]))
return datetime.datetime(1900, 1, 1, value.hour, value.minute,
value.second, value.microsecond)
def year_lookup_bounds_for_date_field(self, value):
first = '%s-01-01'
second = '%s-12-31'
return [first % value, second % value]
class DatabaseWrapper(BaseDatabaseWrapper):
features = DatabaseFeatures()
ops = DatabaseOperations()

View File

@ -84,6 +84,12 @@ class DatabaseOperations(BaseDatabaseOperations):
# sql_flush() implementations). Just return SQL at this point
return sql
def year_lookup_bounds(self, value):
first = '%s-01-01'
second = '%s-12-31 23:59:59.999999'
return [first % value, second % value]
class DatabaseWrapper(BaseDatabaseWrapper):
features = DatabaseFeatures()
ops = DatabaseOperations()
@ -159,7 +165,7 @@ def _sqlite_extract(lookup_type, dt):
dt = util.typecast_timestamp(dt)
except (ValueError, TypeError):
return None
return str(getattr(dt, lookup_type))
return getattr(dt, lookup_type)
def _sqlite_date_trunc(lookup_type, dt):
try:

View File

@ -117,3 +117,10 @@ def truncate_name(name, length=None):
hash = md5.md5(name).hexdigest()[:4]
return '%s%s' % (name[:length-4], hash)
def format_number(value, max_digits, decimal_places):
"""
Formats a number into a string with the requisite number of digits and
decimal places.
"""
return u"%.*f" % (decimal_places, value)

View File

@ -218,19 +218,30 @@ class Field(object):
"Returns field's value just before saving."
return getattr(model_instance, self.attname)
def get_db_prep_value(self, value):
"""Returns field's value prepared for interacting with the database
backend.
Used by the default implementations of ``get_db_prep_save``and
`get_db_prep_lookup```
"""
return value
def get_db_prep_save(self, value):
"Returns field's value prepared for saving into a database."
return value
return self.get_db_prep_value(value)
def get_db_prep_lookup(self, lookup_type, value):
"Returns field's value prepared for database lookup."
if hasattr(value, 'as_sql'):
sql, params = value.as_sql()
return QueryWrapper(('(%s)' % sql), params)
if lookup_type in ('exact', 'regex', 'iregex', 'gt', 'gte', 'lt', 'lte', 'month', 'day', 'search'):
if lookup_type in ('regex', 'iregex', 'month', 'day', 'search'):
return [value]
elif lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte'):
return [self.get_db_prep_value(value)]
elif lookup_type in ('range', 'in'):
return value
return [self.get_db_prep_value(v) for v in value]
elif lookup_type in ('contains', 'icontains'):
return ["%%%s%%" % connection.ops.prep_for_like_query(value)]
elif lookup_type == 'iexact':
@ -246,19 +257,12 @@ class Field(object):
value = int(value)
except ValueError:
raise ValueError("The __year lookup type requires an integer argument")
if settings.DATABASE_ENGINE == 'sqlite3':
first = '%s-01-01'
second = '%s-12-31 23:59:59.999999'
elif not connection.features.date_field_supports_time_value and self.get_internal_type() == 'DateField':
first = '%s-01-01'
second = '%s-12-31'
elif not connection.features.supports_usecs:
first = '%s-01-01 00:00:00'
second = '%s-12-31 23:59:59.99'
if self.get_internal_type() == 'DateField':
return connection.ops.year_lookup_bounds_for_date_field(value)
else:
first = '%s-01-01 00:00:00'
second = '%s-12-31 23:59:59.999999'
return [first % value, second % value]
return connection.ops.year_lookup_bounds(value)
raise TypeError("Field has invalid lookup: %s" % lookup_type)
def has_default(self):
@ -457,6 +461,11 @@ class AutoField(Field):
except (TypeError, ValueError):
raise validators.ValidationError, _("This value must be an integer.")
def get_db_prep_value(self, value):
if value is None:
return None
return int(value)
def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True):
if not rel:
return [] # Don't add a FormField unless it's in a related context.
@ -498,6 +507,11 @@ class BooleanField(Field):
if value in ('f', 'False', '0'): return False
raise validators.ValidationError, _("This value must be either True or False.")
def get_db_prep_value(self, value):
if value is None:
return None
return bool(value)
def get_manipulator_field_objs(self):
return [oldforms.CheckboxField]
@ -559,15 +573,6 @@ class DateField(Field):
except ValueError:
raise validators.ValidationError, _('Enter a valid date in YYYY-MM-DD format.')
def get_db_prep_lookup(self, lookup_type, value):
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 = datetime_safe.new_date(value).strftime('%Y-%m-%d')
else:
value = smart_unicode(value)
return Field.get_db_prep_lookup(self, lookup_type, value)
def pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and add):
value = datetime.datetime.now()
@ -591,16 +596,9 @@ class DateField(Field):
else:
return self.editable or self.auto_now or self.auto_now_add
def get_db_prep_save(self, value):
# Casts dates into string format for entry into database.
if value is not None:
try:
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.
pass
return Field.get_db_prep_save(self, value)
def get_db_prep_value(self, value):
# Casts dates into the format expected by the backend
return connection.ops.value_to_db_date(self.to_python(value))
def get_manipulator_field_objs(self):
return [oldforms.DateField]
@ -629,33 +627,37 @@ class DateTimeField(DateField):
return value
if isinstance(value, datetime.date):
return datetime.datetime(value.year, value.month, value.day)
# Attempt to parse a datetime:
value = smart_str(value)
# split usecs, because they are not recognized by strptime.
if '.' in value:
try:
value, usecs = value.split('.')
usecs = int(usecs)
except ValueError:
raise validators.ValidationError, _('Enter a valid date/time in YYYY-MM-DD HH:MM[ss[.uuuuuu]] format.')
else:
usecs = 0
kwargs = {'microsecond': usecs}
try: # Seconds are optional, so try converting seconds first.
return datetime.datetime(*time.strptime(value, '%Y-%m-%d %H:%M:%S')[:6])
return datetime.datetime(*time.strptime(value, '%Y-%m-%d %H:%M:%S')[:6],
**kwargs)
except ValueError:
try: # Try without seconds.
return datetime.datetime(*time.strptime(value, '%Y-%m-%d %H:%M')[:5])
return datetime.datetime(*time.strptime(value, '%Y-%m-%d %H:%M')[:5],
**kwargs)
except ValueError: # Try without hour/minutes/seconds.
try:
return datetime.datetime(*time.strptime(value, '%Y-%m-%d')[:3])
return datetime.datetime(*time.strptime(value, '%Y-%m-%d')[:3],
**kwargs)
except ValueError:
raise validators.ValidationError, _('Enter a valid date/time in YYYY-MM-DD HH:MM format.')
raise validators.ValidationError, _('Enter a valid date/time in YYYY-MM-DD HH:MM[ss[.uuuuuu]] format.')
def get_db_prep_save(self, value):
# Casts dates into string format for entry into database.
if value is not None:
# MySQL will throw a warning if microseconds are given, because it
# doesn't support microseconds.
if not connection.features.supports_usecs and hasattr(value, 'microsecond'):
value = value.replace(microsecond=0)
value = smart_unicode(value)
return Field.get_db_prep_save(self, value)
def get_db_prep_lookup(self, lookup_type, value):
if lookup_type in ('range', 'in'):
value = [smart_unicode(v) for v in value]
else:
value = smart_unicode(value)
return Field.get_db_prep_lookup(self, lookup_type, value)
def get_db_prep_value(self, value):
# Casts dates into the format expected by the backend
return connection.ops.value_to_db_datetime(self.to_python(value))
def get_manipulator_field_objs(self):
return [oldforms.DateField, oldforms.TimeField]
@ -720,26 +722,18 @@ class DecimalField(Field):
Formats a number into a string with the requisite number of digits and
decimal places.
"""
num_chars = self.max_digits
# Allow for a decimal point
if self.decimal_places > 0:
num_chars += 1
# Allow for a minus sign
if value < 0:
num_chars += 1
# Method moved to django.db.backends.util.
#
# It is preserved because it is used by the oracle backend
# (django.db.backends.oracle.query), and also for
# backwards-compatibility with any external code which may have used
# this method.
from django.db.backends import util
return util.format_number(value, self.max_digits, self.decimal_places)
return u"%.*f" % (self.decimal_places, value)
def get_db_prep_save(self, value):
value = self._format(value)
return super(DecimalField, self).get_db_prep_save(value)
def get_db_prep_lookup(self, lookup_type, value):
if lookup_type in ('range', 'in'):
value = [self._format(v) for v in value]
else:
value = self._format(value)
return super(DecimalField, self).get_db_prep_lookup(lookup_type, value)
def get_db_prep_value(self, value):
return connection.ops.value_to_db_decimal(value, self.max_digits,
self.decimal_places)
def get_manipulator_field_objs(self):
return [curry(oldforms.DecimalField, max_digits=self.max_digits, decimal_places=self.decimal_places)]
@ -778,7 +772,7 @@ class FileField(Field):
def get_internal_type(self):
return "FileField"
def get_db_prep_save(self, value):
def get_db_prep_value(self, value):
"Returns field's value prepared for saving into a database."
# Need to convert UploadedFile objects provided via a form to unicode for database insertion
if hasattr(value, 'name'):
@ -919,6 +913,11 @@ class FilePathField(Field):
class FloatField(Field):
empty_strings_allowed = False
def get_db_prep_value(self, value):
if value is None:
return None
return float(value)
def get_manipulator_field_objs(self):
return [oldforms.FloatField]
@ -966,6 +965,11 @@ class ImageField(FileField):
class IntegerField(Field):
empty_strings_allowed = False
def get_db_prep_value(self, value):
if value is None:
return None
return int(value)
def get_manipulator_field_objs(self):
return [oldforms.IntegerField]
@ -1013,6 +1017,11 @@ class NullBooleanField(Field):
if value in ('f', 'False', '0'): return False
raise validators.ValidationError, _("This value must be either None, True or False.")
def get_db_prep_value(self, value):
if value is None:
return None
return bool(value)
def get_manipulator_field_objs(self):
return [oldforms.NullBooleanField]
@ -1025,7 +1034,7 @@ class NullBooleanField(Field):
defaults.update(kwargs)
return super(NullBooleanField, self).formfield(**defaults)
class PhoneNumberField(IntegerField):
class PhoneNumberField(Field):
def get_manipulator_field_objs(self):
return [oldforms.PhoneNumberField]
@ -1107,20 +1116,34 @@ class TimeField(Field):
def get_internal_type(self):
return "TimeField"
def get_db_prep_lookup(self, lookup_type, value):
if connection.features.time_field_needs_date:
# Oracle requires a date in order to parse.
def prep(value):
if isinstance(value, datetime.time):
value = datetime.datetime.combine(datetime.date(1900, 1, 1), value)
return smart_unicode(value)
def to_python(self, value):
if value is None:
return None
if isinstance(value, datetime.time):
return value
# Attempt to parse a datetime:
value = smart_str(value)
# split usecs, because they are not recognized by strptime.
if '.' in value:
try:
value, usecs = value.split('.')
usecs = int(usecs)
except ValueError:
raise validators.ValidationError, _('Enter a valid time in HH:MM[:ss[.uuuuuu]] format.')
else:
prep = smart_unicode
if lookup_type in ('range', 'in'):
value = [prep(v) for v in value]
else:
value = prep(value)
return Field.get_db_prep_lookup(self, lookup_type, value)
usecs = 0
kwargs = {'microsecond': usecs}
try: # Seconds are optional, so try converting seconds first.
return datetime.time(*time.strptime(value, '%H:%M:%S')[3:6],
**kwargs)
except ValueError:
try: # Try without seconds.
return datetime.time(*time.strptime(value, '%H:%M')[3:5],
**kwargs)
except ValueError:
raise validators.ValidationError, _('Enter a valid time in HH:MM[:ss[.uuuuuu]] format.')
def pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and add):
@ -1130,23 +1153,9 @@ class TimeField(Field):
else:
return super(TimeField, self).pre_save(model_instance, add)
def get_db_prep_save(self, value):
# Casts dates into string format for entry into database.
if value is not None:
# MySQL will throw a warning if microseconds are given, because it
# doesn't support microseconds.
if not connection.features.supports_usecs and hasattr(value, 'microsecond'):
value = value.replace(microsecond=0)
if connection.features.time_field_needs_date:
# cx_Oracle expects a datetime.datetime to persist into TIMESTAMP field.
if isinstance(value, datetime.time):
value = datetime.datetime(1900, 1, 1, value.hour, value.minute,
value.second, value.microsecond)
elif isinstance(value, basestring):
value = datetime.datetime(*(time.strptime(value, '%H:%M:%S')[:6]))
else:
value = smart_unicode(value)
return Field.get_db_prep_save(self, value)
def get_db_prep_value(self, value):
# Casts times into the format expected by the backend
return connection.ops.value_to_db_time(self.to_python(value))
def get_manipulator_field_objs(self):
return [oldforms.TimeField]

View File

@ -385,8 +385,8 @@ Python object type we want to store in the model's attribute.
called when it is created, you should be using `The SubfieldBase metaclass`_
mentioned earlier. Otherwise ``to_python()`` won't be called automatically.
``get_db_prep_save(self, value)``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``get_db_prep_value(self, value)``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is the reverse of ``to_python()`` when working with the database backends
(as opposed to serialization). The ``value`` parameter is the current value of
@ -399,10 +399,20 @@ For example::
class HandField(models.Field):
# ...
def get_db_prep_save(self, value):
def get_db_prep_value(self, value):
return ''.join([''.join(l) for l in (value.north,
value.east, value.south, value.west)])
``get_db_prep_save(self, value)``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Same as the above, but called when the Field value must be *saved* to the
database. As the default implementation just calls ``get_db_prep_value``, you
shouldn't need to implement this method unless your custom field need a special
conversion when being saved that is not the same as the used for normal query
parameters (which is implemented by ``get_db_prep_value``).
``pre_save(self, model_instance, add)``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -440,14 +450,21 @@ by with handling the lookup types that need special handling for your field
and pass the rest of the ``get_db_prep_lookup()`` method of the parent class.
If you needed to implement ``get_db_prep_save()``, you will usually need to
implement ``get_db_prep_lookup()``. The usual reason is because of the
``range`` and ``in`` lookups. In these case, you will passed a list of
objects (presumably of the right type) and will need to convert them to a list
of things of the right type for passing to the database. Sometimes you can
reuse ``get_db_prep_save()``, or at least factor out some common pieces from
both methods into a help function.
implement ``get_db_prep_lookup()``. If you don't, ``get_db_prep_value`` will be
called by the default implementation, to manage ``exact``, ``gt``, ``gte``,
``lt``, ``lte``, ``in`` and ``range`` lookups.
For example::
You may also want to implement this method to limit the lookup types that could
be used with your custom field type.
Note that, for ``range`` and ``in`` lookups, ``get_db_prep_lookup`` will receive
a list of objects (presumably of the right type) and will need to convert them
to a list of things of the right type for passing to the database. Most of the
time, you can reuse ``get_db_prep_value()``, or at least factor out some common
pieces.
For example, the following code implements ``get_db_prep_lookup`` to limit the
accepted lookup types to ``exact`` and ``in``::
class HandField(models.Field):
# ...
@ -455,9 +472,9 @@ For example::
def get_db_prep_lookup(self, lookup_type, value):
# We only handle 'exact' and 'in'. All others are errors.
if lookup_type == 'exact':
return self.get_db_prep_save(value)
return self.get_db_prep_value(value)
elif lookup_type == 'in':
return [self.get_db_prep_save(v) for v in value]
return [self.get_db_prep_value(v) for v in value]
else:
raise TypeError('Lookup type %r not supported.' % lookup_type)
@ -557,7 +574,7 @@ we can reuse some existing conversion code::
def flatten_data(self, follow, obj=None):
value = self._get_val_from_obj(obj)
return {self.attname: self.get_db_prep_save(value)}
return {self.attname: self.get_db_prep_value(value)}
Some general advice
--------------------

View File

@ -31,7 +31,8 @@ class Article(models.Model):
SELECT id, headline, pub_date
FROM custom_methods_article
WHERE pub_date = %s
AND id != %s""", [str(self.pub_date), self.id])
AND id != %s""", [connection.ops.value_to_db_date(self.pub_date),
self.id])
# The asterisk in "(*row)" tells Python to expand the list into
# positional arguments to Article().
return [self.__class__(*row) for row in cursor.fetchall()]

View File

@ -16,6 +16,7 @@ class Person(models.Model):
birthdate = models.DateField()
favorite_moment = models.DateTimeField()
email = models.EmailField()
best_time = models.TimeField()
def __unicode__(self):
return self.name
@ -28,7 +29,8 @@ __test__ = {'API_TESTS':"""
... 'name': 'John',
... 'birthdate': datetime.date(2000, 5, 3),
... 'favorite_moment': datetime.datetime(2002, 4, 3, 13, 23),
... 'email': 'john@example.com'
... 'email': 'john@example.com',
... 'best_time': datetime.time(16, 20),
... }
>>> p = Person(**valid_params)
>>> p.validate()
@ -130,6 +132,22 @@ datetime.datetime(2002, 4, 3, 13, 23)
>>> p.favorite_moment
datetime.datetime(2002, 4, 3, 0, 0)
>>> p = Person(**dict(valid_params, best_time='16:20:00'))
>>> p.validate()
{}
>>> p.best_time
datetime.time(16, 20)
>>> p = Person(**dict(valid_params, best_time='16:20'))
>>> p.validate()
{}
>>> p.best_time
datetime.time(16, 20)
>>> p = Person(**dict(valid_params, best_time='bar'))
>>> p.validate()['best_time']
[u'Enter a valid time in HH:MM[:ss[.uuuuuu]] format.']
>>> p = Person(**dict(valid_params, email='john@example.com'))
>>> p.validate()
{}
@ -153,5 +171,7 @@ u'john@example.com'
[u'This field is required.']
>>> errors['birthdate']
[u'This field is required.']
>>> errors['best_time']
[u'This field is required.']
"""}

View File

@ -20,16 +20,26 @@ ValidationError: [u'This value must be a decimal number.']
>>> x = f.to_python(2)
>>> y = f.to_python('2.6')
>>> f.get_db_prep_save(x)
>>> f._format(x)
u'2.0'
>>> f.get_db_prep_save(y)
>>> f._format(y)
u'2.6'
>>> f.get_db_prep_save(None)
>>> f.get_db_prep_lookup('exact', x)
[u'2.0']
>>> f.get_db_prep_lookup('exact', y)
[u'2.6']
>>> f._format(None)
>>> f.get_db_prep_lookup('exact', None)
[None]
# DateTimeField and TimeField to_python should support usecs:
>>> f = DateTimeField()
>>> f.to_python('2001-01-02 03:04:05.000006')
datetime.datetime(2001, 1, 2, 3, 4, 5, 6)
>>> f.to_python('2001-01-02 03:04:05.999999')
datetime.datetime(2001, 1, 2, 3, 4, 5, 999999)
>>> f = TimeField()
>>> f.to_python('01:02:03.000004')
datetime.time(1, 2, 3, 4)
>>> f.to_python('01:02:03.999999')
datetime.time(1, 2, 3, 999999)
"""

View File

@ -29,6 +29,9 @@ class Movie(models.Model):
class Party(models.Model):
when = models.DateField()
class Event(models.Model):
when = models.DateTimeField()
__test__ = {'API_TESTS': """
(NOTE: Part of the regression test here is merely parsing the model
declaration. The verbose_name, in particular, did not always work.)
@ -68,5 +71,21 @@ u''
>>> [p.when for p in Party.objects.filter(when__year = 1998)]
[datetime.date(1998, 12, 31)]
# Check that get_next_by_FIELD and get_previous_by_FIELD don't crash when we
# have usecs values stored on the database
#
# [It crashed after the Field.get_db_prep_* refactor, because on most backends
# DateTimeFields supports usecs, but DateTimeField.to_python didn't recognize
# them. (Note that Model._get_next_or_previous_by_FIELD coerces values to
# strings)]
#
>>> e = Event.objects.create(when = datetime.datetime(2000, 1, 1, 16, 0, 0))
>>> e = Event.objects.create(when = datetime.datetime(2000, 1, 1, 6, 1, 1))
>>> e = Event.objects.create(when = datetime.datetime(2000, 1, 1, 13, 1, 1))
>>> e = Event.objects.create(when = datetime.datetime(2000, 1, 1, 12, 0, 20, 24))
>>> e.get_next_by_when().when
datetime.datetime(2000, 1, 1, 13, 1, 1)
>>> e.get_previous_by_when().when
datetime.datetime(2000, 1, 1, 6, 1, 1)
"""
}