Bagaimana menyetel nilai default bidang model Django ke pemanggilan fungsi / dapat dipanggil (mis., Tanggal relatif terhadap waktu pembuatan objek model)

101

DIEDIT:

Bagaimana saya dapat menyetel standar bidang Django ke fungsi yang dievaluasi setiap kali objek model baru dibuat?

Saya ingin melakukan sesuatu seperti berikut, kecuali bahwa dalam kode ini, kode akan dievaluasi satu kali dan menetapkan default ke tanggal yang sama untuk setiap objek model yang dibuat, daripada mengevaluasi kode setiap kali objek model dibuat:

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



ASLI:

Saya ingin membuat nilai default untuk parameter fungsi sedemikian rupa sehingga dinamis dan dipanggil dan disetel setiap kali fungsi dipanggil. Bagaimana saya bisa melakukan itu? misalnya,

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

Secara khusus, saya ingin melakukannya di Django, misalnya,

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
sumber
Pertanyaannya salah: ini tidak ada hubungannya dengan default parameter fungsi. Lihat jawaban saya.
Ned Batchelder
Sesuai komentar @ NedBatchelder, saya mengedit pertanyaan saya (ditunjukkan sebagai "DIEDIT:") dan meninggalkan yang asli (ditunjukkan sebagai "ASLI:")
Rob Bednark

Jawaban:

140

Pertanyaannya salah arah. Saat membuat bidang model di Django, Anda tidak sedang mendefinisikan sebuah fungsi, jadi nilai default fungsi tidak relevan:

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

Baris terakhir ini tidak mendefinisikan sebuah fungsi; itu memanggil fungsi untuk membuat field di kelas.

PRE Django 1.7

Django mengijinkan Anda melewatkan sebuah yang dapat dipanggil sebagai default , dan itu akan memanggilnya setiap kali, seperti yang Anda inginkan:

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+

Harap dicatat bahwa sejak Django 1.7, penggunaan lambda sebagai nilai default tidak direkomendasikan (lihat komentar @stvnw). Cara yang tepat untuk melakukan ini adalah dengan mendeklarasikan fungsi sebelum kolom dan menggunakannya sebagai callable di default_value bernama 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)

Informasi lebih lanjut di jawaban @simanas di bawah ini

Ned Batchelder
sumber
2
Terima kasih @NedBatchelder. Inilah yang saya cari. Saya tidak menyadari bahwa 'default' dapat menerima panggilan. Dan sekarang saya melihat bagaimana pertanyaan saya memang salah arah mengenai pemanggilan fungsi vs definisi fungsi.
Rob Bednark
25
Perhatikan bahwa untuk Django> = 1.7 penggunaan lambda tidak direkomendasikan untuk opsi bidang karena mereka tidak kompatibel dengan migrasi. Ref: Dokumen , tiket
StvnW
4
Harap hapus centang pada jawaban ini, karena ini salah untuk Djange> = 1.7. Jawaban @Simanas benar-benar benar dan oleh karena itu harus diterima.
AplusKminus
Bagaimana cara kerjanya dengan migrasi? Apakah semua objek lama mendapatkan tanggal berdasarkan waktu migrasi? Apakah mungkin menyetel default yang berbeda untuk digunakan dengan migrasi?
timthelion
3
Bagaimana jika saya ingin meneruskan beberapa parameter ke get_default_my_date?
Sadan A.
59

Melakukan ini default=datetime.now()+timedelta(days=1) benar-benar salah!

Ini akan dievaluasi ketika Anda memulai contoh Anda dari django. Jika Anda berada di bawah apache, ini mungkin akan berhasil, karena pada beberapa konfigurasi apache mencabut aplikasi django Anda pada setiap permintaan, tetapi Anda masih dapat menemukan diri Anda suatu hari nanti melihat-lihat kode Anda dan mencoba mencari tahu mengapa ini dihitung tidak seperti yang Anda harapkan .

Cara yang benar untuk melakukan ini adalah dengan meneruskan objek yang dapat dipanggil ke argumen default. Ini bisa berupa fungsi datetime.today atau fungsi kustom Anda. Kemudian itu dievaluasi setiap kali Anda meminta nilai default baru.

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
sumber
21
Jika default saya didasarkan pada nilai bidang lain dalam model, apakah mungkin untuk meneruskan bidang itu sebagai parameter seperti pada sesuatu seperti get_deadline(my_parameter)?
YPCrumble
@YPCrumble ini seharusnya menjadi pertanyaan baru!
jsmedmar
2
Nggak. Itu tidak mungkin. Anda harus menggunakan beberapa pendekatan berbeda untuk melakukannya.
Simanas
Bagaimana Anda menghapus deadlinebidang lagi sekaligus menghapus get_deadlinefungsinya? Saya telah menghapus bidang dengan fungsi untuk nilai default, tetapi sekarang Django lumpuh saat memulai setelah menghapus fungsi tersebut. Saya dapat mengedit migrasi secara manual, yang akan baik-baik saja dalam kasus ini, tetapi bagaimana jika Anda hanya mengubah fungsi default, dan ingin menghapus fungsi lama?
beruic
8

