Django

Code

Ticket #6095: 6095-rc1.diff

File 6095-rc1.diff, 53.2 kB (added by russellm, 4 months ago)

First (hopefully final) release candidate for m2m-intermediates

  • 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 
    597617class ForeignKey(RelatedField, Field): 
    598618    empty_strings_allowed = False 
     
    722742            num_in_admin=kwargs.pop('num_in_admin', 0), 
    723743            related_name=kwargs.pop('related_name', None), 
    724744            limit_choices_to=kwargs.pop('limit_choices_to', None), 
    725             symmetrical=kwargs.pop('symmetrical', True)) 
     745            symmetrical=kwargs.pop('symmetrical', True), 
     746            through=kwargs.pop('through', None)) 
     747             
    726748        self.db_table = kwargs.pop('db_table', None) 
     749        if kwargs['rel'].through is not None: 
     750            self.creates_table = False 
     751            assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used." 
     752        else: 
     753            self.creates_table = True 
     754 
    727755        Field.__init__(self, **kwargs) 
    728756 
    729757        msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.') 
     
    738766 
    739767    def _get_m2m_db_table(self, opts): 
    740768        "Function that can be curried to provide the m2m table name for this relation" 
    741         if self.db_table: 
     769        if self.rel.through is not None: 
     770            return self.rel.through_model._meta.db_table 
     771        elif self.db_table: 
    742772            return self.db_table 
    743773        else: 
    744774            return '%s_%s' % (opts.db_table, self.name) 
    745775 
    746776    def _get_m2m_column_name(self, related): 
    747777        "Function that can be curried to provide the source column name for the m2m table" 
    748         # If this is an m2m relation to self, avoid the inevitable name clash 
    749         if related.model == related.parent_model: 
    750             return 'from_' + related.model._meta.object_name.lower() + '_id' 
    751         else: 
    752             return related.model._meta.object_name.lower() + '_id' 
     778        try: 
     779            return self._m2m_column_name_cache 
     780        except: 
     781            if self.rel.through is not None: 
     782                for f in self.rel.through_model._meta.fields: 
     783                    if hasattr(f,'rel') and f.rel and f.rel.to == related.model: 
     784                        self._m2m_column_name_cache = f.column 
     785                        break 
     786            # If this is an m2m relation to self, avoid the inevitable name clash 
     787            elif related.model == related.parent_model: 
     788                self._m2m_column_name_cache = 'from_' + related.model._meta.object_name.lower() + '_id' 
     789            else: 
     790                self._m2m_column_name_cache = related.model._meta.object_name.lower() + '_id' 
     791                 
     792            # Return the newly cached value 
     793            return self._m2m_column_name_cache 
    753794 
    754795    def _get_m2m_reverse_name(self, related): 
    755796        "Function that can be curried to provide the related column name for the m2m table" 
    756         # If this is an m2m relation to self, avoid the inevitable name clash 
    757         if related.model == related.parent_model: 
    758             return 'to_' + related.parent_model._meta.object_name.lower() + '_id' 
    759         else: 
    760             return related.parent_model._meta.object_name.lower() + '_id' 
     797        try: 
     798            return self._m2m_reverse_name_cache 
     799        except: 
     800            if self.rel.through is not None: 
     801                found = False 
     802                for f in self.rel.through_model._meta.fields: 
     803                    if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model: 
     804                        if related.model == related.parent_model: 
     805                            # If this is an m2m-intermediate to self,  
     806                            # the first foreign key you find will be  
     807                            # the source column. Keep searching for 
     808                            # the second foreign key. 
     809                            if found: 
     810                                self._m2m_reverse_name_cache = f.column 
     811                                break 
     812                            else: 
     813                                found = True 
     814                        else: 
     815                            self._m2m_reverse_name_cache = f.column 
     816                            break 
     817            # If this is an m2m relation to self, avoid the inevitable name clash 
     818            elif related.model == related.parent_model: 
     819                self._m2m_reverse_name_cache = 'to_' + related.parent_model._meta.object_name.lower() + '_id' 
     820            else: 
     821                self._m2m_reverse_name_cache = related.parent_model._meta.object_name.lower() + '_id' 
    761822 
     823            # Return the newly cached value 
     824            return self._m2m_reverse_name_cache 
     825 
    762826    def isValidIDList(self, field_data, all_data): 
    763827        "Validates that the value is a valid list of foreign keys" 
    764828        mod = self.rel.to 
     
    791855        return new_data 
    792856 
    793857    def contribute_to_class(self, cls, name): 
    794         super(ManyToManyField, self).contribute_to_class(cls, name) 
     858        super(ManyToManyField, self).contribute_to_class(cls, name)         
    795859        # Add the descriptor for the m2m relation 
    796860        setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self)) 
    797861 
    798862        # Set up the accessor for the m2m table name for the relation 
    799863        self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta) 
    800  
     864         
     865        # Populate some necessary rel arguments so that cross-app relations 
     866        # work correctly. 
     867        if isinstance(self.rel.through, basestring): 
     868            def resolve_through_model(field, model, cls): 
     869                field.rel.through_model = model 
     870            add_lazy_relation(cls, self, self.rel.through, resolve_through_model) 
     871        elif self.rel.through: 
     872            self.rel.through_model = self.rel.through 
     873            self.rel.through = self.rel.through._meta.object_name 
     874             
    801875        if isinstance(self.rel.to, basestring): 
    802876            target = self.rel.to 
    803877        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                    from_model, to_model = cls, f.rel.to 
     119                    if from_model == to_model and f.rel.symmetrical: 
     120                        e.add(opts, "Many-to-many fields with intermediate tables cannot be symmetrical.") 
     121                    seen_from, seen_to, seen_self = False, False, 0 
     122                    for inter_field in f.rel.through_model._meta.fields: 
     123                        rel_to = getattr(inter_field.rel, 'to', None) 
     124                        if from_model == to_model: # relation to self 
     125                            if rel_to == from_model: 
     126                                seen_self += 1 
     127                            if seen_self > 2: 
     128                                e.add(opts, "Intermediary model %s has more than two foreign keys to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, from_model._meta.object_name)) 
     129                        else: 
     130                            if rel_to == from_model: 
     131                                if seen_from: 
     132                                    e.add(opts, "Intermediary model %s has more than one foreign key to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, rel_from._meta.object_name)) 
     133                                else: 
     134                                    seen_from = True 
     135                            elif rel_to == to_model: 
     136                                if seen_to: 
     137                                    e.add(opts, "Intermediary model %s has more than one foreign key to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, rel_to._meta.object_name)) 
     138                                else: 
     139                                    seen_to = True 
     140                    if f.rel.through_model not in models.get_models(): 
     141                        e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed." % (f.name, f.rel.through)) 
     142                    signature = (f.rel.to, cls, f.rel.through_model) 
     143                    if signature in seen_intermediary_signatures: 
     144                        e.add(opts, "The model %s has two manually-defined m2m relations through the model %s, which is not permitted. Please consider using an extra field on your intermediary model instead." % (cls._meta.object_name, f.rel.through_model._meta.object_name)) 
     145                    else: 
     146                        seen_intermediary_signatures.append(signature) 
     147                    seen_related_fk, seen_this_fk = False, False 
     148                    for field in f.rel.through_model._meta.fields: 
     149                        if field.rel: 
     150                            if not seen_related_fk and field.rel.to == f.rel.to: 
     151                                seen_related_fk = True 
     152                            elif field.rel.to == cls: 
     153                                seen_this_fk = True 
     154                    if not seen_related_fk or not seen_this_fk: 
     155                        e.add(opts, "'%s' has a manually-defined m2m relation through model %s, which does not have foreign keys to %s and %s" % (f.name, f.rel.through, f.rel.to._meta.object_name, cls._meta.object_name)) 
     156                else: 
     157                    e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed" % (f.name, f.rel.through)) 
     158             
    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") 
     143    too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK") 
     144 
     145class PersonSelfRefM2MExplicit(models.Model): 
     146    name = models.CharField(max_length=5) 
     147    friends = models.ManyToManyField('self', through="ExplicitRelationship", symmetrical=True) 
     148 
     149class Relationship(models.Model): 
     150    first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set") 
     151    second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set") 
     152    date_added = models.DateTimeField() 
     153 
     154class ExplicitRelationship(models.Model): 
     155    first = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_from_set") 
     156    second = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_to_set") 
     157    date_added = models.DateTimeField() 
     158 
     159class RelationshipTripleFK(models.Model): 
     160    first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2") 
     161    second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2") 
     162    third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far") 
     163    date_added = models.DateTimeField() 
     164 
     165class RelationshipDoubleFK(models.Model): 
     166    first = models.ForeignKey(Person, related_name="first_related_name") 
     167    second = models.ForeignKey(Person, related_name="second_related_name") 
     168    third = models.ForeignKey(Group, related_name="rel_to_set") 
     169    date_added = models.DateTimeField() 
     170 
    114171model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute. 
    115172invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute. 
    116173invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute. 
     
    195252invalid_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'. 
    196253invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed 
    197254invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed 
     255invalid_models.grouptwo: 'primary' has a manually-defined m2m relation through model Membership, which does not have foreign keys to Person and GroupTwo 
     256invalid_models.grouptwo: 'secondary' has a manually-defined m2m relation through model MembershipMissingFK, which does not have foreign keys to Group and GroupTwo 
     257invalid_models.missingmanualm2mmodel: 'missing_m2m' specifies an m2m relation through model MissingM2MModel, which has not been installed 
     258invalid_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. 
     259invalid_models.group: Intermediary model RelationshipDoubleFK has more than one foreign key to Person, which is ambiguous and is not permitted. 
     260invalid_models.personselfrefm2m: Many-to-many fields with intermediate tables cannot be symmetrical. 
     261invalid_models.personselfrefm2m: Intermediary model RelationshipTripleFK has more than two foreign keys to PersonSelfRefM2M, which is ambiguous and is not permitted. 
     262invalid_models.personselfrefm2mexplicit: Many-to-many fields with intermediate tables cannot be symmetrical. 
    198263""" 
  • tests/modeltests/m2m_through/__init__.py

    old new  
     1 
     2 
  • 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", symmetrical=False) 
     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 isn't a symmetrical relation, Tony's friend link still exists. 
     274>>> tony.friends.all() 
     275[<PersonSelfRefM2M: Chris>] 
     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  
     1 
     2 
  • 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"""} 
     103from django.db import models 
     104from datetime import datetime 
     105from django.contrib.auth.models import User 
     106 
     107# Forward declared intermediate model 
     108class Membership(models.Model): 
     109    person = models.ForeignKey('Person') 
     110    group = models.ForeignKey('Group') 
     111    date_joined = models.DateTimeField(default=datetime.now) 
     112     
     113    def __unicode__(self): 
     114        return "%s is a member of %s" % (self.person.name, self.group.name) 
     115 
     116class UserMembership(models.Model): 
     117    user = models.ForeignKey(User) 
     118    group = models.ForeignKey('Group') 
     119    date_joined = models.DateTimeField(default=datetime.now) 
     120     
     121    def __unicode__(self): 
     122        return "%s is a user and member of %s" % (self.user.username, self.group.name) 
     123 
     124class Person(models.Model): 
     125    name = models.CharField(max_length=128) 
     126 
     127    def __unicode__(self): 
     128        return self.name 
     129 
     130