Fixed #6095 -- Added the ability to specify the model to use to manage 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
This commit is contained in:
Russell Keith-Magee 2008-07-29 12:41:08 +00:00
parent f752f69238
commit 174641b9b3
13 changed files with 957 additions and 43 deletions

View File

@ -154,6 +154,7 @@ answer newbie questions, and generally made Django that much better:
Maciej Fijalkowski Maciej Fijalkowski
Matthew Flanagan <http://wadofstuff.blogspot.com> Matthew Flanagan <http://wadofstuff.blogspot.com>
Eric Floehr <eric@intellovations.com> Eric Floehr <eric@intellovations.com>
Eric Florenzano <floguy@gmail.com>
Vincent Foley <vfoleybourgon@yahoo.ca> Vincent Foley <vfoleybourgon@yahoo.ca>
Rudolph Froger <rfroger@estrate.nl> Rudolph Froger <rfroger@estrate.nl>
Jorge Gajon <gajon@gajon.org> Jorge Gajon <gajon@gajon.org>

View File

@ -161,7 +161,10 @@ class BaseModelAdmin(object):
kwargs['empty_label'] = db_field.blank and _('None') or None kwargs['empty_label'] = db_field.blank and _('None') or None
else: else:
if isinstance(db_field, models.ManyToManyField): if isinstance(db_field, models.ManyToManyField):
if db_field.name in self.raw_id_fields: # If it uses an intermediary model, don't show field in admin.
if db_field.rel.through is not None:
return None
elif db_field.name in self.raw_id_fields:
kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel) kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel)
kwargs['help_text'] = '' kwargs['help_text'] = ''
elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)): elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):

View File

@ -104,6 +104,9 @@ class GenericRelation(RelatedField, Field):
limit_choices_to=kwargs.pop('limit_choices_to', None), limit_choices_to=kwargs.pop('limit_choices_to', None),
symmetrical=kwargs.pop('symmetrical', True)) symmetrical=kwargs.pop('symmetrical', True))
# By its very nature, a GenericRelation doesn't create a table.
self.creates_table = False
# Override content-type/object-id field names on the related class # Override content-type/object-id field names on the related class
self.object_id_field_name = kwargs.pop("object_id_field", "object_id") self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
self.content_type_field_name = kwargs.pop("content_type_field", "content_type") self.content_type_field_name = kwargs.pop("content_type_field", "content_type")

View File

@ -353,7 +353,7 @@ def many_to_many_sql_for_model(model, style):
qn = connection.ops.quote_name qn = connection.ops.quote_name
inline_references = connection.features.inline_fk_references inline_references = connection.features.inline_fk_references
for f in opts.local_many_to_many: for f in opts.local_many_to_many:
if not isinstance(f.rel, generic.GenericRel): if f.creates_table:
tablespace = f.db_tablespace or opts.db_tablespace tablespace = f.db_tablespace or opts.db_tablespace
if tablespace and connection.features.supports_tablespaces: if tablespace and connection.features.supports_tablespaces:
tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True) tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True)

View File

@ -102,6 +102,7 @@ def get_validation_errors(outfile, app=None):
if r.get_accessor_name() == rel_query_name: if r.get_accessor_name() == rel_query_name:
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)) 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))
seen_intermediary_signatures = []
for i, f in enumerate(opts.local_many_to_many): for i, f in enumerate(opts.local_many_to_many):
# Check to see if the related m2m field will clash with any # Check to see if the related m2m field will clash with any
# existing fields, m2m fields, m2m related objects or related # existing fields, m2m fields, m2m related objects or related
@ -112,7 +113,49 @@ def get_validation_errors(outfile, app=None):
# so skip the next section # so skip the next section
if isinstance(f.rel.to, (str, unicode)): if isinstance(f.rel.to, (str, unicode)):
continue continue
if getattr(f.rel, 'through', None) is not None:
if hasattr(f.rel, 'through_model'):
from_model, to_model = cls, f.rel.to
if from_model == to_model and f.rel.symmetrical:
e.add(opts, "Many-to-many fields with intermediate tables cannot be symmetrical.")
seen_from, seen_to, seen_self = False, False, 0
for inter_field in f.rel.through_model._meta.fields:
rel_to = getattr(inter_field.rel, 'to', None)
if from_model == to_model: # relation to self
if rel_to == from_model:
seen_self += 1
if seen_self > 2:
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))
else:
if rel_to == from_model:
if seen_from:
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))
else:
seen_from = True
elif rel_to == to_model:
if seen_to:
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))
else:
seen_to = True
if f.rel.through_model not in models.get_models():
e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed." % (f.name, f.rel.through))
signature = (f.rel.to, cls, f.rel.through_model)
if signature in seen_intermediary_signatures:
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))
else:
seen_intermediary_signatures.append(signature)
seen_related_fk, seen_this_fk = False, False
for field in f.rel.through_model._meta.fields:
if field.rel:
if not seen_related_fk and field.rel.to == f.rel.to:
seen_related_fk = True
elif field.rel.to == cls:
seen_this_fk = True
if not seen_related_fk or not seen_this_fk:
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))
else:
e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed" % (f.name, f.rel.through))
rel_opts = f.rel.to._meta rel_opts = f.rel.to._meta
rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name() rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
rel_query_name = f.related_query_name() rel_query_name = f.related_query_name()

View File

