carltongibson / django-unique-user-email

Enable login-by-email with the default User model for your Django project by making auth.User.email unique.
MIT License
113 stars 6 forks source link

ValueError: No constraint named unique_user_email on model User #5

Closed trottomv closed 10 months ago

trottomv commented 10 months ago

Hi, even after following the various instructions for installing the app, I receive the following error when applying migrations.

 File "~/.local/lib/python3.11/site-packages/django/db/migrations/state.py", line 968, in get_constraint_by_name
    raise ValueError("No constraint named %s on model %s" % (name, self.name))
ValueError: No constraint named unique_user_email on model User
make: *** [Makefile:59: migrate] Error 1

python version 3.11 django version Django==4.2.7 postgresql version 14

carltongibson commented 10 months ago

Hi @trottomv — You'll need to give me more to go on. This works directly for me.

Set up a project:

mktmpenv
pip install Django django-unique-user-email psycopg
django-admin startproject issue5
cd issue5
bbedit .

Adjust the settings:

# in settings.py
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "issue5",
    }
}
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "unique_user_email",
]

And migrate:

./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, unique_user_email
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK
  Applying unique_user_email.0001_initial... OK
trottomv commented 10 months ago

Hi @carltongibson, I was in a specific context with some 'bells and whistles,' such as Docker. I found myself in a situation where I needed to apply modifications to an already started project, although still very minimal. However, the auth migrations had already been applied, this context to clarify: https://docs.djangoproject.com/en/4.2/topics/auth/customizing/#changing-to-a-custom-user-model-mid-project

I attempted to execute the same identical minimal setup on a virtualenv, as you clearly outlined above. Indeed, the first migration for 'django_unique_email' run succesfully, but...

Upon running the subsequent 'makemigrations,' a migration 0013 was added to 'auth' that removes the constraint:

$ python manage.py makemigrations
Migrations for 'auth':
/Users/trotto/experiments/django_unique_email/.venv/lib/python3.11/site-packages/django/contrib/auth/migrations/0013_remove_user_unique_user_email.py
- Remove constraint unique_user_email from model user

Following this, each subsequent 'python manage.py migrate' is executed, an error a bit different that I've reported earlier is encountered. It doesn't exactly the same error I initially reported, I can't reproduce it in this minimal virtualenv context now. However, it seems that the migration 0013 in 'auth' is causing some disruption.

I'm pasting here the entire traceback for completeness:

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, unique_user_email
Running migrations:
  Applying auth.0014_remove_user_unique_user_email...Traceback (most recent call last):
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/django/db/backends/utils.py", line 87, in _execute
    return self.cursor.execute(sql)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/psycopg/cursor.py", line 737, in execute
    raise ex.with_traceback(None)
psycopg.errors.UndefinedObject: constraint "unique_user_email" of relation "auth_user" does not exist

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

