Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #11398 - Added a pre_syncdb signal

  • Loading branch information...
commit 3de1288042f2dc1cb8a2b36ae0fc4d9e0beb6494 1 parent 11b0653
Donald Stufft authored May 17, 2013
2  django/core/management/commands/flush.py
@@ -20,7 +20,7 @@ class Command(NoArgsCommand):
20 20
             default=DEFAULT_DB_ALIAS, help='Nominates a database to flush. '
21 21
                 'Defaults to the "default" database.'),
22 22
         make_option('--no-initial-data', action='store_false', dest='load_initial_data', default=True,
23  
- 		            help='Tells Django not to load any initial data after database synchronization.'),
  23
+            help='Tells Django not to load any initial data after database synchronization.'),
24 24
     )
25 25
     help = ('Returns the database to the state it was in immediately after '
26 26
            'syncdb was executed. This means that all data will be removed '
6  django/core/management/commands/syncdb.py
... ...
@@ -1,11 +1,12 @@
1 1
 from optparse import make_option
  2
+import itertools
2 3
 import traceback
3 4
 
4 5
 from django.conf import settings
5 6
 from django.core.management import call_command
6 7
 from django.core.management.base import NoArgsCommand
7 8
 from django.core.management.color import no_style
8  
-from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal
  9
+from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal, emit_pre_sync_signal
9 10
 from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS
10 11
 from django.utils.datastructures import SortedDict
11 12
 from django.utils.importlib import import_module
@@ -80,6 +81,9 @@ def model_installed(model):
80 81
             for app_name, model_list in all_models
81 82
         )
82 83
 
  84
+        create_models = set([x for x in itertools.chain(*manifest.values())])
  85
+        emit_pre_sync_signal(create_models, verbosity, interactive, db)
  86
+
83 87
         # Create the tables for each model
84 88
         if verbosity >= 1:
85 89
             self.stdout.write("Creating tables ...\n")
14  django/core/management/sql.py
@@ -137,6 +137,7 @@ def sql_indexes(app, style, connection):
137 137
         output.extend(connection.creation.sql_indexes_for_model(model, style))
138 138
     return output
139 139
 
  140
+
140 141
 def sql_destroy_indexes(app, style, connection):
141 142
     "Returns a list of the DROP INDEX SQL statements for all models in the given app."
142 143
     output = []
@@ -191,6 +192,19 @@ def custom_sql_for_model(model, style, connection):
191 192
     return output
192 193
 
193 194
 
  195
+def emit_pre_sync_signal(create_models, verbosity, interactive, db):
  196
+    # Emit the pre_sync signal for every application.
  197
+    for app in models.get_apps():
  198
+        app_name = app.__name__.split('.')[-2]
  199
+        if verbosity >= 2:
  200
+            print("Running pre-sync handlers for application %s" % app_name)
  201
+        models.signals.pre_syncdb.send(sender=app, app=app,
  202
+                                       create_models=create_models,
  203
+                                       verbosity=verbosity,
  204
+                                       interactive=interactive,
  205
+                                       db=db)
  206
+
  207
+
194 208
 def emit_post_sync_signal(created_models, verbosity, interactive, db):
195 209
     # Emit the post_sync signal for every application.
196 210
     for app in models.get_apps():
1  django/db/models/signals.py
@@ -12,6 +12,7 @@
12 12
 pre_delete = Signal(providing_args=["instance", "using"], use_caching=True)
13 13
 post_delete = Signal(providing_args=["instance", "using"], use_caching=True)
14 14
 
  15
+pre_syncdb = Signal(providing_args=["app", "create_models", "verbosity", "interactive", "db"])
15 16
 post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive", "db"], use_caching=True)
16 17
 
17 18
 m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"], use_caching=True)
47  docs/ref/signals.txt
@@ -360,6 +360,53 @@ Management signals
360 360
 
361 361
 Signals sent by :doc:`django-admin </ref/django-admin>`.
362 362
 
  363
+pre_syncdb
  364
+----------
  365
+
  366
+.. data:: django.db.models.signals.pre_syncdb
  367
+   :module:
  368
+
  369
+Sent by the :djadmin:`syncdb` command before it starts to install an
  370
+application.
  371
+
  372
+Any handlers that listen to this signal need to be written in a particular
  373
+place: a ``management`` module in one of your :setting:`INSTALLED_APPS`. If
  374
+handlers are registered anywhere else they may not be loaded by
  375
+:djadmin:`syncdb`.
  376
+
  377
+Arguments sent with this signal:
  378
+
  379
+``sender``
  380
+    The ``models`` module that was just installed. That is, if
  381
+    :djadmin:`syncdb` just installed an app called ``"foo.bar.myapp"``,
  382
+    ``sender`` will be the ``foo.bar.myapp.models`` module.
  383
+
  384
+``app``
  385
+    Same as ``sender``.
  386
+
  387
+``create_models``
  388
+    A list of the model classes from any app which :djadmin:`syncdb` plans to
  389
+    create.
  390
+
  391
+
  392
+``verbosity``
  393
+    Indicates how much information manage.py is printing on screen. See
  394
