Campo de modelo de Django predeterminado basado en otro campo en el mismo modelo

91

Tengo un modelo que me gustaría que contenga el nombre de un sujeto y sus iniciales (los datos son algo anonimizados y se rastrean por iniciales).

Ahora mismo, escribí

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)

Como se indica en la última línea, preferiría poder tener las iniciales realmente almacenadas en la base de datos como un campo (independiente del nombre), pero eso se inicializa con un valor predeterminado basado en el campo de nombre. Sin embargo, estoy teniendo problemas ya que los modelos de django no parecen tener un "yo".

Si cambio la línea a subject_init = models.CharField("Subject initials", max_length=2, default=subject_initials), puedo hacer syncdb, pero no puedo crear nuevos sujetos.

¿Es esto posible en Django, tener una función invocable que dé un valor predeterminado a algún campo en función del valor de otro campo?

(Para los curiosos, la razón por la que quiero separar las iniciales de mi tienda por separado es en casos excepcionales en los que los apellidos extraños pueden tener diferentes a los que estoy rastreando. Por ejemplo, alguien más decidió que las iniciales del Sujeto 1 llamado "John O'Mallory" son "JM" en lugar de "JO" y desea corregirlo, editarlo como administrador).

dr jimbob
fuente

Respuestas:

88

¡Los modelos ciertamente tienen un "yo"! Es solo que está tratando de definir un atributo de una clase de modelo como dependiente de una instancia de modelo; eso no es posible, ya que la instancia no existe (y no puede) existir antes de que defina la clase y sus atributos.

Para obtener el efecto que desea, anule el método save () de la clase modelo. Realice los cambios que desee en la instancia necesarios, luego llame al método de la superclase para hacer el guardado real. He aquí un ejemplo rápido.

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

Esto se trata en Anulación de métodos de modelo en la documentación.

Elfo Sternberg
fuente
4
Tenga en cuenta que en Python 3 simplemente puede llamar super().save(*args, **kwargs)(sin los Subject, selfargumentos) como en el ejemplo en la documentación referenciada.
Kurt Peek
1
Lástima que esto no permita definir un valor de atributo de modelo predeterminado basado en otro, lo cual es particularmente útil en el lado del administrador (por ejemplo, para un campo autoincrementado), ya que lo maneja al guardarlo. ¿O lo estoy entendiendo mal?
Vadorequest
18

No sé si hay una mejor manera de hacer esto, pero puede usar un controlador de señal para la pre_saveseñal :

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
fuente
1
Importó en post_savelugar de pre_save.
Ali Rasim Kocal
1
@arkocal: gracias por el aviso, solo lo arreglé. Puede sugerir ediciones usted mismo en tales casos, ayuda a arreglar cosas como esta :)
Gabi Purcaru
1
Parece que a esta implementación le falta el **kwargsargumento que deberían tener todas las funciones del receptor de acuerdo con docs.djangoproject.com/en/2.0/topics/signals/… ?
Kurt Peek
La documentación recomienda no señales para las rutas de código que controlas . La respuesta aceptada de anular save(), quizás cambiada a anular __init__, es mejor porque es más explícita.
Adam Johnson
7

Usando señales de Django , esto se puede hacer bastante temprano, recibiendo la post_initseñal del modelo.

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

La post_initclase envía la señal una vez que ha realizado la inicialización en la instancia. De esta forma, la instancia obtiene un valor nameantes de probar si sus campos que no aceptan valores NULL están configurados.

nariz grande
fuente
La documentación recomienda no señales para las rutas de código que controlas . La respuesta aceptada de anular save(), quizás cambiada a anular __init__, es mejor porque es más explícita.
Adam Johnson
2

Como implementación alternativa de la respuesta de Gabi Purcaru , también puede conectarse a la pre_saveseñal usando el receiverdecorador :

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

Esta función de receptor también toma los **kwargsargumentos de palabras clave comodín que todos los manejadores de señales deben tomar de acuerdo con https://docs.djangoproject.com/en/2.0/topics/signals/#receiver-functions .

Kurt Peek
fuente