Code

Ticket #2333: fixtures-2.diff

File fixtures-2.diff, 43.4 KB (added by russellm, 7 years ago)

Test Fixtures, version 2

Line 
1Index: django/test/testcases.py
2===================================================================
3--- django/test/testcases.py    (revision 4431)
4+++ django/test/testcases.py    (working copy)
5@@ -1,5 +1,7 @@
6 import re, doctest, unittest
7 from django.db import transaction
8+from django.core import management
9+from django.db.models import get_apps
10     
11 normalize_long_ints = lambda s: re.sub(r'(?<![\w])(\d+)L(?![\w])', '\\1', s)
12 
13@@ -19,6 +21,7 @@
14     def __init__(self, *args, **kwargs):
15         doctest.DocTestRunner.__init__(self, *args, **kwargs)
16         self.optionflags = doctest.ELLIPSIS
17+        management.flush(verbosity=0, interactive=False)
18         
19     def report_unexpected_exception(self, out, test, example, exc_info):
20         doctest.DocTestRunner.report_unexpected_exception(self,out,test,example,exc_info)
21@@ -28,3 +31,21 @@
22         from django.db import transaction
23         transaction.rollback_unless_managed()
24 
25+class TestCase(unittest.TestCase):   
26+    def install_fixtures(self):
27+        """If the Test Case class has a 'fixtures' member, clear the database and
28+        install the named fixtures at the start of each test.
29+       
30+        """
31+        management.flush(verbosity=0, interactive=False)
32+        if hasattr(self, 'fixtures'):
33+            management.load_data(self.fixtures, verbosity=0)
34+
35+    def run(self, result=None):
36+        """Wrapper around default run method so that user-defined Test Cases
37+        automatically call install_fixtures without having to include a call to
38+        super().
39+       
40+        """
41+        self.install_fixtures()
42+        super(TestCase, self).run(result)
43Index: django/test/__init__.py
44===================================================================
45--- django/test/__init__.py     (revision 4431)
46+++ django/test/__init__.py     (working copy)
47@@ -0,0 +1,6 @@
48+"""
49+Django Unit Test and Doctest framework.
50+"""
51+
52+from django.test.client import Client
53+from django.test.testcases import TestCase
54Index: django/db/backends/ado_mssql/base.py
55===================================================================
56--- django/db/backends/ado_mssql/base.py        (revision 4431)
57+++ django/db/backends/ado_mssql/base.py        (working copy)
58@@ -134,6 +134,19 @@
59 def get_pk_default_value():
60     return "DEFAULT"
61 
62+def get_sql_flush(sql_styler, full_table_list):
63+    """Return a list of SQL statements required to remove all data from
64+    all tables in the database (without actually removing the tables
65+    themselves) and put the database in an empty 'initial' state
66+    """
67+    # Return a list of 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements
68+    # TODO - SQL not actually tested against ADO MSSQL yet!
69+    # TODO - autoincrement indices reset required? See other get_sql_flush() implementations
70+    sql_list = ['%s %s;' % \
71+                (sql_styler.SQL_KEYWORD('TRUNCATE'),
72+                 sql_styler.SQL_FIELD(quote_name(table))
73+                 )  for table in full_table_list]
74+
75 OPERATOR_MAPPING = {
76     'exact': '= %s',
77     'iexact': 'LIKE %s',
78Index: django/db/backends/postgresql/base.py
79===================================================================
80--- django/db/backends/postgresql/base.py       (revision 4431)
81+++ django/db/backends/postgresql/base.py       (working copy)
82@@ -145,6 +145,52 @@
83 def get_pk_default_value():
84     return "DEFAULT"
85 
86+def get_sql_flush(style, tables, sequences):
87+    """Return a list of SQL statements required to remove all data from
88+    all tables in the database (without actually removing the tables
89+    themselves) and put the database in an empty 'initial' state
90+   
91+    """
92+    # Postgres can do 'TRUNCATE x, y, z...;'. In fact, it *has to* in order to be able to
93+    # truncate tables referenced by a foreign key in any other table. The result is a
94+    # single SQL TRUNCATE statement.
95+    if tables:
96+        sql = ['%s %s;' % \
97+            (style.SQL_KEYWORD('TRUNCATE'),
98+             style.SQL_FIELD(', '.join(quote_name(table) for table in tables))
99+        )]
100+   
101+        # 'ALTER SEQUENCE sequence_name RESTART WITH 1;'... style SQL statements
102+        # to reset sequence indices
103+        for sequence_info in sequences:
104+            table_name = sequence_info['table']
105+            column_name = sequence_info['column']
106+            if column_name and len(column_name)>0:
107+                # sequence name in this case will be <table>_<column>_seq
108+                sql.append("%s %s %s %s %s %s;" % \
109+                    (style.SQL_KEYWORD('ALTER'),
110+                    style.SQL_KEYWORD('SEQUENCE'),
111+                    style.SQL_FIELD('%s_%s_seq' % (table_name, column_name)),
112+                    style.SQL_KEYWORD('RESTART'),
113+                    style.SQL_KEYWORD('WITH'),
114+                    style.SQL_FIELD('1')
115+                    )
116+                )
117+            else:
118+                # sequence name in this case will be <table>_id_seq
119+                sql.append("%s %s %s %s %s %s;" % \
120+                    (style.SQL_KEYWORD('ALTER'),
121+                     style.SQL_KEYWORD('SEQUENCE'),
122+                     style.SQL_FIELD('%s_id_seq' % table_name),
123+                     style.SQL_KEYWORD('RESTART'),
124+                     style.SQL_KEYWORD('WITH'),
125+                     style.SQL_FIELD('1')
126+                     )
127+                )
128+        return sql
129+    else:
130+        return []
131+       
132 # Register these custom typecasts, because Django expects dates/times to be
133 # in Python's native (standard-library) datetime/time format, whereas psycopg
134 # use mx.DateTime by default.
135Index: django/db/backends/sqlite3/base.py
136===================================================================
137--- django/db/backends/sqlite3/base.py  (revision 4431)
138+++ django/db/backends/sqlite3/base.py  (working copy)
139@@ -148,6 +148,24 @@
140 def get_pk_default_value():
141     return "NULL"
142 
143+def get_sql_flush(style, tables, sequences):
144+    """Return a list of SQL statements required to remove all data from
145+    all tables in the database (without actually removing the tables
146+    themselves) and put the database in an empty 'initial' state
147+   
148+    """
149+    # NB: The generated SQL below is specific to SQLite
150+    # Note: The DELETE FROM... SQL generated below works for SQLite databases
151+    # because constraints don't exist
152+    sql = ['%s %s %s;' % \
153+            (style.SQL_KEYWORD('DELETE'),
154+             style.SQL_KEYWORD('FROM'),
155+             style.SQL_FIELD(quote_name(table))
156+             ) for table in tables]
157+    # Note: No requirement for reset of auto-incremented indices (cf. other
158+    # get_sql_flush() implementations). Just return SQL at this point
159+    return sql
160+
161 def _sqlite_date_trunc(lookup_type, dt):
162     try:
163         dt = util.typecast_timestamp(dt)
164Index: django/db/backends/mysql/base.py
165===================================================================
166--- django/db/backends/mysql/base.py    (revision 4431)
167+++ django/db/backends/mysql/base.py    (working copy)
168@@ -183,6 +183,33 @@
169 def get_pk_default_value():
170     return "DEFAULT"
171 
172+def get_sql_flush(style, tables, sequences):
173+    """Return a list of SQL statements required to remove all data from
174+    all tables in the database (without actually removing the tables
175+    themselves) and put the database in an empty 'initial' state
176+   
177+    """
178+    # NB: The generated SQL below is specific to MySQL
179+    # 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements
180+    # to clear all tables of all data
181+    if tables:
182+        sql = ['%s %s;' % \
183+                (style.SQL_KEYWORD('TRUNCATE'),
184+                 style.SQL_FIELD(quote_name(table))
185+                )  for table in tables]
186+        # 'ALTER TABLE table AUTO_INCREMENT = 1;'... style SQL statements
187+        # to reset sequence indices
188+        sql.extend(["%s %s %s %s %s;" % \
189+            (style.SQL_KEYWORD('ALTER'),
190+             style.SQL_KEYWORD('TABLE'),
191+             style.SQL_TABLE(quote_name(sequence['table'])),
192+             style.SQL_KEYWORD('AUTO_INCREMENT'),
193+             style.SQL_FIELD('= 1'),
194+            ) for sequence in sequences])
195+        return sql
196+    else:
197+        return []
198+
199 OPERATOR_MAPPING = {
200     'exact': '= %s',
201     'iexact': 'LIKE %s',
202Index: django/db/backends/oracle/base.py
203===================================================================
204--- django/db/backends/oracle/base.py   (revision 4431)
205+++ django/db/backends/oracle/base.py   (working copy)
206@@ -117,6 +117,20 @@
207 def get_pk_default_value():
208     return "DEFAULT"
209 
210+def get_sql_flush(style, tables, sequences):
211+    """Return a list of SQL statements required to remove all data from
212+    all tables in the database (without actually removing the tables
213+    themselves) and put the database in an empty 'initial' state
214+    """
215+    # Return a list of 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements
216+    # TODO - SQL not actually tested against Oracle yet!
217+    # TODO - autoincrement indices reset required? See other get_sql_flush() implementations
218+    sql = ['%s %s;' % \
219+            (style.SQL_KEYWORD('TRUNCATE'),
220+             style.SQL_FIELD(quote_name(table))
221+             )  for table in tables]
222+
223+
224 OPERATOR_MAPPING = {
225     'exact': '= %s',
226     'iexact': 'LIKE %s',
227Index: django/db/backends/postgresql_psycopg2/base.py
228===================================================================
229--- django/db/backends/postgresql_psycopg2/base.py      (revision 4431)
230+++ django/db/backends/postgresql_psycopg2/base.py      (working copy)
231@@ -105,6 +105,50 @@
232 def get_pk_default_value():
233     return "DEFAULT"
234 
235+def get_sql_flush(style, tables, sequences):
236+    """Return a list of SQL statements required to remove all data from
237+    all tables in the database (without actually removing the tables
238+    themselves) and put the database in an empty 'initial' state
239+    """
240+    # Postgres can do 'TRUNCATE x, y, z...;'. In fact, it *has to* in order to be able to
241+    # truncate tables referenced by a foreign key in any other table. The result is a
242+    # single SQL TRUNCATE statement
243+    if tables:
244+        sql = ['%s %s;' % \
245+                (style.SQL_KEYWORD('TRUNCATE'),
246+                 style.SQL_FIELD(', '.join(quote_name(table) for table in tables))
247+                )]
248+        # 'ALTER SEQUENCE sequence_name RESTART WITH 1;'... style SQL statements
249+        # to reset sequence indices
250+        for sequence in sequences:
251+            table_name = sequence['table']
252+            column_name = sequence['column']
253+            if column_name and len(column_name) > 0:
254+                # sequence name in this case will be <table>_<column>_seq
255+                sql.append("%s %s %s %s %s %s;" % \
256+                    (style.SQL_KEYWORD('ALTER'),
257+                     style.SQL_KEYWORD('SEQUENCE'),
258+                     style.SQL_FIELD('%s_%s_seq' % (table_name, column_name)),
259+                     style.SQL_KEYWORD('RESTART'),
260+                     style.SQL_KEYWORD('WITH'),
261+                     style.SQL_FIELD('1')
262+                     )
263+                )
264+            else:
265+                # sequence name in this case will be <table>_id_seq
266+                sql.append("%s %s %s %s %s %s;" % \
267+                    (style.SQL_KEYWORD('ALTER'),
268+                     style.SQL_KEYWORD('SEQUENCE'),
269+                     style.SQL_FIELD('%s_id_seq' % table_name),
270+                     style.SQL_KEYWORD('RESTART'),
271+                     style.SQL_KEYWORD('WITH'),
272+                     style.SQL_FIELD('1')
273+                     )
274+                )
275+        return sql
276+    else:
277+        return []
278+       
279 OPERATOR_MAPPING = {
280     'exact': '= %s',
281     'iexact': 'ILIKE %s',
282Index: django/db/backends/dummy/base.py
283===================================================================
284--- django/db/backends/dummy/base.py    (revision 4431)
285+++ django/db/backends/dummy/base.py    (working copy)
286@@ -38,4 +38,6 @@
287 get_random_function_sql = complain
288 get_fulltext_search_sql = complain
289 get_drop_foreignkey_sql = complain
290+get_sql_flush = complain
291+
292 OPERATOR_MAPPING = {}
293Index: django/conf/global_settings.py
294===================================================================
295--- django/conf/global_settings.py      (revision 4431)
296+++ django/conf/global_settings.py      (working copy)
297@@ -315,3 +315,10 @@
298 # The name of the database to use for testing purposes.
299 # If None, a name of 'test_' + DATABASE_NAME will be assumed
300 TEST_DATABASE_NAME = None
301+
302+############
303+# FIXTURES #
304+############
305+
306+# The list of directories to search for fixtures
307+FIXTURE_DIRS = ()
308Index: django/core/serializers/base.py
309===================================================================
310--- django/core/serializers/base.py     (revision 4431)
311+++ django/core/serializers/base.py     (working copy)
312@@ -141,7 +141,7 @@
313 
314 class DeserializedObject(object):
315     """
316-    A deserialzed model.
317+    A deserialized model.
318 
319     Basically a container for holding the pre-saved deserialized data along
320     with the many-to-many data saved with the object.
321Index: django/core/serializers/__init__.py
322===================================================================
323--- django/core/serializers/__init__.py (revision 4431)
324+++ django/core/serializers/__init__.py (working copy)
325@@ -40,6 +40,11 @@
326     if not _serializers:
327         _load_serializers()
328     return _serializers[format].Serializer
329+
330+def get_serializer_formats():
331+    if not _serializers:
332+        _load_serializers()
333+    return _serializers.keys()
334     
335 def get_deserializer(format):
336     if not _serializers:
337Index: django/core/management.py
338===================================================================
339--- django/core/management.py   (revision 4431)
340+++ django/core/management.py   (working copy)
341@@ -68,6 +68,25 @@
342     cursor = connection.cursor()
343     return get_introspection_module().get_table_list(cursor)
344 
345+def _get_sequence_list():
346+    "Returns a list of information about all DB sequences for all models in all apps"
347+    from django.db import models
348+
349+    apps = models.get_apps()
350+    sequence_list = []
351+
352+    for app in apps:
353+        for model in models.get_models(app):
354+            for f in model._meta.fields:
355+                if isinstance(f, models.AutoField):
356+                    sequence_list.append({'table':model._meta.db_table,'column':f.column,})
357+                    break # Only one AutoField is allowed per model, so don't bother continuing.
358+
359+            for f in model._meta.many_to_many:
360+                sequence_list.append({'table':f.m2m_db_table(),'column':None,})
361+
362+    return sequence_list
363+
364 # If the foreign key points to an AutoField, a PositiveIntegerField or a
365 # PositiveSmallIntegerField, the foreign key should be an IntegerField, not the
366 # referred field type. Otherwise, the foreign key should be the same type of
367@@ -330,7 +349,15 @@
368 get_sql_reset.help_doc = "Prints the DROP TABLE SQL, then the CREATE TABLE SQL, for the given app name(s)."
369 get_sql_reset.args = APP_ARGS
370 
371-def get_sql_initial_data_for_model(model):
372+def get_sql_flush():
373+    "Returns a list of the SQL statements used to flush the database"
374+    from django.db import backend
375+    statements = backend.get_sql_flush(style, _get_table_list(), _get_sequence_list())
376+    return statements
377+get_sql_flush.help_doc = "Returns a list of the SQL statements required to return all tables in the database to the state they were in just after they were installed."
378+get_sql_flush.args = ''
379+
380+def get_custom_sql_for_model(model):
381     from django.db import models
382     from django.conf import settings
383 
384@@ -357,8 +384,8 @@
385 
386     return output
387 
388-def get_sql_initial_data(app):
389-    "Returns a list of the initial INSERT SQL statements for the given app."
390+def get_custom_sql(app):
391+    "Returns a list of the custom table modifying SQL statements for the given app."
392     from django.db.models import get_models
393     output = []
394 
395@@ -366,12 +393,18 @@
396     app_dir = os.path.normpath(os.path.join(os.path.dirname(app.__file__), 'sql'))
397 
398     for model in app_models:
399-        output.extend(get_sql_initial_data_for_model(model))
400+        output.extend(get_custom_sql_for_model(model))
401 
402     return output
403-get_sql_initial_data.help_doc = "Prints the initial INSERT SQL statements for the given app name(s)."
404-get_sql_initial_data.args = APP_ARGS
405+get_custom_sql.help_doc = "Prints the custom table modifying SQL statements for the given app name(s)."
406+get_custom_sql.args = APP_ARGS
407 
408+def get_sql_initial_data(apps):
409+    "Returns a list of the initial INSERT SQL statements for the given app."
410+    return style.ERROR("This action has been renamed. Try './manage.py sqlcustom %s'." % ' '.join(apps and apps or ['app1', 'app2']))
411+get_sql_initial_data.help_doc = "RENAMED: see 'sqlcustom'"
412+get_sql_initial_data.args = ''
413+
414 def get_sql_sequence_reset(app):
415     "Returns a list of the SQL statements to reset PostgreSQL sequences for the given app."
416     from django.db import backend, models
417@@ -428,16 +461,26 @@
418 
419 def get_sql_all(app):
420     "Returns a list of CREATE TABLE SQL, initial-data inserts, and CREATE INDEX SQL for the given module."
421-    return get_sql_create(app) + get_sql_initial_data(app) + get_sql_indexes(app)
422+    return get_sql_create(app) + get_custom_sql(app) + get_sql_indexes(app)
423 get_sql_all.help_doc = "Prints the CREATE TABLE, initial-data and CREATE INDEX SQL statements for the given model module name(s)."
424 get_sql_all.args = APP_ARGS
425 
426+def _emit_post_sync_signal(created_models, verbosity, interactive):
427+    from django.db import models
428+    from django.dispatch import dispatcher
429+    # Emit the post_sync signal for every application.
430+    for app in models.get_apps():
431+        app_name = app.__name__.split('.')[-2]
432+        if verbosity >= 2:
433+            print "Running post-sync handlers for application", app_name
434+        dispatcher.send(signal=models.signals.post_syncdb, sender=app,
435+            app=app, created_models=created_models,
436+            verbosity=verbosity, interactive=interactive)
437+
438 def syncdb(verbosity=1, interactive=True):
439     "Creates the database tables for all apps in INSTALLED_APPS whose tables haven't already been created."
440     from django.db import connection, transaction, models, get_creation_module
441-    from django.db.models import signals
442     from django.conf import settings
443-    from django.dispatch import dispatcher
444 
445     disable_termcolors()
446 
447@@ -499,27 +542,22 @@
448 
449     # Send the post_syncdb signal, so individual apps can do whatever they need
450     # to do at this point.
451+    _emit_post_sync_signal(created_models, verbosity, interactive)
452+
453+    # Install custom SQL for the app (but only if this
454+    # is a model we've just created)
455     for app in models.get_apps():
456-        app_name = app.__name__.split('.')[-2]
457-        if verbosity >= 2:
458-            print "Running post-sync handlers for application", app_name
459-        dispatcher.send(signal=signals.post_syncdb, sender=app,
460-            app=app, created_models=created_models,
461-            verbosity=verbosity, interactive=interactive)
462-
463-        # Install initial data for the app (but only if this is a model we've
464-        # just created)
465         for model in models.get_models(app):
466             if model in created_models:
467-                initial_sql = get_sql_initial_data_for_model(model)
468-                if initial_sql:
469+                custom_sql = get_custom_sql_for_model(model)
470+                if custom_sql:
471                     if verbosity >= 1:
472-                        print "Installing initial data for %s.%s model" % (app_name, model._meta.object_name)
473+                        print "Installing custom SQL for %s.%s model" % (app_name, model._meta.object_name)
474                     try:
475-                        for sql in initial_sql:
476+                        for sql in custom_sql:
477                             cursor.execute(sql)
478                     except Exception, e:
479-                        sys.stderr.write("Failed to install initial SQL data for %s.%s model: %s" % \
480+                        sys.stderr.write("Failed to install custom SQL for %s.%s model: %s" % \
481                                             (app_name, model._meta.object_name, e))
482                         transaction.rollback_unless_managed()
483                     else:
484@@ -544,7 +582,10 @@
485                     else:
486                         transaction.commit_unless_managed()
487 
488-syncdb.args = ''
489+    # Install the 'initialdata' fixture, using format discovery
490+    load_data(['initial_data'], verbosity=verbosity)
491+syncdb.help_doc = "Create the database tables for all apps in INSTALLED_APPS whose tables haven't already been created."
492+syncdb.args = '[--verbosity] [--interactive]'
493 
494 def get_admin_index(app):
495     "Returns admin-index template snippet (in list form) for the given app."
496@@ -597,36 +638,6 @@
497     print '\n'.join(output)
498 diffsettings.args = ""
499 
500-def install(app):
501-    "Executes the equivalent of 'get_sql_all' in the current database."
502-    from django.db import connection, transaction
503-
504-    app_name = app.__name__.split('.')[-2]
505-
506-    disable_termcolors()
507-
508-    # First, try validating the models.
509-    _check_for_validation_errors(app)
510-
511-    sql_list = get_sql_all(app)
512-
513-    try:
514-        cursor = connection.cursor()
515-        for sql in sql_list:
516-            cursor.execute(sql)
517-    except Exception, e:
518-        sys.stderr.write(style.ERROR("""Error: %s couldn't be installed. Possible reasons:
519-  * The database isn't running or isn't configured correctly.
520-  * At least one of the database tables already exists.
521-  * The SQL was invalid.
522-Hint: Look at the output of 'django-admin.py sqlall %s'. That's the SQL this command wasn't able to run.
523-The full error: """ % (app_name, app_name)) + style.ERROR_OUTPUT(str(e)) + '\n')
524-        transaction.rollback_unless_managed()
525-        sys.exit(1)
526-    transaction.commit_unless_managed()
527-install.help_doc = "Executes ``sqlall`` for the given app(s) in the current database."
528-install.args = APP_ARGS
529-
530 def reset(app, interactive=True):
531     "Executes the equivalent of 'get_sql_reset' in the current database."
532     from django.db import connection, transaction
533@@ -668,8 +679,69 @@
534     else:
535         print "Reset cancelled."
536 reset.help_doc = "Executes ``sqlreset`` for the given app(s) in the current database."
537-reset.args = APP_ARGS
538+reset.args = '[--interactive]' + APP_ARGS
539 
540+def flush(verbosity=1, interactive=True):
541+    "Returns all tables in the database to the same state they were in immediately after syncdb."
542+    from django.conf import settings
543+    from django.db import connection, transaction, models
544+    from django.dispatch import dispatcher
545+   
546+    disable_termcolors()
547+
548+    # First, try validating the models.
549+    _check_for_validation_errors()
550+
551+    # Import the 'management' module within each installed app, to register
552+    # dispatcher events.
553+    for app_name in settings.INSTALLED_APPS:
554+        try:
555+            __import__(app_name + '.management', {}, {}, [''])
556+        except ImportError:
557+            pass
558+   
559+    sql_list = get_sql_flush()
560+
561+    if interactive:
562+        confirm = raw_input("""
563+You have requested a flush of the database.
564+This will IRREVERSIBLY DESTROY all data currently in the database,
565+and return each table to the state it was in after syncdb.
566+Are you sure you want to do this?
567+
568+Type 'yes' to continue, or 'no' to cancel: """)
569+    else:
570+        confirm = 'yes'
571+
572+    if confirm == 'yes':
573+        try:
574+            cursor = connection.cursor()
575+            for sql in sql_list:
576+                cursor.execute(sql)
577+        except Exception, e:
578+            sys.stderr.write(style.ERROR("""Error: Database %s couldn't be flushed. Possible reasons:
579+  * The database isn't running or isn't configured correctly.
580+  * At least one of the expected database tables doesn't exist.
581+  * The SQL was invalid.
582+Hint: Look at the output of 'django-admin.py sqlflush'. That's the SQL this command wasn't able to run.
583+The full error: """ % settings.DATABASE_NAME + style.ERROR_OUTPUT(str(e)) + '\n'))
584+            transaction.rollback_unless_managed()
585+            sys.exit(1)
586+        transaction.commit_unless_managed()
587+
588+        # Emit the post sync signal. This allows individual
589+        # applications to respond as if the database had been
590+        # sync'd from scratch.
591+        _emit_post_sync_signal(models.get_models(), verbosity, interactive)
592+       
593+        # Reinstall the initial_data fixture
594+        load_data(['initial_data'], verbosity=verbosity)
595+       
596+    else:
597+        print "Flush cancelled."
598+flush.help_doc = "Executes ``sqlflush`` on the current database."
599+flush.args = '[--verbosity] [--interactive]'
600+
601 def _start_helper(app_or_project, name, directory, other_name=''):
602     other = {'project': 'app', 'app': 'project'}[app_or_project]
603     if not _is_valid_dir_name(name):
604@@ -751,7 +823,7 @@
605     yield "#     * Make sure each model has one field with primary_key=True"
606     yield "# Feel free to rename the models, but don't rename db_table values or field names."
607     yield "#"
608-    yield "# Also note: You'll have to insert the output of 'django-admin.py sqlinitialdata [appname]'"
609+    yield "# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]'"
610     yield "# into your database."
611     yield ''
612     yield 'from django.db import models'
613@@ -1239,6 +1311,118 @@
614 test.help_doc = 'Runs the test suite for the specified applications, or the entire site if no apps are specified'
615 test.args = '[--verbosity] ' + APP_ARGS
616 
617+def load_data(fixture_labels, verbosity=1):
618+    "Installs the provided fixture file(s) as data in the database."
619+    from django.db.models import get_apps
620+    from django.core import serializers
621+    from django.db import transaction
622+    from django.conf import settings
623+    import sys
624+     
625+    # Keep a count of the installed objects and fixtures
626+    count = [0,0]
627+   
628+    humanize = lambda dirname: dirname and "'%s'" % dirname or 'absolute path'
629+
630+    # Start transaction management. All fixtures are installed in a
631+    # single transaction to ensure that all references are resolved.
632+    transaction.enter_transaction_management()
633+    transaction.managed(True)
634+   
635+    app_fixtures = [os.path.join(os.path.dirname(app.__file__),'fixtures') for app in get_apps()]
636+    for fixture_label in fixture_labels:
637+        if verbosity > 0:
638+            print "Loading '%s' fixtures..." % fixture_label
639+        for fixture_dir in app_fixtures + list(settings.FIXTURE_DIRS) + ['']:
640+            if verbosity > 1:
641+                print "Checking %s for fixtures..." % humanize(fixture_dir)
642+            try:
643+                fixture_name, format = fixture_label.rsplit('.', 1)
644+                formats = [format]
645+            except ValueError:
646+                fixture_name = fixture_label
647+                formats = serializers.get_serializer_formats()
648+           
649+            label_found = False
650+            for format in formats:
651+                serializer = serializers.get_serializer(format)
652+                if verbosity > 1:
653+                    print "Trying %s for %s fixture '%s'..." % \
654+                        (humanize(fixture_dir), format, fixture_name)
655+                try:
656+                    full_path = os.path.join(fixture_dir, '.'.join([fixture_name, format]))
657+                    fixture = open(full_path, 'r')
658+                    if label_found:
659+                        fixture.close()
660+                        print style.ERROR("Multiple fixtures named '%s' in %s. Aborting." %
661+                            (fixture_name, humanize(fixture_dir)))
662+                        transaction.rollback()
663+                        transaction.leave_transaction_management()
664+                        return
665+                    else:
666+                        count[1] += 1
667+                        if verbosity > 0:
668+                            print "Installing %s fixture '%s' from %s." % \
669+                                (format, fixture_name, humanize(fixture_dir))
670+                        try:
671+                            objects =  serializers.deserialize(format, fixture)
672+                            for obj in objects:
673+                                count[0] += 1
674+                                obj.save()
675+                            label_found = True
676+                        except Exception, e:
677+                            fixture.close()
678+                            sys.stderr.write(
679+                                style.ERROR("Problem installing fixture '%s': %s\n" %
680+                                     (full_path, str(e))))
681+                            transaction.rollback()
682+                            transaction.leave_transaction_management()
683+                            return
684+                        fixture.close()
685+                except:
686+                    if verbosity > 1:
687+                        print "No %s fixture '%s' in %s." % \
688+                            (format, fixture_name, humanize(fixture_dir))
689+    if count[0] == 0:
690+        if verbosity > 0:
691+            print "No fixtures found."
692+    else:
693+        if verbosity > 0:
694+            print "Installed %d object(s) from %d fixture(s)" % tuple(count)
695+    transaction.commit()
696+    transaction.leave_transaction_management()
697+       
698+load_data.help_doc = 'Installs the named fixture(s) in the database'
699+load_data.args = "[--verbosity] fixture, fixture, ..."
700+
701+def dump_data(app_labels, format='json'):
702+    "Output the current contents of the database as a fixture of the given format"
703+    from django.db.models import get_app, get_apps, get_models
704+    from django.core import serializers
705+
706+    if len(app_labels) == 0:
707+        app_list = get_apps()
708+    else:
709+        app_list = [get_app(app_label) for app_label in app_labels]
710+
711+    # Check that the serialization format exists; this is a shortcut to
712+    # avoid collating all the objects and _then_ failing.
713+    try:
714+        serializers.get_serializer(format)
715+    except KeyError:
716+        sys.stderr.write(style.ERROR("Unknown serialization format: %s\n" % format))       
717+   
718+    objects = []
719+    for app in app_list:
720+        for model in get_models(app):
721+            objects.extend(model.objects.all())
722+    try:
723+        print serializers.serialize(format, objects)
724+    except Exception, e:
725+        sys.stderr.write(style.ERROR("Unable to serialize database: %s\n" % e))
726+dump_data.help_doc = 'Output the contents of the database as a fixture of the given format'
727+dump_data.args = '[--format]' + APP_ARGS
728+
729 # Utilities for command-line script
730 
731 DEFAULT_ACTION_MAPPING = {
732@@ -1246,8 +1430,10 @@
733     'createcachetable' : createcachetable,
734     'dbshell': dbshell,
735     'diffsettings': diffsettings,
736+    'dumpdata': dump_data,
737+    'flush': flush,
738     'inspectdb': inspectdb,
739-    'install': install,
740+    'loaddata': load_data,
741     'reset': reset,
742     'runfcgi': runfcgi,
743     'runserver': runserver,
744@@ -1255,6 +1441,8 @@
745     'sql': get_sql_create,
746     'sqlall': get_sql_all,
747     'sqlclear': get_sql_delete,
748+    'sqlcustom': get_custom_sql,
749+    'sqlflush': get_sql_flush,
750     'sqlindexes': get_sql_indexes,
751     'sqlinitialdata': get_sql_initial_data,
752     'sqlreset': get_sql_reset,
753@@ -1271,7 +1459,6 @@
754     'createcachetable',
755     'dbshell',
756     'diffsettings',
757-    'install',
758     'reset',
759     'sqlindexes',
760     'syncdb',
761@@ -1318,6 +1505,8 @@
762         help='Tells Django to NOT prompt the user for input of any kind.')
763     parser.add_option('--noreload', action='store_false', dest='use_reloader', default=True,
764         help='Tells Django to NOT use the auto-reloader when running the development server.')
765+    parser.add_option('--format', default='json', dest='format',
766+        help='Specifies the output serialization format for fixtures')   
767     parser.add_option('--verbosity', action='store', dest='verbosity', default='1',
768         type='choice', choices=['0', '1', '2'],
769         help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'),
770@@ -1351,7 +1540,7 @@
771         action_mapping[action](options.plain is True)
772     elif action in ('validate', 'diffsettings', 'dbshell'):
773         action_mapping[action]()
774-    elif action == 'syncdb':
775+    elif action in ('flush', 'syncdb'):
776         action_mapping[action](int(options.verbosity), options.interactive)
777     elif action == 'inspectdb':
778         try:
779@@ -1365,11 +1554,16 @@
780             action_mapping[action](args[1])
781         except IndexError:
782             parser.print_usage_and_exit()
783-    elif action == 'test':
784+    elif action in ('test', 'loaddata'):
785         try:
786             action_mapping[action](args[1:], int(options.verbosity))
787         except IndexError:
788             parser.print_usage_and_exit()
789+    elif action == 'dumpdata':
790+        try:
791+            action_mapping[action](args[1:], options.format)
792+        except IndexError:
793+            parser.print_usage_and_exit()
794     elif action in ('startapp', 'startproject'):
795         try:
796             name = args[1]
797@@ -1388,6 +1582,10 @@
798         action_mapping[action](addr, port, options.use_reloader, options.admin_media_path)
799     elif action == 'runfcgi':
800         action_mapping[action](args[1:])
801+    elif action == 'sqlinitialdata':
802+        print action_mapping[action](args[1:])
803+    elif action == 'sqlflush':
804+        print '\n'.join(action_mapping[action]())
805     else:
806         from django.db import models
807         validate(silent_success=True)
808
809Property changes on: tests/modeltests/fixtures
810___________________________________________________________________
811Name: svn:ignore
812   + *.pyc
813
814
815Index: tests/modeltests/fixtures/__init__.py
816===================================================================
817
818Property changes on: tests/modeltests/fixtures/fixtures
819___________________________________________________________________
820Name: svn:ignore
821   + *.pyc
822
823
824Index: tests/modeltests/fixtures/fixtures/fixture1.json
825===================================================================
826--- tests/modeltests/fixtures/fixtures/fixture1.json    (revision 0)
827+++ tests/modeltests/fixtures/fixtures/fixture1.json    (revision 0)
828@@ -0,0 +1,18 @@
829+[
830+    {
831+        "pk": "2",
832+        "model": "fixtures.article",
833+        "fields": {
834+            "headline": "Poker has no place on ESPN",
835+            "pub_date": "2006-06-16 12:00:00"
836+        }
837+    },
838+    {
839+        "pk": "3",
840+        "model": "fixtures.article",
841+        "fields": {
842+            "headline": "Time to reform copyright",
843+            "pub_date": "2006-06-16 13:00:00"
844+        }
845+    }
846+]
847\ No newline at end of file
848Index: tests/modeltests/fixtures/fixtures/fixture2.json
849===================================================================
850--- tests/modeltests/fixtures/fixtures/fixture2.json    (revision 0)
851+++ tests/modeltests/fixtures/fixtures/fixture2.json    (revision 0)
852@@ -0,0 +1,18 @@
853+[
854+    {
855+        "pk": "3",
856+        "model": "fixtures.article",
857+        "fields": {
858+            "headline": "Copyright is fine the way it is",
859+            "pub_date": "2006-06-16 14:00:00"
860+        }
861+    },
862+    {
863+        "pk": "4",
864+        "model": "fixtures.article",
865+        "fields": {
866+            "headline": "Django conquers world!",
867+            "pub_date": "2006-06-16 15:00:00"
868+        }
869+    }
870+]
871\ No newline at end of file
872Index: tests/modeltests/fixtures/fixtures/fixture2.xml
873===================================================================
874--- tests/modeltests/fixtures/fixtures/fixture2.xml     (revision 0)
875+++ tests/modeltests/fixtures/fixtures/fixture2.xml     (revision 0)
876@@ -0,0 +1,11 @@
877+<?xml version="1.0" encoding="utf-8"?>
878+<django-objects version="1.0">
879+    <object pk="2" model="fixtures.article">
880+        <field type="CharField" name="headline">Poker on TV is great!</field>
881+        <field type="DateTimeField" name="pub_date">2006-06-16 11:00:00</field>
882+    </object>
883+    <object pk="5" model="fixtures.article">
884+        <field type="CharField" name="headline">XML identified as leading cause of cancer</field>
885+        <field type="DateTimeField" name="pub_date">2006-06-16 16:00:00</field>
886+    </object>
887+</django-objects>
888\ No newline at end of file
889Index: tests/modeltests/fixtures/fixtures/fixture3.xml
890===================================================================
891--- tests/modeltests/fixtures/fixtures/fixture3.xml     (revision 0)
892+++ tests/modeltests/fixtures/fixtures/fixture3.xml     (revision 0)
893@@ -0,0 +1,11 @@
894+<?xml version="1.0" encoding="utf-8"?>
895+<django-objects version="1.0">
896+    <object pk="2" model="fixtures.article">
897+        <field type="CharField" name="headline">Poker on TV is great!</field>
898+        <field type="DateTimeField" name="pub_date">2006-06-16 11:00:00</field>
899+    </object>
900+    <object pk="5" model="fixtures.article">
901+        <field type="CharField" name="headline">XML identified as leading cause of cancer</field>
902+        <field type="DateTimeField" name="pub_date">2006-06-16 16:00:00</field>
903+    </object>
904+</django-objects>
905\ No newline at end of file
906Index: tests/modeltests/fixtures/models.py
907===================================================================
908--- tests/modeltests/fixtures/models.py (revision 0)
909+++ tests/modeltests/fixtures/models.py (revision 0)
910@@ -0,0 +1,84 @@
911+"""
912+39. Fixtures.
913+
914+Fixtures are a way of loading data into the database in bulk. Fixure data
915+can be stored in any serializable format (including JSON and XML). Fixtures
916+are identified by name, and are stored in either a directory named 'fixtures'
917+in the application directory, on in one of the directories named in the
918+FIXTURE_DIRS setting.
919+"""
920+
921+from django.db import models
922+
923+class Article(models.Model):
924+    headline = models.CharField(maxlength=100, default='Default headline')
925+    pub_date = models.DateTimeField()
926+
927+    def __str__(self):
928+        return self.headline
929+       
930+    class Meta:
931+        ordering = ('-pub_date', 'headline')
932+       
933+__test__ = {'API_TESTS': """
934+>>> from django.core import management
935+>>> from django.db.models import get_app
936+
937+# Syncdb introduces 1 initial data object from initial_data.json.
938+>>> Article.objects.all()
939+[<Article: Python program becomes self aware>]
940+
941+# Load fixture 1. Single JSON file, with two objects.
942+>>> management.load_data(['fixture1.json'], verbosity=0)
943+>>> Article.objects.all()
944+[<Article: Time to reform copyright>, <Article: Poker has no place on ESPN>, <Article: Python program becomes self aware>]
945+
946+# Load fixture 2. JSON file imported by default. Overwrites some existing objects
947+>>> management.load_data(['fixture2.json'], verbosity=0)
948+>>> Article.objects.all()
949+[<Article: Django conquers world!>, <Article: Copyright is fine the way it is>, <Article: Poker has no place on ESPN>, <Article: Python program becomes self aware>]
950+
951+# Load fixture 3, XML format.
952+>>> management.load_data(['fixture3.xml'], verbosity=0)
953+>>> Article.objects.all()
954+[<Article: XML identified as leading cause of cancer>, <Article: Django conquers world!>, <Article: Copyright is fine the way it is>, <Article: Poker on TV is great!>, <Article: Python program becomes self aware>]
955+
956+# Load a fixture that doesn't exist
957+>>> management.load_data(['unknown.json'], verbosity=0)
958+
959+# object list is unaffected
960+>>> Article.objects.all()
961+[<Article: XML identified as leading cause of cancer>, <Article: Django conquers world!>, <Article: Copyright is fine the way it is>, <Article: Poker on TV is great!>, <Article: Python program becomes self aware>]
962+
963+# Reset the database representation of this app. This will delete all data.
964+>>> management.flush(verbosity=0, interactive=False)
965+>>> Article.objects.all()
966+[<Article: Python program becomes self aware>]
967+
968+# Load fixture 1 again, using format discovery
969+>>> management.load_data(['fixture1'], verbosity=0)
970+>>> Article.objects.all()
971+[<Article: Time to reform copyright>, <Article: Poker has no place on ESPN>, <Article: Python program becomes self aware>]
972+
973+# Try to load fixture 2 using format discovery; this will fail
974+# because there are two fixture2's in the fixtures directory
975+>>> management.load_data(['fixture2'], verbosity=0) # doctest: +ELLIPSIS
976+Multiple fixtures named 'fixture2' in '.../fixtures'. Aborting.
977+
978+>>> Article.objects.all()
979+[<Article: Time to reform copyright>, <Article: Poker has no place on ESPN>, <Article: Python program becomes self aware>]
980+
981+# Dump the current contents of the database as a JSON fixture
982+>>> management.dump_data(['fixtures'], format='json')
983+[{"pk": "3", "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": "2", "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": "1", "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}]
984+"""}
985+
986+from django.test import TestCase
987+
988+class SampleTestCase(TestCase):
989+    fixtures = ['fixture1.json', 'fixture2.json']
990+       
991+    def testClassFixtures(self):
992+        "Check that test case has installed 4 fixture objects"
993+        self.assertEqual(Article.objects.count(), 4)
994+        self.assertEquals(str(Article.objects.all()), "[<Article: Django conquers world!>, <Article: Copyright is fine the way it is>, <Article: Poker has no place on ESPN>, <Article: Python program becomes self aware>]")
995Index: tests/modeltests/test_client/management.py
996===================================================================
997--- tests/modeltests/test_client/management.py  (revision 4431)
998+++ tests/modeltests/test_client/management.py  (working copy)
999@@ -1,10 +0,0 @@
1000-from django.dispatch import dispatcher
1001-from django.db.models import signals
1002-import models as test_client_app
1003-from django.contrib.auth.models import User
1004-
1005-def setup_test(app, created_models, verbosity):
1006-    # Create a user account for the login-based tests
1007-    User.objects.create_user('testclient','testclient@example.com', 'password')
1008-
1009-dispatcher.connect(setup_test, sender=test_client_app, signal=signals.post_syncdb)
1010
1011Property changes on: tests/modeltests/test_client/fixtures
1012___________________________________________________________________
1013Name: svn:ignore
1014   + *.pyc
1015
1016
1017Index: tests/modeltests/test_client/fixtures/testdata.json
1018===================================================================
1019--- tests/modeltests/test_client/fixtures/testdata.json (revision 0)
1020+++ tests/modeltests/test_client/fixtures/testdata.json (revision 0)
1021@@ -0,0 +1,20 @@
1022+[
1023+    {
1024+        "pk": "1",
1025+        "model": "auth.user",
1026+        "fields": {
1027+            "username": "testclient",
1028+            "first_name": "Test",
1029+            "last_name": "Client",
1030+            "is_active": true,
1031+            "is_superuser": false,
1032+            "is_staff": false,
1033+            "last_login": "2006-12-17 07:03:31",
1034+            "groups": [],
1035+            "user_permissions": [],
1036+            "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161",
1037+            "email": "testclient@example.com",
1038+            "date_joined": "2006-12-17 07:03:31"
1039+        }
1040+    }
1041+]
1042\ No newline at end of file
1043Index: tests/modeltests/test_client/models.py
1044===================================================================
1045--- tests/modeltests/test_client/models.py      (revision 4431)
1046+++ tests/modeltests/test_client/models.py      (working copy)
1047@@ -19,10 +19,11 @@
1048 rather than the HTML rendered to the end-user.
1049 
1050 """
1051-from django.test.client import Client
1052-import unittest
1053+from django.test import Client, TestCase
1054 
1055-class ClientTest(unittest.TestCase):
1056+class ClientTest(TestCase):
1057+    fixtures = ['testdata.json']
1058+   
1059     def setUp(self):
1060         "Set up test environment"
1061         self.client = Client()
1062Index: tests/urls.py
1063===================================================================
1064--- tests/urls.py       (revision 4431)
1065+++ tests/urls.py       (working copy)
1066@@ -6,5 +6,5 @@
1067 
1068     # Always provide the auth system login and logout views
1069     (r'^accounts/login/$', 'django.contrib.auth.views.login', {'template_name': 'login.html'}),
1070-    (r'^accounts/logout/$', 'django.contrib.auth.views.login'),
1071+    (r'^accounts/logout/$', 'django.contrib.auth.views.logout'),
1072 )