Traceback (most recent call last):
  File "~/django_unique_email/issue5/manage.py", line 22, in <module>
    main()
  File "~/django_unique_email/issue5/manage.py", line 18, in main
    execute_from_command_line(sys.argv)
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/django/core/management/__init__.py", line 442, in execute_from_command_line
    utility.execute()
  File "~/django_unique_email/.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 "~/django_unique_email/.venv/lib/python3.11/site-packages/django/core/management/base.py", line 412, in run_from_argv
    self.execute(*args, **cmd_options)
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/django/core/management/base.py", line 458, in execute
    output = self.handle(*args, **options)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/django/core/management/base.py", line 106, in wrapper
    res = handle_func(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/django/core/management/commands/migrate.py", line 356, in handle
    post_migrate_state = executor.migrate(
                         ^^^^^^^^^^^^^^^^^
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/django/db/migrations/executor.py", line 135, in migrate
    state = self._migrate_all_forwards(
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/django/db/migrations/executor.py", line 167, in _migrate_all_forwards
    state = self.apply_migration(
            ^^^^^^^^^^^^^^^^^^^^^
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/django/db/migrations/executor.py", line 252, in apply_migration
    state = migration.apply(state, schema_editor)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/django/db/migrations/migration.py", line 132, in apply
    operation.database_forwards(
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/django/db/migrations/operations/models.py", line 1178, in database_forwards
    schema_editor.remove_constraint(model, constraint)
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/django/db/backends/base/schema.py", line 542, in remove_constraint
    self.execute(sql)
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/django/db/backends/postgresql/schema.py", line 48, in execute
    return super().execute(sql, None)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/django/db/backends/base/schema.py", line 201, in execute
    cursor.execute(sql, params)
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/django/db/backends/utils.py", line 102, in execute
    return super().execute(sql, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/django/db/backends/utils.py", line 67, in execute
    return self._execute_with_wrappers(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/django/db/backends/utils.py", line 80, in _execute_with_wrappers
    return executor(sql, params, many, context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/django/db/backends/utils.py", line 84, in _execute
    with self.db.wrap_database_errors:
  File "~/django_unique_email/.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 "~/django_unique_email/.venv/lib/python3.11/site-packages/django/db/backends/utils.py", line 87, in _execute
    return self.cursor.execute(sql)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "~/django_unique_email/.venv/lib/python3.11/site-packages/psycopg/cursor.py", line 737, in execute
    raise ex.with_traceback(None)
django.db.utils.ProgrammingError: constraint "unique_user_email" of relation "auth_user" does not exist
carltongibson commented 10 months ago

OK, good, I got you… 👍 — We'll need to massage the auto-detector there 🤔 (I'll need to have a play.)

In the meantime, if you make sure to always pass the [app_label ...] argument to makemigrations you won't trigger the issue in auth.

carltongibson commented 10 months ago

Hi @trottomv, it looks like it works with the following diff:

diff --git a/src/unique_user_email/apps.py b/src/unique_user_email/apps.py
index d407dfb..fea87f7 100644
--- a/src/unique_user_email/apps.py
+++ b/src/unique_user_email/apps.py
@@ -1,4 +1,5 @@
 from django.apps import AppConfig
+from django import db

 class UniqueUserEmailConfig(AppConfig):
@@ -23,4 +24,14 @@ class UniqueUserEmailConfig(AppConfig):
         ]
         User._meta.constraints = User.Meta.constraints
         # ... as long as original_attrs is not updated.
-        # User._meta.original_attrs["constraints"] = User.Meta.constraints
+        #   BUT that must be updated once the unique_user_email migration is applied.
+        with db.connection.cursor() as cursor:
+            try:
+                cursor.execute(
+                    "SELECT name FROM django_migrations WHERE app = 'unique_user_email'"
+                )
+            except db.OperationalError:
+                pass  # No migrations run yet.
+            else:
+                if bool(cursor.fetchall()):
+                    User._meta.original_attrs["constraints"] = User.Meta.constraints

I don't know if it'd be the best test but a makemigrations --dry-run should output No changes detected if it's all working correctly. (Any better ideas? 🤔)

Would you fancy making a pull request to resolve this issue? 🎁

trottomv commented 10 months ago

I would avoid the RAW query and the various with try except else and suggest utilizing the django orm through MigrationRecorder.

 from django.db.migrations.recorder import MigrationRecorder

... 

if bool(MigrationRecorder.Migration.objects.filter(app="unique_user_email")):
    User._meta.original_attrs["constraints"] = User.Meta.constraints       
carltongibson commented 10 months ago

Yep, happy to look at that. 👍

trottomv commented 10 months ago

I have opened the PR that fix that. After various tests and checks, it seems to me that the control on the presence of migration is a bit redundant and, indeed, could create other problems. So, I limited myself to adding a test and uncommenting the already existing line that acts on original_attrs["constraint"] without introducing the hypothesized control. Am I missing something? Because it should be enough that "unique_user_email" is present in the INSTALLED_APPS and avoid checking the presence of migrations.

carltongibson commented 10 months ago

@trottomv Great thanks. I replied on the PR... — let's continue there 🎁