(2018-01-18) Vitor Freitas => Never use the built-in Django User model directly

Advice

Never user the built-in Django User model directly , even if the built-in Django User implementation fulfill all the requirements of your application.

At least extend the AbstractUser model and switch the AUTH_USER_MODEL on your settings .

Requirements always change .

You may need to customize the User model in the future, and switching the AUTH_USER_MODEL after your application is in production will be very painful.

Mainly because you will need to update all the foreign keys to the User model.

It can be done, but this simple measure (which, honestly, is effortless in the beginning of the project) can save you from headaches in the future.

 1 class User(AbstractUser):
 2   USER_TYPE_CHOICES = (
 3       (1, 'student'),
 4       (2, 'teacher'),
 5       (3, 'secretary'),
 6       (4, 'supervisor'),
 7       (5, 'admin'),
 8   )
 9
10   user_type = models.PositiveSmallIntegerField(choices=USER_TYPE_CHOICES)
 1 class Role(models.Model):
 2   '''
 3   The Role entries are managed by the system,
 4   automatically created via a Django data migration.
 5   '''
 6   STUDENT = 1
 7   TEACHER = 2
 8   SECRETARY = 3
 9   SUPERVISOR = 4
10   ADMIN = 5
11   ROLE_CHOICES = (
12       (STUDENT, 'student'),
13       (TEACHER, 'teacher'),
14       (SECRETARY, 'secretary'),
15       (SUPERVISOR, 'supervisor'),
16       (ADMIN, 'admin'),
17   )
18
19   id = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, primary_key=True)
20
21   def __str__(self):
22       return self.get_id_display()
23
24
25 class User(AbstractUser):
26   roles = models.ManyToManyField(Role)

Description

  1"""Définition d'un modèle User personnalisé User(table auth_user).
  2
  3Django permet de surcharger le modèle d’utilisateur par défaut en attribuant
  4une valeur au réglage AUTH_USER_MODEL se référant à un modèle personnalisé
  5
  6    AUTH_USER_MODEL = 'accounts.User'
  7
  8Voir
  9
 10- https://docs.djangoproject.com/fr/1.11/ref/contrib/auth/
 11- https://docs.djangoproject.com/fr/1.10/topics/auth/customizing/#auth-custom-user
 12
 13- Source: https://makina-corpus.com/blog/metier/2014/combiner-une-authentification-ldap-et-lauthentification-classique-django
 14
 15
 16"""
 17import logging
 18
 19from django.contrib.auth.models import AbstractUser
 20from django.contrib.auth.models import User
 21from django.db import models
 22from django.utils.translation import ugettext as _
 23
 24# https://docs.djangoproject.com/en/dev/topics/auth/customizing/#extending-the-existing-user-model
 25
 26# Get an instance of a logger
 27logger = logging.getLogger(__name__)
 28
 29
 30class CustomUser(AbstractUser):
 31    """Un utilisateur authentifié intranet (auth_user)
 32
 33    .. seealso:: https://code.djangoproject.com/ticket/25313
 34
 35
 36    I did it at least twice. Unfortunately I don't remember all the details.
 37
 38    I think a reasonable procedure is:
 39
 40    - Create a custom user model identical to auth.User, call it User
 41      (so many-to-many tables keep the same name) and
 42      set db_table='auth_user' (so it uses the same table)
 43    - Throw away all your migrations
 44    - Recreate a fresh set of migrations
 45    - Sacrifice a chicken, perhaps two if you're anxious; also make a backup
 46      of your database
 47    - Truncate the django_migrations table
 48    - Fake-apply the new set of migrations
 49    - Unset db_table, make other changes to the custom model,
 50      generate migrations, apply them
 51
 52    It is highly recommended to do this on a database that enforces
 53    foreign key constraints.
 54    Don't try this on SQLite on your laptop and expect it to work on
 55    Postgres on the servers!
 56
 57    - username
 58
 59    """
 60
 61    class Meta:
 62        """Meta class accounts.User."""
 63
 64        managed = True
 65        db_table = "auth_user"
 66        ordering = ["username"]
 67        verbose_name = _("Comptes utilisateur")
 68        verbose_name_plural = _("Comptes utilisateur")
 69
 70    from_ldap = models.BooleanField(
 71        editable=False,
 72        default=False,
 73        help_text=_("Issu de la base LDAP ?"),
 74        verbose_name=_("Issu de la base LDAP ?"),
 75    )
 76
 77    def __str__(self):
 78        infos = f"{self.first_name} {self.last_name}"
 79        return infos
 80
 81    def save(self, *args, **kwargs):
 82        """Sauvegarde de User."""
 83        try:
 84            logger.info("User.save({})".format(self))
 85            super().save(*args, **kwargs)
 86            logger.info("... {} was successfully saved.".format(self))
 87        except Exception as error:
 88            logger.info("...{} was NOT saved. PROBLEM:{}".format(self, error))
 89
 90    def updated_from_ldap(self, nom, prenom, email):
 91        """Les informations en provenance de LDAP different-elles
 92        des informations courantes ?
 93        """
 94        updated_from_ldap = False
 95
 96        nom = nom.strip()
 97        prenom = prenom.strip()
 98
 99        identifiant = self.username
100        sprenom = self.first_name
101        snom = self.last_name
102        semail = self.email
103
104        # analyse des cas de modifications
105
106        # 1) le nom, prénom ou email a été changé
107        if snom != nom:
108            self.last_name = nom
109            logger.info(f"{identifiant} a changé de nom <{snom}>=><{nom}>")
110            updated_from_ldap = True
111
112        if sprenom != prenom:
113            self.first_name = prenom
114            logger.info(f"Le prénom de {identifiant} a changé {sprenom}=>{prenom}")
115            updated_from_ldap = True
116
117        if semail != email:
118            self.email = email
119            logger.info(
120                f"L'adresse couriel de {identifiant} a changé {semail}=>{email}"
121            )
122            updated_from_ldap = True
123
124        # 2) le email passe à 'nomail@id3.eu' ce qui signifie que cet utilisateur
125        # ne fait plus partie d'id3
126        employe_id3 = True
127        if (self.is_active) and (email == "nomail@id3.eu"):
128            # l'utilisateur n'est plus actif
129            self.is_active = False
130            self.is_staff = False
131            self.email = email
132            logger.info(f"{identifiant} n'est plus actif")
133            employe_id3 = False
134            updated_from_ldap = True
135
136        if updated_from_ldap:
137            try:
138                self.save()
139                if not employe_id3:
140                    # l'employé ne fait plus partie d'id3
141                    self.a_quitte_id3()
142
143            except:
144                updated_from_ldap = False
145
146        return updated_from_ldap
147
148    def a_quitte_id3(self):
149        """Départ d'un employé."""
150        from employes.models import Employe
151
152        try:
153            employe = Employe.objects.get(user=self)
154            employe.a_quitte_id3()
155            return employe
156        except Exception as error:
157            logger.error("No employe for LDAP user: {self}")
158            return None
159
160    def get_employe(self):
161        """Retourne l'employé associé à l'utilisateur LDAP."""
162        from employes.models import Employe
163
164        # logger.info(f"account <{self}>")
165        try:
166            employe = Employe.objects.get(login=self.username)
167            return employe
168        except Exception as error:
169            logger.error(f"No employe for user: <{self}>")
170            return None
171
172    def get_employe_id(self):
173        """Retourne l'id de l'employé associé à l'utilisateur LDAP."""
174        from employes.models import Employe
175
176        employe_id = None
177        employe = self.get_employe()
178        if employe != None:
179            employe_id = employe.id
180
181        return employe_id