Skip to content

Commit

Permalink
Stop using UserCreationForm.
Browse files Browse the repository at this point in the history
This was already planned because of the issues with UserCreationForm
hard-coding the default Django user model, but as of Django
5.1.0 (which inadvertently added an admin-only field to the form) it's
a bit more urgent to switch away.

This commit makes the following major changes:

* Introduces a pair of new base form classes -- one extremely minimal
  base class and a new default RegistrationForm derived from it --
  which serve as the base registration form classes.

* Enforces case-insensitive uniqueness of username on
  RegistrationForm. As of Django 4.2, UserCreationForm enforces
  case-insensitive username uniqueness by default, which means
  django-registration can finally enforce it by default as well. As
  a side effect, this deprecates RegistrationFormCaseInsensitive,
  because it existed solely to add the case-insensitive uniqueness
  validator on the username.

The new base form classes derive much of their field-inclusion logic
from the standard EMAIL_FIELD, USERNAME_FIELD, and REQUIRED_FIELDS
attributes of user model classe, greatly simplifying custom-user
support. The largest potentially-confusing situation is when a custom
user model uses the email address as the "username"; the solution for
that case to set EMAIL_FIELD and USERNAME_FIELD to the same
value (mentioned in the documentation).
  • Loading branch information
ubernostrum committed Sep 16, 2024
1 parent 7e5fd8d commit 90ba1d8
Show file tree
Hide file tree
Showing 12 changed files with 505 additions and 328 deletions.
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[flake8]
extend-ignore = E203, E501, W503
extend-ignore = E203, E501, W503, B008
max-complexity = 13
max-line-length = 88
select = C,E,F,W,B,B950
24 changes: 20 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,31 @@
"""

# SPDX-License-Identifier: BSD-3-Clause

import os
import sys
from importlib.metadata import version as get_version

import django
from django.conf import settings

settings.configure(
INSTALLED_APPS=[
"django.contrib.auth",
"django.contrib.contenttypes",
"django_registration",
],
DEBUG=True,
)

django.setup()

extensions = [
"notfound.extension",
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
"sphinxcontrib_django",
"sphinxext.opengraph",
"sphinx_copybutton",
"sphinx_inline_tabs",
Expand All @@ -22,9 +38,9 @@
source_suffix = ".rst"
master_doc = "index"
project = "django-registration"
copyright = "2007, James Bennett"
version = "3.5a1"
release = "3.5a1"
copyright = "James Bennett and contributors"
version = get_version("django-registration")
release = version
exclude_trees = ["_build"]
pygments_style = "sphinx"
htmlhelp_basename = "django-registrationdoc"
Expand Down
242 changes: 61 additions & 181 deletions docs/custom-user.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,214 +10,94 @@ projects choose to use a custom user model from the start of their development,
even if it begins as a copy of the default model, in order to avoid the
difficulty of migrating to a custom user model later on.

In general, django-registration will work with a custom user model, though at
least some additional configuration is always required in order to do so. If
you are using a custom user model, please read this document thoroughly
*before* using django-registration, in order to ensure you've taken all the
necessary steps to ensure support.
In general, ``django-registration`` will work with a custom user model, though
there is some required configuration. If you are using a custom user model,
please read this document thoroughly *before* using ``django-registration``, in
order to ensure you've taken all the necessary steps to ensure support.

The process for using a custom user model with django-registration can be
summarized as follows:

1. Compare your custom user model to the assumptions made by the built-in
registration workflows.
Required attributes and methods
-------------------------------

2. If your user model is compatible with those assumptions, write a short
subclass of :class:`~django_registration.forms.RegistrationForm` pointed at
your user model, and instruct django-registration to use that form.
The following attributes *must* be present on your user model:

3. If your user model is *not* compatible with those assumptions, either write
subclasses of the appropriate views in django-registration which will be
compatible with your user model, or modify your user model to be compatible
with the built-in views.
* :attr:`~django.contrib.auth.models.CustomUser.EMAIL_FIELD`: a :class:`str`
specifying the name of the field containing the user's email address.

These steps are covered in more detail below.
* :attr:`~django.contrib.auth.models.CustomUser.USERNAME_FIELD`: a :class:`str`
specifying the name of the field containing the user's "username". If you use
the email address as the primary "username"/identifier, set this to the same
field name as ``EMAIL_FIELD``.

* :attr:`~django.contrib.auth.models.CustomUser.REQUIRED_FIELDS`: a
:class:`list` of names of fields on your user model which must be included in
the registration form.

Django's :class:`~django.contrib.auth.models.AbstractUser`, which is what many
custom user models will inherit from and also what the default Django user
model inherits from, sets all three of these, and generally for a custom user
model you would only need to override ``REQUIRED_FIELDS`` in order to specify
any additional custom fields of your model which should be included in the
registration form.

However, if you have a custom user model which inherits from Django's
:class:`~django.contrib.auth.models.AbstractBaseUser` (which is an even more
minimal base class than ``AbstractUser``), or which does not inherit from any
of Django's abstract base user classes, you will need to set all three of the
above attributes on your custom user model for it to be usable with this form.

Additionally, the following two methods may be required on your model:

* If you are using the default
:class:`~django_registration.forms.RegistrationForm` or a subclass of it,
your user model must implement the
:meth:`~django.contrib.auth.models.AbstractBaseUser.set_password` method to
store the user's selected password. If your user model inherits from
``AbstractUser`` or ``AbstractBaseUser``, this is implemented for you.

* If you use a registration workflow which sends an email to the
newly-registered user (such as :ref:`the built-in two-step activation
workflow <activation-workflow>`), your user model must implement the
:meth:`~django.contrib.auth.models.User.email_user` method, with the same API
as Django's implementation. If your user model inherits from
``AbstractUser``, this is implemented for you.


Compatibility of the built-in workflows with custom user models
---------------------------------------------------------------

Django provides a number of helpers to make it easier for code to generically
work with custom user models, and django-registration makes use of
work with custom user models, and ``django-registration`` makes use of
these. However, the built-in registration workflows must still make *some*
assumptions about the structure of your user model in order to work with it. If
you intend to use one of django-registration's built-in registration workflows,
please carefully read the appropriate section to see what it expects from your
user model.
you intend to use one of ``django-registration``'s built-in registration
workflows, please carefully read the appropriate section to see what it expects
from your user model.


The two-step activation workflow
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:ref:`The two-step activation workflow <activation-workflow>` requires that the
following be true of your user model:

* Your user model must set the attribute
:attr:`~django.contrib.auth.models.CustomUser.USERNAME_FIELD` to indicate the
field used as the username.

* Your user model must have a field (of some textual type, ideally
:class:`~django.db.models.EmailField`) for storing an email address, and it
must define the method
:meth:`~django.contrib.auth.models.AbstractBaseUser.get_email_field_name` to
indicate the name of the email field.

* The username and email fields must be distinct. If you wish to use the email
address as the username, you will need to write your own completely custom
registration form.
In addition to the general requirements above, :ref:`The two-step activation
workflow <activation-workflow>` requires that the following be true of your
user model:

* Your user model must have a field named
:attr:`~django.contrib.auth.models.User.is_active`, and that field must be a
:class:`~django.db.models.BooleanField` indicating whether the user's account
is active.

If your user model is a subclass of Django's
:class:`~django.contrib.auth.models.AbstractBaseUser`, all of the above will be
automatically handled for you.

If your custom user model defines additional fields beyond the minimum
requirements, you'll either need to ensure that all of those fields are
optional (i.e., can be ``NULL`` in your database, or provide a suitable default
value defined in the model), or specify the correct list of fields to display
in your :class:`~django_registration.forms.RegistrationForm` subclass.
is active. If your user model inherits from Django's
:class:`~django.contrib.auth.models.AbstractUser`, this field is defined for
you.


The one-step workflow
~~~~~~~~~~~~~~~~~~~~~

:ref:`The one-step workflow <one-step-workflow>` places the following
requirements on your user model:

* Your user model must set the attribute
:attr:`~django.contrib.auth.models.CustomUser.USERNAME_FIELD` to indicate the
field used as the username.

* It must define a textual field named ``password`` for storing the user's
password.

Also note that the base :class:`~django_registration.forms.RegistrationForm`
includes and requires an email field, so either provide that field on your
model and set the
:meth:`~django.contrib.auth.models.AbstractBaseUser.get_email_field_name`
attribute to indicate which field it is, or subclass
:class:`~django_registration.forms.RegistrationForm` and override to remove the
`email` field or make it optional.

If your user model is a subclass of Django's
:class:`~django.contrib.auth.models.AbstractBaseUser`, all of the above will be
automatically handled for you.

If your custom user model defines additional fields beyond the minimum
requirements, you'll either need to ensure that all of those fields are
optional (i.e., can be ``NULL`` in your database, or provide a suitable default
value defined in the model), or specify the correct list of fields to display
in your :class:`~django_registration.forms.RegistrationForm` subclass.

Because the one-step workflow logs in the new account immediately after
creating it, you also must either use Django's
Because :ref:`the one-step workflow <one-step-workflow>` logs in the new
account immediately after creating it, you must either use Django's
:class:`~django.contrib.auth.backends.ModelBackend` as an `authentication
backend
<https://docs.djangoproject.com/en/stable/topics/auth/customizing/#other-authentication-sources>`_,
or use an authentication backend which accepts a combination of
``USERNAME_FIELD`` and ``password`` as sufficient credentials to authenticate a
user.


Writing your form subclass
--------------------------

The base :class:`~djangqo_registration.views.RegistrationView` contains
code which compares the declared model of your registration form with
the user model of your Django installation. If these are not the same
model, the view will deliberately crash by raising an
:exc:`~django.core.exceptions.ImproperlyConfigured` exception, with an
error message alerting you to the problem.

This will happen automatically if you attempt to use django-registration with a
custom user model and also attempt to use the default, unmodified
:class:`~django-registration.forms.RegistrationForm`. This is, again, a
deliberate design feature of django-registration, and not a bug:
django-registration has no way of knowing in advance if your user model is
compatible with the assumptions made by the built-in registration workflows
(see above), so it requires you to take the explicit step of replacing the
default registration form as a way of confirming you've manually checked the
compatibility of your user model.

In the case where your user model is compatible with the default behavior of
django-registration, you will be able to subclass
:class:`~django_registration.forms.RegistrationForm`, set it to use your custom
user model as the model, and then configure the views in django-registration to
use your form subclass. For example, you might do the following (in a
``forms.py`` module somewhere in your codebase -- do **not** directly edit
django-registration's code):

.. code-block:: python
from django_registration.forms import RegistrationForm
from mycustomuserapp.models import MyCustomUser
class MyCustomUserForm(RegistrationForm):
class Meta(RegistrationForm.Meta):
model = MyCustomUser
You may also need to specify the fields to include in the form, if the set of
fields to include is different from the default set specified by the base
:class:`~django_registration.forms.RegistrationForm`.

Then in your URL configuration (example here uses the two-step activation
workflow), configure the registration view to use the form class you wrote:

.. code-block:: python
from django.urls import include, path
from django_registration.backends.activation.views import RegistrationView
from mycustomuserapp.forms import MyCustomUserForm
urlpatterns = [
# ... other URL patterns here
path('accounts/register/',
RegistrationView.as_view(
form_class=MyCustomUserForm
),
name='django_registration_register',
),
path('accounts/',
include('django_registration.backends.activation.urls')
),
# ... more URL patterns
]
Incompatible user models
------------------------

If your custom user model is not compatible with the built-in workflows of
django-registration, you have several options.

One is to subclass the built-in form and view classes of django-registration
and make the necessary adjustments to achieve compatibility with your user
model. For example, if you want to use the two-step activation workflow, but
your user model uses a completely different way of marking accounts
active/inactive (compared to the the assumed ``is_active`` field), you might
write subclasses of that workflow's
:class:`~django_registration.backends.activation.views.RegistrationView` and
:class:`~django_registration.backends.activation.views.ActivationView` which
make use of your user model's mechanism for marking accounts active/inactive,
and then use those views along with an appropriate subclass of
:class:`~django_registration.forms.RegistrationForm`.

Or, if the incompatibilities are relatively minor and you don't mind making the
change, you might use Django's migration framework to adjust your custom user
model to match the assumptions made by django-registration's built-in
workflows, thus allowing them to be used unmodified.

Finally, it may sometimes be the case that a given user model requires a
completely custom set of form and view classes to support. Typically, this will
also involve an account-registration process far enough from what
django-registration's built-in workflows provide that you would be writing your
own workflow in any case.
or else use an authentication backend which accepts a combination of your
model's ``USERNAME_FIELD`` and a password value named ``"password"`` as
sufficient credentials to authenticate a user.
6 changes: 5 additions & 1 deletion docs/docs_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@
"""

INSTALLED_APPS = ["django_registration"]
INSTALLED_APPS = [
"django.contrib.auth",
"django.contrib.contenttypes",
"django_registration",
]
Loading

0 comments on commit 90ba1d8

Please sign in to comment.