diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 4ea694d064..56a4ccb48a 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -296,3 +296,9 @@ BANNED_IPS = ()
##################
AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
+
+###########
+# TESTING #
+###########
+
+TEST_RUNNER='django.test.simple.run_tests'
diff --git a/django/contrib/admin/media/css/global.css b/django/contrib/admin/media/css/global.css
index e08aa29992..639f3321eb 100644
--- a/django/contrib/admin/media/css/global.css
+++ b/django/contrib/admin/media/css/global.css
@@ -1,4 +1,4 @@
-body { margin:0; padding:0; font-size:12px; font-family:"Lucida Grande","Bitstream Vera Sans",Verdana,Arial,sans-serif; color:#333; background:#fff; }
+body { margin:0; padding:0; font-size:12px; font-family:"Lucida Grande","DejaVu Sans","Bitstream Vera Sans",Verdana,Arial,sans-serif; color:#333; background:#fff; }
/* LINKS */
a:link, a:visited { color: #5b80b2; text-decoration:none; }
diff --git a/django/contrib/admin/templates/admin_doc/model_detail.html b/django/contrib/admin/templates/admin_doc/model_detail.html
index 44fc43e704..70133e26dd 100644
--- a/django/contrib/admin/templates/admin_doc/model_detail.html
+++ b/django/contrib/admin/templates/admin_doc/model_detail.html
@@ -35,7 +35,7 @@
{{ field.name }} |
{{ field.data_type }} |
- {% if field.verbose %}{{ field.verbose|escape }}{% endif %}{% if field.help_text %} - {{ field.help_text|escape }}{% endif %} |
+ {% if field.verbose %}{{ field.verbose }}{% endif %}{% if field.help_text %} - {{ field.help_text }}{% endif %} |
{% endfor %}
diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py
index add1d4abac..4af7bc1db8 100644
--- a/django/contrib/admin/templatetags/admin_modify.py
+++ b/django/contrib/admin/templatetags/admin_modify.py
@@ -195,7 +195,7 @@ def filter_interface_script_maybe(bound_field):
f = bound_field.field
if f.rel and isinstance(f.rel, models.ManyToManyRel) and f.rel.filter_interface:
return '\n' % (
+ ' SelectFilter.init("id_%s", %r, %s, "%s"); });\n' % (
f.name, f.verbose_name, f.rel.filter_interface-1, settings.ADMIN_MEDIA_PREFIX)
else:
return ''
diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
index 2a8b135bc7..9b091b28da 100644
--- a/django/contrib/admin/views/main.py
+++ b/django/contrib/admin/views/main.py
@@ -423,7 +423,7 @@ def _get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current
if current_depth > 16:
return # Avoid recursing too deep.
opts_seen = []
- for related in opts.related_objects():
+ for related in opts.get_all_related_objects():
if related.opts in opts_seen:
continue
opts_seen.append(related.opts)
diff --git a/django/contrib/auth/management.py b/django/contrib/auth/management.py
index 1a07417f1d..3f52681747 100644
--- a/django/contrib/auth/management.py
+++ b/django/contrib/auth/management.py
@@ -16,7 +16,7 @@ def _get_all_permissions(opts):
perms.append((_get_permission_codename(action, opts), 'Can %s %s' % (action, opts.verbose_name)))
return perms + list(opts.permissions)
-def create_permissions(app, created_models):
+def create_permissions(app, created_models, verbosity):
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import Permission
app_models = get_models(app)
@@ -27,13 +27,13 @@ def create_permissions(app, created_models):
for codename, name in _get_all_permissions(klass._meta):
p, created = Permission.objects.get_or_create(codename=codename, content_type__pk=ctype.id,
defaults={'name': name, 'content_type': ctype})
- if created:
+ if created and verbosity >= 2:
print "Adding permission '%s'" % p
-def create_superuser(app, created_models):
+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
- if User in created_models:
+ 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): "
confirm = raw_input(msg)
diff --git a/django/contrib/contenttypes/management.py b/django/contrib/contenttypes/management.py
index a9174584bc..de3a685477 100644
--- a/django/contrib/contenttypes/management.py
+++ b/django/contrib/contenttypes/management.py
@@ -5,7 +5,7 @@ Creates content types for all installed models.
from django.dispatch import dispatcher
from django.db.models import get_models, signals
-def create_contenttypes(app, created_models):
+def create_contenttypes(app, created_models, verbosity):
from django.contrib.contenttypes.models import ContentType
app_models = get_models(app)
if not app_models:
@@ -19,6 +19,7 @@ def create_contenttypes(app, created_models):
ct = ContentType(name=str(opts.verbose_name),
app_label=opts.app_label, model=opts.object_name.lower())
ct.save()
- print "Adding content type '%s | %s'" % (ct.app_label, ct.model)
+ if verbosity >= 2:
+ print "Adding content type '%s | %s'" % (ct.app_label, ct.model)
dispatcher.connect(create_contenttypes, signal=signals.post_syncdb)
diff --git a/django/contrib/flatpages/views.py b/django/contrib/flatpages/views.py
index 4ad24795f7..f386a52101 100644
--- a/django/contrib/flatpages/views.py
+++ b/django/contrib/flatpages/views.py
@@ -3,6 +3,7 @@ from django.template import loader, RequestContext
from django.shortcuts import get_object_or_404
from django.http import HttpResponse
from django.conf import settings
+from django.core.xheaders import populate_xheaders
DEFAULT_TEMPLATE = 'flatpages/default.html'
@@ -32,4 +33,6 @@ def flatpage(request, url):
c = RequestContext(request, {
'flatpage': f,
})
- return HttpResponse(t.render(c))
+ response = HttpResponse(t.render(c))
+ populate_xheaders(request, response, FlatPage, f.id)
+ return response
diff --git a/django/contrib/sites/management.py b/django/contrib/sites/management.py
index 0e1a50227a..6831cab96d 100644
--- a/django/contrib/sites/management.py
+++ b/django/contrib/sites/management.py
@@ -7,9 +7,10 @@ from django.db.models import signals
from django.contrib.sites.models import Site
from django.contrib.sites import models as site_app
-def create_default_site(app, created_models):
+def create_default_site(app, created_models, verbosity):
if Site in created_models:
- print "Creating example.com Site object"
+ if verbosity >= 2:
+ print "Creating example.com Site object"
s = Site(domain="example.com", name="example.com")
s.save()
diff --git a/django/core/management.py b/django/core/management.py
index a469c72901..51c8c760d5 100644
--- a/django/core/management.py
+++ b/django/core/management.py
@@ -423,7 +423,7 @@ def get_sql_all(app):
get_sql_all.help_doc = "Prints the CREATE TABLE, initial-data and CREATE INDEX SQL statements for the given model module name(s)."
get_sql_all.args = APP_ARGS
-def syncdb():
+def syncdb(verbosity=2, interactive=True):
"Creates the database tables for all apps in INSTALLED_APPS whose tables haven't already been created."
from django.db import connection, transaction, models, get_creation_module
from django.db.models import signals
@@ -471,7 +471,8 @@ def syncdb():
except KeyError:
pending_references[refto] = refs
sql.extend(_get_sql_for_pending_references(model, pending_references))
- print "Creating table %s" % model._meta.db_table
+ if verbosity >= 2:
+ print "Creating table %s" % model._meta.db_table
for statement in sql:
cursor.execute(statement)
table_list.append(model._meta.db_table)
@@ -480,7 +481,8 @@ def syncdb():
if model in created_models:
sql = _get_many_to_many_sql_for_model(model)
if sql:
- print "Creating many-to-many tables for %s model" % model.__name__
+ if verbosity >= 2:
+ print "Creating many-to-many tables for %s model" % model.__name__
for statement in sql:
cursor.execute(statement)
@@ -490,7 +492,8 @@ def syncdb():
# to do at this point.
for app in models.get_apps():
dispatcher.send(signal=signals.post_syncdb, sender=app,
- app=app, created_models=created_models)
+ app=app, created_models=created_models,
+ verbosity=verbosity, interactive=interactive)
# Install initial data for the app (but only if this is a model we've
# just created)
@@ -1154,6 +1157,29 @@ def runfcgi(args):
runfastcgi(args)
runfcgi.args = '[various KEY=val options, use `runfcgi help` for help]'
+def test(verbosity, app_labels):
+ "Runs the test suite for the specified applications"
+ from django.conf import settings
+ from django.db.models import get_app, get_apps
+
+ if len(app_labels) == 0:
+ app_list = get_apps()
+ else:
+ app_list = [get_app(app_label) for app_label in app_labels]
+
+ test_path = settings.TEST_RUNNER.split('.')
+ # Allow for Python 2.5 relative paths
+ if len(test_path) > 1:
+ test_module_name = '.'.join(test_path[:-1])
+ else:
+ test_module_name = '.'
+ test_module = __import__(test_module_name, [],[],test_path[-1])
+ test_runner = getattr(test_module, test_path[-1])
+
+ test_runner(app_list, verbosity)
+test.help_doc = 'Runs the test suite for the specified applications, or the entire site if no apps are specified'
+test.args = '[--verbosity] ' + APP_ARGS
+
# Utilities for command-line script
DEFAULT_ACTION_MAPPING = {
@@ -1178,6 +1204,7 @@ DEFAULT_ACTION_MAPPING = {
'startproject': startproject,
'syncdb': syncdb,
'validate': validate,
+ 'test':test,
}
NO_SQL_TRANSACTION = (
@@ -1228,8 +1255,14 @@ def execute_from_command_line(action_mapping=DEFAULT_ACTION_MAPPING, argv=None):
help='Lets you manually add a directory the Python path, e.g. "/home/djangoprojects/myproject".')
parser.add_option('--plain', action='store_true', dest='plain',
help='Tells Django to use plain Python, not IPython, for "shell" command.')
+ parser.add_option('--noinput', action='store_false', dest='interactive', default=True,
+ help='Tells Django to NOT prompt the user for input of any kind.')
parser.add_option('--noreload', action='store_false', dest='use_reloader', default=True,
help='Tells Django to NOT use the auto-reloader when running the development server.')
+ parser.add_option('--verbosity', action='store', dest='verbosity', default='2',
+ type='choice', choices=['0', '1', '2'],
+ help='Verbosity level; 0=minimal output, 1=normal output, 2=all output')
+
options, args = parser.parse_args(argv[1:])
# Take care of options.
@@ -1256,8 +1289,10 @@ def execute_from_command_line(action_mapping=DEFAULT_ACTION_MAPPING, argv=None):
if action == 'shell':
action_mapping[action](options.plain is True)
- elif action in ('syncdb', 'validate', 'diffsettings', 'dbshell'):
+ elif action in ('validate', 'diffsettings', 'dbshell'):
action_mapping[action]()
+ elif action == 'syncdb':
+ action_mapping[action](int(options.verbosity), options.interactive)
elif action == 'inspectdb':
try:
for line in action_mapping[action]():
@@ -1270,6 +1305,11 @@ def execute_from_command_line(action_mapping=DEFAULT_ACTION_MAPPING, argv=None):
action_mapping[action](args[1])
except IndexError:
parser.print_usage_and_exit()
+ elif action == 'test':
+ try:
+ action_mapping[action](int(options.verbosity), args[1:])
+ except IndexError:
+ parser.print_usage_and_exit()
elif action in ('startapp', 'startproject'):
try:
name = args[1]
diff --git a/django/core/validators.py b/django/core/validators.py
index 81bea23e36..8f40ceb51a 100644
--- a/django/core/validators.py
+++ b/django/core/validators.py
@@ -68,7 +68,7 @@ def isAlphaNumericURL(field_data, all_data):
def isSlug(field_data, all_data):
if not slug_re.search(field_data):
- raise ValidationError, "This value must contain only letters, numbers, underscores or hyphens."
+ raise ValidationError, gettext("This value must contain only letters, numbers, underscores or hyphens.")
def isLowerCase(field_data, all_data):
if field_data.lower() != field_data:
diff --git a/tests/othertests/__init__.py b/django/test/__init__.py
similarity index 100%
rename from tests/othertests/__init__.py
rename to django/test/__init__.py
diff --git a/django/test/client.py b/django/test/client.py
new file mode 100644
index 0000000000..871f6cfb9b
--- /dev/null
+++ b/django/test/client.py
@@ -0,0 +1,208 @@
+from cStringIO import StringIO
+from django.contrib.admin.views.decorators import LOGIN_FORM_KEY, _encode_post_data
+from django.core.handlers.base import BaseHandler
+from django.core.handlers.wsgi import WSGIRequest
+from django.dispatch import dispatcher
+from django.http import urlencode, SimpleCookie
+from django.template import signals
+from django.utils.functional import curry
+
+class ClientHandler(BaseHandler):
+ """
+ A HTTP Handler that can be used for testing purposes.
+ Uses the WSGI interface to compose requests, but returns
+ the raw HttpResponse object
+ """
+ def __call__(self, environ):
+ from django.conf import settings
+ from django.core import signals
+
+ # Set up middleware if needed. We couldn't do this earlier, because
+ # settings weren't available.
+ if self._request_middleware is None:
+ self.load_middleware()
+
+ dispatcher.send(signal=signals.request_started)
+ try:
+ request = WSGIRequest(environ)
+ response = self.get_response(request.path, request)
+
+ # Apply response middleware
+ for middleware_method in self._response_middleware:
+ response = middleware_method(request, response)
+
+ finally:
+ dispatcher.send(signal=signals.request_finished)
+
+ return response
+
+def store_rendered_templates(store, signal, sender, template, context):
+ "A utility function for storing templates and contexts that are rendered"
+ store.setdefault('template',[]).append(template)
+ store.setdefault('context',[]).append(context)
+
+def encode_multipart(boundary, data):
+ """
+ A simple method for encoding multipart POST data from a dictionary of
+ form values.
+
+ The key will be used as the form data name; the value will be transmitted
+ as content. If the value is a file, the contents of the file will be sent
+ as an application/octet-stream; otherwise, str(value) will be sent.
+ """
+ lines = []
+ for (key, value) in data.items():
+ if isinstance(value, file):
+ lines.extend([
+ '--' + boundary,
+ 'Content-Disposition: form-data; name="%s"' % key,
+ '',
+ '--' + boundary,
+ 'Content-Disposition: form-data; name="%s_file"; filename="%s"' % (key, value.name),
+ 'Content-Type: application/octet-stream',
+ '',
+ value.read()
+ ])
+ else:
+ lines.extend([
+ '--' + boundary,
+ 'Content-Disposition: form-data; name="%s"' % key,
+ '',
+ str(value)
+ ])
+
+ lines.extend([
+ '--' + boundary + '--',
+ '',
+ ])
+ return '\r\n'.join(lines)
+
+class Client:
+ """
+ A class that can act as a client for testing purposes.
+
+ It allows the user to compose GET and POST requests, and
+ obtain the response that the server gave to those requests.
+ The server Response objects are annotated with the details
+ of the contexts and templates that were rendered during the
+ process of serving the request.
+
+ Client objects are stateful - they will retain cookie (and
+ thus session) details for the lifetime of the Client instance.
+
+ This is not intended as a replacement for Twill/Selenium or
+ the like - it is here to allow testing against the
+ contexts and templates produced by a view, rather than the
+ HTML rendered to the end-user.
+ """
+ def __init__(self, **defaults):
+ self.handler = TestHandler()
+ self.defaults = defaults
+ self.cookie = SimpleCookie()
+
+ def request(self, **request):
+ """
+ The master request method. Composes the environment dictionary
+ and passes to the handler, returning the result of the handler.
+ Assumes defaults for the query environment, which can be overridden
+ using the arguments to the request.
+ """
+
+ environ = {
+ 'HTTP_COOKIE': self.cookie,
+ 'PATH_INFO': '/',
+ 'QUERY_STRING': '',
+ 'REQUEST_METHOD': 'GET',
+ 'SCRIPT_NAME': None,
+ 'SERVER_NAME': 'testserver',
+ 'SERVER_PORT': 80,
+ 'SERVER_PROTOCOL': 'HTTP/1.1',
+ }
+ environ.update(self.defaults)
+ environ.update(request)
+
+ # Curry a data dictionary into an instance of
+ # the template renderer callback function
+ data = {}
+ on_template_render = curry(store_rendered_templates, data)
+ dispatcher.connect(on_template_render, signal=signals.template_rendered)
+
+ response = self.handler(environ)
+
+ # Add any rendered template detail to the response
+ # If there was only one template rendered (the most likely case),
+ # flatten the list to a single element
+ for detail in ('template', 'context'):
+ if data.get(detail):
+ if len(data[detail]) == 1:
+ setattr(response, detail, data[detail][0]);
+ else:
+ setattr(response, detail, data[detail])
+ else:
+ setattr(response, detail, None)
+
+ if response.cookies:
+ self.cookie.update(response.cookies)
+
+ return response
+
+ def get(self, path, data={}, **extra):
+ "Request a response from the server using GET."
+ r = {
+ 'CONTENT_LENGTH': None,
+ 'CONTENT_TYPE': 'text/html; charset=utf-8',
+ 'PATH_INFO': path,
+ 'QUERY_STRING': urlencode(data),
+ 'REQUEST_METHOD': 'GET',
+ }
+ r.update(extra)
+
+ return self.request(**r)
+
+ def post(self, path, data={}, **extra):
+ "Request a response from the server using POST."
+
+ BOUNDARY = 'BoUnDaRyStRiNg'
+
+ encoded = encode_multipart(BOUNDARY, data)
+ stream = StringIO(encoded)
+ r = {
+ 'CONTENT_LENGTH': len(encoded),
+ 'CONTENT_TYPE': 'multipart/form-data; boundary=%s' % BOUNDARY,
+ 'PATH_INFO': path,
+ 'REQUEST_METHOD': 'POST',
+ 'wsgi.input': stream,
+ }
+ r.update(extra)
+
+ return self.request(**r)
+
+ def login(self, path, username, password, **extra):
+ """
+ A specialized sequence of GET and POST to log into a view that
+ is protected by @login_required or a similar access decorator.
+
+ path should be the URL of the login page, or of any page that
+ is login protected.
+
+ Returns True if login was successful; False if otherwise.
+ """
+ # First, GET the login page.
+ # This is required to establish the session.
+ response = self.get(path)
+ if response.status_code != 200:
+ return False
+
+ # Set up the block of form data required by the login page.
+ form_data = {
+ 'username': username,
+ 'password': password,
+ 'this_is_the_login_form': 1,
+ 'post_data': _encode_post_data({LOGIN_FORM_KEY: 1})
+ }
+ response = self.post(path, data=form_data, **extra)
+
+ # login page should give response 200 (if you requested the login
+ # page specifically), or 302 (if you requested a login
+ # protected page, to which the login can redirect).
+ return response.status_code in (200,302)
diff --git a/tests/doctest.py b/django/test/doctest.py
similarity index 99%
rename from tests/doctest.py
rename to django/test/doctest.py
index df7aa978d3..d600d15e52 100644
--- a/tests/doctest.py
+++ b/django/test/doctest.py
@@ -2104,7 +2104,7 @@ def set_unittest_reportflags(flags):
class DocTestCase(unittest.TestCase):
def __init__(self, test, optionflags=0, setUp=None, tearDown=None,
- checker=None):
+ checker=None, runner=DocTestRunner):
unittest.TestCase.__init__(self)
self._dt_optionflags = optionflags
@@ -2112,6 +2112,7 @@ class DocTestCase(unittest.TestCase):
self._dt_test = test
self._dt_setUp = setUp
self._dt_tearDown = tearDown
+ self._dt_runner = runner
def setUp(self):
test = self._dt_test
@@ -2138,8 +2139,8 @@ class DocTestCase(unittest.TestCase):
# so add the default reporting flags
optionflags |= _unittest_reportflags
- runner = DocTestRunner(optionflags=optionflags,
- checker=self._dt_checker, verbose=False)
+ runner = self._dt_runner(optionflags=optionflags,
+ checker=self._dt_checker, verbose=False)
try:
runner.DIVIDER = "-"*70
@@ -2248,7 +2249,7 @@ class DocTestCase(unittest.TestCase):
return "Doctest: " + self._dt_test.name
def DocTestSuite(module=None, globs=None, extraglobs=None, test_finder=None,
- **options):
+ test_class=DocTestCase, **options):
"""
Convert doctest tests for a module to a unittest test suite.
@@ -2306,7 +2307,7 @@ def DocTestSuite(module=None, globs=None, extraglobs=None, test_finder=None,
if filename[-4:] in (".pyc", ".pyo"):
filename = filename[:-1]
test.filename = filename
- suite.addTest(DocTestCase(test, **options))
+ suite.addTest(test_class(test, **options))
return suite
diff --git a/django/test/simple.py b/django/test/simple.py
new file mode 100644
index 0000000000..e72f693459
--- /dev/null
+++ b/django/test/simple.py
@@ -0,0 +1,67 @@
+import unittest, doctest
+from django.conf import settings
+from django.core import management
+from django.test.utils import create_test_db, destroy_test_db
+from django.test.testcases import OutputChecker, DocTestRunner
+
+# The module name for tests outside models.py
+TEST_MODULE = 'tests'
+
+doctestOutputChecker = OutputChecker()
+
+def build_suite(app_module):
+ "Create a complete Django test suite for the provided application module"
+ suite = unittest.TestSuite()
+
+ # Load unit and doctests in the models.py file
+ suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(app_module))
+ try:
+ suite.addTest(doctest.DocTestSuite(app_module,
+ checker=doctestOutputChecker,
+ runner=DocTestRunner))
+ except ValueError:
+ # No doc tests in models.py
+ pass
+
+ # Check to see if a separate 'tests' module exists parallel to the
+ # models module
+ try:
+ app_path = app_module.__name__.split('.')[:-1]
+ test_module = __import__('.'.join(app_path + [TEST_MODULE]), [], [], TEST_MODULE)
+
+ suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(test_module))
+ try:
+ suite.addTest(doctest.DocTestSuite(test_module,
+ checker=doctestOutputChecker,
+ runner=DocTestRunner))
+ except ValueError:
+ # No doc tests in tests.py
+ pass
+ except ImportError:
+ # No tests.py file for application
+ pass
+
+ return suite
+
+def run_tests(module_list, verbosity=1, extra_tests=[]):
+ """
+ Run the unit tests for all the modules in the provided list.
+ This testrunner will search each of the modules in the provided list,
+ looking for doctests and unittests in models.py or tests.py within
+ the module. A list of 'extra' tests may also be provided; these tests
+ will be added to the test suite.
+ """
+
+ settings.DEBUG = False
+ suite = unittest.TestSuite()
+
+ for module in module_list:
+ suite.addTest(build_suite(module))
+
+ for test in extra_tests:
+ suite.addTest(test)
+
+ old_name = create_test_db(verbosity)
+ management.syncdb(verbosity, interactive=False)
+ unittest.TextTestRunner(verbosity=verbosity).run(suite)
+ destroy_test_db(old_name, verbosity)
diff --git a/django/test/testcases.py b/django/test/testcases.py
new file mode 100644
index 0000000000..1cfef6f19e
--- /dev/null
+++ b/django/test/testcases.py
@@ -0,0 +1,30 @@
+import re, doctest, unittest
+from django.db import transaction
+
+normalize_long_ints = lambda s: re.sub(r'(?= 1:
+ print "Creating test database..."
+ # If we're using SQLite, it's more convenient to test against an
+ # in-memory database.
+ if settings.DATABASE_ENGINE == "sqlite3":
+ TEST_DATABASE_NAME = ":memory:"
+ else:
+ TEST_DATABASE_NAME = TEST_DATABASE_PREFIX + settings.DATABASE_NAME
+
+ # Create the test database and connect to it. We need to autocommit
+ # if the database supports it because PostgreSQL doesn't allow
+ # CREATE/DROP DATABASE statements within transactions.
+ cursor = connection.cursor()
+ _set_autocommit(connection)
+ try:
+ cursor.execute("CREATE DATABASE %s" % TEST_DATABASE_NAME)
+ except Exception, e:
+ sys.stderr.write("Got an error creating the test database: %s\n" % e)
+ if not autoclobber:
+ confirm = raw_input("It appears the test database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % TEST_DATABASE_NAME)
+ if autoclobber or confirm == 'yes':
+ try:
+ if verbosity >= 1:
+ print "Destroying old test database..."
+ cursor.execute("DROP DATABASE %s" % TEST_DATABASE_NAME)
+ if verbosity >= 1:
+ print "Creating test database..."
+ cursor.execute("CREATE DATABASE %s" % TEST_DATABASE_NAME)
+ except Exception, e:
+ sys.stderr.write("Got an error recreating the test database: %s\n" % e)
+ sys.exit(2)
+ else:
+ print "Tests cancelled."
+ sys.exit(1)
+
+ connection.close()
+ old_database_name = settings.DATABASE_NAME
+ settings.DATABASE_NAME = TEST_DATABASE_NAME
+
+ # Get a cursor (even though we don't need one yet). This has
+ # the side effect of initializing the test database.
+ cursor = connection.cursor()
+
+ return old_database_name
+
+def destroy_test_db(old_database_name, verbosity=1):
+ # Unless we're using SQLite, remove the test database to clean up after
+ # ourselves. Connect to the previous database (not the test database)
+ # to do so, because it's not allowed to delete a database while being
+ # connected to it.
+ if verbosity >= 1:
+ print "Destroying test database..."
+ if settings.DATABASE_ENGINE != "sqlite3":
+ connection.close()
+ TEST_DATABASE_NAME = settings.DATABASE_NAME
+ settings.DATABASE_NAME = old_database_name
+ cursor = connection.cursor()
+ _set_autocommit(connection)
+ time.sleep(1) # To avoid "database is being accessed by other users" errors.
+ cursor.execute("DROP DATABASE %s" % TEST_DATABASE_NAME)
+ connection.close()
+
diff --git a/django/views/generic/date_based.py b/django/views/generic/date_based.py
index 860199c22e..d13c0293be 100644
--- a/django/views/generic/date_based.py
+++ b/django/views/generic/date_based.py
@@ -1,6 +1,7 @@
from django.template import loader, RequestContext
from django.core.exceptions import ObjectDoesNotExist
from django.core.xheaders import populate_xheaders
+from django.db.models.fields import DateTimeField
from django.http import Http404, HttpResponse
import datetime, time
@@ -235,9 +236,10 @@ def archive_day(request, year, month, day, queryset, date_field,
model = queryset.model
now = datetime.datetime.now()
- lookup_kwargs = {
- '%s__range' % date_field: (datetime.datetime.combine(date, datetime.time.min), datetime.datetime.combine(date, datetime.time.max)),
- }
+ if isinstance(model._meta.get_field(date_field), DateTimeField):
+ lookup_kwargs = {'%s__range' % date_field: (datetime.datetime.combine(date, datetime.time.min), datetime.datetime.combine(date, datetime.time.max))}
+ else:
+ lookup_kwargs = {date_field: date}
# Only bother to check current date if the date isn't in the past and future objects aren't requested.
if date >= now.date() and not allow_future:
@@ -304,9 +306,10 @@ def object_detail(request, year, month, day, queryset, date_field,
model = queryset.model
now = datetime.datetime.now()
- lookup_kwargs = {
- '%s__range' % date_field: (datetime.datetime.combine(date, datetime.time.min), datetime.datetime.combine(date, datetime.time.max)),
- }
+ if isinstance(model._meta.get_field(date_field), DateTimeField):
+ lookup_kwargs = {'%s__range' % date_field: (datetime.datetime.combine(date, datetime.time.min), datetime.datetime.combine(date, datetime.time.max))}
+ else:
+ lookup_kwargs = {date_field: date}
# Only bother to check current date if the date isn't in the past and future objects aren't requested.
if date >= now.date() and not allow_future:
diff --git a/docs/faq.txt b/docs/faq.txt
index 42d9ddea55..d39e203c76 100644
--- a/docs/faq.txt
+++ b/docs/faq.txt
@@ -16,12 +16,17 @@ hours to take a complicated Web application from concept to public launch.
At the same time, the World Online Web developers have consistently been
perfectionists when it comes to following best practices of Web development.
-Thus, Django was designed not only to allow fast Web development, but
-*best-practice* Web development.
+In fall 2003, the World Online developers (Adrian Holovaty and Simon Willison)
+ditched PHP and began using Python to develop its Web sites. As they built
+intensive, richly interactive sites such as Lawrence.com, they began to extract
+a generic Web development framework that let them build Web applications more
+and more quickly. They tweaked this framework constantly, adding improvements
+over two years.
-Django would not be possible without a whole host of open-source projects --
-`Apache`_, `Python`_, and `PostgreSQL`_ to name a few -- and we're thrilled to
-be able to give something back to the open-source community.
+In summer 2005, World Online decided to open-source the resulting software,
+Django. Django would not be possible without a whole host of open-source
+projects -- `Apache`_, `Python`_, and `PostgreSQL`_ to name a few -- and we're
+thrilled to be able to give something back to the open-source community.
.. _Apache: http://httpd.apache.org/
.. _Python: http://www.python.org/
@@ -42,8 +47,8 @@ Django is pronounced **JANG**-oh. Rhymes with FANG-oh. The "D" is silent.
Is Django stable?
-----------------
-Yes. World Online has been using Django for more than two years. Sites built on
-Django have weathered traffic spikes of over one million hits an hour and a
+Yes. World Online has been using Django for more than three years. Sites built
+on Django have weathered traffic spikes of over one million hits an hour and a
number of Slashdottings. Yes, it's quite stable.
Does Django scale?
@@ -630,6 +635,14 @@ You can also use the Python API. See `creating users`_ for full info.
Contributing code
=================
+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 do Django: http://www.djangoproject.com/documentation/contributing/
+
I submitted a bug fix in the ticket system several weeks ago. Why are you ignoring my patch?
--------------------------------------------------------------------------------------------
diff --git a/docs/model-api.txt b/docs/model-api.txt
index 0a3abe4e26..b46a11c463 100644
--- a/docs/model-api.txt
+++ b/docs/model-api.txt
@@ -1222,10 +1222,13 @@ A few special cases to note about ``list_display``:
of the related object.
* ``ManyToManyField`` fields aren't supported, because that would entail
- executing a separate SQL statement for each row in the table.
+ executing a separate SQL statement for each row in the table. If you
+ want to do this nonetheless, give your model a custom method, and add
+ that method's name to ``list_display``. (See below for more on custom
+ methods in ``list_display``.)
- * If the field is a ``BooleanField``, Django will display a pretty "on" or
- "off" icon instead of ``True`` or ``False``.
+ * If the field is a ``BooleanField`` or ``NullBooleanField``, Django will
+ display a pretty "on" or "off" icon instead of ``True`` or ``False``.
* If the string given is a method of the model, Django will call it and
display the output. This method should have a ``short_description``
@@ -1262,6 +1265,16 @@ A few special cases to note about ``list_display``:
return '%s %s' % (self.color_code, self.first_name, self.last_name)
colored_name.allow_tags = True
+ * The ``__str__()`` method is just as valid in ``list_display`` as any
+ other model method, so it's perfectly OK to do this::
+
+ list_display = ('__str__', 'some_other_field')
+
+ * For any element of ``list_display`` that is not a field on the model, the
+ change list page will not allow ordering by that column. This is because
+ ordering is done at the database level, and Django has no way of knowing
+ how to order the result of a custom method at the SQL level.
+
``list_display_links``
----------------------
diff --git a/docs/overview.txt b/docs/overview.txt
index 5a399582e8..8e6274dd9a 100644
--- a/docs/overview.txt
+++ b/docs/overview.txt
@@ -159,7 +159,7 @@ of contents for your app, it contains a simple mapping between URL patterns and
Python callback functions. URLconfs also serve to decouple URLs from Python
code.
-Here's what a URLconf might look like for the above ``Reporter``/``Article``
+Here's what a URLconf might look like for the ``Reporter``/``Article``
example above::
from django.conf.urls.defaults import *
diff --git a/docs/settings.txt b/docs/settings.txt
index 67e0498e1a..d9df111155 100644
--- a/docs/settings.txt
+++ b/docs/settings.txt
@@ -473,25 +473,36 @@ LANGUAGES
Default: A tuple of all available languages. Currently, this is::
LANGUAGES = (
+ ('ar', _('Arabic')),
('bn', _('Bengali')),
('cs', _('Czech')),
('cy', _('Welsh')),
('da', _('Danish')),
('de', _('German')),
+ ('el', _('Greek')),
('en', _('English')),
('es', _('Spanish')),
+ ('es_AR', _('Argentinean Spanish')),
('fr', _('French')),
('gl', _('Galician')),
+ ('hu', _('Hungarian')),
+ ('he', _('Hebrew')),
('is', _('Icelandic')),
('it', _('Italian')),
+ ('ja', _('Japanese')),
+ ('nl', _('Dutch')),
('no', _('Norwegian')),
('pt-br', _('Brazilian')),
('ro', _('Romanian')),
('ru', _('Russian')),
('sk', _('Slovak')),
+ ('sl', _('Slovenian')),
('sr', _('Serbian')),
('sv', _('Swedish')),
+ ('ta', _('Tamil')),
+ ('uk', _('Ukrainian')),
('zh-cn', _('Simplified Chinese')),
+ ('zh-tw', _('Traditional Chinese')),
)
A tuple of two-tuples in the format (language code, language name). This
diff --git a/docs/templates.txt b/docs/templates.txt
index 49d30018fe..c9e76d6c94 100644
--- a/docs/templates.txt
+++ b/docs/templates.txt
@@ -141,6 +141,7 @@ It's easiest to understand template inheritance by starting with an example::
{% block content %}{% endblock %}