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 + Sander Dijkhuis Jordan Dimov dne@mayonnaise.net Maximillian Dornseif 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 __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
_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
    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::
  • States @@ -371,14 +371,61 @@ def unordered_list(value):
""" - 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
  • %s\n%s
      \n%s\n%s
    \n%s
  • ' % (indent, force_unicode(value[0]), indent, - u'\n'.join([_helper(v, tabs+1) for v in value[1]]), indent, indent) - else: - return u'%s
  • %s
  • ' % (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
      \n%s\n%s
    \n%s' % (indent, sublist, + indent, indent) + output.append('%s
  • %s%s
  • ' % (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
      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::
    • States
        @@ -1317,6 +1322,9 @@ then ``{{ var|unordered_list }}`` would return::
    • +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
    • item 1
    • \n\t
    • item 2
    • ' + +>>> unordered_list([u'item 1', [u'item 1.1']]) +u'\t
    • item 1\n\t
        \n\t\t
      • item 1.1
      • \n\t
      \n\t
    • ' + +>>> unordered_list([u'item 1', [u'item 1.1', u'item1.2'], u'item 2']) +u'\t
    • item 1\n\t
        \n\t\t
      • item 1.1
      • \n\t\t
      • item1.2
      • \n\t
      \n\t
    • \n\t
    • item 2
    • ' + +>>> unordered_list([u'item 1', [u'item 1.1', [u'item 1.1.1', [u'item 1.1.1.1']]]]) +u'\t
    • item 1\n\t
        \n\t\t
      • item 1.1\n\t\t
          \n\t\t\t
        • item 1.1.1\n\t\t\t
            \n\t\t\t\t
          • item 1.1.1.1
          • \n\t\t\t
          \n\t\t\t
        • \n\t\t
        \n\t\t
      • \n\t
      \n\t
    • ' + +>>> unordered_list(['States', ['Kansas', ['Lawrence', 'Topeka'], 'Illinois']]) +u'\t
    • States\n\t
        \n\t\t
      • Kansas\n\t\t
          \n\t\t\t
        • Lawrence
        • \n\t\t\t
        • Topeka
        • \n\t\t
        \n\t\t
      • \n\t\t
      • Illinois
      • \n\t
      \n\t
    • ' + +# Old format for unordered lists should still work >>> unordered_list([u'item 1', []]) u'\t
    • item 1
    • ' @@ -275,6 +291,9 @@ u'\t
    • item 1\n\t
        \n\t\t
      • item 1.1
      • \n\t
      \n\t
    • ' >>> unordered_list([u'item 1', [[u'item 1.1', []], [u'item 1.2', []]]]) u'\t
    • item 1\n\t
        \n\t\t
      • item 1.1
      • \n\t\t
      • item 1.2
      • \n\t
      \n\t
    • ' +>>> unordered_list(['States', [['Kansas', [['Lawrence', []], ['Topeka', []]]], ['Illinois', []]]]) +u'\t
    • States\n\t
        \n\t\t
      • Kansas\n\t\t
          \n\t\t\t
        • Lawrence
        • \n\t\t\t
        • Topeka
        • \n\t\t
        \n\t\t
      • \n\t\t
      • Illinois
      • \n\t
      \n\t
    • ' + >>> add(u'1', u'2') 3