Opened 3 months ago

Last modified 6 weeks ago

#21832 new New feature

allow USERNAME_FIELD to be a ForeignKey

Reported by: cjerdonek Owned by: nobody
Component: contrib.auth Version: master
Severity: Normal Keywords: USERNAME_FIELD,ForeignKey,foreign key,auth,username
Cc: bmispelon, chris.jerdonek@…, loic@… Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no


This issue is to add support for USERNAME_FIELD being a ForeignKey. Currently, this isn't supported because, for example, the createsuperuser command doesn't support inputting the corresponding foreign key data (also see #21755 for another issue with the createsuperuser command needed for this).

One use case for USERNAME_FIELD being a ForeignKey is if the username is an email address, and you would like to have a separate table for e-mail address related data (e.g. total e-mails sent, last e-mail sent, etc), as well as allow changing the email address on a user account.

Attachments (0)

Change History (8)

comment:1 Changed 3 months ago by cjerdonek

  • Cc chris.jerdonek@… added
  • Needs documentation unset
  • Needs tests unset
  • Patch needs improvement unset

comment:2 Changed 6 weeks ago by anubhav9042

Can we change the implementation of USERNAME_FIELD

USERNAME_FIELD = 'somefield'


USERNAME_FIELD = ('somefield',)
USERNAME_FIELD = ('somefield','somemodel')

In this way, we can get to know the model in superuser command and we can then pass an appropriate model_instance
(The tuple's second item being optional, ofc)

Last edited 6 weeks ago by anubhav9042 (previous) (diff)

comment:3 Changed 6 weeks ago by akaariai

  • Triage Stage changed from Unreviewed to Accepted

There seems to be three different use cases here:

  • "Local FK" - USERNAME_FIELD points to local foreign key, and the value for that foreign key can be used to create the remote model. Example: USERNAME_FIELD = 'email', email points to class Email's field 'email', and that is the only required field of Email. This should be relatively easy to support.
  • "Remote field's value". Like above, but now the user model's email field points to Email model's id field, but field 'email' of the remote model is also required. This is harder to support, we need more logic so that we know to ask the email for the related model. Syntax could be USERNAME_FIELD = 'email__email'.
  • Remote model with multiple required fields. We need support for validating & creating the remote model before we create the user model. Support for REQUIRED_FIELDS = 'email__otherfield' is required, and all the logic to then create the related models, validation and so on.

I am marking this accepted, but I think we should only support the first case above. It should be relatively straightforward to support. The other cases seem to be complex and I am not sure they are worth the added value.

comment:4 Changed 6 weeks ago by anubhav9042

I have had conversation regarding this issue with @loic and @akaariai.
I hope to work on this in GSoC this year(i.e if I am accepted).

comment:5 Changed 6 weeks ago by loic84

  • Cc loic@… added

@akaariai summed up the situation pretty well, but to make sure everyone is on the same page as it took us some time to figure it out on IRC:

  • "Local FK":
class Email(models.Model):
    email = models.CharField(max_length=254)

class User(models.Model):
    email = models.ForeignKey(Email, to_field='email')

    USERNAME_FIELD = 'email'
  • "Remote field's value":
class Email(models.Model):
    email = models.CharField(max_length=254)

class User(models.Model):
    email = models.ForeignKey(Email)

    USERNAME_FIELD = 'email__email'
Last edited 6 weeks ago by loic84 (previous) (diff)

comment:6 Changed 6 weeks ago by cjerdonek

For the record, in my use case the email address model has two required fields: a "normalized" email address with the unique option (think lower-cased, for example) and a non-normalized email address to store the user's preferred spelling/casing. This lets uniqueness be enforced at the database level. The user manager's get_by_natural_key() method is overridden to normalize the given email address before querying for a user.

Is the issue with the other cases only the creation phase? It seems like an API to construct a model object from required fields might be worth having anyways. Such an API could handle foreign keys recursively.

In any case, it would be good if any commits related to this issue do not preclude or make it harder to solve more general use cases like the one I outlined.

comment:7 Changed 6 weeks ago by russellm

Question: Other than createsuperuser, is there anything currently preventing this from working?

If the problem is just createsuperuer, would an acceptable fix to bail out (gracefully) from createsuperuser if USERNAME_FIELD can't be gathered at the command line, and leave it up to the user to open a shell and use UserModel.objects.create_superuser()?

comment:8 Changed 6 weeks ago by cjerdonek

Other than createsuperuser, is there anything currently preventing this from working?

Yes, pretty much anywhere that USERNAME_FIELD appears in the code needs to be reviewed, for example AbstractBaseUser.get_username() and RemoteUserBackend.authenticate(). In my experience, though, most of these can be dealt with through straightforward subclassing and overriding. However, the createsuperuser command was the most awkward to deal with. I wound up overriding the command, but the solution I'm currently using is brittle and somewhat of a hack (relying on internal details, etc).

My hope for this issue is to reduce these barriers. In other words, I wouldn't mind if foreign keys aren't supported out of the box, but it would be nice if it were easier to make things work (and in a way that's more likely to work in future versions of Django, etc).

Add Comment

Modify Ticket

Change Properties
<Author field>
as new
The owner will be changed from nobody to anonymous. Next status will be 'assigned'
as The resolution will be set. Next status will be 'closed'

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

Note: See TracTickets for help on using tickets.