Fixes #2333 -- Added test fixtures framework.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@4659 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
f54777406d
commit
f2582eb972
|
@ -319,3 +319,10 @@ TEST_RUNNER = 'django.test.simple.run_tests'
|
|||
# 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 = ()
|
||||
|
|
|
@ -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 @@ The full error: """ % (app_name, app_name)) + style.ERROR_OUTPUT(str(e)) + '\n')
|
|||
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 @@ DEFAULT_ACTION_MAPPING = {
|
|||
'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 @@ DEFAULT_ACTION_MAPPING = {
|
|||
'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 @@ NO_SQL_TRANSACTION = (
|
|||
'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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -141,7 +141,7 @@ class Deserializer(object):
|
|||
|
||||
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.
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -39,4 +39,6 @@ get_random_function_sql = complain
|
|||
get_deferrable_sql = complain
|
||||
get_fulltext_search_sql = complain
|
||||
get_drop_foreignkey_sql = complain
|
||||
get_sql_flush = complain
|
||||
|
||||
OPERATOR_MAPPING = {}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -52,6 +52,8 @@ class UnicodeCursorWrapper(object):
|
|||
else:
|
||||
return getattr(self.cursor, attr)
|
||||
|
||||
postgres_version = None
|
||||
|
||||
class DatabaseWrapper(local):
|
||||
def __init__(self, **kwargs):
|
||||
self.connection = None
|
||||
|
@ -81,6 +83,10 @@ class DatabaseWrapper(local):
|
|||
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.
|
||||
|
|
|
@ -20,6 +20,8 @@ except ImportError:
|
|||
# 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 @@ class DatabaseWrapper(local):
|
|||
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',
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
"""
|
||||
Django Unit Test and Doctest framework.
|
||||
"""
|
||||
|
||||
from django.test.client import Client
|
||||
from django.test.testcases import TestCase
|
|
@ -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 @@ class DocTestRunner(doctest.DocTestRunner):
|
|||
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)
|
||||
|
|
|
@ -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.
|
||||
**New in Django development version**
|
||||
|
||||
sqlinitialdata [appname appname ...]
|
||||
--------------------------------------------
|
||||
|
||||
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
|
||||
------
|
||||
|
||||
|
|
|
@ -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
|
||||
------------
|
||||
|
||||
|
|
|
@ -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
|
||||
=============
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,10 @@
|
|||
[
|
||||
{
|
||||
"pk": "1",
|
||||
"model": "fixtures.article",
|
||||
"fields": {
|
||||
"headline": "Python program becomes self aware",
|
||||
"pub_date": "2006-06-16 11:00:00"
|
||||
}
|
||||
}
|
||||
]
|
|
@ -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>]")
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
|
@ -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)
|
|
@ -19,10 +19,11 @@ testing against the contexts and templates produced by a view,
|
|||
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()
|
||||
|
|
|
@ -6,7 +6,7 @@ urlpatterns = patterns('',
|
|||
|
||||
# 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')),
|
||||
|
|
Loading…
Reference in New Issue