Mathieu Agopian : Django1.5 : passer au Configurable User Model

Depuis la version 1.5 de Django, il est possible d'utiliser un Configurable User Model en lieu et place de django.contrib.auth.User.

Cela permet, par exemple, de se passer de proxy model ou encore de fusionner le profil avec l'utilisateur, pour éviter des join dans les requêtes SQL.

Très pratique, et facile à mettre en place sur un projet qui commence juste, mais comment gérer ça en utilisant South sur un projet déjà bien en place ?

Le but est donc de fusionner l'utilisateur et le profil, avec pour aide/contrainte d'utiliser South, autant sur des plateformes existantes (serveur de production, de pré-production) que sur les plateformes de développement : donc les migrations doivent fonctionner sur une création de base, tout autant que sur une migration simple.

Nous allons détailler plusieurs stratégies.

Contexte

Notre projet utilise depuis longtemps un proxy model sur l'utilisateur, ne rajoutant que quelques méthodes. Toutes les données liées à l'utilisateur sont par ailleurs stockées dans un profil, qui est utilisé par le biais de get_profile() (et le setting AUTH_PROFILE_MODULE).

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


class RH2User(User):

    class Meta:
        proxy = True

    ...


class RH2UserProfile(models.Model):
    some_field = models.CharField(max_length=50)
    some_other_field = models.BooleanField()

    def some_method(self):
        ...

Le résultat, une fois le profil fusionné avec l'utilisateur :

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


class RH2User(AbstractUser):
    some_field = models.CharField(max_length=50)
    some_other_field = models.BooleanField()

    def some_method(self):
        ...

Ne pas oublier de fusionner les managers, les méthodes save(), et de dédoublonner les champs ayant le même nom (dans notre cas, RH2UserProfile.last_login a été renommé en RH2User.previous_last_login, étant donné que le modèle auth.User d'origine avait déjà un champ last_login).

Il faut par ailleurs rechercher et remplacer le cas échéant toutes les occurrences de :

La problématique

À partir du moment où le paramètre AUTH_USER_MODEL est renseigné :

Il y a donc principalement deux stratégies pour les migrations South, une fois qu'on a notre modèle RH2User complet (et non plus proxy) ainsi que AUTH_USER_MODEL = 'account.RH2User' dans les paramètres :

Dans les deux cas, il faudra être attentif à l'ordre d'exécution des migrations : toutes les applications ayant une relation avec l'utilisateur devront dépendre de la migration initiale qui crée la table auth_user ou account_rh2user.

Dans le deuxième cas, il faudra de plus que la première des migrations suivant le renommage, pour chaque application, dépende de cette migration.

Création de account_rh2user et modification des migrations

Le plus simple est de créer une migration de schéma pour avoir le code nécessaire à la migration 0001_initial de l'application account :

$ python manage.py schemamigration account

Il suffit alors de recopier le code de la migration créée, de le rajouter au fichier account/migrations/0001_initial.py, puis de supprimer cette nouvelle migration qui ne sera pas utilisée.

Il faut ensuite modifier chacune des migrations, en prenant exemple sur ce qui a été fait sur django-oauth2-provider.

Il reste la problématique de la migration des serveurs déjà en production (qui ont déjà un certain nombre de migrations effectuées, et une base de donnée à conserver). Une solution serait de créer une migration de données et de tester l'existence de la table auth_user, et le cas échéant de dupliquer les données dans la table account_rh2user.

N'ayant pas testé cette solution, je ne peux la garantir.

Création de auth_user puis renommage

C'est la solution que nous avons choisi, étant donné le nombre de migrations que nous avons (près d'une centaine), qu'il aurait fallu modifier une à une, ainsi que le soucis de migration des serveurs déjà en production.

Il faut dans l'ordre :

Conclusion

Le plus compliqué dans toute cette histoire est la gestion de dépendances entre les migrations.

Une autre solution non évoquée aurait été de repartir de 0 pour les migrations : supprimer toutes les migrations existantes, ainsi que la table south_migrationhistory, puis reconvertir toutes les applications à South :

$ python manage.py convert_to_south ....

L'avantage est qu'il n'y a alors aucun soucis de dépendances entre les migrations, et qu'on repart de quelque chose de propre.

Les inconvénients sont multiples : gérer une migration (à la main?) pour les plateformes en cours d'utilisation, impossibilité de retourner en arrière automatiquement, perte de l'historique...

Il y a une autre possibilité (à tester !) qui consiste à spécifier l'attribut db_table = 'auth_user' dans la Meta de notre nouveau modèle RH2User, pour qu'il utilise exactement la même table. En théorie, il n'y a alors pas besoin de migration, mais il reste à gérer la fusion du profil dans l'utilisateur.