Ticket #9475: 9475.m2m_add_remove.r11739.diff

File 9475.m2m_add_remove.r11739.diff, 22.6 KB (added by Johannes Dollinger, 14 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

     
    405405
    406406        return manager
    407407
    408 def create_many_related_manager(superclass, rel=False):
     408def create_many_related_manager(superclass, field):
    409409    """Creates a manager that subclasses 'superclass' (which is a Manager)
    410410    and adds behavior for many-to-many related objects."""
    411     through = rel.through
     411    through = field.rel.through
     412    can_add = field.can_add()
     413    can_remove = field.can_remove()
     414   
    412415    class ManyRelatedManager(superclass):
    413416        def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
    414417                join_table=None, source_field_name=None, target_field_name=None):
     
    427430        def get_query_set(self):
    428431            return superclass.get_query_set(self)._next_is_sticky().filter(**(self.core_filters))
    429432
    430         # If the ManyToMany relation has an intermediary model,
    431         # the add and remove methods do not exist.
    432         if rel.through._meta.auto_created:
     433        if can_add:
    433434            def add(self, *objs):
    434435                self._add_items(self.source_field_name, self.target_field_name, *objs)
    435436
     
    438439                    self._add_items(self.target_field_name, self.source_field_name, *objs)
    439440            add.alters_data = True
    440441
     442        if can_remove:
    441443            def remove(self, *objs):
    442444                self._remove_items(self.source_field_name, self.target_field_name, *objs)
    443445
     
    457459        def create(self, **kwargs):
    458460            # This check needs to be done here, since we can't later remove this
    459461            # from the method lookup table, as we do with add and remove.
    460             if not rel.through._meta.auto_created:
     462            if not can_add:
    461463                opts = through._meta
    462                 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)
     464                raise AttributeError, "Cannot use create() on this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)
    463465            new_obj = super(ManyRelatedManager, self).create(**kwargs)
    464466            self.add(new_obj)
    465467            return new_obj
    466468        create.alters_data = True
    467469
    468470        def get_or_create(self, **kwargs):
     471            # This check needs to be done here, since we can't later remove this
     472            # from the method lookup table, as we do with add and remove.
     473            if not can_add:
     474                opts = through._meta
     475                raise AttributeError, "Cannot use get_or_create() on this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)           
    469476            obj, created = \
    470477                    super(ManyRelatedManager, self).get_or_create(**kwargs)
    471478            # We only need to add() if created because if we got an object back
     
    552559        # model's default manager.
    553560        rel_model = self.related.model
    554561        superclass = rel_model._default_manager.__class__
    555         RelatedManager = create_many_related_manager(superclass, self.related.field.rel)
     562        RelatedManager = create_many_related_manager(superclass, self.related.field)
    556563
    557564        manager = RelatedManager(
    558565            model=rel_model,
     
    569576        if instance is None:
    570577            raise AttributeError, "Manager must be accessed via instance"
    571578
    572         if not self.related.field.rel.through._meta.auto_created:
     579        if not self.related.field.can_add():
    573580            opts = self.related.field.rel.through._meta
    574             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)
     581            raise AttributeError, "Cannot set values on this ManyToManyField. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)
    575582
    576583        manager = self.__get__(instance)
    577584        manager.clear()
     
    602609        # model's default manager.
    603610        rel_model=self.field.rel.to
    604611        superclass = rel_model._default_manager.__class__
    605         RelatedManager = create_many_related_manager(superclass, self.field.rel)
     612        RelatedManager = create_many_related_manager(superclass, self.field)
    606613
    607614        manager = RelatedManager(
    608615            model=rel_model,
     
    619626        if instance is None:
    620627            raise AttributeError, "Manager must be accessed via instance"
    621628
    622         if not self.field.rel.through._meta.auto_created:
     629        if not self.field.can_add():
    623630            opts = self.field.rel.through._meta
    624             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)
     631            raise AttributeError, "Cannot set values a this ManyToManyField.  Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)
    625632
    626633        manager = self.__get__(instance)
    627634        manager.clear()
     
    870877        self.db_table = kwargs.pop('db_table', None)
    871878        if kwargs['rel'].through is not None:
    872879            assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used."
     880           
     881        self._can_add = kwargs.pop('can_add', None)
     882        self._can_remove = kwargs.pop('can_remove', None)
    873883
    874884        Field.__init__(self, **kwargs)
    875885
     
    888898        else:
    889899            return util.truncate_name('%s_%s' % (opts.db_table, self.name),
    890900                                      connection.ops.max_name_length())
    891 
    892     def _get_m2m_attr(self, related, attr):
    893         "Function that can be curried to provide the source column name for the m2m table"
    894         cache_attr = '_m2m_%s_cache' % attr
     901                                     
     902    def _get_intermediary_fields(self, related):
     903        cache_attr = '_m2m_intermediary_fields_cache'
    895904        if hasattr(self, cache_attr):
    896905            return getattr(self, cache_attr)
     906           
     907        candidates = []
     908        related_candidates = []
     909        auto_add = True
    897910        for f in self.rel.through._meta.fields:
    898             if hasattr(f,'rel') and f.rel and f.rel.to == related.model:
    899                 setattr(self, cache_attr, getattr(f, attr))
    900                 return getattr(self, cache_attr)
     911            if hasattr(f,'rel') and f.rel:
     912                if f.rel.to == related.model:
     913                    candidates.append(f)
     914                    continue
     915                elif f.rel.to == related.parent_model:
     916                    related_candidates.append(f)
     917                    continue
     918            if isinstance(f, AutoField) or f.null or f.has_default():
     919                continue
     920            elif getattr(f, 'auto_now_add', False) or getattr(f, 'auto_now', False):
     921                continue
     922            else:
     923                auto_add = False
     924        if related.model == related.parent_model:
     925            # m2m to self
     926            assert len(candidates) == 2, "There are too many ForeignKeys to %s" % related.model
     927            field, related_field = candidates
     928        else:
     929            assert len(candidates) == 1, "There are no ForeignKeys to %s" % related.model
     930            assert len(related_candidates) == 1, "There are no ForeignKeys to %s" % related.parent_model
     931            # TODO: intelligently pick a candidate if there is more than one. For now, just use the first.
     932            field, related_field = candidates[0], related_candidates[0]
    901933
     934        if self._can_add is None:
     935            self._can_add = auto_add
     936
     937        if self._can_remove is None:
     938            self._can_remove = False
     939            unique_together = [frozenset(ut) for ut in self.rel.through._meta.unique_together]               
     940            names = frozenset([field.name, related_field.name])
     941            for ut in unique_together:
     942                if names <= ut:
     943                    self._can_remove = True
     944                    break
     945       
     946        setattr(self, cache_attr, (field, related_field))
     947        return (field, related_field)
     948       
     949    def _get_can_add(self, related):
     950        if self._can_add is None:
     951            self._get_intermediary_fields(related)
     952        return self._can_add
     953       
     954    def _get_can_remove(self, related):
     955        if self._can_remove is None:
     956            self._get_intermediary_fields(related)
     957        return self._can_remove
     958
     959    def _get_m2m_attr(self, related, attr):
     960        "Function that can be curried to provide a source field attribute"
     961        cache_attr = '_m2m_%s_cache' % attr
     962        if not hasattr(self, cache_attr):
     963            field, _ = self._get_intermediary_fields(related)
     964            setattr(self, cache_attr, getattr(field, attr))
     965        return getattr(self, cache_attr)
     966
    902967    def _get_m2m_reverse_attr(self, related, attr):
    903         "Function that can be curried to provide the related column name for the m2m table"
     968        "Function that can be curried to provide a related field attribute"
    904969        cache_attr = '_m2m_reverse_%s_cache' % attr
    905         if hasattr(self, cache_attr):
    906             return getattr(self, cache_attr)
    907         found = False
    908         for f in self.rel.through._meta.fields:
    909             if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model:
    910                 if related.model == related.parent_model:
    911                     # If this is an m2m-intermediate to self,
    912                     # the first foreign key you find will be
    913                     # the source column. Keep searching for
    914                     # the second foreign key.
    915                     if found:
    916                         setattr(self, cache_attr, getattr(f, attr))
    917                         break
    918                     else:
    919                         found = True
    920                 else:
    921                     setattr(self, cache_attr, getattr(f, attr))
    922                     break
     970        if not hasattr(self, cache_attr):
     971            _, related_field = self._get_intermediary_fields(related)
     972            setattr(self, cache_attr, getattr(related_field, attr))           
    923973        return getattr(self, cache_attr)
    924974
    925975    def isValidIDList(self, field_data, all_data):
     
    10041054
    10051055        self.m2m_field_name = curry(self._get_m2m_attr, related, 'name')
    10061056        self.m2m_reverse_field_name = curry(self._get_m2m_reverse_attr, related, 'name')
     1057       
     1058        self.can_add = curry(self._get_can_add, related)
     1059        self.can_remove = curry(self._get_can_remove, related)
    10071060
    10081061    def set_attributes_from_rel(self):
    10091062        pass
Back to Top