+    the :djadminopt:`--verbosity` flag for details.
  395
+
  396
+    Functions which listen for :data:`pre_syncdb` should adjust what they
  397
+    output to the screen based on the value of this argument.
  398
+
  399
+``interactive``
  400
+    If ``interactive`` is ``True``, it's safe to prompt the user to input
  401
+    things on the command line. If ``interactive`` is ``False``, functions
  402
+    which listen for this signal should not try to prompt for anything.
  403
+
  404
+    For example, the :mod:`django.contrib.auth` app only prompts to create a
  405
+    superuser when ``interactive`` is ``True``.
  406
+
  407
+``db``
  408
+    The alias of database on which a command will operate.
  409
+
363 410
 post_syncdb
364 411
 -----------
365 412
 
0  tests/syncdb_signals/__init__.py
No changes.
11  tests/syncdb_signals/models.py
... ...
@@ -0,0 +1,11 @@
  1
+# from django.db import models
  2
+
  3
+
  4
+# class Author(models.Model):
  5
+#     name = models.CharField(max_length=100)
  6
+
  7
+#     class Meta:
  8
+#         ordering = ['name']
  9
+
  10
+#     def __unicode__(self):
  11
+#         return self.name
79  tests/syncdb_signals/tests.py
... ...
@@ -0,0 +1,79 @@
  1
+from django.db import connections
  2
+from django.db.models import signals
  3
+from django.test import TestCase
  4
+from django.core import management
  5
+from django.utils import six
  6
+
  7
+from shared_models import models
  8
+
  9
+
  10
+PRE_SYNCDB_ARGS = ['app', 'create_models', 'verbosity', 'interactive', 'db']
  11
+SYNCDB_DATABASE = 'default'
  12
+SYNCDB_VERBOSITY = 1
  13
+SYNCDB_INTERACTIVE = False
  14
+
  15
+
  16
+class PreSyncdbReceiver(object):
  17
+    def __init__(self):
  18
+        self.call_counter = 0
  19
+        self.call_args = None
  20
+
  21
+    def __call__(self, signal, sender, **kwargs):
  22
+        self.call_counter = self.call_counter + 1
  23
+        self.call_args = kwargs
  24
+
  25
+
  26
+class OneTimeReceiver(object):
  27
+    """
  28
+    Special receiver for handle the fact that test runner calls syncdb for
  29
+    several databases and several times for some of them.
  30
+    """
  31
+
  32
+    def __init__(self):
  33
+        self.call_counter = 0
  34
+        self.call_args = None
  35
+        self.tables = None  # list of tables at the time of the call
  36
+
  37
+    def __call__(self, signal, sender, **kwargs):
  38
+        # Although test runner calls syncdb for several databases,
  39
+        # testing for only one of them is quite sufficient.
  40
+        if kwargs['db'] == SYNCDB_DATABASE:
  41
+            self.call_counter = self.call_counter + 1
  42
+            self.call_args = kwargs
  43
+            connection = connections[SYNCDB_DATABASE]
  44
+            self.tables = connection.introspection.table_names()
  45
+            # we need to test only one call of syncdb
  46
+            signals.pre_syncdb.disconnect(pre_syncdb_receiver, sender=models)
  47
+
  48
+
  49
+# We connect receiver here and not in unit test code because we need to
  50
+# connect receiver before test runner creates database.  That is, sequence of
  51
+# actions would be:
  52
+#
  53
+#   1. Test runner imports this module.
  54
+#   2. We connect receiver.
  55
+#   3. Test runner calls syncdb for create default database.
  56
+#   4. Test runner execute our unit test code.
  57
+pre_syncdb_receiver = OneTimeReceiver()
  58
+signals.pre_syncdb.connect(pre_syncdb_receiver, sender=models)
  59
+
  60
+
  61
+class SyncdbSignalTests(TestCase):
  62
+    def test_pre_syncdb_call_time(self):
  63
+        self.assertEqual(pre_syncdb_receiver.call_counter, 1)
  64
+        self.assertFalse(pre_syncdb_receiver.tables)
  65
+
  66
+    def test_pre_syncdb_args(self):
  67
+        r = PreSyncdbReceiver()
  68
+        signals.pre_syncdb.connect(r, sender=models)
  69
+        management.call_command('syncdb', database=SYNCDB_DATABASE,
  70
+            verbosity=SYNCDB_VERBOSITY, interactive=SYNCDB_INTERACTIVE,
  71
+            load_initial_data=False, stdout=six.StringIO())
  72
+
  73
+        args = r.call_args
  74
+        self.assertEqual(r.call_counter, 1)
  75
+        self.assertEqual(set(args), set(PRE_SYNCDB_ARGS))
  76
+        self.assertEqual(args['app'], models)
  77
+        self.assertEqual(args['verbosity'], SYNCDB_VERBOSITY)
  78
+        self.assertEqual(args['interactive'], SYNCDB_INTERACTIVE)
  79
+        self.assertEqual(args['db'], 'default')

0 notes on commit 3de1288

Please sign in to comment.
Something went wrong with that request. Please try again.