4 metodi per estendere il comportamento della classe User in Django

Django Web Framework

Ultimamente mi sono trovato ad affrontare un problema di cui ci sono svariate soluzioni: estendere il comportamento della classe User.

 

Premessa: NON pensate che l’ORM di Django abbia risolto magicamente tutti i problemi. Purtroppo gli RDBMS non supportano di default gli oggetti/classi/ereditarietà, per cui in un modo o nell’altro bisogna fare una traduzione concettuale: entità <-> oggetto. Spesso tale traduzione crea un po’ di ambiguità: il campo “indirizzo” è di User o no? Il campo “lavoro” è dell’entità Utente, ma come estendo User?

Esistono svariati metodi per farlo:

  • Usare get_profile() e una classe esterna associata a User;
  • Usare un Proxy Model;
  • Subclassare User;
  • Monkey-patching con il metodo add_to_class;

1. user_instance.get_profile()

Il primo metodo è quello consigliato dai creatori di Django e in teoria quello che bisognerebbe utilizzare. Innanzitutto bisogna creare un modello che contenga metodi e attributi che estendono il comportamento dell’Utente. Per convenzione lo chiameremo UserProfile:

class UserProfile(models.Model):
    user = models.ForeignKey(User, unique=True)
    url = models.URLField()
    home_address = models.TextField()
    phone_numer = models.PhoneNumberField()

Poichè si tratta di una relazione uno-a-uno, bisognerà creare un riferimento a User.
Con questo metodo sarà creata una nuova tabella chiamata user_profile contenente i campi specificati. Per fare in modo che venga istanziato un oggetto UserProfile al momento della creazione di un utente, dovremo usare il segnale post_saved:

def create_profile(sender, instance, created, **kwargs):
    if created:
        profile, created = UserProfile.\
                 objects.get_or_create(user=instance)
post_save.connect(create_profile, sender=User)

Infine, poichè get_profile non sa a quale modello riferirsi, dovremo specificare l’opzione AUTH_PROFILE_MODULE in settings.py. Ad esempio: AUTH_PROFILE_MODULE = ‘accounts.UserProfile’.

Pro:

  • Implementazione facile.
  • Accoppiamento debole: la classe User non ha bisogno di UserProfile per esistere o per effettuare determinate azioni. Non è a conoscenza di tale modello. Lo usa solo come valore di ritorno di una funzione. (è possibile immaginare un Factory, che però restituisce sempre e solo un unico valore).
  • Forte coesione: User è responsabile solo di ciò che essa rappresenta. Non aggiunge nè rimuove comportamenti dopo che le abbiamo associato UserProfile.

Contro:

  • Creata una tabella per user_profile.
  • Query aggiuntiva ogni volta che bisogna accedere a un’istanza di UserProfile.
  • Qualcuno storce il naso quando bisogna chiamare un metodo di UserProfile, perchè bisogna scrivere un codice come questo per ottenerne un campo:
user = User.objects.get(pk=1)
profile = user.get_profile()
print profile.home_address

2. Usare un Proxy Model

Un altro metodo per estendere il comportamento di User è quello di usare un Proxy Model. Come per user_instance.get_profile(), anche qui non andremo a modificare User, perchè in realtà ciò che faremo sarà creare una classe che farà da proxy per tale modello.

Basterà aggiungere i metodi di cui abbiamo bisogno e usare, ogni volta che lo desideriamo, il nostro proxy invece di User. Ad es.:

class ProxyUser(User):
    class Meta:
        proxy = True

def get_username_and_email_as_tuple(self):
    return self.username, self.email

Poichè condividono la stessa interfaccia, la nostra classe ProxyUser potrà usare anche i metodi di User. Inoltre, dato che si tratta di un Proxy, sarà possibile effettuare modifiche e aggiornamenti sul modello non proxy (quindi su User).

Un uso che si può fare di un ProxyModel è il seguente:

class OrderedUser(User):
    class Meta:
        ordering = ["username"]
        proxy = True

In pratica se desideriamo ottenere la lista ordinata degli utenti in base allo username sarà sufficiente chiamare OrderedUser.objects.all().

Un altro motivo per cui si può usare un Proxy è quello di aggiungere un manager personalizzato.

Basterà creare il manager e associarlo al Proxy:

class ProxyUserManager(models.Manager):
... # aggiungi i metodi che vuoi

class ProxyUser(User):
    objects = ProxyUserManager()
    class Meta:
        proxy = True

Pro:

  • Implementazione facile.
  • Permette di avere proxy dedicati a singoli comportamenti (ordering, …).
  • Poichè usiamo un’interfaccia, User, c’è un accoppiamento molto debole.
  • Facilità di estensione con nuove applicazioni: un’app può fornire un proxy per User che permetta di effettuare delle operazioni senza la necessità di dover cambiare l’infrastruttura sottostante. Un proxy del genere favorisce dunque l’estensibilità.
  • Non bisogna effettuare 2 query (o join) per ottenere un’istanza di User.

Contro:

  • Non permette di aggiungere campi a User.
  • Non permette di accedere alle nuove funzioni tramite le istanze di User: bisogna usare i singoli Proxy.

Per maggiori informazioni, rimando alla documentazione ufficiale.

3. Subclassare User

