diff options
| author | Derek Anderson <public@kered.org> | 2006-10-26 19:09:51 +0000 |
|---|---|---|
| committer | Derek Anderson <public@kered.org> | 2006-10-26 19:09:51 +0000 |
| commit | 42851d90dadbf62f5d342ce5c4f496ba1eeba987 (patch) | |
| tree | a5d0e5c178afb2d7dbb7bf5ab37db9ced42f4b52 /django/db | |
| parent | 450889c9a6f7da3c2fce77a0ccf4c4cea9e29710 (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.py | 18 | ||||
| -rw-r--r-- | django/db/backends/mysql/introspection.py | 3 | ||||
| -rw-r--r-- | django/db/backends/oracle/base.py | 1 | ||||
| -rw-r--r-- | django/db/backends/oracle/introspection.py | 2 | ||||
| -rw-r--r-- | django/db/backends/postgresql_psycopg2/base.py | 21 | ||||
| -rw-r--r-- | django/db/backends/postgresql_psycopg2/introspection.py | 1 | ||||
| -rw-r--r-- | django/db/backends/sqlite3/base.py | 17 | ||||
| -rw-r--r-- | django/db/backends/util.py | 8 | ||||
| -rw-r--r-- | django/db/models/__init__.py | 14 | ||||
| -rw-r--r-- | django/db/models/base.py | 18 | ||||
| -rw-r--r-- | django/db/models/fields/__init__.py | 15 | ||||
| -rw-r--r-- | django/db/models/fields/generic.py | 2 | ||||
| -rw-r--r-- | django/db/models/fields/related.py | 24 | ||||
| -rw-r--r-- | django/db/models/loading.py | 10 | ||||
| -rw-r--r-- | django/db/models/manager.py | 3 | ||||
| -rw-r--r-- | django/db/models/manipulators.py | 17 | ||||
| -rw-r--r-- | django/db/models/query.py | 57 | ||||
| -rw-r--r-- | django/db/models/related.py | 3 |
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()) |
