Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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

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

git-svn-id: http://code.djangoproject.com/svn/django/trunk@8136 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 174641b9b35022b80ea1d1a917a9d95b37a551d7 1 parent f752f69
@freakboy3742 freakboy3742 authored
View
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>
View
5 django/contrib/admin/options.py
@@ -161,7 +161,10 @@ def formfield_for_dbfield(self, db_field, **kwargs):
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)):
View
3  django/contrib/contenttypes/generic.py
@@ -104,6 +104,9 @@ def __init__(self, to, **kwargs):
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")
View
2  django/core/management/sql.py
@@ -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)
View
45 django/core/management/validation.py
@@ -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,7 +113,49 @@ 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()
rel_query_name = f.related_query_name()
View
150 django/db/models/fields/related.py
@@ -23,7 +23,7 @@
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 @@ class MyModel(Model):
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 @@ class MyModel(Model):
# 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 @@ def contribute_to_class(self, cls, name):
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 @@ def __set__(self, instance, value):
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 __init__(self, model=None, core_filters=None, instance=None, symmetrical=Non
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,21 +365,24 @@ def __init__(self, model=None, core_filters=None, instance=None, symmetrical=Non
def get_query_set(self):
return superclass.get_query_set(self).filter(**(self.core_filters))
- def add(self, *objs):
- self._add_items(self.source_col_name, self.target_col_name, *objs)
+ # 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)
- # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
- if self.symmetrical:
- self._add_items(self.target_col_name, self.source_col_name, *objs)
- add.alters_data = True
+ # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
+ if self.symmetrical:
+ self._add_items(self.target_col_name, self.source_col_name, *objs)
+ add.alters_data = True
- def remove(self, *objs):
- self._remove_items(self.source_col_name, self.target_col_name, *objs)
+ def remove(self, *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 self.symmetrical:
- self._remove_items(self.target_col_name, self.source_col_name, *objs)
- remove.alters_data = True
+ # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
+ if self.symmetrical:
+ self._remove_items(self.target_col_name, self.source_col_name, *objs)
+ remove.alters_data = True
def clear(self):
self._clear_items(self.source_col_name)
@@ -386,6 +393,10 @@ def clear(self):
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 @@ def __get__(self, instance, instance_type=None):
# 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 @@ def __set__(self, instance, value):
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 @@ def __get__(self, instance, instance_type=None):
# 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 @@ def __set__(self, instance, value):
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 @@ def __init__(self, to, field_name, num_in_admin=0, min_num_in_admin=None,
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 @@ def __init__(self, to, num_in_admin=0, related_name=None,
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 @@ def __init__(self, to, **kwargs):
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 @@ def get_choices_default(self):
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"
- # 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'
- else:
- return related.model._meta.object_name.lower() + '_id'
+ 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
+ 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):
"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
- if related.model == related.parent_model:
- return 'to_' + related.parent_model._meta.object_name.lower() + '_id'
- else:
- return related.parent_model._meta.object_name.lower() + '_id'
+ 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:
+ # 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):
"Validates that the value is a valid list of foreign keys"
@@ -792,13 +856,23 @@ def flatten_data(self, follow, obj = None):
return new_data
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
setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self))
# 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:
View
55 docs/admin.txt
@@ -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
=====================
View
129 docs/model-api.txt
@@ -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
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
``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
~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1145,7 +1270,7 @@ any parent classes in ``unique_together``.
For convenience, unique_together can be a single list when dealing
with a single set of fields::
- unique_together = ("driver", "restaurant")
+ unique_together = ("driver", "restaurant")
``verbose_name``
----------------
View
65 tests/modeltests/invalid_models/models.py
@@ -110,6 +110,63 @@ class Car(models.Model):
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.
@@ -195,4 +252,12 @@ class MissingRelations(models.Model):
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.
"""
View
2  tests/modeltests/m2m_through/__init__.py
@@ -0,0 +1,2 @@
+
+
View
337 tests/modeltests/m2m_through/models.py
@@ -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
2  tests/regressiontests/m2m_through_regress/__init__.py
@@ -0,0 +1,2 @@
+
+
View
204 tests/regressiontests/m2m_through_regress/models.py
@@ -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>]
+
+"""}

0 comments on commit 174641b

Please sign in to comment.
Something went wrong with that request. Please try again.