summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorSimon Charette <charette.s@gmail.com>2013-11-04 23:11:51 -0500
committerSimon Charette <charette.s@gmail.com>2013-11-24 17:51:22 -0500
commiteb38257e5199ca06b8b32f82dd50f64fd6b0d98d (patch)
tree218b5face70be2baaffaabd3e5dac009a227727e /django
parent03bc0a8ac5b465a9a1ec88dedb0a2f4bd91dd547 (diff)
Fixed #21391 -- Allow model signals to lazily reference their senders.
Diffstat (limited to 'django')
-rw-r--r--django/core/management/validation.py30
-rw-r--r--django/db/models/signals.py68
2 files changed, 88 insertions, 10 deletions
diff --git a/django/core/management/validation.py b/django/core/management/validation.py
index 0a976d72a1..5bf9413c20 100644
--- a/django/core/management/validation.py
+++ b/django/core/management/validation.py
@@ -1,5 +1,6 @@
import collections
import sys
+import types
from django.conf import settings
from django.core.management.color import color_style
@@ -25,7 +26,7 @@ def get_validation_errors(outfile, app=None):
validates all models of all installed apps. Writes errors, if any, to outfile.
Returns number of errors.
"""
- from django.db import models, connection
+ from django.db import connection, models
from django.db.models.loading import get_app_errors
from django.db.models.deletion import SET_NULL, SET_DEFAULT
@@ -363,6 +364,8 @@ def get_validation_errors(outfile, app=None):
for it in opts.index_together:
validate_local_fields(e, opts, "index_together", it)
+ validate_model_signals(e)
+
return len(e.errors)
@@ -382,3 +385,28 @@ def validate_local_fields(e, opts, field_name, fields):
e.add(opts, '"%s" refers to %s. ManyToManyFields are not supported in %s.' % (field_name, f.name, field_name))
if f not in opts.local_fields:
e.add(opts, '"%s" refers to %s. This is not in the same model as the %s statement.' % (field_name, f.name, field_name))
+
+
+def validate_model_signals(e):
+ """Ensure lazily referenced model signals senders are installed."""
+ from django.db import models
+
+ for name in dir(models.signals):
+ obj = getattr(models.signals, name)
+ if isinstance(obj, models.signals.ModelSignal):
+ for reference, receivers in obj.unresolved_references.items():
+ for receiver, _, _ in receivers:
+ # The receiver is either a function or an instance of class
+ # defining a `__call__` method.
+ if isinstance(receiver, types.FunctionType):
+ description = "The `%s` function" % receiver.__name__
+ else:
+ description = "An instance of the `%s` class" % receiver.__class__.__name__
+ e.add(
+ receiver.__module__,
+ "%s was connected to the `%s` signal "
+ "with a lazy reference to the '%s' sender, "
+ "which has not been installed." % (
+ description, name, '.'.join(reference)
+ )
+ )
diff --git a/django/db/models/signals.py b/django/db/models/signals.py
index 6b7605839c..6b011c2099 100644
--- a/django/db/models/signals.py
+++ b/django/db/models/signals.py
@@ -1,20 +1,70 @@
+from collections import defaultdict
+
+from django.db.models.loading import get_model
from django.dispatch import Signal
+from django.utils import six
+
class_prepared = Signal(providing_args=["class"])
-pre_init = Signal(providing_args=["instance", "args", "kwargs"], use_caching=True)
-post_init = Signal(providing_args=["instance"], use_caching=True)
-pre_save = Signal(providing_args=["instance", "raw", "using", "update_fields"],
- use_caching=True)
-post_save = Signal(providing_args=["instance", "raw", "created", "using", "update_fields"], use_caching=True)
+class ModelSignal(Signal):
+ """
+ Signal subclass that allows the sender to be lazily specified as a string
+ of the `app_label.ModelName` form.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(ModelSignal, self).__init__(*args, **kwargs)
+ self.unresolved_references = defaultdict(list)
+ class_prepared.connect(self._resolve_references)
+
+ def _resolve_references(self, sender, **kwargs):
+ opts = sender._meta
+ reference = (opts.app_label, opts.object_name)
+ try:
+ receivers = self.unresolved_references.pop(reference)
+ except KeyError:
+ pass
+ else:
+ for receiver, weak, dispatch_uid in receivers:
+ super(ModelSignal, self).connect(
+ receiver, sender=sender, weak=weak, dispatch_uid=dispatch_uid
+ )
+
+ def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
+ if isinstance(sender, six.string_types):
+ try:
+ app_label, object_name = sender.split('.')
+ except ValueError:
+ raise ValueError(
+ "Specified sender must either be a model or a "
+ "model name of the 'app_label.ModelName' form."
+ )
+ sender = get_model(app_label, object_name, only_installed=False)
+ if sender is None:
+ reference = (app_label, object_name)
+ self.unresolved_references[reference].append(
+ (receiver, weak, dispatch_uid)
+ )
+ return
+ super(ModelSignal, self).connect(
+ receiver, sender=sender, weak=weak, dispatch_uid=dispatch_uid
+ )
-pre_delete = Signal(providing_args=["instance", "using"], use_caching=True)
-post_delete = Signal(providing_args=["instance", "using"], use_caching=True)
+pre_init = ModelSignal(providing_args=["instance", "args", "kwargs"], use_caching=True)
+post_init = ModelSignal(providing_args=["instance"], use_caching=True)
+
+pre_save = ModelSignal(providing_args=["instance", "raw", "using", "update_fields"],
+ use_caching=True)
+post_save = ModelSignal(providing_args=["instance", "raw", "created", "using", "update_fields"], use_caching=True)
+
+pre_delete = ModelSignal(providing_args=["instance", "using"], use_caching=True)
+post_delete = ModelSignal(providing_args=["instance", "using"], use_caching=True)
+
+m2m_changed = ModelSignal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"], use_caching=True)
pre_migrate = Signal(providing_args=["app", "create_models", "verbosity", "interactive", "db"])
pre_syncdb = pre_migrate
post_migrate = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive", "db"])
post_syncdb = post_migrate
-
-m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"], use_caching=True)