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:
parent
f752f69238
commit
174641b9b3
1
AUTHORS
1
AUTHORS
|
@ -154,6 +154,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Maciej Fijalkowski
|
||||
Matthew Flanagan <http://wadofstuff.blogspot.com>
|
||||
Eric Floehr <eric@intellovations.com>
|
||||
Eric Florenzano <floguy@gmail.com>
|
||||
Vincent Foley <vfoleybourgon@yahoo.ca>
|
||||
Rudolph Froger <rfroger@estrate.nl>
|
||||
Jorge Gajon <gajon@gajon.org>
|
||||
|
|
|
@ -161,7 +161,10 @@ class BaseModelAdmin(object):
|
|||
kwargs['empty_label'] = db_field.blank and _('None') or None
|
||||
else:
|
||||
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['help_text'] = ''
|
||||
elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
|
||||
|
|
|
@ -104,6 +104,9 @@ class GenericRelation(RelatedField, Field):
|
|||
limit_choices_to=kwargs.pop('limit_choices_to', None),
|
||||
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
|
||||
self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
|
||||
self.content_type_field_name = kwargs.pop("content_type_field", "content_type")
|
||||
|
|
|
@ -353,7 +353,7 @@ def many_to_many_sql_for_model(model, style):
|
|||
qn = connection.ops.quote_name
|
||||
inline_references = connection.features.inline_fk_references
|
||||
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
|
||||
if tablespace and connection.features.supports_tablespaces:
|
||||
tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True)
|
||||
|
|
|
@ -102,6 +102,7 @@ def get_validation_errors(outfile, app=None):
|
|||
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))
|
||||
|
||||
seen_intermediary_signatures = []
|
||||
for i, f in enumerate(opts.local_many_to_many):
|
||||
# Check to see if the related m2m field will clash with any
|
||||
# existing fields, m2m fields, m2m related objects or related
|
||||
|
@ -112,6 +113,48 @@ def get_validation_errors(outfile, app=None):
|
|||
# so skip the next section
|
||||
if isinstance(f.rel.to, (str, unicode)):
|
||||
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_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
|
||||
|
|
|
@ -23,7 +23,7 @@ RECURSIVE_RELATIONSHIP_CONSTANT = 'self'
|
|||
|
||||
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,
|
||||
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
|
||||
lazy relationships -- then the relation won't be set up until the
|
||||
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
|
||||
if relation == RECURSIVE_RELATIONSHIP_CONSTANT:
|
||||
|
@ -66,11 +68,10 @@ def add_lazy_relation(cls, field, relation):
|
|||
# is prepared.
|
||||
model = get_model(app_label, model_name, False)
|
||||
if model:
|
||||
field.rel.to = model
|
||||
field.do_related_class(model, cls)
|
||||
operation(field, model, cls)
|
||||
else:
|
||||
key = (app_label, model_name)
|
||||
value = (cls, field)
|
||||
value = (cls, field, operation)
|
||||
pending_lookups.setdefault(key, []).append(value)
|
||||
|
||||
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.
|
||||
"""
|
||||
key = (sender._meta.app_label, sender.__name__)
|
||||
for cls, field in pending_lookups.pop(key, []):
|
||||
field.rel.to = sender
|
||||
field.do_related_class(sender, cls)
|
||||
for cls, field, operation in pending_lookups.pop(key, []):
|
||||
operation(field, sender, cls)
|
||||
|
||||
dispatcher.connect(do_pending_lookups, signal=signals.class_prepared)
|
||||
|
||||
|
@ -108,7 +108,10 @@ class RelatedField(object):
|
|||
|
||||
other = self.rel.to
|
||||
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:
|
||||
self.do_related_class(other, cls)
|
||||
|
||||
|
@ -340,7 +343,7 @@ class ForeignRelatedObjectsDescriptor(object):
|
|||
manager.clear()
|
||||
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)
|
||||
and adds behavior for many-to-many related objects."""
|
||||
class ManyRelatedManager(superclass):
|
||||
|
@ -354,6 +357,7 @@ def create_many_related_manager(superclass):
|
|||
self.join_table = join_table
|
||||
self.source_col_name = source_col_name
|
||||
self.target_col_name = target_col_name
|
||||
self.through = through
|
||||
self._pk_val = self.instance._get_pk_val()
|
||||
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__)
|
||||
|
@ -361,6 +365,9 @@ def create_many_related_manager(superclass):
|
|||
def get_query_set(self):
|
||||
return superclass.get_query_set(self).filter(**(self.core_filters))
|
||||
|
||||
# If the ManyToMany relation has an intermediary model,
|
||||
# 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)
|
||||
|
||||
|
@ -386,6 +393,10 @@ def create_many_related_manager(superclass):
|
|||
clear.alters_data = True
|
||||
|
||||
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.save()
|
||||
self.add(new_obj)
|
||||
|
@ -473,7 +484,7 @@ class ManyRelatedObjectsDescriptor(object):
|
|||
# model's default manager.
|
||||
rel_model = self.related.model
|
||||
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
|
||||
manager = RelatedManager(
|
||||
|
@ -492,6 +503,10 @@ class ManyRelatedObjectsDescriptor(object):
|
|||
if instance is None:
|
||||
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.clear()
|
||||
manager.add(*value)
|
||||
|
@ -514,7 +529,7 @@ class ReverseManyRelatedObjectsDescriptor(object):
|
|||
# model's default manager.
|
||||
rel_model=self.field.rel.to
|
||||
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
|
||||
manager = RelatedManager(
|
||||
|
@ -533,6 +548,10 @@ class ReverseManyRelatedObjectsDescriptor(object):
|
|||
if instance is None:
|
||||
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.clear()
|
||||
manager.add(*value)
|
||||
|
@ -584,7 +603,7 @@ class OneToOneRel(ManyToOneRel):
|
|||
|
||||
class ManyToManyRel(object):
|
||||
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.num_in_admin = num_in_admin
|
||||
self.related_name = related_name
|
||||
|
@ -594,6 +613,7 @@ class ManyToManyRel(object):
|
|||
self.edit_inline = False
|
||||
self.symmetrical = symmetrical
|
||||
self.multiple = True
|
||||
self.through = through
|
||||
|
||||
class ForeignKey(RelatedField, Field):
|
||||
empty_strings_allowed = False
|
||||
|
@ -723,8 +743,16 @@ class ManyToManyField(RelatedField, Field):
|
|||
num_in_admin=kwargs.pop('num_in_admin', 0),
|
||||
related_name=kwargs.pop('related_name', 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)
|
||||
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)
|
||||
|
||||
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):
|
||||
"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
|
||||
else:
|
||||
return '%s_%s' % (opts.db_table, self.name)
|
||||
|
||||
def _get_m2m_column_name(self, related):
|
||||
"Function that can be curried to provide the source column name for the m2m table"
|
||||
try:
|
||||
return self._m2m_column_name_cache
|
||||
except:
|
||||
if self.rel.through is not None:
|
||||
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
|
||||
if related.model == related.parent_model:
|
||||
return 'from_' + related.model._meta.object_name.lower() + '_id'
|
||||
elif related.model == related.parent_model:
|
||||
self._m2m_column_name_cache = 'from_' + related.model._meta.object_name.lower() + '_id'
|
||||
else:
|
||||
return related.model._meta.object_name.lower() + '_id'
|
||||
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):
|
||||
"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:
|
||||
return self._m2m_reverse_name_cache
|
||||
except:
|
||||
if self.rel.through is not None:
|
||||
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:
|
||||
return 'to_' + related.parent_model._meta.object_name.lower() + '_id'
|
||||
# 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:
|
||||
return related.parent_model._meta.object_name.lower() + '_id'
|
||||
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):
|
||||
"Validates that the value is a valid list of foreign keys"
|
||||
|
@ -799,6 +863,16 @@ class ManyToManyField(RelatedField, Field):
|
|||
# Set up the accessor for the m2m table name for the relation
|
||||
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):
|
||||
target = self.rel.to
|
||||
else:
|
||||
|
|
|
@ -617,6 +617,61 @@ automatically::
|
|||
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
|
||||
=====================
|
||||
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -111,6 +111,63 @@ class MissingRelations(models.Model):
|
|||
rel1 = models.ForeignKey("Rel1")
|
||||
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.
|
||||
invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
|
||||
invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" 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.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.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.
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
|
|
@ -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>]
|
||||
"""}
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
|
|
@ -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>]
|
||||
|
||||
"""}
|
Loading…
Reference in New Issue