Custom django data migration creating records allauth.account EmailAddress model returns error – django.db.migrations.exceptions.NodeNotFoundError:

Question:

I am upgrading a django project that was created with the default django auth. Email verification was implemented with the django.contrib.auth.tokens package. The way it worked was that the ‘is_active’ flag of the default django user which itself is extended with a custom user is initially set to False and changes to True after the user verifies the email.

Now, I have upgraded the project to use django-allauth, gotten every thing else to work just fine (in development), EXCEPT the email verification. Since django-allauth extends the User model with the EmailAddress model and checks ‘verified’ flag on this model to determine if an email has been verified/confirmed, I decided to write a custom data migration which creates the records in the EmailAddress table and sets verified = True if user.is_active = True. However, I get the error below:

django.db.migrations.exceptions.NodeNotFoundError: Migration accounts.0003_create_allauth_email_records_for_existing_users dependencies reference nonexistent parent node (‘allauth.account’,
‘0002_email_max_length’)

accounts/migrations/0003_create_allauth_email_records_for_existing_users.py

# Generated by Django 3.2.11 on 2022-12-09 10:56

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('accounts', '0002_delete_profile'),
        ('allauth.account', '0002_email_max_length')
    ]

    def create_allauth_email_records_for_existing_users(apps, schema_editor):

        UserModel = apps.get_model("accounts", "User")
        EmailMoldel = apps.get_model("allauth.account", "EmailAddress")

        for user in UserModel.objects.all():
            email_record = EmailMoldel.objects.filter(email=user.email).first()
            
            if email_record == None:

                if user.is_active:
                    email_verified = True
                else:
                    email_verified = False

                new_email = EmailMoldel.objects.create(
                    user = user,
                    email = user.email,
                    verified = email_verified,
                    )
    
    def reverse_func(apps, schema_editor):
        UserModel = apps.get_model("accounts", "User")
        EmailMoldel = apps.get_model("allauth.account", "EmailAddress")

        for email in EmailMoldel.objects.all():
            if not email.primary:
                email.delete()

    operations = [
        migrations.RunPython(
           create_allauth_email_records_for_existing_users, reverse_code= reverse_func
        )
    ]

accounts/models.py:

from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils.translation import gettext_lazy as _

class User(AbstractUser):
    email = models.EmailField(_('email address'), unique=True)

settings file: project/settings/base.py


from pathlib import Path
import os

# Build paths inside the project like this: BASE_DIR / 'subdir'.
# + add new ".parent" after creating new directorty for settings
BASE_DIR = Path(__file__).resolve().parent.parent.parent  

PROJECT_DIR = Path(__file__).resolve().parent.parent

# Quick-start development setings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '********************'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['*']

STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'



# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.humanize',    
    
    # 3rd party apps
    "crispy_forms",
    "crispy_bootstrap5",
    'rest_framework',
    'djmoney',
    'stripe',
    'background_task',
    'mathfilters',
    'allauth', # migration for account and socialaccount
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.github',
    'allauth.socialaccount.providers.google',
    'allauth.socialaccount.providers.twitter',

    
    # project apps
    'members',
    'accounts',
    'eventsApi',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    "whitenoise.middleware.WhiteNoiseMiddleware", # whitenoise
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',


]
SITE_ID = 1
ROOT_URLCONF = 'project.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
           os.path.realpath (PROJECT_DIR) + '/templates/',
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',

                 
            ],
        },
    },
]

WSGI_APPLICATION = 'project.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True

# cors origin settings
CORS_ORIGIN_ALLOW_ALL = True


# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

# email backend
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'


# set auth user model
AUTH_USER_MODEL = 'accounts.User'

# custom auth backend
AUTHENTICATION_BACKENDS = [
    'accounts.custom_backends.EmailAuthBackend',

     # `allauth` specific authentication methods, such as login by e-mail
    'allauth.account.auth_backends.AuthenticationBackend',
    
    ]

# set social auth adapter
SOCIALACCOUNT_ADAPTER = "accounts.allauth_custom.social_auth_adapter.SocialAccountAdapter"
ACCOUNT_EMAIL_REQUIRED = True
SOCIALACCOUNT_QUERY_EMAIL = True
ACCOUNT_AUTHENTICATION_METHOD = 'username_email'
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
ACCOUNT_LOGOUT_ON_GET = True

ACCOUNT_FORMS = {'signup': 'accounts.allauth_custom.custom_forms.CustomSignupForm'}

#set default login redirect url 
LOGIN_REDIRECT_URL = '/profile/dashboard/'

# djnago cripsy forms settings
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"

CRISPY_TEMPLATE_PACK = 'bootstrap5'


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/

STATIC_URL = '/static/'

STATICFILES_FINDERS = [
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]

STATICFILES_DIRS = [
    os.path.join(PROJECT_DIR, 'static'), # other project based static files
    os.path.join(BASE_DIR, 'static_vue'), # static files compiled from vue
]

STATIC_ROOT = os.path.join(BASE_DIR, 'static_root')

