Opened 3 months ago

Closed 3 months ago

Last modified 3 months ago

#35410 closed Bug (invalid)

Can't Set a Default Value for ForeignKey Field in Custom User Model

Reported by: Ebram Shehata Owned by: nobody
Component: contrib.auth Version: 5.0
Severity: Normal Keywords: migrations, foreignkey, user, models
Cc: Ebram Shehata Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Ebram Shehata)

Hello,

So, I'm trying to add a ForeignKey field with a default value in a custom user model.
The use case is that each user should be assigned to a department. But all new users
should have a default department with name 'UNASSIGNED'.

  • How to reproduce:
  1. Create a blank Django project.
  2. Create a new 'profiles' app.
  3. Register the app in settings.py and point AUTH_USER_MODEL to "profiles.UserProfile".
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    "profiles"
]
AUTH_USER_MODEL = "profiles.UserProfile"
  1. Add the following models to profiles/models.py:
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin

from django.db import models


class Department(models.Model):
    name = models.CharField(max_length=256, unique=True)


def unassigned_department():
    return Department.objects.get_or_create(name="UNASSIGNED")[0].pk


class UserProfile(AbstractBaseUser, PermissionsMixin):
    department = models.ForeignKey(
        Department,
        on_delete=models.CASCADE,
        default=unassigned_department,
        related_name="user_profiles",
    )
    username = models.CharField(max_length=256, unique=True)

    is_active = models.BooleanField(default=True, null=False)
    is_superuser = models.BooleanField(default=False, null=False)
    is_staff = models.BooleanField(default=False, null=False)

    USERNAME_FIELD = "username"
  1. Run python manage.py makemigrations.

You'll get the following error:
django.db.utils.OperationalError: no such table: profiles_department

Django versions I tried: 4.2.7 and 5.0.4.

I also noticed that if I inherit from django.db.models.Model in UserProfile de-register it
from AUTH_USER_MODEL setting, I can create migrations successfully and migrate the
database too! I also could create instances that have the default department as expected.

Here's the full traceback:

Traceback (most recent call last):
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/backends/utils.py", line 105, in _execute
    return self.cursor.execute(sql, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/backends/sqlite3/base.py", line 329, in execute
    return super().execute(query, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
sqlite3.OperationalError: no such table: profiles_department

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/SOME-DIR/blank_django/src/blanked/manage.py", line 22, in <module>
    main()
  File "/SOME-DIR/blank_django/src/blanked/manage.py", line 18, in main
    execute_from_command_line(sys.argv)
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/core/management/__init__.py", line 442, in execute_from_command_line
    utility.execute()
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/core/management/__init__.py", line 436, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/core/management/base.py", line 413, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/core/management/base.py", line 454, in execute
    self.check()
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/core/management/base.py", line 486, in check
    all_issues = checks.run_checks(
                 ^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/core/checks/registry.py", line 88, in run_checks
    new_errors = check(app_configs=app_configs, databases=databases)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/contrib/auth/checks.py", line 84, in check_user_model
    if isinstance(cls().is_anonymous, MethodType):
                  ^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/models/base.py", line 535, in __init__
    val = field.get_default()
          ^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/models/fields/related.py", line 1134, in get_default
    field_default = super().get_default()
                    ^^^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py", line 1021, in get_default
    return self._get_default()
           ^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/blanked/profiles/models.py", line 11, in unassigned_department
    return Department.objects.get_or_create(name="UNASSIGNED")[0].pk
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/models/manager.py", line 87, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/models/query.py", line 948, in get_or_create
    return self.get(**kwargs), False
           ^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/models/query.py", line 645, in get
    num = len(clone)
          ^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/models/query.py", line 382, in __len__
    self._fetch_all()
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/models/query.py", line 1928, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/models/query.py", line 91, in __iter__
    results = compiler.execute_sql(
              ^^^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/models/sql/compiler.py", line 1562, in execute_sql
    cursor.execute(sql, params)
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/backends/utils.py", line 122, in execute
    return super().execute(sql, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/backends/utils.py", line 79, in execute
    return self._execute_with_wrappers(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/backends/utils.py", line 92, in _execute_with_wrappers
    return executor(sql, params, many, context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/backends/utils.py", line 100, in _execute
    with self.db.wrap_database_errors:
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/utils.py", line 91, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/backends/utils.py", line 105, in _execute
    return self.cursor.execute(sql, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/backends/sqlite3/base.py", line 329, in execute
    return super().execute(query, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
django.db.utils.OperationalError: no such table: profiles_department

Change History (3)

comment:1 by Tim Graham, 3 months ago

Resolution: invalid
Status: newclosed

You didn't provide the full traceback but I suspect the error comes from Department.objects.get_or_create(). I don't think Django is at fault. You could make the migrations in two steps, adding the default only after the Department table is created.

in reply to:  1 comment:2 by Ebram Shehata, 3 months ago

Description: modified (diff)

Replying to Tim Graham:

You didn't provide the full traceback but I suspect the error comes from Department.objects.get_or_create(). I don't think Django is at fault. You could make the migrations in two steps, adding the default only after the Department table is created.

Oh, I just added the traceback.

  • I think that is not the expected behavior when someone uses a callable as default parameter. I expect it to be used when the value was not provided and I'm trying to create an instance and of course not when I make migrations and that is working as expected when the target model is not a custom user model (inheriting from models.Model).
  • I also think the work around of creating a data migration for creating the default instance is not actually convenient. Because I think the scenario could be:

Develop stuff.
Create migration for the project and the a special migration for that default instance.
After that we'll have to actually migrate the database so far to create that instance.
Now develop the custom user profile and make migrations.
Now we should be able to migrate database but no. Here's a problem that appeared with this scenario after adding the custom user model in this case:

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, profiles, sessions
Traceback (most recent call last):
  File "/SOME-DIR/blank_django/src/blanked/manage.py", line 22, in <module>
    main()
  File "/SOME-DIR/blank_django/src/blanked/manage.py", line 18, in main
    execute_from_command_line(sys.argv)
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/core/management/__init__.py", line 442, in execute_from_command_line
    utility.execute()
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/core/management/__init__.py", line 436, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/core/management/base.py", line 413, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/core/management/base.py", line 459, in execute
    output = self.handle(*args, **options)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/core/management/base.py", line 107, in wrapper
    res = handle_func(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/core/management/commands/migrate.py", line 302, in handle
    pre_migrate_apps = pre_migrate_state.apps
                       ^^^^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/utils/functional.py", line 47, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
                                         ^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/migrations/state.py", line 566, in apps
    return StateApps(self.real_apps, self.models)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/SOME-DIR/blank_django/src/venv/lib/python3.11/site-packages/django/db/migrations/state.py", line 637, in __init__
    raise ValueError("\n".join(error.msg for error in errors))
ValueError: The field admin.LogEntry.user was declared with a lazy reference to 'profiles.userprofile', but app 'profiles' doesn't provide model 'userprofile'.

Overall about the scenario of having to create a data migration for that default instance, I think it's not convenient and it is not what's expected from a callable in default parameter. I think it should be behaving the same as with regular models, not having a special behavior if we're creating a custom user model.

comment:3 by Simon Charette, 3 months ago

Maybe I ask you are not simply NULL to denote the lack of assignment? Trying to perform database operations in a Field.default callback is doomed to fail in a myriad of ways hence why the documentation suggests that a literal value be returned instead.

I think we could adjust the checks to avoid creating user model instances at check time but there are tons of ways why the pattern you are using now might break.

Last edited 3 months ago by Simon Charette (previous) (diff)
Note: See TracTickets for help on using tickets.
Back to Top