Default Bidang Model Django Berdasarkan Bidang Lain dalam Model Sama

92

Saya memiliki model yang saya ingin berisi nama subjek dan inisialnya (datanya agak dianonimkan dan dilacak dengan inisial).

Sekarang, saya menulis

class Subject(models.Model):

    name = models.CharField("Name", max_length=30)
    def subject_initials(self):
        return ''.join(map(lambda x: '' if len(x)==0 else x[0],
                           self.name.split(' ')))
    # Next line is what I want to do (or something equivalent), but doesn't work with
    # NameError: name 'self' is not defined
    subject_init = models.CharField("Subject Initials", max_length=5, default=self.subject_initials)

Seperti yang ditunjukkan oleh baris terakhir, saya lebih suka agar inisial benar-benar disimpan dalam database sebagai bidang (terlepas dari nama), tetapi itu diinisialisasi dengan nilai default berdasarkan bidang nama. Namun, saya mengalami masalah karena model django sepertinya tidak memiliki 'diri'.

Jika saya mengubah baris menjadi subject_init = models.CharField("Subject initials", max_length=2, default=subject_initials), saya dapat melakukan syncdb, tetapi tidak dapat membuat subjek baru.

Apakah ini mungkin di Django, memiliki fungsi yang dapat dipanggil memberikan default untuk beberapa bidang berdasarkan nilai dari bidang lain?

(Bagi yang penasaran, alasan saya ingin memisahkan inisial toko saya secara terpisah adalah dalam kasus yang jarang terjadi di mana nama belakang yang aneh mungkin berbeda dari yang saya lacak. Misalnya, orang lain memutuskan bahwa Subjek 1 Bernama inisial "John O'Mallory" adalah "JM" bukan "JO" dan ingin memperbaiki mengeditnya sebagai administrator.)

dr jimbob
sumber

Jawaban:

89

Model pasti punya "diri"! Hanya saja Anda mencoba untuk mendefinisikan atribut kelas model sebagai tergantung pada contoh model; itu tidak mungkin, karena instance tidak (dan tidak dapat) ada sebelum Anda mendefinisikan kelas dan atributnya.

Untuk mendapatkan efek yang Anda inginkan, ganti metode save () dari kelas model. Buat perubahan apa pun yang Anda inginkan pada instance yang diperlukan, lalu panggil metode superclass untuk melakukan penghematan sebenarnya. Berikut contoh singkatnya.

def save(self, *args, **kwargs):
    if not self.subject_init:
        self.subject_init = self.subject_initials()
    super(Subject, self).save(*args, **kwargs)

Ini tercakup dalam Metode Model Utama dalam dokumentasi.

Elf Sternberg
sumber
5
Perhatikan bahwa di Python 3 Anda cukup memanggil super().save(*args, **kwargs)(tanpa Subject, selfargumen) seperti pada contoh di dokumentasi yang direferensikan.
Kurt Peek
1
Sayang sekali, ini tidak memungkinkan untuk menentukan nilai atribut model default berdasarkan yang lain, yang sangat berguna di sisi Admin (misalnya, untuk bidang yang bertambah otomatis), karena ini menanganinya saat menyimpannya. Atau apakah saya salah paham?
Vadorequest
18

Saya tidak tahu apakah ada cara yang lebih baik untuk melakukan hal ini, tetapi Anda dapat menggunakan penangan sinyal untuk para pre_savesinyal :

from django.db.models.signals import pre_save

def default_subject(sender, instance, using):
    if not instance.subject_init:
        instance.subject_init = instance.subject_initials()

pre_save.connect(default_subject, sender=Subject)
Gabi Purcaru
sumber
1
Anda mengimpor, post_savebukan pre_save.
Ali Rasim Kocal
1
@arkocal: terima kasih atas perhatiannya, baru saja diperbaiki. Anda dapat menyarankan pengeditan sendiri dalam kasus seperti itu, ini membantu memperbaiki hal-hal seperti ini :)
Gabi Purcaru
1
Sepertinya implementasi ini kehilangan **kwargsargumen yang harus dimiliki oleh semua fungsi receiver menurut docs.djangoproject.com/en/2.0/topics/signals/… ?
Kurt Peek
Dokumentasi merekomendasikan sinyal untuk jalur kode yang Anda kontrol . Jawaban yang diterima dari menimpa save(), mungkin diubah menjadi menimpa __init__, lebih baik karena lebih eksplisit.
Adam Johnson
7

Menggunakan sinyal Django , ini bisa dilakukan cukup dini, dengan menerima para post_initsinyal dari model.

from django.db import models
import django.dispatch

class LoremIpsum(models.Model):
    name = models.CharField(
        "Name",
        max_length=30,
    )
    subject_initials = models.CharField(
        "Subject Initials",
        max_length=5,
    )

@django.dispatch.receiver(models.signals.post_init, sender=LoremIpsum)
def set_default_loremipsum_initials(sender, instance, *args, **kwargs):
    """
    Set the default value for `subject_initials` on the `instance`.

    :param sender: The `LoremIpsum` class that sent the signal.
    :param instance: The `LoremIpsum` instance that is being
        initialised.
    :return: None.
    """
    if not instance.subject_initials:
        instance.subject_initials = "".join(map(
                (lambda x: x[0] if x else ""),
                instance.name.split(" ")))

The post_initsinyal dikirim oleh kelas setelah telah dilakukan inisialisasi pada contoh. Dengan cara ini, instance mendapatkan nilai namesebelum menguji apakah kolom non-nullable-nya telah disetel.

hidung besar
sumber
Dokumentasi merekomendasikan sinyal untuk jalur kode yang Anda kontrol . Jawaban yang diterima dari menimpa save(), mungkin diubah menjadi menimpa __init__, lebih baik karena lebih eksplisit.
Adam Johnson
2

Sebagai implementasi alternatif dari jawaban Gabi Purcaru , Anda juga dapat terhubung ke pre_savesinyal menggunakan receiverdekorator :

from django.db.models.signals import pre_save
from django.dispatch import receiver


@receiver(pre_save, sender=Subject)
def default_subject(sender, instance, **kwargs):
    if not instance.subject_init:
        instance.subject_init = instance.subject_initials()

Fungsi penerima ini juga mengambil **kwargsargumen kata kunci wildcard yang harus diambil oleh semua penangan sinyal sesuai dengan https://docs.djangoproject.com/en/2.0/topics/signals/#receiver-functions .

Kurt Peek
sumber