From 4708aee3116695ee385def25e596f2559e43742c Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 24 Feb 2006 10:36:04 +0000 Subject: [PATCH] magic-removal: Fixes #1346 -- Added ability for m2m relations to self to be optionally non-symmetrical. Added unit tests for non-symmetrical behaviour. git-svn-id: http://code.djangoproject.com/svn/django/branches/magic-removal@2383 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/db/models/fields/related.py | 27 +++--- tests/modeltests/m2m_recursive/models.py | 100 +++++++++++++++++++++++ 2 files changed, 117 insertions(+), 10 deletions(-) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index a7fc3539bd..70cb1cb912 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -275,6 +275,7 @@ class ReverseManyRelatedObjectsDescriptor(object): qn = backend.quote_name this_opts = instance.__class__._meta + symmetrical = self.field.rel.symmetrical rel_model = self.field.rel.to rel_opts = rel_model._meta join_table = qn(self.field.m2m_db_table()) @@ -301,8 +302,8 @@ class ReverseManyRelatedObjectsDescriptor(object): _add_m2m_items(self, superclass, rel_model, join_table, source_col_name, target_col_name, instance._get_pk_val(), *objs, **kwargs) - # If this is an m2m relation to self, add the mirror entry in the m2m table - if instance.__class__ == rel_model: + # If this is a symmmetrical m2m relation to self, add the mirror entry in the m2m table + if instance.__class__ == rel_model and symmetrical: _add_m2m_items(self, superclass, rel_model, join_table, target_col_name, source_col_name, instance._get_pk_val(), *objs, **kwargs) @@ -312,8 +313,8 @@ class ReverseManyRelatedObjectsDescriptor(object): _remove_m2m_items(rel_model, join_table, source_col_name, target_col_name, instance._get_pk_val(), *objs) - # If this is an m2m relation to self, remove the mirror entry in the m2m table - if instance.__class__ == rel_model: + # If this is a symmmetrical m2m relation to self, remove the mirror entry in the m2m table + if instance.__class__ == rel_model and symmetrical: _remove_m2m_items(rel_model, join_table, target_col_name, source_col_name, instance._get_pk_val(), *objs) @@ -322,8 +323,8 @@ class ReverseManyRelatedObjectsDescriptor(object): def clear(self): _clear_m2m_items(join_table, source_col_name, instance._get_pk_val()) - # If this is an m2m relation to self, clear the mirror entry in the m2m table - if instance.__class__ == rel_model: + # If this is a symmmetrical m2m relation to self, clear the mirror entry in the m2m table + if instance.__class__ == rel_model and symmetrical: _clear_m2m_items(join_table, target_col_name, instance._get_pk_val()) clear.alters_data = True @@ -472,7 +473,8 @@ class ManyToManyField(RelatedField, Field): related_name=kwargs.pop('related_name', None), filter_interface=kwargs.pop('filter_interface', None), limit_choices_to=kwargs.pop('limit_choices_to', None), - raw_id_admin=kwargs.pop('raw_id_admin', False)) + raw_id_admin=kwargs.pop('raw_id_admin', False), + symmetrical=kwargs.pop('symmetrical', True)) if kwargs["rel"].raw_id_admin: kwargs.setdefault("validator_list", []).append(self.isValidIDList) Field.__init__(self, **kwargs) @@ -559,8 +561,12 @@ class ManyToManyField(RelatedField, Field): self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta) def contribute_to_related_class(self, cls, related): - setattr(cls, related.get_accessor_name(), ManyRelatedObjectsDescriptor(related, 'm2m')) - # Add the descriptor for the m2m relation + # m2m relations to self do not have a ManyRelatedObjectsDescriptor, + # as it would be redundant - unless the field is non-symmetrical. + if related.model != related.parent_model or not self.rel.symmetrical: + # Add the descriptor for the m2m relation + setattr(cls, related.get_accessor_name(), ManyRelatedObjectsDescriptor(related, 'm2m')) + self.rel.singular = self.rel.singular or self.rel.to._meta.object_name.lower() # Set up the accessors for the column names on the m2m table @@ -601,7 +607,7 @@ class OneToOne(ManyToOne): class ManyToMany: def __init__(self, to, singular=None, related_name=None, - filter_interface=None, limit_choices_to=None, raw_id_admin=False): + filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True): self.to = to self.singular = singular or None self.related_name = related_name @@ -609,4 +615,5 @@ class ManyToMany: self.limit_choices_to = limit_choices_to or {} self.edit_inline = False self.raw_id_admin = raw_id_admin + self.symmetrical = symmetrical assert not (self.raw_id_admin and self.filter_interface), "ManyToMany relationships may not use both raw_id_admin and filter_interface" diff --git a/tests/modeltests/m2m_recursive/models.py b/tests/modeltests/m2m_recursive/models.py index d6fc2d05c1..ff8a5a8f47 100644 --- a/tests/modeltests/m2m_recursive/models.py +++ b/tests/modeltests/m2m_recursive/models.py @@ -4,6 +4,10 @@ In this example, A Person can have many friends, who are also people. Friendship is a symmetrical relationshiup - if I am your friend, you are my friend. +A person can also have many idols - but while I may idolize you, you may not think +the same of me. 'Idols' is an example of a non-symmetrical m2m field. Only recursive +m2m fields may be non-symmetrical, and they are symmetrical by default. + This test validates that the m2m table will create a mangled name for the m2m table if there will be a clash, and tests that symmetry is preserved where appropriate. """ @@ -13,6 +17,7 @@ from django.db import models class Person(models.Model): name = models.CharField(maxlength=20) friends = models.ManyToManyField('self') + idols = models.ManyToManyField('self', symmetrical=False, related_name='stalkers') def __repr__(self): return self.name @@ -89,4 +94,99 @@ API_TESTS = """ [Chuck] +# Add some idols in the direction of field definition +# Anne idolizes Bill and Chuck +>>> a.idols.add(b,c) + +# Bill idolizes Anne right back +>>> b.idols.add(a) + +# David is idolized by Anne and Chuck - add in reverse direction +>>> d.stalkers.add(a,c) + +# Who are Anne's idols? +>>> a.idols.all() +[Bill, Chuck, David] + +# Who is stalking Anne? +>>> a.stalkers.all() +[Bill] + +# Who are Bill's idols? +>>> b.idols.all() +[Anne] + +# Who is stalking Bill? +>>> b.stalkers.all() +[Anne] + +# Who are Chuck's idols? +>>> c.idols.all() +[David] + +# Who is stalking Chuck? +>>> c.stalkers.all() +[Anne] + +# Who are David's idols? +>>> d.idols.all() +[] + +# Who is stalking David +>>> d.stalkers.all() +[Anne, Chuck] + +# Bill is already being stalked by Anne - add Anne again, but in the reverse direction +>>> b.stalkers.add(a) + +# Who are Anne's idols? +>>> a.idols.all() +[Bill, Chuck, David] + +# Who is stalking Anne? +[Bill] + +# Who are Bill's idols +>>> b.idols.all() +[Anne] + +# Who is stalking Bill? +>>> b.stalkers.all() +[Anne] + +# Remove Anne from Bill's list of stalkers +>>> b.stalkers.remove(a) + +# Who are Anne's idols? +>>> a.idols.all() +[Chuck, David] + +# Who is stalking Anne? +>>> a.stalkers.all() +[Bill] + +# Who are Bill's idols? +>>> b.idols.all() +[Anne] + +# Who is stalking Bill? +>>> b.stalkers.all() +[] + +# Clear Anne's group of idols +>>> a.idols.clear() + +# Who are Anne's idols +>>> a.idols.all() +[] + +# Reverse relationships should also be gone +# Who is stalking Chuck? +>>> c.stalkers.all() +[] + +# Who is friends with David? +>>> d.stalkers.all() +[Chuck] + """