Instantly share code, notes, and snippets.
Created
December 11, 2014 09:24
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save tokibito/6ae36040b4388b6c0217 to your computer and use it in GitHub Desktop.
django diff
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- C:\Users\tokibito\Desktop\src\Django-1.6.8\django\db\models\fields\related.py Thu Oct 23 01:48:04 2014 | |
+++ C:\Users\tokibito\Desktop\src\Django-1.7.1\django\db\models\fields\related.py Thu Oct 23 01:54:58 2014 | |
@@ -1,24 +1,28 @@ | |
+from __future__ import unicode_literals | |
+ | |
from operator import attrgetter | |
-from django.db import connection, connections, router | |
-from django.db.backends import util | |
-from django.db.models import signals, get_model | |
+from django.apps import apps | |
+from django.core import checks | |
+from django.db import connection, connections, router, transaction | |
+from django.db.backends import utils | |
+from django.db.models import signals, Q | |
+from django.db.models.deletion import SET_NULL, SET_DEFAULT, CASCADE | |
from django.db.models.fields import (AutoField, Field, IntegerField, | |
PositiveIntegerField, PositiveSmallIntegerField, FieldDoesNotExist) | |
+from django.db.models.lookups import IsNull | |
from django.db.models.related import RelatedObject, PathInfo | |
from django.db.models.query import QuerySet | |
-from django.db.models.deletion import CASCADE | |
-from django.utils.encoding import smart_text | |
+from django.db.models.sql.datastructures import Col | |
+from django.utils.encoding import force_text, smart_text | |
from django.utils import six | |
-from django.utils.deprecation import RenameMethodsBase | |
+from django.utils.deprecation import RenameMethodsBase, RemovedInDjango18Warning | |
from django.utils.translation import ugettext_lazy as _ | |
from django.utils.functional import curry, cached_property | |
from django.core import exceptions | |
from django import forms | |
RECURSIVE_RELATIONSHIP_CONSTANT = 'self' | |
- | |
-pending_lookups = {} | |
def add_lazy_relation(cls, field, relation, operation): | |
@@ -70,14 +74,14 @@ | |
# string right away. If get_model returns None, it means that the related | |
# model isn't loaded yet, so we need to pend the relation until the class | |
# is prepared. | |
- model = get_model(app_label, model_name, | |
- seed_cache=False, only_installed=False) | |
- if model: | |
- operation(field, model, cls) | |
- else: | |
+ try: | |
+ model = cls._meta.apps.get_registered_model(app_label, model_name) | |
+ except LookupError: | |
key = (app_label, model_name) | |
value = (cls, field, operation) | |
- pending_lookups.setdefault(key, []).append(value) | |
+ cls._meta.apps._pending_lookups.setdefault(key, []).append(value) | |
+ else: | |
+ operation(field, model, cls) | |
def do_pending_lookups(sender, **kwargs): | |
@@ -85,14 +89,161 @@ | |
Handle any pending relations to the sending model. Sent from class_prepared. | |
""" | |
key = (sender._meta.app_label, sender.__name__) | |
- for cls, field, operation in pending_lookups.pop(key, []): | |
+ for cls, field, operation in sender._meta.apps._pending_lookups.pop(key, []): | |
operation(field, sender, cls) | |
signals.class_prepared.connect(do_pending_lookups) | |
-#HACK | |
class RelatedField(Field): | |
+ def check(self, **kwargs): | |
+ errors = super(RelatedField, self).check(**kwargs) | |
+ errors.extend(self._check_relation_model_exists()) | |
+ errors.extend(self._check_referencing_to_swapped_model()) | |
+ errors.extend(self._check_clashes()) | |
+ return errors | |
+ | |
+ def _check_relation_model_exists(self): | |
+ rel_is_missing = self.rel.to not in apps.get_models() | |
+ rel_is_string = isinstance(self.rel.to, six.string_types) | |
+ model_name = self.rel.to if rel_is_string else self.rel.to._meta.object_name | |
+ if rel_is_missing and (rel_is_string or not self.rel.to._meta.swapped): | |
+ return [ | |
+ checks.Error( | |
+ ("Field defines a relation with model '%s', which " | |
+ "is either not installed, or is abstract.") % model_name, | |
+ hint=None, | |
+ obj=self, | |
+ id='fields.E300', | |
+ ) | |
+ ] | |
+ return [] | |
+ | |
+ def _check_referencing_to_swapped_model(self): | |
+ if (self.rel.to not in apps.get_models() and | |
+ not isinstance(self.rel.to, six.string_types) and | |
+ self.rel.to._meta.swapped): | |
+ model = "%s.%s" % ( | |
+ self.rel.to._meta.app_label, | |
+ self.rel.to._meta.object_name | |
+ ) | |
+ return [ | |
+ checks.Error( | |
+ ("Field defines a relation with the model '%s', " | |
+ "which has been swapped out.") % model, | |
+ hint="Update the relation to point at 'settings.%s'." % self.rel.to._meta.swappable, | |
+ obj=self, | |
+ id='fields.E301', | |
+ ) | |
+ ] | |
+ return [] | |
+ | |
+ def _check_clashes(self): | |
+ """ Check accessor and reverse query name clashes. """ | |
+ | |
+ from django.db.models.base import ModelBase | |
+ | |
+ errors = [] | |
+ opts = self.model._meta | |
+ | |
+ # `f.rel.to` may be a string instead of a model. Skip if model name is | |
+ # not resolved. | |
+ if not isinstance(self.rel.to, ModelBase): | |
+ return [] | |
+ | |
+ # If the field doesn't install backward relation on the target model (so | |
+ # `is_hidden` returns True), then there are no clashes to check and we | |
+ # can skip these fields. | |
+ if self.rel.is_hidden(): | |
+ return [] | |
+ | |
+ try: | |
+ self.related | |
+ except AttributeError: | |
+ return [] | |
+ | |
+ # Consider that we are checking field `Model.foreign` and the models | |
+ # are: | |
+ # | |
+ # class Target(models.Model): | |
+ # model = models.IntegerField() | |
+ # model_set = models.IntegerField() | |
+ # | |
+ # class Model(models.Model): | |
+ # foreign = models.ForeignKey(Target) | |
+ # m2m = models.ManyToManyField(Target) | |
+ | |
+ rel_opts = self.rel.to._meta | |
+ # rel_opts.object_name == "Target" | |
+ rel_name = self.related.get_accessor_name() # i. e. "model_set" | |
+ rel_query_name = self.related_query_name() # i. e. "model" | |
+ field_name = "%s.%s" % (opts.object_name, | |
+ self.name) # i. e. "Model.field" | |
+ | |
+ # Check clashes between accessor or reverse query name of `field` | |
+ # and any other field name -- i. e. accessor for Model.foreign is | |
+ # model_set and it clashes with Target.model_set. | |
+ potential_clashes = rel_opts.fields + rel_opts.many_to_many | |
+ for clash_field in potential_clashes: | |
+ clash_name = "%s.%s" % (rel_opts.object_name, | |
+ clash_field.name) # i. e. "Target.model_set" | |
+ if clash_field.name == rel_name: | |
+ errors.append( | |
+ checks.Error( | |
+ "Reverse accessor for '%s' clashes with field name '%s'." % (field_name, clash_name), | |
+ hint=("Rename field '%s', or add/change a related_name " | |
+ "argument to the definition for field '%s'.") % (clash_name, field_name), | |
+ obj=self, | |
+ id='fields.E302', | |
+ ) | |
+ ) | |
+ | |
+ if clash_field.name == rel_query_name: | |
+ errors.append( | |
+ checks.Error( | |
+ "Reverse query name for '%s' clashes with field name '%s'." % (field_name, clash_name), | |
+ hint=("Rename field '%s', or add/change a related_name " | |
+ "argument to the definition for field '%s'.") % (clash_name, field_name), | |
+ obj=self, | |
+ id='fields.E303', | |
+ ) | |
+ ) | |
+ | |
+ # Check clashes between accessors/reverse query names of `field` and | |
+ # any other field accessor -- i. e. Model.foreign accessor clashes with | |
+ # Model.m2m accessor. | |
+ potential_clashes = rel_opts.get_all_related_many_to_many_objects() | |
+ potential_clashes += rel_opts.get_all_related_objects() | |
+ potential_clashes = (r for r in potential_clashes | |
+ if r.field is not self) | |
+ for clash_field in potential_clashes: | |
+ clash_name = "%s.%s" % ( # i. e. "Model.m2m" | |
+ clash_field.model._meta.object_name, | |
+ clash_field.field.name) | |
+ if clash_field.get_accessor_name() == rel_name: | |
+ errors.append( | |
+ checks.Error( | |
+ "Reverse accessor for '%s' clashes with reverse accessor for '%s'." % (field_name, clash_name), | |
+ hint=("Add or change a related_name argument " | |
+ "to the definition for '%s' or '%s'.") % (field_name, clash_name), | |
+ obj=self, | |
+ id='fields.E304', | |
+ ) | |
+ ) | |
+ | |
+ if clash_field.get_accessor_name() == rel_query_name: | |
+ errors.append( | |
+ checks.Error( | |
+ "Reverse query name for '%s' clashes with reverse query name for '%s'." % (field_name, clash_name), | |
+ hint=("Add or change a related_name argument " | |
+ "to the definition for '%s' or '%s'.") % (field_name, clash_name), | |
+ obj=self, | |
+ id='fields.E305', | |
+ ) | |
+ ) | |
+ | |
+ return errors | |
+ | |
def db_type(self, connection): | |
'''By default related field will not have a column | |
as it relates columns to another table''' | |
@@ -122,6 +273,30 @@ | |
else: | |
self.do_related_class(other, cls) | |
+ @property | |
+ def swappable_setting(self): | |
+ """ | |
+ Gets the setting that this is powered from for swapping, or None | |
+ if it's not swapped in / marked with swappable=False. | |
+ """ | |
+ if self.swappable: | |
+ # Work out string form of "to" | |
+ if isinstance(self.rel.to, six.string_types): | |
+ to_string = self.rel.to | |
+ else: | |
+ to_string = "%s.%s" % ( | |
+ self.rel.to._meta.app_label, | |
+ self.rel.to._meta.object_name, | |
+ ) | |
+ # See if anything swapped/swappable matches | |
+ for model in apps.get_models(include_swapped=True): | |
+ if model._meta.swapped: | |
+ if model._meta.swapped == to_string: | |
+ return model._meta.swappable | |
+ if ("%s.%s" % (model._meta.app_label, model._meta.object_name)) == to_string and model._meta.swappable: | |
+ return model._meta.swappable | |
+ return None | |
+ | |
def set_attributes_from_rel(self): | |
self.name = self.name or (self.rel.to._meta.model_name + '_' + self.rel.to._meta.pk.name) | |
if self.verbose_name is None: | |
@@ -133,6 +308,35 @@ | |
self.related = RelatedObject(other, cls, self) | |
if not cls._meta.abstract: | |
self.contribute_to_related_class(other, self.related) | |
+ | |
+ def get_limit_choices_to(self): | |
+ """Returns 'limit_choices_to' for this model field. | |
+ | |
+ If it is a callable, it will be invoked and the result will be | |
+ returned. | |
+ """ | |
+ if callable(self.rel.limit_choices_to): | |
+ return self.rel.limit_choices_to() | |
+ return self.rel.limit_choices_to | |
+ | |
+ def formfield(self, **kwargs): | |
+ """Passes ``limit_choices_to`` to field being constructed. | |
+ | |
+ Only passes it if there is a type that supports related fields. | |
+ This is a similar strategy used to pass the ``queryset`` to the field | |
+ being constructed. | |
+ """ | |
+ defaults = {} | |
+ if hasattr(self.rel, 'get_related_field'): | |
+ # If this is a callable, do not invoke it here. Just pass | |
+ # it in the defaults for when the form class will later be | |
+ # instantiated. | |
+ limit_choices_to = self.rel.limit_choices_to | |
+ defaults.update({ | |
+ 'limit_choices_to': limit_choices_to, | |
+ }) | |
+ defaults.update(kwargs) | |
+ return super(RelatedField, self).formfield(**defaults) | |
def related_query_name(self): | |
# This method defines the name that can be used to identify this | |
@@ -144,8 +348,8 @@ | |
class RenameRelatedObjectDescriptorMethods(RenameMethodsBase): | |
renamed_methods = ( | |
- ('get_query_set', 'get_queryset', PendingDeprecationWarning), | |
- ('get_prefetch_query_set', 'get_prefetch_queryset', PendingDeprecationWarning), | |
+ ('get_query_set', 'get_queryset', RemovedInDjango18Warning), | |
+ ('get_prefetch_query_set', 'get_prefetch_queryset', RemovedInDjango18Warning), | |
) | |
@@ -159,26 +363,43 @@ | |
self.related = related | |
self.cache_name = related.get_cache_name() | |
+ @cached_property | |
+ def RelatedObjectDoesNotExist(self): | |
+ # The exception isn't created at initialization time for the sake of | |
+ # consistency with `ReverseSingleRelatedObjectDescriptor`. | |
+ return type( | |
+ str('RelatedObjectDoesNotExist'), | |
+ (self.related.model.DoesNotExist, AttributeError), | |
+ {} | |
+ ) | |
+ | |
def is_cached(self, instance): | |
return hasattr(instance, self.cache_name) | |
- def get_queryset(self, **db_hints): | |
- db = router.db_for_read(self.related.model, **db_hints) | |
- return self.related.model._base_manager.using(db) | |
- | |
- def get_prefetch_queryset(self, instances): | |
+ def get_queryset(self, **hints): | |
+ # Gotcha: we return a `Manager` instance (i.e. not a `QuerySet`)! | |
+ return self.related.model._base_manager.db_manager(hints=hints) | |
+ | |
+ def get_prefetch_queryset(self, instances, queryset=None): | |
+ if queryset is None: | |
+ # Despite its name `get_queryset()` returns an instance of | |
+ # `Manager`, therefore we call `all()` to normalize to `QuerySet`. | |
+ queryset = self.get_queryset().all() | |
+ queryset._add_hints(instance=instances[0]) | |
+ | |
rel_obj_attr = attrgetter(self.related.field.attname) | |
instance_attr = lambda obj: obj._get_pk_val() | |
instances_dict = dict((instance_attr(inst), inst) for inst in instances) | |
query = {'%s__in' % self.related.field.name: instances} | |
- qs = self.get_queryset(instance=instances[0]).filter(**query) | |
+ queryset = queryset.filter(**query) | |
+ | |
# Since we're going to assign directly in the cache, | |
# we must manage the reverse relation cache manually. | |
rel_obj_cache_name = self.related.field.get_cache_name() | |
- for rel_obj in qs: | |
+ for rel_obj in queryset: | |
instance = instances_dict[rel_obj_attr(rel_obj)] | |
setattr(rel_obj, rel_obj_cache_name, instance) | |
- return qs, rel_obj_attr, instance_attr, True, self.cache_name | |
+ return queryset, rel_obj_attr, instance_attr, True, self.cache_name | |
def __get__(self, instance, instance_type=None): | |
if instance is None: | |
@@ -201,9 +422,12 @@ | |
setattr(rel_obj, self.related.field.get_cache_name(), instance) | |
setattr(instance, self.cache_name, rel_obj) | |
if rel_obj is None: | |
- raise self.related.model.DoesNotExist("%s has no %s." % ( | |
- instance.__class__.__name__, | |
- self.related.get_accessor_name())) | |
+ raise self.RelatedObjectDoesNotExist( | |
+ "%s has no %s." % ( | |
+ instance.__class__.__name__, | |
+ self.related.get_accessor_name() | |
+ ) | |
+ ) | |
else: | |
return rel_obj | |
@@ -214,13 +438,22 @@ | |
# If null=True, we can assign null here, but otherwise the value needs | |
# to be an instance of the related class. | |
- if value is None and self.related.field.null == False: | |
- raise ValueError('Cannot assign None: "%s.%s" does not allow null values.' % | |
- (instance._meta.object_name, self.related.get_accessor_name())) | |
+ if value is None and self.related.field.null is False: | |
+ raise ValueError( | |
+ 'Cannot assign None: "%s.%s" does not allow null values.' % ( | |
+ instance._meta.object_name, | |
+ self.related.get_accessor_name(), | |
+ ) | |
+ ) | |
elif value is not None and not isinstance(value, self.related.model): | |
- raise ValueError('Cannot assign "%r": "%s.%s" must be a "%s" instance.' % | |
- (value, instance._meta.object_name, | |
- self.related.get_accessor_name(), self.related.opts.object_name)) | |
+ raise ValueError( | |
+ 'Cannot assign "%r": "%s.%s" must be a "%s" instance.' % ( | |
+ value, | |
+ instance._meta.object_name, | |
+ self.related.get_accessor_name(), | |
+ self.related.opts.object_name, | |
+ ) | |
+ ) | |
elif value is not None: | |
if instance._state.db is None: | |
instance._state.db = router.db_for_write(instance.__class__, instance=value) | |
@@ -230,10 +463,12 @@ | |
if not router.allow_relation(value, instance): | |
raise ValueError('Cannot assign "%r": the current database router prevents this relation.' % value) | |
- related_pk = tuple([getattr(instance, field.attname) for field in self.related.field.foreign_related_fields]) | |
+ related_pk = tuple(getattr(instance, field.attname) for field in self.related.field.foreign_related_fields) | |
if None in related_pk: | |
- raise ValueError('Cannot assign "%r": "%s" instance isn\'t saved in the database.' % | |
- (value, instance._meta.object_name)) | |
+ raise ValueError( | |
+ 'Cannot assign "%r": "%s" instance isn\'t saved in the database.' % | |
+ (value, instance._meta.object_name) | |
+ ) | |
# Set the value of the related field to the value of the related object's related field | |
for index, field in enumerate(self.related.field.local_related_fields): | |
@@ -256,20 +491,37 @@ | |
self.field = field_with_rel | |
self.cache_name = self.field.get_cache_name() | |
+ @cached_property | |
+ def RelatedObjectDoesNotExist(self): | |
+ # The exception can't be created at initialization time since the | |
+ # related model might not be resolved yet; `rel.to` might still be | |
+ # a string model reference. | |
+ return type( | |
+ str('RelatedObjectDoesNotExist'), | |
+ (self.field.rel.to.DoesNotExist, AttributeError), | |
+ {} | |
+ ) | |
+ | |
def is_cached(self, instance): | |
return hasattr(instance, self.cache_name) | |
- def get_queryset(self, **db_hints): | |
- db = router.db_for_read(self.field.rel.to, **db_hints) | |
- rel_mgr = self.field.rel.to._default_manager | |
+ def get_queryset(self, **hints): | |
+ rel_mgr = self.field.rel.to._default_manager.db_manager(hints=hints) | |
# If the related manager indicates that it should be used for | |
# related fields, respect that. | |
if getattr(rel_mgr, 'use_for_related_fields', False): | |
- return rel_mgr.using(db) | |
+ # Gotcha: we return a `Manager` instance (i.e. not a `QuerySet`)! | |
+ return rel_mgr | |
else: | |
- return QuerySet(self.field.rel.to).using(db) | |
- | |
- def get_prefetch_queryset(self, instances): | |
+ return QuerySet(self.field.rel.to, hints=hints) | |
+ | |
+ def get_prefetch_queryset(self, instances, queryset=None): | |
+ if queryset is None: | |
+ # Despite its name `get_queryset()` may return an instance of | |
+ # `Manager`, therefore we call `all()` to normalize to `QuerySet`. | |
+ queryset = self.get_queryset().all() | |
+ queryset._add_hints(instance=instances[0]) | |
+ | |
rel_obj_attr = self.field.get_foreign_related_value | |
instance_attr = self.field.get_local_related_value | |
instances_dict = dict((instance_attr(inst), inst) for inst in instances) | |
@@ -285,16 +537,16 @@ | |
query = {'%s__in' % related_field.name: set(instance_attr(inst)[0] for inst in instances)} | |
else: | |
query = {'%s__in' % self.field.related_query_name(): instances} | |
- | |
- qs = self.get_queryset(instance=instances[0]).filter(**query) | |
+ queryset = queryset.filter(**query) | |
+ | |
# Since we're going to assign directly in the cache, | |
# we must manage the reverse relation cache manually. | |
if not self.field.rel.multiple: | |
rel_obj_cache_name = self.field.related.get_cache_name() | |
- for rel_obj in qs: | |
+ for rel_obj in queryset: | |
instance = instances_dict[rel_obj_attr(rel_obj)] | |
setattr(rel_obj, rel_obj_cache_name, instance) | |
- return qs, rel_obj_attr, instance_attr, True, self.cache_name | |
+ return queryset, rel_obj_attr, instance_attr, True, self.cache_name | |
def __get__(self, instance, instance_type=None): | |
if instance is None: | |
@@ -322,21 +574,29 @@ | |
setattr(rel_obj, self.field.related.get_cache_name(), instance) | |
setattr(instance, self.cache_name, rel_obj) | |
if rel_obj is None and not self.field.null: | |
- raise self.field.rel.to.DoesNotExist( | |
- "%s has no %s." % (self.field.model.__name__, self.field.name)) | |
+ raise self.RelatedObjectDoesNotExist( | |
+ "%s has no %s." % (self.field.model.__name__, self.field.name) | |
+ ) | |
else: | |
return rel_obj | |
def __set__(self, instance, value): | |
# If null=True, we can assign null here, but otherwise the value needs | |
# to be an instance of the related class. | |
- if value is None and self.field.null == False: | |
- raise ValueError('Cannot assign None: "%s.%s" does not allow null values.' % | |
- (instance._meta.object_name, self.field.name)) | |
+ if value is None and self.field.null is False: | |
+ raise ValueError( | |
+ 'Cannot assign None: "%s.%s" does not allow null values.' % | |
+ (instance._meta.object_name, self.field.name) | |
+ ) | |
elif value is not None and not isinstance(value, self.field.rel.to): | |
- raise ValueError('Cannot assign "%r": "%s.%s" must be a "%s" instance.' % | |
- (value, instance._meta.object_name, | |
- self.field.name, self.field.rel.to._meta.object_name)) | |
+ raise ValueError( | |
+ 'Cannot assign "%r": "%s.%s" must be a "%s" instance.' % ( | |
+ value, | |
+ instance._meta.object_name, | |
+ self.field.name, | |
+ self.field.rel.to._meta.object_name, | |
+ ) | |
+ ) | |
elif value is not None: | |
if instance._state.db is None: | |
instance._state.db = router.db_for_write(instance.__class__, instance=value) | |
@@ -380,6 +640,129 @@ | |
setattr(value, self.field.related.get_cache_name(), instance) | |
+def create_foreign_related_manager(superclass, rel_field, rel_model): | |
+ class RelatedManager(superclass): | |
+ def __init__(self, instance): | |
+ super(RelatedManager, self).__init__() | |
+ self.instance = instance | |
+ self.core_filters = {'%s__exact' % rel_field.name: instance} | |
+ self.model = rel_model | |
+ | |
+ def __call__(self, **kwargs): | |
+ # We use **kwargs rather than a kwarg argument to enforce the | |
+ # `manager='manager_name'` syntax. | |
+ manager = getattr(self.model, kwargs.pop('manager')) | |
+ manager_class = create_foreign_related_manager(manager.__class__, rel_field, rel_model) | |
+ return manager_class(self.instance) | |
+ do_not_call_in_templates = True | |
+ | |
+ def get_queryset(self): | |
+ try: | |
+ return self.instance._prefetched_objects_cache[rel_field.related_query_name()] | |
+ except (AttributeError, KeyError): | |
+ db = self._db or router.db_for_read(self.model, instance=self.instance) | |
+ empty_strings_as_null = connections[db].features.interprets_empty_strings_as_nulls | |
+ qs = super(RelatedManager, self).get_queryset() | |
+ qs._add_hints(instance=self.instance) | |
+ if self._db: | |
+ qs = qs.using(self._db) | |
+ qs = qs.filter(**self.core_filters) | |
+ for field in rel_field.foreign_related_fields: | |
+ val = getattr(self.instance, field.attname) | |
+ if val is None or (val == '' and empty_strings_as_null): | |
+ return qs.none() | |
+ qs._known_related_objects = {rel_field: {self.instance.pk: self.instance}} | |
+ return qs | |
+ | |
+ def get_prefetch_queryset(self, instances, queryset=None): | |
+ if queryset is None: | |
+ queryset = super(RelatedManager, self).get_queryset() | |
+ | |
+ queryset._add_hints(instance=instances[0]) | |
+ queryset = queryset.using(queryset._db or self._db) | |
+ | |
+ rel_obj_attr = rel_field.get_local_related_value | |
+ instance_attr = rel_field.get_foreign_related_value | |
+ instances_dict = dict((instance_attr(inst), inst) for inst in instances) | |
+ query = {'%s__in' % rel_field.name: instances} | |
+ queryset = queryset.filter(**query) | |
+ | |
+ # Since we just bypassed this class' get_queryset(), we must manage | |
+ # the reverse relation manually. | |
+ for rel_obj in queryset: | |
+ instance = instances_dict[rel_obj_attr(rel_obj)] | |
+ setattr(rel_obj, rel_field.name, instance) | |
+ cache_name = rel_field.related_query_name() | |
+ return queryset, rel_obj_attr, instance_attr, False, cache_name | |
+ | |
+ def add(self, *objs): | |
+ objs = list(objs) | |
+ db = router.db_for_write(self.model, instance=self.instance) | |
+ with transaction.commit_on_success_unless_managed( | |
+ using=db, savepoint=False): | |
+ for obj in objs: | |
+ if not isinstance(obj, self.model): | |
+ raise TypeError("'%s' instance expected, got %r" % | |
+ (self.model._meta.object_name, obj)) | |
+ setattr(obj, rel_field.name, self.instance) | |
+ obj.save() | |
+ add.alters_data = True | |
+ | |
+ def create(self, **kwargs): | |
+ kwargs[rel_field.name] = self.instance | |
+ db = router.db_for_write(self.model, instance=self.instance) | |
+ return super(RelatedManager, self.db_manager(db)).create(**kwargs) | |
+ create.alters_data = True | |
+ | |
+ def get_or_create(self, **kwargs): | |
+ kwargs[rel_field.name] = self.instance | |
+ db = router.db_for_write(self.model, instance=self.instance) | |
+ return super(RelatedManager, self.db_manager(db)).get_or_create(**kwargs) | |
+ get_or_create.alters_data = True | |
+ | |
+ def update_or_create(self, **kwargs): | |
+ kwargs[rel_field.name] = self.instance | |
+ db = router.db_for_write(self.model, instance=self.instance) | |
+ return super(RelatedManager, self.db_manager(db)).update_or_create(**kwargs) | |
+ update_or_create.alters_data = True | |
+ | |
+ # remove() and clear() are only provided if the ForeignKey can have a value of null. | |
+ if rel_field.null: | |
+ def remove(self, *objs, **kwargs): | |
+ if not objs: | |
+ return | |
+ bulk = kwargs.pop('bulk', True) | |
+ val = rel_field.get_foreign_related_value(self.instance) | |
+ old_ids = set() | |
+ for obj in objs: | |
+ # Is obj actually part of this descriptor set? | |
+ if rel_field.get_local_related_value(obj) == val: | |
+ old_ids.add(obj.pk) | |
+ else: | |
+ raise rel_field.rel.to.DoesNotExist("%r is not related to %r." % (obj, self.instance)) | |
+ self._clear(self.filter(pk__in=old_ids), bulk) | |
+ remove.alters_data = True | |
+ | |
+ def clear(self, **kwargs): | |
+ bulk = kwargs.pop('bulk', True) | |
+ self._clear(self, bulk) | |
+ clear.alters_data = True | |
+ | |
+ def _clear(self, queryset, bulk): | |
+ db = router.db_for_write(self.model, instance=self.instance) | |
+ queryset = queryset.using(db) | |
+ if bulk: | |
+ queryset.update(**{rel_field.name: None}) | |
+ else: | |
+ with transaction.commit_on_success_unless_managed(using=db, savepoint=False): | |
+ for obj in queryset: | |
+ setattr(obj, rel_field.name, None) | |
+ obj.save(update_fields=[rel_field.name]) | |
+ _clear.alters_data = True | |
+ | |
+ return RelatedManager | |
+ | |
+ | |
class ForeignRelatedObjectsDescriptor(object): | |
# This class provides the functionality that makes the related-object | |
# managers available as attributes on a model class, for fields that have | |
@@ -407,86 +790,11 @@ | |
def related_manager_cls(self): | |
# Dynamically create a class that subclasses the related model's default | |
# manager. | |
- superclass = self.related.model._default_manager.__class__ | |
- rel_field = self.related.field | |
- rel_model = self.related.model | |
- | |
- class RelatedManager(superclass): | |
- def __init__(self, instance): | |
- super(RelatedManager, self).__init__() | |
- self.instance = instance | |
- self.core_filters= {'%s__exact' % rel_field.name: instance} | |
- self.model = rel_model | |
- | |
- def get_queryset(self): | |
- try: | |
- return self.instance._prefetched_objects_cache[rel_field.related_query_name()] | |
- except (AttributeError, KeyError): | |
- db = self._db or router.db_for_read(self.model, instance=self.instance) | |
- qs = super(RelatedManager, self).get_queryset().using(db).filter(**self.core_filters) | |
- empty_strings_as_null = connections[db].features.interprets_empty_strings_as_nulls | |
- for field in rel_field.foreign_related_fields: | |
- val = getattr(self.instance, field.attname) | |
- if val is None or (val == '' and empty_strings_as_null): | |
- return qs.none() | |
- qs._known_related_objects = {rel_field: {self.instance.pk: self.instance}} | |
- return qs | |
- | |
- def get_prefetch_queryset(self, instances): | |
- rel_obj_attr = rel_field.get_local_related_value | |
- instance_attr = rel_field.get_foreign_related_value | |
- instances_dict = dict((instance_attr(inst), inst) for inst in instances) | |
- db = self._db or router.db_for_read(self.model, instance=instances[0]) | |
- query = {'%s__in' % rel_field.name: instances} | |
- qs = super(RelatedManager, self).get_queryset().using(db).filter(**query) | |
- # Since we just bypassed this class' get_queryset(), we must manage | |
- # the reverse relation manually. | |
- for rel_obj in qs: | |
- instance = instances_dict[rel_obj_attr(rel_obj)] | |
- setattr(rel_obj, rel_field.name, instance) | |
- cache_name = rel_field.related_query_name() | |
- return qs, rel_obj_attr, instance_attr, False, cache_name | |
- | |
- def add(self, *objs): | |
- for obj in objs: | |
- if not isinstance(obj, self.model): | |
- raise TypeError("'%s' instance expected, got %r" % (self.model._meta.object_name, obj)) | |
- setattr(obj, rel_field.name, self.instance) | |
- obj.save() | |
- add.alters_data = True | |
- | |
- def create(self, **kwargs): | |
- kwargs[rel_field.name] = self.instance | |
- db = router.db_for_write(self.model, instance=self.instance) | |
- return super(RelatedManager, self.db_manager(db)).create(**kwargs) | |
- create.alters_data = True | |
- | |
- def get_or_create(self, **kwargs): | |
- # Update kwargs with the related object that this | |
- # ForeignRelatedObjectsDescriptor knows about. | |
- kwargs[rel_field.name] = self.instance | |
- db = router.db_for_write(self.model, instance=self.instance) | |
- return super(RelatedManager, self.db_manager(db)).get_or_create(**kwargs) | |
- get_or_create.alters_data = True | |
- | |
- # remove() and clear() are only provided if the ForeignKey can have a value of null. | |
- if rel_field.null: | |
- def remove(self, *objs): | |
- val = rel_field.get_foreign_related_value(self.instance) | |
- for obj in objs: | |
- # Is obj actually part of this descriptor set? | |
- if rel_field.get_local_related_value(obj) == val: | |
- setattr(obj, rel_field.name, None) | |
- obj.save() | |
- else: | |
- raise rel_field.rel.to.DoesNotExist("%r is not related to %r." % (obj, self.instance)) | |
- remove.alters_data = True | |
- | |
- def clear(self): | |
- self.update(**{rel_field.name: None}) | |
- clear.alters_data = True | |
- | |
- return RelatedManager | |
+ return create_foreign_related_manager( | |
+ self.related.model._default_manager.__class__, | |
+ self.related.field, | |
+ self.related.model, | |
+ ) | |
def create_many_related_manager(superclass, rel): | |
@@ -510,14 +818,13 @@ | |
self.instance = instance | |
self.symmetrical = symmetrical | |
self.source_field = source_field | |
+ self.target_field = through._meta.get_field(target_field_name) | |
self.source_field_name = source_field_name | |
self.target_field_name = target_field_name | |
self.reverse = reverse | |
self.through = through | |
self.prefetch_cache_name = prefetch_cache_name | |
self.related_val = source_field.get_foreign_related_value(instance) | |
- # Used for single column related auto created models | |
- self._fk_val = self.related_val[0] | |
if None in self.related_val: | |
raise ValueError('"%r" needs to have a value for field "%s" before ' | |
'this many-to-many relationship can be used.' % | |
@@ -530,31 +837,59 @@ | |
"a many-to-many relationship can be used." % | |
instance.__class__.__name__) | |
- | |
- def _get_fk_val(self, obj, field_name): | |
- """ | |
- Returns the correct value for this relationship's foreign key. This | |
- might be something else than pk value when to_field is used. | |
- """ | |
- fk = self.through._meta.get_field(field_name) | |
- if fk.rel.field_name and fk.rel.field_name != fk.rel.to._meta.pk.attname: | |
- attname = fk.rel.get_related_field().get_attname() | |
- return fk.get_prep_lookup('exact', getattr(obj, attname)) | |
- else: | |
- return obj.pk | |
+ def __call__(self, **kwargs): | |
+ # We use **kwargs rather than a kwarg argument to enforce the | |
+ # `manager='manager_name'` syntax. | |
+ manager = getattr(self.model, kwargs.pop('manager')) | |
+ manager_class = create_many_related_manager(manager.__class__, rel) | |
+ return manager_class( | |
+ model=self.model, | |
+ query_field_name=self.query_field_name, | |
+ instance=self.instance, | |
+ symmetrical=self.symmetrical, | |
+ source_field_name=self.source_field_name, | |
+ target_field_name=self.target_field_name, | |
+ reverse=self.reverse, | |
+ through=self.through, | |
+ prefetch_cache_name=self.prefetch_cache_name, | |
+ ) | |
+ do_not_call_in_templates = True | |
+ | |
+ def _build_remove_filters(self, removed_vals): | |
+ filters = Q(**{self.source_field_name: self.related_val}) | |
+ # No need to add a subquery condition if removed_vals is a QuerySet without | |
+ # filters. | |
+ removed_vals_filters = (not isinstance(removed_vals, QuerySet) or | |
+ removed_vals._has_filters()) | |
+ if removed_vals_filters: | |
+ filters &= Q(**{'%s__in' % self.target_field_name: removed_vals}) | |
+ if self.symmetrical: | |
+ symmetrical_filters = Q(**{self.target_field_name: self.related_val}) | |
+ if removed_vals_filters: | |
+ symmetrical_filters &= Q( | |
+ **{'%s__in' % self.source_field_name: removed_vals}) | |
+ filters |= symmetrical_filters | |
+ return filters | |
def get_queryset(self): | |
try: | |
return self.instance._prefetched_objects_cache[self.prefetch_cache_name] | |
except (AttributeError, KeyError): | |
- db = self._db or router.db_for_read(self.instance.__class__, instance=self.instance) | |
- return super(ManyRelatedManager, self).get_queryset().using(db)._next_is_sticky().filter(**self.core_filters) | |
- | |
- def get_prefetch_queryset(self, instances): | |
- instance = instances[0] | |
- db = self._db or router.db_for_read(instance.__class__, instance=instance) | |
+ qs = super(ManyRelatedManager, self).get_queryset() | |
+ qs._add_hints(instance=self.instance) | |
+ if self._db: | |
+ qs = qs.using(self._db) | |
+ return qs._next_is_sticky().filter(**self.core_filters) | |
+ | |
+ def get_prefetch_queryset(self, instances, queryset=None): | |
+ if queryset is None: | |
+ queryset = super(ManyRelatedManager, self).get_queryset() | |
+ | |
+ queryset._add_hints(instance=instances[0]) | |
+ queryset = queryset.using(queryset._db or self._db) | |
+ | |
query = {'%s__in' % self.query_field_name: instances} | |
- qs = super(ManyRelatedManager, self).get_queryset().using(db)._next_is_sticky().filter(**query) | |
+ queryset = queryset._next_is_sticky().filter(**query) | |
# M2M: need to annotate the query in order to get the primary model | |
# that the secondary model was actually related to. We know that | |
@@ -565,42 +900,54 @@ | |
# dealing with PK values. | |
fk = self.through._meta.get_field(self.source_field_name) | |
join_table = self.through._meta.db_table | |
- connection = connections[db] | |
+ connection = connections[queryset.db] | |
qn = connection.ops.quote_name | |
- qs = qs.extra(select=dict( | |
+ queryset = queryset.extra(select=dict( | |
('_prefetch_related_val_%s' % f.attname, | |
'%s.%s' % (qn(join_table), qn(f.column))) for f in fk.local_related_fields)) | |
- return (qs, | |
- lambda result: tuple([getattr(result, '_prefetch_related_val_%s' % f.attname) for f in fk.local_related_fields]), | |
- lambda inst: tuple([getattr(inst, f.attname) for f in fk.foreign_related_fields]), | |
+ return (queryset, | |
+ lambda result: tuple(getattr(result, '_prefetch_related_val_%s' % f.attname) for f in fk.local_related_fields), | |
+ lambda inst: tuple(getattr(inst, f.attname) for f in fk.foreign_related_fields), | |
False, | |
self.prefetch_cache_name) | |
- # If the ManyToMany relation has an intermediary model, | |
- # the add and remove methods do not exist. | |
- if rel.through._meta.auto_created: | |
- def add(self, *objs): | |
- self._add_items(self.source_field_name, self.target_field_name, *objs) | |
- | |
- # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table | |
- if self.symmetrical: | |
- self._add_items(self.target_field_name, self.source_field_name, *objs) | |
- add.alters_data = True | |
- | |
- def remove(self, *objs): | |
- self._remove_items(self.source_field_name, self.target_field_name, *objs) | |
- | |
- # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table | |
- if self.symmetrical: | |
- self._remove_items(self.target_field_name, self.source_field_name, *objs) | |
- remove.alters_data = True | |
+ def add(self, *objs): | |
+ if not rel.through._meta.auto_created: | |
+ opts = self.through._meta | |
+ raise AttributeError( | |
+ "Cannot use add() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % | |
+ (opts.app_label, opts.object_name) | |
+ ) | |
+ self._add_items(self.source_field_name, self.target_field_name, *objs) | |
+ | |
+ # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table | |
+ if self.symmetrical: | |
+ self._add_items(self.target_field_name, self.source_field_name, *objs) | |
+ add.alters_data = True | |
+ | |
+ def remove(self, *objs): | |
+ if not rel.through._meta.auto_created: | |
+ opts = self.through._meta | |
+ raise AttributeError( | |
+ "Cannot use remove() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % | |
+ (opts.app_label, opts.object_name) | |
+ ) | |
+ self._remove_items(self.source_field_name, self.target_field_name, *objs) | |
+ remove.alters_data = True | |
def clear(self): | |
- self._clear_items(self.source_field_name) | |
- | |
- # If this is a symmetrical m2m relation to self, clear the mirror entry in the m2m table | |
- if self.symmetrical: | |
- self._clear_items(self.target_field_name) | |
+ db = router.db_for_write(self.through, instance=self.instance) | |
+ | |
+ signals.m2m_changed.send(sender=self.through, action="pre_clear", | |
+ instance=self.instance, reverse=self.reverse, | |
+ model=self.model, pk_set=None, using=db) | |
+ | |
+ filters = self._build_remove_filters(super(ManyRelatedManager, self).get_queryset().using(db)) | |
+ self.through._default_manager.using(db).filter(filters).delete() | |
+ | |
+ signals.m2m_changed.send(sender=self.through, action="post_clear", | |
+ instance=self.instance, reverse=self.reverse, | |
+ model=self.model, pk_set=None, using=db) | |
clear.alters_data = True | |
def create(self, **kwargs): | |
@@ -608,7 +955,10 @@ | |
# from the method lookup table, as we do with add and remove. | |
if not self.through._meta.auto_created: | |
opts = self.through._meta | |
- raise AttributeError("Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)) | |
+ raise AttributeError( | |
+ "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % | |
+ (opts.app_label, opts.object_name) | |
+ ) | |
db = router.db_for_write(self.instance.__class__, instance=self.instance) | |
new_obj = super(ManyRelatedManager, self.db_manager(db)).create(**kwargs) | |
self.add(new_obj) | |
@@ -617,14 +967,23 @@ | |
def get_or_create(self, **kwargs): | |
db = router.db_for_write(self.instance.__class__, instance=self.instance) | |
- obj, created = \ | |
- super(ManyRelatedManager, self.db_manager(db)).get_or_create(**kwargs) | |
+ obj, created = super(ManyRelatedManager, self.db_manager(db)).get_or_create(**kwargs) | |
# We only need to add() if created because if we got an object back | |
# from get() then the relationship already exists. | |
if created: | |
self.add(obj) | |
return obj, created | |
get_or_create.alters_data = True | |
+ | |
+ def update_or_create(self, **kwargs): | |
+ db = router.db_for_write(self.instance.__class__, instance=self.instance) | |
+ obj, created = super(ManyRelatedManager, self.db_manager(db)).update_or_create(**kwargs) | |
+ # We only need to add() if created because if we got an object back | |
+ # from get() then the relationship already exists. | |
+ if created: | |
+ self.add(obj) | |
+ return obj, created | |
+ update_or_create.alters_data = True | |
def _add_items(self, source_field_name, target_field_name, *objs): | |
# source_field_name: the PK fieldname in join table for the source object | |
@@ -638,21 +997,29 @@ | |
for obj in objs: | |
if isinstance(obj, self.model): | |
if not router.allow_relation(obj, self.instance): | |
- raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' % | |
- (obj, self.instance._state.db, obj._state.db)) | |
- fk_val = self._get_fk_val(obj, target_field_name) | |
+ raise ValueError( | |
+ 'Cannot add "%r": instance is on database "%s", value is on database "%s"' % | |
+ (obj, self.instance._state.db, obj._state.db) | |
+ ) | |
+ fk_val = self.through._meta.get_field( | |
+ target_field_name).get_foreign_related_value(obj)[0] | |
if fk_val is None: | |
- raise ValueError('Cannot add "%r": the value for field "%s" is None' % | |
- (obj, target_field_name)) | |
- new_ids.add(self._get_fk_val(obj, target_field_name)) | |
+ raise ValueError( | |
+ 'Cannot add "%r": the value for field "%s" is None' % | |
+ (obj, target_field_name) | |
+ ) | |
+ new_ids.add(fk_val) | |
elif isinstance(obj, Model): | |
- raise TypeError("'%s' instance expected, got %r" % (self.model._meta.object_name, obj)) | |
+ raise TypeError( | |
+ "'%s' instance expected, got %r" % | |
+ (self.model._meta.object_name, obj) | |
+ ) | |
else: | |
new_ids.add(obj) | |
db = router.db_for_write(self.through, instance=self.instance) | |
vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True) | |
vals = vals.filter(**{ | |
- source_field_name: self._fk_val, | |
+ source_field_name: self.related_val[0], | |
'%s__in' % target_field_name: new_ids, | |
}) | |
new_ids = new_ids - set(vals) | |
@@ -666,7 +1033,7 @@ | |
# Add the ones that aren't there already | |
self.through._default_manager.using(db).bulk_create([ | |
self.through(**{ | |
- '%s_id' % source_field_name: self._fk_val, | |
+ '%s_id' % source_field_name: self.related_val[0], | |
'%s_id' % target_field_name: obj_id, | |
}) | |
for obj_id in new_ids | |
@@ -683,55 +1050,36 @@ | |
# source_field_name: the PK colname in join table for the source object | |
# target_field_name: the PK colname in join table for the target object | |
# *objs - objects to remove | |
- | |
- # If there aren't any objects, there is nothing to do. | |
- if objs: | |
- # Check that all the objects are of the right type | |
- old_ids = set() | |
- for obj in objs: | |
- if isinstance(obj, self.model): | |
- old_ids.add(self._get_fk_val(obj, target_field_name)) | |
- else: | |
- old_ids.add(obj) | |
- # Work out what DB we're operating on | |
- db = router.db_for_write(self.through, instance=self.instance) | |
- # Send a signal to the other end if need be. | |
- if self.reverse or source_field_name == self.source_field_name: | |
- # Don't send the signal when we are deleting the | |
- # duplicate data row for symmetrical reverse entries. | |
- signals.m2m_changed.send(sender=self.through, action="pre_remove", | |
- instance=self.instance, reverse=self.reverse, | |
- model=self.model, pk_set=old_ids, using=db) | |
- # Remove the specified objects from the join table | |
- self.through._default_manager.using(db).filter(**{ | |
- source_field_name: self._fk_val, | |
- '%s__in' % target_field_name: old_ids | |
- }).delete() | |
- if self.reverse or source_field_name == self.source_field_name: | |
- # Don't send the signal when we are deleting the | |
- # duplicate data row for symmetrical reverse entries. | |
- signals.m2m_changed.send(sender=self.through, action="post_remove", | |
- instance=self.instance, reverse=self.reverse, | |
- model=self.model, pk_set=old_ids, using=db) | |
- | |
- def _clear_items(self, source_field_name): | |
+ if not objs: | |
+ return | |
+ | |
+ # Check that all the objects are of the right type | |
+ old_ids = set() | |
+ for obj in objs: | |
+ if isinstance(obj, self.model): | |
+ fk_val = self.target_field.get_foreign_related_value(obj)[0] | |
+ old_ids.add(fk_val) | |
+ else: | |
+ old_ids.add(obj) | |
+ | |
db = router.db_for_write(self.through, instance=self.instance) | |
- # source_field_name: the PK colname in join table for the source object | |
- if self.reverse or source_field_name == self.source_field_name: | |
- # Don't send the signal when we are clearing the | |
- # duplicate data rows for symmetrical reverse entries. | |
- signals.m2m_changed.send(sender=self.through, action="pre_clear", | |
- instance=self.instance, reverse=self.reverse, | |
- model=self.model, pk_set=None, using=db) | |
- self.through._default_manager.using(db).filter(**{ | |
- source_field_name: self.related_val | |
- }).delete() | |
- if self.reverse or source_field_name == self.source_field_name: | |
- # Don't send the signal when we are clearing the | |
- # duplicate data rows for symmetrical reverse entries. | |
- signals.m2m_changed.send(sender=self.through, action="post_clear", | |
- instance=self.instance, reverse=self.reverse, | |
- model=self.model, pk_set=None, using=db) | |
+ | |
+ # Send a signal to the other end if need be. | |
+ signals.m2m_changed.send(sender=self.through, action="pre_remove", | |
+ instance=self.instance, reverse=self.reverse, | |
+ model=self.model, pk_set=old_ids, using=db) | |
+ target_model_qs = super(ManyRelatedManager, self).get_queryset() | |
+ if target_model_qs._has_filters(): | |
+ old_vals = target_model_qs.using(db).filter(**{ | |
+ '%s__in' % self.target_field.related_field.attname: old_ids}) | |
+ else: | |
+ old_vals = old_ids | |
+ filters = self._build_remove_filters(old_vals) | |
+ self.through._default_manager.using(db).filter(filters).delete() | |
+ | |
+ signals.m2m_changed.send(sender=self.through, action="post_remove", | |
+ instance=self.instance, reverse=self.reverse, | |
+ model=self.model, pk_set=old_ids, using=db) | |
return ManyRelatedManager | |
@@ -841,6 +1189,7 @@ | |
manager.clear() | |
manager.add(*value) | |
+ | |
class ForeignObjectRel(object): | |
def __init__(self, field, to, related_name=None, limit_choices_to=None, | |
parent_link=False, on_delete=None, related_query_name=None): | |
@@ -877,6 +1226,12 @@ | |
# By default foreign object doesn't relate to any remote field (for | |
# example custom multicolumn joins currently have no remote field). | |
self.field_name = None | |
+ | |
+ def get_lookup_constraint(self, constraint_class, alias, targets, sources, lookup_type, | |
+ raw_value): | |
+ return self.field.get_lookup_constraint(constraint_class, alias, targets, sources, | |
+ lookup_type, raw_value) | |
+ | |
class ManyToOneRel(ForeignObjectRel): | |
def __init__(self, field, to, field_name, related_name=None, limit_choices_to=None, | |
@@ -906,16 +1261,18 @@ | |
parent_link=False, on_delete=None, related_query_name=None): | |
super(OneToOneRel, self).__init__(field, to, field_name, | |
related_name=related_name, limit_choices_to=limit_choices_to, | |
- parent_link=parent_link, on_delete=on_delete, related_query_name=related_query_name, | |
- ) | |
+ parent_link=parent_link, on_delete=on_delete, related_query_name=related_query_name) | |
self.multiple = False | |
class ManyToManyRel(object): | |
def __init__(self, to, related_name=None, limit_choices_to=None, | |
- symmetrical=True, through=None, db_constraint=True, related_query_name=None): | |
+ symmetrical=True, through=None, through_fields=None, | |
+ db_constraint=True, related_query_name=None): | |
if through and not db_constraint: | |
raise ValueError("Can't supply a through model and db_constraint=False") | |
+ if through_fields and not through: | |
+ raise ValueError("Cannot specify through_fields without a through model") | |
self.to = to | |
self.related_name = related_name | |
self.related_query_name = related_query_name | |
@@ -925,6 +1282,7 @@ | |
self.symmetrical = symmetrical | |
self.multiple = True | |
self.through = through | |
+ self.through_fields = through_fields | |
self.db_constraint = db_constraint | |
def is_hidden(self): | |
@@ -943,10 +1301,12 @@ | |
class ForeignObject(RelatedField): | |
requires_unique_target = True | |
generate_reverse_relation = True | |
- | |
- def __init__(self, to, from_fields, to_fields, **kwargs): | |
+ related_accessor_class = ForeignRelatedObjectsDescriptor | |
+ | |
+ def __init__(self, to, from_fields, to_fields, swappable=True, **kwargs): | |
self.from_fields = from_fields | |
self.to_fields = to_fields | |
+ self.swappable = swappable | |
if 'rel' not in kwargs: | |
kwargs['rel'] = ForeignObjectRel( | |
@@ -961,9 +1321,94 @@ | |
super(ForeignObject, self).__init__(**kwargs) | |
+ def check(self, **kwargs): | |
+ errors = super(ForeignObject, self).check(**kwargs) | |
+ errors.extend(self._check_unique_target()) | |
+ return errors | |
+ | |
+ def _check_unique_target(self): | |
+ rel_is_string = isinstance(self.rel.to, six.string_types) | |
+ if rel_is_string or not self.requires_unique_target: | |
+ return [] | |
+ | |
+ # Skip if the | |
+ try: | |
+ self.foreign_related_fields | |
+ except FieldDoesNotExist: | |
+ return [] | |
+ | |
+ try: | |
+ self.related | |
+ except AttributeError: | |
+ return [] | |
+ | |
+ has_unique_field = any(rel_field.unique | |
+ for rel_field in self.foreign_related_fields) | |
+ if not has_unique_field and len(self.foreign_related_fields) > 1: | |
+ field_combination = ', '.join("'%s'" % rel_field.name | |
+ for rel_field in self.foreign_related_fields) | |
+ model_name = self.rel.to.__name__ | |
+ return [ | |
+ checks.Error( | |
+ "None of the fields %s on model '%s' have a unique=True constraint." % (field_combination, model_name), | |
+ hint=None, | |
+ obj=self, | |
+ id='fields.E310', | |
+ ) | |
+ ] | |
+ elif not has_unique_field: | |
+ field_name = self.foreign_related_fields[0].name | |
+ model_name = self.rel.to.__name__ | |
+ return [ | |
+ checks.Error( | |
+ ("'%s.%s' must set unique=True " | |
+ "because it is referenced by a foreign key.") % (model_name, field_name), | |
+ hint=None, | |
+ obj=self, | |
+ id='fields.E311', | |
+ ) | |
+ ] | |
+ else: | |
+ return [] | |
+ | |
+ def deconstruct(self): | |
+ name, path, args, kwargs = super(ForeignObject, self).deconstruct() | |
+ kwargs['from_fields'] = self.from_fields | |
+ kwargs['to_fields'] = self.to_fields | |
+ if self.rel.related_name is not None: | |
+ kwargs['related_name'] = force_text(self.rel.related_name) | |
+ if self.rel.related_query_name is not None: | |
+ kwargs['related_query_name'] = self.rel.related_query_name | |
+ if self.rel.on_delete != CASCADE: | |
+ kwargs['on_delete'] = self.rel.on_delete | |
+ if self.rel.parent_link: | |
+ kwargs['parent_link'] = self.rel.parent_link | |
+ # Work out string form of "to" | |
+ if isinstance(self.rel.to, six.string_types): | |
+ kwargs['to'] = self.rel.to | |
+ else: | |
+ kwargs['to'] = "%s.%s" % (self.rel.to._meta.app_label, self.rel.to._meta.object_name) | |
+ # If swappable is True, then see if we're actually pointing to the target | |
+ # of a swap. | |
+ swappable_setting = self.swappable_setting | |
+ if swappable_setting is not None: | |
+ # If it's already a settings reference, error | |
+ if hasattr(kwargs['to'], "setting_name"): | |
+ if kwargs['to'].setting_name != swappable_setting: | |
+ raise ValueError("Cannot deconstruct a ForeignKey pointing to a model that is swapped in place of more than one model (%s and %s)" % (kwargs['to'].setting_name, swappable_setting)) | |
+ # Set it | |
+ from django.db.migrations.writer import SettingsReference | |
+ kwargs['to'] = SettingsReference( | |
+ kwargs['to'], | |
+ swappable_setting, | |
+ ) | |
+ return name, path, args, kwargs | |
+ | |
def resolve_related_fields(self): | |
if len(self.from_fields) < 1 or len(self.from_fields) != len(self.to_fields): | |
raise ValueError('Foreign Object from and to fields must be the same non-zero length') | |
+ if isinstance(self.rel.to, six.string_types): | |
+ raise ValueError('Related model %r cannot be resolved' % self.rel.to) | |
related_fields = [] | |
for index in range(len(self.from_fields)): | |
from_field_name = self.from_fields[index] | |
@@ -987,11 +1432,11 @@ | |
@property | |
def local_related_fields(self): | |
- return tuple([lhs_field for lhs_field, rhs_field in self.related_fields]) | |
+ return tuple(lhs_field for lhs_field, rhs_field in self.related_fields) | |
@property | |
def foreign_related_fields(self): | |
- return tuple([rhs_field for lhs_field, rhs_field in self.related_fields]) | |
+ return tuple(rhs_field for lhs_field, rhs_field in self.related_fields) | |
def get_local_related_value(self, instance): | |
return self.get_instance_value_for_fields(instance, self.local_related_fields) | |
@@ -1002,14 +1447,19 @@ | |
@staticmethod | |
def get_instance_value_for_fields(instance, fields): | |
ret = [] | |
+ opts = instance._meta | |
for field in fields: | |
# Gotcha: in some cases (like fixture loading) a model can have | |
# different values in parent_ptr_id and parent's id. So, use | |
# instance.pk (that is, parent_ptr_id) when asked for instance.id. | |
if field.primary_key: | |
- ret.append(instance.pk) | |
- else: | |
- ret.append(getattr(instance, field.attname)) | |
+ possible_parent_link = opts.get_ancestor_link(field.model) | |
+ if (not possible_parent_link or | |
+ possible_parent_link.primary_key or | |
+ possible_parent_link.model._meta.abstract): | |
+ ret.append(instance.pk) | |
+ continue | |
+ ret.append(getattr(instance, field.attname)) | |
return tuple(ret) | |
def get_attname_column(self): | |
@@ -1018,7 +1468,7 @@ | |
def get_joining_columns(self, reverse_join=False): | |
source = self.reverse_related_fields if reverse_join else self.related_fields | |
- return tuple([(lhs_field.column, rhs_field.column) for lhs_field, rhs_field in source]) | |
+ return tuple((lhs_field.column, rhs_field.column) for lhs_field, rhs_field in source) | |
def get_reverse_joining_columns(self): | |
return self.get_joining_columns(reverse_join=True) | |
@@ -1068,14 +1518,16 @@ | |
pathinfos = [PathInfo(from_opts, opts, (opts.pk,), self.rel, not self.unique, False)] | |
return pathinfos | |
- def get_lookup_constraint(self, constraint_class, alias, targets, sources, lookup_type, | |
+ def get_lookup_constraint(self, constraint_class, alias, targets, sources, lookups, | |
raw_value): | |
- from django.db.models.sql.where import SubqueryConstraint, Constraint, AND, OR | |
+ from django.db.models.sql.where import SubqueryConstraint, AND, OR | |
root_constraint = constraint_class() | |
assert len(targets) == len(sources) | |
+ if len(lookups) > 1: | |
+ raise exceptions.FieldError('Relation fields do not support nested lookups') | |
+ lookup_type = lookups[0] | |
def get_normalized_value(value): | |
- | |
from django.db.models import Model | |
if isinstance(value, Model): | |
value_list = [] | |
@@ -1096,28 +1548,27 @@ | |
[source.name for source in sources], raw_value), | |
AND) | |
elif lookup_type == 'isnull': | |
- root_constraint.add( | |
- (Constraint(alias, targets[0].column, targets[0]), lookup_type, raw_value), AND) | |
+ root_constraint.add(IsNull(Col(alias, targets[0], sources[0]), raw_value), AND) | |
elif (lookup_type == 'exact' or (lookup_type in ['gt', 'lt', 'gte', 'lte'] | |
and not is_multicolumn)): | |
value = get_normalized_value(raw_value) | |
- for index, source in enumerate(sources): | |
+ for target, source, val in zip(targets, sources, value): | |
+ lookup_class = target.get_lookup(lookup_type) | |
root_constraint.add( | |
- (Constraint(alias, targets[index].column, sources[index]), lookup_type, | |
- value[index]), AND) | |
+ lookup_class(Col(alias, target, source), val), AND) | |
elif lookup_type in ['range', 'in'] and not is_multicolumn: | |
values = [get_normalized_value(value) for value in raw_value] | |
value = [val[0] for val in values] | |
- root_constraint.add( | |
- (Constraint(alias, targets[0].column, sources[0]), lookup_type, value), AND) | |
+ lookup_class = targets[0].get_lookup(lookup_type) | |
+ root_constraint.add(lookup_class(Col(alias, targets[0], sources[0]), value), AND) | |
elif lookup_type == 'in': | |
values = [get_normalized_value(value) for value in raw_value] | |
for value in values: | |
value_constraint = constraint_class() | |
- for index, target in enumerate(targets): | |
- value_constraint.add( | |
- (Constraint(alias, target.column, sources[index]), 'exact', value[index]), | |
- AND) | |
+ for source, target, val in zip(sources, targets, value): | |
+ lookup_class = target.get_lookup('exact') | |
+ lookup = lookup_class(Col(alias, target, source), val) | |
+ value_constraint.add(lookup, AND) | |
root_constraint.add(value_constraint, OR) | |
else: | |
raise TypeError('Related Field got invalid lookup: %s' % lookup_type) | |
@@ -1125,10 +1576,10 @@ | |
@property | |
def attnames(self): | |
- return tuple([field.attname for field in self.local_related_fields]) | |
+ return tuple(field.attname for field in self.local_related_fields) | |
def get_defaults(self): | |
- return tuple([field.get_default() for field in self.local_related_fields]) | |
+ return tuple(field.get_default() for field in self.local_related_fields) | |
def contribute_to_class(self, cls, name, virtual_only=False): | |
super(ForeignObject, self).contribute_to_class(cls, name, virtual_only=virtual_only) | |
@@ -1138,7 +1589,10 @@ | |
# Internal FK's - i.e., those with a related name ending with '+' - | |
# and swapped models don't get a related descriptor. | |
if not self.rel.is_hidden() and not related.model._meta.swapped: | |
- setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) | |
+ setattr(cls, related.get_accessor_name(), self.related_accessor_class(related)) | |
+ # While 'limit_choices_to' might be a callable, simply pass | |
+ # it along for later - this is too early because it's still | |
+ # model load time. | |
if self.rel.limit_choices_to: | |
cls._meta.related_fkey_lookups.append(self.rel.limit_choices_to) | |
@@ -1153,11 +1607,10 @@ | |
def __init__(self, to, to_field=None, rel_class=ManyToOneRel, | |
db_constraint=True, **kwargs): | |
try: | |
- to_name = to._meta.object_name.lower() | |
+ to._meta.model_name | |
except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT | |
assert isinstance(to, six.string_types), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT) | |
else: | |
- assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) | |
# For backwards compatibility purposes, we need to *try* and set | |
# the to_field during FK construction. It won't be guaranteed to | |
# be correct until contribute_to_class is called. Refs #12190. | |
@@ -1178,6 +1631,51 @@ | |
) | |
super(ForeignKey, self).__init__(to, ['self'], [to_field], **kwargs) | |
+ def check(self, **kwargs): | |
+ errors = super(ForeignKey, self).check(**kwargs) | |
+ errors.extend(self._check_on_delete()) | |
+ return errors | |
+ | |
+ def _check_on_delete(self): | |
+ on_delete = getattr(self.rel, 'on_delete', None) | |
+ if on_delete == SET_NULL and not self.null: | |
+ return [ | |
+ checks.Error( | |
+ 'Field specifies on_delete=SET_NULL, but cannot be null.', | |
+ hint='Set null=True argument on the field, or change the on_delete rule.', | |
+ obj=self, | |
+ id='fields.E320', | |
+ ) | |
+ ] | |
+ elif on_delete == SET_DEFAULT and not self.has_default(): | |
+ return [ | |
+ checks.Error( | |
+ 'Field specifies on_delete=SET_DEFAULT, but has no default value.', | |
+ hint='Set a default value, or change the on_delete rule.', | |
+ obj=self, | |
+ id='fields.E321', | |
+ ) | |
+ ] | |
+ else: | |
+ return [] | |
+ | |
+ def deconstruct(self): | |
+ name, path, args, kwargs = super(ForeignKey, self).deconstruct() | |
+ del kwargs['to_fields'] | |
+ del kwargs['from_fields'] | |
+ # Handle the simpler arguments | |
+ if self.db_index: | |
+ del kwargs['db_index'] | |
+ else: | |
+ kwargs['db_index'] = False | |
+ if self.db_constraint is not True: | |
+ kwargs['db_constraint'] = self.db_constraint | |
+ # Rel needs more work. | |
+ to_meta = getattr(self.rel.to, "_meta", None) | |
+ if self.rel.field_name and (not to_meta or (to_meta.pk and self.rel.field_name != to_meta.pk.name)): | |
+ kwargs['to_field'] = self.rel.field_name | |
+ return name, path, args, kwargs | |
+ | |
@property | |
def related_field(self): | |
return self.foreign_related_fields[0] | |
@@ -1200,9 +1698,9 @@ | |
using = router.db_for_read(model_instance.__class__, instance=model_instance) | |
qs = self.rel.to._default_manager.using(using).filter( | |
- **{self.rel.field_name: value} | |
- ) | |
- qs = qs.complex_filter(self.rel.limit_choices_to) | |
+ **{self.rel.field_name: value} | |
+ ) | |
+ qs = qs.complex_filter(self.get_limit_choices_to()) | |
if not qs.exists(): | |
raise exceptions.ValidationError( | |
self.error_messages['invalid'], | |
@@ -1229,11 +1727,12 @@ | |
return field_default | |
def get_db_prep_save(self, value, connection): | |
- if value == '' or value == None: | |
+ if value is None or (value == '' and | |
+ (not self.related_field.empty_strings_allowed or | |
+ connection.features.interprets_empty_strings_as_nulls)): | |
return None | |
else: | |
- return self.related_field.get_db_prep_save(value, | |
- connection=connection) | |
+ return self.related_field.get_db_prep_save(value, connection=connection) | |
def value_to_string(self, obj): | |
if not obj: | |
@@ -1260,7 +1759,7 @@ | |
(self.name, self.rel.to)) | |
defaults = { | |
'form_class': forms.ModelChoiceField, | |
- 'queryset': self.rel.to._default_manager.using(db).complex_filter(self.rel.limit_choices_to), | |
+ 'queryset': self.rel.to._default_manager.using(db), | |
'to_field_name': self.rel.field_name, | |
} | |
defaults.update(kwargs) | |
@@ -1281,6 +1780,9 @@ | |
return IntegerField().db_type(connection=connection) | |
return rel_field.db_type(connection=connection) | |
+ def db_parameters(self, connection): | |
+ return {"type": self.db_type(connection), "check": []} | |
+ | |
class OneToOneField(ForeignKey): | |
""" | |
@@ -1289,15 +1791,18 @@ | |
always returns the object pointed to (since there will only ever be one), | |
rather than returning a list. | |
""" | |
+ related_accessor_class = SingleRelatedObjectDescriptor | |
description = _("One-to-one relationship") | |
def __init__(self, to, to_field=None, **kwargs): | |
kwargs['unique'] = True | |
super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs) | |
- def contribute_to_related_class(self, cls, related): | |
- setattr(cls, related.get_accessor_name(), | |
- SingleRelatedObjectDescriptor(related)) | |
+ def deconstruct(self): | |
+ name, path, args, kwargs = super(OneToOneField, self).deconstruct() | |
+ if "unique" in kwargs: | |
+ del kwargs['unique'] | |
+ return name, path, args, kwargs | |
def formfield(self, **kwargs): | |
if self.rel.parent_link: | |
@@ -1336,7 +1841,7 @@ | |
else: | |
from_ = klass._meta.model_name | |
to = to.lower() | |
- meta = type('Meta', (object,), { | |
+ meta = type(str('Meta'), (object,), { | |
'db_table': field._get_m2m_db_table(klass._meta), | |
'managed': managed, | |
'auto_created': klass, | |
@@ -1345,6 +1850,7 @@ | |
'unique_together': (from_, to), | |
'verbose_name': '%(from)s-%(to)s relationship' % {'from': from_, 'to': to}, | |
'verbose_name_plural': '%(from)s-%(to)s relationships' % {'from': from_, 'to': to}, | |
+ 'apps': field.model._meta.apps, | |
}) | |
# Construct and return the new class. | |
return type(str(name), (models.Model,), { | |
@@ -1358,14 +1864,12 @@ | |
class ManyToManyField(RelatedField): | |
description = _("Many-to-many relationship") | |
- def __init__(self, to, db_constraint=True, **kwargs): | |
+ def __init__(self, to, db_constraint=True, swappable=True, **kwargs): | |
try: | |
- assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) | |
+ to._meta | |
except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT | |
assert isinstance(to, six.string_types), "%s(%r) is invalid. First parameter to ManyToManyField must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT) | |
- # Python 2.6 and earlier require dictionary keys to be of str type, | |
- # not unicode and class names must be ASCII (in Python 2.x), so we | |
- # forcibly coerce it here (breaks early if there's a problem). | |
+ # Class names must be ASCII in Python 2.x, so we forcibly coerce it here to break early if there's a problem. | |
to = str(to) | |
kwargs['verbose_name'] = kwargs.get('verbose_name', None) | |
@@ -1375,14 +1879,267 @@ | |
limit_choices_to=kwargs.pop('limit_choices_to', None), | |
symmetrical=kwargs.pop('symmetrical', to == RECURSIVE_RELATIONSHIP_CONSTANT), | |
through=kwargs.pop('through', None), | |
+ through_fields=kwargs.pop('through_fields', None), | |
db_constraint=db_constraint, | |
) | |
+ self.swappable = swappable | |
self.db_table = kwargs.pop('db_table', None) | |
if kwargs['rel'].through is not None: | |
assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used." | |
super(ManyToManyField, self).__init__(**kwargs) | |
+ | |
+ def check(self, **kwargs): | |
+ errors = super(ManyToManyField, self).check(**kwargs) | |
+ errors.extend(self._check_unique(**kwargs)) | |
+ errors.extend(self._check_relationship_model(**kwargs)) | |
+ return errors | |
+ | |
+ def _check_unique(self, **kwargs): | |
+ if self.unique: | |
+ return [ | |
+ checks.Error( | |
+ 'ManyToManyFields cannot be unique.', | |
+ hint=None, | |
+ obj=self, | |
+ id='fields.E330', | |
+ ) | |
+ ] | |
+ return [] | |
+ | |
+ def _check_relationship_model(self, from_model=None, **kwargs): | |
+ if hasattr(self.rel.through, '_meta'): | |
+ qualified_model_name = "%s.%s" % ( | |
+ self.rel.through._meta.app_label, self.rel.through.__name__) | |
+ else: | |
+ qualified_model_name = self.rel.through | |
+ | |
+ errors = [] | |
+ | |
+ if self.rel.through not in apps.get_models(include_auto_created=True): | |
+ # The relationship model is not installed. | |
+ errors.append( | |
+ checks.Error( | |
+ ("Field specifies a many-to-many relation through model " | |
+ "'%s', which has not been installed.") % | |
+ qualified_model_name, | |
+ hint=None, | |
+ obj=self, | |
+ id='fields.E331', | |
+ ) | |
+ ) | |
+ | |
+ else: | |
+ | |
+ assert from_model is not None, \ | |
+ "ManyToManyField with intermediate " \ | |
+ "tables cannot be checked if you don't pass the model " \ | |
+ "where the field is attached to." | |
+ | |
+ # Set some useful local variables | |
+ to_model = self.rel.to | |
+ from_model_name = from_model._meta.object_name | |
+ if isinstance(to_model, six.string_types): | |
+ to_model_name = to_model | |
+ else: | |
+ to_model_name = to_model._meta.object_name | |
+ relationship_model_name = self.rel.through._meta.object_name | |
+ self_referential = from_model == to_model | |
+ | |
+ # Check symmetrical attribute. | |
+ if (self_referential and self.rel.symmetrical and | |
+ not self.rel.through._meta.auto_created): | |
+ errors.append( | |
+ checks.Error( | |
+ 'Many-to-many fields with intermediate tables must not be symmetrical.', | |
+ hint=None, | |
+ obj=self, | |
+ id='fields.E332', | |
+ ) | |
+ ) | |
+ | |
+ # Count foreign keys in intermediate model | |
+ if self_referential: | |
+ seen_self = sum(from_model == getattr(field.rel, 'to', None) | |
+ for field in self.rel.through._meta.fields) | |
+ | |
+ if seen_self > 2 and not self.rel.through_fields: | |
+ errors.append( | |
+ checks.Error( | |
+ ("The model is used as an intermediate model by " | |
+ "'%s', but it has more than two foreign keys " | |
+ "to '%s', which is ambiguous. You must specify " | |
+ "which two foreign keys Django should use via the " | |
+ "through_fields keyword argument.") % (self, from_model_name), | |
+ hint=("Use through_fields to specify which two " | |
+ "foreign keys Django should use."), | |
+ obj=self.rel.through, | |
+ id='fields.E333', | |
+ ) | |
+ ) | |
+ | |
+ else: | |
+ # Count foreign keys in relationship model | |
+ seen_from = sum(from_model == getattr(field.rel, 'to', None) | |
+ for field in self.rel.through._meta.fields) | |
+ seen_to = sum(to_model == getattr(field.rel, 'to', None) | |
+ for field in self.rel.through._meta.fields) | |
+ | |
+ if seen_from > 1 and not self.rel.through_fields: | |
+ errors.append( | |
+ checks.Error( | |
+ ("The model is used as an intermediate model by " | |
+ "'%s', but it has more than one foreign key " | |
+ "from '%s', which is ambiguous. You must specify " | |
+ "which foreign key Django should use via the " | |
+ "through_fields keyword argument.") % (self, from_model_name), | |
+ hint=('If you want to create a recursive relationship, ' | |
+ 'use ForeignKey("self", symmetrical=False, ' | |
+ 'through="%s").') % relationship_model_name, | |
+ obj=self, | |
+ id='fields.E334', | |
+ ) | |
+ ) | |
+ | |
+ if seen_to > 1 and not self.rel.through_fields: | |
+ errors.append( | |
+ checks.Error( | |
+ ("The model is used as an intermediate model by " | |
+ "'%s', but it has more than one foreign key " | |
+ "to '%s', which is ambiguous. You must specify " | |
+ "which foreign key Django should use via the " | |
+ "through_fields keyword argument.") % (self, to_model_name), | |
+ hint=('If you want to create a recursive ' | |
+ 'relationship, use ForeignKey("self", ' | |
+ 'symmetrical=False, through="%s").') % relationship_model_name, | |
+ obj=self, | |
+ id='fields.E335', | |
+ ) | |
+ ) | |
+ | |
+ if seen_from == 0 or seen_to == 0: | |
+ errors.append( | |
+ checks.Error( | |
+ ("The model is used as an intermediate model by " | |
+ "'%s', but it does not have a foreign key to '%s' or '%s'.") % ( | |
+ self, from_model_name, to_model_name | |
+ ), | |
+ hint=None, | |
+ obj=self.rel.through, | |
+ id='fields.E336', | |
+ ) | |
+ ) | |
+ | |
+ # Validate `through_fields` | |
+ if self.rel.through_fields is not None: | |
+ # Validate that we're given an iterable of at least two items | |
+ # and that none of them is "falsy" | |
+ if not (len(self.rel.through_fields) >= 2 and | |
+ self.rel.through_fields[0] and self.rel.through_fields[1]): | |
+ errors.append( | |
+ checks.Error( | |
+ ("Field specifies 'through_fields' but does not " | |
+ "provide the names of the two link fields that should be " | |
+ "used for the relation through model " | |
+ "'%s'.") % qualified_model_name, | |
+ hint=("Make sure you specify 'through_fields' as " | |
+ "through_fields=('field1', 'field2')"), | |
+ obj=self, | |
+ id='fields.E337', | |
+ ) | |
+ ) | |
+ | |
+ # Validate the given through fields -- they should be actual | |
+ # fields on the through model, and also be foreign keys to the | |
+ # expected models | |
+ else: | |
+ assert from_model is not None, \ | |
+ "ManyToManyField with intermediate " \ | |
+ "tables cannot be checked if you don't pass the model " \ | |
+ "where the field is attached to." | |
+ | |
+ source, through, target = from_model, self.rel.through, self.rel.to | |
+ source_field_name, target_field_name = self.rel.through_fields[:2] | |
+ | |
+ for field_name, related_model in ((source_field_name, source), | |
+ (target_field_name, target)): | |
+ | |
+ possible_field_names = [] | |
+ for f in through._meta.fields: | |
+ if hasattr(f, 'rel') and getattr(f.rel, 'to', None) == related_model: | |
+ possible_field_names.append(f.name) | |
+ if possible_field_names: | |
+ hint = ("Did you mean one of the following foreign " | |
+ "keys to '%s': %s?") % (related_model._meta.object_name, | |
+ ', '.join(possible_field_names)) | |
+ else: | |
+ hint = None | |
+ | |
+ try: | |
+ field = through._meta.get_field(field_name) | |
+ except FieldDoesNotExist: | |
+ errors.append( | |
+ checks.Error( | |
+ ("The intermediary model '%s' has no field '%s'.") % ( | |
+ qualified_model_name, field_name), | |
+ hint=hint, | |
+ obj=self, | |
+ id='fields.E338', | |
+ ) | |
+ ) | |
+ else: | |
+ if not (hasattr(field, 'rel') and | |
+ getattr(field.rel, 'to', None) == related_model): | |
+ errors.append( | |
+ checks.Error( | |
+ "'%s.%s' is not a foreign key to '%s'." % ( | |
+ through._meta.object_name, field_name, | |
+ related_model._meta.object_name), | |
+ hint=hint, | |
+ obj=self, | |
+ id='fields.E339', | |
+ ) | |
+ ) | |
+ | |
+ return errors | |
+ | |
+ def deconstruct(self): | |
+ name, path, args, kwargs = super(ManyToManyField, self).deconstruct() | |
+ # Handle the simpler arguments | |
+ if self.db_table is not None: | |
+ kwargs['db_table'] = self.db_table | |
+ if self.rel.db_constraint is not True: | |
+ kwargs['db_constraint'] = self.rel.db_constraint | |
+ if self.rel.related_name is not None: | |
+ kwargs['related_name'] = force_text(self.rel.related_name) | |
+ if self.rel.related_query_name is not None: | |
+ kwargs['related_query_name'] = self.rel.related_query_name | |
+ # Rel needs more work. | |
+ if isinstance(self.rel.to, six.string_types): | |
+ kwargs['to'] = self.rel.to | |
+ else: | |
+ kwargs['to'] = "%s.%s" % (self.rel.to._meta.app_label, self.rel.to._meta.object_name) | |
+ if getattr(self.rel, 'through', None) is not None: | |
+ if isinstance(self.rel.through, six.string_types): | |
+ kwargs['through'] = self.rel.through | |
+ elif not self.rel.through._meta.auto_created: | |
+ kwargs['through'] = "%s.%s" % (self.rel.through._meta.app_label, self.rel.through._meta.object_name) | |
+ # If swappable is True, then see if we're actually pointing to the target | |
+ # of a swap. | |
+ swappable_setting = self.swappable_setting | |
+ if swappable_setting is not None: | |
+ # If it's already a settings reference, error | |
+ if hasattr(kwargs['to'], "setting_name"): | |
+ if kwargs['to'].setting_name != swappable_setting: | |
+ raise ValueError("Cannot deconstruct a ManyToManyField pointing to a model that is swapped in place of more than one model (%s and %s)" % (kwargs['to'].setting_name, swappable_setting)) | |
+ # Set it | |
+ from django.db.migrations.writer import SettingsReference | |
+ kwargs['to'] = SettingsReference( | |
+ kwargs['to'], | |
+ swappable_setting, | |
+ ) | |
+ return name, path, args, kwargs | |
def _get_path_info(self, direct=False): | |
""" | |
@@ -1418,7 +2175,7 @@ | |
elif self.db_table: | |
return self.db_table | |
else: | |
- return util.truncate_name('%s_%s' % (opts.db_table, self.name), | |
+ return utils.truncate_name('%s_%s' % (opts.db_table, self.name), | |
connection.ops.max_name_length()) | |
def _get_m2m_attr(self, related, attr): | |
@@ -1426,8 +2183,13 @@ | |
cache_attr = '_m2m_%s_cache' % attr | |
if hasattr(self, cache_attr): | |
return getattr(self, cache_attr) | |
+ if self.rel.through_fields is not None: | |
+ link_field_name = self.rel.through_fields[0] | |
+ else: | |
+ link_field_name = None | |
for f in self.rel.through._meta.fields: | |
- if hasattr(f, 'rel') and f.rel and f.rel.to == related.model: | |
+ if hasattr(f, 'rel') and f.rel and f.rel.to == related.model and \ | |
+ (link_field_name is None or link_field_name == f.name): | |
setattr(self, cache_attr, getattr(f, attr)) | |
return getattr(self, cache_attr) | |
@@ -1437,9 +2199,13 @@ | |
if hasattr(self, cache_attr): | |
return getattr(self, cache_attr) | |
found = False | |
+ if self.rel.through_fields is not None: | |
+ link_field_name = self.rel.through_fields[1] | |
+ else: | |
+ link_field_name = None | |
for f in self.rel.through._meta.fields: | |
if hasattr(f, 'rel') and f.rel and f.rel.to == related.parent_model: | |
- if related.model == related.parent_model: | |
+ if link_field_name is None and related.model == related.parent_model: | |
# If this is an m2m-intermediate to self, | |
# the first foreign key you find will be | |
# the source column. Keep searching for | |
@@ -1449,7 +2215,7 @@ | |
break | |
else: | |
found = True | |
- else: | |
+ elif link_field_name is None or link_field_name == f.name: | |
setattr(self, cache_attr, getattr(f, attr)) | |
break | |
return getattr(self, cache_attr) | |
@@ -1532,7 +2298,7 @@ | |
db = kwargs.pop('using', None) | |
defaults = { | |
'form_class': forms.ModelMultipleChoiceField, | |
- 'queryset': self.rel.to._default_manager.using(db).complex_filter(self.rel.limit_choices_to) | |
+ 'queryset': self.rel.to._default_manager.using(db), | |
} | |
defaults.update(kwargs) | |
# If initial is passed in, it's a list of related objects, but the | |
@@ -1543,3 +2309,11 @@ | |
initial = initial() | |
defaults['initial'] = [i._get_pk_val() for i in initial] | |
return super(ManyToManyField, self).formfield(**defaults) | |
+ | |
+ def db_type(self, connection): | |
+ # A ManyToManyField is not represented by a single column, | |
+ # so return None. | |
+ return None | |
+ | |
+ def db_parameters(self, connection): | |
+ return {"type": None, "check": None} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment