Controllo dei segnali di Django con i gestori di contesto

Ti è mai successo?

“Oh, sto provando a testare un po ‘di codice in uno dei miei modelli Django ma il segnale post_save sta eseguendo un comportamento indesiderato”

I gestori di contesto potrebbero essere in grado di aiutarti! Sto sviluppando con Django da 2 anni e, man mano che la mia conoscenza delle diverse caratteristiche è cresciuta, sono aumentati anche i punti critici nell’implementazione della miriade di aspetti che il framework ha da offrire. Mentre l’API dei segnali Django offre una grande flessibilità nella creazione dei modelli, può essere difficile testare il comportamento dei modelli in modo isolato.

Per iniziare, dovremmo porre la domanda “Cosa ci offrono i segnali?” I segnali consentono a un ingegnere di impostare le funzioni di richiamata quando si verifica un evento particolare. Un esempio è il segnale post_save di Django ORM, che viene attivato dopo il salvataggio di un modello Django. Le offerte complete dell’API dei segnali sono disponibili qui: https://docs.djangoproject.com/en/2.1/topics/signals/

Il processo di base per incorporare i segnali nel tuo progetto Django consiste in due semplici passaggi: scrivere una funzione di callback che riceve un particolare messaggio ed esegue una logica con quell’evento; registrare la funzione di richiamata con il segnale Django che si desidera avere eseguito la funzione su evento. Ad esempio, considera il caso in cui potresti voler tracciare il dispositivo a cui un utente nel tuo sistema ha effettuato l’accesso. Il tuo modello.py sarebbe simile a:

  utente di classe (AbstractUser): 
# Utente nel nostro sistema
passclass DeviceLogin (BaseModel):
# Dispositivi diversi da cui un utente potrebbe potenzialmente accedere
DESKTOP = 'DSK'
TELEFONO = 'PHN'
TABLET = 'TBL' LOGIN_NAME_CHOICES = [
(DESKTOP, "Desktop"),
(TELEFONO, "Telefono"),
(TABLET, "Tablet"),
] type = models.CharField (
max_length = 3,
scelte = LOGIN_NAME_CHOICES,
db_index = True,
unica = True
) total_logins = models.IntField () classe UserDeviceLogin (BaseModel):
# Mappa del conteggio di accesso dell'utente al dispositivo
user = models.ForeignKey (Utente, on_delete = models.CASCADE)
login = models.ForeignKey (DeviceLogin, on_delete = models.CASCADE)
login_count = models.IntField () classe Meta:
unique_together = ('user', 'login')

Quando viene creato un utente, si desidera creare i record iniziali nella tabella UserDeviceLogin per iniziare, quindi incrementare il valore del record corrispondente ogni volta che un utente accede. È possibile creare il record iniziale con segnali Django in questo modo:

  da django.db.models.signals import post_save 
da account.models importa utente, DeviceLogin, UserDeviceLogin
def user_post_save (mittente, istanza, creato, * args, ** kwargs):
# Segnale per elaborare il segnale User.post_save
se creato:
per login_type in DeviceLogin.objects.all (). iterator ():
UserDeviceLogin.objects.create (
utente = esempio,
login = LOGIN_TYPE,
login_count = 0
)
post_save.connect (
user_post_save,
mittente = utente,
dispatch_uid = 'user_post_save_create_user-device-login'
)

Per spiegare: il metodo user_post_save è impostato per adattarsi alla firma di un callback del segnale Django. Accetta un sender , instance , un valore booleano che indica se il record è stato created o meno e argomenti e argomenti di parole chiave arbitrari con cui non facciamo nulla. Presuppone (giustamente!) Che l’ instance che gli viene passata sia uno di User , e per ogni record nella tabella DeviceLogin crea un record UserDeviceLogin corrispondente con il conteggio delle occorrenze di accesso impostato su 0. Ciò semplifica l’impostazione delle mappature di tipi stranieri predeterminati per gli utenti quando un utente viene creato nel database.