@ -23,7 +23,7 @@ RECURSIVE_RELATIONSHIP_CONSTANT = 'self'
pending_lookups = {} pending_lookups = {}
def add_lazy_relation(cls, field, relation): def add_lazy_relation(cls, field, relation, operation):
""" """
Adds a lookup on ``cls`` when a related field is defined using a string, Adds a lookup on ``cls`` when a related field is defined using a string,
i.e.:: i.e.::
@ -45,6 +45,8 @@ def add_lazy_relation(cls, field, relation):
If the other model hasn't yet been loaded -- almost a given if you're using If the other model hasn't yet been loaded -- almost a given if you're using
lazy relationships -- then the relation won't be set up until the lazy relationships -- then the relation won't be set up until the
class_prepared signal fires at the end of model initialization. class_prepared signal fires at the end of model initialization.
operation is the work that must be performed once the relation can be resolved.
""" """
# Check for recursive relations # Check for recursive relations
if relation == RECURSIVE_RELATIONSHIP_CONSTANT: if relation == RECURSIVE_RELATIONSHIP_CONSTANT:
@ -66,11 +68,10 @@ def add_lazy_relation(cls, field, relation):
# is prepared. # is prepared.
model = get_model(app_label, model_name, False) model = get_model(app_label, model_name, False)
if model: if model:
field.rel.to = model operation(field, model, cls)
field.do_related_class(model, cls)
else: else:
key = (app_label, model_name) key = (app_label, model_name)
value = (cls, field) value = (cls, field, operation)
pending_lookups.setdefault(key, []).append(value) pending_lookups.setdefault(key, []).append(value)
def do_pending_lookups(sender): def do_pending_lookups(sender):
@ -78,9 +79,8 @@ def do_pending_lookups(sender):
Handle any pending relations to the sending model. Sent from class_prepared. Handle any pending relations to the sending model. Sent from class_prepared.
""" """
key = (sender._meta.app_label, sender.__name__) key = (sender._meta.app_label, sender.__name__)
for cls, field in pending_lookups.pop(key, []): for cls, field, operation in pending_lookups.pop(key, []):
field.rel.to = sender operation(field, sender, cls)
field.do_related_class(sender, cls)
dispatcher.connect(do_pending_lookups, signal=signals.class_prepared) dispatcher.connect(do_pending_lookups, signal=signals.class_prepared)
@ -108,7 +108,10 @@ class RelatedField(object):
other = self.rel.to other = self.rel.to
if isinstance(other, basestring): if isinstance(other, basestring):
add_lazy_relation(cls, self, other) def resolve_related_class(field, model, cls):
field.rel.to = model
field.do_related_class(model, cls)
add_lazy_relation(cls, self, other, resolve_related_class)
else: else:
self.do_related_class(other, cls) self.do_related_class(other, cls)
@ -340,7 +343,7 @@ class ForeignRelatedObjectsDescriptor(object):
manager.clear() manager.clear()
manager.add(*value) manager.add(*value)
def create_many_related_manager(superclass): def create_many_related_manager(superclass, through=False):
"""Creates a manager that subclasses 'superclass' (which is a Manager) """Creates a manager that subclasses 'superclass' (which is a Manager)
and adds behavior for many-to-many related objects.""" and adds behavior for many-to-many related objects."""
class ManyRelatedManager(superclass): class ManyRelatedManager(superclass):
@ -354,6 +357,7 @@ def create_many_related_manager(superclass):
self.join_table = join_table self.join_table = join_table
self.source_col_name = source_col_name self.source_col_name = source_col_name
self.target_col_name = target_col_name self.target_col_name = target_col_name
self.through = through
self._pk_val = self.instance._get_pk_val() self._pk_val = self.instance._get_pk_val()
if self._pk_val is None: if self._pk_val is None:
raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__) raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__)
@ -361,21 +365,24 @@ def create_many_related_manager(superclass):
def get_query_set(self): def get_query_set(self):
return superclass.get_query_set(self).filter(**(self.core_filters)) return superclass.get_query_set(self).filter(**(self.core_filters))
def add(self, *objs): # If the ManyToMany relation has an intermediary model,
self._add_items(self.source_col_name, self.target_col_name, *objs) # the add and remove methods do not exist.
if through is None:
def add(self, *objs):
self._add_items(self.source_col_name, self.target_col_name, *objs)
# If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
if self.symmetrical: if self.symmetrical:
self._add_items(self.target_col_name, self.source_col_name, *objs) self._add_items(self.target_col_name, self.source_col_name, *objs)
add.alters_data = True add.alters_data = True
def remove(self, *objs): def remove(self, *objs):
self._remove_items(self.source_col_name, self.target_col_name, *objs) self._remove_items(self.source_col_name, self.target_col_name, *objs)
# If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
if self.symmetrical: if self.symmetrical:
self._remove_items(self.target_col_name, self.source_col_name, *objs) self._remove_items(self.target_col_name, self.source_col_name, *objs)
remove.alters_data = True remove.alters_data = True
def clear(self): def clear(self):
self._clear_items(self.source_col_name) self._clear_items(self.source_col_name)
@ -386,6 +393,10 @@ def create_many_related_manager(superclass):
clear.alters_data = True clear.alters_data = True
def create(self, **kwargs): def create(self, **kwargs):
# This check needs to be done here, since we can't later remove this
# from the method lookup table, as we do with add and remove.
if through is not None:
raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
new_obj = self.model(**kwargs) new_obj = self.model(**kwargs)
new_obj.save() new_obj.save()
self.add(new_obj) self.add(new_obj)
@ -473,7 +484,7 @@ class ManyRelatedObjectsDescriptor(object):
# model's default manager. # model's default manager.
rel_model = self.related.model rel_model = self.related.model
superclass = rel_model._default_manager.__class__ superclass = rel_model._default_manager.__class__
RelatedManager = create_many_related_manager(superclass) RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through)
qn = connection.ops.quote_name qn = connection.ops.quote_name
manager = RelatedManager( manager = RelatedManager(
@ -492,6 +503,10 @@ class ManyRelatedObjectsDescriptor(object):
if instance is None: if instance is None:
raise AttributeError, "Manager must be accessed via instance" raise AttributeError, "Manager must be accessed via instance"
through = getattr(self.related.field.rel, 'through', None)
if through is not None:
raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
manager = self.__get__(instance) manager = self.__get__(instance)
manager.clear() manager.clear()
manager.add(*value) manager.add(*value)
@ -514,7 +529,7 @@ class ReverseManyRelatedObjectsDescriptor(object):
# model's default manager. # model's default manager.
rel_model=self.field.rel.to rel_model=self.field.rel.to
superclass = rel_model._default_manager.__class__ superclass = rel_model._default_manager.__class__
RelatedManager = create_many_related_manager(superclass) RelatedManager = create_many_related_manager(superclass, self.field.rel.through)
qn = connection.ops.quote_name qn = connection.ops.quote_name
manager = RelatedManager( manager = RelatedManager(
@ -533,6 +548,10 @@ class ReverseManyRelatedObjectsDescriptor(object):
if instance is None: if instance is None:
raise AttributeError, "Manager must be accessed via instance" raise AttributeError, "Manager must be accessed via instance"
through = getattr(self.field.rel, 'through', None)
if through is not None:
raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
manager = self.__get__(instance) manager = self.__get__(instance)
manager.clear() manager.clear()
manager.add(*value) manager.add(*value)
@ -584,7 +603,7 @@ class OneToOneRel(ManyToOneRel):
class ManyToManyRel(object): class ManyToManyRel(object):
def __init__(self, to, num_in_admin=0, related_name=None, def __init__(self, to, num_in_admin=0, related_name=None,
limit_choices_to=None, symmetrical=True): limit_choices_to=None, symmetrical=True, through=None):
self.to = to self.to = to
self.num_in_admin = num_in_admin self.num_in_admin = num_in_admin
self.related_name = related_name self.related_name = related_name
@ -594,6 +613,7 @@ class ManyToManyRel(object):
self.edit_inline = False self.edit_inline = False
self.symmetrical = symmetrical self.symmetrical = symmetrical
self.multiple = True self.multiple = True
self.through = through
class ForeignKey(RelatedField, Field): class ForeignKey(RelatedField, Field):
empty_strings_allowed = False empty_strings_allowed = False
@ -723,8 +743,16 @@ class ManyToManyField(RelatedField, Field):
num_in_admin=kwargs.pop('num_in_admin', 0), num_in_admin=kwargs.pop('num_in_admin', 0),
related_name=kwargs.pop('related_name', None), related_name=kwargs.pop('related_name', None),
limit_choices_to=kwargs.pop('limit_choices_to', None), limit_choices_to=kwargs.pop('limit_choices_to', None),
symmetrical=kwargs.pop('symmetrical', True)) symmetrical=kwargs.pop('symmetrical', True),
through=kwargs.pop('through', None))
self.db_table = kwargs.pop('db_table', None) self.db_table = kwargs.pop('db_table', None)
if kwargs['rel'].through is not None:
self.creates_table = False
assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used."
else:
self.creates_table = True
Field.__init__(self, **kwargs) Field.__init__(self, **kwargs)
msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.') msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.')
@ -739,26 +767,62 @@ class ManyToManyField(RelatedField, Field):
def _get_m2m_db_table(self, opts): def _get_m2m_db_table(self, opts):
"Function that can be curried to provide the m2m table name for this relation" "Function that can be curried to provide the m2m table name for this relation"
if self.db_table: if self.rel.through is not None:
return self.rel.through_model._meta.db_table
elif self.db_table:
return self.db_table return self.db_table
else: else:
return '%s_%s' % (opts.db_table, self.name) return '%s_%s' % (opts.db_table, self.name)
def _get_m2m_column_name(self, related): def _get_m2m_column_name(self, related):
"Function that can be curried to provide the source column name for the m2m table" "Function that can be curried to provide the source column name for the m2m table"
# If this is an m2m relation to self, avoid the inevitable name clash try:
if related.model == related.parent_model: return self._m2m_column_name_cache
return 'from_' + related.model._meta.object_name.lower() + '_id' except:
else: if self.rel.through is not None:
return related.model._meta.object_name.lower() + '_id' for f in self.rel.through_model._meta.fields:
if hasattr(f,'rel') and f.rel and f.rel.to == related.model:
self._m2m_column_name_cache = f.column
break
# If this is an m2m relation to self, avoid the inevitable name clash
elif related.model == related.parent_model:
self._m2m_column_name_cache = 'from_' + related.model._meta.object_name.lower() + '_id'
else:
self._m2m_column_name_cache = related.model._meta.object_name.lower() + '_id'
# Return the newly cached value
return self._m2m_column_name_cache
def _get_m2m_reverse_name(self, related): def _get_m2m_reverse_name(self, related):
"Function that can be curried to provide the related column name for the m2m table" "Function that can be curried to provide the related column name for the m2m table"
# If this is an m2m relation to self, avoid the inevitable name clash try:
if related.model == related.parent_model: return self._m2m_reverse_name_cache
return 'to_' + related.parent_model._meta.object_name.lower() + '_id' except:
else: if self.rel.through is not None:
return related.parent_model._meta.object_name.lower() + '_id' found = False
for f in self.rel.through_model._meta.fields:
if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model:
if related.model == related.parent_model:
# If this is an m2m-intermediate to self,
# the first foreign key you find will be
# the source column. Keep searching for
# the second foreign key.
if found:
self._m2m_reverse_name_cache = f.column
break
else:
found = True
else:
self._m2m_reverse_name_cache = f.column
break
# If this is an m2m relation to self, avoid the inevitable name clash
elif related.model == related.parent_model:
self._m2m_reverse_name_cache = 'to_' + related.parent_model._meta.object_name.lower() + '_id'
else:
self._m2m_reverse_name_cache = related.parent_model._meta.object_name.lower() + '_id'
# Return the newly cached value
return self._m2m_reverse_name_cache
def isValidIDList(self, field_data, all_data): def isValidIDList(self, field_data, all_data):
"Validates that the value is a valid list of foreign keys" "Validates that the value is a valid list of foreign keys"
@ -792,13 +856,23 @@ class ManyToManyField(RelatedField, Field):
return new_data return new_data
def contribute_to_class(self, cls, name): def contribute_to_class(self, cls, name):
super(ManyToManyField, self).contribute_to_class(cls, name) super(ManyToManyField, self).contribute_to_class(cls, name)
# Add the descriptor for the m2m relation # Add the descriptor for the m2m relation
setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self)) setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self))
# Set up the accessor for the m2m table name for the relation # Set up the accessor for the m2m table name for the relation
self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta) self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta)
# Populate some necessary rel arguments so that cross-app relations
# work correctly.
if isinstance(self.rel.through, basestring):
def resolve_through_model(field, model, cls):
field.rel.through_model = model
add_lazy_relation(cls, self, self.rel.through, resolve_through_model)
elif self.rel.through:
self.rel.through_model = self.rel.through
self.rel.through = self.rel.through._meta.object_name
if isinstance(self.rel.to, basestring): if isinstance(self.rel.to, basestring):
target = self.rel.to target = self.rel.to
else: else:

View File

@ -617,6 +617,61 @@ automatically::
FriendshipInline, FriendshipInline,
] ]
Working with Many-to-Many Intermediary Models
----------------------------------------------
By default, admin widgets for many-to-many relations will be displayed inline
on whichever model contains the actual reference to the `ManyToManyField`.
However, when you specify an intermediary model using the ``through``
argument to a ``ManyToManyField``, the admin will not display a widget by
default. This is because each instance of that intermediary model requires
more information than could be displayed in a single widget, and the layout
required for multiple widgets will vary depending on the intermediate model.
However, we still want to be able to edit that information inline. Fortunately,
this is easy to do with inline admin models. Suppose we have the following
models::
class Person(models.Model):
name = models.CharField(max_length=128)
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
class Membership(models.Model):
person = models.ForeignKey(Person)
group = models.ForeignKey(Group)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
The first step in displaying this intermediate model in the admin is to
define an inline model for the Membership table::
class MembershipInline(admin.TabularInline):
model = Membership
extra = 1
This simple example uses the defaults inline form for the Membership model,
and shows 1 extra line. This could be customized using any of the options
available to inline models.
Now create admin views for the ``Person`` and ``Group`` models::
class PersonAdmin(admin.ModelAdmin):
inlines = (MembershipInline,)
class GroupAdmin(admin.ModelAdmin):
inlines = (MembershipInline,)
Finally, register your ``Person`` and ``Group`` models with the admin site::
admin.site.register(Person, PersonAdmin)
admin.site.register(Group, GroupAdmin)
Now your admin site is set up to edit ``Membership`` objects inline from either
the ``Person`` or the ``Group`` detail pages.
``AdminSite`` objects ``AdminSite`` objects
===================== =====================