Ada perbedaan penting antara dua konstruktor DateTimeField berikut:

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

Jika Anda menggunakan auto_now_add=Truedalam konstruktor, datetime yang direferensikan oleh my_date adalah "tidak dapat diubah" (hanya disetel sekali saat baris dimasukkan ke tabel).

Dengan auto_now=True, bagaimanapun, nilai tanggal waktu akan diperbarui setiap kali objek disimpan.

Ini jelas merupakan gotcha bagi saya pada satu titik. Untuk referensi, dokumennya ada di sini:

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

damzam
sumber
3
Anda sudah mencampur definisi auto_now dan auto_now_add, sebaliknya.
Michael Bates
@MichaelBates lebih baik sekarang?
Hendy Irawan
4

Terkadang Anda mungkin perlu mengakses data model setelah membuat model pengguna baru.

Berikut adalah cara saya membuat token untuk setiap profil pengguna baru menggunakan 4 karakter pertama dari nama pengguna mereka:

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
sumber
3

Anda tidak dapat melakukannya secara langsung; nilai default dievaluasi ketika definisi fungsi dievaluasi. Tetapi ada dua cara untuk mengatasinya.

Pertama, Anda dapat membuat (lalu memanggil) fungsi baru setiap saat.

Atau, lebih sederhananya, cukup gunakan nilai khusus untuk menandai default. Sebagai contoh:

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

Jika Nonemerupakan nilai parameter yang masuk akal, dan tidak ada nilai wajar lainnya yang dapat Anda gunakan sebagai gantinya, Anda dapat membuat nilai baru yang pasti berada di luar domain fungsi Anda:

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

Dalam beberapa kasus yang jarang terjadi, Anda menulis kode meta yang benar-benar harus dapat mengambil apa pun, bahkan, katakanlah mydate.func_defaults[0],. Dalam hal ini, Anda harus melakukan sesuatu seperti ini:

def mydate(*args, **kw):
  if 'date' in kw:
    date = kw['date']
  elif len(args):
    date = args[0]
  else:
    date = datetime.now()
  print date
abarnert
sumber
1
Perhatikan bahwa tidak ada alasan untuk benar-benar membuat instance kelas nilai dummy - cukup gunakan kelas itu sendiri sebagai nilai dummy.
Amber
Anda BISA melakukan ini secara langsung, cukup teruskan fungsi alih-alih hasil pemanggilan fungsi. Lihat jawaban saya yang diposting.
Tidak, kamu tidak bisa. Hasil dari fungsi ini bukanlah jenis yang sama seperti parameter normal, sehingga tidak dapat digunakan secara wajar sebagai nilai default untuk parameter normal tersebut.
abarnert
1
Ya, jika Anda mengirimkan objek alih-alih fungsi, itu akan memunculkan pengecualian. Hal yang sama juga berlaku jika Anda meneruskan fungsi ke parameter yang mengharapkan string. Saya tidak melihat relevansinya.
Relevan karena myfuncfungsinya dibuat untuk mengambil (dan mencetak) a datetime; Anda telah mengubahnya sehingga tidak dapat digunakan seperti itu.
abarnert
2

Meneruskan fungsi sebagai parameter alih-alih meneruskan hasil pemanggilan fungsi.

Artinya, alih-alih ini:

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

Coba ini:

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

sumber
Ini tidak berhasil, karena sekarang pengguna tidak dapat benar-benar mengirimkan parameter — Anda akan mencoba memanggilnya sebagai fungsi dan membuat pengecualian. Dalam hal ini Anda mungkin juga tidak mengambil params dan print datetime.now()tanpa syarat.
abarnert
Cobalah. Anda tidak melewati tanggal, Anda meneruskan fungsi datetime.now.
Ya, defaultnya meneruskan datetime.nowfungsi tersebut. Tetapi setiap pengguna yang meneruskan nilai non-default akan melewati waktu waktu, bukan fungsi. foo = datetime.now(); myfunc(foo)akan meningkat TypeError: 'datetime.datetime' object is not callable. Jika saya benar-benar ingin menggunakan fungsi Anda, saya harus melakukan sesuatu seperti itu myfunc(lambda: foo), yang bukan antarmuka yang masuk akal.
abarnert
1
Antarmuka yang menerima fungsi tidak biasa. Saya bisa mulai memberi contoh penamaan dari python stdlib, jika Anda suka.
2
Django sudah menerima dapat dipanggil persis seperti yang disarankan pertanyaan ini.
Ned Batchelder