Fixed #11398 - Added a pre_syncdb signal

This commit is contained in:
Donald Stufft 2013-05-17 18:18:35 -04:00
parent 11b06532f7
commit 3de1288042
8 changed files with 158 additions and 2 deletions

View File

@ -20,7 +20,7 @@ class Command(NoArgsCommand):
default=DEFAULT_DB_ALIAS, help='Nominates a database to flush. ' default=DEFAULT_DB_ALIAS, help='Nominates a database to flush. '
'Defaults to the "default" database.'), 'Defaults to the "default" database.'),
make_option('--no-initial-data', action='store_false', dest='load_initial_data', default=True, make_option('--no-initial-data', action='store_false', dest='load_initial_data', default=True,
help='Tells Django not to load any initial data after database synchronization.'), help='Tells Django not to load any initial data after database synchronization.'),
) )
help = ('Returns the database to the state it was in immediately after ' help = ('Returns the database to the state it was in immediately after '
'syncdb was executed. This means that all data will be removed ' 'syncdb was executed. This means that all data will be removed '

View File

@ -1,11 +1,12 @@
from optparse import make_option from optparse import make_option
import itertools
import traceback import traceback
from django.conf import settings from django.conf import settings
from django.core.management import call_command from django.core.management import call_command
from django.core.management.base import NoArgsCommand from django.core.management.base import NoArgsCommand
from django.core.management.color import no_style from django.core.management.color import no_style
from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal, emit_pre_sync_signal
from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from django.utils.importlib import import_module from django.utils.importlib import import_module
@ -80,6 +81,9 @@ class Command(NoArgsCommand):
for app_name, model_list in all_models for app_name, model_list in all_models
) )
create_models = set([x for x in itertools.chain(*manifest.values())])
emit_pre_sync_signal(create_models, verbosity, interactive, db)
# Create the tables for each model # Create the tables for each model
if verbosity >= 1: if verbosity >= 1:
self.stdout.write("Creating tables ...\n") self.stdout.write("Creating tables ...\n")

View File

@ -137,6 +137,7 @@ def sql_indexes(app, style, connection):
output.extend(connection.creation.sql_indexes_for_model(model, style)) output.extend(connection.creation.sql_indexes_for_model(model, style))
return output return output
def sql_destroy_indexes(app, style, connection): def sql_destroy_indexes(app, style, connection):
"Returns a list of the DROP INDEX SQL statements for all models in the given app." "Returns a list of the DROP INDEX SQL statements for all models in the given app."
output = [] output = []
@ -191,6 +192,19 @@ def custom_sql_for_model(model, style, connection):
return output return output
def emit_pre_sync_signal(create_models, verbosity, interactive, db):
# Emit the pre_sync signal for every application.
for app in models.get_apps():
app_name = app.__name__.split('.')[-2]
if verbosity >= 2:
print("Running pre-sync handlers for application %s" % app_name)
models.signals.pre_syncdb.send(sender=app, app=app,
create_models=create_models,
verbosity=verbosity,
interactive=interactive,
db=db)
def emit_post_sync_signal(created_models, verbosity, interactive, db): def emit_post_sync_signal(created_models, verbosity, interactive, db):
# Emit the post_sync signal for every application. # Emit the post_sync signal for every application.
for app in models.get_apps(): for app in models.get_apps():

View File

@ -12,6 +12,7 @@ post_save = Signal(providing_args=["instance", "raw", "created", "using", "updat
pre_delete = Signal(providing_args=["instance", "using"], use_caching=True) pre_delete = Signal(providing_args=["instance", "using"], use_caching=True)
post_delete = Signal(providing_args=["instance", "using"], use_caching=True) post_delete = Signal(providing_args=["instance", "using"], use_caching=True)
pre_syncdb = Signal(providing_args=["app", "create_models", "verbosity", "interactive", "db"])
post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive", "db"], use_caching=True) post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive", "db"], use_caching=True)
m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"], use_caching=True) m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"], use_caching=True)

View File

