Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Fixes #2333 -- Added test fixtures framework.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@4659 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit f2582eb972303978a5fa115825a646a230624116 1 parent f547774
@freakboy3742 freakboy3742 authored
Showing with 887 additions and 86 deletions.
  1. +7 −0 django/conf/global_settings.py
  2. +264 −60 django/core/management.py
  3. +5 −0 django/core/serializers/__init__.py
  4. +1 −1  django/core/serializers/base.py
  5. +13 −0 django/db/backends/ado_mssql/base.py
  6. +2 −0  django/db/backends/dummy/base.py
  7. +30 −0 django/db/backends/mysql/base.py
  8. +14 −0 django/db/backends/oracle/base.py
  9. +62 −0 django/db/backends/postgresql/base.py
  10. +58 −0 django/db/backends/postgresql_psycopg2/base.py
  11. +18 −0 django/db/backends/sqlite3/base.py
  12. +6 −0 django/test/__init__.py
  13. +20 −0 django/test/testcases.py
  14. +131 −9 docs/django-admin.txt
  15. +24 −0 docs/settings.txt
  16. +49 −2 docs/testing.txt
  17. +2 −0  tests/modeltests/fixtures/__init__.py
  18. +18 −0 tests/modeltests/fixtures/fixtures/fixture1.json
  19. +18 −0 tests/modeltests/fixtures/fixtures/fixture2.json
  20. +11 −0 tests/modeltests/fixtures/fixtures/fixture2.xml
  21. +11 −0 tests/modeltests/fixtures/fixtures/fixture3.xml
  22. +10 −0 tests/modeltests/fixtures/fixtures/initial_data.json
  23. +88 −0 tests/modeltests/fixtures/models.py
  24. +20 −0 tests/modeltests/test_client/fixtures/testdata.json
  25. +0 −10 tests/modeltests/test_client/management.py
  26. +4 −3 tests/modeltests/test_client/models.py
  27. +1 −1  tests/urls.py
