Code

Opened 9 months ago

Closed 9 months ago

Last modified 9 months ago

#20835 closed New feature (duplicate)

Allow standard m2m behaviour on through-relationships if extra-fields are nullable or have default values

Reported by: anonymous Owned by: nobody
Component: Database layer (models, ORM) Version: 1.6-beta-1
Severity: Normal Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

I'm going to illustrate this feature using example from the documentation with minor changes:
https://docs.djangoproject.com/en/dev/topics/db/models/#extra-fields-on-many-to-many-relationships

class Person(models.Model):
    name = models.CharField(max_length=128)

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')

class Membership(models.Model):
    person = models.ForeignKey(Person)
    group = models.ForeignKey(Group)
    date_joined = models.DateField(null=True) # added null=True
    invite_reason = models.CharField(max_length=64, default='unknown') # added default='unknown'
# THIS WILL NOT WORK
>>> beatles.members.add(john)
# NEITHER WILL THIS
>>> beatles.members.create(name="George Harrison")
# AND NEITHER WILL THIS
>>> beatles.members = [john, paul, ringo, george]

This is due to the fact that extra fields must be populated. It's okay.
But when extra-fields are nullable or have default values it's makes sense to allow adding entries via .add() method. Extra fields can be populated on the fly, using default values or nulls, if nulls are allowed.

So I propose, that in this case the following should work:

>>> beatles.members.add(john) # Creates a Membership with person=John, date_joined=null, invite_reason='unknown'

I've looked up the code which is blocking this behaviour:
django/db/models/fields/related.py:564

        if rel.through._meta.auto_created: # it's here
            def add(self, *objs):
                self._add_items(self.source_field_name, self.target_field_name, *objs)

                # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
                if self.symmetrical:
                    self._add_items(self.target_field_name, self.source_field_name, *objs)
            add.alters_data = True

In some places there are also exceptions:

            raise AttributeError("Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name))
            raise AttributeError("Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name))

So actually, if one comments out these "raises" and unlocks the .add() method then everything works smoothly. Even the ModelAdmin can now save m2m's without need to write your own save_m2m method. Of course, only if all the extra fields are nullable or have default values.

Attachments (0)

Change History (1)

comment:1 Changed 9 months ago by russellm

  • Needs documentation unset
  • Needs tests unset
  • Patch needs improvement unset
  • Resolution set to duplicate
  • Status changed from new to closed

Duplicate of #9475, and some others; the API implications of this were considered as part of #6095, which was the ticket that added manual m2m through models.

Last edited 9 months ago by russellm (previous) (diff)

Add Comment

Modify Ticket

Change Properties
<Author field>
Action
as closed
as The resolution will be set. Next status will be 'closed'
The resolution will be deleted. Next status will be 'new'
Author


E-mail address and user name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.