From b3b71a0922334c70bbc646a4873010f808196671 Mon Sep 17 00:00:00 2001 From: Malcolm Tredinnick Date: Tue, 29 Jul 2008 05:09:29 +0000 Subject: [PATCH] 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 --- django/db/backends/__init__.py | 70 +++++- django/db/backends/mysql/base.py | 19 +- django/db/backends/oracle/base.py | 19 +- django/db/backends/sqlite3/base.py | 8 +- django/db/backends/util.py | 7 + django/db/models/fields/__init__.py | 219 +++++++++--------- docs/custom_model_fields.txt | 43 ++-- tests/modeltests/custom_methods/models.py | 3 +- tests/modeltests/validation/models.py | 22 +- tests/regressiontests/model_fields/tests.py | 24 +- tests/regressiontests/model_regress/models.py | 19 ++ 11 files changed, 318 insertions(+), 135 deletions(-) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 899a779673..c65af85d8d 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -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) + diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 74138a7b11..3b8d897925 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -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() diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 517be0d78b..bcf141fd7e 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -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() diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 48d9ad4c4b..71be86f00b 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -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: diff --git a/django/db/backends/util.py b/django/db/backends/util.py index 367072879e..25b41e3fb6 100644 --- a/django/db/backends/util.py +++ b/django/db/backends/util.py @@ -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) diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index edd7ba257f..6f914f5f36 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -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] diff --git a/docs/custom_model_fields.txt b/docs/custom_model_fields.txt index 6da15e7f42..956747f1b9 100644 --- a/docs/custom_model_fields.txt +++ b/docs/custom_model_fields.txt @@ -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 -------------------- diff --git a/tests/modeltests/custom_methods/models.py b/tests/modeltests/custom_methods/models.py index b0ca4131a5..d420871373 100644 --- a/tests/modeltests/custom_methods/models.py +++ b/tests/modeltests/custom_methods/models.py @@ -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()] diff --git a/tests/modeltests/validation/models.py b/tests/modeltests/validation/models.py index 63f9f7a361..7ed9d66674 100644 --- a/tests/modeltests/validation/models.py +++ b/tests/modeltests/validation/models.py @@ -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.'] """} diff --git a/tests/regressiontests/model_fields/tests.py b/tests/regressiontests/model_fields/tests.py index c2ba9ee008..5be852f407 100644 --- a/tests/regressiontests/model_fields/tests.py +++ b/tests/regressiontests/model_fields/tests.py @@ -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) + + """ diff --git a/tests/regressiontests/model_regress/models.py b/tests/regressiontests/model_regress/models.py index 2252531564..9e11b43d2b 100644 --- a/tests/regressiontests/model_regress/models.py +++ b/tests/regressiontests/model_regress/models.py @@ -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) """ }