@ -360,6 +360,53 @@ Management signals
Signals sent by :doc:`django-admin </ref/django-admin>`. Signals sent by :doc:`django-admin </ref/django-admin>`.
pre_syncdb
----------
.. data:: django.db.models.signals.pre_syncdb
:module:
Sent by the :djadmin:`syncdb` command before it starts to install an
application.
Any handlers that listen to this signal need to be written in a particular
place: a ``management`` module in one of your :setting:`INSTALLED_APPS`. If
handlers are registered anywhere else they may not be loaded by
:djadmin:`syncdb`.
Arguments sent with this signal:
``sender``
The ``models`` module that was just installed. That is, if
:djadmin:`syncdb` just installed an app called ``"foo.bar.myapp"``,
``sender`` will be the ``foo.bar.myapp.models`` module.
``app``
Same as ``sender``.
``create_models``
A list of the model classes from any app which :djadmin:`syncdb` plans to
create.
``verbosity``
Indicates how much information manage.py is printing on screen. See
the :djadminopt:`--verbosity` flag for details.
Functions which listen for :data:`pre_syncdb` should adjust what they
output to the screen based on the value of this argument.
``interactive``
If ``interactive`` is ``True``, it's safe to prompt the user to input
things on the command line. If ``interactive`` is ``False``, functions
which listen for this signal should not try to prompt for anything.
For example, the :mod:`django.contrib.auth` app only prompts to create a
superuser when ``interactive`` is ``True``.
``db``
The alias of database on which a command will operate.
post_syncdb post_syncdb
----------- -----------

View File

View File

@ -0,0 +1,11 @@
# from django.db import models
# class Author(models.Model):
# name = models.CharField(max_length=100)
# class Meta:
# ordering = ['name']
# def __unicode__(self):
# return self.name

View File

@ -0,0 +1,79 @@
from django.db import connections
from django.db.models import signals
from django.test import TestCase
from django.core import management
from django.utils import six
from shared_models import models
PRE_SYNCDB_ARGS = ['app', 'create_models', 'verbosity', 'interactive', 'db']
SYNCDB_DATABASE = 'default'
SYNCDB_VERBOSITY = 1
SYNCDB_INTERACTIVE = False
class PreSyncdbReceiver(object):
def __init__(self):
self.call_counter = 0
self.call_args = None
def __call__(self, signal, sender, **kwargs):
self.call_counter = self.call_counter + 1
self.call_args = kwargs
class OneTimeReceiver(object):
"""
Special receiver for handle the fact that test runner calls syncdb for
several databases and several times for some of them.
"""
def __init__(self):
self.call_counter = 0
self.call_args = None
self.tables = None # list of tables at the time of the call
def __call__(self, signal, sender, **kwargs):
# Although test runner calls syncdb for several databases,
# testing for only one of them is quite sufficient.
if kwargs['db'] == SYNCDB_DATABASE:
self.call_counter = self.call_counter + 1
self.call_args = kwargs
connection = connections[SYNCDB_DATABASE]
self.tables = connection.introspection.table_names()
# we need to test only one call of syncdb
signals.pre_syncdb.disconnect(pre_syncdb_receiver, sender=models)
# We connect receiver here and not in unit test code because we need to
# connect receiver before test runner creates database. That is, sequence of
# actions would be:
#
# 1. Test runner imports this module.
# 2. We connect receiver.
# 3. Test runner calls syncdb for create default database.
# 4. Test runner execute our unit test code.
pre_syncdb_receiver = OneTimeReceiver()
signals.pre_syncdb.connect(pre_syncdb_receiver, sender=models)
class SyncdbSignalTests(TestCase):
def test_pre_syncdb_call_time(self):
self.assertEqual(pre_syncdb_receiver.call_counter, 1)
self.assertFalse(pre_syncdb_receiver.tables)
def test_pre_syncdb_args(self):
r = PreSyncdbReceiver()
signals.pre_syncdb.connect(r, sender=models)
management.call_command('syncdb', database=SYNCDB_DATABASE,
verbosity=SYNCDB_VERBOSITY, interactive=SYNCDB_INTERACTIVE,
load_initial_data=False, stdout=six.StringIO())
args = r.call_args
self.assertEqual(r.call_counter, 1)
self.assertEqual(set(args), set(PRE_SYNCDB_ARGS))
self.assertEqual(args['app'], models)
self.assertEqual(args['verbosity'], SYNCDB_VERBOSITY)
self.assertEqual(args['interactive'], SYNCDB_INTERACTIVE)
self.assertEqual(args['db'], 'default')