View
7 django/conf/global_settings.py
@@ -319,3 +319,10 @@
# The name of the database to use for testing purposes.
# If None, a name of 'test_' + DATABASE_NAME will be assumed
TEST_DATABASE_NAME = None
+
+############
+# FIXTURES #
+############
+
+# The list of directories to search for fixtures
+FIXTURE_DIRS = ()
View
324 django/core/management.py
@@ -68,6 +68,25 @@ def _get_table_list():
cursor = connection.cursor()
return get_introspection_module().get_table_list(cursor)
+def _get_sequence_list():
+ "Returns a list of information about all DB sequences for all models in all apps"
+ from django.db import models
+
+ apps = models.get_apps()
+ sequence_list = []
+
+ for app in apps:
+ for model in models.get_models(app):
+ for f in model._meta.fields:
+ if isinstance(f, models.AutoField):
+ sequence_list.append({'table':model._meta.db_table,'column':f.column,})
+ break # Only one AutoField is allowed per model, so don't bother continuing.
+
+ for f in model._meta.many_to_many:
+ sequence_list.append({'table':f.m2m_db_table(),'column':None,})
+
+ return sequence_list
+
# If the foreign key points to an AutoField, a PositiveIntegerField or a
# PositiveSmallIntegerField, the foreign key should be an IntegerField, not the
# referred field type. Otherwise, the foreign key should be the same type of
@@ -334,7 +353,15 @@ def get_sql_reset(app):
get_sql_reset.help_doc = "Prints the DROP TABLE SQL, then the CREATE TABLE SQL, for the given app name(s)."
get_sql_reset.args = APP_ARGS
-def get_sql_initial_data_for_model(model):
+def get_sql_flush():
+ "Returns a list of the SQL statements used to flush the database"
+ from django.db import backend
+ statements = backend.get_sql_flush(style, _get_table_list(), _get_sequence_list())
+ return statements
+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."
+get_sql_flush.args = ''
+
+def get_custom_sql_for_model(model):
from django.db import models
from django.conf import settings
@@ -361,8 +388,8 @@ def get_sql_initial_data_for_model(model):
return output
-def get_sql_initial_data(app):
- "Returns a list of the initial INSERT SQL statements for the given app."
+def get_custom_sql(app):
+ "Returns a list of the custom table modifying SQL statements for the given app."
from django.db.models import get_models
output = []
@@ -370,11 +397,17 @@ def get_sql_initial_data(app):
app_dir = os.path.normpath(os.path.join(os.path.dirname(app.__file__), 'sql'))
for model in app_models:
- output.extend(get_sql_initial_data_for_model(model))
+ output.extend(get_custom_sql_for_model(model))
return output
-get_sql_initial_data.help_doc = "Prints the initial INSERT SQL statements for the given app name(s)."
-get_sql_initial_data.args = APP_ARGS
+get_custom_sql.help_doc = "Prints the custom table modifying SQL statements for the given app name(s)."
+get_custom_sql.args = APP_ARGS
+
+def get_sql_initial_data(apps):
+ "Returns a list of the initial INSERT SQL statements for the given app."
+ return style.ERROR("This action has been renamed. Try './manage.py sqlcustom %s'." % ' '.join(apps and apps or ['app1', 'app2']))
+get_sql_initial_data.help_doc = "RENAMED: see 'sqlcustom'"
+get_sql_initial_data.args = ''
def get_sql_sequence_reset(app):
"Returns a list of the SQL statements to reset PostgreSQL sequences for the given app."
@@ -432,16 +465,26 @@ def get_sql_indexes_for_model(model):
def get_sql_all(app):
"Returns a list of CREATE TABLE SQL, initial-data inserts, and CREATE INDEX SQL for the given module."
- return get_sql_create(app) + get_sql_initial_data(app) + get_sql_indexes(app)
+ return get_sql_create(app) + get_custom_sql(app) + get_sql_indexes(app)
get_sql_all.help_doc = "Prints the CREATE TABLE, initial-data and CREATE INDEX SQL statements for the given model module name(s)."
get_sql_all.args = APP_ARGS
+def _emit_post_sync_signal(created_models, verbosity, interactive):
+ from django.db import models
+ from django.dispatch import dispatcher
+ # Emit the post_sync signal for every application.
+ for app in models.get_apps():
+ app_name = app.__name__.split('.')[-2]
+ if verbosity >= 2:
+ print "Running post-sync handlers for application", app_name
+ dispatcher.send(signal=models.signals.post_syncdb, sender=app,
+ app=app, created_models=created_models,
+ verbosity=verbosity, interactive=interactive)
+
def syncdb(verbosity=1, interactive=True):
"Creates the database tables for all apps in INSTALLED_APPS whose tables haven't already been created."
from django.db import connection, transaction, models, get_creation_module
- from django.db.models import signals
from django.conf import settings
- from django.dispatch import dispatcher
disable_termcolors()
@@ -503,27 +546,22 @@ def syncdb(verbosity=1, interactive=True):
# Send the post_syncdb signal, so individual apps can do whatever they need
# to do at this point.
- for app in models.get_apps():
- app_name = app.__name__.split('.')[-2]
- if verbosity >= 2:
- print "Running post-sync handlers for application", app_name
- dispatcher.send(signal=signals.post_syncdb, sender=app,
- app=app, created_models=created_models,
- verbosity=verbosity, interactive=interactive)
+ _emit_post_sync_signal(created_models, verbosity, interactive)
- # Install initial data for the app (but only if this is a model we've
- # just created)
+ # Install custom SQL for the app (but only if this
+ # is a model we've just created)
+ for app in models.get_apps():
for model in models.get_models(app):
if model in created_models:
- initial_sql = get_sql_initial_data_for_model(model)
- if initial_sql:
+ custom_sql = get_custom_sql_for_model(model)
+ if custom_sql:
if verbosity >= 1:
- print "Installing initial data for %s.%s model" % (app_name, model._meta.object_name)
+ print "Installing custom SQL for %s.%s model" % (app_name, model._meta.object_name)
try:
- for sql in initial_sql:
+ for sql in custom_sql:
cursor.execute(sql)
except Exception, e:
- sys.stderr.write("Failed to install initial SQL data for %s.%s model: %s" % \
+ sys.stderr.write("Failed to install custom SQL for %s.%s model: %s" % \
(app_name, model._meta.object_name, e))
transaction.rollback_unless_managed()
else:
@@ -548,7 +586,10 @@ def syncdb(verbosity=1, interactive=True):
else:
transaction.commit_unless_managed()
-syncdb.args = ''
+ # Install the 'initialdata' fixture, using format discovery
+ load_data(['initial_data'], verbosity=verbosity)
+syncdb.help_doc = "Create the database tables for all apps in INSTALLED_APPS whose tables haven't already been created."
+syncdb.args = '[--verbosity] [--interactive]'
def get_admin_index(app):
"Returns admin-index template snippet (in list form) for the given app."
@@ -601,36 +642,6 @@ def diffsettings():
print '\n'.join(output)
diffsettings.args = ""
-def install(app):
- "Executes the equivalent of 'get_sql_all' in the current database."
- from django.db import connection, transaction
-
- app_name = app.__name__.split('.')[-2]
-
- disable_termcolors()
-
- # First, try validating the models.
- _check_for_validation_errors(app)
-
- sql_list = get_sql_all(app)
-
- try:
- cursor = connection.cursor()
- for sql in sql_list:
- cursor.execute(sql)
- except Exception, e:
- sys.stderr.write(style.ERROR("""Error: %s couldn't be installed. Possible reasons:
- * The database isn't running or isn't configured correctly.
- * At least one of the database tables already exists.
- * The SQL was invalid.
-Hint: Look at the output of 'django-admin.py sqlall %s'. That's the SQL this command wasn't able to run.
-The full error: """ % (app_name, app_name)) + style.ERROR_OUTPUT(str(e)) + '\n')
- transaction.rollback_unless_managed()
- sys.exit(1)
- transaction.commit_unless_managed()
-install.help_doc = "Executes ``sqlall`` for the given app(s) in the current database."
-install.args = APP_ARGS
-
def reset(app, interactive=True):
"Executes the equivalent of 'get_sql_reset' in the current database."
from django.db import connection, transaction
@@ -672,7 +683,68 @@ def reset(app, interactive=True):
else:
print "Reset cancelled."
reset.help_doc = "Executes ``sqlreset`` for the given app(s) in the current database."
-reset.args = APP_ARGS
+reset.args = '[--interactive]' + APP_ARGS
+
+def flush(verbosity=1, interactive=True):
+ "Returns all tables in the database to the same state they were in immediately after syncdb."
+ from django.conf import settings
+ from django.db import connection, transaction, models
+ from django.dispatch import dispatcher
+
+ disable_termcolors()
+
+ # First, try validating the models.
+ _check_for_validation_errors()
+
+ # Import the 'management' module within each installed app, to register
+ # dispatcher events.
+ for app_name in settings.INSTALLED_APPS:
+ try:
+ __import__(app_name + '.management', {}, {}, [''])
+ except ImportError:
+ pass
+
+ sql_list = get_sql_flush()
+
+ if interactive:
+ confirm = raw_input("""
+You have requested a flush of the database.
+This will IRREVERSIBLY DESTROY all data currently in the database,
+and return each table to the state it was in after syncdb.
+Are you sure you want to do this?
+
+Type 'yes' to continue, or 'no' to cancel: """)
+ else:
+ confirm = 'yes'
+
+ if confirm == 'yes':
+ try:
+ cursor = connection.cursor()
+ for sql in sql_list:
+ cursor.execute(sql)
+ except Exception, e:
+ sys.stderr.write(style.ERROR("""Error: Database %s couldn't be flushed. Possible reasons:
+ * The database isn't running or isn't configured correctly.
+ * At least one of the expected database tables doesn't exist.
+ * The SQL was invalid.
+Hint: Look at the output of 'django-admin.py sqlflush'. That's the SQL this command wasn't able to run.
+The full error: """ % settings.DATABASE_NAME + style.ERROR_OUTPUT(str(e)) + '\n'))
+ transaction.rollback_unless_managed()
+ sys.exit(1)
+ transaction.commit_unless_managed()
+
+ # Emit the post sync signal. This allows individual
+ # applications to respond as if the database had been
+ # sync'd from scratch.
+ _emit_post_sync_signal(models.get_models(), verbosity, interactive)
+
+ # Reinstall the initial_data fixture
+ load_data(['initial_data'], verbosity=verbosity)
+
+ else:
+ print "Flush cancelled."
+flush.help_doc = "Executes ``sqlflush`` on the current database."
+flush.args = '[--verbosity] [--interactive]'
def _start_helper(app_or_project, name, directory, other_name=''):
other = {'project': 'app', 'app': 'project'}[app_or_project]
@@ -755,7 +827,7 @@ def inspectdb():
yield "# * Make sure each model has one field with primary_key=True"
yield "# Feel free to rename the models, but don't rename db_table values or field names."
yield "#"
- yield "# Also note: You'll have to insert the output of 'django-admin.py sqlinitialdata [appname]'"
+ yield "# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]'"
yield "# into your database."
yield ''
yield 'from django.db import models'
@@ -1251,6 +1323,124 @@ def test(app_labels, verbosity=1):
test.help_doc = 'Runs the test suite for the specified applications, or the entire site if no apps are specified'
test.args = '[--verbosity] ' + APP_ARGS
+def load_data(fixture_labels, verbosity=1):
+ "Installs the provided fixture file(s) as data in the database."
+ from django.db.models import get_apps
+ from django.core import serializers
+ from django.db import connection, transaction
+ from django.conf import settings
+ import sys
+
+ # Keep a count of the installed objects and fixtures
+ count = [0,0]
+
+ humanize = lambda dirname: dirname and "'%s'" % dirname or 'absolute path'
+
+ # Get a cursor (even though we don't need one yet). This has
+ # the side effect of initializing the test database (if
+ # it isn't already initialized).
+ cursor = connection.cursor()
+
+ # Start transaction management. All fixtures are installed in a
+ # single transaction to ensure that all references are resolved.
+ transaction.commit_unless_managed()
+ transaction.enter_transaction_management()
+ transaction.managed(True)
+
+ app_fixtures = [os.path.join(os.path.dirname(app.__file__),'fixtures') for app in get_apps()]
+ for fixture_label in fixture_labels:
+ if verbosity > 0:
+ print "Loading '%s' fixtures..." % fixture_label
+ for fixture_dir in app_fixtures + list(settings.FIXTURE_DIRS) + ['']:
+ if verbosity > 1:
+ print "Checking %s for fixtures..." % humanize(fixture_dir)
+ try:
+ fixture_name, format = fixture_label.rsplit('.', 1)
+ formats = [format]
+ except ValueError:
+ fixture_name = fixture_label
+ formats = serializers.get_serializer_formats()
+
+ label_found = False
+ for format in formats:
+ serializer = serializers.get_serializer(format)
+ if verbosity > 1:
+ print "Trying %s for %s fixture '%s'..." % \
+ (humanize(fixture_dir), format, fixture_name)
+ try:
+ full_path = os.path.join(fixture_dir, '.'.join([fixture_name, format]))
+ fixture = open(full_path, 'r')
+ if label_found:
+ fixture.close()
+ print style.ERROR("Multiple fixtures named '%s' in %s. Aborting." %
+ (fixture_name, humanize(fixture_dir)))
+ transaction.rollback()
+ transaction.leave_transaction_management()
+ return
+ else:
+ count[1] += 1
+ if verbosity > 0:
+ print "Installing %s fixture '%s' from %s." % \
+ (format, fixture_name, humanize(fixture_dir))
+ try:
+ objects = serializers.deserialize(format, fixture)
+ for obj in objects:
+ count[0] += 1
+ obj.save()
+ label_found = True
+ except Exception, e:
+ fixture.close()
+ sys.stderr.write(
+ style.ERROR("Problem installing fixture '%s': %s\n" %
+ (full_path, str(e))))
+ transaction.rollback()
+ transaction.leave_transaction_management()
+ return
+ fixture.close()
+ except:
+ if verbosity > 1:
+ print "No %s fixture '%s' in %s." % \
+ (format, fixture_name, humanize(fixture_dir))
+ if count[0] == 0:
+ if verbosity > 0:
+ print "No fixtures found."
+ else:
+ if verbosity > 0:
+ print "Installed %d object(s) from %d fixture(s)" % tuple(count)
+ transaction.commit()
+ transaction.leave_transaction_management()
+
+load_data.help_doc = 'Installs the named fixture(s) in the database'
+load_data.args = "[--verbosity] fixture, fixture, ..."
+
+def dump_data(app_labels, format='json'):
+ "Output the current contents of the database as a fixture of the given format"
+ from django.db.models import get_app, get_apps, get_models
+ from django.core import serializers
+
+ if len(app_labels) == 0:
+ app_list = get_apps()
+ else:
+ app_list = [get_app(app_label) for app_label in app_labels]
+
+ # Check that the serialization format exists; this is a shortcut to
+ # avoid collating all the objects and _then_ failing.
+ try:
+ serializers.get_serializer(format)
+ except KeyError:
+ sys.stderr.write(style.ERROR("Unknown serialization format: %s\n" % format))
+
+ objects = []
+ for app in app_list:
+ for model in get_models(app):
+ objects.extend(model.objects.all())
+ try:
+ print serializers.serialize(format, objects)
+ except Exception, e:
+ sys.stderr.write(style.ERROR("Unable to serialize database: %s\n" % e))
+dump_data.help_doc = 'Output the contents of the database as a fixture of the given format'
+dump_data.args = '[--format]' + APP_ARGS
+
# Utilities for command-line script
DEFAULT_ACTION_MAPPING = {
@@ -1258,8 +1448,10 @@ def test(app_labels, verbosity=1):
'createcachetable' : createcachetable,
'dbshell': dbshell,
'diffsettings': diffsettings,
+ 'dumpdata': dump_data,
+ 'flush': flush,
'inspectdb': inspectdb,
- 'install': install,
+ 'loaddata': load_data,
'reset': reset,
'runfcgi': runfcgi,
'runserver': runserver,
@@ -1267,6 +1459,8 @@ def test(app_labels, verbosity=1):
'sql': get_sql_create,
'sqlall': get_sql_all,
'sqlclear': get_sql_delete,
+ 'sqlcustom': get_custom_sql,
+ 'sqlflush': get_sql_flush,
'sqlindexes': get_sql_indexes,
'sqlinitialdata': get_sql_initial_data,
'sqlreset': get_sql_reset,
@@ -1283,7 +1477,6 @@ def test(app_labels, verbosity=1):
'createcachetable',
'dbshell',
'diffsettings',
- 'install',
'reset',
'sqlindexes',
'syncdb',
@@ -1330,6 +1523,8 @@ def execute_from_command_line(action_mapping=DEFAULT_ACTION_MAPPING, argv=None):
help='Tells Django to NOT prompt the user for input of any kind.')
parser.add_option('--noreload', action='store_false', dest='use_reloader', default=True,
help='Tells Django to NOT use the auto-reloader when running the development server.')
+ parser.add_option('--format', default='json', dest='format',
+ help='Specifies the output serialization format for fixtures')
parser.add_option('--verbosity', action='store', dest='verbosity', default='1',
type='choice', choices=['0', '1', '2'],
help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'),
@@ -1363,7 +1558,7 @@ def execute_from_command_line(action_mapping=DEFAULT_ACTION_MAPPING, argv=None):
action_mapping[action](options.plain is True)
elif action in ('validate', 'diffsettings', 'dbshell'):
action_mapping[action]()
- elif action == 'syncdb':
+ elif action in ('flush', 'syncdb'):
action_mapping[action](int(options.verbosity), options.interactive)
elif action == 'inspectdb':
try:
@@ -1377,11 +1572,16 @@ def execute_from_command_line(action_mapping=DEFAULT_ACTION_MAPPING, argv=None):
action_mapping[action](args[1])
except IndexError:
parser.print_usage_and_exit()
- elif action == 'test':
+ elif action in ('test', 'loaddata'):
try:
action_mapping[action](args[1:], int(options.verbosity))
except IndexError:
parser.print_usage_and_exit()
+ elif action == 'dumpdata':
+ try:
+ action_mapping[action](args[1:], options.format)
+ except IndexError:
+ parser.print_usage_and_exit()
elif action in ('startapp', 'startproject'):
try:
name = args[1]
@@ -1400,6 +1600,10 @@ def execute_from_command_line(action_mapping=DEFAULT_ACTION_MAPPING, argv=None):
action_mapping[action](addr, port, options.use_reloader, options.admin_media_path)
elif action == 'runfcgi':
action_mapping[action](args[1:])
+ elif action == 'sqlinitialdata':
+ print action_mapping[action](args[1:])
+ elif action == 'sqlflush':
+ print '\n'.join(action_mapping[action]())
else:
from django.db import models
validate(silent_success=True)
View
5 django/core/serializers/__init__.py
@@ -40,6 +40,11 @@ def get_serializer(format):
if not _serializers:
_load_serializers()
return _serializers[format].Serializer
+
+def get_serializer_formats():
+ if not _serializers:
+ _load_serializers()
+ return _serializers.keys()
def get_deserializer(format):
if not _serializers:
View
2  django/core/serializers/base.py
@@ -141,7 +141,7 @@ def next(self):
class DeserializedObject(object):
"""
- A deserialzed model.
+ A deserialized model.
Basically a container for holding the pre-saved deserialized data along
with the many-to-many data saved with the object.
View
13 django/db/backends/ado_mssql/base.py
@@ -137,6 +137,19 @@ def get_drop_foreignkey_sql():
def get_pk_default_value():
return "DEFAULT"
+def get_sql_flush(sql_styler, full_table_list):
+ """Return a list of SQL statements required to remove all data from
+ all tables in the database (without actually removing the tables
+ themselves) and put the database in an empty 'initial' state
+ """
+ # Return a list of 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements
+ # TODO - SQL not actually tested against ADO MSSQL yet!
+ # TODO - autoincrement indices reset required? See other get_sql_flush() implementations
+ sql_list = ['%s %s;' % \
+ (sql_styler.SQL_KEYWORD('TRUNCATE'),
+ sql_styler.SQL_FIELD(quote_name(table))
+ ) for table in full_table_list]
+
OPERATOR_MAPPING = {
'exact': '= %s',
'iexact': 'LIKE %s',
View
2  django/db/backends/dummy/base.py
@@ -39,4 +39,6 @@ def close(self):
get_deferrable_sql = complain
get_fulltext_search_sql = complain
get_drop_foreignkey_sql = complain
+get_sql_flush = complain
+
OPERATOR_MAPPING = {}
View
30 django/db/backends/mysql/base.py
@@ -186,6 +186,36 @@ def get_drop_foreignkey_sql():
def get_pk_default_value():
return "DEFAULT"
+def get_sql_flush(style, tables, sequences):
+ """Return a list of SQL statements required to remove all data from
+ all tables in the database (without actually removing the tables
+ themselves) and put the database in an empty 'initial' state
+
+ """
+ # NB: The generated SQL below is specific to MySQL
+ # 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements
+ # to clear all tables of all data
+ if tables:
+ sql = ['SET FOREIGN_KEY_CHECKS = 0;'] + \
+ ['%s %s;' % \
+ (style.SQL_KEYWORD('TRUNCATE'),
+ style.SQL_FIELD(quote_name(table))
+ ) for table in tables] + \
+ ['SET FOREIGN_KEY_CHECKS = 1;']
+
+ # 'ALTER TABLE table AUTO_INCREMENT = 1;'... style SQL statements
+ # to reset sequence indices
+ sql.extend(["%s %s %s %s %s;" % \
+ (style.SQL_KEYWORD('ALTER'),
+ style.SQL_KEYWORD('TABLE'),
+ style.SQL_TABLE(quote_name(sequence['table'])),
+ style.SQL_KEYWORD('AUTO_INCREMENT'),
+ style.SQL_FIELD('= 1'),
+ ) for sequence in sequences])
+ return sql
+ else:
+ return []
+
OPERATOR_MAPPING = {
'exact': '= %s',
'iexact': 'LIKE %s',
View
14 django/db/backends/oracle/base.py
@@ -120,6 +120,20 @@ def get_drop_foreignkey_sql():
def get_pk_default_value():
return "DEFAULT"
+def get_sql_flush(style, tables, sequences):
+ """Return a list of SQL statements required to remove all data from
+ all tables in the database (without actually removing the tables
+ themselves) and put the database in an empty 'initial' state
+ """
+ # Return a list of 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements
+ # TODO - SQL not actually tested against Oracle yet!
+ # TODO - autoincrement indices reset required? See other get_sql_flush() implementations
+ sql = ['%s %s;' % \
+ (style.SQL_KEYWORD('TRUNCATE'),
+ style.SQL_FIELD(quote_name(table))
+ ) for table in tables]
+
+
OPERATOR_MAPPING = {
'exact': '= %s',
'iexact': 'LIKE %s',
View
62 django/db/backends/postgresql/base.py
@@ -52,6 +52,8 @@ def __getattr__(self, attr):
else:
return getattr(self.cursor, attr)
+postgres_version = None
+
class DatabaseWrapper(local):
def __init__(self, **kwargs):
self.connection = None
@@ -81,6 +83,10 @@ def cursor(self):
if set_tz:
cursor.execute("SET TIME ZONE %s", [settings.TIME_ZONE])
cursor = UnicodeCursorWrapper(cursor, settings.DEFAULT_CHARSET)
+ global postgres_version
+ if not postgres_version:
+ cursor.execute("SELECT version()")
+ postgres_version = [int(val) for val in cursor.dictfetchone()['version'].split()[1].split('.')]
if settings.DEBUG:
return util.CursorDebugWrapper(cursor, self)
return cursor
@@ -151,6 +157,62 @@ def get_drop_foreignkey_sql():
def get_pk_default_value():
return "DEFAULT"
+def get_sql_flush(style, tables, sequences):
+ """Return a list of SQL statements required to remove all data from
+ all tables in the database (without actually removing the tables
+ themselves) and put the database in an empty 'initial' state
+
+ """
+ if tables:
+ if postgres_version[0] >= 8 and postgres_version[1] >= 1:
+ # Postgres 8.1+ can do 'TRUNCATE x, y, z...;'. In fact, it *has to* in order to be able to
+ # truncate tables referenced by a foreign key in any other table. The result is a
+ # single SQL TRUNCATE statement.
+ sql = ['%s %s;' % \
+ (style.SQL_KEYWORD('TRUNCATE'),
+ style.SQL_FIELD(', '.join(quote_name(table) for table in tables))
+ )]
+ else:
+ # Older versions of Postgres can't do TRUNCATE in a single call, so they must use
+ # a simple delete.
+ sql = ['%s %s %s;' % \
+ (style.SQL_KEYWORD('DELETE'),
+ style.SQL_KEYWORD('FROM'),
+ style.SQL_FIELD(quote_name(table))
+ ) for table in tables]
+
+ # 'ALTER SEQUENCE sequence_name RESTART WITH 1;'... style SQL statements
+ # to reset sequence indices
+ for sequence_info in sequences:
+ table_name = sequence_info['table']
+ column_name = sequence_info['column']
+ if column_name and len(column_name)>0:
+ # sequence name in this case will be <table>_<column>_seq
+ sql.append("%s %s %s %s %s %s;" % \
+ (style.SQL_KEYWORD('ALTER'),
+ style.SQL_KEYWORD('SEQUENCE'),
+ style.SQL_FIELD('%s_%s_seq' % (table_name, column_name)),
+ style.SQL_KEYWORD('RESTART'),
+ style.SQL_KEYWORD('WITH'),
+ style.SQL_FIELD('1')
+ )
+ )
+ else:
+ # sequence name in this case will be <table>_id_seq
+ sql.append("%s %s %s %s %s %s;" % \
+ (style.SQL_KEYWORD('ALTER'),
+ style.SQL_KEYWORD('SEQUENCE'),
+ style.SQL_FIELD('%s_id_seq' % table_name),
+ style.SQL_KEYWORD('RESTART'),
+ style.SQL_KEYWORD('WITH'),
+ style.SQL_FIELD('1')
+ )
+ )
+ return sql
+ else:
+ return []
+
+
# Register these custom typecasts, because Django expects dates/times to be
# in Python's native (standard-library) datetime/time format, whereas psycopg
# use mx.DateTime by default.
View
58 django/db/backends/postgresql_psycopg2/base.py
@@ -20,6 +20,8 @@
# Import copy of _thread_local.py from Python 2.4
from django.utils._threading_local import local
+postgres_version = None
+
class DatabaseWrapper(local):
def __init__(self, **kwargs):
self.connection = None
@@ -49,6 +51,10 @@ def cursor(self):
cursor.tzinfo_factory = None
if set_tz:
cursor.execute("SET TIME ZONE %s", [settings.TIME_ZONE])
+ global postgres_version
+ if not postgres_version:
+ cursor.execute("SELECT version()")
+ postgres_version = [int(val) for val in cursor.dictfetchone()['version'].split()[1].split('.')]
if settings.DEBUG:
return util.CursorDebugWrapper(cursor, self)
return cursor
@@ -111,6 +117,58 @@ def get_drop_foreignkey_sql():
def get_pk_default_value():
return "DEFAULT"
+def get_sql_flush(style, tables, sequences):
+ """Return a list of SQL statements required to remove all data from
+ all tables in the database (without actually removing the tables
+ themselves) and put the database in an empty 'initial' state
+ """
+ if tables:
+ if postgres_version[0] >= 8 and postgres_version[1] >= 1:
+ # Postgres 8.1+ can do 'TRUNCATE x, y, z...;'. In fact, it *has to* in order to be able to
+ # truncate tables referenced by a foreign key in any other table. The result is a
+ # single SQL TRUNCATE statement
+ sql = ['%s %s;' % \
+ (style.SQL_KEYWORD('TRUNCATE'),
+ style.SQL_FIELD(', '.join(quote_name(table) for table in tables))
+ )]
+ else:
+ sql = ['%s %s %s;' % \
+ (style.SQL_KEYWORD('DELETE'),
+ style.SQL_KEYWORD('FROM'),
+ style.SQL_FIELD(quote_name(table))
+ ) for table in tables]
+
+ # 'ALTER SEQUENCE sequence_name RESTART WITH 1;'... style SQL statements
+ # to reset sequence indices
+ for sequence in sequences:
+ table_name = sequence['table']
+ column_name = sequence['column']
+ if column_name and len(column_name) > 0:
+ # sequence name in this case will be <table>_<column>_seq
+ sql.append("%s %s %s %s %s %s;" % \
+ (style.SQL_KEYWORD('ALTER'),
+ style.SQL_KEYWORD('SEQUENCE'),
+ style.SQL_FIELD('%s_%s_seq' % (table_name, column_name)),
+ style.SQL_KEYWORD('RESTART'),
+ style.SQL_KEYWORD('WITH'),
+ style.SQL_FIELD('1')
+ )
+ )
+ else:
+ # sequence name in this case will be <table>_id_seq
+ sql.append("%s %s %s %s %s %s;" % \
+ (style.SQL_KEYWORD('ALTER'),
+ style.SQL_KEYWORD('SEQUENCE'),
+ style.SQL_FIELD('%s_id_seq' % table_name),
+ style.SQL_KEYWORD('RESTART'),
+ style.SQL_KEYWORD('WITH'),
+ style.SQL_FIELD('1')
+ )
+ )
+ return sql
+ else:
+ return []
+
OPERATOR_MAPPING = {
'exact': '= %s',
'iexact': 'ILIKE %s',
View
18 django/db/backends/sqlite3/base.py
@@ -151,6 +151,24 @@ def get_drop_foreignkey_sql():
def get_pk_default_value():
return "NULL"
+def get_sql_flush(style, tables, sequences):
+ """Return a list of SQL statements required to remove all data from
+ all tables in the database (without actually removing the tables
+ themselves) and put the database in an empty 'initial' state
+
+ """
+ # NB: The generated SQL below is specific to SQLite
+ # Note: The DELETE FROM... SQL generated below works for SQLite databases
+ # because constraints don't exist
+ sql = ['%s %s %s;' % \
+ (style.SQL_KEYWORD('DELETE'),
+ style.SQL_KEYWORD('FROM'),
+ style.SQL_FIELD(quote_name(table))
+ ) for table in tables]
+ # Note: No requirement for reset of auto-incremented indices (cf. other
+ # get_sql_flush() implementations). Just return SQL at this point
+ return sql
+
def _sqlite_date_trunc(lookup_type, dt):
try:
dt = util.typecast_timestamp(dt)
View
6 django/test/__init__.py
@@ -0,0 +1,6 @@
+"""
+Django Unit Test and Doctest framework.
+"""
+
+from django.test.client import Client
+from django.test.testcases import TestCase
View
20 django/test/testcases.py
@@ -1,5 +1,7 @@
import re, doctest, unittest
from django.db import transaction
+from django.core import management
+from django.db.models import get_apps
normalize_long_ints = lambda s: re.sub(r'(?<![\w])(\d+)L(?![\w])', '\\1', s)
@@ -28,3 +30,21 @@ def report_unexpected_exception(self, out, test, example, exc_info):
from django.db import transaction
transaction.rollback_unless_managed()
+class TestCase(unittest.TestCase):
+ def install_fixtures(self):
+ """If the Test Case class has a 'fixtures' member, clear the database and
+ install the named fixtures at the start of each test.
+
+ """
+ management.flush(verbosity=0, interactive=False)
+ if hasattr(self, 'fixtures'):
+ management.load_data(self.fixtures, verbosity=0)
+
+ def run(self, result=None):
+ """Wrapper around default run method so that user-defined Test Cases
+ automatically call install_fixtures without having to include a call to
+ super().
+
+ """
+ self.install_fixtures()
+ super(TestCase, self).run(result)
View
140 docs/django-admin.txt
@@ -97,6 +97,33 @@ example, the default settings don't define ``ROOT_URLCONF``, so
Note that Django's default settings live in ``django/conf/global_settings.py``,
if you're ever curious to see the full list of defaults.
+dumpdata [appname appname ...]
+------------------------------
+
+**New in Django development version**
+
+Output to standard output all data in the database associated with the named
+application(s).
+
+By default, the database will be dumped in JSON format. If you want the output
+to be in another format, use the ``--format`` option (e.g., ``format=xml``).
+You may specify any Django serialization backend (including any user specified
+serialization backends named in the ``SERIALIZATION_MODULES`` setting).
+
+If no application name is provided, all installed applications will be dumped.
+
+The output of ``dumpdata`` can be used as input for ``loaddata``.
+
+flush
+-----
+
+**New in Django development version**
+
+Return the database to the state it was in immediately after syncdb was
+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.
+
inspectdb
---------
@@ -141,8 +168,78 @@ only works in PostgreSQL and with certain types of MySQL tables.
install [appname appname ...]
-----------------------------
+**Removed in Django development version**
+
Executes the equivalent of ``sqlall`` for the given appnames.
+loaddata [fixture fixture ...]
+------------------------------
+
+**New in Django development version**
+
+Searches for and loads the contents of the named fixture into the database.
+
+A *Fixture* is a collection of files that contain the serialized contents of
+the database. Each fixture has a unique name; however, the files that
+comprise the fixture can be distributed over multiple directories, in
+multiple applications.
+
+Django will search in three locations for fixtures:
+
+ 1. In the ``fixtures`` directory of every installed application
+ 2. In any directory named in the ``FIXTURE_DIRS`` setting
+ 3. In the literal path named by the fixture
+
+Django will load any and all fixtures it finds in these locations that match
+the provided fixture names.
+
+If the named fixture has a file extension, only fixtures of that type
+will be loaded. For example::
+
+ django-admin.py loaddata mydata.json
+
+would only load JSON fixtures called ``mydata``. The fixture extension
+must correspond to the registered name of a serializer (e.g., ``json`` or
+``xml``).
+
+If you omit the extension, Django will search all available fixture types
+for a matching fixture. For example::
+
+ django-admin.py loaddata mydata
+
+would look for any fixture of any fixture type called ``mydata``. If a fixture
+directory contained ``mydata.json``, that fixture would be loaded
+as a JSON fixture. However, if two fixtures with the same name but different
+fixture type are discovered (for example, if ``mydata.json`` and
+``mydata.xml`` were found in the same fixture directory), fixture
+installation will be aborted, and any data installed in the call to
+``loaddata`` will be removed from the database.
+
+The fixtures that are named can include directory components. These
+directories will be inluded in the search path. For example::
+
+ django-admin.py loaddata foo/bar/mydata.json
+
+would search ``<appname>/fixtures/foo/bar/mydata.json`` for each installed
+application, ``<dirname>/foo/bar/mydata.json`` for each directory in
+``FIXTURE_DIRS``, and the literal path ``foo/bar/mydata.json``.
+
+Note that the order in which fixture files are processed is undefined. However,
+all fixture data is installed as a single transaction, so data in
+one fixture can reference data in another fixture. If the database backend
+supports row-level constraints, these constraints will be checked at the
+end of the transaction.
+
+.. admonition:: MySQL and Fixtures
+
+ Unfortunately, MySQL isn't capable of completely supporting all the
+ features of Django fixtures. If you use MyISAM tables, MySQL doesn't
+ support transactions or constraints, so you won't get a rollback if
+ multiple transaction files are found, or validation of fixture data.
+ If you use InnoDB tables, you won't be able to have any forward
+ references in your data files - MySQL doesn't provide a mechanism to
+ defer checking of row constraints until a transaction is committed.
+
reset [appname appname ...]
---------------------------
Executes the equivalent of ``sqlreset`` for the given appnames.
@@ -250,15 +347,12 @@ sqlclear [appname appname ...]
Prints the DROP TABLE SQL statements for the given appnames.
-sqlindexes [appname appname ...]
-----------------------------------------
+sqlcustom [appname appname ...]
+-------------------------------
-Prints the CREATE INDEX SQL statements for the given appnames.
-
-sqlinitialdata [appname appname ...]
---------------------------------------------
+**New in Django development version**
-Prints the initial INSERT SQL statements for the given appnames.
+Prints the custom SQL statements for the given appnames.
For each model in each specified app, this command looks for the file
``<appname>/sql/<modelname>.sql``, where ``<appname>`` is the given appname and
@@ -269,11 +363,23 @@ command.
Each of the SQL files, if given, is expected to contain valid SQL. The SQL
files are piped directly into the database after all of the models'
-table-creation statements have been executed. Use this SQL hook to populate
-tables with any necessary initial records, SQL functions or test data.
+table-creation statements have been executed. Use this SQL hook to make any
+table modifications, or insert any SQL functions into the database.
Note that the order in which the SQL files are processed is undefined.
+sqlindexes [appname appname ...]
+----------------------------------------
+
+Prints the CREATE INDEX SQL statements for the given appnames.
+
+sqlinitialdata [appname appname ...]
+--------------------------------------------
+
+**Removed in Django development version**
+
+This method has been renamed ``sqlcustom`` in the development version of Django.
+
sqlreset [appname appname ...]
--------------------------------------
@@ -313,6 +419,10 @@ this command to install the default apps.
If you're installing the ``django.contrib.auth`` application, ``syncdb`` will
give you the option of creating a superuser immediately.
+``syncdb`` will also search for and install any fixture named ``initial_data``.
+See the documentation for ``loaddata`` for details on the specification of
+fixture data files.
+
test
----
@@ -362,6 +472,18 @@ setting the Python path for you.
.. _import search path: http://diveintopython.org/getting_to_know_python/everything_is_an_object.html
+--format
+---------
+
+**New in Django development version**
+
+Example usage::
+
+ django-admin.py dumpdata --format=xml
+
+Specifies the output format that will be used. The name provided must be the name
+of a registered serializer.
+
--help
------
View
24 docs/settings.txt
@@ -422,6 +422,19 @@ Subject-line prefix for e-mail messages sent with ``django.core.mail.mail_admins
or ``django.core.mail.mail_managers``. You'll probably want to include the
trailing space.
+FIXTURE_DIRS
+-------------
+
+**New in Django development version**
+
+Default: ``()`` (Empty tuple)
+
+List of locations of the fixture data files, in search order. Note that
+these paths should use Unix-style forward slashes, even on Windows. See
+`Testing Django Applications`_.
+
+.. _Testing Django Applications: ../testing/
+
IGNORABLE_404_ENDS
------------------
@@ -653,6 +666,17 @@ link). This is only used if ``CommonMiddleware`` is installed (see the
`middleware docs`_). See also ``IGNORABLE_404_STARTS``,
``IGNORABLE_404_ENDS`` and the section on `error reporting via e-mail`_
+SERIALIZATION_MODULES
+---------------------
+
+Default: Not defined.
+
+A dictionary of modules containing serializer definitions (provided as
+strings), keyed by a string identifier for that serialization type. For
+example, to define a YAML serializer, use::
+
+ SERIALIZATION_MODULES = { 'yaml' : 'path.to.yaml_serializer' }
+
SERVER_EMAIL
------------
View
51 docs/testing.txt
@@ -198,7 +198,6 @@ used as test conditions.
.. _Twill: http://twill.idyll.org/
.. _Selenium: http://www.openqa.org/selenium/
-
Making requests
~~~~~~~~~~~~~~~
@@ -357,7 +356,55 @@ The following is a simple unit test using the Test Client::
Fixtures
--------
-Feature still to come...
+A test case for a database-backed website isn't much use if there isn't any
+data in the database. To make it easy to put test data into the database,
+Django provides a fixtures framework.
+
+A *Fixture* is a collection of files that contain the serialized contents of
+the database. Each fixture has a unique name; however, the files that
+comprise the fixture can be distributed over multiple directories, in
+multiple applications.
+
+.. note::
+ If you have synchronized a Django project, you have already experienced
+ the use of one fixture -- the ``initial_data`` fixture. Every time you
+ synchronize the database, Django installs the ``initial_data`` fixture.
+ This provides a mechanism to populate a new database with any initial
+ data (such as a default set of categories). Fixtures with other names
+ can be installed manually using ``django-admin.py loaddata``.
+
+
+However, for the purposes of unit testing, each test must be able to
+guarantee the contents of the database at the start of each and every
+test. To do this, Django provides a TestCase baseclass that can integrate
+with fixtures.
+
+Moving from a normal unittest TestCase to a Django TestCase is easy - just
+change the base class of your test, and define a list of fixtures
+to be used. For example, the test case from `Writing unittests`_ would
+look like::
+
+ from django.test import TestCase
+ from myapp.models import Animal
+
+ class AnimalTestCase(TestCase):
+ fixtures = ['mammals.json', 'birds']
+
+ def setUp(self):
+ # test definitions as before
+
+At the start of each test vase, before ``setUp()`` is run, Django will
+flush the database, returning the database the state it was in directly
+after ``syncdb`` was called. Then, all the named fixtures are installed.
+In this example, any JSON fixture called ``mammals``, and any fixture
+named ``birds`` will be installed. See the documentation on
+`loading fixtures`_ for more details on defining and installing fixtures.
+
+.. _`loading fixtures`: ../django-admin/#loaddata-fixture-fixture
+
+This flush/load procedure is repeated for each test in the test case, so you
+can be certain that the outcome of a test will not be affected by
+another test, or the order of test execution.
Running tests
=============
View
2  tests/modeltests/fixtures/__init__.py
@@ -0,0 +1,2 @@
+
+
View
18 tests/modeltests/fixtures/fixtures/fixture1.json
@@ -0,0 +1,18 @@
+[
+ {
+ "pk": "2",
+ "model": "fixtures.article",
+ "fields": {
+ "headline": "Poker has no place on ESPN",
+ "pub_date": "2006-06-16 12:00:00"
+ }
+ },
+ {
+ "pk": "3",
+ "model": "fixtures.article",
+ "fields": {
+ "headline": "Time to reform copyright",
+ "pub_date": "2006-06-16 13:00:00"
+ }
+ }
+]
View
18 tests/modeltests/fixtures/fixtures/fixture2.json
@@ -0,0 +1,18 @@
+[
+ {
+ "pk": "3",
+ "model": "fixtures.article",
+ "fields": {
+ "headline": "Copyright is fine the way it is",
+ "pub_date": "2006-06-16 14:00:00"
+ }
+ },
+ {
+ "pk": "4",
+ "model": "fixtures.article",
+ "fields": {
+ "headline": "Django conquers world!",
+ "pub_date": "2006-06-16 15:00:00"
+ }
+ }
+]
View
11 tests/modeltests/fixtures/fixtures/fixture2.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+ <object pk="2" model="fixtures.article">
+ <field type="CharField" name="headline">Poker on TV is great!</field>
+ <field type="DateTimeField" name="pub_date">2006-06-16 11:00:00</field>
+ </object>
+ <object pk="5" model="fixtures.article">
+ <field type="CharField" name="headline">XML identified as leading cause of cancer</field>
+ <field type="DateTimeField" name="pub_date">2006-06-16 16:00:00</field>
+ </object>
+</django-objects>
View
11 tests/modeltests/fixtures/fixtures/fixture3.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+ <object pk="2" model="fixtures.article">
+ <field type="CharField" name="headline">Poker on TV is great!</field>
+ <field type="DateTimeField" name="pub_date">2006-06-16 11:00:00</field>
+ </object>
+ <object pk="5" model="fixtures.article">
+ <field type="CharField" name="headline">XML identified as leading cause of cancer</field>
+ <field type="DateTimeField" name="pub_date">2006-06-16 16:00:00</field>
+ </object>
+</django-objects>
View
10 tests/modeltests/fixtures/fixtures/initial_data.json
@@ -0,0 +1,10 @@
+[
+ {
+ "pk": "1",
+ "model": "fixtures.article",
+ "fields": {
+ "headline": "Python program becomes self aware",
+ "pub_date": "2006-06-16 11:00:00"
+ }
+ }
+]
View
88 tests/modeltests/fixtures/models.py
@@ -0,0 +1,88 @@
+"""
+39. Fixtures.
+
+Fixtures are a way of loading data into the database in bulk. Fixure data
+can be stored in any serializable format (including JSON and XML). Fixtures
+are identified by name, and are stored in either a directory named 'fixtures'
+in the application directory, on in one of the directories named in the
+FIXTURE_DIRS setting.
+"""
+
+from django.db import models
+
+class Article(models.Model):
+ headline = models.CharField(maxlength=100, default='Default headline')
+ pub_date = models.DateTimeField()
+
+ def __str__(self):
+ return self.headline
+
+ class Meta:
+ ordering = ('-pub_date', 'headline')
+
+__test__ = {'API_TESTS': """
+>>> from django.core import management
+>>> from django.db.models import get_app
+
+# Reset the database representation of this app.
+# This will return the database to a clean initial state.
+>>> management.flush(verbosity=0, interactive=False)
+
+# Syncdb introduces 1 initial data object from initial_data.json.
+>>> Article.objects.all()
+[<Article: Python program becomes self aware>]
+
+# Load fixture 1. Single JSON file, with two objects.
+>>> management.load_data(['fixture1.json'], verbosity=0)
+>>> Article.objects.all()
+[<Article: Time to reform copyright>, <Article: Poker has no place on ESPN>, <Article: Python program becomes self aware>]
+
+# Load fixture 2. JSON file imported by default. Overwrites some existing objects
+>>> management.load_data(['fixture2.json'], verbosity=0)
+>>> 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>]
+
+# Load fixture 3, XML format.
+>>> management.load_data(['fixture3.xml'], verbosity=0)
+>>> Article.objects.all()
+[<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>]
+
+# Load a fixture that doesn't exist
+>>> management.load_data(['unknown.json'], verbosity=0)
+
+# object list is unaffected
+>>> Article.objects.all()
+[<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>]
+
+# Reset the database representation of this app. This will delete all data.
+>>> management.flush(verbosity=0, interactive=False)
+>>> Article.objects.all()
+[<Article: Python program becomes self aware>]
+
+# Load fixture 1 again, using format discovery
+>>> management.load_data(['fixture1'], verbosity=0)
+>>> Article.objects.all()
+[<Article: Time to reform copyright>, <Article: Poker has no place on ESPN>, <Article: Python program becomes self aware>]
+
+# Try to load fixture 2 using format discovery; this will fail
+# because there are two fixture2's in the fixtures directory
+>>> management.load_data(['fixture2'], verbosity=0) # doctest: +ELLIPSIS
+Multiple fixtures named 'fixture2' in '.../fixtures'. Aborting.
+
+>>> Article.objects.all()
+[<Article: Time to reform copyright>, <Article: Poker has no place on ESPN>, <Article: Python program becomes self aware>]
+
+# Dump the current contents of the database as a JSON fixture
+>>> management.dump_data(['fixtures'], format='json')
+[{"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"}}]
+"""}
+
+from django.test import TestCase
+
+class SampleTestCase(TestCase):
+ fixtures = ['fixture1.json', 'fixture2.json']
+
+ def testClassFixtures(self):
+ "Check that test case has installed 4 fixture objects"
+ self.assertEqual(Article.objects.count(), 4)
+ 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>]")
View
20 tests/modeltests/test_client/fixtures/testdata.json
@@ -0,0 +1,20 @@
+[
+ {
+ "pk": "1",
+ "model": "auth.user",
+ "fields": {
+ "username": "testclient",
+ "first_name": "Test",
+ "last_name": "Client",
+ "is_active": true,
+ "is_superuser": false,
+ "is_staff": false,
+ "last_login": "2006-12-17 07:03:31",
+ "groups": [],
+ "user_permissions": [],
+ "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161",
+ "email": "testclient@example.com",
+ "date_joined": "2006-12-17 07:03:31"
+ }
+ }
+]
View
10 tests/modeltests/test_client/management.py
@@ -1,10 +0,0 @@
-from django.dispatch import dispatcher
-from django.db.models import signals
-import models as test_client_app
-from django.contrib.auth.models import User
-
-def setup_test(app, created_models, verbosity):
- # Create a user account for the login-based tests
- User.objects.create_user('testclient','testclient@example.com', 'password')
-
-dispatcher.connect(setup_test, sender=test_client_app, signal=signals.post_syncdb)
View
7 tests/modeltests/test_client/models.py
@@ -19,10 +19,11 @@
rather than the HTML rendered to the end-user.
"""
-from django.test.client import Client
-import unittest
+from django.test import Client, TestCase
-class ClientTest(unittest.TestCase):
+class ClientTest(TestCase):
+ fixtures = ['testdata.json']
+
def setUp(self):
"Set up test environment"
self.client = Client()
View
2  tests/urls.py
@@ -6,7 +6,7 @@
# Always provide the auth system login and logout views
(r'^accounts/login/$', 'django.contrib.auth.views.login', {'template_name': 'login.html'}),
- (r'^accounts/logout/$', 'django.contrib.auth.views.login'),
+ (r'^accounts/logout/$', 'django.contrib.auth.views.logout'),
# test urlconf for {% url %} template tag
(r'^url_tag/', include('regressiontests.templates.urls')),
Please sign in to comment.
Something went wrong with that request. Please try again.