Quindi nella tua app.py tutto ciò che dovresti fare è registrare il signal.py

modulo una volta pronta l’app:

  # Potrebbe anche essere necessario includerlo nei tuoi account / __ init__.py: 
# default_app_config = 'accounts.apps.AccountsConfig'da django.apps import AppConfigclass AccountsConfig (AppConfig):
nome = "account"

def ready (auto):
# Registra segnali
import account.signals

Tutto bene, vero?

“Ma resisti! Esiste un vincolo unique_together sul modello UserDeviceLogin. Dovremmo scrivere un test per questo! ”

Ottimo lavoro tu! Sì, dovremmo testare il comportamento unique_together per non introdurre un serpente nell’erba che rompe l’integrità del nostro database lungo la linea. Il nostro test per questo sarebbe simile a questo:

  class TestUserDevivceLogin (TestCase): 
def test_uniqueness_on_user_device_login (self):
user = User.objects.create (username = 'test_user')
login_type = DeviceLogin.objects.get (name = DeviceLogin.PHONE)
UserDeviceLogin.objects.create (utente = utente, login = login_type)

con self.assertRaises (IntegrityError):
UserDeviceLogin.objects.create (
user = utente,
login = LOGIN_TYPE
)

Questo caso di test crea prima un mapping UserDeviceLogin per un determinato utente, quindi tenta di creare un altro mapping UserDeviceLogin per la stessa coppia utente / tipo. Prevediamo che il test dovrebbe generare un IntegrityError se proviamo a eseguire questa azione, poiché il vincolo unique_together dovrebbe impedire che ciò accada.

Ti incoraggio a capire perché questo non riesce da solo prima di procedere.


Quindi si scopre che questo caso di test genera l’eccezione che vogliamo, ma non dove lo vogliamo! Dopo aver creato il nostro utente di prova, il segnale post_save viene generato per l’ User creato. Ciò creerebbe già un UserDeviceLogin per ogni record DeviceLogin nel database. Quindi, quando andiamo a creare il primo mapping dei dispositivi di accesso, abbiamo già attivato un IntegrityError poiché OGNI record UserDeviceLogin è stato creato per il nostro utente di test.

Quindi sembra che vogliamo disabilitare temporaneamente il segnale di salvataggio post User per questo caso di test. Sfortunatamente, non sembra esserci alcuna possibilità di “disattivare” i segnali Django con un decoratore o un gestore del contesto. Fino ad ora!

L’essenza è qui: https://gist.github.com/nickdibari/dde5a222983fa3e0aa46145646d9b986

In sostanza, questo fornisce un’interfaccia per disabilitare un particolare segnale / callback / accoppiamento mittente per un contesto. Quindi, se abbiamo modificato il nostro test precedente in questo modo:

  da django.db.models.signals import post_savefrom accounts.signals import user_post_saveclass TestUserEmot (TestCase): 
def test_uniqueness_on_user_emot_fields (self):
dispatch_uid = 'user_post_save_create_user-device-login'
con SignalDisconnect (post_save, user_post_save,
Utente, dispatch_uid):
# Il segnale è disabilitato qui dentro!
user = User.objects.create (
username = 'test_user'
)
login_type = DeviceLogin.objects.get (name = DeviceLogin.PHONE)
UserDeviceLogin.objects.create (utente = utente, login = login_type)

con self.assertRaises (IntegrityError):
UserDeviceLogin.objects.create (
user = utente,
login = LOGIN_TYPE
)

Ora il nostro test ha superato! Disabilitando il segnale post_save quando creiamo l’utente, non ci sono record UserDeviceLogin per l’utente, permettendoci di creare una particolare mappatura e testare l’integrità del nostro database.

Spero che sia utile, ho trascorso un bel po ‘di tempo a sbattere la testa contro il muro cercando di capire perché il mio test stava fallendo inaspettatamente prima di rendermi conto che il problema era con il segnale. Felice sviluppo!