summaryrefslogtreecommitdiff
path: root/django/db
diff options
context:
space:
mode:
authorDerek Anderson <public@kered.org>2006-10-26 19:09:51 +0000
committerDerek Anderson <public@kered.org>2006-10-26 19:09:51 +0000
commit42851d90dadbf62f5d342ce5c4f496ba1eeba987 (patch)
treea5d0e5c178afb2d7dbb7bf5ab37db9ced42f4b52 /django/db
parent450889c9a6f7da3c2fce77a0ccf4c4cea9e29710 (diff)
committing to schema-evolution
merge from HEAD git-svn-id: http://code.djangoproject.com/svn/django/branches/schema-evolution@3937 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django/db')
-rw-r--r--django/db/backends/mysql/base.py18
-rw-r--r--django/db/backends/mysql/introspection.py3
-rw-r--r--django/db/backends/oracle/base.py1
-rw-r--r--django/db/backends/oracle/introspection.py2
-rw-r--r--django/db/backends/postgresql_psycopg2/base.py21
-rw-r--r--django/db/backends/postgresql_psycopg2/introspection.py1
-rw-r--r--django/db/backends/sqlite3/base.py17
-rw-r--r--django/db/backends/util.py8
-rw-r--r--django/db/models/__init__.py14
-rw-r--r--django/db/models/base.py18
-rw-r--r--django/db/models/fields/__init__.py15
-rw-r--r--django/db/models/fields/generic.py2
-rw-r--r--django/db/models/fields/related.py24
-rw-r--r--django/db/models/loading.py10
-rw-r--r--django/db/models/manager.py3
-rw-r--r--django/db/models/manipulators.py17
-rw-r--r--django/db/models/query.py57
-rw-r--r--django/db/models/related.py3
18 files changed, 139 insertions, 95 deletions
diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py
index 50b76cc3a0..924c032b4c 100644
--- a/django/db/backends/mysql/base.py
+++ b/django/db/backends/mysql/base.py
@@ -13,6 +13,7 @@ except ImportError, e:
from MySQLdb.converters import conversions
from MySQLdb.constants import FIELD_TYPE
import types
+import re
DatabaseError = Database.DatabaseError
@@ -24,6 +25,12 @@ django_conversions.update({
FIELD_TYPE.TIME: util.typecast_time,
})
+# This should match the numerical portion of the version numbers (we can treat
+# versions like 5.0.24 and 5.0.24a as the same). Based on the list of version
+# at http://dev.mysql.com/doc/refman/4.1/en/news.html and
+# http://dev.mysql.com/doc/refman/5.0/en/news.html .
+server_version_re = re.compile(r'(\d{1,2})\.(\d{1,2})\.(\d{1,2})')
+
# This is an extra debug layer over MySQL queries, to display warnings.
# It's only used when DEBUG=True.
class MysqlDebugWrapper:
@@ -61,6 +68,7 @@ class DatabaseWrapper(local):
def __init__(self):
self.connection = None
self.queries = []
+ self.server_version = None
def _valid_connection(self):
if self.connection is not None:
@@ -110,6 +118,16 @@ class DatabaseWrapper(local):
self.connection.close()
self.connection = None
+ def get_server_version(self):
+ if not self.server_version:
+ if not self._valid_connection():
+ self.cursor()
+ m = server_version_re.match(self.connection.get_server_info())
+ if not m:
+ raise Exception('Unable to determine MySQL version from version string %r' % self.connection.get_server_info())
+ self.server_version = tuple([int(x) for x in m.groups()])
+ return self.server_version
+
supports_constraints = True
def quote_name(name):
diff --git a/django/db/backends/mysql/introspection.py b/django/db/backends/mysql/introspection.py
index 202fced1dc..7caf5fa060 100644
--- a/django/db/backends/mysql/introspection.py
+++ b/django/db/backends/mysql/introspection.py
@@ -36,13 +36,14 @@ def get_relations(cursor, table_name):
SELECT column_name, referenced_table_name, referenced_column_name
FROM information_schema.key_column_usage
WHERE table_name = %s
+ AND table_schema = DATABASE()
AND referenced_table_name IS NOT NULL
AND referenced_column_name IS NOT NULL""", [table_name])
constraints.extend(cursor.fetchall())
except (ProgrammingError, OperationalError):
# Fall back to "SHOW CREATE TABLE", for previous MySQL versions.
# Go through all constraints and save the equal matches.
- cursor.execute("SHOW CREATE TABLE %s" % table_name)
+ cursor.execute("SHOW CREATE TABLE %s" % quote_name(table_name))
for row in cursor.fetchall():
pos = 0
while True:
diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py
index 188c4bb678..dfe2df11dc 100644
--- a/django/db/backends/oracle/base.py
+++ b/django/db/backends/oracle/base.py
@@ -10,7 +10,6 @@ try:
except ImportError, e:
from django.core.exceptions import ImproperlyConfigured
raise ImproperlyConfigured, "Error loading cx_Oracle module: %s" % e
-import types
DatabaseError = Database.Error
diff --git a/django/db/backends/oracle/introspection.py b/django/db/backends/oracle/introspection.py
index 656741e440..ecc8f372a8 100644
--- a/django/db/backends/oracle/introspection.py
+++ b/django/db/backends/oracle/introspection.py
@@ -1,5 +1,3 @@
-from django.db import transaction
-from django.db.backends.oracle.base import quote_name
import re
foreign_key_re = re.compile(r"\sCONSTRAINT `[^`]*` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)` \(`([^`]*)`\)")
diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py
index 55cba81b70..4c835d89fc 100644
--- a/django/db/backends/postgresql_psycopg2/base.py
+++ b/django/db/backends/postgresql_psycopg2/base.py
@@ -43,6 +43,7 @@ class DatabaseWrapper(local):
self.connection = Database.connect(conn_string)
self.connection.set_isolation_level(1) # make transactions transparent to all cursors
cursor = self.connection.cursor()
+ cursor.tzinfo_factory = None
cursor.execute("SET TIME ZONE %s", [settings.TIME_ZONE])
if settings.DEBUG:
return util.CursorDebugWrapper(cursor, self)
@@ -67,23 +68,9 @@ def quote_name(name):
return name # Quoting once is enough.
return '"%s"' % name
-def dictfetchone(cursor):
- "Returns a row from the cursor as a dict"
- # TODO: cursor.dictfetchone() doesn't exist in psycopg2,
- # but no Django code uses this. Safe to remove?
- return cursor.dictfetchone()
-
-def dictfetchmany(cursor, number):
- "Returns a certain number of rows from a cursor as a dict"
- # TODO: cursor.dictfetchmany() doesn't exist in psycopg2,
- # but no Django code uses this. Safe to remove?
- return cursor.dictfetchmany(number)
-
-def dictfetchall(cursor):
- "Returns all rows from a cursor as a dict"
- # TODO: cursor.dictfetchall() doesn't exist in psycopg2,
- # but no Django code uses this. Safe to remove?
- return cursor.dictfetchall()
+dictfetchone = util.dictfetchone
+dictfetchmany = util.dictfetchmany
+dictfetchall = util.dictfetchall
def get_last_insert_id(cursor, table_name, pk_name):
cursor.execute("SELECT CURRVAL('\"%s_%s_seq\"')" % (table_name, pk_name))
diff --git a/django/db/backends/postgresql_psycopg2/introspection.py b/django/db/backends/postgresql_psycopg2/introspection.py
index b991493d39..a546da8c45 100644
--- a/django/db/backends/postgresql_psycopg2/introspection.py
+++ b/django/db/backends/postgresql_psycopg2/introspection.py
@@ -1,4 +1,3 @@
-from django.db import transaction
from django.db.backends.postgresql_psycopg2.base import quote_name
def get_table_list(cursor):
diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
index 5c884ff121..01e4b3aebf 100644
--- a/django/db/backends/sqlite3/base.py
+++ b/django/db/backends/sqlite3/base.py
@@ -4,10 +4,18 @@ SQLite3 backend for django. Requires pysqlite2 (http://pysqlite.org/).
from django.db.backends import util
try:
- from pysqlite2 import dbapi2 as Database
+ try:
+ from sqlite3 import dbapi2 as Database
+ except ImportError:
+ from pysqlite2 import dbapi2 as Database
except ImportError, e:
+ import sys
from django.core.exceptions import ImproperlyConfigured
- raise ImproperlyConfigured, "Error loading pysqlite2 module: %s" % e
+ if sys.version_info < (2, 5, 0):
+ module = 'pysqlite2'
+ else:
+ module = 'sqlite3'
+ raise ImproperlyConfigured, "Error loading %s module: %s" % (module, e)
DatabaseError = Database.DatabaseError
@@ -62,7 +70,10 @@ class DatabaseWrapper(local):
self.connection.rollback()
def close(self):
- if self.connection is not None:
+ from django.conf import settings
+ # If database is in memory, closing the connection destroys the database.
+ # To prevent accidental data loss, ignore close requests on an in-memory db.
+ if self.connection is not None and settings.DATABASE_NAME != ":memory:":
self.connection.close()
self.connection = None
diff --git a/django/db/backends/util.py b/django/db/backends/util.py
index 74d33f42ca..3ec1b41485 100644
--- a/django/db/backends/util.py
+++ b/django/db/backends/util.py
@@ -98,7 +98,7 @@ def rev_typecast_boolean(obj, d):
def _dict_helper(desc, row):
"Returns a dictionary for the given cursor.description and result row."
- return dict([(desc[col[0]][0], col[1]) for col in enumerate(row)])
+ return dict(zip([col[0] for col in desc], row))
def dictfetchone(cursor):
"Returns a row from the cursor as a dict"
@@ -110,9 +110,11 @@ def dictfetchone(cursor):
def dictfetchmany(cursor, number):
"Returns a certain number of rows from a cursor as a dict"
desc = cursor.description
- return [_dict_helper(desc, row) for row in cursor.fetchmany(number)]
+ for row in cursor.fetchmany(number):
+ yield _dict_helper(desc, row)
def dictfetchall(cursor):
"Returns all rows from a cursor as a dict"
desc = cursor.description
- return [_dict_helper(desc, row) for row in cursor.fetchall()]
+ for row in cursor.fetchall():
+ yield _dict_helper(desc, row)
diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py
index 82b1238723..0308dd047a 100644
--- a/django/db/models/__init__.py
+++ b/django/db/models/__init__.py
@@ -16,6 +16,18 @@ from django.utils.text import capfirst
# Admin stages.
ADD, CHANGE, BOTH = 1, 2, 3
+# Decorator. Takes a function that returns a tuple in this format:
+# (viewname, viewargs, viewkwargs)
+# Returns a function that calls urlresolvers.reverse() on that data, to return
+# the URL for those parameters.
+def permalink(func):
+ from django.core.urlresolvers import reverse
+ def inner(*args, **kwargs):
+ bits = func(*args, **kwargs)
+ viewname = bits[0]
+ return reverse(bits[0], None, *bits[1:3])
+ return inner
+
class LazyDate(object):
"""
Use in limit_choices_to to compare the field to dates calculated at run time
@@ -35,7 +47,7 @@ class LazyDate(object):
return "<LazyDate: %s>" % self.delta
def __get_value__(self):
- return datetime.datetime.now() + self.delta
+ return (datetime.datetime.now() + self.delta).date()
def __getattr__(self, attr):
return getattr(self.__get_value__(), attr)
diff --git a/django/db/models/base.py b/django/db/models/base.py
index c89033c491..bdae7eccc2 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -4,8 +4,7 @@ from django.core import validators
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.fields import AutoField, ImageField, FieldDoesNotExist
from django.db.models.fields.related import OneToOneRel, ManyToOneRel
-from django.db.models.related import RelatedObject
-from django.db.models.query import orderlist2sql, delete_objects
+from django.db.models.query import delete_objects
from django.db.models.options import Options, AdminOptions
from django.db import connection, backend, transaction
from django.db.models import signals
@@ -45,7 +44,7 @@ class ModelBase(type):
new_class._meta.app_label = model_module.__name__.split('.')[-2]
# Bail out early if we have already created this class.
- m = get_model(new_class._meta.app_label, name)
+ m = get_model(new_class._meta.app_label, name, False)
if m is not None:
return m
@@ -69,7 +68,7 @@ class ModelBase(type):
# the first class for this model to register with the framework. There
# should only be one class for each model, so we must always return the
# registered version.
- return get_model(new_class._meta.app_label, name)
+ return get_model(new_class._meta.app_label, name, False)
class Model(object):
__metaclass__ = ModelBase
@@ -177,11 +176,12 @@ class Model(object):
# If it does already exist, do an UPDATE.
if cursor.fetchone():
db_values = [f.get_db_prep_save(f.pre_save(self, False)) for f in non_pks]
- cursor.execute("UPDATE %s SET %s WHERE %s=%%s" % \
- (backend.quote_name(self._meta.db_table),
- ','.join(['%s=%%s' % backend.quote_name(f.column) for f in non_pks]),
- backend.quote_name(self._meta.pk.column)),
- db_values + [pk_val])
+ if db_values:
+ cursor.execute("UPDATE %s SET %s WHERE %s=%%s" % \
+ (backend.quote_name(self._meta.db_table),
+ ','.join(['%s=%%s' % backend.quote_name(f.column) for f in non_pks]),
+ backend.quote_name(self._meta.pk.column)),
+ db_values + [pk_val])
else:
record_exists = False
if not pk_set or not record_exists:
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index 98661fe36c..02b5ba8b9e 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -5,6 +5,7 @@ from django.core import validators
from django import forms
from django.core.exceptions import ObjectDoesNotExist
from django.utils.functional import curry
+from django.utils.itercompat import tee
from django.utils.text import capfirst
from django.utils.translation import gettext, gettext_lazy
import datetime, os, time
@@ -80,7 +81,7 @@ class Field(object):
self.prepopulate_from = prepopulate_from
self.unique_for_date, self.unique_for_month = unique_for_date, unique_for_month
self.unique_for_year = unique_for_year
- self.choices = choices or []
+ self._choices = choices or []
self.radio_admin = radio_admin
self.help_text = help_text
self.db_column = db_column
@@ -325,6 +326,14 @@ class Field(object):
def bind(self, fieldmapping, original, bound_field_class):
return bound_field_class(self, fieldmapping, original)
+ def _get_choices(self):
+ if hasattr(self._choices, 'next'):
+ choices, self._choices = tee(self._choices)
+ return choices
+ else:
+ return self._choices
+ choices = property(_get_choices)
+
class AutoField(Field):
empty_strings_allowed = False
def __init__(self, *args, **kwargs):
@@ -368,8 +377,8 @@ class BooleanField(Field):
def to_python(self, value):
if value in (True, False): return value
- if value in ('t', 'True'): return True
- if value in ('f', 'False'): return False
+ if value in ('t', 'True', '1'): return True
+ if value in ('f', 'False', '0'): return False
raise validators.ValidationError, gettext("This value must be either True or False.")
def get_manipulator_field_objs(self):
diff --git a/django/db/models/fields/generic.py b/django/db/models/fields/generic.py
index 5f4de40e69..7d7651029c 100644
--- a/django/db/models/fields/generic.py
+++ b/django/db/models/fields/generic.py
@@ -117,7 +117,7 @@ class GenericRelation(RelatedField, Field):
return self.object_id_field_name
def m2m_reverse_name(self):
- return self.model._meta.pk.attname
+ return self.object_id_field_name
def contribute_to_class(self, cls, name):
super(GenericRelation, self).contribute_to_class(cls, name)
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index 9a8a61878e..bd9262d55a 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -1,8 +1,8 @@
-from django.db import backend, connection, transaction
+from django.db import backend, transaction
from django.db.models import signals, get_model
from django.db.models.fields import AutoField, Field, IntegerField, get_ul_class
from django.db.models.related import RelatedObject
-from django.utils.translation import gettext_lazy, string_concat
+from django.utils.translation import gettext_lazy, string_concat, ngettext
from django.utils.functional import curry
from django.core import validators
from django import forms
@@ -23,9 +23,9 @@ def add_lookup(rel_cls, field):
name = field.rel.to
module = rel_cls.__module__
key = (module, name)
- # Has the model already been loaded?
+ # Has the model already been loaded?
# If so, resolve the string reference right away
- model = get_model(rel_cls._meta.app_label,field.rel.to)
+ model = get_model(rel_cls._meta.app_label, field.rel.to, False)
if model:
field.rel.to = model
field.do_related_class(model, rel_cls)
@@ -46,7 +46,7 @@ def manipulator_valid_rel_key(f, self, field_data, all_data):
"Validates that the value is a valid foreign key"
klass = f.rel.to
try:
- klass._default_manager.get(pk=field_data)
+ klass._default_manager.get(**{f.rel.field_name: field_data})
except klass.DoesNotExist:
raise validators.ValidationError, _("Please enter a valid %s.") % f.verbose_name
@@ -79,11 +79,11 @@ class RelatedField(object):
self.contribute_to_related_class(other, related)
def get_db_prep_lookup(self, lookup_type, value):
- # If we are doing a lookup on a Related Field, we must be
- # comparing object instances. The value should be the PK of value,
+ # If we are doing a lookup on a Related Field, we must be
+ # comparing object instances. The value should be the PK of value,
# not value itself.
def pk_trace(value):
- # Value may be a primary key, or an object held in a relation.
+ # Value may be a primary key, or an object held in a relation.
# If it is an object, then we need to get the primary key value for
# that object. In certain conditions (especially one-to-one relations),
# the primary key may itself be an object - so we need to keep drilling
@@ -94,8 +94,8 @@ class RelatedField(object):
v = getattr(v, v._meta.pk.name)
except AttributeError:
pass
- return v
-
+ return v
+
if lookup_type == 'exact':
return [pk_trace(value)]
if lookup_type == 'in':
@@ -103,7 +103,7 @@ class RelatedField(object):
elif lookup_type == 'isnull':
return []
raise TypeError, "Related Field has invalid lookup: %s" % lookup_type
-
+
def _get_related_query_name(self, opts):
# This method defines the name that can be used to identify this related object
# in a table-spanning query. It uses the lower-cased object_name by default,
@@ -618,7 +618,7 @@ class ManyToManyField(RelatedField, Field):
msg = gettext_lazy('Separate multiple IDs with commas.')
else:
msg = gettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.')
- self.help_text = string_concat(self.help_text, msg)
+ self.help_text = string_concat(self.help_text, ' ', msg)
def get_manipulator_field_objs(self):
if self.rel.raw_id_admin:
diff --git a/django/db/models/loading.py b/django/db/models/loading.py
index c7920fa4e0..22f83bfd78 100644
--- a/django/db/models/loading.py
+++ b/django/db/models/loading.py
@@ -32,7 +32,7 @@ def get_apps():
_app_errors[app_name] = e
return _app_list
-def get_app(app_label, emptyOK = False):
+def get_app(app_label, emptyOK=False):
"Returns the module containing the models for the given app_label. If the app has no models in it and 'emptyOK' is True, returns None."
get_apps() # Run get_apps() to populate the _app_list cache. Slightly hackish.
for app_name in settings.INSTALLED_APPS:
@@ -75,11 +75,15 @@ def get_models(app_mod=None):
model_list.extend(get_models(app_mod))
return model_list
-def get_model(app_label, model_name):
+def get_model(app_label, model_name, seed_cache=True):
"""
- Returns the model matching the given app_label and case-insensitive model_name.
+ Returns the model matching the given app_label and case-insensitive
+ model_name.
+
Returns None if no model is found.
"""
+ if seed_cache:
+ get_apps()
try:
model_dict = _app_models[app_label]
except KeyError:
diff --git a/django/db/models/manager.py b/django/db/models/manager.py
index 46a1710c1c..6005874516 100644
--- a/django/db/models/manager.py
+++ b/django/db/models/manager.py
@@ -1,10 +1,7 @@
-from django.utils.functional import curry
-from django.db import backend, connection
from django.db.models.query import QuerySet
from django.dispatch import dispatcher
from django.db.models import signals
from django.db.models.fields import FieldDoesNotExist
-from django.utils.datastructures import SortedDict
# Size of each "chunk" for get_iterator calls.
# Larger values are slightly faster at the expense of more storage space.
diff --git a/django/db/models/manipulators.py b/django/db/models/manipulators.py
index 454c318e5d..83ddda844e 100644
--- a/django/db/models/manipulators.py
+++ b/django/db/models/manipulators.py
@@ -5,7 +5,7 @@ from django.db.models.fields import FileField, AutoField
from django.dispatch import dispatcher
from django.db.models import signals
from django.utils.functional import curry
-from django.utils.datastructures import DotExpandedDict, MultiValueDict
+from django.utils.datastructures import DotExpandedDict
from django.utils.text import capfirst
import types
@@ -76,7 +76,7 @@ class AutomaticManipulator(forms.Manipulator):
# Add field for ordering.
if self.change and self.opts.get_ordered_objects():
- self.fields.append(formfields.CommaSeparatedIntegerField(field_name="order_"))
+ self.fields.append(forms.CommaSeparatedIntegerField(field_name="order_"))
def save(self, new_data):
# TODO: big cleanup when core fields go -> use recursive manipulators.
@@ -138,7 +138,7 @@ class AutomaticManipulator(forms.Manipulator):
child_follow = self.follow.get(related.name, None)
if child_follow:
- obj_list = expanded_data[related.var_name].items()
+ obj_list = expanded_data.get(related.var_name, {}).items()
if not obj_list:
continue
@@ -177,7 +177,7 @@ class AutomaticManipulator(forms.Manipulator):
# case, because they'll be dealt with later.
if f == related.field:
- param = getattr(new_object, related.field.rel.field_name)
+ param = getattr(new_object, related.field.rel.get_related_field().attname)
elif (not self.change) and isinstance(f, AutoField):
param = None
elif self.change and (isinstance(f, FileField) or not child_follow.get(f.name, None)):
@@ -215,8 +215,11 @@ class AutomaticManipulator(forms.Manipulator):
# Save many-to-many objects.
for f in related.opts.many_to_many:
if child_follow.get(f.name, None) and not f.rel.edit_inline:
- was_changed = getattr(new_rel_obj, 'set_%s' % f.name)(rel_new_data[f.attname])
- if self.change and was_changed:
+ new_value = rel_new_data[f.attname]
+ if f.rel.raw_id_admin:
+ new_value = new_value[0]
+ setattr(new_rel_obj, f.name, f.rel.to.objects.filter(pk__in=new_value))
+ if self.change:
self.fields_changed.append('%s for %s "%s"' % (f.verbose_name, related.opts.verbose_name, new_rel_obj))
# If, in the change stage, all of the core fields were blank and
@@ -300,7 +303,7 @@ def manipulator_validator_unique_together(field_name_list, opts, self, field_dat
pass
else:
raise validators.ValidationError, _("%(object)s with this %(type)s already exists for the given %(field)s.") % \
- {'object': capfirst(opts.verbose_name), 'type': field_list[0].verbose_name, 'field': get_text_list(field_name_list[1:], 'and')}
+ {'object': capfirst(opts.verbose_name), 'type': field_list[0].verbose_name, 'field': get_text_list([f.verbose_name for f in field_list[1:]], 'and')}
def manipulator_validator_unique_for_date(from_field, date_field, opts, lookup_type, self, field_data, all_data):
from django.db.models.fields.related import ManyToOneRel
diff --git a/django/db/models/query.py b/django/db/models/query.py
index 2b4a13354b..53ed63ae5b 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -18,7 +18,7 @@ QUERY_TERMS = (
'exact', 'iexact', 'contains', 'icontains',
'gt', 'gte', 'lt', 'lte', 'in',
'startswith', 'istartswith', 'endswith', 'iendswith',
- 'range', 'year', 'month', 'day', 'isnull',
+ 'range', 'year', 'month', 'day', 'isnull', 'search',
)
# Size of each "chunk" for get_iterator calls.
@@ -707,34 +707,35 @@ def parse_lookup(kwarg_items, opts):
joins, where, params = SortedDict(), [], []
for kwarg, value in kwarg_items:
- if value is not None:
- path = kwarg.split(LOOKUP_SEPARATOR)
- # Extract the last elements of the kwarg.
- # The very-last is the lookup_type (equals, like, etc).
- # The second-last is the table column on which the lookup_type is
- # to be performed.
- # The exceptions to this are:
- # 1) "pk", which is an implicit id__exact;
- # if we find "pk", make the lookup_type "exact', and insert
- # a dummy name of None, which we will replace when
- # we know which table column to grab as the primary key.
- # 2) If there is only one part, or the last part is not a query
- # term, assume that the query is an __exact
- lookup_type = path.pop()
- if lookup_type == 'pk':
- lookup_type = 'exact'
- path.append(None)
- elif len(path) == 0 or lookup_type not in QUERY_TERMS:
- path.append(lookup_type)
- lookup_type = 'exact'
+ path = kwarg.split(LOOKUP_SEPARATOR)
+ # Extract the last elements of the kwarg.
+ # The very-last is the lookup_type (equals, like, etc).
+ # The second-last is the table column on which the lookup_type is
+ # to be performed. If this name is 'pk', it will be substituted with
+ # the name of the primary key.
+ # If there is only one part, or the last part is not a query
+ # term, assume that the query is an __exact
+ lookup_type = path.pop()
+ if lookup_type == 'pk':
+ lookup_type = 'exact'
+ path.append(None)
+ elif len(path) == 0 or lookup_type not in QUERY_TERMS:
+ path.append(lookup_type)
+ lookup_type = 'exact'
- if len(path) < 1:
- raise TypeError, "Cannot parse keyword query %r" % kwarg
+ if len(path) < 1:
+ raise TypeError, "Cannot parse keyword query %r" % kwarg
+
+ if value is None:
+ # Interpret '__exact=None' as the sql '= NULL'; otherwise, reject
+ # all uses of None as a query value.
+ if lookup_type != 'exact':
+ raise ValueError, "Cannot use None as a query value"
- joins2, where2, params2 = lookup_inner(path, lookup_type, value, opts, opts.db_table, None)
- joins.update(joins2)
- where.extend(where2)
- params.extend(params2)
+ joins2, where2, params2 = lookup_inner(path, lookup_type, value, opts, opts.db_table, None)
+ joins.update(joins2)
+ where.extend(where2)
+ params.extend(params2)
return joins, where, params
class FieldFound(Exception):
@@ -766,7 +767,7 @@ def lookup_inner(path, lookup_type, value, opts, table, column):
name = path.pop(0)
# Has the primary key been requested? If so, expand it out
# to be the name of the current class' primary key
- if name is None:
+ if name is None or name == 'pk':
name = current_opts.pk.name
# Try to find the name in the fields associated with the current class
diff --git a/django/db/models/related.py b/django/db/models/related.py
index ee3b916cf4..ac1ec50ca2 100644
--- a/django/db/models/related.py
+++ b/django/db/models/related.py
@@ -131,6 +131,9 @@ class RelatedObject(object):
# many-to-many objects. It uses the lower-cased object_name + "_set",
# but this can be overridden with the "related_name" option.
if self.field.rel.multiple:
+ # If this is a symmetrical m2m relation on self, there is no reverse accessor.
+ if getattr(self.field.rel, 'symmetrical', False) and self.model == self.parent_model:
+ return None
return self.field.rel.related_name or (self.opts.object_name.lower() + '_set')
else:
return self.field.rel.related_name or (self.opts.object_name.lower())