View File

@ -655,7 +655,7 @@ Note that this value is *not* HTML-escaped when it's displayed in the admin
interface. This lets you include HTML in ``help_text`` if you so desire. For interface. This lets you include HTML in ``help_text`` if you so desire. For
example:: example::
help_text="Please use the following format: <em>YYYY-MM-DD</em>." help_text="Please use the following format: <em>YYYY-MM-DD</em>."
Alternatively you can use plain text and Alternatively you can use plain text and
``django.utils.html.escape()`` to escape any HTML special characters. ``django.utils.html.escape()`` to escape any HTML special characters.
@ -944,6 +944,131 @@ the relationship should work. All are optional:
======================= ============================================================ ======================= ============================================================
Extra fields on many-to-many relationships
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**New in Django development version**
When you're only dealing with simple many-to-many relationships such as
mixing and matching pizzas and toppings, a standard ``ManyToManyField``
is all you need. However, sometimes you may need to associate data with the
relationship between two models.
For example, consider the case of an application tracking the musical groups
which musicians belong to. There is a many-to-many relationship between a person
and the groups of which they are a member, so you could use a ManyToManyField
to represent this relationship. However, there is a lot of detail about the
membership that you might want to collect, such as the date at which the person
joined the group.
For these situations, Django allows you to specify the model that will be used
to govern the many-to-many relationship. You can then put extra fields on the
intermediate model. The intermediate model is associated with the
``ManyToManyField`` using the ``through`` argument to point to the model
that will act as an intermediary. For our musician example, the code would look
something like this::
class Person(models.Model):
name = models.CharField(max_length=128)
def __unicode__(self):
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
def __unicode__(self):
return self.name
class Membership(models.Model):
person = models.ForeignKey(Person)
group = models.ForeignKey(Group)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
When you set up the intermediary model, you explicitly specify foreign
keys to the models that are involved in the ManyToMany relation. This
explicit declaration defines how the two models are related.
There are a few restrictions on the intermediate model:
* Your intermediate model must contain one - and *only* one - foreign key
on the target model (this would be ``Person`` in our example). If you
have more than one foreign key, a validation error will be raised.
* Your intermediate model must contain one - and *only* one - foreign key
on the source model (this would be ``Group`` in our example). If you
have more than one foreign key, a validation error will be raised.
* If the many-to-many relation is a relation on itself, the relationship
must be non-symmetric.
Now that you have set up your ``ManyToManyField`` to use your intermediary
model (Membership, in this case), you're ready to start creating some
many-to-many relationships. You do this by creating instances of the
intermediate model::
>>> ringo = Person.objects.create(name="Ringo Starr")
>>> paul = Person.objects.create(name="Paul McCartney")
>>> beatles = Group.objects.create(name="The Beatles")
>>> m1 = Membership(person=ringo, group=beatles,
... date_joined=date(1962, 8, 16),
... invite_reason= "Needed a new drummer.")
>>> m1.save()
>>> beatles.members.all()
[<Person: Ringo Starr>]
>>> ringo.group_set.all()
[<Group: The Beatles>]
>>> m2 = Membership.objects.create(person=paul, group=beatles,
... date_joined=date(1960, 8, 1),
... invite_reason= "Wanted to form a band.")
>>> beatles.members.all()
[<Person: Ringo Starr>, <Person: Paul McCartney>]
Unlike normal many-to-many fields, you *can't* use ``add``, ``create``,
or assignment (i.e., ``beatles.members = [...]``) to create relationships::
# THIS WILL NOT WORK
>>> beatles.members.add(john)
# NEITHER WILL THIS
>>> beatles.members.create(name="George Harrison")
# AND NEITHER WILL THIS
>>> beatles.members = [john, paul, ringo, george]
Why? You can't just create a relationship between a Person and a Group - you
need to specify all the detail for the relationship required by the
Membership table. The simple ``add``, ``create`` and assignment calls
don't provide a way to specify this extra detail. As a result, they are
disabled for many-to-many relationships that use an intermediate model.
The only way to create a many-to-many relationship with an intermediate table
is to create instances of the intermediate model.
The ``remove`` method is disabled for similar reasons. However, the
``clear()`` method can be used to remove all many-to-many relationships
for an instance::
# Beatles have broken up
>>> beatles.members.clear()
Once you have established the many-to-many relationships by creating instances
of your intermediate model, you can issue queries. Just as with normal
many-to-many relationships, you can query using the attributes of the
many-to-many-related model::
# Find all the groups with a member whose name starts with 'Paul'
>>> Groups.objects.filter(person__name__startswith='Paul')
[<Group: The Beatles>]
As you are using an intermediate table, you can also query on the attributes
of the intermediate model::
# Find all the members of the Beatles that joined after 1 Jan 1961
>>> Person.objects.filter(
... group__name='The Beatles',
... membership__date_joined__gt=date(1961,1,1))
[<Person: Ringo Starr]
One-to-one relationships One-to-one relationships
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
@ -1145,7 +1270,7 @@ any parent classes in ``unique_together``.
For convenience, unique_together can be a single list when dealing For convenience, unique_together can be a single list when dealing
with a single set of fields:: with a single set of fields::
unique_together = ("driver", "restaurant") unique_together = ("driver", "restaurant")
``verbose_name`` ``verbose_name``
---------------- ----------------

