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:
Russell Keith-Magee 2007-03-01 13:11:08 +00:00
parent f54777406d
commit f2582eb972
27 changed files with 887 additions and 86 deletions

View File

@ -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 = ()

View File

@ -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)

View File

@ -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 File

@ -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.

View File

@ -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 File

@ -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 = {}

View File

@ -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 File

@ -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 File

@ -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.

View File

@ -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',

View File

@ -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 File

@ -0,0 +1,6 @@
"""
Django Unit Test and Doctest framework.
"""
from django.test.client import Client
from django.test.testcases import TestCase

View File

@ -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)

View File

@ -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
------

View File

@ -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 File

@ -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 File

@ -0,0 +1,2 @@

View File

@ -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 File

@ -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 File

@ -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 File

@ -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 File

@ -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 File

@ -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 File

@ -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 File

@ -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 File

@ -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()

View File

@ -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')),