I'm documenting[*] the process I followed but have come to a gray area and need some expert assistance.

Having achieved a working system with a new database table containing all the existing data and now called ...

    public.common_user

... I notice that it has a user_id sequence called ...

    public.auth_user_id_seq

... along with similarly named sequences for similarly named tables which now exist as common_...  tables.

I can easily enough rename those sequences so they match the owning tables - and I want to do so - but the question is should it be done via raw SQL within the migration system?

Is there a proper way to align the names?

Another way (which I've tested) is to edit a database dump and reload that.

What is the correct approach? Is it even legal (ORM rules) to rename the table?

Thanks for any advice

Mike




[*] https://www.caktusgroup.com/blog/2019/04/26/how-switch-custom-django-user-model-mid-project/ by Tobias McNulty as a variation of Django docs https://docs.djangoproject.com/en/2.2/topics/auth/customizing/#changing-to-a-custom-user-model-mid-project

TL;DR

Custom user documentation (UNFINISHED DRAFT)

Based on Tobias McNulty's "How to Switch to a Custom Django User Model Mid-Project"[1]

Assumptions
- Existing project without a custom user model
- All migrations are up to date and deployed in production
- Existing auth_user table has data which must be kept
- Relationships with other models exist and must be kept

Strategy

There are two strategies. One is to throw away history, delete all migrations, empty (truncate) the migrations table and start again.[2] Very attractive if the project repo is young and history is fresh and therefore disposable.

The other strategy is to use the migration system to make the switch, ensuring nothing breaks. That is the Tobias approach and the one documented here.

This strategy is a genuine bottleneck. All pending changes must be completed and fully deployed before starting and no planned changes are commenced until after the switch is fully deployed.

Objective

- Completely align development, staging and production systems
- Series of new migrations
- Series of sql commands to adjust content_type records
- Series of scripts to execute migrations and sql commands

Process

1. Ensure all references to User everywhere (including 3rd party apps) are indirect[3][4]. Ensure all code concerned with access control and relying on users or user authentication is covered by unit tests as far as possible and all tests are passing.


2. Make migrations and apply them. Ensure development, staging and production systems are all synchronised and each database (structure) is identical. This starts the bottleneck.


3. Start a new app or use an existing one which has no models.py. The reason there needs to be initially no models is the migration which creates the custom user must be '0001_initial.py' to persuade Django there are no dependency issues. In this documentation I call the app "common" but it can be anything eg "proj_user", "accounts" etc.

    python manage.py startapp common


4. Write a new common/models.py ...

    from django.db import models
    from django.contrib.auth.models import AbstractUser


    class User(AbstractUser):
        """ Retain the model name 'User' to avoid unnecessary refactoring during         the switchover process. Make no other changes here until after complete
        deployment to production.
        """
        class Meta:
            # use the existing Django users table for the initial migration
            db_table = "auth_user"


5. Write a new common/admin.py

    from django.contrib import admin
    from django.contrib.auth.admin import UserAdmin
    from .models import User


    admin.site.register(User, UserAdmin)


6. Include the new app in settings.py among other local apps and adjust AUTH_USER_MODEL ...

    INSTALLED_APPS = [
        # ...
        'common',
    ]

    AUTH_USER_MODEL = 'common.User'


7. Make the initial migration to create the new User model ...

    python manage.py makemigrations  --> common/migrations/0001_initial.py


8. Write a script to deploy (rather than execute) the migration as follows ... [5]

Windows 10 - PostgreSQL 10 ...

    :: deploy_migration.bat
    :: defeat Django's sanity check by manually entering that migration in the database     :: and for good measure update content_types to avoid further Django sanity checks

    set host=dev_laptop
    set dbowner=whoever

    psql --username=%dbowner% --port=5432 --dbname=ssds --host=%host% --command "INSERT INTO public.django_migrations (app, name, applied) VALUES ('common', '0001_initial', CURRENT_TIMESTAMP)";

    psql --username=%dbowner% --port=5432 --dbname=ssds --host=%host% --command "UPDATE public.django_content_type SET app_label = 'common' WHERE app_label = 'auth' and model = 'user'";


Linux (Ubuntu 18.04) - PostgreSQL 10 ...

    # fetch_ssds.py [6]
    # These next two psql command lines fake an initial migration to create
    # a custom-user in a pre-existing project and adjust content_types to
    # prevent Django from barfing if it automatically tried to add them
    #
    import os

    host="dev_laptop"
    dbowner="whoever"

    cmd = "sudo psql --username=%s --port=5432 --dbname=ssds --host=%s --command \"INSERT INTO public.django_migrations (app, name, applied) VALUES ('common', '0001_initial', CURRENT_TIMESTAMP);\"" % (dbowner, host)
    #
    os.system(cmd)
    #
    cmd = "sudo psql --username=%s --port=5432 --dbname=ssds --host=%s --command \"UPDATE public.django_content_type SET app_label = 'common' WHERE app_label = 'auth' and model = 'user';\"" % (dbowner, host)
    #
    os.system(cmd)


9. After deploying with the above technique in development run all unit tests and correct any errors or failures both in project code and in the above scripts. Refresh the development database (structure) from production (again) and repeat step 8 above and test again. All unit tests must pass. Important - repeat until perfect.


10. Deploy to staging using one of the above scripts from step 8, modified for the staging environment. When perfectly deployed on staging and all testing is done, ensure production is backed up then deploy to production in similar fashion. This ends the bottleneck.

The balance of this process is optional


11. To be written after resolving sequence naming questions


12. Edit common/models.py then makemigrations to rename the table of existing users from auth_user to common_user. Finally migrate to execute the rename to common_user

    class User(AbstractUser):
        """ Retain the model name 'User' to avoid unnecessary refactoring during         the switchover process. Make no other changes here until after complete
        deployment to production.

        Comment out Meta entirely to migrate to the default table name
        """
        pass

        #class Meta:
        #    # use the existing Django users table for the initial migration
        #    db_table = "auth_user"



[1] https://www.caktusgroup.com/blog/2019/04/26/how-switch-custom-django-user-model-mid-project/ by Tobias McNulty as a variation of Django docs https://docs.djangoproject.com/en/2.2/topics/auth/customizing/#changing-to-a-custom-user-model-mid-project

[2] https://code.djangoproject.com/ticket/25313#comment:2 by Aymeric Augustin

[3] https://docs.djangoproject.com/en/2.2/topics/auth/customizing/#referencing-the-user-model

[4] Note that get_user_model() cannot be called at the module level in any models.py file (and by extension any file that a models.py imports), since you'll end up with a circular import. Generally, it's easier to keep calls to get_user_model() inside a method whenever possible (so it's called at run time rather than load time), and use settings.AUTH_USER_MODEL in all other cases. This isn't always possible (e.g., when creating a ModelForm), but the less you use it at the module level, the fewer circular imports you'll have to stumble your way through. (From Tobias [1])

[5] Tobias notes that Django won't permit 'migrate common --fake-initial' if there are other migrations which include settings.AUTH_USER_MODEL

[6] fetch_ssds.py is a comprehensive auto-deployment script. Only the relevant (and simplified) portion is shown.


--
You received this message because you are subscribed to the Google Groups "Django 
users" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to django-users+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-users/93579fbe-d97d-5886-0797-079d8980dbc5%40dewhirst.com.au.

Reply via email to