View File

@ -110,6 +110,63 @@ class Car(models.Model):
class MissingRelations(models.Model): class MissingRelations(models.Model):
rel1 = models.ForeignKey("Rel1") rel1 = models.ForeignKey("Rel1")
rel2 = models.ManyToManyField("Rel2") rel2 = models.ManyToManyField("Rel2")
class MissingManualM2MModel(models.Model):
name = models.CharField(max_length=5)
missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel")
class Person(models.Model):
name = models.CharField(max_length=5)
class Group(models.Model):
name = models.CharField(max_length=5)
primary = models.ManyToManyField(Person, through="Membership", related_name="primary")
secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary")
tertiary = models.ManyToManyField(Person, through="RelationshipDoubleFK", related_name="tertiary")
class GroupTwo(models.Model):
name = models.CharField(max_length=5)
primary = models.ManyToManyField(Person, through="Membership")
secondary = models.ManyToManyField(Group, through="MembershipMissingFK")
class Membership(models.Model):
person = models.ForeignKey(Person)
group = models.ForeignKey(Group)
not_default_or_null = models.CharField(max_length=5)
class MembershipMissingFK(models.Model):
person = models.ForeignKey(Person)
class PersonSelfRefM2M(models.Model):
name = models.CharField(max_length=5)
friends = models.ManyToManyField('self', through="Relationship")
too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK")
class PersonSelfRefM2MExplicit(models.Model):
name = models.CharField(max_length=5)
friends = models.ManyToManyField('self', through="ExplicitRelationship", symmetrical=True)
class Relationship(models.Model):
first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set")
second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set")
date_added = models.DateTimeField()
class ExplicitRelationship(models.Model):
first = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_from_set")
second = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_to_set")
date_added = models.DateTimeField()
class RelationshipTripleFK(models.Model):
first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2")
second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2")
third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far")
date_added = models.DateTimeField()
class RelationshipDoubleFK(models.Model):
first = models.ForeignKey(Person, related_name="first_related_name")
second = models.ForeignKey(Person, related_name="second_related_name")
third = models.ForeignKey(Group, related_name="rel_to_set")
date_added = models.DateTimeField()
model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute. model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute.
invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute. invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
@ -195,4 +252,12 @@ invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_3' clashes wi
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'. 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'.
invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed
invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed
invalid_models.grouptwo: 'primary' has a manually-defined m2m relation through model Membership, which does not have foreign keys to Person and GroupTwo
invalid_models.grouptwo: 'secondary' has a manually-defined m2m relation through model MembershipMissingFK, which does not have foreign keys to Group and GroupTwo
invalid_models.missingmanualm2mmodel: 'missing_m2m' specifies an m2m relation through model MissingM2MModel, which has not been installed
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.
invalid_models.group: Intermediary model RelationshipDoubleFK has more than one foreign key to Person, which is ambiguous and is not permitted.
invalid_models.personselfrefm2m: Many-to-many fields with intermediate tables cannot be symmetrical.
invalid_models.personselfrefm2m: Intermediary model RelationshipTripleFK has more than two foreign keys to PersonSelfRefM2M, which is ambiguous and is not permitted.
invalid_models.personselfrefm2mexplicit: Many-to-many fields with intermediate tables cannot be symmetrical.
""" """

