summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJustin Bronn <jbronn@gmail.com>2007-08-26 17:47:34 +0000
committerJustin Bronn <jbronn@gmail.com>2007-08-26 17:47:34 +0000
commitcb64f5375cc1884226cad05b671e8660214a4a92 (patch)
tree740d3abe9b8d2ed8c8255c9b6ea378950cbf6b39
parent2052b508eb92c62fc0678efd4936c5ec1e0e735b (diff)
gis: Merged revisions 6000-6020 via svnmerge from [repos:django/trunk trunk].
git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@6021 bcc190cf-cafb-0310-a4f2-bffc1f526a37
-rw-r--r--AUTHORS1
-rw-r--r--django/contrib/admin/media/js/SelectBox.js12
-rw-r--r--django/contrib/auth/views.py2
-rw-r--r--django/contrib/gis/tests/test_spatialrefsys.py1
-rw-r--r--django/core/management/commands/flush.py2
-rw-r--r--django/core/management/commands/runserver.py4
-rw-r--r--django/core/management/commands/sqlflush.py2
-rw-r--r--django/core/management/sql.py34
-rw-r--r--django/db/backends/dummy/base.py10
-rw-r--r--django/db/backends/postgresql/base.py3
-rw-r--r--django/db/backends/postgresql/operations.py43
-rw-r--r--django/db/backends/postgresql_psycopg2/base.py3
-rw-r--r--django/template/defaultfilters.py65
-rw-r--r--docs/db-api.txt19
-rw-r--r--docs/django-admin.txt24
-rw-r--r--docs/faq.txt4
-rw-r--r--docs/model-api.txt2
-rw-r--r--docs/newforms.txt2
-rw-r--r--docs/settings.txt9
-rw-r--r--docs/templates.txt12
-rw-r--r--docs/tutorial01.txt33
-rw-r--r--tests/regressiontests/defaultfilters/tests.py19
22 files changed, 227 insertions, 79 deletions
diff --git a/AUTHORS b/AUTHORS
index bc5c44b335..311c96c8ea 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -94,6 +94,7 @@ answer newbie questions, and generally made Django that much better:
Alex Dedul
deric@monowerks.com
Max Derkachev <mderk@yandex.ru>
+ Sander Dijkhuis <sander.dijkhuis@gmail.com>
Jordan Dimov <s3x3y1@gmail.com>
dne@mayonnaise.net
Maximillian Dornseif <md@hudora.de>
diff --git a/django/contrib/admin/media/js/SelectBox.js b/django/contrib/admin/media/js/SelectBox.js
index af8de204cb..f28c861513 100644
--- a/django/contrib/admin/media/js/SelectBox.js
+++ b/django/contrib/admin/media/js/SelectBox.js
@@ -6,7 +6,7 @@ var SelectBox = {
SelectBox.cache[id] = new Array();
var cache = SelectBox.cache[id];
for (var i = 0; (node = box.options[i]); i++) {
- cache.push({ value: node.value, text: node.text, displayed: 1 });
+ cache.push({value: node.value, text: node.text, displayed: 1});
}
},
redisplay: function(id) {
@@ -50,7 +50,7 @@ var SelectBox = {
SelectBox.cache[id].length--;
},
add_to_cache: function(id, option) {
- SelectBox.cache[id].push({ value: option.value, text: option.text, displayed: 1 });
+ SelectBox.cache[id].push({value: option.value, text: option.text, displayed: 1});
},
cache_contains: function(id, value) {
// Check if an item is contained in the cache
@@ -68,7 +68,7 @@ var SelectBox = {
var option;
for (var i = 0; (option = from_box.options[i]); i++) {
if (option.selected && SelectBox.cache_contains(from, option.value)) {
- SelectBox.add_to_cache(to, { value: option.value, text: option.text, displayed: 1 });
+ SelectBox.add_to_cache(to, {value: option.value, text: option.text, displayed: 1});
SelectBox.delete_from_cache(from, option.value);
}
}
@@ -80,8 +80,10 @@ var SelectBox = {
var to_box = document.getElementById(to);
var option;
for (var i = 0; (option = from_box.options[i]); i++) {
- SelectBox.add_to_cache(to, { value: option.value, text: option.text, displayed: 1 });
- SelectBox.delete_from_cache(from, option.value);
+ if (SelectBox.cache_contains(from, option.value)) {
+ SelectBox.add_to_cache(to, {value: option.value, text: option.text, displayed: 1});
+ SelectBox.delete_from_cache(from, option.value);
+ }
}
SelectBox.redisplay(from);
SelectBox.redisplay(to);
diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py
index 6c40228fab..f1129379d6 100644
--- a/django/contrib/auth/views.py
+++ b/django/contrib/auth/views.py
@@ -17,7 +17,7 @@ def login(request, template_name='registration/login.html'):
errors = manipulator.get_validation_errors(request.POST)
if not errors:
# Light security check -- make sure redirect_to isn't garbage.
- if not redirect_to or '://' in redirect_to or ' ' in redirect_to:
+ if not redirect_to or '//' in redirect_to or ' ' in redirect_to:
from django.conf import settings
redirect_to = settings.LOGIN_REDIRECT_URL
from django.contrib.auth import login
diff --git a/django/contrib/gis/tests/test_spatialrefsys.py b/django/contrib/gis/tests/test_spatialrefsys.py
index 554f55175f..95644d7ec9 100644
--- a/django/contrib/gis/tests/test_spatialrefsys.py
+++ b/django/contrib/gis/tests/test_spatialrefsys.py
@@ -51,7 +51,6 @@ class SpatialRefSysTest(unittest.TestCase):
def test03_ellipsoid(self):
"Testing the ellipsoid property."
- return
for sd in test_srs:
# Getting the ellipsoid and precision parameters.
ellps1 = sd['ellipsoid']
diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py
index bd1dc204d2..395359d269 100644
--- a/django/core/management/commands/flush.py
+++ b/django/core/management/commands/flush.py
@@ -24,7 +24,7 @@ class Command(NoArgsCommand):
except ImportError:
pass
- sql_list = sql_flush(self.style)
+ sql_list = sql_flush(self.style, only_django=True)
if interactive:
confirm = raw_input("""You have requested a flush of the database.
diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py
index f089e80b16..d06744e9fa 100644
--- a/django/core/management/commands/runserver.py
+++ b/django/core/management/commands/runserver.py
@@ -30,7 +30,7 @@ class Command(BaseCommand):
raise CommandError("%r is not a valid port number." % port)
use_reloader = options.get('use_reloader', True)
- admin_media_dir = options.get('admin_media_dir', '')
+ admin_media_path = options.get('admin_media_path', '')
shutdown_message = options.get('shutdown_message', '')
quit_command = (sys.platform == 'win32') and 'CTRL-BREAK' or 'CONTROL-C'
@@ -42,7 +42,7 @@ class Command(BaseCommand):
print "Development server is running at http://%s:%s/" % (addr, port)
print "Quit the server with %s." % quit_command
try:
- path = admin_media_dir or django.__path__[0] + '/contrib/admin/media'
+ path = admin_media_path or django.__path__[0] + '/contrib/admin/media'
handler = AdminMediaHandler(WSGIHandler(), path)
run(addr, int(port), handler)
except WSGIServerException, e:
diff --git a/django/core/management/commands/sqlflush.py b/django/core/management/commands/sqlflush.py
index 7d14fe61e1..261aa0d423 100644
--- a/django/core/management/commands/sqlflush.py
+++ b/django/core/management/commands/sqlflush.py
@@ -7,4 +7,4 @@ class Command(NoArgsCommand):
def handle_noargs(self, **options):
from django.core.management.sql import sql_flush
- return '\n'.join(sql_flush(self.style))
+ return '\n'.join(sql_flush(self.style, only_django=True))
diff --git a/django/core/management/sql.py b/django/core/management/sql.py
index b984d74d4b..92effe0fa2 100644
--- a/django/core/management/sql.py
+++ b/django/core/management/sql.py
@@ -13,6 +13,25 @@ def table_list():
cursor = connection.cursor()
return get_introspection_module().get_table_list(cursor)
+def django_table_list(only_existing=False):
+ """
+ Returns a list of all table names that have associated Django models and
+ are in INSTALLED_APPS.
+
+ If only_existing is True, the resulting list will only include the tables
+ that actually exist in the database.
+ """
+ from django.db import models
+ tables = []
+ for app in models.get_apps():
+ for model in models.get_models(app):
+ tables.append(model._meta.db_table)
+ tables.extend([f.m2m_db_table() for f in model._meta.many_to_many])
+ if only_existing:
+ existing = table_list()
+ tables = [t for t in tables if t in existing]
+ return tables
+
def installed_models(table_list):
"Returns a set of all models that are installed, given a list of existing table names."
from django.db import connection, models
@@ -181,10 +200,19 @@ def sql_reset(app, style):
"Returns a list of the DROP TABLE SQL, then the CREATE TABLE SQL, for the given module."
return sql_delete(app, style) + sql_all(app, style)
-def sql_flush(style):
- "Returns a list of the SQL statements used to flush the database."
+def sql_flush(style, only_django=False):
+ """
+ Returns a list of the SQL statements used to flush the database.
+
+ If only_django is True, then only table names that have associated Django
+ models and are in INSTALLED_APPS will be included.
+ """
from django.db import connection
- statements = connection.ops.sql_flush(style, table_list(), sequence_list())
+ if only_django:
+ tables = django_table_list()
+ else:
+ tables = table_list()
+ statements = connection.ops.sql_flush(style, tables, sequence_list())
return statements
def sql_custom(app, style):
diff --git a/django/db/backends/dummy/base.py b/django/db/backends/dummy/base.py
index 50191f88fe..fd25d3038f 100644
--- a/django/db/backends/dummy/base.py
+++ b/django/db/backends/dummy/base.py
@@ -8,6 +8,7 @@ ImproperlyConfigured.
"""
from django.core.exceptions import ImproperlyConfigured
+from django.db.backends import BaseDatabaseFeatures, BaseDatabaseOperations
def complain(*args, **kwargs):
raise ImproperlyConfigured, "You haven't set the DATABASE_ENGINE setting yet."
@@ -21,13 +22,12 @@ class DatabaseError(Exception):
class IntegrityError(DatabaseError):
pass
-class ComplainOnGetattr(object):
- def __getattr__(self, *args, **kwargs):
- complain()
+class DatabaseOperations(BaseDatabaseOperations):
+ quote_name = complain
class DatabaseWrapper(object):
- features = ComplainOnGetattr()
- ops = ComplainOnGetattr()
+ features = BaseDatabaseFeatures()
+ ops = DatabaseOperations()
operators = {}
cursor = complain
_commit = complain
diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py
index ca07ae21d9..c8b87c2dd1 100644
--- a/django/db/backends/postgresql/base.py
+++ b/django/db/backends/postgresql/base.py
@@ -102,9 +102,6 @@ class DatabaseWrapper(BaseDatabaseWrapper):
cursor.execute("SET TIME ZONE %s", [settings.TIME_ZONE])
cursor.execute("SET client_encoding to 'UNICODE'")
cursor = UnicodeCursorWrapper(cursor, 'utf-8')
- if self.ops.postgres_version is None:
- cursor.execute("SELECT version()")
- self.ops.postgres_version = [int(val) for val in cursor.fetchone()[0].split()[1].split('.')]
return cursor
def typecast_string(s):
diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py
index 21c017038f..9f36596ace 100644
--- a/django/db/backends/postgresql/operations.py
+++ b/django/db/backends/postgresql/operations.py
@@ -4,8 +4,17 @@ from django.db.backends import BaseDatabaseOperations
# used by both the 'postgresql' and 'postgresql_psycopg2' backends.
class DatabaseOperations(BaseDatabaseOperations):
- def __init__(self, postgres_version=None):
- self.postgres_version = postgres_version
+ def __init__(self):
+ self._postgres_version = None
+
+ def _get_postgres_version(self):
+ if self._postgres_version is None:
+ from django.db import connection
+ cursor = connection.cursor()
+ cursor.execute("SELECT version()")
+ self._postgres_version = [int(val) for val in cursor.fetchone()[0].split()[1].split('.')]
+ return self._postgres_version
+ postgres_version = property(_get_postgres_version)
def date_extract_sql(self, lookup_type, field_name):
# http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT
@@ -52,28 +61,14 @@ class DatabaseOperations(BaseDatabaseOperations):
for sequence_info in sequences:
table_name = sequence_info['table']
column_name = sequence_info['column']
- if column_name and len(column_name)>0:
- # sequence name in this case will be <table>_<column>_seq
- sql.append("%s %s %s %s %s %s;" % \
- (style.SQL_KEYWORD('ALTER'),
- style.SQL_KEYWORD('SEQUENCE'),
- style.SQL_FIELD(self.quote_name('%s_%s_seq' % (table_name, column_name))),
- style.SQL_KEYWORD('RESTART'),
- style.SQL_KEYWORD('WITH'),
- style.SQL_FIELD('1')
- )
- )
+ if column_name and len(column_name) > 0:
+ sequence_name = '%s_%s_seq' % (table_name, column_name)
else:
- # sequence name in this case will be <table>_id_seq
- sql.append("%s %s %s %s %s %s;" % \
- (style.SQL_KEYWORD('ALTER'),
- style.SQL_KEYWORD('SEQUENCE'),
- style.SQL_FIELD(self.quote_name('%s_id_seq' % table_name)),
- style.SQL_KEYWORD('RESTART'),
- style.SQL_KEYWORD('WITH'),
- style.SQL_FIELD('1')
- )
- )
+ sequence_name = '%s_id_seq' % table_name
+ sql.append("%s setval('%s', 1, false);" % \
+ (style.SQL_KEYWORD('SELECT'),
+ style.SQL_FIELD(self.quote_name(sequence_name)))
+ )
return sql
else:
return []
@@ -106,4 +101,4 @@ class DatabaseOperations(BaseDatabaseOperations):
style.SQL_KEYWORD('IS NOT'),
style.SQL_KEYWORD('FROM'),
style.SQL_TABLE(f.m2m_db_table())))
- return output \ No newline at end of file
+ return output
diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py
index 43ca7a1ec5..a7b080d505 100644
--- a/django/db/backends/postgresql_psycopg2/base.py
+++ b/django/db/backends/postgresql_psycopg2/base.py
@@ -64,7 +64,4 @@ class DatabaseWrapper(BaseDatabaseWrapper):
cursor.tzinfo_factory = None
if set_tz:
cursor.execute("SET TIME ZONE %s", [settings.TIME_ZONE])
- if self.ops.postgres_version is None:
- cursor.execute("SELECT version()")
- self.ops.postgres_version = [int(val) for val in cursor.fetchone()[0].split()[1].split('.')]
return cursor
diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py
index 6779b9b5f2..22babfd6c0 100644
--- a/django/template/defaultfilters.py
+++ b/django/template/defaultfilters.py
@@ -355,8 +355,8 @@ def unordered_list(value):
Recursively takes a self-nested list and returns an HTML unordered list --
WITHOUT opening and closing <ul> tags.
- The list is assumed to be in the proper format. For example, if ``var`` contains
- ``['States', [['Kansas', [['Lawrence', []], ['Topeka', []]]], ['Illinois', []]]]``,
+ The list is assumed to be in the proper format. For example, if ``var``
+ contains: ``['States', ['Kansas', ['Lawrence', 'Topeka'], 'Illinois']]``,
then ``{{ var|unordered_list }}`` would return::
<li>States
@@ -371,14 +371,61 @@ def unordered_list(value):
</ul>
</li>
"""
- def _helper(value, tabs):
+ def convert_old_style_list(list_):
+ """
+ Converts old style lists to the new easier to understand format.
+
+ The old list format looked like:
+ ['Item 1', [['Item 1.1', []], ['Item 1.2', []]]
+
+ And it is converted to:
+ ['Item 1', ['Item 1.1', 'Item 1.2]]
+ """
+ if not isinstance(list_, (tuple, list)) or len(list_) != 2:
+ return list_, False
+ first_item, second_item = list_
+ if second_item == []:
+ return [first_item], True
+ old_style_list = True
+ new_second_item = []
+ for sublist in second_item:
+ item, old_style_list = convert_old_style_list(sublist)
+ if not old_style_list:
+ break
+ new_second_item.extend(item)
+ if old_style_list:
+ second_item = new_second_item
+ return [first_item, second_item], old_style_list
+ def _helper(list_, tabs=1):
indent = u'\t' * tabs
- if value[1]:
- return u'%s<li>%s\n%s<ul>\n%s\n%s</ul>\n%s</li>' % (indent, force_unicode(value[0]), indent,
- u'\n'.join([_helper(v, tabs+1) for v in value[1]]), indent, indent)
- else:
- return u'%s<li>%s</li>' % (indent, force_unicode(value[0]))
- return _helper(value, 1)
+ output = []
+
+ list_length = len(list_)
+ i = 0
+ while i < list_length:
+ title = list_[i]
+ sublist = ''
+ sublist_item = None
+ if isinstance(title, (list, tuple)):
+ sublist_item = title
+ title = ''
+ elif i < list_length - 1:
+ next_item = list_[i+1]
+ if next_item and isinstance(next_item, (list, tuple)):
+ # The next item is a sub-list.
+ sublist_item = next_item
+ # We've processed the next item now too.
+ i += 1
+ if sublist_item:
+ sublist = _helper(sublist_item, tabs+1)
+ sublist = '\n%s<ul>\n%s\n%s</ul>\n%s' % (indent, sublist,
+ indent, indent)
+ output.append('%s<li>%s%s</li>' % (indent, force_unicode(title),
+ sublist))
+ i += 1
+ return '\n'.join(output)
+ value, converted = convert_old_style_list(value)
+ return _helper(value)
###################
# INTEGERS #
diff --git a/docs/db-api.txt b/docs/db-api.txt
index 766a6ae519..3198f335c4 100644
--- a/docs/db-api.txt
+++ b/docs/db-api.txt
@@ -207,14 +207,23 @@ the database until you explicitly call ``save()``.
The ``save()`` method has no return value.
-Updating ``ForeignKey`` fields works exactly the same way; simply assign an
-object of the right type to the field in question::
+Saving ForeignKey and ManyToManyField fields
+--------------------------------------------
+
+Updating ``ForeignKey`` fields works exactly the same way as saving a normal
+field; simply assign an object of the right type to the field in question::
+
+ cheese_blog = Blog.objects.get(name="Cheddar Talk")
+ entry.blog = cheese_blog
+ entry.save()
+
+Updating a ``ManyToManyField`` works a little differently; use the ``add()``
+method on the field to add a record to the relation::
joe = Author.objects.create(name="Joe")
- entry.author = joe
- entry.save()
+ entry.authors.add(joe)
-Django will complain if you try to assign an object of the wrong type.
+Django will complain if you try to assign or add an object of the wrong type.
How Django knows to UPDATE vs. INSERT
-------------------------------------
diff --git a/docs/django-admin.txt b/docs/django-admin.txt
index aea990c5dc..e3d1067dd3 100644
--- a/docs/django-admin.txt
+++ b/docs/django-admin.txt
@@ -124,6 +124,13 @@ executed. This means that all data will be removed from the database, any
post-synchronization handlers will be re-executed, and the ``initial_data``
fixture will be re-installed.
+The behavior of this command has changed in the Django development version.
+Previously, this command cleared *every* table in the database, including any
+table that Django didn't know about (i.e., tables that didn't have associated
+models and/or weren't in ``INSTALLED_APPS``). Now, the command only clears
+tables that are represented by Django models and are activated in
+``INSTALLED_APPS``.
+
inspectdb
---------
@@ -240,6 +247,7 @@ Executes the equivalent of ``sqlreset`` for the given appnames.
runfcgi [options]
-----------------
+
Starts a set of FastCGI processes suitable for use with any web server
which supports the FastCGI protocol. See the `FastCGI deployment
documentation`_ for details. Requires the Python FastCGI module from
@@ -337,7 +345,7 @@ Refer to the description of ``sqlcustom`` for an explanation of how to
specify initial data.
sqlclear [appname appname ...]
---------------------------------------
+------------------------------
Prints the DROP TABLE SQL statements for the given appnames.
@@ -360,18 +368,23 @@ table modifications, or insert any SQL functions into the database.
Note that the order in which the SQL files are processed is undefined.
+sqlflush
+--------
+
+Prints the SQL statements that would be executed for the `flush`_ command.
+
sqlindexes [appname appname ...]
-----------------------------------------
+--------------------------------
Prints the CREATE INDEX SQL statements for the given appnames.
sqlreset [appname appname ...]
---------------------------------------
+------------------------------
Prints the DROP TABLE SQL, then the CREATE TABLE SQL, for the given appnames.
sqlsequencereset [appname appname ...]
-----------------------------------------------
+--------------------------------------
Prints the SQL statements for resetting sequences for the given
appnames.
@@ -466,6 +479,9 @@ This is useful in a number of ways:
Note that this server can only run on the default port on localhost; it does
not yet accept a ``host`` or ``port`` parameter.
+Also note that it does *not* automatically detect changes to your Python source
+code (as ``runserver`` does). It does, however, detect changes to templates.
+
.. _unit tests: ../testing/
validate
diff --git a/docs/faq.txt b/docs/faq.txt
index 844ea77809..cef0508562 100644
--- a/docs/faq.txt
+++ b/docs/faq.txt
@@ -204,10 +204,6 @@ out a few points, we want to make sure they reflect the final state of things
at Django 1.0, not some intermediary step. In other words, we don't want to
spend a lot of energy creating screencasts yet, because Django APIs will shift.
-In the meantime, though, check out this `unofficial Django screencast`_.
-
-.. _unofficial Django screencast: http://www.throwingbeans.org/django_screencasts.html
-
Is Django a content-management-system (CMS)?
--------------------------------------------
diff --git a/docs/model-api.txt b/docs/model-api.txt
index 0f872c3097..7dac54992f 100644
--- a/docs/model-api.txt
+++ b/docs/model-api.txt
@@ -344,7 +344,7 @@ development version. See the `Django 0.96 documentation`_ for the old behavior.
``ImageField``
~~~~~~~~~~~~~~
-Like ``FileField``, but validates that the uploaded object is a valid
+Like `FileField`_, but validates that the uploaded object is a valid
image. Has two extra optional arguments, ``height_field`` and
``width_field``, which, if set, will be auto-populated with the height and
width of the image each time a model instance is saved.
diff --git a/docs/newforms.txt b/docs/newforms.txt
index 0b59a7ad65..36c627b398 100644
--- a/docs/newforms.txt
+++ b/docs/newforms.txt
@@ -1554,7 +1554,7 @@ as a custom extension to the ``TextInput`` widget::
class CommentWidget(forms.TextInput):
def __init__(self, *args, **kwargs):
kwargs.setdefault('attrs',{}).update({'size': '40'})
- super(forms.TextInput, self).__init__(*args, **kwargs)
+ super(CommentWidget, self).__init__(*args, **kwargs)
Then you can use this widget in your forms::
diff --git a/docs/settings.txt b/docs/settings.txt
index 050e377713..3f98296778 100644
--- a/docs/settings.txt
+++ b/docs/settings.txt
@@ -1101,10 +1101,11 @@ To disable this behavior, just remove all entries from the ``ADMINS`` setting.
404 errors
----------
-When ``DEBUG`` is ``False`` and your ``MIDDLEWARE_CLASSES`` setting includes
-``CommonMiddleware``, Django will e-mail the users listed in the ``MANAGERS``
-setting whenever your code raises a 404 and the request has a referer.
-(It doesn't bother to e-mail for 404s that don't have a referer.)
+When ``DEBUG`` is ``False``, ``SEND_BROKEN_LINK_EMAILS`` is ``True`` and your
+``MIDDLEWARE_CLASSES`` setting includes ``CommonMiddleware``, Django will
+e-mail the users listed in the ``MANAGERS`` setting whenever your code raises
+a 404 and the request has a referer. (It doesn't bother to e-mail for 404s
+that don't have a referer.)
You can tell Django to stop reporting particular 404s by tweaking the
``IGNORABLE_404_ENDS`` and ``IGNORABLE_404_STARTS`` settings. Both should be a
diff --git a/docs/templates.txt b/docs/templates.txt
index 6cebd3b7bd..8bfa40dc5f 100644
--- a/docs/templates.txt
+++ b/docs/templates.txt
@@ -1301,9 +1301,14 @@ unordered_list
Recursively takes a self-nested list and returns an HTML unordered list --
WITHOUT opening and closing <ul> tags.
+**Changed in Django development version**
+
+The format accepted by ``unordered_list`` has changed to an easier to
+understand format.
+
The list is assumed to be in the proper format. For example, if ``var`` contains
-``['States', [['Kansas', [['Lawrence', []], ['Topeka', []]]], ['Illinois', []]]]``,
-then ``{{ var|unordered_list }}`` would return::
+``['States', ['Kansas', ['Lawrence', 'Topeka'], 'Illinois']]``, then
+``{{ var|unordered_list }}`` would return::
<li>States
<ul>
@@ -1317,6 +1322,9 @@ then ``{{ var|unordered_list }}`` would return::
</ul>
</li>
+Note: the previous more restrictive and verbose format is still supported:
+``['States', [['Kansas', [['Lawrence', []], ['Topeka', []]]], ['Illinois', []]]]``,
+
upper
~~~~~
diff --git a/docs/tutorial01.txt b/docs/tutorial01.txt
index cf2b76e9be..60c527216b 100644
--- a/docs/tutorial01.txt
+++ b/docs/tutorial01.txt
@@ -259,6 +259,22 @@ These concepts are represented by simple Python classes. Edit the
choice = models.CharField(max_length=200)
votes = models.IntegerField()
+.. admonition:: Errors about ``max_length``
+
+ If Django gives you an error message saying that ``max_length`` is
+ not a valid argument, you're most likely using an old version of
+ Django. (This version of the tutorial is written for the latest
+ development version of Django.) If you're using a Subversion checkout
+ of Django's development version (see `the installation docs`_ for
+ more information), you shouldn't have any problems.
+
+ If you want to stick with an older version of Django, you'll want to
+ switch to `the Django 0.96 tutorial`_, because this tutorial covers
+ several features that only exist in the Django development version.
+
+.. _the installation docs: ../install/
+.. _the Django 0.96 tutorial: ../0.96/tutorial01/
+
The code is straightforward. Each model is represented by a class that
subclasses ``django.db.models.Model``. Each model has a number of class
variables, each of which represents a database field in the model.
@@ -487,6 +503,23 @@ the ``polls/models.py`` file) and adding a ``__unicode__()`` method to both
def __unicode__(self):
return self.choice
+.. admonition:: If ``__unicode__()`` doesn't seem to work
+
+ If you add the ``__unicode__()`` method to your models and don't
+ see any change in how they're represented, you're most likely using
+ an old version of Django. (This version of the tutorial is written
+ for the latest development version of Django.) If you're using a
+ Subversion checkout of of Django's development version (see `the
+ installation docs`_ for more information), you shouldn't have any
+ problems.
+
+ If you want to stick with an older version of Django, you'll want to
+ switch to `the Django 0.96 tutorial`_, because this tutorial covers
+ several features that only exist in the Django development version.
+
+.. _the installation docs: ../install/
+.. _the Django 0.96 tutorial: ../0.96/tutorial01/
+
It's important to add ``__unicode__()`` methods to your models, not only for
your own sanity when dealing with the interactive prompt, but also because
objects' representations are used throughout Django's automatically-generated
diff --git a/tests/regressiontests/defaultfilters/tests.py b/tests/regressiontests/defaultfilters/tests.py
index a1efae66f6..9482f1cc9f 100644
--- a/tests/regressiontests/defaultfilters/tests.py
+++ b/tests/regressiontests/defaultfilters/tests.py
@@ -266,6 +266,22 @@ u'bc'
>>> slice_(u'abcdefg', u'0::2')
u'aceg'
+>>> unordered_list([u'item 1', u'item 2'])
+u'\t<li>item 1</li>\n\t<li>item 2</li>'
+
+>>> unordered_list([u'item 1', [u'item 1.1']])
+u'\t<li>item 1\n\t<ul>\n\t\t<li>item 1.1</li>\n\t</ul>\n\t</li>'
+
+>>> unordered_list([u'item 1', [u'item 1.1', u'item1.2'], u'item 2'])
+u'\t<li>item 1\n\t<ul>\n\t\t<li>item 1.1</li>\n\t\t<li>item1.2</li>\n\t</ul>\n\t</li>\n\t<li>item 2</li>'
+
+>>> unordered_list([u'item 1', [u'item 1.1', [u'item 1.1.1', [u'item 1.1.1.1']]]])
+u'\t<li>item 1\n\t<ul>\n\t\t<li>item 1.1\n\t\t<ul>\n\t\t\t<li>item 1.1.1\n\t\t\t<ul>\n\t\t\t\t<li>item 1.1.1.1</li>\n\t\t\t</ul>\n\t\t\t</li>\n\t\t</ul>\n\t\t</li>\n\t</ul>\n\t</li>'
+
+>>> unordered_list(['States', ['Kansas', ['Lawrence', 'Topeka'], 'Illinois']])
+u'\t<li>States\n\t<ul>\n\t\t<li>Kansas\n\t\t<ul>\n\t\t\t<li>Lawrence</li>\n\t\t\t<li>Topeka</li>\n\t\t</ul>\n\t\t</li>\n\t\t<li>Illinois</li>\n\t</ul>\n\t</li>'
+
+# Old format for unordered lists should still work
>>> unordered_list([u'item 1', []])
u'\t<li>item 1</li>'
@@ -275,6 +291,9 @@ u'\t<li>item 1\n\t<ul>\n\t\t<li>item 1.1</li>\n\t</ul>\n\t</li>'
>>> unordered_list([u'item 1', [[u'item 1.1', []], [u'item 1.2', []]]])
u'\t<li>item 1\n\t<ul>\n\t\t<li>item 1.1</li>\n\t\t<li>item 1.2</li>\n\t</ul>\n\t</li>'
+>>> unordered_list(['States', [['Kansas', [['Lawrence', []], ['Topeka', []]]], ['Illinois', []]]])
+u'\t<li>States\n\t<ul>\n\t\t<li>Kansas\n\t\t<ul>\n\t\t\t<li>Lawrence</li>\n\t\t\t<li>Topeka</li>\n\t\t</ul>\n\t\t</li>\n\t\t<li>Illinois</li>\n\t</ul>\n\t</li>'
+
>>> add(u'1', u'2')
3