Comment définir la valeur par défaut d'un champ de modèle Django sur un appel de fonction / appelable (par exemple, une date relative à l'heure de création de l'objet modèle)

101

ÉDITÉ:

Comment puis-je définir la valeur par défaut d'un champ Django sur une fonction qui est évaluée chaque fois qu'un nouvel objet modèle est créé?

Je veux faire quelque chose comme ce qui suit, sauf que dans ce code, le code est évalué une fois et définit la valeur par défaut à la même date pour chaque objet de modèle créé, plutôt que d'évaluer le code chaque fois qu'un objet de modèle est créé:

from datetime import datetime, timedelta
class MyModel(models.Model):
  # default to 1 day from now
  my_date = models.DateTimeField(default=datetime.now() + timedelta(days=1))



ORIGINAL:

Je veux créer une valeur par défaut pour un paramètre de fonction de telle sorte qu'il soit dynamique et qu'il soit appelé et défini à chaque fois que la fonction est appelée. Comment puis je faire ça? par exemple,

from datetime import datetime
def mydate(date=datetime.now()):
  print date

mydate() 
mydate() # prints the same thing as the previous call; but I want it to be a newer value

Plus précisément, je veux le faire dans Django, par exemple,

from datetime import datetime, timedelta
class MyModel(models.Model):
  # default to 1 day from now
  my_date = models.DateTimeField(default=datetime.now() + timedelta(days=1))
Rob Bednark
la source
La question est mal formulée: elle n'a rien à voir avec les valeurs par défaut des paramètres de fonction. Voyez ma réponse.
Ned Batchelder
Selon le commentaire de @ NedBatchelder, j'ai édité ma question (indiquée comme "EDITED:") et laissé l'original (indiqué comme "ORIGINAL:")
Rob Bednark

Réponses:

140

La question est erronée. Lors de la création d'un champ de modèle dans Django, vous ne définissez pas de fonction, les valeurs par défaut de la fonction ne sont donc pas pertinentes:

from datetime import datetime, timedelta
class MyModel(models.Model):
  # default to 1 day from now
  my_date = models.DateTimeField(default=datetime.now() + timedelta(days=1))

Cette dernière ligne ne définit pas une fonction; il appelle une fonction pour créer un champ dans la classe.

PRE Django 1.7

Django vous permet de passer un appelable par défaut , et il l'invoquera à chaque fois, comme vous le souhaitez:

from datetime import datetime, timedelta
class MyModel(models.Model):
  # default to 1 day from now
  my_date = models.DateTimeField(default=lambda: datetime.now() + timedelta(days=1))

Django 1.7+

Veuillez noter que depuis Django 1.7, l'utilisation de lambda comme valeur par défaut n'est pas recommandée (cf @stvnw commentaire). La bonne façon de faire est de déclarer une fonction avant le champ et de l'utiliser comme appelable dans default_value nommé arg:

from datetime import datetime, timedelta

# default to 1 day from now
def get_default_my_date():
  return datetime.now() + timedelta(days=1)

class MyModel(models.Model):
  my_date = models.DateTimeField(default=get_default_my_date)

Plus d'informations dans la réponse @simanas ci-dessous

Ned Batchelder
la source
2
Merci @NedBatchelder. Ceci est exactement ce que je cherchais. Je ne savais pas que «default» pouvait prendre un appelable. Et maintenant, je vois comment ma question était en effet erronée concernant l'invocation de fonction par rapport à la définition de fonction.
Rob Bednark
25
Notez que pour Django> = 1.7, l'utilisation de lambdas n'est pas recommandée pour les options de champ car elles sont incompatibles avec les migrations. Ref: Docs , ticket
StvnW
4
Veuillez décocher cette réponse, car elle est incorrecte pour Djange> = 1.7. La réponse de @Simanas est absolument correcte et doit donc être acceptée.
AplusKminus
Comment cela fonctionne-t-il avec les migrations? Tous les anciens objets reçoivent-ils une date basée sur l'heure de la migration? Est-il possible de définir une valeur par défaut différente à utiliser avec les migrations?
timthelion
3
Et si je veux passer un paramètre à get_default_my_date?
Sadan A.
59

Faire cela default=datetime.now()+timedelta(days=1)est absolument faux!

Il est évalué lorsque vous démarrez votre instance de django. Si vous êtes sous Apache, cela fonctionnera probablement, car sur certaines configurations, Apache révoque votre application django à chaque demande, mais vous pouvez quand même vous retrouver un jour en train de parcourir votre code et d'essayer de comprendre pourquoi cela n'est pas calculé comme prévu .

La bonne façon de faire est de passer un objet appelable à l'argument par défaut. Il peut s'agir d'une fonction datetime.today ou de votre fonction personnalisée. Ensuite, il est évalué chaque fois que vous demandez une nouvelle valeur par défaut.

def get_deadline():
    return datetime.today() + timedelta(days=20)

class Bill(models.Model):
    name = models.CharField(max_length=50)
    customer = models.ForeignKey(User, related_name='bills')
    date = models.DateField(default=datetime.today)
    deadline = models.DateField(default=get_deadline)
Simanas
la source
21
Si ma valeur par défaut est basée sur la valeur d'un autre champ dans le modèle, est-il possible de passer ce champ en tant que paramètre comme dans quelque chose comme get_deadline(my_parameter)?
YPCrumble
@YPCrumble cela devrait être une nouvelle question!
jsmedmar
2
Nan. Ce n'est pas possible. Vous devrez utiliser une approche différente pour le faire.
Simanas
Comment supprimer à deadlinenouveau le champ tout en supprimant également la get_deadlinefonction? J'ai supprimé un champ avec une fonction pour une valeur par défaut, mais maintenant Django plante au démarrage après avoir supprimé la fonction. Je pourrais modifier manuellement la migration, ce qui serait correct dans ce cas, mais que se passerait-il si vous changiez simplement la fonction par défaut et que vous vouliez supprimer l'ancienne fonction?
beruic
8