View File

@ -0,0 +1,2 @@

View File

@ -0,0 +1,337 @@
from django.db import models
from datetime import datetime
# M2M described on one of the models
class Person(models.Model):
name = models.CharField(max_length=128)
class Meta:
ordering = ('name',)
def __unicode__(self):
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
custom_members = models.ManyToManyField(Person, through='CustomMembership', related_name="custom")
nodefaultsnonulls = models.ManyToManyField(Person, through='TestNoDefaultsOrNulls', related_name="testnodefaultsnonulls")
class Meta:
ordering = ('name',)
def __unicode__(self):
return self.name
class Membership(models.Model):
person = models.ForeignKey(Person)
group = models.ForeignKey(Group)
date_joined = models.DateTimeField(default=datetime.now)
invite_reason = models.CharField(max_length=64, null=True)
class Meta:
ordering = ('date_joined','invite_reason')
def __unicode__(self):
return "%s is a member of %s" % (self.person.name, self.group.name)
class CustomMembership(models.Model):
person = models.ForeignKey(Person, db_column="custom_person_column", related_name="custom_person_related_name")
group = models.ForeignKey(Group)
weird_fk = models.ForeignKey(Membership, null=True)
date_joined = models.DateTimeField(default=datetime.now)
def __unicode__(self):
return "%s is a member of %s" % (self.person.name, self.group.name)
class Meta:
db_table = "test_table"
class TestNoDefaultsOrNulls(models.Model):
person = models.ForeignKey(Person)
group = models.ForeignKey(Group)
nodefaultnonull = models.CharField(max_length=5)
class PersonSelfRefM2M(models.Model):
name = models.CharField(max_length=5)
friends = models.ManyToManyField('self', through="Friendship", symmetrical=False)
def __unicode__(self):
return self.name
class Friendship(models.Model):
first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set")
second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set")
date_friended = models.DateTimeField()
__test__ = {'API_TESTS':"""
>>> from datetime import datetime
### Creation and Saving Tests ###
>>> bob = Person.objects.create(name='Bob')
>>> jim = Person.objects.create(name='Jim')
>>> jane = Person.objects.create(name='Jane')
>>> rock = Group.objects.create(name='Rock')
>>> roll = Group.objects.create(name='Roll')
# We start out by making sure that the Group 'rock' has no members.
>>> rock.members.all()
[]
# To make Jim a member of Group Rock, simply create a Membership object.
>>> m1 = Membership.objects.create(person=jim, group=rock)
# We can do the same for Jane and Rock.
>>> m2 = Membership.objects.create(person=jane, group=rock)
# Let's check to make sure that it worked. Jane and Jim should be members of Rock.
>>> rock.members.all()
[<Person: Jane>, <Person: Jim>]
# Now we can add a bunch more Membership objects to test with.
>>> m3 = Membership.objects.create(person=bob, group=roll)
>>> m4 = Membership.objects.create(person=jim, group=roll)
>>> m5 = Membership.objects.create(person=jane, group=roll)
# We can get Jim's Group membership as with any ForeignKey.
>>> jim.group_set.all()
[<Group: Rock>, <Group: Roll>]
# Querying the intermediary model works like normal.
# In this case we get Jane's membership to Rock.
>>> m = Membership.objects.get(person=jane, group=rock)
>>> m
<Membership: Jane is a member of Rock>
# Now we set some date_joined dates for further testing.
>>> m2.invite_reason = "She was just awesome."
>>> m2.date_joined = datetime(2006, 1, 1)
>>> m2.save()
>>> m5.date_joined = datetime(2004, 1, 1)
>>> m5.save()
>>> m3.date_joined = datetime(2004, 1, 1)
>>> m3.save()
# It's not only get that works. Filter works like normal as well.
>>> Membership.objects.filter(person=jim)
[<Membership: Jim is a member of Rock>, <Membership: Jim is a member of Roll>]
### Forward Descriptors Tests ###
# Due to complications with adding via an intermediary model,
# the add method is not provided.
>>> rock.members.add(bob)
Traceback (most recent call last):
...
AttributeError: 'ManyRelatedManager' object has no attribute 'add'
# Create is also disabled as it suffers from the same problems as add.
>>> rock.members.create(name='Anne')
Traceback (most recent call last):
...
AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
# Remove has similar complications, and is not provided either.
>>> rock.members.remove(jim)
Traceback (most recent call last):
...
AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
# Here we back up the list of all members of Rock.
>>> backup = list(rock.members.all())
# ...and we verify that it has worked.
>>> backup
[<Person: Jane>, <Person: Jim>]
# The clear function should still work.
>>> rock.members.clear()
# Now there will be no members of Rock.
>>> rock.members.all()
[]
# Assignment should not work with models specifying a through model for many of
# the same reasons as adding.
>>> rock.members = backup
Traceback (most recent call last):
...
AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
# Let's re-save those instances that we've cleared.
>>> m1.save()
>>> m2.save()
# Verifying that those instances were re-saved successfully.
>>> rock.members.all()
[<Person: Jane>, <Person: Jim>]
### Reverse Descriptors Tests ###
# Due to complications with adding via an intermediary model,
# the add method is not provided.
>>> bob.group_set.add(rock)
Traceback (most recent call last):
...
AttributeError: 'ManyRelatedManager' object has no attribute 'add'
# Create is also disabled as it suffers from the same problems as add.
>>> bob.group_set.create(name='Funk')
Traceback (most recent call last):
...
AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
# Remove has similar complications, and is not provided either.
>>> jim.group_set.remove(rock)
Traceback (most recent call last):
...
AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
# Here we back up the list of all of Jim's groups.
>>> backup = list(jim.group_set.all())
>>> backup
[<Group: Rock>, <Group: Roll>]
# The clear function should still work.
>>> jim.group_set.clear()
# Now Jim will be in no groups.
>>> jim.group_set.all()
[]
# Assignment should not work with models specifying a through model for many of
# the same reasons as adding.
>>> jim.group_set = backup
Traceback (most recent call last):
...
AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
# Let's re-save those instances that we've cleared.
>>> m1.save()
>>> m4.save()
# Verifying that those instances were re-saved successfully.
>>> jim.group_set.all()
[<Group: Rock>, <Group: Roll>]
### Custom Tests ###
# Let's see if we can query through our second relationship.
>>> rock.custom_members.all()
[]
# We can query in the opposite direction as well.
>>> bob.custom.all()
[]
# Let's create some membership objects in this custom relationship.
>>> cm1 = CustomMembership.objects.create(person=bob, group=rock)
>>> cm2 = CustomMembership.objects.create(person=jim, group=rock)
# If we get the number of people in Rock, it should be both Bob and Jim.
>>> rock.custom_members.all()
[<Person: Bob>, <Person: Jim>]
# Bob should only be in one custom group.
>>> bob.custom.all()
[<Group: Rock>]
# Let's make sure our new descriptors don't conflict with the FK related_name.
>>> bob.custom_person_related_name.all()
[<CustomMembership: Bob is a member of Rock>]
### SELF-REFERENTIAL TESTS ###
# Let's first create a person who has no friends.
>>> tony = PersonSelfRefM2M.objects.create(name="Tony")
>>> tony.friends.all()
[]
# Now let's create another person for Tony to be friends with.
>>> chris = PersonSelfRefM2M.objects.create(name="Chris")
>>> f = Friendship.objects.create(first=tony, second=chris, date_friended=datetime.now())
# Tony should now show that Chris is his friend.
>>> tony.friends.all()
[<PersonSelfRefM2M: Chris>]
# But we haven't established that Chris is Tony's Friend.
>>> chris.friends.all()
[]
# So let's do that now.
>>> f2 = Friendship.objects.create(first=chris, second=tony, date_friended=datetime.now())
# Having added Chris as a friend, let's make sure that his friend set reflects
# that addition.
>>> chris.friends.all()
[<PersonSelfRefM2M: Tony>]
# Chris gets mad and wants to get rid of all of his friends.
>>> chris.friends.clear()
# Now he should not have any more friends.
>>> chris.friends.all()
[]
# Since this isn't a symmetrical relation, Tony's friend link still exists.
>>> tony.friends.all()
[<PersonSelfRefM2M: Chris>]
### QUERY TESTS ###
# We can query for the related model by using its attribute name (members, in
# this case).
>>> Group.objects.filter(members__name='Bob')
[<Group: Roll>]
# To query through the intermediary model, we specify its model name.
# In this case, membership.
>>> Group.objects.filter(membership__invite_reason="She was just awesome.")
[<Group: Rock>]
# If we want to query in the reverse direction by the related model, use its
# model name (group, in this case).
>>> Person.objects.filter(group__name="Rock")
[<Person: Jane>, <Person: Jim>]
# If the m2m field has specified a related_name, using that will work.
>>> Person.objects.filter(custom__name="Rock")
[<Person: Bob>, <Person: Jim>]
# To query through the intermediary model in the reverse direction, we again
# specify its model name (membership, in this case).
>>> Person.objects.filter(membership__invite_reason="She was just awesome.")
[<Person: Jane>]
# Let's see all of the groups that Jane joined after 1 Jan 2005:
>>> Group.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__person =jane)
[<Group: Rock>]
# Queries also work in the reverse direction: Now let's see all of the people
# that have joined Rock since 1 Jan 2005:
>>> Person.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__group=rock)
[<Person: Jane>, <Person: Jim>]
# Conceivably, queries through membership could return correct, but non-unique
# querysets. To demonstrate this, we query for all people who have joined a
# group after 2004:
>>> Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1))
[<Person: Jane>, <Person: Jim>, <Person: Jim>]
# Jim showed up twice, because he joined two groups ('Rock', and 'Roll'):
>>> [(m.person.name, m.group.name) for m in
... Membership.objects.filter(date_joined__gt=datetime(2004, 1, 1))]
[(u'Jane', u'Rock'), (u'Jim', u'Rock'), (u'Jim', u'Roll')]
# QuerySet's distinct() method can correct this problem.
>>> Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1)).distinct()
[<Person: Jane>, <Person: Jim>]
"""}

