diff --git a/AUTHORS b/AUTHORS
index ce4643ac59..7d82f69006 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -57,6 +57,7 @@ answer newbie questions, and generally made Django that much better:
David Ascher
Jökull Sólberg Auðunsson
Arthur
+ av0000@mail.ru
David Avsajanishvili
axiak@mit.edu
Niran Babalola
@@ -79,6 +80,7 @@ answer newbie questions, and generally made Django that much better:
brut.alll@gmail.com
btoll@bestweb.net
Jonathan Buchanan
+ Keith Bussell
Juan Manuel Caicedo
Trevor Caira
Ricardo Javier Cárdenes Medina
@@ -368,7 +370,7 @@ answer newbie questions, and generally made Django that much better:
Makoto Tsuyuki
tt@gurgle.no
David Tulig
- Amit Upadhyay
+ Amit Upadhyay
Geert Vanderkelen
I.S. van Oostveen
viestards.lists@gmail.com
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 8eea31b0db..cdf71c00dc 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -289,7 +289,7 @@ SESSION_COOKIE_DOMAIN = None # A string like ".lawren
SESSION_COOKIE_SECURE = False # Whether the session cookie should be secure (https:// only).
SESSION_COOKIE_PATH = '/' # The path of the session cookie.
SESSION_SAVE_EVERY_REQUEST = False # Whether to save the session data on every request.
-SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Whether sessions expire when a user closes his browser.
+SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Whether a user's session cookie expires when they close their browser.
SESSION_ENGINE = 'django.contrib.sessions.backends.db' # The module to store session data
SESSION_FILE_PATH = None # Directory to store session files if using the file session module. If None, the backend will use a sensible default.
diff --git a/django/contrib/auth/create_superuser.py b/django/contrib/auth/create_superuser.py
index 7b6cefd268..7b58678b78 100644
--- a/django/contrib/auth/create_superuser.py
+++ b/django/contrib/auth/create_superuser.py
@@ -1,94 +1,8 @@
"""
-Helper function for creating superusers in the authentication system.
-
-If run from the command line, this module lets you create a superuser
-interactively.
+Create a superuser from the command line. Deprecated; use manage.py
+createsuperuser instead.
"""
-from django.core import validators
-from django.contrib.auth.models import User
-import getpass
-import os
-import sys
-import re
-
-RE_VALID_USERNAME = re.compile('\w+$')
-
-def createsuperuser(username=None, email=None, password=None):
- """
- Helper function for creating a superuser from the command line. All
- arguments are optional and will be prompted-for if invalid or not given.
- """
- try:
- import pwd
- except ImportError:
- default_username = ''
- else:
- # Determine the current system user's username, to use as a default.
- default_username = pwd.getpwuid(os.getuid())[0].replace(' ', '').lower()
-
- # Determine whether the default username is taken, so we don't display
- # it as an option.
- if default_username:
- try:
- User.objects.get(username=default_username)
- except User.DoesNotExist:
- pass
- else:
- default_username = ''
-
- try:
- while 1:
- if not username:
- input_msg = 'Username'
- if default_username:
- input_msg += ' (Leave blank to use %r)' % default_username
- username = raw_input(input_msg + ': ')
- if default_username and username == '':
- username = default_username
- if not RE_VALID_USERNAME.match(username):
- sys.stderr.write("Error: That username is invalid. Use only letters, digits and underscores.\n")
- username = None
- continue
- try:
- User.objects.get(username=username)
- except User.DoesNotExist:
- break
- else:
- sys.stderr.write("Error: That username is already taken.\n")
- username = None
- while 1:
- if not email:
- email = raw_input('E-mail address: ')
- try:
- validators.isValidEmail(email, None)
- except validators.ValidationError:
- sys.stderr.write("Error: That e-mail address is invalid.\n")
- email = None
- else:
- break
- while 1:
- if not password:
- password = getpass.getpass()
- password2 = getpass.getpass('Password (again): ')
- if password != password2:
- sys.stderr.write("Error: Your passwords didn't match.\n")
- password = None
- continue
- if password.strip() == '':
- sys.stderr.write("Error: Blank passwords aren't allowed.\n")
- password = None
- continue
- break
- except KeyboardInterrupt:
- sys.stderr.write("\nOperation cancelled.\n")
- sys.exit(1)
- u = User.objects.create_user(username, email, password)
- u.is_staff = True
- u.is_active = True
- u.is_superuser = True
- u.save()
- print "Superuser created successfully."
-
if __name__ == "__main__":
- createsuperuser()
+ from django.core.management import call_command
+ call_command("createsuperuser")
diff --git a/django/contrib/auth/management.py b/django/contrib/auth/management/__init__.py
similarity index 78%
rename from django/contrib/auth/management.py
rename to django/contrib/auth/management/__init__.py
index 2b4cb8bd19..8394bee5cd 100644
--- a/django/contrib/auth/management.py
+++ b/django/contrib/auth/management/__init__.py
@@ -32,7 +32,7 @@ def create_permissions(app, created_models, verbosity):
def create_superuser(app, created_models, verbosity, **kwargs):
from django.contrib.auth.models import User
- from django.contrib.auth.create_superuser import createsuperuser as do_create
+ from django.core.management import call_command
if User in created_models and kwargs.get('interactive', True):
msg = "\nYou just installed Django's auth system, which means you don't have " \
"any superusers defined.\nWould you like to create one now? (yes/no): "
@@ -42,8 +42,10 @@ def create_superuser(app, created_models, verbosity, **kwargs):
confirm = raw_input('Please enter either "yes" or "no": ')
continue
if confirm == 'yes':
- do_create()
+ call_command("createsuperuser", interactive=True)
break
-dispatcher.connect(create_permissions, signal=signals.post_syncdb)
-dispatcher.connect(create_superuser, sender=auth_app, signal=signals.post_syncdb)
+if 'create_permissions' not in [i.__name__ for i in dispatcher.getAllReceivers(signal=signals.post_syncdb)]:
+ dispatcher.connect(create_permissions, signal=signals.post_syncdb)
+if 'create_superuser' not in [i.__name__ for i in dispatcher.getAllReceivers(signal=signals.post_syncdb, sender=auth_app)]:
+ dispatcher.connect(create_superuser, sender=auth_app, signal=signals.post_syncdb)
\ No newline at end of file
diff --git a/django/contrib/auth/management/commands/__init__.py b/django/contrib/auth/management/commands/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py
new file mode 100644
index 0000000000..4299762c74
--- /dev/null
+++ b/django/contrib/auth/management/commands/createsuperuser.py
@@ -0,0 +1,123 @@
+"""
+Management utility to create superusers.
+"""
+
+import getpass
+import os
+import re
+import sys
+from optparse import make_option
+from django.contrib.auth.models import User, UNUSABLE_PASSWORD
+from django.core import validators
+from django.core.management.base import BaseCommand, CommandError
+
+RE_VALID_USERNAME = re.compile('\w+$')
+
+class Command(BaseCommand):
+ option_list = BaseCommand.option_list + (
+ make_option('--username', dest='username', default=None,
+ help='Specifies the username for the superuser.'),
+ make_option('--email', dest='email', default=None,
+ help='Specifies the email address for the superuser.'),
+ make_option('--noinput', action='store_false', dest='interactive', default=True,
+ help='Tells Django to NOT prompt the user for input of any kind. ' \
+ 'You must use --username and --email with --noinput, and ' \
+ 'superusers created with --noinput will not be able to log in ' \
+ 'until they\'re given a valid password.'),
+ )
+ help = 'Used to create a superuser.'
+
+ def handle(self, *args, **options):
+ username = options.get('username', None)
+ email = options.get('email', None)
+ interactive = options.get('interactive')
+
+ # Do quick and dirty validation if --noinput
+ if not interactive:
+ if not username or not email:
+ raise CommandError("You must use --username and --email with --noinput.")
+ if not RE_VALID_USERNAME.match(username):
+ raise CommandError("Invalid username. Use only letters, digits, and underscores")
+ try:
+ validators.isValidEmail(email, None)
+ except validators.ValidationError:
+ raise CommandError("Invalid email address.")
+
+ password = ''
+
+ # Try to determine the current system user's username to use as a default.
+ try:
+ import pwd
+ except ImportError:
+ default_username = ''
+ else:
+ default_username = pwd.getpwuid(os.getuid())[0].replace(' ', '').lower()
+
+ # Determine whether the default username is taken, so we don't display
+ # it as an option.
+ if default_username:
+ try:
+ User.objects.get(username=default_username)
+ except User.DoesNotExist:
+ pass
+ else:
+ default_username = ''
+
+ # Prompt for username/email/password. Enclose this whole thing in a
+ # try/except to trap for a keyboard interrupt and exit gracefully.
+ if interactive:
+ try:
+
+ # Get a username
+ while 1:
+ if not username:
+ input_msg = 'Username'
+ if default_username:
+ input_msg += ' (Leave blank to use %r)' % default_username
+ username = raw_input(input_msg + ': ')
+ if default_username and username == '':
+ username = default_username
+ if not RE_VALID_USERNAME.match(username):
+ sys.stderr.write("Error: That username is invalid. Use only letters, digits and underscores.\n")
+ username = None
+ continue
+ try:
+ User.objects.get(username=username)
+ except User.DoesNotExist:
+ break
+ else:
+ sys.stderr.write("Error: That username is already taken.\n")
+ username = None
+
+ # Get an email
+ while 1:
+ if not email:
+ email = raw_input('E-mail address: ')
+ try:
+ validators.isValidEmail(email, None)
+ except validators.ValidationError:
+ sys.stderr.write("Error: That e-mail address is invalid.\n")
+ email = None
+ else:
+ break
+
+ # Get a password
+ while 1:
+ if not password:
+ password = getpass.getpass()
+ password2 = getpass.getpass('Password (again): ')
+ if password != password2:
+ sys.stderr.write("Error: Your passwords didn't match.\n")
+ password = None
+ continue
+ if password.strip() == '':
+ sys.stderr.write("Error: Blank passwords aren't allowed.\n")
+ password = None
+ continue
+ break
+ except KeyboardInterrupt:
+ sys.stderr.write("\nOperation cancelled.\n")
+ sys.exit(1)
+
+ User.objects.create_superuser(username, email, password)
+ print "Superuser created successfully."
diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py
index e3f7541dda..56b4cbc082 100644
--- a/django/contrib/auth/models.py
+++ b/django/contrib/auth/models.py
@@ -113,6 +113,13 @@ class UserManager(models.Manager):
user.save()
return user
+ def create_superuser(self, username, email, password):
+ u = self.create_user(username, email, password)
+ u.is_staff = True
+ u.is_active = True
+ u.is_superuser = True
+ u.save()
+
def make_random_password(self, length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'):
"Generates a random password with the given length and given allowed_chars"
# Note that default value of allowed_chars does not have "I" or letters
diff --git a/django/contrib/auth/tests/basic.py b/django/contrib/auth/tests/basic.py
index 950815e791..8667ca9e5a 100644
--- a/django/contrib/auth/tests/basic.py
+++ b/django/contrib/auth/tests/basic.py
@@ -36,4 +36,21 @@ False
[]
>>> a.user_permissions.all()
[]
+
+#
+# Tests for createsuperuser management command.
+# It's nearly impossible to test the interactive mode -- a command test helper
+# would be needed (and *awesome*) -- so just test the non-interactive mode.
+# This covers most of the important validation, but not all.
+#
+>>> from django.core.management import call_command
+
+>>> call_command("createsuperuser", noinput=True, username="joe", email="joe@somewhere.org")
+Superuser created successfully.
+
+>>> u = User.objects.get(username="joe")
+>>> u.email
+u'joe@somewhere.org'
+>>> u.password
+u'!'
"""
\ No newline at end of file
diff --git a/django/contrib/sessions/backends/base.py b/django/contrib/sessions/backends/base.py
index b8726fd2bd..1063760915 100644
--- a/django/contrib/sessions/backends/base.py
+++ b/django/contrib/sessions/backends/base.py
@@ -4,6 +4,7 @@ import os
import random
import sys
import time
+from datetime import datetime, timedelta
from django.conf import settings
from django.core.exceptions import SuspiciousOperation
@@ -128,6 +129,62 @@ class SessionBase(object):
_session = property(_get_session)
+ def get_expiry_age(self):
+ """Get the number of seconds until the session expires."""
+ expiry = self.get('_session_expiry')
+ if not expiry: # Checks both None and 0 cases
+ return settings.SESSION_COOKIE_AGE
+ if not isinstance(expiry, datetime):
+ return expiry
+ delta = expiry - datetime.now()
+ return delta.days * 86400 + delta.seconds
+
+ def get_expiry_date(self):
+ """Get session the expiry date (as a datetime object)."""
+ expiry = self.get('_session_expiry')
+ if isinstance(expiry, datetime):
+ return expiry
+ if not expiry: # Checks both None and 0 cases
+ expiry = settings.SESSION_COOKIE_AGE
+ return datetime.now() + timedelta(seconds=expiry)
+
+ def set_expiry(self, value):
+ """
+ Sets a custom expiration for the session. ``value`` can be an integer, a
+ Python ``datetime`` or ``timedelta`` object or ``None``.
+
+ If ``value`` is an integer, the session will expire after that many
+ seconds of inactivity. If set to ``0`` then the session will expire on
+ browser close.
+
+ If ``value`` is a ``datetime`` or ``timedelta`` object, the session
+ will expire at that specific future time.
+
+ If ``value`` is ``None``, the session uses the global session expiry
+ policy.
+ """
+ if value is None:
+ # Remove any custom expiration for this session.
+ try:
+ del self['_session_expiry']
+ except KeyError:
+ pass
+ return
+ if isinstance(value, timedelta):
+ value = datetime.now() + value
+ self['_session_expiry'] = value
+
+ def get_expire_at_browser_close(self):
+ """
+ Returns ``True`` if the session is set to expire when the browser
+ closes, and ``False`` if there's an expiry date. Use
+ ``get_expiry_date()`` or ``get_expiry_age()`` to find the actual expiry
+ date/age, if there is one.
+ """
+ if self.get('_session_expiry') is None:
+ return settings.SESSION_EXPIRE_AT_BROWSER_CLOSE
+ return self.get('_session_expiry') == 0
+
# Methods that child classes must implement.
def exists(self, session_key):
diff --git a/django/contrib/sessions/backends/cache.py b/django/contrib/sessions/backends/cache.py
index c3e641e691..7626163a13 100644
--- a/django/contrib/sessions/backends/cache.py
+++ b/django/contrib/sessions/backends/cache.py
@@ -4,23 +4,23 @@ from django.core.cache import cache
class SessionStore(SessionBase):
"""
- A cache-based session store.
+ A cache-based session store.
"""
def __init__(self, session_key=None):
self._cache = cache
super(SessionStore, self).__init__(session_key)
-
+
def load(self):
session_data = self._cache.get(self.session_key)
return session_data or {}
def save(self):
- self._cache.set(self.session_key, self._session, settings.SESSION_COOKIE_AGE)
+ self._cache.set(self.session_key, self._session, self.get_expiry_age())
def exists(self, session_key):
if self._cache.get(session_key):
return True
return False
-
+
def delete(self, session_key):
self._cache.delete(session_key)
\ No newline at end of file
diff --git a/django/contrib/sessions/backends/db.py b/django/contrib/sessions/backends/db.py
index 0f79d9ee1a..b1c1097865 100644
--- a/django/contrib/sessions/backends/db.py
+++ b/django/contrib/sessions/backends/db.py
@@ -41,7 +41,7 @@ class SessionStore(SessionBase):
Session.objects.create(
session_key = self.session_key,
session_data = self.encode(self._session),
- expire_date = datetime.datetime.now() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE)
+ expire_date = self.get_expiry_date()
)
def delete(self, session_key):
diff --git a/django/contrib/sessions/middleware.py b/django/contrib/sessions/middleware.py
index 2af2312e76..a7b376dde0 100644
--- a/django/contrib/sessions/middleware.py
+++ b/django/contrib/sessions/middleware.py
@@ -26,14 +26,14 @@ class SessionMiddleware(object):
if accessed:
patch_vary_headers(response, ('Cookie',))
if modified or settings.SESSION_SAVE_EVERY_REQUEST:
- if settings.SESSION_EXPIRE_AT_BROWSER_CLOSE:
+ if request.session.get_expire_at_browser_close():
max_age = None
expires = None
else:
- max_age = settings.SESSION_COOKIE_AGE
- expires_time = time.time() + settings.SESSION_COOKIE_AGE
+ max_age = request.session.get_expiry_age()
+ expires_time = time.time() + max_age
expires = cookie_date(expires_time)
- # Save the seesion data and refresh the client cookie.
+ # Save the session data and refresh the client cookie.
request.session.save()
response.set_cookie(settings.SESSION_COOKIE_NAME,
request.session.session_key, max_age=max_age,
diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py
index b2c664ce7b..0f162b211f 100644
--- a/django/contrib/sessions/tests.py
+++ b/django/contrib/sessions/tests.py
@@ -88,6 +88,100 @@ False
>>> s.pop('some key', 'does not exist')
'does not exist'
+
+#########################
+# Custom session expiry #
+#########################
+
+>>> from django.conf import settings
+>>> from datetime import datetime, timedelta
+
+>>> td10 = timedelta(seconds=10)
+
+# A normal session has a max age equal to settings
+>>> s.get_expiry_age() == settings.SESSION_COOKIE_AGE
+True
+
+# So does a custom session with an idle expiration time of 0 (but it'll expire
+# at browser close)
+>>> s.set_expiry(0)
+>>> s.get_expiry_age() == settings.SESSION_COOKIE_AGE
+True
+
+# Custom session idle expiration time
+>>> s.set_expiry(10)
+>>> delta = s.get_expiry_date() - datetime.now()
+>>> delta.seconds in (9, 10)
+True
+>>> age = s.get_expiry_age()
+>>> age in (9, 10)
+True
+
+# Custom session fixed expiry date (timedelta)
+>>> s.set_expiry(td10)
+>>> delta = s.get_expiry_date() - datetime.now()
+>>> delta.seconds in (9, 10)
+True
+>>> age = s.get_expiry_age()
+>>> age in (9, 10)
+True
+
+# Custom session fixed expiry date (fixed datetime)
+>>> s.set_expiry(datetime.now() + td10)
+>>> delta = s.get_expiry_date() - datetime.now()
+>>> delta.seconds in (9, 10)
+True
+>>> age = s.get_expiry_age()
+>>> age in (9, 10)
+True
+
+# Set back to default session age
+>>> s.set_expiry(None)
+>>> s.get_expiry_age() == settings.SESSION_COOKIE_AGE
+True
+
+# Allow to set back to default session age even if no alternate has been set
+>>> s.set_expiry(None)
+
+
+# We're changing the setting then reverting back to the original setting at the
+# end of these tests.
+>>> original_expire_at_browser_close = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE
+>>> settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = False
+
+# Custom session age
+>>> s.set_expiry(10)
+>>> s.get_expire_at_browser_close()
+False
+
+# Custom expire-at-browser-close
+>>> s.set_expiry(0)
+>>> s.get_expire_at_browser_close()
+True
+
+# Default session age
+>>> s.set_expiry(None)
+>>> s.get_expire_at_browser_close()
+False
+
+>>> settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = True
+
+# Custom session age
+>>> s.set_expiry(10)
+>>> s.get_expire_at_browser_close()
+False
+
+# Custom expire-at-browser-close
+>>> s.set_expiry(0)
+>>> s.get_expire_at_browser_close()
+True
+
+# Default session age
+>>> s.set_expiry(None)
+>>> s.get_expire_at_browser_close()
+True
+
+>>> settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = original_expire_at_browser_close
"""
if __name__ == '__main__':
diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py
index d06b131d6f..193bb26ccf 100644
--- a/django/core/management/commands/loaddata.py
+++ b/django/core/management/commands/loaddata.py
@@ -32,6 +32,7 @@ class Command(BaseCommand):
# Keep a count of the installed objects and fixtures
fixture_count = 0
object_count = 0
+ objects_per_fixture = []
models = set()
humanize = lambda dirname: dirname and "'%s'" % dirname or 'absolute path'
@@ -60,11 +61,16 @@ class Command(BaseCommand):
else:
formats = []
- if verbosity >= 2:
- if formats:
+ if formats:
+ if verbosity > 1:
print "Loading '%s' fixtures..." % fixture_name
- else:
- print "Skipping fixture '%s': %s is not a known serialization format" % (fixture_name, format)
+ else:
+ sys.stderr.write(
+ self.style.ERROR("Problem installing fixture '%s': %s is not a known serialization format." %
+ (fixture_name, format)))
+ transaction.rollback()
+ transaction.leave_transaction_management()
+ return
if os.path.isabs(fixture_name):
fixture_dirs = [fixture_name]
@@ -93,6 +99,7 @@ class Command(BaseCommand):
return
else:
fixture_count += 1
+ objects_per_fixture.append(0)
if verbosity > 0:
print "Installing %s fixture '%s' from %s." % \
(format, fixture_name, humanize(fixture_dir))
@@ -100,6 +107,7 @@ class Command(BaseCommand):
objects = serializers.deserialize(format, fixture)
for obj in objects:
object_count += 1
+ objects_per_fixture[-1] += 1
models.add(obj.object.__class__)
obj.save()
label_found = True
@@ -117,10 +125,23 @@ class Command(BaseCommand):
return
fixture.close()
except:
- if verbosity >= 2:
+ if verbosity > 1:
print "No %s fixture '%s' in %s." % \
(format, fixture_name, humanize(fixture_dir))
+
+ # If any of the fixtures we loaded contain 0 objects, assume that an
+ # error was encountered during fixture loading.
+ if 0 in objects_per_fixture:
+ sys.stderr.write(
+ self.style.ERROR("No fixture data found for '%s'. (File format may be invalid.)" %
+ (fixture_name)))
+ transaction.rollback()
+ transaction.leave_transaction_management()
+ return
+
+ # If we found even one object in a fixture, we need to reset the
+ # database sequences.
if object_count > 0:
sequence_sql = connection.ops.sequence_reset_sql(self.style, models)
if sequence_sql:
@@ -128,12 +149,12 @@ class Command(BaseCommand):
print "Resetting sequences"
for line in sequence_sql:
cursor.execute(line)
-
+
transaction.commit()
transaction.leave_transaction_management()
if object_count == 0:
- if verbosity >= 2:
+ if verbosity > 1:
print "No fixtures found."
else:
if verbosity > 0:
diff --git a/django/core/management/sql.py b/django/core/management/sql.py
index 574be5a1ee..c635fcab8a 100644
--- a/django/core/management/sql.py
+++ b/django/core/management/sql.py
@@ -446,7 +446,7 @@ def custom_sql_for_model(model):
fp = open(sql_file, 'U')
for statement in statements.split(fp.read().decode(settings.FILE_CHARSET)):
# Remove any comments from the file
- statement = re.sub(ur"--.*[\n\Z]", "", statement)
+ statement = re.sub(ur"--.*([\n\Z]|$)", "", statement)
if statement.strip():
output.append(statement + u";")
fp.close()
diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py
index a79497ecec..e22a35815b 100644
--- a/django/core/serializers/base.py
+++ b/django/core/serializers/base.py
@@ -38,7 +38,7 @@ class Serializer(object):
self.start_serialization()
for obj in queryset:
self.start_object(obj)
- for field in obj._meta.fields:
+ for field in obj._meta.local_fields:
if field.serialize:
if field.rel is None:
if self.selected_fields is None or field.attname in self.selected_fields:
diff --git a/django/db/models/base.py b/django/db/models/base.py
index 01c2f31794..a253f38f47 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -287,12 +287,17 @@ class Model(object):
meta = cls._meta
signal = False
- for parent, field in meta.parents.items():
- self.save_base(raw, parent)
- setattr(self, field.attname, self._get_pk_val(parent._meta))
+ # If we are in a raw save, save the object exactly as presented.
+ # That means that we don't try to be smart about saving attributes
+ # that might have come from the parent class - we just save the
+ # attributes we have been given to the class we have been given.
+ if not raw:
+ for parent, field in meta.parents.items():
+ self.save_base(raw, parent)
+ setattr(self, field.attname, self._get_pk_val(parent._meta))
non_pks = [f for f in meta.local_fields if not f.primary_key]
-
+
# First, try an UPDATE. If that doesn't update anything, do an INSERT.
pk_val = self._get_pk_val(meta)
# Note: the comparison with '' is required for compatibility with
diff --git a/django/db/models/options.py b/django/db/models/options.py
index 3948a5fd10..c78ab1cd48 100644
--- a/django/db/models/options.py
+++ b/django/db/models/options.py
@@ -55,8 +55,12 @@ class Options(object):
# Next, apply any overridden values from 'class Meta'.
if self.meta:
meta_attrs = self.meta.__dict__.copy()
- del meta_attrs['__module__']
- del meta_attrs['__doc__']
+ for name in self.meta.__dict__:
+ # Ignore any private attributes that Django doesn't care about.
+ # NOTE: We can't modify a dictionary's contents while looping
+ # over it, so we loop over the *original* dictionary instead.
+ if name.startswith('_'):
+ del meta_attrs[name]
for attr_name in DEFAULT_NAMES:
if attr_name in meta_attrs:
setattr(self, attr_name, meta_attrs.pop(attr_name))
@@ -97,7 +101,7 @@ class Options(object):
# field.
field = self.parents.value_for_index(0)
field.primary_key = True
- self.pk = field
+ self.setup_pk(field)
else:
auto = AutoField(verbose_name='ID', primary_key=True,
auto_created=True)
diff --git a/django/db/models/query.py b/django/db/models/query.py
index 6b341ba9ab..12731caa94 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -292,6 +292,8 @@ class QuerySet(object):
Updates all elements in the current QuerySet, setting all the given
fields to the appropriate values.
"""
+ assert self.query.can_filter(), \
+ "Cannot update a query once a slice has been taken."
query = self.query.clone(sql.UpdateQuery)
query.add_update_values(kwargs)
query.execute_sql(None)
@@ -306,6 +308,8 @@ class QuerySet(object):
code (it requires too much poking around at model internals to be
useful at that level).
"""
+ assert self.query.can_filter(), \
+ "Cannot update a query once a slice has been taken."
query = self.query.clone(sql.UpdateQuery)
query.add_update_fields(values)
query.execute_sql(None)
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
index a6957bab7b..3044882a86 100644
--- a/django/db/models/sql/query.py
+++ b/django/db/models/sql/query.py
@@ -851,7 +851,7 @@ class Query(object):
return alias
def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1,
- used=None, requested=None, restricted=None):
+ used=None, requested=None, restricted=None, nullable=None):
"""
Fill in the information needed for a select_related query. The current
depth is measured as the number of connections away from the root model
@@ -883,6 +883,10 @@ class Query(object):
(not restricted and f.null) or f.rel.parent_link):
continue
table = f.rel.to._meta.db_table
+ if nullable or f.null:
+ promote = True
+ else:
+ promote = False
if model:
int_opts = opts
alias = root_alias
@@ -891,12 +895,12 @@ class Query(object):
int_opts = int_model._meta
alias = self.join((alias, int_opts.db_table, lhs_col,
int_opts.pk.column), exclusions=used,
- promote=f.null)
+ promote=promote)
else:
alias = root_alias
alias = self.join((alias, table, f.column,
f.rel.get_related_field().column), exclusions=used,
- promote=f.null)
+ promote=promote)
used.add(alias)
self.related_select_cols.extend([(alias, f2.column)
for f2 in f.rel.to._meta.fields])
@@ -905,8 +909,12 @@ class Query(object):
next = requested.get(f.name, {})
else:
next = False
+ if f.null is not None:
+ new_nullable = f.null
+ else:
+ new_nullable = None
self.fill_related_selections(f.rel.to._meta, alias, cur_depth + 1,
- used, next, restricted)
+ used, next, restricted, new_nullable)
def add_filter(self, filter_expr, connector=AND, negate=False, trim=False,
can_reuse=None):
diff --git a/docs/authentication.txt b/docs/authentication.txt
index 28e73faba7..00cb8e45ca 100644
--- a/docs/authentication.txt
+++ b/docs/authentication.txt
@@ -263,14 +263,25 @@ Creating superusers
-------------------
``manage.py syncdb`` prompts you to create a superuser the first time you run
-it after adding ``'django.contrib.auth'`` to your ``INSTALLED_APPS``. But if
-you need to create a superuser after that via the command line, you can use the
-``create_superuser.py`` utility. Just run this command::
+it after adding ``'django.contrib.auth'`` to your ``INSTALLED_APPS``. If you need
+to create a superuser at a later date, you can use a command line utility.
+
+**New in Django development version.**::
+
+ manage.py createsuperuser --username=joe --email=joe@example.com
+
+You will be prompted for a password. Once entered, the user is created. If you
+leave off the ``--username`` or the ``--email`` option, It will prompt you for
+those values as well.
+
+If you're using an older release of Django, the old way of creating a superuser
+on the command line still works::
python /path/to/django/contrib/auth/create_superuser.py
-Make sure to substitute ``/path/to/`` with the path to the Django codebase on
-your filesystem.
+Where ``/path/to`` is the path to the Django codebase on your filesystem. The
+``manage.py`` command is prefered since it'll figure out the correct path and
+environment for you.
Storing additional information about users
------------------------------------------
diff --git a/docs/django-admin.txt b/docs/django-admin.txt
index e79c105bbd..5aed3b0816 100644
--- a/docs/django-admin.txt
+++ b/docs/django-admin.txt
@@ -109,6 +109,31 @@ the program name (``psql``, ``mysql``, ``sqlite3``) will find the program in
the right place. There's no way to specify the location of the program
manually.
+createsuperuser
+---------------
+
+**New in Django development version**
+
+Creates a superuser account (a user who has all permissions). This is
+useful if you need to create an initial superuser account but did not
+do so during ``syncdb``, or if you need to programmatically generate
+superuser accounts for your site(s).
+
+When run interactively, this command will prompt for a password for
+the new superuser account; when run non-interactively, no password
+will be set and the superuser account will not be able to log in until
+a password has been manually set for it.
+
+The username and e-mail address for the new account can be supplied by
+using the ``--username`` and ``--email`` arguments on the command
+line; if not supplied, ``createsuperuser`` will prompt for them when
+running interactively.
+
+This command is only available if Django's `authentication system`_
+(``django.contrib.auth``) is installed.
+
+.. _authentication system: ../authentication/
+
diffsettings
------------
diff --git a/docs/faq.txt b/docs/faq.txt
index 56c9536eda..5e36f1d933 100644
--- a/docs/faq.txt
+++ b/docs/faq.txt
@@ -228,13 +228,14 @@ Short answer: When we're comfortable with Django's APIs, have added all
features that we feel are necessary to earn a "1.0" status, and are ready to
begin maintaining backwards compatibility.
-The merging of Django's `magic-removal branch`_ went a long way toward Django
-1.0.
+The merging of Django's `Queryset Refactor branch`_ went a long way toward Django
+1.0. Merging the `Newforms Admin branch` will be another important step.
Of course, you should note that `quite a few production sites`_ use Django in
its current status. Don't let the lack of a 1.0 turn you off.
-.. _magic-removal branch: http://code.djangoproject.com/wiki/RemovingTheMagic
+.. _Queryset Refactor branch: http://code.djangoproject.com/wiki/QuerysetRefactorBranch
+.. _Newforms Admin branch: http://code.djangoproject.com/wiki/NewformsAdminBranch
.. _quite a few production sites: http://code.djangoproject.com/wiki/DjangoPoweredSites
How can I download the Django documentation to read it offline?
@@ -259,7 +260,9 @@ Where can I find Django developers for hire?
Consult our `developers for hire page`_ for a list of Django developers who
would be happy to help you.
-You might also be interested in posting a job to http://www.gypsyjobs.com/ .
+You might also be interested in posting a job to http://djangogigs.com/ .
+If you want to find Django-capable people in your local area, try
+http://djangopeople.net/ .
.. _developers for hire page: http://code.djangoproject.com/wiki/DevelopersForHire
@@ -643,6 +646,81 @@ You can also use the Python API. See `creating users`_ for full info.
.. _creating users: ../authentication/#creating-users
+Getting Help
+============
+
+How do I do X? Why doesn't Y work? Where can I go to get help?
+--------------------------------------------------------------
+
+If this FAQ doesn't contain an answer to your question, you might want to
+try the `django-users mailing list`_. Feel free to ask any question related
+to installing, using, or debugging Django.
+
+If you prefer IRC, the `#django IRC channel`_ on freenode is an active
+community of helpful individuals who may be able to solve your problem.
+
+.. _`django-users mailing list`: http://groups.google.com/group/django-users
+.. _`#django IRC channel`: irc://irc.freenode.net/django
+
+Why hasn't my message appeared on django-users?
+-----------------------------------------------
+
+django-users_ has a lot of subscribers. This is good for the community, as
+there are lot of people that can contribute answers to questions.
+Unfortunately, it also means that django-users_ is an attractive target for
+spammers.
+
+In order to combat the spam problem, when you join the django-users_ mailing
+list, we manually moderate the first message you send to the list. This means
+that spammers get caught, but it also means that your first question to the
+list might take a little longer to get answered. We apologize for any
+inconvenience that this policy may cause.
+
+.. _django-users: http://groups.google.com/group/django-users
+
+Nobody on django-users answered my question? What should I do?
+--------------------------------------------------------------
+
+Wait. Ask again later. Try making your question more specific, or provide
+a better example of your problem.
+
+Remember, the readers of django-users_ are all volunteers. If nobody has
+answered your question, it may be because nobody knows the answer, it may
+be because nobody can understand the question, or it may be that everybody
+that can help is extremely busy.
+
+Resist any temptation to mail the `django-developers mailing list`_ in an
+attempt to get an answer to your question. django-developers_ is for discussing
+the development of Django itself. Attempts to use django-developers_ as
+a second-tier support mechanism will not be met an enthusiastic response.
+
+.. _`django-developers mailing list`: http://groups.google.com/group/django-developers
+.. _django-developers: http://groups.google.com/group/django-developers
+
+I think I've found a bug! What should I do?
+-------------------------------------------
+
+Detailed instructions on how to handle a potential bug can be found in our
+`Guide to contributing to Django`_.
+
+.. _`Guide to contributing to Django`: ../contributing/#reporting-bugs
+
+I think I've found a security problem! What should I do?
+--------------------------------------------------------
+
+If you think you have found a security problem with Django, please send
+a message to security@djangoproject.com. This is a private list only
+open to long-time, highly trusted Django developers, and its archives
+are not publicly readable.
+
+Due to the sensitive nature of security issues, we ask that if you think you
+have found a security problem, *please* don't send a message to one of the
+public mailing lists. Django has a `policy for handling security issues`_;
+while a defect is outstanding, we would like to minimize any damage that
+could be inflicted through public knowledge of that defect.
+
+.. _`policy for handling security issues`: ../contributing/#reporting-security-issues
+
Contributing code
=================
@@ -652,7 +730,7 @@ How can I get started contributing code to Django?
Thanks for asking! We've written an entire document devoted to this question.
It's titled `Contributing to Django`_.
-.. _Contributing to Django: ../contributing/
+.. _`Contributing to Django`: ../contributing/
I submitted a bug fix in the ticket system several weeks ago. Why are you ignoring my patch?
--------------------------------------------------------------------------------------------
@@ -664,6 +742,11 @@ ignored" and "a ticket has not been attended to yet." Django's ticket system
contains hundreds of open tickets, of various degrees of impact on end-user
functionality, and Django's developers have to review and prioritize.
+On top of that - the team working on Django are all volunteers. As a result,
+the amount of time that we have to work on Django is limited, and will vary
+from week to week depending on how much spare time we have. If we are busy, we
+may not be able to spend as much time on Django as we might want.
+
Besides, if your feature request stands no chance of inclusion in Django, we
won't ignore it -- we'll just close the ticket. So if your ticket is still
open, it doesn't mean we're ignoring you; it just means we haven't had time to
diff --git a/docs/serialization.txt b/docs/serialization.txt
index 8a672d8b8a..2a3e7038da 100644
--- a/docs/serialization.txt
+++ b/docs/serialization.txt
@@ -63,6 +63,41 @@ be serialized.
doesn't specify all the fields that are required by a model, the deserializer
will not be able to save deserialized instances.
+Inherited Models
+~~~~~~~~~~~~~~~~
+
+If you have a model that is defined using an `abstract base class`_, you don't
+have to do anything special to serialize that model. Just call the serializer
+on the object (or objects) that you want to serialize, and the output will be
+a complete representation of the serialized object.
+
+However, if you have a model that uses `multi-table inheritance`_, you also
+need to serialize all of the base classes for the model. This is because only
+the fields that are locally defined on the model will be serialized. For
+example, consider the following models::
+
+ class Place(models.Model):
+ name = models.CharField(max_length=50)
+
+ class Restaurant(Place):
+ serves_hot_dogs = models.BooleanField()
+
+If you only serialize the Restaurant model::
+
+ data = serializers.serialize('xml', Restaurant.objects.all())
+
+the fields on the serialized output will only contain the `serves_hot_dogs`
+attribute. The `name` attribute of the base class will be ignored.
+
+In order to fully serialize your Restaurant instances, you will need to
+serialize the Place models as well::
+
+ all_objects = list(Restaurant.objects.all()) + list(Place.objects.all())
+ data = serializers.serialize('xml', all_objects)
+
+.. _abstract base class: http://www.djangoproject.com/documentation/model-api/#abstract-base-classes
+.. _multi-table inheritance: http://www.djangoproject.com/documentation/model-api/#multi-table-inheritance
+
Deserializing data
------------------
diff --git a/docs/sessions.txt b/docs/sessions.txt
index d8bac5b8d4..86ed49f135 100644
--- a/docs/sessions.txt
+++ b/docs/sessions.txt
@@ -80,19 +80,24 @@ attribute, which is a dictionary-like object. You can read it and write to it.
It implements the following standard dictionary methods:
* ``__getitem__(key)``
+
Example: ``fav_color = request.session['fav_color']``
* ``__setitem__(key, value)``
+
Example: ``request.session['fav_color'] = 'blue'``
* ``__delitem__(key)``
+
Example: ``del request.session['fav_color']``. This raises ``KeyError``
if the given ``key`` isn't already in the session.
* ``__contains__(key)``
+
Example: ``'fav_color' in request.session``
* ``get(key, default=None)``
+
Example: ``fav_color = request.session.get('fav_color', 'red')``
* ``keys()``
@@ -101,23 +106,70 @@ It implements the following standard dictionary methods:
* ``setdefault()`` (**New in Django development version**)
-It also has these three methods:
+It also has these methods:
* ``set_test_cookie()``
+
Sets a test cookie to determine whether the user's browser supports
cookies. Due to the way cookies work, you won't be able to test this
until the user's next page request. See "Setting test cookies" below for
more information.
* ``test_cookie_worked()``
+
Returns either ``True`` or ``False``, depending on whether the user's
browser accepted the test cookie. Due to the way cookies work, you'll
have to call ``set_test_cookie()`` on a previous, separate page request.
See "Setting test cookies" below for more information.
* ``delete_test_cookie()``
+
Deletes the test cookie. Use this to clean up after yourself.
+ * ``set_expiry(value)``
+
+ **New in Django development version**
+
+ Sets the expiration time for the session. You can pass a number of
+ different values:
+
+ * If ``value`` is an integer, the session will expire after that
+ many seconds of inactivity. For example, calling
+ ``request.session.set_expiry(300)`` would make the session expire
+ in 5 minutes.
+
+ * If ``value`` is a ``datetime`` or ``timedelta`` object, the
+ session will expire at that specific time.
+
+ * If ``value`` is ``0`` then the user's session cookie will expire
+ when their browser is closed.
+
+ * If ``value`` is ``None``, the session reverts to using the global
+ session expiry policy.
+
+ * ``get_expiry_age()``
+
+ **New in Django development version**
+
+ Returns the number of seconds until this session expires. For sessions
+ with no custom expiration (or those set to expire at browser close), this
+ will equal ``settings.SESSION_COOKIE_AGE``.
+
+ * ``get_expiry_date()``
+
+ **New in Django development version**
+
+ Returns the date this session will expire. For sessions with no custom
+ expiration (or those set to expire at browser close), this will equal the
+ date ``settings.SESSION_COOKIE_AGE`` seconds from now.
+
+ * ``get_expire_at_browser_close()``
+
+ **New in Django development version**
+
+ Returns either ``True`` or ``False``, depending on whether the user's
+ session cookie will expire when their browser is closed.
+
You can edit ``request.session`` at any point in your view. You can edit it
multiple times.
@@ -278,6 +330,12 @@ browser-length cookies -- cookies that expire as soon as the user closes his or
her browser. Use this if you want people to have to log in every time they open
a browser.
+**New in Django development version**
+
+This setting is a global default and can be overwritten at a per-session level
+by explicitly calling ``request.session.set_expiry()`` as described above in
+`using sessions in views`_.
+
Clearing the session table
==========================
diff --git a/tests/modeltests/model_inheritance/models.py b/tests/modeltests/model_inheritance/models.py
index b1a751f5e8..7c737b6bd1 100644
--- a/tests/modeltests/model_inheritance/models.py
+++ b/tests/modeltests/model_inheritance/models.py
@@ -147,8 +147,13 @@ Test constructor for Restaurant.
>>> c.save()
>>> ir = ItalianRestaurant(name='Ristorante Miron', address='1234 W. Ash', serves_hot_dogs=False, serves_pizza=False, serves_gnocchi=True, rating=4, chef=c)
>>> ir.save()
+>>> ItalianRestaurant.objects.filter(address='1234 W. Ash')
+[]
+
>>> ir.address = '1234 W. Elm'
>>> ir.save()
+>>> ItalianRestaurant.objects.filter(address='1234 W. Elm')
+[]
# Make sure Restaurant and ItalianRestaurant have the right fields in the right
# order.
diff --git a/tests/modeltests/update/models.py b/tests/modeltests/update/models.py
index 3b0f83389f..8a35b61a7c 100644
--- a/tests/modeltests/update/models.py
+++ b/tests/modeltests/update/models.py
@@ -63,5 +63,12 @@ a manager method.
>>> DataPoint.objects.values('value').distinct()
[{'value': u'thing'}]
+We do not support update on already sliced query sets.
+
+>>> DataPoint.objects.all()[:2].update(another_value='another thing')
+Traceback (most recent call last):
+ ...
+AssertionError: Cannot update a query once a slice has been taken.
+
"""
}
diff --git a/tests/regressiontests/fixtures_regress/fixtures/bad_fixture1.unkn b/tests/regressiontests/fixtures_regress/fixtures/bad_fixture1.unkn
new file mode 100644
index 0000000000..a8b0a0c56c
--- /dev/null
+++ b/tests/regressiontests/fixtures_regress/fixtures/bad_fixture1.unkn
@@ -0,0 +1 @@
+This data shouldn't load, as it's of an unknown file format.
\ No newline at end of file
diff --git a/tests/regressiontests/fixtures_regress/fixtures/bad_fixture2.xml b/tests/regressiontests/fixtures_regress/fixtures/bad_fixture2.xml
new file mode 100644
index 0000000000..87b809fbc6
--- /dev/null
+++ b/tests/regressiontests/fixtures_regress/fixtures/bad_fixture2.xml
@@ -0,0 +1,7 @@
+
+
+
+ Poker on TV is great!
+ 2006-06-16 11:00:00
+
+
\ No newline at end of file
diff --git a/tests/regressiontests/fixtures_regress/models.py b/tests/regressiontests/fixtures_regress/models.py
index 144debe05a..59fc167d50 100644
--- a/tests/regressiontests/fixtures_regress/models.py
+++ b/tests/regressiontests/fixtures_regress/models.py
@@ -71,4 +71,27 @@ __test__ = {'API_TESTS':"""
>>> Absolute.load_count
1
+###############################################
+# Test for ticket #4371 -- fixture loading fails silently in testcases
+# Validate that error conditions are caught correctly
+
+# redirect stderr for the next few tests...
+>>> import sys
+>>> savestderr = sys.stderr
+>>> sys.stderr = sys.stdout
+
+# Loading data of an unknown format should fail
+>>> management.call_command('loaddata', 'bad_fixture1.unkn', verbosity=0)
+Problem installing fixture 'bad_fixture1': unkn is not a known serialization format.
+
+# Loading a fixture file with invalid data using explicit filename
+>>> management.call_command('loaddata', 'bad_fixture2.xml', verbosity=0)
+No fixture data found for 'bad_fixture2'. (File format may be invalid.)
+
+# Loading a fixture file with invalid data without file extension
+>>> management.call_command('loaddata', 'bad_fixture2', verbosity=0)
+No fixture data found for 'bad_fixture2'. (File format may be invalid.)
+
+>>> sys.stderr = savestderr
+
"""}
diff --git a/tests/regressiontests/model_inheritance_regress/__init__.py b/tests/regressiontests/model_inheritance_regress/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/regressiontests/model_inheritance_regress/models.py b/tests/regressiontests/model_inheritance_regress/models.py
new file mode 100644
index 0000000000..8801715a0c
--- /dev/null
+++ b/tests/regressiontests/model_inheritance_regress/models.py
@@ -0,0 +1,120 @@
+"""
+Regression tests for Model inheritance behaviour.
+"""
+
+from django.db import models
+
+class Place(models.Model):
+ name = models.CharField(max_length=50)
+ address = models.CharField(max_length=80)
+
+ class Meta:
+ ordering = ('name',)
+
+ def __unicode__(self):
+ return u"%s the place" % self.name
+
+class Restaurant(Place):
+ serves_hot_dogs = models.BooleanField()
+ serves_pizza = models.BooleanField()
+
+ def __unicode__(self):
+ return u"%s the restaurant" % self.name
+
+class ItalianRestaurant(Restaurant):
+ serves_gnocchi = models.BooleanField()
+
+ def __unicode__(self):
+ return u"%s the italian restaurant" % self.name
+
+class ParkingLot(Place):
+ # An explicit link to the parent (we can control the attribute name).
+ parent = models.OneToOneField(Place, primary_key=True, parent_link=True)
+ capacity = models.IntegerField()
+
+ def __unicode__(self):
+ return u"%s the parking lot" % self.name
+
+__test__ = {'API_TESTS':"""
+# Regression for #7350, #7202
+# Check that when you create a Parent object with a specific reference to an existent
+# child instance, saving the Parent doesn't duplicate the child.
+# This behaviour is only activated during a raw save - it is mostly relevant to
+# deserialization, but any sort of CORBA style 'narrow()' API would require a
+# similar approach.
+
+# Create a child-parent-grandparent chain
+>>> place1 = Place(name="Guido's House of Pasta", address='944 W. Fullerton')
+>>> place1.save_base(raw=True)
+>>> restaurant = Restaurant(place_ptr=place1, serves_hot_dogs=True, serves_pizza=False)
+>>> restaurant.save_base(raw=True)
+>>> italian_restaurant = ItalianRestaurant(restaurant_ptr=restaurant, serves_gnocchi=True)
+>>> italian_restaurant.save_base(raw=True)
+
+# Create a child-parent chain with an explicit parent link
+>>> place2 = Place(name='Main St', address='111 Main St')
+>>> place2.save_base(raw=True)
+>>> park = ParkingLot(parent=place2, capacity=100)
+>>> park.save_base(raw=True)
+
+# Check that no extra parent objects have been created.
+>>> Place.objects.all()
+[, ]
+
+>>> dicts = Restaurant.objects.values('name','serves_hot_dogs')
+>>> [sorted(d.items()) for d in dicts]
+[[('name', u"Guido's House of Pasta"), ('serves_hot_dogs', True)]]
+
+>>> dicts = ItalianRestaurant.objects.values('name','serves_hot_dogs','serves_gnocchi')
+>>> [sorted(d.items()) for d in dicts]
+[[('name', u"Guido's House of Pasta"), ('serves_gnocchi', True), ('serves_hot_dogs', True)]]
+
+>>> dicts = ParkingLot.objects.values('name','capacity')
+>>> [sorted(d.items()) for d in dicts]
+[[('capacity', 100), ('name', u'Main St')]]
+
+# You can also update objects when using a raw save.
+>>> place1.name = "Guido's All New House of Pasta"
+>>> place1.save_base(raw=True)
+
+>>> restaurant.serves_hot_dogs = False
+>>> restaurant.save_base(raw=True)
+
+>>> italian_restaurant.serves_gnocchi = False
+>>> italian_restaurant.save_base(raw=True)
+
+>>> place2.name='Derelict lot'
+>>> place2.save_base(raw=True)
+
+>>> park.capacity = 50
+>>> park.save_base(raw=True)
+
+# No extra parent objects after an update, either.
+>>> Place.objects.all()
+[, ]
+
+>>> dicts = Restaurant.objects.values('name','serves_hot_dogs')
+>>> [sorted(d.items()) for d in dicts]
+[[('name', u"Guido's All New House of Pasta"), ('serves_hot_dogs', False)]]
+
+>>> dicts = ItalianRestaurant.objects.values('name','serves_hot_dogs','serves_gnocchi')
+>>> [sorted(d.items()) for d in dicts]
+[[('name', u"Guido's All New House of Pasta"), ('serves_gnocchi', False), ('serves_hot_dogs', False)]]
+
+>>> dicts = ParkingLot.objects.values('name','capacity')
+>>> [sorted(d.items()) for d in dicts]
+[[('capacity', 50), ('name', u'Derelict lot')]]
+
+# If you try to raw_save a parent attribute onto a child object,
+# the attribute will be ignored.
+
+>>> italian_restaurant.name = "Lorenzo's Pasta Hut"
+>>> italian_restaurant.save_base(raw=True)
+
+# Note that the name has not changed
+# - name is an attribute of Place, not ItalianRestaurant
+>>> dicts = ItalianRestaurant.objects.values('name','serves_hot_dogs','serves_gnocchi')
+>>> [sorted(d.items()) for d in dicts]
+[[('name', u"Guido's All New House of Pasta"), ('serves_gnocchi', False), ('serves_hot_dogs', False)]]
+
+"""}
diff --git a/tests/regressiontests/null_fk/__init__.py b/tests/regressiontests/null_fk/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/regressiontests/null_fk/models.py b/tests/regressiontests/null_fk/models.py
new file mode 100644
index 0000000000..1bc266c033
--- /dev/null
+++ b/tests/regressiontests/null_fk/models.py
@@ -0,0 +1,55 @@
+"""
+Regression tests for proper working of ForeignKey(null=True). Tests these bugs:
+
+ * #7369: FK non-null after null relationship on select_related() generates an invalid query
+
+"""
+
+from django.db import models
+
+class SystemInfo(models.Model):
+ system_name = models.CharField(max_length=32)
+
+class Forum(models.Model):
+ system_info = models.ForeignKey(SystemInfo)
+ forum_name = models.CharField(max_length=32)
+
+class Post(models.Model):
+ forum = models.ForeignKey(Forum, null=True)
+ title = models.CharField(max_length=32)
+
+ def __unicode__(self):
+ return self.title
+
+class Comment(models.Model):
+ post = models.ForeignKey(Post, null=True)
+ comment_text = models.CharField(max_length=250)
+
+ def __unicode__(self):
+ return self.comment_text
+
+__test__ = {'API_TESTS':"""
+
+>>> s = SystemInfo.objects.create(system_name='First forum')
+>>> f = Forum.objects.create(system_info=s, forum_name='First forum')
+>>> p = Post.objects.create(forum=f, title='First Post')
+>>> c1 = Comment.objects.create(post=p, comment_text='My first comment')
+>>> c2 = Comment.objects.create(comment_text='My second comment')
+
+# Starting from comment, make sure that a .select_related(...) with a specified
+# set of fields will properly LEFT JOIN multiple levels of NULLs (and the things
+# that come after the NULLs, or else data that should exist won't).
+>>> c = Comment.objects.select_related().get(id=1)
+>>> c.post
+
+>>> c = Comment.objects.select_related().get(id=2)
+>>> print c.post
+None
+
+>>> comments = Comment.objects.select_related('post__forum__system_info').all()
+>>> [(c.id, c.post.id) for c in comments]
+[(1, 1), (2, None)]
+>>> [(c.comment_text, c.post.title) for c in comments]
+[(u'My first comment', u'First Post'), (u'My second comment', None)]
+
+"""}
diff --git a/tests/regressiontests/serializers_regress/models.py b/tests/regressiontests/serializers_regress/models.py
index 593e61ecc7..7d3f9d3b1d 100644
--- a/tests/regressiontests/serializers_regress/models.py
+++ b/tests/regressiontests/serializers_regress/models.py
@@ -223,3 +223,23 @@ class ModifyingSaveData(models.Model):
"A save method that modifies the data in the object"
self.data = 666
super(ModifyingSaveData, self).save(raw)
+
+# Tests for serialization of models using inheritance.
+# Regression for #7202, #7350
+class AbstractBaseModel(models.Model):
+ parent_data = models.IntegerField()
+ class Meta:
+ abstract = True
+
+class InheritAbstractModel(AbstractBaseModel):
+ child_data = models.IntegerField()
+
+class BaseModel(models.Model):
+ parent_data = models.IntegerField()
+
+class InheritBaseModel(BaseModel):
+ child_data = models.IntegerField()
+
+class ExplicitInheritBaseModel(BaseModel):
+ parent = models.OneToOneField(BaseModel)
+ child_data = models.IntegerField()
diff --git a/tests/regressiontests/serializers_regress/tests.py b/tests/regressiontests/serializers_regress/tests.py
index db34f8cf77..9bc5eec1eb 100644
--- a/tests/regressiontests/serializers_regress/tests.py
+++ b/tests/regressiontests/serializers_regress/tests.py
@@ -32,7 +32,7 @@ def data_create(pk, klass, data):
instance = klass(id=pk)
instance.data = data
models.Model.save_base(instance, raw=True)
- return instance
+ return [instance]
def generic_create(pk, klass, data):
instance = klass(id=pk)
@@ -40,32 +40,45 @@ def generic_create(pk, klass, data):
models.Model.save_base(instance, raw=True)
for tag in data[1:]:
instance.tags.create(data=tag)
- return instance
+ return [instance]
def fk_create(pk, klass, data):
instance = klass(id=pk)
setattr(instance, 'data_id', data)
models.Model.save_base(instance, raw=True)
- return instance
+ return [instance]
def m2m_create(pk, klass, data):
instance = klass(id=pk)
models.Model.save_base(instance, raw=True)
instance.data = data
- return instance
+ return [instance]
def o2o_create(pk, klass, data):
instance = klass()
instance.data_id = data
models.Model.save_base(instance, raw=True)
- return instance
+ return [instance]
def pk_create(pk, klass, data):
instance = klass()
instance.data = data
models.Model.save_base(instance, raw=True)
- return instance
+ return [instance]
+def inherited_create(pk, klass, data):
+ instance = klass(id=pk,**data)
+ # This isn't a raw save because:
+ # 1) we're testing inheritance, not field behaviour, so none
+ # of the field values need to be protected.
+ # 2) saving the child class and having the parent created
+ # automatically is easier than manually creating both.
+ models.Model.save(instance)
+ created = [instance]
+ for klass,field in instance._meta.parents.items():
+ created.append(klass.objects.get(id=pk))
+ return created
+
# A set of functions that can be used to compare
# test data objects of various kinds
def data_compare(testcase, pk, klass, data):
@@ -94,6 +107,11 @@ def pk_compare(testcase, pk, klass, data):
instance = klass.objects.get(data=data)
testcase.assertEqual(data, instance.data)
+def inherited_compare(testcase, pk, klass, data):
+ instance = klass.objects.get(id=pk)
+ for key,value in data.items():
+ testcase.assertEqual(value, getattr(instance,key))
+
# Define some data types. Each data type is
# actually a pair of functions; one to create
# and one to compare objects of that type
@@ -103,6 +121,7 @@ fk_obj = (fk_create, fk_compare)
m2m_obj = (m2m_create, m2m_compare)
o2o_obj = (o2o_create, o2o_compare)
pk_obj = (pk_create, pk_compare)
+inherited_obj = (inherited_create, inherited_compare)
test_data = [
# Format: (data type, PK value, Model Class, data)
@@ -255,6 +274,10 @@ The end."""),
(data_obj, 800, AutoNowDateTimeData, datetime.datetime(2006,6,16,10,42,37)),
(data_obj, 810, ModifyingSaveData, 42),
+
+ (inherited_obj, 900, InheritAbstractModel, {'child_data':37,'parent_data':42}),
+ (inherited_obj, 910, ExplicitInheritBaseModel, {'child_data':37,'parent_data':42}),
+ (inherited_obj, 920, InheritBaseModel, {'child_data':37,'parent_data':42}),
]
# Because Oracle treats the empty string as NULL, Oracle is expected to fail
@@ -277,13 +300,19 @@ def serializerTest(format, self):
# Create all the objects defined in the test data
objects = []
+ instance_count = {}
transaction.enter_transaction_management()
transaction.managed(True)
for (func, pk, klass, datum) in test_data:
- objects.append(func[0](pk, klass, datum))
+ objects.extend(func[0](pk, klass, datum))
+ instance_count[klass] = 0
transaction.commit()
transaction.leave_transaction_management()
+ # Get a count of the number of objects created for each class
+ for klass in instance_count:
+ instance_count[klass] = klass.objects.count()
+
# Add the generic tagged objects to the object list
objects.extend(Tag.objects.all())
@@ -304,6 +333,11 @@ def serializerTest(format, self):
for (func, pk, klass, datum) in test_data:
func[1](self, pk, klass, datum)
+ # Assert that the number of objects deserialized is the
+ # same as the number that was serialized.
+ for klass, count in instance_count.items():
+ self.assertEquals(count, klass.objects.count())
+
def fieldsTest(format, self):
# Clear the database first
management.call_command('flush', verbosity=0, interactive=False)