# Django-background-tasks config
BACKGROUND_TASK_RUN_ASYNC = True
MAX_RUN_TIME = 300

Here is the error and stack trace

Traceback (most recent call last):
  File "C:UsersUSER[**]manage.py", line 22, in <module>
    main()
  File "C:UsersUSER[**]manage.py", line 18, in main    execute_from_command_line(sys.argv)
  File "C:UsersUSER[**]myEnvlibsite-packagesdjangocoremanagement__init__.py", line 419, in execute_from_command_line
    utility.execute()
  File "C:UsersUSER[**]myEnvlibsite-packagesdjangocoremanagement__init__.py", line 413, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "C:UsersUSER[**]myEnvlibsite-packagesdjangocoremanagementbase.py", line 354, in run_from_argv
    self.execute(*args, **cmd_options)
  File "C:UsersUSER[**]myEnvlibsite-packagesdjangocoremanagementbase.py", line 398, in execute
    output = self.handle(*args, **options)
  File "C:UsersUSER[**]myEnvlibsite-packagesdjangocoremanagementbase.py", line 89, in wrapped
    res = handle_func(*args, **kwargs)
  File "C:UsersUSER[**]myEnvlibsite-packagesdjangocoremanagementcommandsmigrate.py", line 92, in handle
    executor = MigrationExecutor(connection, self.migration_progress_callback)
  File "C:UsersUSER[**]myEnvlibsite-packagesdjangodbmigrationsexecutor.py", line 18, in __init__
    self.loader = MigrationLoader(self.connection)
  File "C:UsersUSER[**]myEnvlibsite-packagesdjangodbmigrationsloader.py", line 53, in __init__
    self.build_graph()
  File "C:UsersUSER[**]myEnvlibsite-packagesdjangodbmigrationsloader.py", line 259, in build_graph
    self.graph.validate_consistency()
  File "C:Users[**]myEnvlibsite-packagesdjangodbmigrationsgraph.py", line 195, in validate_consistency
    [n.raise_error() for n in self.node_map.values() if isinstance(n, DummyNode)]
  File "C:Users[**]myEnvlibsite-packagesdjangodbmigrationsgraph.py", line 195, in <listcomp>
    [n.raise_error() for n in self.node_map.values() if isinstance(n, DummyNode)]
  File "C:UsersUSER[**]myEnvlibsite-packagesdjangodbmigrationsgraph.py", line 58, in raise_error
    raise NodeNotFoundError(self.error_message, self.key, origin=self.origin)
django.db.migrations.exceptions.NodeNotFoundError: Migration accounts.0003_create_allauth_email_records_for_existing_users dependencies reference nonexistent parent node ('allauth.account', 
'0002_email_max_length')

The issue appears to be from this snippet in the migration file:

dependencies = [
        ('accounts', '0002_delete_profile'),
        ('allauth.account', '0002_email_max_length')
    ]

Apparently ‘allauth.account’ is not a valid parent node. However, removing it from the dependencies throws the error described here: https://docs.djangoproject.com/en/4.1/howto/writing-migrations/#migrating-data-between-third-party-apps.

Other threads I have found on here suggest deleting the migration or dropping the db which are not valid options for me. How do I write a custom data-migration that can find the ‘allauth.account’ app?

Thanks in advance!!!.

Asked By: Del_Wiz

||

Answers:

is the package installed in your environment?

pip install django-allauth

Answered By: PresidentNick

So I had this sorted and it was simpler than I thought.

Apparently, I just had to use "account" instead of "allauth.account" in the migrations file. This might not necessarily seem obvious (at least, it wasn’t to me) because if one looks at the django-allauth library, account is very much like a nested app. Also, using "allauth.account" works in settings.INSTALLED_APPS.

New accounts/migrations/0003_create_allauth_email_records_for_existing_users.py:

# Generated by Django 3.2.11 on 2022-12-09 10:56

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('accounts', '0002_delete_profile'),
        ('account', '0002_email_max_length') # ***changed here
    ]

    def create_allauth_email_records_for_existing_users(apps, schema_editor):

        UserModel = apps.get_model("accounts", "User")
        EmailMoldel = apps.get_model("account", "EmailAddress") # ***changed here

        for user in UserModel.objects.all():
            email_record = EmailMoldel.objects.filter(email=user.email).first()
            
            if email_record == None:

                if user.is_active:
                    email_verified = True
                else:
                    email_verified = False

                new_email = EmailMoldel.objects.create(
                    user = user,
                    email = user.email,
                    verified = email_verified,
                    )
    
    def reverse_func(apps, schema_editor):
        UserModel = apps.get_model("accounts", "User")
        EmailMoldel = apps.get_model("account", "EmailAddress") # ***changed here

        for email in EmailMoldel.objects.all():
            if not email.primary:
                email.delete()

    operations = [
        migrations.RunPython(
           create_allauth_email_records_for_existing_users, reverse_code= reverse_func
        )
    ]

Answered By: Del_Wiz