View File

@ -0,0 +1,2 @@

View File

@ -0,0 +1,204 @@
from django.db import models
from datetime import datetime
from django.contrib.auth.models import User
# Forward declared intermediate model
class Membership(models.Model):
person = models.ForeignKey('Person')
group = models.ForeignKey('Group')
date_joined = models.DateTimeField(default=datetime.now)
def __unicode__(self):
return "%s is a member of %s" % (self.person.name, self.group.name)
class UserMembership(models.Model):
user = models.ForeignKey(User)
group = models.ForeignKey('Group')
date_joined = models.DateTimeField(default=datetime.now)
def __unicode__(self):
return "%s is a user and member of %s" % (self.user.username, self.group.name)
class Person(models.Model):
name = models.CharField(max_length=128)
def __unicode__(self):
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
# Membership object defined as a class
members = models.ManyToManyField(Person, through=Membership)
user_members = models.ManyToManyField(User, through='UserMembership')
def __unicode__(self):
return self.name
__test__ = {'API_TESTS':"""
# Create some dummy data
>>> bob = Person.objects.create(name='Bob')
>>> jim = Person.objects.create(name='Jim')
>>> rock = Group.objects.create(name='Rock')
>>> roll = Group.objects.create(name='Roll')
>>> frank = User.objects.create_user('frank','frank@example.com','password')
>>> jane = User.objects.create_user('jane','jane@example.com','password')
# Now test that the forward declared Membership works
>>> Membership.objects.create(person=bob, group=rock)
<Membership: Bob is a member of Rock>
>>> Membership.objects.create(person=bob, group=roll)
<Membership: Bob is a member of Roll>
>>> Membership.objects.create(person=jim, group=rock)
<Membership: Jim is a member of Rock>
>>> bob.group_set.all()
[<Group: Rock>, <Group: Roll>]
>>> roll.members.all()
[<Person: Bob>]
# Error messages use the model name, not repr of the class name
>>> bob.group_set = []
Traceback (most recent call last):
...
AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
>>> roll.members = []
Traceback (most recent call last):
...
AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
>>> rock.members.create(name='Anne')
Traceback (most recent call last):
...
AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
>>> bob.group_set.create(name='Funk')
Traceback (most recent call last):
...
AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
# Now test that the intermediate with a relationship outside
# the current app (i.e., UserMembership) workds
>>> UserMembership.objects.create(user=frank, group=rock)
<UserMembership: frank is a user and member of Rock>
>>> UserMembership.objects.create(user=frank, group=roll)
<UserMembership: frank is a user and member of Roll>
>>> UserMembership.objects.create(user=jane, group=rock)
<UserMembership: jane is a user and member of Rock>
>>> frank.group_set.all()
[<Group: Rock>, <Group: Roll>]
>>> roll.user_members.all()
[<User: frank>]
"""}
from django.db import models
from datetime import datetime
from django.contrib.auth.models import User
# Forward declared intermediate model
class Membership(models.Model):
person = models.ForeignKey('Person')
group = models.ForeignKey('Group')
date_joined = models.DateTimeField(default=datetime.now)
def __unicode__(self):
return "%s is a member of %s" % (self.person.name, self.group.name)
class UserMembership(models.Model):
user = models.ForeignKey(User)
group = models.ForeignKey('Group')
date_joined = models.DateTimeField(default=datetime.now)
def __unicode__(self):
return "%s is a user and member of %s" % (self.user.username, self.group.name)
class Person(models.Model):
name = models.CharField(max_length=128)
def __unicode__(self):
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
# Membership object defined as a class
members = models.ManyToManyField(Person, through=Membership)
user_members = models.ManyToManyField(User, through='UserMembership')
def __unicode__(self):
return self.name
__test__ = {'API_TESTS':"""
# Create some dummy data
>>> bob = Person.objects.create(name='Bob')
>>> jim = Person.objects.create(name='Jim')
>>> rock = Group.objects.create(name='Rock')
>>> roll = Group.objects.create(name='Roll')
>>> frank = User.objects.create_user('frank','frank@example.com','password')
>>> jane = User.objects.create_user('jane','jane@example.com','password')
# Now test that the forward declared Membership works
>>> Membership.objects.create(person=bob, group=rock)
<Membership: Bob is a member of Rock>
>>> Membership.objects.create(person=bob, group=roll)
<Membership: Bob is a member of Roll>
>>> Membership.objects.create(person=jim, group=rock)
<Membership: Jim is a member of Rock>
>>> bob.group_set.all()
[<Group: Rock>, <Group: Roll>]
>>> roll.members.all()
[<Person: Bob>]
# Error messages use the model name, not repr of the class name
>>> bob.group_set = []
Traceback (most recent call last):
...
AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
>>> roll.members = []
Traceback (most recent call last):
...
AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
>>> rock.members.create(name='Anne')
Traceback (most recent call last):
...
AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
>>> bob.group_set.create(name='Funk')
Traceback (most recent call last):
...
AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
# Now test that the intermediate with a relationship outside
# the current app (i.e., UserMembership) workds
>>> UserMembership.objects.create(user=frank, group=rock)
<UserMembership: frank is a user and member of Rock>
>>> UserMembership.objects.create(user=frank, group=roll)
<UserMembership: frank is a user and member of Roll>
>>> UserMembership.objects.create(user=jane, group=rock)
<UserMembership: jane is a user and member of Rock>
>>> frank.group_set.all()
[<Group: Rock>, <Group: Roll>]
>>> roll.user_members.all()
[<User: frank>]
"""}