diff options
Diffstat (limited to 'django/db')
-rw-r--r-- | django/db/backends/__init__.py | 70 | ||||
-rw-r--r-- | django/db/backends/mysql/base.py | 19 | ||||
-rw-r--r-- | django/db/backends/oracle/base.py | 30 | ||||
-rw-r--r-- | django/db/backends/oracle/query.py | 9 | ||||
-rw-r--r-- | django/db/backends/sqlite3/base.py | 8 | ||||
-rw-r--r-- | django/db/backends/util.py | 12 | ||||
-rw-r--r-- | django/db/models/__init__.py | 2 | ||||
-rw-r--r-- | django/db/models/base.py | 38 | ||||
-rw-r--r-- | django/db/models/fields/__init__.py | 244 | ||||
-rw-r--r-- | django/db/models/fields/related.py | 173 | ||||
-rw-r--r-- | django/db/models/fields/subclassing.py | 5 | ||||
-rw-r--r-- | django/db/models/query.py | 35 | ||||
-rw-r--r-- | django/db/models/sql/query.py | 34 | ||||
-rw-r--r-- | django/db/models/sql/where.py | 39 |
14 files changed, 471 insertions, 247 deletions
diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 899a779673..d65eacd042 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 datetime 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 time 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 a41084bca3..bdb73b1864 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -5,10 +5,11 @@ 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 -from django.utils.datastructures import SortedDict from django.utils.encoding import smart_str, force_unicode # Oracle takes client-side character set encoding from the environment. @@ -29,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): @@ -181,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() @@ -245,10 +259,10 @@ class DatabaseWrapper(BaseDatabaseWrapper): class OracleParam(object): """ - Wrapper object for formatting parameters for Oracle. If the string - representation of the value is large enough (greater than 4000 characters) + Wrapper object for formatting parameters for Oracle. If the string + representation of the value is large enough (greater than 4000 characters) the input size needs to be set as NCLOB. Alternatively, if the parameter has - an `input_size` attribute, then the value of the `input_size` attribute will + an `input_size` attribute, then the value of the `input_size` attribute will be used instead. Otherwise, no input size will be set for the parameter when executing the query. """ @@ -282,7 +296,7 @@ class FormatStylePlaceholderCursor(Database.Cursor): return result else: return tuple([OracleParam(p, self.charset, True) for p in params]) - + def _guess_input_sizes(self, params_list): if isinstance(params_list[0], dict): sizes = {} @@ -303,7 +317,7 @@ class FormatStylePlaceholderCursor(Database.Cursor): return dict([(k, p.smart_str) for k, p in params.iteritems()]) else: return [p.smart_str for p in params] - + def execute(self, query, params=None): if params is None: params = [] diff --git a/django/db/backends/oracle/query.py b/django/db/backends/oracle/query.py index 7e50c7b5db..49e1c4131c 100644 --- a/django/db/backends/oracle/query.py +++ b/django/db/backends/oracle/query.py @@ -87,9 +87,11 @@ def query_class(QueryClass, Database): If 'with_limits' is False, any limit/offset information is not included in the query. """ + # The `do_offset` flag indicates whether we need to construct # the SQL needed to use limit/offset w/Oracle. - do_offset = with_limits and (self.high_mark or self.low_mark) + do_offset = with_limits and (self.high_mark is not None + or self.low_mark) # If no offsets, just return the result of the base class # `as_sql`. @@ -117,7 +119,7 @@ def query_class(QueryClass, Database): # Getting the selection SQL and the params, which has the `rn` # extra selection SQL. self.extra_select['rn'] = 'ROW_NUMBER() OVER (ORDER BY %s )' % rn_orderby - sql, params= super(OracleQuery, self).as_sql(with_limits=False, + sql, params = super(OracleQuery, self).as_sql(with_limits=False, with_col_aliases=True) # Constructing the result SQL, using the initial select SQL @@ -126,7 +128,7 @@ def query_class(QueryClass, Database): # Place WHERE condition on `rn` for the desired range. result.append('WHERE rn > %d' % self.low_mark) - if self.high_mark: + if self.high_mark is not None: result.append('AND rn <= %d' % self.high_mark) # Returning the SQL w/params. @@ -148,4 +150,3 @@ def query_class(QueryClass, Database): _classes[QueryClass] = OracleQuery return OracleQuery - 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..7228b4046b 100644 --- a/django/db/backends/util.py +++ b/django/db/backends/util.py @@ -1,7 +1,8 @@ import datetime -import md5 from time import time +from django.utils.hashcompat import md5_constructor + try: import decimal except ImportError: @@ -114,6 +115,13 @@ def truncate_name(name, length=None): if length is None or len(name) <= length: return name - hash = md5.md5(name).hexdigest()[:4] + hash = md5_constructor(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/__init__.py b/django/db/models/__init__.py index bd6cc3542d..18c47e86f3 100644 --- a/django/db/models/__init__.py +++ b/django/db/models/__init__.py @@ -10,8 +10,6 @@ from django.db.models.fields import * from django.db.models.fields.subclassing import SubfieldBase from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel, TABULAR, STACKED from django.db.models import signals -from django.utils.functional import curry -from django.utils.text import capfirst # Admin stages. ADD, CHANGE, BOTH = 1, 2, 3 diff --git a/django/db/models/base.py b/django/db/models/base.py index 080e0af588..7d7def3bad 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -12,7 +12,7 @@ import django.db.models.manipulators # Imported to register signal handler. import django.db.models.manager # Ditto. from django.core import validators from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError -from django.db.models.fields import AutoField, ImageField, FieldDoesNotExist +from django.db.models.fields import AutoField, ImageField from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField from django.db.models.query import delete_objects, Q, CollectedObjects from django.db.models.options import Options @@ -20,7 +20,6 @@ from django.db import connection, transaction from django.db.models import signals from django.db.models.loading import register_models, get_model from django.dispatch import dispatcher -from django.utils.datastructures import SortedDict from django.utils.functional import curry from django.utils.encoding import smart_str, force_unicode, smart_unicode from django.core.files.move import file_move_safe @@ -201,6 +200,7 @@ class Model(object): # keywords, or default. for field in fields_iter: + rel_obj = None if kwargs: if isinstance(field.rel, ManyToOneRel): try: @@ -217,17 +217,18 @@ class Model(object): # pass in "None" for related objects if it's allowed. if rel_obj is None and field.null: val = None - else: - try: - val = getattr(rel_obj, field.rel.get_related_field().attname) - except AttributeError: - raise TypeError("Invalid value: %r should be a %s instance, not a %s" % - (field.name, field.rel.to, type(rel_obj))) else: val = kwargs.pop(field.attname, field.get_default()) else: val = field.get_default() - setattr(self, field.attname, val) + # If we got passed a related instance, set it using the field.name + # instead of field.attname (e.g. "user" instead of "user_id") so + # that the object gets properly cached (and type checked) by the + # RelatedObjectDescriptor. + if rel_obj: + setattr(self, field.name, rel_obj) + else: + setattr(self, field.attname, val) if kwargs: for prop in kwargs.keys(): @@ -299,6 +300,12 @@ class Model(object): # attributes we have been given to the class we have been given. if not raw: for parent, field in meta.parents.items(): + # At this point, parent's primary key field may be unknown + # (for example, from administration form which doesn't fill + # this field). If so, fill it. + if getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None: + setattr(self, parent._meta.pk.attname, getattr(self, field.attname)) + self.save_base(raw, parent) setattr(self, field.attname, self._get_pk_val(parent._meta)) @@ -472,11 +479,12 @@ class Model(object): return os.path.getsize(self._get_FIELD_filename(field)) def _save_FIELD_file(self, field, filename, raw_field, save=True): - directory = field.get_directory_name() - try: # Create the date-based directory if it doesn't exist. - os.makedirs(os.path.join(settings.MEDIA_ROOT, directory)) - except OSError: # Directory probably already exists. - pass + # Create the upload directory if it doesn't already exist + directory = os.path.join(settings.MEDIA_ROOT, field.get_directory_name()) + if not os.path.exists(directory): + os.makedirs(directory) + elif not os.path.isdir(directory): + raise IOError('%s exists and is not a directory' % directory) # Check for old-style usage (files-as-dictionaries). Warn here first # since there are multiple locations where we need to support both new @@ -494,7 +502,7 @@ class Model(object): elif isinstance(raw_field, basestring): import warnings warnings.warn( - message = "Representing uploaded files as dictionaries is deprecated. Use django.core.files.uploadedfile.SimpleUploadedFile instead.", + message = "Representing uploaded files as strings is deprecated. Use django.core.files.uploadedfile.SimpleUploadedFile instead.", category = DeprecationWarning, stacklevel = 2 ) diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 78f75aea35..494c42cc54 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -22,7 +22,6 @@ from django.utils.itercompat import tee 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: @@ -62,10 +61,6 @@ def manipulator_validator_unique(f, opts, self, field_data, all_data): # getattr(obj, opts.pk.attname) class Field(object): - # Provide backwards compatibility for the maxlength attribute and - # argument for this class and all subclasses. - __metaclass__ = LegacyMaxlength - # Designates whether empty strings fundamentally are allowed at the # database level. empty_strings_allowed = True @@ -191,7 +186,8 @@ class Field(object): def set_attributes_from_name(self, name): self.name = name self.attname, self.column = self.get_attname_column() - self.verbose_name = self.verbose_name or (name and name.replace('_', ' ')) + if self.verbose_name is None and name: + self.verbose_name = name.replace('_', ' ') def contribute_to_class(self, cls, name): self.set_attributes_from_name(name) @@ -217,19 +213,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': @@ -245,19 +252,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): @@ -288,7 +288,7 @@ class Field(object): if self.choices: field_objs = [oldforms.SelectField] - params['choices'] = self.flatchoices + params['choices'] = self.get_flatchoices() else: field_objs = self.get_manipulator_field_objs() return (field_objs, params) @@ -362,7 +362,8 @@ class Field(object): return val def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH): - "Returns a list of tuples used as SelectField choices for this field." + """Returns choices with a default blank choices included, for use + as SelectField choices for this field.""" first_choice = include_blank and blank_choice or [] if self.choices: return first_choice + list(self.choices) @@ -376,6 +377,11 @@ class Field(object): def get_choices_default(self): return self.get_choices() + def get_flatchoices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH): + "Returns flattened choices with a default blank choice included." + first_choice = include_blank and blank_choice or [] + return first_choice + list(self.flatchoices) + def _get_val_from_obj(self, obj): if obj: return getattr(obj, self.attname) @@ -408,15 +414,16 @@ class Field(object): choices = property(_get_choices) def _get_flatchoices(self): + """Flattened version of choices tuple.""" flat = [] - for choice, value in self.get_choices_default(): + for choice, value in self.choices: if type(value) in (list, tuple): flat.extend(value) else: flat.append((choice,value)) return flat flatchoices = property(_get_flatchoices) - + def save_form_data(self, instance, data): setattr(instance, self.name, data) @@ -449,6 +456,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. @@ -477,6 +489,8 @@ class AutoField(Field): class BooleanField(Field): def __init__(self, *args, **kwargs): kwargs['blank'] = True + if 'default' not in kwargs and not kwargs.get('null'): + kwargs['default'] = False Field.__init__(self, *args, **kwargs) def get_internal_type(self): @@ -488,6 +502,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] @@ -549,15 +568,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() @@ -581,16 +591,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] @@ -619,33 +622,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] @@ -710,26 +717,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 - - 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) + # 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) - 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(self.to_python(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)] @@ -768,7 +767,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'): @@ -909,6 +908,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] @@ -956,6 +960,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] @@ -1003,6 +1012,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] @@ -1015,7 +1029,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] @@ -1097,20 +1111,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) - else: - prep = smart_unicode - if lookup_type in ('range', 'in'): - value = [prep(v) for v in 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: - 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): @@ -1120,23 +1148,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/django/db/models/fields/related.py b/django/db/models/fields/related.py index 735dda4969..ab2b9a6c5e 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -2,11 +2,10 @@ from django.db import connection, transaction from django.db.models import signals, get_model from django.db.models.fields import AutoField, Field, IntegerField, PositiveIntegerField, PositiveSmallIntegerField, FieldDoesNotExist from django.db.models.related import RelatedObject +from django.db.models.query import QuerySet from django.db.models.query_utils import QueryWrapper -from django.utils.text import capfirst from django.utils.translation import ugettext_lazy, string_concat, ungettext, ugettext as _ from django.utils.functional import curry -from django.utils.encoding import smart_unicode from django.core import validators from django import oldforms from django import forms @@ -24,7 +23,7 @@ RECURSIVE_RELATIONSHIP_CONSTANT = 'self' pending_lookups = {} -def add_lazy_relation(cls, field, relation): +def add_lazy_relation(cls, field, relation, operation): """ Adds a lookup on ``cls`` when a related field is defined using a string, i.e.:: @@ -46,6 +45,8 @@ def add_lazy_relation(cls, field, relation): If the other model hasn't yet been loaded -- almost a given if you're using lazy relationships -- then the relation won't be set up until the class_prepared signal fires at the end of model initialization. + + operation is the work that must be performed once the relation can be resolved. """ # Check for recursive relations if relation == RECURSIVE_RELATIONSHIP_CONSTANT: @@ -67,11 +68,10 @@ def add_lazy_relation(cls, field, relation): # is prepared. model = get_model(app_label, model_name, False) if model: - field.rel.to = model - field.do_related_class(model, cls) + operation(field, model, cls) else: key = (app_label, model_name) - value = (cls, field) + value = (cls, field, operation) pending_lookups.setdefault(key, []).append(value) def do_pending_lookups(sender): @@ -79,9 +79,8 @@ def do_pending_lookups(sender): Handle any pending relations to the sending model. Sent from class_prepared. """ key = (sender._meta.app_label, sender.__name__) - for cls, field in pending_lookups.pop(key, []): - field.rel.to = sender - field.do_related_class(sender, cls) + for cls, field, operation in pending_lookups.pop(key, []): + operation(field, sender, cls) dispatcher.connect(do_pending_lookups, signal=signals.class_prepared) @@ -109,13 +108,17 @@ class RelatedField(object): other = self.rel.to if isinstance(other, basestring): - add_lazy_relation(cls, self, other) + def resolve_related_class(field, model, cls): + field.rel.to = model + field.do_related_class(model, cls) + add_lazy_relation(cls, self, other, resolve_related_class) else: self.do_related_class(other, cls) def set_attributes_from_rel(self): self.name = self.name or (self.rel.to._meta.object_name.lower() + '_' + self.rel.to._meta.pk.name) - self.verbose_name = self.verbose_name or self.rel.to._meta.verbose_name + if self.verbose_name is None: + self.verbose_name = self.rel.to._meta.verbose_name self.rel.field_name = self.rel.field_name or self.rel.to._meta.pk.name def do_related_class(self, other, cls): @@ -236,7 +239,14 @@ class ReverseSingleRelatedObjectDescriptor(object): params = {'%s__pk' % self.field.rel.field_name: val} else: params = {'%s__exact' % self.field.rel.field_name: val} - rel_obj = self.field.rel.to._default_manager.get(**params) + + # If the related manager indicates that it should be used for + # related fields, respect that. + rel_mgr = self.field.rel.to._default_manager + if getattr(rel_mgr, 'use_for_related_fields', False): + rel_obj = rel_mgr.get(**params) + else: + rel_obj = QuerySet(self.field.rel.to).get(**params) setattr(instance, cache_name, rel_obj) return rel_obj @@ -340,7 +350,7 @@ class ForeignRelatedObjectsDescriptor(object): manager.clear() manager.add(*value) -def create_many_related_manager(superclass): +def create_many_related_manager(superclass, through=False): """Creates a manager that subclasses 'superclass' (which is a Manager) and adds behavior for many-to-many related objects.""" class ManyRelatedManager(superclass): @@ -354,6 +364,7 @@ def create_many_related_manager(superclass): self.join_table = join_table self.source_col_name = source_col_name self.target_col_name = target_col_name + self.through = through self._pk_val = self.instance._get_pk_val() if self._pk_val is None: raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__) @@ -361,21 +372,24 @@ def create_many_related_manager(superclass): def get_query_set(self): return superclass.get_query_set(self).filter(**(self.core_filters)) - def add(self, *objs): - self._add_items(self.source_col_name, self.target_col_name, *objs) + # If the ManyToMany relation has an intermediary model, + # the add and remove methods do not exist. + if through is None: + def add(self, *objs): + self._add_items(self.source_col_name, self.target_col_name, *objs) - # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table - if self.symmetrical: - self._add_items(self.target_col_name, self.source_col_name, *objs) - add.alters_data = True + # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table + if self.symmetrical: + self._add_items(self.target_col_name, self.source_col_name, *objs) + add.alters_data = True - def remove(self, *objs): - self._remove_items(self.source_col_name, self.target_col_name, *objs) + def remove(self, *objs): + self._remove_items(self.source_col_name, self.target_col_name, *objs) - # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table - if self.symmetrical: - self._remove_items(self.target_col_name, self.source_col_name, *objs) - remove.alters_data = True + # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table + if self.symmetrical: + self._remove_items(self.target_col_name, self.source_col_name, *objs) + remove.alters_data = True def clear(self): self._clear_items(self.source_col_name) @@ -386,6 +400,10 @@ def create_many_related_manager(superclass): clear.alters_data = True def create(self, **kwargs): + # This check needs to be done here, since we can't later remove this + # from the method lookup table, as we do with add and remove. + if through is not None: + raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through new_obj = self.model(**kwargs) new_obj.save() self.add(new_obj) @@ -473,7 +491,7 @@ class ManyRelatedObjectsDescriptor(object): # model's default manager. rel_model = self.related.model superclass = rel_model._default_manager.__class__ - RelatedManager = create_many_related_manager(superclass) + RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through) qn = connection.ops.quote_name manager = RelatedManager( @@ -492,6 +510,10 @@ class ManyRelatedObjectsDescriptor(object): if instance is None: raise AttributeError, "Manager must be accessed via instance" + through = getattr(self.related.field.rel, 'through', None) + if through is not None: + raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through + manager = self.__get__(instance) manager.clear() manager.add(*value) @@ -514,7 +536,7 @@ class ReverseManyRelatedObjectsDescriptor(object): # model's default manager. rel_model=self.field.rel.to superclass = rel_model._default_manager.__class__ - RelatedManager = create_many_related_manager(superclass) + RelatedManager = create_many_related_manager(superclass, self.field.rel.through) qn = connection.ops.quote_name manager = RelatedManager( @@ -533,6 +555,10 @@ class ReverseManyRelatedObjectsDescriptor(object): if instance is None: raise AttributeError, "Manager must be accessed via instance" + through = getattr(self.field.rel, 'through', None) + if through is not None: + raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through + manager = self.__get__(instance) manager.clear() manager.add(*value) @@ -584,7 +610,7 @@ class OneToOneRel(ManyToOneRel): class ManyToManyRel(object): def __init__(self, to, num_in_admin=0, related_name=None, - limit_choices_to=None, symmetrical=True): + limit_choices_to=None, symmetrical=True, through=None): self.to = to self.num_in_admin = num_in_admin self.related_name = related_name @@ -594,6 +620,7 @@ class ManyToManyRel(object): self.edit_inline = False self.symmetrical = symmetrical self.multiple = True + self.through = through class ForeignKey(RelatedField, Field): empty_strings_allowed = False @@ -604,12 +631,7 @@ class ForeignKey(RelatedField, Field): assert isinstance(to, basestring), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT) else: to_field = to_field or to._meta.pk.name - kwargs['verbose_name'] = kwargs.get('verbose_name', '') - - if 'edit_inline_type' in kwargs: - import warnings - warnings.warn("edit_inline_type is deprecated. Use edit_inline instead.", DeprecationWarning) - kwargs['edit_inline'] = kwargs.pop('edit_inline_type') + kwargs['verbose_name'] = kwargs.get('verbose_name', None) kwargs['rel'] = rel_class(to, to_field, num_in_admin=kwargs.pop('num_in_admin', 3), @@ -705,6 +727,7 @@ class OneToOneField(ForeignKey): """ def __init__(self, to, to_field=None, **kwargs): kwargs['unique'] = True + kwargs['editable'] = False if 'num_in_admin' not in kwargs: kwargs['num_in_admin'] = 0 super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs) @@ -722,8 +745,16 @@ class ManyToManyField(RelatedField, Field): num_in_admin=kwargs.pop('num_in_admin', 0), related_name=kwargs.pop('related_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), - symmetrical=kwargs.pop('symmetrical', True)) + symmetrical=kwargs.pop('symmetrical', True), + through=kwargs.pop('through', None)) + self.db_table = kwargs.pop('db_table', None) + if kwargs['rel'].through is not None: + self.creates_table = False + assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used." + else: + self.creates_table = True + Field.__init__(self, **kwargs) msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.') @@ -738,26 +769,62 @@ class ManyToManyField(RelatedField, Field): def _get_m2m_db_table(self, opts): "Function that can be curried to provide the m2m table name for this relation" - if self.db_table: + if self.rel.through is not None: + return self.rel.through_model._meta.db_table + elif self.db_table: return self.db_table else: return '%s_%s' % (opts.db_table, self.name) def _get_m2m_column_name(self, related): "Function that can be curried to provide the source column name for the m2m table" - # If this is an m2m relation to self, avoid the inevitable name clash - if related.model == related.parent_model: - return 'from_' + related.model._meta.object_name.lower() + '_id' - else: - return related.model._meta.object_name.lower() + '_id' + try: + return self._m2m_column_name_cache + except: + if self.rel.through is not None: + for f in self.rel.through_model._meta.fields: + if hasattr(f,'rel') and f.rel and f.rel.to == related.model: + self._m2m_column_name_cache = f.column + break + # If this is an m2m relation to self, avoid the inevitable name clash + elif related.model == related.parent_model: + self._m2m_column_name_cache = 'from_' + related.model._meta.object_name.lower() + '_id' + else: + self._m2m_column_name_cache = related.model._meta.object_name.lower() + '_id' + + # Return the newly cached value + return self._m2m_column_name_cache def _get_m2m_reverse_name(self, related): "Function that can be curried to provide the related column name for the m2m table" - # If this is an m2m relation to self, avoid the inevitable name clash - if related.model == related.parent_model: - return 'to_' + related.parent_model._meta.object_name.lower() + '_id' - else: - return related.parent_model._meta.object_name.lower() + '_id' + try: + return self._m2m_reverse_name_cache + except: + if self.rel.through is not None: + found = False + for f in self.rel.through_model._meta.fields: + if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model: + if related.model == related.parent_model: + # If this is an m2m-intermediate to self, + # the first foreign key you find will be + # the source column. Keep searching for + # the second foreign key. + if found: + self._m2m_reverse_name_cache = f.column + break + else: + found = True + else: + self._m2m_reverse_name_cache = f.column + break + # If this is an m2m relation to self, avoid the inevitable name clash + elif related.model == related.parent_model: + self._m2m_reverse_name_cache = 'to_' + related.parent_model._meta.object_name.lower() + '_id' + else: + self._m2m_reverse_name_cache = related.parent_model._meta.object_name.lower() + '_id' + + # Return the newly cached value + return self._m2m_reverse_name_cache def isValidIDList(self, field_data, all_data): "Validates that the value is a valid list of foreign keys" @@ -791,13 +858,23 @@ class ManyToManyField(RelatedField, Field): return new_data def contribute_to_class(self, cls, name): - super(ManyToManyField, self).contribute_to_class(cls, name) + super(ManyToManyField, self).contribute_to_class(cls, name) # Add the descriptor for the m2m relation setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self)) # Set up the accessor for the m2m table name for the relation self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta) - + + # Populate some necessary rel arguments so that cross-app relations + # work correctly. + if isinstance(self.rel.through, basestring): + def resolve_through_model(field, model, cls): + field.rel.through_model = model + add_lazy_relation(cls, self, self.rel.through, resolve_through_model) + elif self.rel.through: + self.rel.through_model = self.rel.through + self.rel.through = self.rel.through._meta.object_name + if isinstance(self.rel.to, basestring): target = self.rel.to else: diff --git a/django/db/models/fields/subclassing.py b/django/db/models/fields/subclassing.py index 36f7e4d934..10add10739 100644 --- a/django/db/models/fields/subclassing.py +++ b/django/db/models/fields/subclassing.py @@ -5,9 +5,7 @@ Add SubfieldBase as the __metaclass__ for your Field subclass, implement to_python() and the other necessary methods and everything will work seamlessly. """ -from django.utils.maxlength import LegacyMaxlength - -class SubfieldBase(LegacyMaxlength): +class SubfieldBase(type): """ A metaclass for custom Field subclasses. This ensures the model's attribute has the descriptor protocol attached to it. @@ -50,4 +48,3 @@ def make_contrib(func=None): setattr(cls, self.name, Creator(self)) return contribute_to_class - diff --git a/django/db/models/query.py b/django/db/models/query.py index f0a0cf8218..5b24195a7d 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1,4 +1,3 @@ -import warnings try: set except NameError: @@ -757,22 +756,6 @@ class EmptyQuerySet(QuerySet): yield iter([]).next() -# QOperator, QNot, QAnd and QOr are temporarily retained for backwards -# compatibility. All the old functionality is now part of the 'Q' class. -class QOperator(Q): - def __init__(self, *args, **kwargs): - warnings.warn('Use Q instead of QOr, QAnd or QOperation.', - DeprecationWarning, stacklevel=2) - super(QOperator, self).__init__(*args, **kwargs) - -QOr = QAnd = QOperator - - -def QNot(q): - warnings.warn('Use ~q instead of QNot(q)', DeprecationWarning, stacklevel=2) - return ~q - - def get_cached_row(klass, row, index_start, max_depth=0, cur_depth=0, requested=None): """ @@ -785,7 +768,11 @@ def get_cached_row(klass, row, index_start, max_depth=0, cur_depth=0, restricted = requested is not None index_end = index_start + len(klass._meta.fields) - obj = klass(*row[index_start:index_end]) + fields = row[index_start:index_end] + if not [x for x in fields if x is not None]: + # If we only have a list of Nones, there was not related object. + return None, index_end + obj = klass(*fields) for f in klass._meta.fields: if not select_related_descend(f, restricted, requested): continue @@ -831,9 +818,15 @@ def delete_objects(seen_objs): del_query.delete_batch_related(pk_list) update_query = sql.UpdateQuery(cls, connection) - for field in cls._meta.fields: - if field.rel and field.null and field.rel.to in seen_objs: - update_query.clear_related(field, pk_list) + for field, model in cls._meta.get_fields_with_model(): + if (field.rel and field.null and field.rel.to in seen_objs and + filter(lambda f: f.column == field.column, + field.rel.to._meta.fields)): + if model: + sql.UpdateQuery(model, connection).clear_related(field, + pk_list) + else: + update_query.clear_related(field, pk_list) # Now delete the actual data. for cls in ordered_classes: diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index ef69d7657f..18dd8cc3f2 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -7,7 +7,6 @@ databases). The abstraction barrier only works one way: this module has to know all about the internals of models in order to get the information it needs. """ -import datetime from copy import deepcopy from django.utils.tree import Node @@ -107,6 +106,8 @@ class Query(object): Pickling support. """ obj_dict = self.__dict__.copy() + obj_dict['related_select_fields'] = [] + obj_dict['related_select_cols'] = [] del obj_dict['connection'] return obj_dict @@ -196,14 +197,18 @@ class Query(object): Returns an iterator over the results from executing this query. """ resolve_columns = hasattr(self, 'resolve_columns') - if resolve_columns: - if self.select_fields: - fields = self.select_fields + self.related_select_fields - else: - fields = self.model._meta.fields + fields = None for rows in self.execute_sql(MULTI): for row in rows: if resolve_columns: + if fields is None: + # We only set this up here because + # related_select_fields isn't populated until + # execute_sql() has been called. + if self.select_fields: + fields = self.select_fields + self.related_select_fields + else: + fields = self.model._meta.fields row = self.resolve_columns(row, fields) yield row @@ -1088,20 +1093,23 @@ class Query(object): join_it = iter(join_list) table_it = iter(self.tables) join_it.next(), table_it.next() + table_promote = False for join in join_it: table = table_it.next() if join == table and self.alias_refcount[join] > 1: continue - self.promote_alias(join) + join_promote = self.promote_alias(join) if table != join: - self.promote_alias(table) + table_promote = self.promote_alias(table) break for join in join_it: - self.promote_alias(join) + if self.promote_alias(join, join_promote): + join_promote = True for table in table_it: # Some of these will have been promoted from the join_list, but # that's harmless. - self.promote_alias(table) + if self.promote_alias(table, table_promote): + table_promote = True self.where.add((alias, col, field, lookup_type, value), connector) @@ -1214,7 +1222,6 @@ class Query(object): raise MultiJoin(pos + 1) if model: # The field lives on a base class of the current model. - alias_list = [] for int_model in opts.get_base_chain(model): lhs_col = opts.parents[int_model].column dedupe = lhs_col in opts.duplicate_targets @@ -1618,8 +1625,9 @@ class Query(object): if self.ordering_aliases: result = order_modified_iter(cursor, len(self.ordering_aliases), self.connection.features.empty_fetchmany_value) - result = iter((lambda: cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE)), - self.connection.features.empty_fetchmany_value) + else: + result = iter((lambda: cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE)), + self.connection.features.empty_fetchmany_value) if not self.connection.features.can_use_chunked_reads: # If we are using non-chunked reads, we return the same data # structure as normally, but ensure it is all read into memory diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py index 18e4bf2f7e..662d99a4a2 100644 --- a/django/db/models/sql/where.py +++ b/django/db/models/sql/where.py @@ -35,20 +35,30 @@ class WhereNode(tree.Node): storing any reference to field objects). Otherwise, the 'data' is stored unchanged and can be anything with an 'as_sql()' method. """ + # Because of circular imports, we need to import this here. + from django.db.models.base import ObjectDoesNotExist + if not isinstance(data, (list, tuple)): super(WhereNode, self).add(data, connector) return alias, col, field, lookup_type, value = data - if field: - params = field.get_db_prep_lookup(lookup_type, value) - db_type = field.db_type() - else: - # This is possible when we add a comparison to NULL sometimes (we - # don't really need to waste time looking up the associated field - # object). - params = Field().get_db_prep_lookup(lookup_type, value) - db_type = None + try: + if field: + params = field.get_db_prep_lookup(lookup_type, value) + db_type = field.db_type() + else: + # This is possible when we add a comparison to NULL sometimes + # (we don't really need to waste time looking up the associated + # field object). + params = Field().get_db_prep_lookup(lookup_type, value) + db_type = None + except ObjectDoesNotExist: + # This can happen when trying to insert a reference to a null pk. + # We break out of the normal path and indicate there's nothing to + # match. + super(WhereNode, self).add(NothingNode(), connector) + return if isinstance(value, datetime.datetime): annotation = datetime.datetime else: @@ -190,3 +200,14 @@ class EverythingNode(object): def relabel_aliases(self, change_map, node=None): return + +class NothingNode(object): + """ + A node that matches nothing. + """ + def as_sql(self, qn=None): + raise EmptyResultSet + + def relabel_aliases(self, change_map, node=None): + return + |