Il existe une distinction importante entre les deux constructeurs DateTimeField suivants:

my_date = models.DateTimeField(auto_now=True)
my_date = models.DateTimeField(auto_now_add=True)

Si vous utilisez auto_now_add=Truedans le constructeur, la date / heure référencée par my_date est "immuable" (définie une seule fois lorsque la ligne est insérée dans la table).

Avec auto_now=True, cependant, la valeur datetime sera mise à jour à chaque fois que l'objet est enregistré.

C'était définitivement un piège pour moi à un moment donné. Pour référence, les documents sont ici:

https://docs.djangoproject.com/en/dev/ref/models/fields/#datetimefield

Damzam
la source
3
Vous avez mélangé vos définitions d'auto_now et d'auto_now_add, c'est l'inverse.
Michael Bates
@MichaelBates mieux maintenant?
Hendy Irawan
4

Parfois, vous devrez peut-être accéder aux données du modèle après avoir créé un nouveau modèle utilisateur.

Voici comment je génère un jeton pour chaque nouveau profil d'utilisateur en utilisant les 4 premiers caractères de leur nom d'utilisateur:

from django.dispatch import receiver
class Profile(models.Model):
    auth_token = models.CharField(max_length=13, default=None, null=True, blank=True)


@receiver(post_save, sender=User) # this is called after a User model is saved.
def create_user_profile(sender, instance, created, **kwargs):
    if created: # only run the following if the profile is new
        new_profile = Profile.objects.create(user=instance)
        new_profile.create_auth_token()
        new_profile.save()

def create_auth_token(self):
    import random, string
    auth = self.user.username[:4] # get first 4 characters in user name
    self.auth_token =  auth + ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits + string.ascii_lowercase) for _ in range(random.randint(3, 5)))
CoreCreatives
la source
3

Vous ne pouvez pas faire cela directement; la valeur par défaut est évaluée lors de l'évaluation de la définition de fonction. Mais il y a deux façons de contourner cela.

Tout d'abord, vous pouvez créer (puis appeler) une nouvelle fonction à chaque fois.

Ou, plus simplement, utilisez simplement une valeur spéciale pour marquer la valeur par défaut. Par exemple:

from datetime import datetime
def mydate(date=None):
  if date is None:
    date = datetime.now()
  print date

Si Noneest une valeur de paramètre parfaitement raisonnable, et qu'il n'y a pas d'autre valeur raisonnable que vous pourriez utiliser à sa place, vous pouvez simplement créer une nouvelle valeur qui est définitivement en dehors du domaine de votre fonction:

from datetime import datetime
class _MyDateDummyDefault(object):
  pass
def mydate(date=_MyDateDummyDefault):
  if date is _MyDateDummyDefault:
    date = datetime.now()
  print date
del _MyDateDummyDefault

Dans certains cas rares, vous êtes méta-écriture de code qui fait vraiment besoin d'être en mesure de prendre absolument rien, même, disons, mydate.func_defaults[0]. Dans ce cas, vous devez faire quelque chose comme ceci:

def mydate(*args, **kw):
  if 'date' in kw:
    date = kw['date']
  elif len(args):
    date = args[0]
  else:
    date = datetime.now()
  print date
Abarnert
la source
1
Notez qu'il n'y a aucune raison d'instancier réellement la classe de valeur factice - utilisez simplement la classe elle-même comme valeur factice.
Ambre
Vous POUVEZ le faire directement, il suffit de transmettre la fonction au lieu du résultat de l'appel de fonction. Voir ma réponse publiée.
Non, tu ne peux pas. Le résultat de la fonction n'est pas le même type de chose que les paramètres normaux, il ne peut donc pas raisonnablement être utilisé comme valeur par défaut pour ces paramètres normaux.
abarnert le
1
Eh bien oui, si vous passez un objet au lieu d'une fonction, cela déclenchera une exception. La même chose est vraie si vous passez une fonction dans un paramètre qui attend une chaîne. Je ne vois pas en quoi c'est pertinent.
C'est pertinent car sa myfuncfonction est faite pour prendre (et imprimer) un datetime; vous l'avez changé pour ne pas l'utiliser de cette façon.
abarnert le
2

Passez la fonction en tant que paramètre au lieu de transmettre le résultat de l'appel de fonction.

Autrement dit, au lieu de ceci:

def myfunc(date=datetime.now()):
    print date

Essaye ça:

def myfunc(date=datetime.now):
    print date()

la source
Cela ne fonctionne pas, car maintenant l'utilisateur ne peut pas réellement passer un paramètre - vous allez essayer de l'appeler en tant que fonction et lancer une exception. Dans ce cas, vous pouvez aussi bien ne prendre aucun paramètre et print datetime.now()sans condition.
abarnert le
Essayez-le. Vous ne passez pas de date, vous passez la fonction datetime.now.
Oui, la valeur par défaut passe la datetime.nowfonction. Mais tout utilisateur qui transmet une valeur autre que celle par défaut transmettra une date / heure, pas une fonction. foo = datetime.now(); myfunc(foo)augmentera TypeError: 'datetime.datetime' object is not callable. Si je voulais réellement utiliser votre fonction, je devrais faire quelque chose comme à la myfunc(lambda: foo)place, ce qui n'est pas une interface raisonnable.
abarnert le
1
Les interfaces qui acceptent des fonctions ne sont pas inhabituelles. Je pourrais commencer à nommer des exemples à partir du python stdlib, si vous le souhaitez.
2
Django accepte déjà un appelable exactement comme cette question le suggère.
Ned Batchelder