Django

Code

Ticket #6095: 6095-r8090.2.diff

File 6095-r8090.2.diff, 46.3 kB (added by floguy, 4 months ago)

Added a few more validation checks (along with corresponding invalid_model tests), fixed a typo in documentation, added a section in admin documentation on how to integrate m2m intermediary models inline with the admin.

  • django/db/models/fields/related.py

    old new  
    2323 
    2424pending_lookups = {} 
    2525 
    26 def add_lazy_relation(cls, field, relation): 
     26def add_lazy_relation(cls, field, relation, operation): 
    2727    """ 
    2828    Adds a lookup on ``cls`` when a related field is defined using a string, 
    2929    i.e.:: 
     
    4545    If the other model hasn't yet been loaded -- almost a given if you're using 
    4646    lazy relationships -- then the relation won't be set up until the 
    4747    class_prepared signal fires at the end of model initialization. 
     48     
     49    operation is the work that must be performed once the relation can be resolved. 
    4850    """ 
    4951    # Check for recursive relations 
    5052    if relation == RECURSIVE_RELATIONSHIP_CONSTANT: 
     
    6668    # is prepared. 
    6769    model = get_model(app_label, model_name, False) 
    6870    if model: 
    69         field.rel.to = model 
    70         field.do_related_class(model, cls) 
     71        operation(field, model, cls) 
    7172    else: 
    7273        key = (app_label, model_name) 
    73         value = (cls, field
     74        value = (cls, field, operation
    7475        pending_lookups.setdefault(key, []).append(value) 
    7576 
    7677def do_pending_lookups(sender): 
     
    7879    Handle any pending relations to the sending model. Sent from class_prepared. 
    7980    """ 
    8081    key = (sender._meta.app_label, sender.__name__) 
    81     for cls, field in pending_lookups.pop(key, []): 
    82         field.rel.to = sender 
    83         field.do_related_class(sender, cls) 
     82    for cls, field, operation in pending_lookups.pop(key, []): 
     83        operation(field, sender, cls) 
    8484 
    8585dispatcher.connect(do_pending_lookups, signal=signals.class_prepared) 
    8686 
     
    108108 
    109109        other = self.rel.to 
    110110        if isinstance(other, basestring): 
    111             add_lazy_relation(cls, self, other) 
     111            def resolve_related_class(field, model, cls): 
     112                field.rel.to = model 
     113                field.do_related_class(model, cls) 
     114            add_lazy_relation(cls, self, other, resolve_related_class) 
    112115        else: 
    113116            self.do_related_class(other, cls) 
    114117 
     
    339342            manager.clear() 
    340343        manager.add(*value) 
    341344 
    342 def create_many_related_manager(superclass): 
     345def create_many_related_manager(superclass, through=False): 
    343346    """Creates a manager that subclasses 'superclass' (which is a Manager) 
    344347    and adds behavior for many-to-many related objects.""" 
    345348    class ManyRelatedManager(superclass): 
     
    353356            self.join_table = join_table 
    354357            self.source_col_name = source_col_name 
    355358            self.target_col_name = target_col_name 
     359            self.through = through 
    356360            self._pk_val = self.instance._get_pk_val() 
    357361            if self._pk_val is None: 
    358362                raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__) 
     
    360364        def get_query_set(self): 
    361365            return superclass.get_query_set(self).filter(**(self.core_filters)) 
    362366 
    363         def add(self, *objs): 
    364             self._add_items(self.source_col_name, self.target_col_name, *objs) 
     367        # If the ManyToMany relation has an intermediary model,  
     368        # the add and remove methods do not exist. 
     369        if through is None: 
     370            def add(self, *objs): 
     371                self._add_items(self.source_col_name, self.target_col_name, *objs) 
    365372 
    366             # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table 
    367             if self.symmetrical: 
    368                 self._add_items(self.target_col_name, self.source_col_name, *objs) 
    369         add.alters_data = True 
     373                # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table 
     374                if self.symmetrical: 
     375                    self._add_items(self.target_col_name, self.source_col_name, *objs) 
     376            add.alters_data = True 
    370377 
    371         def remove(self, *objs): 
    372             self._remove_items(self.source_col_name, self.target_col_name, *objs) 
     378            def remove(self, *objs): 
     379                self._remove_items(self.source_col_name, self.target_col_name, *objs) 
    373380 
    374             # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table 
    375             if self.symmetrical: 
    376                 self._remove_items(self.target_col_name, self.source_col_name, *objs) 
    377         remove.alters_data = True 
     381                # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table 
     382                if self.symmetrical: 
     383                    self._remove_items(self.target_col_name, self.source_col_name, *objs) 
     384            remove.alters_data = True 
    378385 
    379386        def clear(self): 
    380387            self._clear_items(self.source_col_name) 
     
    385392        clear.alters_data = True 
    386393 
    387394        def create(self, **kwargs): 
     395            # This check needs to be done here, since we can't later remove this 
     396            # from the method lookup table, as we do with add and remove. 
     397            if through is not None: 
     398                raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through 
    388399            new_obj = self.model(**kwargs) 
    389400            new_obj.save() 
    390401            self.add(new_obj) 
     
    472483        # model's default manager. 
    473484        rel_model = self.related.model 
    474485        superclass = rel_model._default_manager.__class__ 
    475         RelatedManager = create_many_related_manager(superclass
     486        RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through
    476487 
    477488        qn = connection.ops.quote_name 
    478489        manager = RelatedManager( 
     
    491502        if instance is None: 
    492503            raise AttributeError, "Manager must be accessed via instance" 
    493504 
     505        through = getattr(self.related.field.rel, 'through', None) 
     506        if through is not None: 
     507            raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through 
     508 
    494509        manager = self.__get__(instance) 
    495510        manager.clear() 
    496511        manager.add(*value) 
     
    513528        # model's default manager. 
    514529        rel_model=self.field.rel.to 
    515530        superclass = rel_model._default_manager.__class__ 
    516         RelatedManager = create_many_related_manager(superclass
     531        RelatedManager = create_many_related_manager(superclass, self.field.rel.through
    517532 
    518533        qn = connection.ops.quote_name 
    519534        manager = RelatedManager( 
     
    532547        if instance is None: 
    533548            raise AttributeError, "Manager must be accessed via instance" 
    534549 
     550        through = getattr(self.field.rel, 'through', None) 
     551        if through is not None: 
     552            raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model.  Use %s's Manager instead." % through 
     553 
    535554        manager = self.__get__(instance) 
    536555        manager.clear() 
    537556        manager.add(*value) 
     
    583602 
    584603class ManyToManyRel(object): 
    585604    def __init__(self, to, num_in_admin=0, related_name=None, 
    586         limit_choices_to=None, symmetrical=True): 
     605        limit_choices_to=None, symmetrical=True, through=None): 
    587606        self.to = to 
    588607        self.num_in_admin = num_in_admin 
    589608        self.related_name = related_name 
     
    593612        self.edit_inline = False 
    594613        self.symmetrical = symmetrical 
    595614        self.multiple = True 
     615        self.through = through 
    596616 
     617 
    597618class ForeignKey(RelatedField, Field): 
    598619    empty_strings_allowed = False 
    599620    def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): 
     
    718739class ManyToManyField(RelatedField, Field): 
    719740    def __init__(self, to, **kwargs): 
    720741        kwargs['verbose_name'] = kwargs.get('verbose_name', None) 
     742        self.symmetrical_specified = kwargs.get('symmetrical', None) is not None 
    721743        kwargs['rel'] = ManyToManyRel(to, 
    722744            num_in_admin=kwargs.pop('num_in_admin', 0), 
    723745            related_name=kwargs.pop('related_name', None), 
    724746            limit_choices_to=kwargs.pop('limit_choices_to', None), 
    725             symmetrical=kwargs.pop('symmetrical', True)) 
     747            symmetrical=kwargs.pop('symmetrical', True), 
     748            through=kwargs.pop('through', None)) 
     749             
    726750        self.db_table = kwargs.pop('db_table', None) 
     751        if kwargs['rel'].through is not None: 
     752            self.creates_table = False 
     753            assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used." 
     754        else: 
     755            self.creates_table = True 
     756 
    727757        Field.__init__(self, **kwargs) 
    728758 
    729759        msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.') 
     
    738768 
    739769    def _get_m2m_db_table(self, opts): 
    740770        "Function that can be curried to provide the m2m table name for this relation" 
    741         if self.db_table: 
     771        if self.rel.through is not None: 
     772            return self.rel.through_model._meta.db_table 
     773        elif self.db_table: 
    742774            return self.db_table 
    743775        else: 
    744776            return '%s_%s' % (opts.db_table, self.name) 
    745777 
    746778    def _get_m2m_column_name(self, related): 
    747779        "Function that can be curried to provide the source column name for the m2m table" 
     780        if self.rel.through is not None: 
     781            for f in self.rel.through_model._meta.fields: 
     782                if hasattr(f,'rel') and f.rel and f.rel.to == related.model: 
     783                    return f.column 
    748784        # If this is an m2m relation to self, avoid the inevitable name clash 
    749         if related.model == related.parent_model: 
     785        elif related.model == related.parent_model: 
    750786            return 'from_' + related.model._meta.object_name.lower() + '_id' 
    751787        else: 
    752788            return related.model._meta.object_name.lower() + '_id' 
    753789 
    754790    def _get_m2m_reverse_name(self, related): 
    755791        "Function that can be curried to provide the related column name for the m2m table" 
     792        if self.rel.through is not None: 
     793            found = False 
     794            for f in self.rel.through_model._meta.fields: 
     795                if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model: 
     796                    if related.model == related.parent_model: 
     797                        # If this is an m2m-intermediate to self,  
     798                        # the first foreign key you find will be  
     799                        # the source column. Keep searching for 
     800                        # the second foreign key. 
     801                        if found: 
     802                            return f.column 
     803                        else: 
     804                            found = True 
     805                    else: 
     806                        return f.column 
    756807        # If this is an m2m relation to self, avoid the inevitable name clash 
    757         if related.model == related.parent_model: 
     808        elif related.model == related.parent_model: 
    758809            return 'to_' + related.parent_model._meta.object_name.lower() + '_id' 
    759810        else: 
    760811            return related.parent_model._meta.object_name.lower() + '_id' 
     
    797848 
    798849        # Set up the accessor for the m2m table name for the relation 
    799850        self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta) 
    800  
     851         
     852        # Populate some necessary rel arguments so that cross-app relations 
     853        # work correctly. 
     854        if isinstance(self.rel.through, basestring): 
     855            def resolve_through_model(field, model, cls): 
     856                field.rel.through_model = model 
     857            add_lazy_relation(cls, self, self.rel.through, resolve_through_model) 
     858        elif self.rel.through: 
     859            self.rel.through_model = self.rel.through 
     860            self.rel.through = self.rel.through._meta.object_name 
     861             
    801862        if isinstance(self.rel.to, basestring): 
    802863            target = self.rel.to 
    803864        else: 
  • django/core/management/validation.py

    old new  
    102102                        if r.get_accessor_name() == rel_query_name: 
    103103                            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)) 
    104104 
     105        seen_intermediary_signatures = []  
    105106        for i, f in enumerate(opts.local_many_to_many): 
    106107            # Check to see if the related m2m field will clash with any 
    107108            # existing fields, m2m fields, m2m related objects or related 
     
    112113                # so skip the next section 
    113114                if isinstance(f.rel.to, (str, unicode)): 
    114115                    continue 
    115  
     116            if getattr(f.rel, 'through', None) is not None: 
     117                if hasattr(f.rel, 'through_model'): 
     118                    if f.symmetrical_specified: 
     119                        e.add(opts, "The symmetrical field has no meaning on m2m relations with intermediary models.") 
     120                    from_model, to_model = cls, f.rel.to 
     121                    seen_from, seen_to, seen_self = False, False, 0 
     122                    for inter_field in f.rel.through_model._meta.fields: 
     123                        rel_to = getattr(inter_field.rel, 'to', None) 
     124                        if from_model == to_model: 
     125                            if rel_to == from_model: 
     126                                seen_self += 1 
     127                            if seen_self > 2: 
     128                                e.add(opts, "Intermediary model %s has more than two foreign keys through itself, which is ambiguous and is not permitted." % f.rel.through_model._meta.object_name) 
     129                        else: 
     130                            if rel_to == from_model: 
     131                                if seen_from: 
     132                                    e.add(opts, "Intermediary model %s has more than one foreign key to one of its sides, %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, rel_from._meta.object_name)) 
     133                                else: 
     134                                    seen_from = True 
     135                            elif rel_to == to_model: 
     136                                if seen_to: 
     137                                    e.add(opts, "Intermediary model %s has more than one foreign key to one of its sides, %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, rel_to._meta.object_name)) 
     138                                else: 
     139                                    seen_to = True 
     140                    if f.rel.through_model not in models.get_models(): 
     141                        e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed" % (f.name, f.rel.through)) 
     142                    signature = (f.rel.to, cls, f.rel.through_model) 
     143                    if signature in seen_intermediary_signatures: 
     144                        e.add(opts, "The model %s has two manually-defined m2m relations through the model %s, which is not permitted.  Please consider using an extra field on your intermediary model instead." % (cls._meta.object_name, f.rel.through_model._meta.object_name)) 
     145                    else: 
     146                        seen_intermediary_signatures.append(signature) 
     147                    seen_related_fk, seen_this_fk = False, False 
     148                    for field in f.rel.through_model._meta.fields: 
     149                        if field.rel: 
     150                            if not seen_related_fk and field.rel.to == f.rel.to: 
     151                                seen_related_fk = True 
     152                            elif field.rel.to == cls: 
     153                                seen_this_fk = True 
     154                    if not seen_related_fk or not seen_this_fk: 
     155                        e.add(opts, "'%s' has a manually-defined m2m relation through model %s, which does not have foreign keys to %s and %s" % (f.name, f.rel.through, f.rel.to._meta.object_name, cls._meta.object_name)) 
     156                else: 
     157                    e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed" % (f.name, f.rel.through)) 
     158             
    116159            rel_opts = f.rel.to._meta 
    117160            rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name() 
    118161            rel_query_name = f.related_query_name() 
  • django/core/management/sql.py

    old new  
    353353    qn = connection.ops.quote_name 
    354354    inline_references = connection.features.inline_fk_references 
    355355    for f in opts.local_many_to_many: 
    356         if not isinstance(f.rel, generic.GenericRel)
     356        if f.creates_table
    357357            tablespace = f.db_tablespace or opts.db_tablespace 
    358358            if tablespace and connection.features.supports_tablespaces:  
    359359                tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True) 
  • django/contrib/admin/options.py

    old new  
    161161                kwargs['empty_label'] = db_field.blank and _('None') or None 
    162162            else: 
    163163                if isinstance(db_field, models.ManyToManyField): 
    164                     if db_field.name in self.raw_id_fields: 
     164                    # If it uses an intermediary model, don't show field in admin.  
     165                    if db_field.rel.through is not None: 
     166                        return None 
     167                    elif db_field.name in self.raw_id_fields: 
    165168                        kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel) 
    166169                        kwargs['help_text'] = '' 
    167170                    elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)): 
  • django/contrib/contenttypes/generic.py

    old new  
    104104                            limit_choices_to=kwargs.pop('limit_choices_to', None), 
    105105                            symmetrical=kwargs.pop('symmetrical', True)) 
    106106 
     107        # By its very nature, a GenericRelation doesn't create a table. 
     108        self.creates_table = False 
     109 
    107110        # Override content-type/object-id field names on the related class 
    108111        self.object_id_field_name = kwargs.pop("object_id_field", "object_id") 
    109112        self.content_type_field_name = kwargs.pop("content_type_field", "content_type") 
  • tests/modeltests/invalid_models/models.py

    old new  
    110110class MissingRelations(models.Model): 
    111111    rel1 = models.ForeignKey("Rel1") 
    112112    rel2 = models.ManyToManyField("Rel2") 
     113     
     114class MissingManualM2MModel(models.Model): 
     115    name = models.CharField(max_length=5) 
     116    missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel") 
     117     
     118class Person(models.Model): 
     119    name = models.CharField(max_length=5) 
    113120 
     121class Group(models.Model): 
     122    name = models.CharField(max_length=5) 
     123    primary = models.ManyToManyField(Person, through="Membership", related_name="primary") 
     124    secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary") 
     125    tertiary = models.ManyToManyField(Person, through="RelationshipDoubleFK", related_name="tertiary") 
     126 
     127class GroupTwo(models.Model): 
     128    name = models.CharField(max_length=5) 
     129    primary = models.ManyToManyField(Person, through="Membership") 
     130    secondary = models.ManyToManyField(Group, through="MembershipMissingFK") 
     131 
     132class Membership(models.Model): 
     133    person = models.ForeignKey(Person) 
     134    group = models.ForeignKey(Group) 
     135    not_default_or_null = models.CharField(max_length=5) 
     136 
     137class MembershipMissingFK(models.Model): 
     138    person = models.ForeignKey(Person) 
     139 
     140class PersonSelfRefM2M(models.Model): 
     141    name = models.CharField(max_length=5) 
     142    friends = models.ManyToManyField('self', through="Relationship", symmetrical=True) 
     143    too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK") 
     144 
     145class Relationship(models.Model): 
     146    first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set") 
     147    second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set") 
     148    date_added = models.DateTimeField() 
     149 
     150class RelationshipTripleFK(models.Model): 
     151    first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2") 
     152    second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2") 
     153    third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far") 
     154    date_added = models.DateTimeField() 
     155 
     156class RelationshipDoubleFK(models.Model): 
     157    first = models.ForeignKey(Person, related_name="first_related_name") 
     158    second = models.ForeignKey(Person, related_name="second_related_name") 
     159    third = models.ForeignKey(Group, related_name="rel_to_set") 
     160    date_added = models.DateTimeField() 
     161 
    114162model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute. 
    115163invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute. 
    116164invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute. 
     
    195243invalid_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'. 
    196244invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed 
    197245invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed 
     246invalid_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. 
     247invalid_models.grouptwo: 'primary' has a manually-defined m2m relation through model Membership, which does not have foreign keys to Person and GroupTwo 
     248invalid_models.grouptwo: 'secondary' has a manually-defined m2m relation through model MembershipMissingFK, which does not have foreign keys to Group and GroupTwo 
     249invalid_models.missingmanualm2mmodel: 'missing_m2m' specifies an m2m relation through model MissingM2MModel, which has not been installed 
     250invalid_models.group: Intermediary model RelationshipDoubleFK has more than one foreign key to one of its sides, Person, which is ambiguous and is not permitted. 
     251invalid_models.personselfrefm2m: The symmetrical field has no meaning on m2m relations with intermediary models. 
     252invalid_models.personselfrefm2m: Intermediary model RelationshipTripleFK has more than two foreign keys through itself, which is ambiguous and is not permitted. 
    198253""" 
  • tests/modeltests/m2m_through/__init__.py

    old new  
  • tests/modeltests/m2m_through/models.py

    old new  
     1from django.db import models 
     2from datetime import datetime 
     3 
     4# M2M described on one of the models 
     5class Person(models.Model): 
     6    name = models.CharField(max_length=128) 
     7 
     8    def __unicode__(self): 
     9        return self.name 
     10 
     11class Group(models.Model): 
     12    name = models.CharField(max_length=128) 
     13    members = models.ManyToManyField(Person, through='Membership') 
     14    custom_members = models.ManyToManyField(Person, through='CustomMembership', related_name="custom") 
     15    nodefaultsnonulls = models.ManyToManyField(Person, through='TestNoDefaultsOrNulls', related_name="testnodefaultsnonulls") 
     16     
     17    def __unicode__(self): 
     18        return self.name 
     19 
     20class Membership(models.Model): 
     21    person = models.ForeignKey(Person) 
     22    group = models.ForeignKey(Group) 
     23    date_joined = models.DateTimeField(default=datetime.now) 
     24    invite_reason = models.CharField(max_length=64, null=True) 
     25     
     26    def __unicode__(self): 
     27        return "%s is a member of %s" % (self.person.name, self.group.name) 
     28 
     29class CustomMembership(models.Model): 
     30    person = models.ForeignKey(Person, db_column="custom_person_column", related_name="custom_person_related_name") 
     31    group = models.ForeignKey(Group) 
     32    weird_fk = models.ForeignKey(Membership, null=True) 
     33    date_joined = models.DateTimeField(default=datetime.now) 
     34     
     35    def __unicode__(self): 
     36        return "%s is a member of %s" % (self.person.name, self.group.name) 
     37     
     38    class Meta: 
     39        db_table = "test_table" 
     40 
     41class TestNoDefaultsOrNulls(models.Model): 
     42    person = models.ForeignKey(Person) 
     43    group = models.ForeignKey(Group) 
     44    nodefaultnonull = models.CharField(max_length=5) 
     45 
     46class PersonSelfRefM2M(models.Model): 
     47    name = models.CharField(max_length=5) 
     48    friends = models.ManyToManyField('self', through="Friendship") 
     49     
     50    def __unicode__(self): 
     51        return self.name 
     52 
     53class Friendship(models.Model): 
     54    first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set") 
     55    second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set") 
     56    date_friended = models.DateTimeField() 
     57 
     58__test__ = {'API_TESTS':""" 
     59>>> from datetime import datetime 
     60 
     61### Creation and Saving Tests ### 
     62 
     63>>> bob = Person.objects.create(name='Bob') 
     64>>> jim = Person.objects.create(name='Jim') 
     65>>> jane = Person.objects.create(name='Jane') 
     66>>> rock = Group.objects.create(name='Rock') 
     67>>> roll = Group.objects.create(name='Roll') 
     68 
     69# We start out by making sure that the Group 'rock' has no members. 
     70>>> rock.members.all() 
     71[] 
     72 
     73# To make Jim a member of Group Rock, simply create a Membership object. 
     74>>> m1 = Membership.objects.create(person=jim, group=rock) 
     75 
     76# We can do the same for Jane and Rock. 
     77>>> m2 = Membership.objects.create(person=jane, group=rock) 
     78 
     79# Let's check to make sure that it worked.  Jane and Jim should be members of Rock. 
     80>>> rock.members.all() 
     81[<Person: Jim>, <Person: Jane>] 
     82 
     83# Now we can add a bunch more Membership objects to test with. 
     84>>> m3 = Membership.objects.create(person=bob, group=roll) 
     85>>> m4 = Membership.objects.create(person=jim, group=roll) 
     86>>> m5 = Membership.objects.create(person=jane, group=roll) 
     87 
     88# We can get Jim's Group membership as with any ForeignKey. 
     89>>> jim.group_set.all() 
     90[<Group: Rock>, <Group: Roll>] 
     91 
     92# Querying the intermediary model works like normal.   
     93# In this case we get Jane's membership to Rock. 
     94>>> m = Membership.objects.get(person=jane, group=rock) 
     95>>> m 
     96<Membership: Jane is a member of Rock> 
     97 
     98# Now we set some date_joined dates for further testing. 
     99>>> m2.invite_reason = "She was just awesome." 
     100>>> m2.date_joined = datetime(2006, 1, 1) 
     101>>> m2.save() 
     102 
     103>>> m5.date_joined = datetime(2004, 1, 1) 
     104>>> m5.save() 
     105 
     106>>> m3.date_joined = datetime(2004, 1, 1) 
     107>>> m3.save() 
     108 
     109# It's not only get that works. Filter works like normal as well. 
     110>>> Membership.objects.filter(person=jim) 
     111[<Membership: Jim is a member of Rock>, <Membership: Jim is a member of Roll>] 
     112 
     113 
     114### Forward Descriptors Tests ### 
     115 
     116# Due to complications with adding via an intermediary model,  
     117# the add method is not provided. 
     118>>> rock.members.add(bob) 
     119Traceback (most recent call last): 
     120... 
     121AttributeError: 'ManyRelatedManager' object has no attribute 'add' 
     122 
     123# Create is also disabled as it suffers from the same problems as add. 
     124>>> rock.members.create(name='Anne') 
     125Traceback (most recent call last): 
     126... 
     127AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. 
     128 
     129# Remove has similar complications, and is not provided either. 
     130>>> rock.members.remove(jim) 
     131Traceback (most recent call last): 
     132... 
     133AttributeError: 'ManyRelatedManager' object has no attribute 'remove' 
     134 
     135# Here we back up the list of all members of Rock. 
     136>>> backup = list(rock.members.all()) 
     137 
     138# ...and we verify that it has worked. 
     139>>> backup 
     140[<Person: Jim>, <Person: Jane>] 
     141 
     142# The clear function should still work. 
     143>>> rock.members.clear() 
     144 
     145# Now there will be no members of Rock. 
     146>>> rock.members.all() 
     147[] 
     148 
     149# Assignment should not work with models specifying a through model for many of 
     150# the same reasons as adding. 
     151>>> rock.members = backup 
     152Traceback (most recent call last): 
     153... 
     154AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead. 
     155 
     156# Let's re-save those instances that we've cleared. 
     157>>> m1.save() 
     158>>> m2.save() 
     159 
     160# Verifying that those instances were re-saved successfully. 
     161>>> rock.members.all() 
     162[<Person: Jim>, <Person: Jane>] 
     163 
     164 
     165### Reverse Descriptors Tests ### 
     166 
     167# Due to complications with adding via an intermediary model,  
     168# the add method is not provided. 
     169>>> bob.group_set.add(rock) 
     170Traceback (most recent call last): 
     171... 
     172AttributeError: 'ManyRelatedManager' object has no attribute 'add' 
     173 
     174# Create is also disabled as it suffers from the same problems as add. 
     175>>> bob.group_set.create(name='Funk') 
     176Traceback (most recent call last): 
     177... 
     178AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. 
     179 
     180# Remove has similar complications, and is not provided either. 
     181>>> jim.group_set.remove(rock) 
     182Traceback (most recent call last): 
     183... 
     184AttributeError: 'ManyRelatedManager' object has no attribute 'remove' 
     185 
     186# Here we back up the list of all of Jim's groups. 
     187>>> backup = list(jim.group_set.all()) 
     188>>> backup 
     189[<Group: Rock>, <Group: Roll>] 
     190 
     191# The clear function should still work. 
     192>>> jim.group_set.clear() 
     193 
     194# Now Jim will be in no groups. 
     195>>> jim.group_set.all() 
     196[] 
     197 
     198# Assignment should not work with models specifying a through model for many of 
     199# the same reasons as adding. 
     200>>> jim.group_set = backup 
     201Traceback (most recent call last): 
     202... 
     203AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead. 
     204 
     205# Let's re-save those instances that we've cleared. 
     206>>> m1.save() 
     207>>> m4.save() 
     208 
     209# Verifying that those instances were re-saved successfully. 
     210>>> jim.group_set.all() 
     211[<Group: Rock>, <Group: Roll>] 
     212 
     213### Custom Tests ### 
     214 
     215# Let's see if we can query through our second relationship. 
     216>>> rock.custom_members.all() 
     217[] 
     218 
     219# We can query in the opposite direction as well. 
     220>>> bob.custom.all() 
     221[] 
     222 
     223# Let's create some membership objects in this custom relationship. 
     224>>> cm1 = CustomMembership.objects.create(person=bob, group=rock) 
     225>>> cm2 = CustomMembership.objects.create(person=jim, group=rock) 
     226 
     227# If we get the number of people in Rock, it should be both Bob and Jim. 
     228>>> rock.custom_members.all() 
     229[<Person: Bob>, <Person: Jim>] 
     230 
     231# Bob should only be in one custom group. 
     232>>> bob.custom.all() 
     233[<Group: Rock>] 
     234 
     235# Let's make sure our new descriptors don't conflict with the FK related_name. 
     236>>> bob.custom_person_related_name.all() 
     237[<CustomMembership: Bob is a member of Rock>] 
     238 
     239### SELF-REFERENTIAL TESTS ### 
     240 
     241# Let's first create a person who has no friends. 
     242>>> tony = PersonSelfRefM2M.objects.create(name="Tony") 
     243>>> tony.friends.all() 
     244[] 
     245 
     246# Now let's create another person for Tony to be friends with. 
     247>>> chris = PersonSelfRefM2M.objects.create(name="Chris") 
     248>>> f = Friendship.objects.create(first=tony, second=chris, date_friended=datetime.now()) 
     249 
     250# Tony should now show that Chris is his friend. 
     251>>> tony.friends.all() 
     252[<PersonSelfRefM2M: Chris>] 
     253 
     254# But we haven't established that Chris is Tony's Friend. 
     255>>> chris.friends.all() 
     256[] 
     257 
     258# So let's do that now. 
     259>>> f2 = Friendship.objects.create(first=chris, second=tony, date_friended=datetime.now()) 
     260 
     261# Having added Chris as a friend, let's make sure that his friend set reflects 
     262# that addition. 
     263>>> chris.friends.all() 
     264[<PersonSelfRefM2M: Tony>] 
     265 
     266# Chris gets mad and wants to get rid of all of his friends. 
     267>>> chris.friends.clear() 
     268 
     269# Now he should not have any more friends. 
     270>>> chris.friends.all() 
     271[] 
     272 
     273# Since this is a symmetrical relation, Tony's friend link is deleted as well. 
     274>>> tony.friends.all() 
     275[] 
     276 
     277 
     278 
     279### QUERY TESTS ### 
     280 
     281# We can query for the related model by using its attribute name (members, in  
     282# this case). 
     283>>> Group.objects.filter(members__name='Bob') 
     284[<Group: Roll>] 
     285 
     286# To query through the intermediary model, we specify its model name. 
     287# In this case, membership. 
     288>>> Group.objects.filter(membership__invite_reason="She was just awesome.") 
     289[<Group: Rock>] 
     290 
     291# If we want to query in the reverse direction by the related model, use its 
     292# model name (group, in this case). 
     293>>> Person.objects.filter(group__name="Rock") 
     294[<Person: Jim>, <Person: Jane>] 
     295 
     296# If the m2m field has specified a related_name, using that will work. 
     297>>> Person.objects.filter(custom__name="Rock") 
     298[<Person: Bob>, <Person: Jim>] 
     299 
     300# To query through the intermediary model in the reverse direction, we again 
     301# specify its model name (membership, in this case). 
     302>>> Person.objects.filter(membership__invite_reason="She was just awesome.") 
     303[<Person: Jane>] 
     304 
     305# Let's see all of the groups that Jane joined after 1 Jan 2005: 
     306>>> Group.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__person =jane) 
     307[<Group: Rock>] 
     308 
     309# Queries also work in the reverse direction: Now let's see all of the people  
     310# that have joined Rock since 1 Jan 2005: 
     311>>> Person.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__group=rock) 
     312[<Person: Jim>, <Person: Jane>] 
     313 
     314# Conceivably, queries through membership could return correct, but non-unique 
     315# querysets.  To demonstrate this, we query for all people who have joined a  
     316# group after 2004: 
     317>>> Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1)) 
     318[<Person: Jim>, <Person: Jim>, <Person: Jane>] 
     319 
     320# Jim showed up twice, because he joined two groups ('Rock', and 'Roll'): 
     321>>> [(m.person.name, m.group.name) for m in  
     322... Membership.objects.filter(date_joined__gt=datetime(2004, 1, 1))] 
     323[(u'Jim', u'Rock'), (u'Jane', u'Rock'), (u'Jim', u'Roll')] 
     324 
     325# QuerySet's distinct() method can correct this problem. 
     326>>> Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1)).distinct() 
     327[<Person: Jim>, <Person: Jane>] 
     328"""} 
  • tests/regressiontests/m2m_through_regress/__init__.py

    old new  
  • tests/regressiontests/m2m_through_regress/models.py

    old new  
     1from django.db import models 
     2from datetime import datetime 
     3from django.contrib.auth.models import User 
     4 
     5# Forward declared intermediate model 
     6class Membership(models.Model): 
     7    person = models.ForeignKey('Person') 
     8    group = models.ForeignKey('Group') 
     9    date_joined = models.DateTimeField(default=datetime.now) 
     10     
     11    def __unicode__(self): 
     12        return "%s is a member of %s" % (self.person.name, self.group.name) 
     13 
     14class UserMembership(models.Model): 
     15    user = models.ForeignKey(User) 
     16    group = models.ForeignKey('Group') 
     17    date_joined = models.DateTimeField(default=datetime.now) 
     18     
     19    def __unicode__(self): 
     20        return "%s is a user and member of %s" % (self.user.username, self.group.name) 
     21 
     22class Person(models.Model): 
     23    name = models.CharField(max_length=128) 
     24 
     25    def __unicode__(self): 
     26        return self.name 
     27 
     28class Group(models.Model): 
     29    name = models.CharField(max_length=128) 
     30    # Membership object defined as a class 
     31    members = models.ManyToManyField(Person, through=Membership) 
     32    user_members = models.ManyToManyField(User, through='UserMembership') 
     33     
     34    def __unicode__(self): 
     35        return self.name 
     36         
     37__test__ = {'API_TESTS':""" 
     38# Create some dummy data 
     39>>> bob = Person.objects.create(name='Bob') 
     40>>> jim = Person.objects.create(name='Jim') 
     41 
     42>>> rock = Group.objects.create(name='Rock') 
     43>>> roll = Group.objects.create(name='Roll') 
     44 
     45>>> frank = User.objects.create_user('frank','frank@example.com','password') 
     46>>> jane = User.objects.create_user('jane','jane@example.com','password') 
     47 
     48# Now test that the forward declared Membership works  
     49>>> Membership.objects.create(person=bob, group=rock) 
     50<Membership: Bob is a member of Rock> 
     51 
     52>>> Membership.objects.create(person=bob, group=roll) 
     53<Membership: Bob is a member of Roll> 
     54 
     55>>> Membership.objects.create(person=jim, group=rock) 
     56<Membership: Jim is a member of Rock> 
     57 
     58>>> bob.group_set.all() 
     59[<Group: Rock>, <Group: Roll>] 
     60 
     61>>> roll.members.all() 
     62[<Person: Bob>] 
     63 
     64# Error messages use the model name, not repr of the class name 
     65>>> bob.group_set = [] 
     66Traceback (most recent call last): 
     67... 
     68AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead. 
     69 
     70>>> roll.members = [] 
     71Traceback (most recent call last): 
     72... 
     73AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead. 
     74 
     75>>> rock.members.create(name='Anne') 
     76Traceback (most recent call last): 
     77... 
     78AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead. 
     79 
     80>>> bob.group_set.create(name='Funk') 
     81Traceback (most recent call last): 
     82... 
     83AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead. 
     84 
     85# Now test that the intermediate with a relationship outside  
     86# the current app (i.e., UserMembership) workds 
     87>>> UserMembership.objects.create(user=frank, group=rock) 
     88<UserMembership: frank is a user and member of Rock> 
     89 
     90>>> UserMembership.objects.create(user=frank, group=roll) 
     91<UserMembership: frank is a user and member of Roll> 
     92 
     93>>> UserMembership.objects.create(user=jane, group=rock) 
     94<UserMembership: jane is a user and member of Rock> 
     95 
     96>>> frank.group_set.all() 
     97[<Group: Rock>, <Group: Roll>] 
     98 
     99>>> roll.user_members.all() 
     100[<User: frank>] 
     101 
     102"""} 
  • AUTHORS

    old new  
    154154    Maciej Fijalkowski 
    155155    Matthew Flanagan <http://wadofstuff.blogspot.com> 
    156156    Eric Floehr <eric@intellovations.com> 
     157    Eric Florenzano <floguy@gmail.com> 
    157158    Vincent Foley <vfoleybourgon@yahoo.ca> 
    158159    Rudolph Froger <rfroger@estrate.nl> 
    159160    Jorge Gajon <gajon@gajon.org> 
  • docs/model-api.txt

    old new  
    945945 
    946946    =======================  ============================================================ 
    947947 
     948Extra fields on many-to-many relationships 
     949~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
     950 
     951When you're only dealing with mixing and matching pizzas and toppings, a 
     952standard ``ManyToManyField`` works great. However, sometimes you may want 
     953to associate data with the relationship between two models.  
     954 
     955For example, consider the case of an application tracking the musical groups 
     956which musicians belong to. There is a many-to-many relationship between a person 
     957and the groups of which they are a member, so you could use a ManyToManyField 
     958to represent this relationship. However, there is a lot of detail about the 
     959membership that you might want to collect, such as the date at which the person 
     960joined the group. 
     961 
     962For these situations, Django allows you to specify the model that will be used 
     963to govern the many-to-many relationship. You can then put extra fields on the 
     964intermediate model. The intermediate model is associated with the  
     965``ManyToManyField`` by using the ``through`` argument to point to the model 
     966that will act as an intermediary. For our musician example, the code would look 
     967something like this:: 
     968 
     969    class Person(models.Model): 
     970        # ... 
     971        name = models.CharField(max_length=128) 
     972 
     973        def __unicode__(self): 
     974            return self.name 
     975 
     976    class Group(models.Model): 
     977        # ... 
     978        name = models.CharField(max_length=128) 
     979        members = models.ManyToManyField(Person, through='Membership') 
     980 
     981        def __unicode__(self): 
     982            return self.name 
     983 
     984    class Membership(models.Model): 
     985        person = models.ForeignKey(Person) 
     986        group = models.ForeignKey(Group) 
     987        date_joined = models.DateField() 
     988        invite_reason = models.CharField(max_length=64) 
     989 
     990When you set up the intermediary model, you must explicitly specify foreign  
     991keys to the models in ManyToMany relation. This explicit declaration makes  
     992it clear how two models are related. 
     993 
     994Now that you have set up your ``ManyToManyField`` to use your intermediary  
     995model (Membership, in this case), you're ready to use the convenience methods 
     996provided by that ``ManyToManyField``.  Here's an example of how you can query 
     997for and use these models:: 
     998     
     999    >>> ringo = Person.objects.create(name="Ringo Starr") 
     1000&nbs