Code

Ticket #2333: fixtures.diff

File fixtures.diff, 34.9 KB (added by russellm, 8 years ago)

Phase 3, version 1 of the testing framework - fixtures

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