Jak ustawić domyślną wartość pola modelu Django na wywoływaną / możliwą do wywołania funkcji (np. Datę w stosunku do czasu utworzenia obiektu modelu)

101

EDYTOWANO:

Jak ustawić wartość domyślną pola Django na funkcję, która jest oceniana za każdym razem, gdy tworzony jest nowy obiekt modelu?

Chcę zrobić coś podobnego do następującego, z tą różnicą, że w tym kodzie kod jest oceniany raz i ustawia domyślną tę samą datę dla każdego utworzonego obiektu modelu, zamiast oceniać kod za każdym razem, gdy zostanie utworzony obiekt modelu:

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))



ORYGINALNY:

Chcę utworzyć wartość domyślną dla parametru funkcji, tak aby była dynamiczna i była wywoływana i ustawiana za każdym razem, gdy funkcja jest wywoływana. Jak mogę to zrobić? na przykład,

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

W szczególności chcę to zrobić w Django, np.

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
źródło
Pytanie jest źle sformułowane: nie ma to nic wspólnego z domyślnymi parametrami funkcji. Zobacz moją odpowiedź.
Ned Batchelder,
Zgodnie z komentarzem @ NedBatcheldera, zredagowałem moje pytanie (oznaczone jako „EDYTOWANE:”) i zostawiłem oryginał (oznaczony jako „ORYGINAŁ:”)
Rob Bednark

Odpowiedzi:

140

Pytanie jest błędne. Podczas tworzenia pola modelu w Django nie definiujesz funkcji, więc domyślne wartości funkcji są nieistotne:

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))

Ta ostatnia linia nie definiuje funkcji; wywołuje funkcję w celu utworzenia pola w klasie.

PRE Django 1.7

Django pozwala domyślnie przekazać wywołanie i będzie je wywoływać za każdym razem, tak jak chcesz:

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+

Zwróć uwagę, że od wersji Django 1.7 używanie lambdy jako wartości domyślnej nie jest zalecane (por. Komentarz @stvnw). Właściwym sposobem na to jest zadeklarowanie funkcji przed polem i użycie jej jako wywoływalnej w default_value o nazwie 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)

Więcej informacji w odpowiedzi @simanas poniżej

Ned Batchelder
źródło
2
Dzięki @NedBatchelder. To jest dokładnie to, czego szukałem. Nie zdawałem sobie sprawy, że „default” może wymagać wywołania. A teraz widzę, że moje pytanie było rzeczywiście błędne w odniesieniu do wywołania funkcji a definicji funkcji.
Rob Bednark
25
Zauważ, że dla Django> = 1.7 użycie lambd nie jest zalecane dla opcji pól, ponieważ są one niekompatybilne z migracjami. Ref: Dokumenty , bilet
StvnW
4
Usuń zaznaczenie tej odpowiedzi, ponieważ jest ona błędna dla Djange> = 1.7. Odpowiedź @Simanas jest całkowicie poprawna i dlatego należy ją zaakceptować.
AplusKminus
Jak to działa w przypadku migracji? Czy wszystkie stare obiekty mają datę opartą na czasie migracji? Czy można ustawić inne domyślne, które będą używane podczas migracji?
timthelion,
3
A jeśli chcę przekazać jakiś parametr get_default_my_date?
Sadan A.
59

Robienie tego default=datetime.now()+timedelta(days=1)jest absolutnie złe!

Jest oceniany, gdy uruchamiasz swoją instancję django. Jeśli jesteś pod apache'em to prawdopodobnie zadziała, ponieważ w niektórych konfiguracjach apache unieważnia twoją aplikację django na każde żądanie, ale nadal możesz znaleźć siebie pewnego dnia, przeglądając swój kod i próbując dowiedzieć się, dlaczego zostało to obliczone nie tak, jak się spodziewasz .

Właściwym sposobem na zrobienie tego jest przekazanie wywoływalnego obiektu do domyślnego argumentu. Może to być funkcja datetime.today lub funkcja niestandardowa. Następnie jest oceniany za każdym razem, gdy zażądasz nowej wartości domyślnej.

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
źródło
21
Jeśli moja wartość domyślna jest oparta na wartości innego pola w modelu, czy można przekazać to pole jako parametr, jak w czymś takim jak get_deadline(my_parameter)?
YPCrumble,
@YPCrumble to powinno być nowe pytanie!
jsmedmar
2
Nie. To nie jest możliwe. Aby to zrobić, będziesz musiał zastosować inne podejście.
Simanas
Jak deadlineponownie usunąć pole, jednocześnie usuwając get_deadlinefunkcję? Usunąłem pole z funkcją dla wartości domyślnej, ale teraz Django ulega awarii podczas uruchamiania po usunięciu funkcji. Mógłbym ręcznie edytować migrację, co w tym przypadku byłoby w porządku, ale co by było, gdybyś po prostu zmienił domyślną funkcję i chciał usunąć starą funkcję?
beruic
8

