Ticket #9475: 9475.m2m_add_remove.r12009.diff

File 9475.m2m_add_remove.r12009.diff, 22.7 KB (added by Johannes Dollinger, 15 years ago)
  • tests/modeltests/m2m_through/models.py

     
    122122
    123123### Forward Descriptors Tests ###
    124124
    125 # Due to complications with adding via an intermediary model,
    126 # the add method is not provided.
    127 >>> rock.members.add(bob)
    128 Traceback (most recent call last):
    129 ...
    130 AttributeError: 'ManyRelatedManager' object has no attribute 'add'
    131 
    132 # Create is also disabled as it suffers from the same problems as add.
    133 >>> rock.members.create(name='Anne')
    134 Traceback (most recent call last):
    135 ...
    136 AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead.
    137 
    138 # Remove has similar complications, and is not provided either.
    139 >>> rock.members.remove(jim)
    140 Traceback (most recent call last):
    141 ...
    142 AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
    143 
    144 # Here we back up the list of all members of Rock.
    145 >>> backup = list(rock.members.all())
    146 
    147 # ...and we verify that it has worked.
    148 >>> backup
    149 [<Person: Jane>, <Person: Jim>]
    150 
    151 # The clear function should still work.
     125# The clear function should work.
    152126>>> rock.members.clear()
    153127
    154128# Now there will be no members of Rock.
    155129>>> rock.members.all()
    156130[]
    157131
    158 # Assignment should not work with models specifying a through model for many of
    159 # the same reasons as adding.
    160 >>> rock.members = backup
    161 Traceback (most recent call last):
    162 ...
    163 AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use m2m_through.Membership's Manager instead.
    164 
    165132# Let's re-save those instances that we've cleared.
    166133>>> m1.save()
    167134>>> m2.save()
     
    173140
    174141### Reverse Descriptors Tests ###
    175142
    176 # Due to complications with adding via an intermediary model,
    177 # the add method is not provided.
    178 >>> bob.group_set.add(rock)
    179 Traceback (most recent call last):
    180 ...
    181 AttributeError: 'ManyRelatedManager' object has no attribute 'add'
    182 
    183 # Create is also disabled as it suffers from the same problems as add.
    184 >>> bob.group_set.create(name='Funk')
    185 Traceback (most recent call last):
    186 ...
    187 AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead.
    188 
    189 # Remove has similar complications, and is not provided either.
    190 >>> jim.group_set.remove(rock)
    191 Traceback (most recent call last):
    192 ...
    193 AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
    194 
    195 # Here we back up the list of all of Jim's groups.
    196 >>> backup = list(jim.group_set.all())
    197 >>> backup
    198 [<Group: Rock>, <Group: Roll>]
    199 
    200 # The clear function should still work.
     143# The clear function should work.
    201144>>> jim.group_set.clear()
    202145
    203146# Now Jim will be in no groups.
    204147>>> jim.group_set.all()
    205148[]
    206149
    207 # Assignment should not work with models specifying a through model for many of
    208 # the same reasons as adding.
    209 >>> jim.group_set = backup
    210 Traceback (most recent call last):
    211 ...
    212 AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use m2m_through.Membership's Manager instead.
    213 
    214150# Let's re-save those instances that we've cleared.
    215151>>> m1.save()
    216152>>> m4.save()
  • tests/modeltests/m2m_add_and_remove/__init__.py

     
     1
     2
  • tests/modeltests/m2m_add_and_remove/models.py

     
     1from django.db import models
     2from django.test import TestCase
     3
     4
     5class M(models.Model):
     6    default = models.ManyToManyField("R", related_name="default_m_set")
     7    default_cannot_remove = models.ManyToManyField("R", can_remove=False, related_name="default_cannot_remove_m_set")
     8    default_cannot_add = models.ManyToManyField("R", can_add=False, related_name="default_cannot_add_m_set")
     9    through_default = models.ManyToManyField("R", through="ThroughDefault", related_name="through_default_m_set")
     10    through_auto = models.ManyToManyField("R", through="ThroughAuto", related_name="through_auto_m_set")
     11    through_ut = models.ManyToManyField("R", through="ThroughUT", related_name="through_ut_m_set")
     12    through_can_add = models.ManyToManyField("R", can_add=True, through="ThroughCanAdd", related_name="through_can_add_m_set")
     13    through_can_remove = models.ManyToManyField("R", can_remove=True, through="ThroughCanRemove", related_name="through_can_remove_m_set")
     14
     15class R(models.Model):
     16    name = models.CharField(max_length=30)
     17
     18class Through(models.Model):
     19    m = models.ForeignKey(M, related_name="%(class)s_set")
     20    r = models.ForeignKey(R, related_name="%(class)s_set")
     21
     22    class Meta:
     23        abstract = True
     24       
     25class ThroughDefault(Through):
     26    extra = models.CharField(max_length=10)
     27
     28class ThroughAuto(Through):
     29    ctime = models.DateTimeField(auto_now_add=True)
     30    mtime = models.DateTimeField(auto_now=True)
     31    default = models.IntegerField(default=42)
     32    null = models.DateTimeField(null=True)
     33   
     34class ThroughUT(Through):
     35    extra = models.CharField(max_length=10)
     36
     37    class Meta:
     38        unique_together = ('m', 'r')
     39       
     40class ThroughCanRemove(Through):
     41    extra = models.CharField(max_length=10)
     42
     43class ThroughCanAdd(Through):
     44    extra = models.CharField(max_length=10)
     45   
     46    def save(self, **kwargs):
     47        self.extra = "foo"
     48        return super(ThroughCanAdd, self).save(**kwargs)
     49
     50class M2mAddRemoveTests(TestCase):
     51    def assert_cannot_remove(self, name):
     52        m = M.objects.create()
     53        r = R.objects.create()
     54        manager = getattr(m, name)
     55        reverse_manager = getattr(r, "%s_m_set" % name)
     56        self.assertRaises(AttributeError, getattr, manager, 'remove')
     57        self.assertRaises(AttributeError, getattr, reverse_manager, 'remove')
     58       
     59    def assert_cannot_add(self, name):
     60        reverse_name = "%s_m_set" % name
     61        m = M.objects.create()
     62        r = R.objects.create()
     63        manager = getattr(m, name)
     64        reverse_manager = getattr(r, reverse_name)
     65        self.assertRaises(AttributeError, getattr, manager, 'add')
     66        self.assertRaises(AttributeError, getattr, reverse_manager, 'add')
     67        self.assertRaises(AttributeError, manager.create)
     68        self.assertRaises(AttributeError, reverse_manager.create)
     69        def assign():
     70            setattr(m, name, [])
     71        self.assertRaises(AttributeError, assign)
     72        def assign_reverse():
     73            setattr(r, reverse_name, [])
     74        self.assertRaises(AttributeError, assign_reverse)
     75       
     76    def assert_can_add(self, name):
     77        reverse_name = "%s_m_set" % name
     78        m = M.objects.create()
     79        r = R.objects.create()
     80        manager = getattr(m, name)
     81        reverse_manager = getattr(r, reverse_name)
     82       
     83        manager.add(r)
     84        self.failUnlessEqual(list(manager.all()), [r])
     85        self.failUnlessEqual(list(reverse_manager.all()), [m])
     86        manager.add(r)
     87        self.failUnlessEqual(list(manager.all()), [r])
     88        self.failUnlessEqual(list(reverse_manager.all()), [m])
     89        manager.clear()
     90       
     91        reverse_manager.add(m)
     92        self.failUnlessEqual(list(manager.all()), [r])
     93        self.failUnlessEqual(list(reverse_manager.all()), [m])
     94        reverse_manager.add(m)
     95        self.failUnlessEqual(list(manager.all()), [r])
     96        self.failUnlessEqual(list(reverse_manager.all()), [m])
     97        reverse_manager.clear()
     98       
     99        r2 = manager.create()
     100        reverse_manager2 = getattr(r2, reverse_name)
     101        self.failUnlessEqual(list(manager.all()), [r2])
     102        self.failUnlessEqual(list(reverse_manager2.all()), [m])
     103        manager.clear()
     104       
     105        m2 = reverse_manager.create()
     106        manager2 = getattr(m2, name)
     107        self.failUnlessEqual(list(manager2.all()), [r])
     108        self.failUnlessEqual(list(reverse_manager.all()), [m2])
     109        reverse_manager.clear()
     110       
     111        setattr(m, name, [r])
     112        self.failUnlessEqual(list(manager.all()), [r])
     113        manager.clear()
     114       
     115        setattr(r, reverse_name, [m])
     116        self.failUnlessEqual(list(reverse_manager.all()), [m])
     117        reverse_manager.clear()
     118       
     119    def assert_can_remove(self, name, extra):
     120        through = M._meta.get_field(name).rel.through
     121        m = M.objects.create()
     122        r = R.objects.create()
     123       
     124        def fill():           
     125            for extra_kwargs in extra:
     126                kwargs = {'m': m, 'r': r}
     127                kwargs.update(extra_kwargs)
     128                through.objects.create(**kwargs)
     129
     130        manager = getattr(m, name)
     131        reverse_manager = getattr(r, "%s_m_set" % name)
     132
     133        fill()
     134        manager.remove(r)
     135        self.failIf(manager.exists())
     136        self.failIf(reverse_manager.exists())
     137       
     138        fill()
     139        reverse_manager.remove(m)
     140        self.failIf(reverse_manager.exists())
     141        self.failIf(manager.exists())
     142       
     143    def _test_managers(self, name, can_remove=False, can_add=False, extra=()):
     144        if can_add:
     145            self.assert_can_add(name)
     146        else:
     147            self.assert_cannot_add(name)
     148        if can_remove:
     149            self.assert_can_remove(name, extra)
     150        else:
     151            self.assert_cannot_remove(name)
     152
     153    def test_default(self):
     154        self._test_managers('default', can_add=True, can_remove=True, extra=[{}])
     155       
     156    def test_default_cannot_remove(self):
     157        self._test_managers('default_cannot_remove', can_add=True, can_remove=False)
     158       
     159    def test_default_cannot_add(self):
     160        self._test_managers('default_cannot_add', can_add=False, can_remove=True, extra=[{}])
     161
     162    def test_through_default(self):
     163        self._test_managers('through_default', can_add=False, can_remove=False)
     164       
     165    def test_through_auto(self):
     166        self._test_managers('through_auto', can_add=True, can_remove=False)
     167       
     168    def test_through_ut(self):
     169        self._test_managers('through_ut', can_add=False, can_remove=True, extra=[{'extra': 'foo'}])
     170       
     171    def test_through_can_add(self):
     172        self._test_managers('through_can_add', can_add=True, can_remove=False)
     173       
     174    def test_through_can_remove(self):
     175        self._test_managers('through_can_remove', can_add=False, can_remove=True, extra=[{'extra': 'foo'}, {'extra': 'bar'}])
     176       
     177 No newline at end of file
  • tests/regressiontests/m2m_through_regress/models.py

     
    8080>>> roll.members.all()
    8181[<Person: Bob>]
    8282
    83 # Error messages use the model name, not repr of the class name
    84 >>> bob.group_set = []
    85 Traceback (most recent call last):
    86 ...
    87 AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use m2m_through_regress.Membership's Manager instead.
    88 
    89 >>> roll.members = []
    90 Traceback (most recent call last):
    91 ...
    92 AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use m2m_through_regress.Membership's Manager instead.
    93 
    94 >>> rock.members.create(name='Anne')
    95 Traceback (most recent call last):
    96 ...
    97 AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model.  Use m2m_through_regress.Membership's Manager instead.
    98 
    99 >>> bob.group_set.create(name='Funk')
    100 Traceback (most recent call last):
    101 ...
    102 AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model.  Use m2m_through_regress.Membership's Manager instead.
    103 
    10483# Now test that the intermediate with a relationship outside
    10584# the current app (i.e., UserMembership) workds
    10685>>> UserMembership.objects.create(user=frank, group=rock)
  • django/db/models/fields/related.py

     
    418418
    419419        return manager
    420420
    421 def create_many_related_manager(superclass, rel=False):
     421def create_many_related_manager(superclass, field):
    422422    """Creates a manager that subclasses 'superclass' (which is a Manager)
    423423    and adds behavior for many-to-many related objects."""
    424     through = rel.through
     424    through = field.rel.through
     425    can_add = field.can_add()
     426    can_remove = field.can_remove()
     427   
    425428    class ManyRelatedManager(superclass):
    426429        def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
    427430                join_table=None, source_field_name=None, target_field_name=None):
     
    440443        def get_query_set(self):
    441444            return superclass.get_query_set(self).using(self.instance._state.db)._next_is_sticky().filter(**(self.core_filters))
    442445
    443         # If the ManyToMany relation has an intermediary model,
    444         # the add and remove methods do not exist.
    445         if rel.through._meta.auto_created:
     446        if can_add:
    446447            def add(self, *objs):
    447448                self._add_items(self.source_field_name, self.target_field_name, *objs)
    448449
     
    451452                    self._add_items(self.target_field_name, self.source_field_name, *objs)
    452453            add.alters_data = True
    453454
     455        if can_remove:
    454456            def remove(self, *objs):
    455457                self._remove_items(self.source_field_name, self.target_field_name, *objs)
    456458
     
    470472        def create(self, **kwargs):
    471473            # This check needs to be done here, since we can't later remove this
    472474            # from the method lookup table, as we do with add and remove.
    473             if not rel.through._meta.auto_created:
     475            if not can_add:
    474476                opts = through._meta
    475                 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)
     477                raise AttributeError("Cannot use create() on this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name))
    476478            new_obj = super(ManyRelatedManager, self).using(self.instance._state.db).create(**kwargs)
    477479            self.add(new_obj)
    478480            return new_obj
    479481        create.alters_data = True
    480482
    481483        def get_or_create(self, **kwargs):
     484            # This check needs to be done here, since we can't later remove this
     485            # from the method lookup table, as we do with add and remove.
     486            if not can_add:
     487                opts = through._meta
     488                raise AttributeError, "Cannot use get_or_create() on this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)
    482489            obj, created = \
    483490                    super(ManyRelatedManager, self).using(self.instance._state.db).get_or_create(**kwargs)
    484491            # We only need to add() if created because if we got an object back
     
    568575        # model's default manager.
    569576        rel_model = self.related.model
    570577        superclass = rel_model._default_manager.__class__
    571         RelatedManager = create_many_related_manager(superclass, self.related.field.rel)
     578        RelatedManager = create_many_related_manager(superclass, self.related.field)
    572579
    573580        manager = RelatedManager(
    574581            model=rel_model,
     
    585592        if instance is None:
    586593            raise AttributeError, "Manager must be accessed via instance"
    587594
    588         if not self.related.field.rel.through._meta.auto_created:
     595        if not self.related.field.can_add():
    589596            opts = self.related.field.rel.through._meta
    590             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)
     597            raise AttributeError, "Cannot set values on this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)
    591598
    592599        manager = self.__get__(instance)
    593600        manager.clear()
     
    618625        # model's default manager.
    619626        rel_model=self.field.rel.to
    620627        superclass = rel_model._default_manager.__class__
    621         RelatedManager = create_many_related_manager(superclass, self.field.rel)
     628        RelatedManager = create_many_related_manager(superclass, self.field)
    622629
    623630        manager = RelatedManager(
    624631            model=rel_model,
     
    635642        if instance is None:
    636643            raise AttributeError, "Manager must be accessed via instance"
    637644
    638         if not self.field.rel.through._meta.auto_created:
     645        if not self.field.can_add():
    639646            opts = self.field.rel.through._meta
    640             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)
     647            raise AttributeError, "Cannot set values a this ManyToManyField.  Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)
    641648
    642649        manager = self.__get__(instance)
    643650        manager.clear()
     
    883890        self.db_table = kwargs.pop('db_table', None)
    884891        if kwargs['rel'].through is not None:
    885892            assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used."
     893           
     894        self._can_add = kwargs.pop('can_add', None)
     895        self._can_remove = kwargs.pop('can_remove', None)
    886896
    887897        Field.__init__(self, **kwargs)
    888898
     
    901911        else:
    902912            return util.truncate_name('%s_%s' % (opts.db_table, self.name),
    903913                                      connection.ops.max_name_length())
    904 
    905     def _get_m2m_attr(self, related, attr):
    906         "Function that can be curried to provide the source column name for the m2m table"
    907         cache_attr = '_m2m_%s_cache' % attr
     914                                     
     915    def _get_intermediary_fields(self, related):
     916        cache_attr = '_m2m_intermediary_fields_cache'
    908917        if hasattr(self, cache_attr):
    909918            return getattr(self, cache_attr)
     919           
     920        candidates = []
     921        related_candidates = []
     922        auto_add = True
    910923        for f in self.rel.through._meta.fields:
    911             if hasattr(f,'rel') and f.rel and f.rel.to == related.model:
    912                 setattr(self, cache_attr, getattr(f, attr))
    913                 return getattr(self, cache_attr)
     924            if hasattr(f,'rel') and f.rel:
     925                if f.rel.to == related.model:
     926                    candidates.append(f)
     927                    continue
     928                elif f.rel.to == related.parent_model:
     929                    related_candidates.append(f)
     930                    continue
     931            if isinstance(f, AutoField) or f.null or f.has_default():
     932                continue
     933            elif getattr(f, 'auto_now_add', False) or getattr(f, 'auto_now', False):
     934                continue
     935            else:
     936                auto_add = False
     937        if related.model == related.parent_model:
     938            # m2m to self
     939            assert len(candidates) == 2, "There are too many ForeignKeys to %s" % related.model
     940            field, related_field = candidates
     941        else:
     942            assert len(candidates) == 1, "There are no ForeignKeys to %s" % related.model
     943            assert len(related_candidates) == 1, "There are no ForeignKeys to %s" % related.parent_model
     944            # TODO: intelligently pick a candidate if there is more than one. For now, just use the first.
     945            field, related_field = candidates[0], related_candidates[0]
    914946
     947        if self._can_add is None:
     948            self._can_add = auto_add
     949
     950        if self._can_remove is None:
     951            self._can_remove = False
     952            unique_together = [frozenset(ut) for ut in self.rel.through._meta.unique_together]               
     953            names = frozenset([field.name, related_field.name])
     954            for ut in unique_together:
     955                if names <= ut:
     956                    self._can_remove = True
     957                    break
     958       
     959        setattr(self, cache_attr, (field, related_field))
     960        return (field, related_field)
     961       
     962    def _get_can_add(self, related):
     963        if self._can_add is None:
     964            self._get_intermediary_fields(related)
     965        return self._can_add
     966       
     967    def _get_can_remove(self, related):
     968        if self._can_remove is None:
     969            self._get_intermediary_fields(related)
     970        return self._can_remove
     971
     972    def _get_m2m_attr(self, related, attr):
     973        "Function that can be curried to provide a source field attribute"
     974        cache_attr = '_m2m_%s_cache' % attr
     975        if not hasattr(self, cache_attr):
     976            field, _ = self._get_intermediary_fields(related)
     977            setattr(self, cache_attr, getattr(field, attr))
     978        return getattr(self, cache_attr)
     979
    915980    def _get_m2m_reverse_attr(self, related, attr):
    916         "Function that can be curried to provide the related column name for the m2m table"
     981        "Function that can be curried to provide a related field attribute"
    917982        cache_attr = '_m2m_reverse_%s_cache' % attr
    918         if hasattr(self, cache_attr):
    919             return getattr(self, cache_attr)
    920         found = False
    921         for f in self.rel.through._meta.fields:
    922             if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model:
    923                 if related.model == related.parent_model:
    924                     # If this is an m2m-intermediate to self,
    925                     # the first foreign key you find will be
    926                     # the source column. Keep searching for
    927                     # the second foreign key.
    928                     if found:
    929                         setattr(self, cache_attr, getattr(f, attr))
    930                         break
    931                     else:
    932                         found = True
    933                 else:
    934                     setattr(self, cache_attr, getattr(f, attr))
    935                     break
     983        if not hasattr(self, cache_attr):
     984            _, related_field = self._get_intermediary_fields(related)
     985            setattr(self, cache_attr, getattr(related_field, attr))           
    936986        return getattr(self, cache_attr)
    937987
    938988    def isValidIDList(self, field_data, all_data):
     
    10171067
    10181068        self.m2m_field_name = curry(self._get_m2m_attr, related, 'name')
    10191069        self.m2m_reverse_field_name = curry(self._get_m2m_reverse_attr, related, 'name')
     1070       
     1071        self.can_add = curry(self._get_can_add, related)
     1072        self.can_remove = curry(self._get_can_remove, related)
    10201073
    10211074    def set_attributes_from_rel(self):
    10221075        pass
Back to Top