Open mikerkelly opened 1 month ago
@mikerkelly to break this into smaller issues.
This comment investigates the first two bullets from the issue description. How much of UserManager
, UserManager.create_user
, get_or_create_user
and UserQuerySet
do we need?
django.contrib.auth.models.UserManager
?The UserManager
that our User
model's manager extends provides the following functionality:
_create_user
function that ensures:
username
is set.GlobalUserModel.normalize_username
to normalize unicode.create_user
and create_superuser
functions to create users with fields is_staff
and is_superuser
set.
with_perm
function to apply a Permission
.
Permission
model. Therefore we don't need this.It requires a bit of digging to understand this. None of these behaviours were documented as important in our code or tested for (except for the _create_user
behaviours in one #4625 unit test test_create_user
where this behaviour was recently identified). If they're needed, we should clarify and test them. If not, we can simplify by slightly extending django.db.models.Manager
or removing the manager entirely.
Social-auth users (GitHub) don't need these steps, as fields are validated through GitHub OAuth. Interactive users are created via interactive.commands.create_user
, which calls User.objects.create
and bypasses UserManager.create_user
altogether.
Our UserManager
create_user
relies on the private parent _create_user
function because the public methods automatically set is_staff
and is_superuser
, which we don’t want. Depending on an internal method is both difficult to follow and risks breaking when we update Django.
Our get_or_create_user
function mimics standard Python Django get_or_create
. There is a comment that this is needed because of the above special behaviour, as the standard manager calls create
instead of _create_user
. However, if we don't need _create_user
, we don't need this function. It's used in two places:
auth_pipeline.py
, populated by GitHub OAuth with appropriate values.jobserver
management command create_user
, where the password
is not populated, and username
is guaranteed to be populated. While email
and username
are normalized, it's unclear how important this is. It does prevent creating multiple users with equivalent but distinct strings when using this command manually.We don’t seem to rely on any of the special behaviors of Django's UserManager
. The create_user
management command only marginally benefits from normalization. We don't allow password-based logins, and currently, no users are created with an actual password. However, as a precaution, we should still hash any password passed to User
creation in case it’s used in the future or if a real password is provided.
Conclusion: we can simplify by extending django.db.models.Manager
with a custom create
method that handles normalization and password hashing. This will make it clearer the behaviours User
creation exhibits. This also lets us remove the custom get_or_create_user
. Both of these reduce the amount of code to understand and maintain.
UserQuerySet
?UserQuerySet
exists to order User
instances so that those with a blank fullname
appear last.
We can replicate this behaviour using Case
and When
clauses in Meta.ordering
, making it the default ordering for the User
model. This way, client code can call objects.all()
without needing to explicitly use order_by_name
. It’s also more in line with standard Django practices to define it this way.
A quick prototype showed that most of the troublesome code in the User
model could just be removed and everything still worked fine with not-too-many-changes. So instead of splitting this I've created PR #4698 with that and some other small improvements from this ticket or noted during that work.
The only outstanding thing from the description is this:
" TODO: rename name and remove the name property once all users have filledin their names"
Raised #4699 for this for triage.
4625 removed
User.is_staff
andUser.is_superuser
as no longer required. They were present as we once useddjango.contrib.admin
but no longer do (instead having a completely custom staff app).While working on #4625 I noticed some possible further simplifications to the
models.User
module. There are several more bits of code or peculiarities that may exist for historical reasons but are no longer required. Simplifying the module and using less custom-behaviour where appropriate would aid understanding and maintenance.This might need breaking up into multiple issues. Possible changes to investigate:
BaseUserManager
, as we extendAbstractBaseUser
, rather thanUserManager
that is intended for managingBaseUser
. The names are a little confusing here, careful.super()._create_user
.UserManager.create_user
behaviour around normalizing username and e-mail and hashing the password as we use social auth, but this needs investigation.name
and remove thename
property once all users have filledin their names"get_full_name
needed?UserQuerySet
for sortingorder_by(Lower("fullname"))
once all users havefullname
populated"UserManager
-- as above. Could it be the default?create_user
-- could this be default, or at least be calledUser.objects.create()
to be idiomatic?get_or_create_user
-- if we do the above, is this function then needed or do we get it "for free" by the normalUser.objects.get_or_create_user
machinery?Separate email and username properties if these are always the same?UserQuerySet
docstring / TODO