Istnieje ważna różnica między następującymi dwoma konstruktorami DateTimeField:

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

Jeśli używasz auto_now_add=Truew konstruktorze, data i godzina, do której odwołuje się my_date, jest „niezmienna” (ustawiana tylko raz, gdy wiersz jest wstawiany do tabeli).

Dzięki auto_now=True, jednak wartość datetime będą aktualizowane za każdym razem, gdy obiekt zostanie zapisany.

W pewnym momencie było to dla mnie zdecydowanie trudne. W celach informacyjnych dokumenty są tutaj:

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

damzam
źródło
3
Masz pomieszane definicje auto_now i auto_now_add, jest odwrotnie.
Michael Bates
@MichaelBates teraz lepiej?
Hendy Irawan
4

Czasami może zajść potrzeba uzyskania dostępu do danych modelu po utworzeniu nowego modelu użytkownika.

Oto jak generuję token dla każdego nowego profilu użytkownika, używając pierwszych 4 znaków jego nazwy użytkownika:

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
źródło
3

Nie możesz tego zrobić bezpośrednio; wartość domyślna jest oceniana podczas oceny definicji funkcji. Ale można to obejść na dwa sposoby.

Po pierwsze, możesz utworzyć (a następnie wywołać) nową funkcję za każdym razem.

Lub, prościej, po prostu użyj specjalnej wartości, aby oznaczyć wartość domyślną. Na przykład:

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

Jeśli Nonejest to całkowicie rozsądna wartość parametru i nie ma innej rozsądnej wartości, której można by użyć w jej miejsce, możesz po prostu utworzyć nową wartość, która jest zdecydowanie poza domeną Twojej funkcji:

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

W niektórych rzadkich przypadkach piszesz metakod, który naprawdę musi być w stanie przyjąć absolutnie wszystko, nawet powiedzmy mydate.func_defaults[0]. W takim przypadku musisz zrobić coś takiego:

def mydate(*args, **kw):
  if 'date' in kw:
    date = kw['date']
  elif len(args):
    date = args[0]
  else:
    date = datetime.now()
  print date
abarnert
źródło
1
Zauważ, że nie ma powodu, aby faktycznie tworzyć instancję fikcyjnej klasy wartości - po prostu użyj samej klasy jako wartości fikcyjnej.
Amber,
Możesz to zrobić bezpośrednio, po prostu przekaż funkcję zamiast wyniku wywołania funkcji. Zobacz moją opublikowaną odpowiedź.
Nie, nie możesz. Wynik funkcji nie jest tego samego typu, co normalne parametry, więc nie można go w rozsądny sposób użyć jako wartości domyślnej dla tych normalnych parametrów.
abarnert
1
Cóż, tak, jeśli przekażesz obiekt zamiast funkcji, zgłosi wyjątek. To samo dotyczy sytuacji, gdy przekazujesz funkcję do parametru oczekującego na łańcuch. Nie rozumiem, jakie to ma znaczenie.
Jest to istotne, ponieważ jego myfuncfunkcja ma pobierać (i drukować) a datetime; zmieniłeś to, więc nie można go używać w ten sposób.
abarnert
2

Przekaż funkcję jako parametr zamiast przekazywania wyniku wywołania funkcji.

To znaczy zamiast tego:

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

Spróbuj tego:

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

źródło
To nie działa, ponieważ teraz użytkownik nie może w rzeczywistości przekazać parametru - spróbujesz wywołać go jako funkcję i zgłosić wyjątek. W takim przypadku równie dobrze możesz po prostu nie przyjmować parametrów i print datetime.now()bezwarunkowo.
abarnert
Spróbuj. Nie przekazujesz daty, przekazujesz funkcję datetime.now.
Tak, domyślnie datetime.nowfunkcja jest przekazywana . Ale każdy użytkownik, który przekaże wartość inną niż domyślna, przekaże datę i godzinę, a nie funkcję. foo = datetime.now(); myfunc(foo)wzrośnie TypeError: 'datetime.datetime' object is not callable. Gdybym rzeczywiście chciał użyć twojej funkcji, musiałbym myfunc(lambda: foo)zamiast tego zrobić coś takiego , co nie jest rozsądnym interfejsem.
abarnert
1
Interfejsy akceptujące funkcje nie są niczym niezwykłym. Mógłbym zacząć nazywać przykłady ze standardowego biblioteki Pythona, jeśli chcesz.
2
Django już akceptuje wywołanie dokładnie tak, jak sugeruje to pytanie.
Ned Batchelder