Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #6095 -- Added the ability to specify the model to use to manag…

…e a ManyToManyField. Thanks to Eric Florenzano for his excellent work on this patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@8136 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 174641b9b35022b80ea1d1a917a9d95b37a551d7 1 parent f752f69
Russell Keith-Magee authored July 29, 2008
1  AUTHORS
@@ -154,6 +154,7 @@ answer newbie questions, and generally made Django that much better:
154 154
     Maciej Fijalkowski
155 155
     Matthew Flanagan <http://wadofstuff.blogspot.com>
156 156
     Eric Floehr <eric@intellovations.com>
  157
+    Eric Florenzano <floguy@gmail.com>
157 158
     Vincent Foley <vfoleybourgon@yahoo.ca>
158 159
     Rudolph Froger <rfroger@estrate.nl>
159 160
     Jorge Gajon <gajon@gajon.org>
5  django/contrib/admin/options.py
@@ -161,7 +161,10 @@ def formfield_for_dbfield(self, db_field, **kwargs):
161 161
                 kwargs['empty_label'] = db_field.blank and _('None') or None
162 162
             else:
163 163
                 if isinstance(db_field, models.ManyToManyField):
164  
-                    if db_field.name in self.raw_id_fields:
  164
+                    # If it uses an intermediary model, don't show field in admin. 
  165
+                    if db_field.rel.through is not None:
  166
+                        return None
  167
+                    elif db_field.name in self.raw_id_fields:
165 168
                         kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel)
166 169
                         kwargs['help_text'] = ''
167 170
                     elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
3  django/contrib/contenttypes/generic.py
@@ -104,6 +104,9 @@ def __init__(self, to, **kwargs):
104 104
                             limit_choices_to=kwargs.pop('limit_choices_to', None),
105 105
                             symmetrical=kwargs.pop('symmetrical', True))
106 106
 
  107
+        # By its very nature, a GenericRelation doesn't create a table.
  108
+        self.creates_table = False
  109
+
107 110
         # Override content-type/object-id field names on the related class
108 111
         self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
109 112
         self.content_type_field_name = kwargs.pop("content_type_field", "content_type")
2  django/core/management/sql.py
@@ -353,7 +353,7 @@ def many_to_many_sql_for_model(model, style):
353 353
     qn = connection.ops.quote_name
354 354
     inline_references = connection.features.inline_fk_references
355 355
     for f in opts.local_many_to_many:
356  
-        if not isinstance(f.rel, generic.GenericRel):
  356
+        if f.creates_table:
357 357
             tablespace = f.db_tablespace or opts.db_tablespace
358 358
             if tablespace and connection.features.supports_tablespaces: 
359 359
                 tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True)
45  django/core/management/validation.py
@@ -102,6 +102,7 @@ def get_validation_errors(outfile, app=None):
102 102
                         if r.get_accessor_name() == rel_query_name:
