mirror of
https://github.com/django/django.git
synced 2025-10-24 22:26:08 +00:00
Fixed #21391 -- Allow model signals to lazily reference their senders.
This commit is contained in:
@@ -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)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
pre_delete = Signal(providing_args=["instance", "using"], use_caching=True)
|
||||
post_delete = Signal(providing_args=["instance", "using"], use_caching=True)
|
||||
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_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)
|
||||
|
||||
Reference in New Issue
Block a user