E’ il metodo forse più intuitivo che si può immaginare:  “Abbiamo bisogno di estendere il comportamento di User? Creiamo una sottoclasse di User e rendiamola quella di default”. Django permette anche questo, nonostante, come già spiegato, bisognerebbe preferire uno dei due metodi precedenti.

Pensateci più di una volta prima di applicare questo metoodo. Come spiega James Bennet, uno degli sviluppatori di Django:

I’d wager that probably 90% or more of the things people say they want to do with subclasses could be better accomplished by instead defining a related model and linking it back with a unique foreign key.

E in seguito spiega il motivo per cui in sostanza bisognerebbe preferire il primo metodo:

I’ve seen a lot of people say they want to subclass User not because they want to change the types of auth-related information, but because they want to add a field for the user’s website URL, or a short “bio” field, or lots of other useful information related to the user.

Did you spot the key word in that last phrase? Other useful information related to the user. That should be a dead giveaway that what we want in the database is a separate table where each row relates back to a row in the auth table. And in OO terms, the user’s website, bio and other information aren’t really part of their authentication and access controls and really should be encapsulated in their own object. So in OO terms what we want is a separate class where each instance has an attribute pointing to an instance of User.

Perché dunque aggiungere responsabilità alla classe User se essa è nata per gestire informazioni come username, email, password, tipologia di utenza (di base) – in sostanza informazioni auth-related?

Se sei ancora qui a leggere vuol dire che sai cosa stai facendo: hai proprio bisogno di cambiare le modalità di autenticazione degli utenti e magari hai bisogno di nuovi campi per farlo :) Vabè, so che lo sapevi già: era solo per ricordartelo ;)

Il codice l’ho rubacchiato da un blog.

from django.contrib.auth.models import User, UserManager

class CustomUser(User):
    timezone = models.CharField(max_length=50,
                                default='Europe/London')
objects = UserManager()

A questo punto non abbiamo fatto altro che estendere User,  ma non sarà possibile usare di default CustomUser invece di User. Ci sarà bisogno del classico CustomUser.objects.metodo().

Dovremo creare, guarda caso, un backend di autenticazione.

from django.conf import settings
from django.contrib.auth.backends import ModelBackend
from django.core.exceptions import ImproperlyConfigured
from django.db.models import get_model

class CustomUserModelBackend(ModelBackend):
    def authenticate(self, username=None, password=None):
        try:
            user = self.\
                 user_class.objects.get(username=username)
            if user.check_password(password):
                return user
        except self.user_class.DoesNotExist:
            return None

    def get_user(self, user_id):
        try:
            return self.user_class.objects.get(pk=user_id)
        except self.user_class.DoesNotExist:
            return None
    @property
    def user_class(self):
        if not hasattr(self, '_user_class'):
            self._user_class = get_model(
            *settings.CUSTOM_USER_MODEL.split('.', 2))
            if not self._user_class:
                raise ImproperlyConfigured(
                          'Could not get custom user model')
        return self._user_class

Infine, bisognerà dire a Django che deve usare questo nuovo backend e la nuova classe CustomUser di default. Modifichiamo settings.py in questo modo:

AUTHENTICATION_BACKENDS = (
'myproject.auth_backends.CustomUserModelBackend',
)

CUSTOM_USER_MODEL = 'accounts.CustomUser'

Fine. Ora non vi resta che piangere cominciare a usare la nuova implementazione.

Pro:

  • Idea intuitiva.
  • Ottimo per aggiungere campi/metodi auth-related.
  • Una sola tabella per l’entità.
  • Una sola query per accedere all’istanza di User.

Contro:

4. Monkey patching

Mi è capitato di leggere dei sorgenti (askbot/models/__init__.py) e inizialmente non capivo cosa fosse quell’add_to_class ripetuto più volte. Non sapevo fosse un metodo di ModelBase. Conoscevo, infatti, solo contribute_to_class e, a dire il vero, pensavo avessero cambiato il nome nella nuova versione (cosa strana, ma può succedere…). Sostanzialmente, comunque, non cambia quasi nulla. In pratica tali metodi permettono di aggiungere attributi/metodi alla classe. Sempre in pratica, quando una cosa del genere avviene a runtime, viene definita “monkey patching“.

Basta aggiungere codice ai propri file per i modelli (models.py per esempio)

User.add_to_class('get_absolute_url', user_get_absolute_url)
User.add_to_class('get_profile_url', get_profile_url)
# altro codice
def user_get_absolute_url(self):
    return self.get_profile_url()
def get_profile_url(self):
    """Returns the URL for this User's profile."""
    return reverse('user_profile',
                   kwargs={
                           'id' : self.id,
                           'slug' : slugify(self.username)
                          }
                  )

e chiamare i metodi sulle singole istanze di User.

Pro:

  • Implementazione facile.
  • Nessuna aggiunta di tabelle.
  • Nessuna query aggiuntiva.

Contro:

Riferimenti Esterni

Di seguito trovate alcuni riferimenti a cui ho dato un’occhiata per approfondire l’argomento.

  • Valerio Maggio

    Complimenti Markon ;)

    Bella guida, completa ed esaustiva!
    Prima di leggere pensavo che la possibilita` del Proxy Model avresti potuto non citarla ma sono stato felicemente smentito!
    ;) Grande!

    • Anonimo

      Grazie Valerio! Ho in mente anche altre cose su Django. Spero solo di averne il tempo…
      Per quanto riguarda il Proxy Model..che dire? Mal fidato. hahahaha