103 103
                             e.add(opts, "Reverse query name for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
104 104
 
  105
+        seen_intermediary_signatures = [] 
105 106
         for i, f in enumerate(opts.local_many_to_many):
106 107
             # Check to see if the related m2m field will clash with any
107 108
             # existing fields, m2m fields, m2m related objects or related
@@ -112,7 +113,49 @@ def get_validation_errors(outfile, app=None):
112 113
                 # so skip the next section
113 114
                 if isinstance(f.rel.to, (str, unicode)):
114 115
                     continue
115  
-
  116
+            if getattr(f.rel, 'through', None) is not None:
  117
+                if hasattr(f.rel, 'through_model'):
  118
+                    from_model, to_model = cls, f.rel.to
  119
+                    if from_model == to_model and f.rel.symmetrical:
  120
+                        e.add(opts, "Many-to-many fields with intermediate tables cannot be symmetrical.")
  121
+                    seen_from, seen_to, seen_self = False, False, 0
  122
+                    for inter_field in f.rel.through_model._meta.fields:
  123
+                        rel_to = getattr(inter_field.rel, 'to', None)
  124
+                        if from_model == to_model: # relation to self
  125
+                            if rel_to == from_model:
  126
+                                seen_self += 1
  127
+                            if seen_self > 2:
  128
+                                e.add(opts, "Intermediary model %s has more than two foreign keys to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, from_model._meta.object_name))
  129
+                        else:
  130
+                            if rel_to == from_model:
  131
+                                if seen_from:
  132
+                                    e.add(opts, "Intermediary model %s has more than one foreign key to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, rel_from._meta.object_name))
  133
+                                else:
  134
+                                    seen_from = True
  135
+                            elif rel_to == to_model:
  136
+                                if seen_to:
  137
+                                    e.add(opts, "Intermediary model %s has more than one foreign key to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, rel_to._meta.object_name))
  138
+                                else:
  139
+                                    seen_to = True
  140
+                    if f.rel.through_model not in models.get_models():
  141
+                        e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed." % (f.name, f.rel.through))
  142
+                    signature = (f.rel.to, cls, f.rel.through_model)
  143
+                    if signature in seen_intermediary_signatures:
  144
+                        e.add(opts, "The model %s has two manually-defined m2m relations through the model %s, which is not permitted. Please consider using an extra field on your intermediary model instead." % (cls._meta.object_name, f.rel.through_model._meta.object_name))
  145
+                    else:
  146
+                        seen_intermediary_signatures.append(signature)
  147
+                    seen_related_fk, seen_this_fk = False, False
  148
+                    for field in f.rel.through_model._meta.fields:
  149
+                        if field.rel:
  150
+                            if not seen_related_fk and field.rel.to == f.rel.to:
  151
+                                seen_related_fk = True
  152
+                            elif field.rel.to == cls:
  153
+                                seen_this_fk = True
  154
+                    if not seen_related_fk or not seen_this_fk:
  155
+                        e.add(opts, "'%s' has a manually-defined m2m relation through model %s, which does not have foreign keys to %s and %s" % (f.name, f.rel.through, f.rel.to._meta.object_name, cls._meta.object_name))
  156
+                else:
  157
+                    e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed" % (f.name, f.rel.through))
  158
+            
116 159
             rel_opts = f.rel.to._meta
117 160
             rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
118 161
             rel_query_name = f.related_query_name()
150  django/db/models/fields/related.py
@@ -23,7 +23,7 @@
@@ -45,6 +45,8 @@ class MyModel(Model):
@@ -66,11 +68,10 @@ class MyModel(Model):
@@ -78,9 +79,8 @@ def do_pending_lookups(sender):
@@ -108,7 +108,10 @@ def contribute_to_class(self, cls, name):
@@ -340,7 +343,7 @@ def __set__(self, instance, value):
@@ -354,6 +357,7 @@ def __init__(self, model=None, core_filters=None, instance=None, symmetrical=Non
@@ -361,21 +365,24 @@ def __init__(self, model=None, core_filters=None, instance=None, symmetrical=Non
@@ -386,6 +393,10 @@ def clear(self):
@@ -473,7 +484,7 @@ def __get__(self, instance, instance_type=None):
@@ -492,6 +503,10 @@ def __set__(self, instance, value):
@@ -514,7 +529,7 @@ def __get__(self, instance, instance_type=None):
@@ -533,6 +548,10 @@ def __set__(self, instance, value):
@@ -584,7 +603,7 @@ def __init__(self, to, field_name, num_in_admin=0, min_num_in_admin=None,
@@ -594,6 +613,7 @@ def __init__(self, to, num_in_admin=0, related_name=None,
@@ -723,8 +743,16 @@ def __init__(self, to, **kwargs):
@@ -739,26 +767,62 @@ def get_choices_default(self):
@@ -792,13 +856,23 @@ def flatten_data(self, follow, obj = None):
55  docs/admin.txt
@@ -617,6 +617,61 @@ automatically::
617 617
             FriendshipInline,
618 618
         ]
619 619
 
  620
+Working with Many-to-Many Intermediary Models
  621
+----------------------------------------------
  622
+
  623
+By default, admin widgets for many-to-many relations will be displayed inline
  624
+on whichever model contains the actual reference to the `ManyToManyField`.  
  625
+However, when you specify an intermediary model using the ``through``
  626
+argument to a ``ManyToManyField``, the admin will not display a widget by 
  627
+default. This is because each instance of that intermediary model requires
  628
+more information than could be displayed in a single widget, and the layout
  629
+required for multiple widgets will vary depending on the intermediate model.
  630
+
  631
+However, we still want to be able to edit that information inline. Fortunately,
  632
+this is easy to do with inline admin models. Suppose we have the following
  633
+models::
  634
+
  635
+    class Person(models.Model):
  636
+        name = models.CharField(max_length=128)
  637
+    
  638
+    class Group(models.Model):
  639
+        name = models.CharField(max_length=128)
  640
+        members = models.ManyToManyField(Person, through='Membership')
  641
+
  642
+    class Membership(models.Model):
  643
+        person = models.ForeignKey(Person)
  644
+        group = models.ForeignKey(Group)
  645
+        date_joined = models.DateField()
  646
+        invite_reason = models.CharField(max_length=64)
  647
+
  648
+The first step in displaying this intermediate model in the admin is to
  649
+define an inline model for the Membership table::
  650
+
  651
+    class MembershipInline(admin.TabularInline):
  652
+        model = Membership
  653
+        extra = 1
  654
+
  655
+This simple example uses the defaults inline form for the Membership model,
  656
+and shows 1 extra line. This could be customized using any of the options
  657
+available to inline models.
  658
+
  659
+Now create admin views for the ``Person`` and ``Group`` models::
  660
+
  661
+    class PersonAdmin(admin.ModelAdmin):
  662
+        inlines = (MembershipInline,)
  663
+
  664
+    class GroupAdmin(admin.ModelAdmin):
  665
+        inlines = (MembershipInline,)
  666
+
  667
+Finally, register your ``Person`` and ``Group`` models with the admin site::
  668
+    
  669
+    admin.site.register(Person, PersonAdmin)
  670
+    admin.site.register(Group, GroupAdmin)
  671
+
  672
+Now your admin site is set up to edit ``Membership`` objects inline from either
  673
+the ``Person`` or the ``Group`` detail pages.
  674
+
620 675
 ``AdminSite`` objects
621 676
 =====================
622 677
 
129  docs/model-api.txt
@@ -655,7 +655,7 @@ Note that this value is *not* HTML-escaped when it's displayed in the admin
655 655
 interface. This lets you include HTML in ``help_text`` if you so desire. For
656 656
 example::
657 657
 
658  
-	help_text="Please use the following format: <em>YYYY-MM-DD</em>."
  658
+    help_text="Please use the following format: <em>YYYY-MM-DD</em>."
659 659
 
660 660
 Alternatively you can use plain text and
661 661
 ``django.utils.html.escape()`` to escape any HTML special characters.
@@ -944,6 +944,131 @@ the relationship should work. All are optional:
944 944
 
945 945
     =======================  ============================================================
946 946
 
  947
+Extra fields on many-to-many relationships
  948
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  949
+
  950
+**New in Django development version** 
  951
+
  952
+When you're only dealing with simple many-to-many relationships such as
  953
+mixing and matching pizzas and toppings, a standard ``ManyToManyField``
  954
+is all you need. However, sometimes you may need to associate data with the
  955
+relationship between two models. 
  956
+
  957
+For example, consider the case of an application tracking the musical groups
  958
+which musicians belong to. There is a many-to-many relationship between a person
  959
+and the groups of which they are a member, so you could use a ManyToManyField
  960
+to represent this relationship. However, there is a lot of detail about the
  961
+membership that you might want to collect, such as the date at which the person
  962
+joined the group.
  963
+
  964
+For these situations, Django allows you to specify the model that will be used
  965
+to govern the many-to-many relationship. You can then put extra fields on the
  966
+intermediate model. The intermediate model is associated with the
  967
+``ManyToManyField`` using the ``through`` argument to point to the model
  968
+that will act as an intermediary. For our musician example, the code would look
  969
+something like this::
  970
+
  971
+    class Person(models.Model):
  972
+        name = models.CharField(max_length=128)
  973
+
  974
+        def __unicode__(self):
  975
+            return self.name
  976
+
  977
+    class Group(models.Model):
  978
+        name = models.CharField(max_length=128)
  979
+        members = models.ManyToManyField(Person, through='Membership')
  980
+
  981
+        def __unicode__(self):
  982
+            return self.name
  983
+
  984
+    class Membership(models.Model):
  985
+        person = models.ForeignKey(Person)
  986
+        group = models.ForeignKey(Group)
  987
+        date_joined = models.DateField()
  988
+        invite_reason = models.CharField(max_length=64)
  989
+
  990
+When you set up the intermediary model, you explicitly specify foreign 
  991
+keys to the models that are involved in the ManyToMany relation. This
  992
+explicit declaration defines how the two models are related.
  993
+
  994
+There are a few restrictions on the intermediate model:
  995
+
  996
+    * Your intermediate model must contain one - and *only* one - foreign key
  997
+      on the target model (this would be ``Person`` in our example). If you
  998
+      have more than one foreign key, a validation error will be raised.
  999
+  
  1000
+    * Your intermediate model must contain one - and *only* one - foreign key 
  1001
+      on the source model (this would be ``Group`` in our example). If you
  1002
+      have more than one foreign key, a validation error will be raised.
  1003
+      
  1004
+    * If the many-to-many relation is a relation on itself, the relationship
  1005
+      must be non-symmetric.
  1006
+
  1007
+Now that you have set up your ``ManyToManyField`` to use your intermediary 
  1008
+model (Membership, in this case), you're ready to start creating some
  1009
+many-to-many relationships. You do this by creating instances of the
  1010
+intermediate model::
  1011
+    
  1012
+    >>> ringo = Person.objects.create(name="Ringo Starr")
  1013
+    >>> paul = Person.objects.create(name="Paul McCartney")
  1014
+    >>> beatles = Group.objects.create(name="The Beatles")
  1015
+    >>> m1 = Membership(person=ringo, group=beatles,
  1016
+    ...     date_joined=date(1962, 8, 16), 
  1017
+    ...     invite_reason= "Needed a new drummer.")
  1018
+    >>> m1.save()
  1019
+    >>> beatles.members.all()
  1020
+    [<Person: Ringo Starr>]
  1021
+    >>> ringo.group_set.all()
  1022
+    [<Group: The Beatles>]
  1023
+    >>> m2 = Membership.objects.create(person=paul, group=beatles,
  1024
+    ...     date_joined=date(1960, 8, 1), 
  1025
+    ...     invite_reason= "Wanted to form a band.")
  1026
+    >>> beatles.members.all()
  1027
+    [<Person: Ringo Starr>, <Person: Paul McCartney>]
  1028
+
  1029
+Unlike normal many-to-many fields, you *can't* use ``add``, ``create``,
  1030
+or assignment (i.e., ``beatles.members = [...]``) to create relationships::
  1031
+
  1032
+    # THIS WILL NOT WORK
  1033
+    >>> beatles.members.add(john)
  1034
+    # NEITHER WILL THIS
  1035
+    >>> beatles.members.create(name="George Harrison")
  1036
+    # AND NEITHER WILL THIS
  1037
+    >>> beatles.members = [john, paul, ringo, george]
  1038
+    
  1039
+Why? You can't just create a relationship between a Person and a Group - you
  1040
+need to specify all the detail for the relationship required by the
  1041
+Membership table. The simple ``add``, ``create`` and assignment calls
  1042
+don't provide a way to specify this extra detail. As a result, they are
  1043
+disabled for many-to-many relationships that use an intermediate model.
  1044
+The only way to create a many-to-many relationship with an intermediate table
  1045
+is to create instances of the intermediate model.
  1046
+
  1047
+The ``remove`` method is disabled for similar reasons. However, the
  1048
+``clear()`` method can be used to remove all many-to-many relationships
  1049
+for an instance::
  1050
+
  1051
+    # Beatles have broken up
  1052
+    >>> beatles.members.clear()
  1053
+
  1054
+Once you have established the many-to-many relationships by creating instances
  1055
+of your intermediate model, you can issue queries. Just as with normal 
  1056
+many-to-many relationships, you can query using the attributes of the 
  1057
+many-to-many-related model::
  1058
+
  1059
+    # Find all the groups with a member whose name starts with 'Paul'
  1060
+    >>> Groups.objects.filter(person__name__startswith='Paul')
  1061
+    [<Group: The Beatles>]
  1062
+
  1063
+As you are using an intermediate table, you can also query on the attributes 
  1064
+of the intermediate model::
  1065
+
  1066
+    # Find all the members of the Beatles that joined after 1 Jan 1961
  1067
+    >>> Person.objects.filter(
  1068
+    ...     group__name='The Beatles',
  1069
+    ...     membership__date_joined__gt=date(1961,1,1))
  1070
+    [<Person: Ringo Starr]
  1071
+    
947 1072
 One-to-one relationships
948 1073
 ~~~~~~~~~~~~~~~~~~~~~~~~
949 1074
 
@@ -1145,7 +1270,7 @@ any parent classes in ``unique_together``.
1145 1270
 For convenience, unique_together can be a single list when dealing
1146 1271
 with a single set of fields::
1147 1272
 
1148  
-	unique_together = ("driver", "restaurant")
  1273
+    unique_together = ("driver", "restaurant")
1149 1274
 
1150 1275
 ``verbose_name``
1151 1276
 ----------------
65  tests/modeltests/invalid_models/models.py
@@ -110,6 +110,63 @@ class Car(models.Model):
110 110
 class MissingRelations(models.Model):
111 111
     rel1 = models.ForeignKey("Rel1")
112 112
     rel2 = models.ManyToManyField("Rel2")
  113
+    
  114
+class MissingManualM2MModel(models.Model):
  115
+    name = models.CharField(max_length=5)
  116
+    missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel")
  117
+    
  118
+class Person(models.Model):
  119
+    name = models.CharField(max_length=5)
  120
+
  121
+class Group(models.Model):
  122
+    name = models.CharField(max_length=5)
  123
+    primary = models.ManyToManyField(Person, through="Membership", related_name="primary")
  124
+    secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary")
  125
+    tertiary = models.ManyToManyField(Person, through="RelationshipDoubleFK", related_name="tertiary")
  126
+
  127
+class GroupTwo(models.Model):
  128
+    name = models.CharField(max_length=5)
  129
+    primary = models.ManyToManyField(Person, through="Membership")
  130
+    secondary = models.ManyToManyField(Group, through="MembershipMissingFK")
  131
+
  132
+class Membership(models.Model):
  133
+    person = models.ForeignKey(Person)
  134
+    group = models.ForeignKey(Group)
  135
+    not_default_or_null = models.CharField(max_length=5)
  136
+
  137
+class MembershipMissingFK(models.Model):
  138
+    person = models.ForeignKey(Person)
  139
+
  140
+class PersonSelfRefM2M(models.Model):
  141
+    name = models.CharField(max_length=5)
  142
+    friends = models.ManyToManyField('self', through="Relationship")
  143
+    too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK")
  144
+
  145
+class PersonSelfRefM2MExplicit(models.Model):
  146
+    name = models.CharField(max_length=5)
  147
+    friends = models.ManyToManyField('self', through="ExplicitRelationship", symmetrical=True)
  148
+
  149
+class Relationship(models.Model):
  150
+    first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set")
  151
+    second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set")
  152
+    date_added = models.DateTimeField()
  153
+
  154
+class ExplicitRelationship(models.Model):
  155
+    first = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_from_set")
  156
+    second = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_to_set")
  157
+    date_added = models.DateTimeField()
  158
+
  159
+class RelationshipTripleFK(models.Model):
  160
+    first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2")
  161
+    second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2")
  162
+    third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far")
  163
+    date_added = models.DateTimeField()
  164
+
  165
+class RelationshipDoubleFK(models.Model):
  166
+    first = models.ForeignKey(Person, related_name="first_related_name")
  167
+    second = models.ForeignKey(Person, related_name="second_related_name")
  168
+    third = models.ForeignKey(Group, related_name="rel_to_set")
  169
+    date_added = models.DateTimeField()
113 170
 
114 171
 model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute.
115 172
 invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
@@ -195,4 +252,12 @@ class MissingRelations(models.Model):
195 252
 invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_4' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'm2m_4'.
196 253
 invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed
197 254
 invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed
  255
+invalid_models.grouptwo: 'primary' has a manually-defined m2m relation through model Membership, which does not have foreign keys to Person and GroupTwo
  256
+invalid_models.grouptwo: 'secondary' has a manually-defined m2m relation through model MembershipMissingFK, which does not have foreign keys to Group and GroupTwo
  257
+invalid_models.missingmanualm2mmodel: 'missing_m2m' specifies an m2m relation through model MissingM2MModel, which has not been installed
  258
+invalid_models.group: The model Group has two manually-defined m2m relations through the model Membership, which is not permitted. Please consider using an extra field on your intermediary model instead.
  259
+invalid_models.group: Intermediary model RelationshipDoubleFK has more than one foreign key to Person, which is ambiguous and is not permitted.
  260
+invalid_models.personselfrefm2m: Many-to-many fields with intermediate tables cannot be symmetrical.
  261
+invalid_models.personselfrefm2m: Intermediary model RelationshipTripleFK has more than two foreign keys to PersonSelfRefM2M, which is ambiguous and is not permitted.
  262
+invalid_models.personselfrefm2mexplicit: Many-to-many fields with intermediate tables cannot be symmetrical.
198 263
 """
2  tests/modeltests/m2m_through/__init__.py
... ...
@@ -0,0 +1,2 @@
  1
+
  2
+
337  tests/modeltests/m2m_through/models.py
... ...
@@ -0,0 +1,337 @@
  1
+from django.db import models
  2
+from datetime import datetime
  3
+
  4
+# M2M described on one of the models
  5
+class Person(models.Model):
  6
+    name = models.CharField(max_length=128)
  7
+
  8
+    class Meta:
  9
+        ordering = ('name',)
  10
+        
  11
+    def __unicode__(self):
  12
+        return self.name
  13
+
  14
+class Group(models.Model):
  15
+    name = models.CharField(max_length=128)
  16
+    members = models.ManyToManyField(Person, through='Membership')
  17
+    custom_members = models.ManyToManyField(Person, through='CustomMembership', related_name="custom")
  18
+    nodefaultsnonulls = models.ManyToManyField(Person, through='TestNoDefaultsOrNulls', related_name="testnodefaultsnonulls")
  19
+
  20
+    class Meta:
  21
+        ordering = ('name',)
  22
+            
  23
+    def __unicode__(self):
  24
+        return self.name
  25
+
  26
+class Membership(models.Model):
  27
+    person = models.ForeignKey(Person)
  28
+    group = models.ForeignKey(Group)
  29
+    date_joined = models.DateTimeField(default=datetime.now)
  30
+    invite_reason = models.CharField(max_length=64, null=True)
  31
+
  32
+    class Meta:
  33
+        ordering = ('date_joined','invite_reason')
  34
+    
  35
+    def __unicode__(self):
  36
+        return "%s is a member of %s" % (self.person.name, self.group.name)
  37
+
  38
+class CustomMembership(models.Model):
  39
+    person = models.ForeignKey(Person, db_column="custom_person_column", related_name="custom_person_related_name")
  40
+    group = models.ForeignKey(Group)
  41
+    weird_fk = models.ForeignKey(Membership, null=True)
  42
+    date_joined = models.DateTimeField(default=datetime.now)
  43
+    
  44
+    def __unicode__(self):
  45
+        return "%s is a member of %s" % (self.person.name, self.group.name)
  46
+    
  47
+    class Meta:
  48
+        db_table = "test_table"
  49
+
  50
+class TestNoDefaultsOrNulls(models.Model):
  51
+    person = models.ForeignKey(Person)
  52
+    group = models.ForeignKey(Group)
  53
+    nodefaultnonull = models.CharField(max_length=5)
  54
+
  55
+class PersonSelfRefM2M(models.Model):
  56
+    name = models.CharField(max_length=5)
  57
+    friends = models.ManyToManyField('self', through="Friendship", symmetrical=False)
  58
+    
  59
+    def __unicode__(self):
  60
+        return self.name
  61
+
  62
+class Friendship(models.Model):
  63
+    first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set")
  64
+    second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set")
  65
+    date_friended = models.DateTimeField()
  66
+
  67
+__test__ = {'API_TESTS':"""
  68
+>>> from datetime import datetime
  69
+
  70
+### Creation and Saving Tests ###
  71
+
  72
+>>> bob = Person.objects.create(name='Bob')
  73
+>>> jim = Person.objects.create(name='Jim')
  74
+>>> jane = Person.objects.create(name='Jane')
  75
+>>> rock = Group.objects.create(name='Rock')
  76
+>>> roll = Group.objects.create(name='Roll')
  77
+
  78
+# We start out by making sure that the Group 'rock' has no members.
  79
+>>> rock.members.all()
  80
+[]
  81
+
  82
+# To make Jim a member of Group Rock, simply create a Membership object.
  83
+>>> m1 = Membership.objects.create(person=jim, group=rock)
  84
+
  85
+# We can do the same for Jane and Rock.
  86
+>>> m2 = Membership.objects.create(person=jane, group=rock)
  87
+
  88
+# Let's check to make sure that it worked.  Jane and Jim should be members of Rock.
  89
+>>> rock.members.all()
  90
+[<Person: Jane>, <Person: Jim>]
  91
+
  92
+# Now we can add a bunch more Membership objects to test with.
  93
+>>> m3 = Membership.objects.create(person=bob, group=roll)
  94
+>>> m4 = Membership.objects.create(person=jim, group=roll)
  95
+>>> m5 = Membership.objects.create(person=jane, group=roll)
  96
+
  97
+# We can get Jim's Group membership as with any ForeignKey.
  98
+>>> jim.group_set.all()
  99
+[<Group: Rock>, <Group: Roll>]
  100
+
  101
+# Querying the intermediary model works like normal.  
  102
+# In this case we get Jane's membership to Rock.
  103
+>>> m = Membership.objects.get(person=jane, group=rock)
  104
+>>> m
  105
+<Membership: Jane is a member of Rock>
  106
+
  107
+# Now we set some date_joined dates for further testing.
  108
+>>> m2.invite_reason = "She was just awesome."
  109
+>>> m2.date_joined = datetime(2006, 1, 1)
  110
+>>> m2.save()
  111
+
  112
+>>> m5.date_joined = datetime(2004, 1, 1)
  113
+>>> m5.save()
  114
+
  115
+>>> m3.date_joined = datetime(2004, 1, 1)
  116
+>>> m3.save()
  117
+
  118
+# It's not only get that works. Filter works like normal as well.
  119
+>>> Membership.objects.filter(person=jim)
  120
+[<Membership: Jim is a member of Rock>, <Membership: Jim is a member of Roll>]
  121
+
  122
+
  123
+### Forward Descriptors Tests ###
  124
+
  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 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.
  152
+>>> rock.members.clear()
  153
+
  154
+# Now there will be no members of Rock.
  155
+>>> rock.members.all()
  156
+[]
  157
+
  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 Membership's Manager instead.
  164
+
  165
+# Let's re-save those instances that we've cleared.
  166
+>>> m1.save()
  167
+>>> m2.save()
  168
+
  169
+# Verifying that those instances were re-saved successfully.
  170
+>>> rock.members.all()
  171
+[<Person: Jane>, <Person: Jim>]
  172
+
  173
+
  174
+### Reverse Descriptors Tests ###
  175
+
  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 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.
  201
+>>> jim.group_set.clear()
  202
+
  203
+# Now Jim will be in no groups.
  204
+>>> jim.group_set.all()
  205
+[]
  206
+
  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 Membership's Manager instead.
  213
+
  214
+# Let's re-save those instances that we've cleared.
  215
+>>> m1.save()
  216
+>>> m4.save()
  217
+
  218
+# Verifying that those instances were re-saved successfully.
  219
+>>> jim.group_set.all()
  220
+[<Group: Rock>, <Group: Roll>]
  221
+
  222
+### Custom Tests ###
  223
+
  224
+# Let's see if we can query through our second relationship.
  225
+>>> rock.custom_members.all()
  226
+[]
  227
+
  228
+# We can query in the opposite direction as well.
  229
+>>> bob.custom.all()
  230
+[]
  231
+
  232
+# Let's create some membership objects in this custom relationship.
  233
+>>> cm1 = CustomMembership.objects.create(person=bob, group=rock)
  234
+>>> cm2 = CustomMembership.objects.create(person=jim, group=rock)
  235
+
  236
+# If we get the number of people in Rock, it should be both Bob and Jim.
  237
+>>> rock.custom_members.all()
  238
+[<Person: Bob>, <Person: Jim>]
  239
+
  240
+# Bob should only be in one custom group.
  241
+>>> bob.custom.all()
  242
+[<Group: Rock>]
  243
+
  244
+# Let's make sure our new descriptors don't conflict with the FK related_name.
  245
+>>> bob.custom_person_related_name.all()
  246
+[<CustomMembership: Bob is a member of Rock>]
  247
+
  248
+### SELF-REFERENTIAL TESTS ###
  249
+
  250
+# Let's first create a person who has no friends.
  251
+>>> tony = PersonSelfRefM2M.objects.create(name="Tony")
  252
+>>> tony.friends.all()
  253
+[]
  254
+
  255
+# Now let's create another person for Tony to be friends with.
  256
+>>> chris = PersonSelfRefM2M.objects.create(name="Chris")
  257
+>>> f = Friendship.objects.create(first=tony, second=chris, date_friended=datetime.now())
  258
+
  259
+# Tony should now show that Chris is his friend.
  260
+>>> tony.friends.all()
  261
+[<PersonSelfRefM2M: Chris>]
  262
+
  263
+# But we haven't established that Chris is Tony's Friend.
  264
+>>> chris.friends.all()
  265
+[]
  266
+
  267
+# So let's do that now.
  268
+>>> f2 = Friendship.objects.create(first=chris, second=tony, date_friended=datetime.now())
  269
+
  270
+# Having added Chris as a friend, let's make sure that his friend set reflects
  271
+# that addition.
  272
+>>> chris.friends.all()
  273
+[<PersonSelfRefM2M: Tony>]
  274
+
  275
+# Chris gets mad and wants to get rid of all of his friends.
  276
+>>> chris.friends.clear()
  277
+
  278
+# Now he should not have any more friends.
  279
+>>> chris.friends.all()
  280
+[]
  281
+
  282
+# Since this isn't a symmetrical relation, Tony's friend link still exists.
  283
+>>> tony.friends.all()
  284
+[<PersonSelfRefM2M: Chris>]
  285
+
  286
+
  287
+
  288
+### QUERY TESTS ###
  289
+
  290
+# We can query for the related model by using its attribute name (members, in 
  291
+# this case).
  292
+>>> Group.objects.filter(members__name='Bob')
  293
+[<Group: Roll>]
  294
+
  295
+# To query through the intermediary model, we specify its model name.
  296
+# In this case, membership.
  297
+>>> Group.objects.filter(membership__invite_reason="She was just awesome.")
  298
+[<Group: Rock>]
  299
+
  300
+# If we want to query in the reverse direction by the related model, use its
  301
+# model name (group, in this case).
  302
+>>> Person.objects.filter(group__name="Rock")
  303
+[<Person: Jane>, <Person: Jim>]
  304
+
  305
+# If the m2m field has specified a related_name, using that will work.
  306
+>>> Person.objects.filter(custom__name="Rock")
  307
+[<Person: Bob>, <Person: Jim>]
  308
+
  309
+# To query through the intermediary model in the reverse direction, we again
  310
+# specify its model name (membership, in this case).
  311
+>>> Person.objects.filter(membership__invite_reason="She was just awesome.")
  312
+[<Person: Jane>]
  313
+
  314
+# Let's see all of the groups that Jane joined after 1 Jan 2005:
  315
+>>> Group.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__person =jane)
  316
+[<Group: Rock>]
  317
+
  318
+# Queries also work in the reverse direction: Now let's see all of the people 
  319
+# that have joined Rock since 1 Jan 2005:
  320
+>>> Person.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__group=rock)
  321
+[<Person: Jane>, <Person: Jim>]
  322
+
  323
+# Conceivably, queries through membership could return correct, but non-unique
  324
+# querysets.  To demonstrate this, we query for all people who have joined a 
  325
+# group after 2004:
  326
+>>> Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1))
  327
+[<Person: Jane>, <Person: Jim>, <Person: Jim>]
  328
+
  329
+# Jim showed up twice, because he joined two groups ('Rock', and 'Roll'):
  330
+>>> [(m.person.name, m.group.name) for m in 
  331
+... Membership.objects.filter(date_joined__gt=datetime(2004, 1, 1))]
  332
+[(u'Jane', u'Rock'), (u'Jim', u'Rock'), (u'Jim', u'Roll')]
  333
+
  334
+# QuerySet's distinct() method can correct this problem.
  335
+>>> Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1)).distinct()
  336
+[<Person: Jane>, <Person: Jim>]
  337
+"""}
2  tests/regressiontests/m2m_through_regress/__init__.py
... ...
@@ -0,0 +1,2 @@
  1
+
  2
+
204  tests/regressiontests/m2m_through_regress/models.py
... ...
@@ -0,0 +1,204 @@
  1
+from django.db import models
  2
+from datetime import datetime
  3
+from django.contrib.auth.models import User
  4
+
  5
+# Forward declared intermediate model
  6
+class Membership(models.Model):
  7
+    person = models.ForeignKey('Person')
  8
+    group = models.ForeignKey('Group')
  9
+    date_joined = models.DateTimeField(default=datetime.now)
  10
+    
  11
+    def __unicode__(self):
  12
+        return "%s is a member of %s" % (self.person.name, self.group.name)
  13
+
  14
+class UserMembership(models.Model):
  15
+    user = models.ForeignKey(User)
  16
+    group = models.ForeignKey('Group')
  17
+    date_joined = models.DateTimeField(default=datetime.now)
  18
+    
  19
+    def __unicode__(self):
  20
+        return "%s is a user and member of %s" % (self.user.username, self.group.name)
  21
+
  22
+class Person(models.Model):
  23
+    name = models.CharField(max_length=128)
  24
+
  25
+    def __unicode__(self):