Skip to content

Instantly share code, notes, and snippets.

@loic
Created January 27, 2015 20:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save loic/b27b6640f13cc838d23a to your computer and use it in GitHub Desktop.
Save loic/b27b6640f13cc838d23a to your computer and use it in GitHub Desktop.
commit 82b4162aebdf319bcc39af342ebee94be8c8eebd
Author: Loic Bistuer <loic.bistuer@gmail.com>
Date: Wed Jan 28 03:12:24 2015 +0700
WIP
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index d917b01..d5bc763 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -806,7 +806,7 @@ class ForeignRelatedObjectsDescriptor(object):
# some other model. In the example "poll.choice_set", the choice_set
# attribute is a ForeignRelatedObjectsDescriptor instance.
def __init__(self, related):
- self.related = related # RelatedObject instance
+ self.related = related
def __get__(self, instance, instance_type=None):
if instance is None:
@@ -839,38 +839,51 @@ class ForeignRelatedObjectsDescriptor(object):
)
-def create_many_related_manager(superclass, rel):
- """Creates a manager that subclasses 'superclass' (which is a Manager)
- and adds behavior for many-to-many related objects."""
+def create_many_related_manager(superclass, rel, reverse):
+ """
+ Creates a manager that subclasses 'superclass' (which is a Manager)
+ and adds behavior for many-to-many related objects.
+ """
+
class ManyRelatedManager(superclass):
- def __init__(self, model=None, query_field_name=None, instance=None, symmetrical=None,
- source_field_name=None, target_field_name=None, reverse=False,
- through=None, prefetch_cache_name=None):
+
+ def __init__(self, instance=None):
super(ManyRelatedManager, self).__init__()
- self.model = model
- self.query_field_name = query_field_name
- source_field = through._meta.get_field(source_field_name)
- source_related_fields = source_field.related_fields
+ self.instance = instance
- self.core_filters = {}
- for lh_field, rh_field in source_related_fields:
- self.core_filters['%s__%s' % (query_field_name, rh_field.name)] = getattr(instance, rh_field.attname)
+ if not reverse:
+ self.model = rel.to
+ self.query_field_name = rel.field.related_query_name()
+ self.prefetch_cache_name = rel.field.name
+ self.source_field_name = rel.field.m2m_field_name()
+ self.target_field_name = rel.field.m2m_reverse_field_name()
+ self.symmetrical = rel.symmetrical
- 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
+ else:
+ self.model = rel.related_model
+ self.query_field_name = rel.field.name
+ self.prefetch_cache_name = rel.field.related_query_name()
+ self.source_field_name = rel.field.m2m_reverse_field_name()
+ self.target_field_name = rel.field.m2m_field_name()
+ self.symmetrical=False
+
+ self.through = rel.through
self.reverse = reverse
- self.through = through
- self.prefetch_cache_name = prefetch_cache_name
- self.related_val = source_field.get_foreign_related_value(instance)
+
+ self.source_field = self.through._meta.get_field(self.source_field_name)
+ self.target_field = self.through._meta.get_field(self.target_field_name)
+
+ self.core_filters = {}
+ for lh_field, rh_field in self.source_field.related_fields:
+ self.core_filters['%s__%s' % (self.query_field_name, rh_field.name)] = getattr(instance, rh_field.attname)
+
+ self.related_val = self.source_field.get_foreign_related_value(instance)
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.' %
- (instance, source_field_name))
+ (instance, self.source_field_name))
+
# Even if this relation is not to pk, we require still pk value.
# The wish is that the instance has been already saved to DB,
# although having a pk value isn't a guarantee of that.
@@ -883,18 +896,8 @@ def create_many_related_manager(superclass, rel):
# 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,
- )
+ manager_class = create_many_related_manager(manager.__class__, rel, self.reverse)
+ return manager_class(instance=self.instance)
do_not_call_in_templates = True
def _build_remove_filters(self, removed_vals):
@@ -959,7 +962,7 @@ def create_many_related_manager(superclass, rel):
)
def add(self, *objs):
- if not rel.through._meta.auto_created:
+ if not self.through._meta.auto_created:
opts = self.through._meta
raise AttributeError(
"Cannot use add() on a ManyToManyField which specifies an "
@@ -977,7 +980,7 @@ def create_many_related_manager(superclass, rel):
add.alters_data = True
def remove(self, *objs):
- if not rel.through._meta.auto_created:
+ if not self.through._meta.auto_created:
opts = self.through._meta
raise AttributeError(
"Cannot use remove() on a ManyToManyField which specifies "
@@ -1144,111 +1147,36 @@ def create_many_related_manager(superclass, rel):
class ManyRelatedObjectsDescriptor(object):
# This class provides the functionality that makes the related-object
# managers available as attributes on a model class, for fields that have
- # multiple "remote" values and have a ManyToManyField pointed at them by
- # some other model (rather than having a ManyToManyField themselves).
- # In the example "publication.article_set", the article_set attribute is a
- # ManyRelatedObjectsDescriptor instance.
- def __init__(self, related):
- self.related = related # RelatedObject instance
-
- @cached_property
- def related_manager_cls(self):
- # Dynamically create a class that subclasses the related
- # model's default manager.
- return create_many_related_manager(
- self.related.related_model._default_manager.__class__,
- self.related.field.rel
- )
-
- def __get__(self, instance, instance_type=None):
- if instance is None:
- return self
-
- rel_model = self.related.related_model
-
- manager = self.related_manager_cls(
- model=rel_model,
- query_field_name=self.related.field.name,
- prefetch_cache_name=self.related.field.related_query_name(),
- instance=instance,
- symmetrical=False,
- source_field_name=self.related.field.m2m_reverse_field_name(),
- target_field_name=self.related.field.m2m_field_name(),
- reverse=True,
- through=self.related.field.rel.through,
- )
-
- return manager
-
- def __set__(self, instance, value):
- if not self.related.field.rel.through._meta.auto_created:
- opts = self.related.field.rel.through._meta
- raise AttributeError(
- "Cannot set values on a ManyToManyField which specifies an "
- "intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)
- )
-
- # Force evaluation of `value` in case it's a queryset whose
- # value could be affected by `manager.clear()`. Refs #19816.
- value = tuple(value)
-
- manager = self.__get__(instance)
- db = router.db_for_write(manager.through, instance=manager.instance)
- with transaction.atomic(using=db, savepoint=False):
- manager.clear()
- manager.add(*value)
-
-
-class ReverseManyRelatedObjectsDescriptor(object):
- # This class provides the functionality that makes the related-object
- # managers available as attributes on a model class, for fields that have
- # multiple "remote" values and have a ManyToManyField defined in their
- # model (rather than having another model pointed *at* them).
- # In the example "article.publications", the publications attribute is a
- # ReverseManyRelatedObjectsDescriptor instance.
- def __init__(self, m2m_field):
- self.field = m2m_field
+ # multiple "remote" values
+ def __init__(self, rel, reverse=False):
+ self.rel = rel
+ self.reverse = reverse
@property
def through(self):
- # through is provided so that you have easy access to the through
+ # This is provided so that you have easy access to the through
# model (Book.authors.through) for inlines, etc. This is done as
# a property to ensure that the fully resolved value is returned.
- return self.field.rel.through
+ return self.rel.through
@cached_property
def related_manager_cls(self):
- # Dynamically create a class that subclasses the related model's
- # default manager.
- return create_many_related_manager(
- self.field.rel.to._default_manager.__class__,
- self.field.rel
- )
+ # Dynamically create a class that subclasses the related
+ # model's default manager.
+ model = self.rel.related_model if self.reverse else self.rel.to
+ return create_many_related_manager(model._default_manager.__class__, self.rel, self.reverse)
def __get__(self, instance, instance_type=None):
if instance is None:
return self
-
- manager = self.related_manager_cls(
- model=self.field.rel.to,
- query_field_name=self.field.related_query_name(),
- prefetch_cache_name=self.field.name,
- instance=instance,
- symmetrical=self.field.rel.symmetrical,
- source_field_name=self.field.m2m_field_name(),
- target_field_name=self.field.m2m_reverse_field_name(),
- reverse=False,
- through=self.field.rel.through,
- )
-
- return manager
+ return self.related_manager_cls(instance=instance)
def __set__(self, instance, value):
- if not self.field.rel.through._meta.auto_created:
- opts = self.field.rel.through._meta
+ if not self.rel.through._meta.auto_created:
+ opts = self.rel.through._meta
raise AttributeError(
"Cannot set values on a ManyToManyField which specifies an "
- "intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)
+ "intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)
)
# Force evaluation of `value` in case it's a queryset whose
@@ -1474,10 +1402,6 @@ class ManyToManyRel(ForeignObjectRel):
self.symmetrical = symmetrical
self.db_constraint = db_constraint
- def is_hidden(self):
- "Should the related object be hidden?"
- return self.related_name is not None and self.related_name[-1] == '+'
-
def get_related_field(self):
"""
Returns the field in the 'to' object to which this relationship is tied.
@@ -2567,7 +2491,7 @@ class ManyToManyField(RelatedField):
self.rel.through = create_many_to_many_intermediary_model(self, cls)
# Add the descriptor for the m2m relation
- setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self))
+ setattr(cls, self.name, ManyRelatedObjectsDescriptor(self.rel))
# Set up the accessor for the m2m table name for the relation
self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta)
@@ -2583,7 +2507,7 @@ class ManyToManyField(RelatedField):
# Internal M2Ms (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.related_model._meta.swapped:
- setattr(cls, related.get_accessor_name(), ManyRelatedObjectsDescriptor(related))
+ setattr(cls, related.get_accessor_name(), ManyRelatedObjectsDescriptor(related, reverse=True))
# Set up the accessors for the column names on the m2m table
self.m2m_column_name = curry(self._get_m2m_attr, related, 'column')
diff --git a/tests/many_to_many/tests.py b/tests/many_to_many/tests.py
index adb8978..8626a44 100644
--- a/tests/many_to_many/tests.py
+++ b/tests/many_to_many/tests.py
@@ -385,7 +385,7 @@ class ManyToManyTests(TestCase):
def test_reverse_assign_with_queryset(self):
# Ensure that querysets used in M2M assignments are pre-evaluated
# so their value isn't affected by the clearing operation in
- # ReverseManyRelatedObjectsDescriptor.__set__. Refs #19816.
+ # ManyRelatedObjectsDescriptor.__set__. Refs #19816.
self.p1.article_set = [self.a1, self.a2]
qs = self.p1.article_set.filter(headline='Django lets you build Web apps easily')
diff --git a/tests/schema/fields.py b/tests/schema/fields.py
index 4f70c96..6e58069 100644
--- a/tests/schema/fields.py
+++ b/tests/schema/fields.py
@@ -1,9 +1,10 @@
from django.db.models.fields.related import (
create_many_to_many_intermediary_model,
ManyToManyField, ManyToManyRel, RelatedField,
- RECURSIVE_RELATIONSHIP_CONSTANT, ReverseManyRelatedObjectsDescriptor,
+ RECURSIVE_RELATIONSHIP_CONSTANT, ManyRelatedObjectsDescriptor,
)
+
from django.utils.functional import curry
@@ -41,7 +42,7 @@ class CustomManyToManyField(RelatedField):
super(CustomManyToManyField, self).contribute_to_class(cls, name, **kwargs)
if not self.rel.through and not cls._meta.abstract and not cls._meta.swapped:
self.rel.through = create_many_to_many_intermediary_model(self, cls)
- setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self))
+ setattr(cls, self.name, ManyRelatedObjectsDescriptor(self))
self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta)
def get_internal_type(self):
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment