Desactivar temporalmente auto_now / auto_now_add

104

Tengo un modelo como este:

class FooBar(models.Model):
    createtime = models.DateTimeField(auto_now_add=True)
    lastupdatetime = models.DateTimeField(auto_now=True)

Quiero sobrescribir los dos campos de fecha para algunas instancias del modelo (que se usan al migrar datos). La solución actual tiene este aspecto:

for field in new_entry._meta.local_fields:
    if field.name == "lastupdatetime":
        field.auto_now = False
    elif field.name == "createtime":
        field.auto_now_add = False

new_entry.createtime = date
new_entry.lastupdatetime = date
new_entry.save()

for field in new_entry._meta.local_fields:
    if field.name == "lastupdatetime":
        field.auto_now = True
    elif field.name == "createtime":
        field.auto_now_add = True

¿Existe una solución mejor?

jedie
fuente
new_entry.createtime.auto_now = ¿Falso?
Akonsu
3
+1 - Esto sería realmente bueno para la prueba
Frank Henard
@akonsu Nope: el objeto 'datetime.datetime' no tiene atributo 'auto_now'
mlissner
2
Vale la pena señalar que más de unos pocos desarrolladores centrales están a favor de desaprobarauto_now(_add)
mlissner
new_entry._meta.get_field ('date_update') es más directo
Sérgio

Respuestas:

99

Recientemente me enfrenté a esta situación mientras probaba mi aplicación. Necesitaba "forzar" una marca de tiempo vencida. En mi caso, hice el truco usando una actualización de conjunto de consultas. Me gusta esto:

# my model
class FooBar(models.Model):
    title = models.CharField(max_length=255)
    updated_at = models.DateTimeField(auto_now=True, auto_now_add=True)



# my tests
foo = FooBar.objects.get(pk=1)

# force a timestamp
lastweek = datetime.datetime.now() - datetime.timedelta(days=7)
FooBar.objects.filter(pk=foo.pk).update(updated_at=lastweek)

# do the testing.
dirleyrls
fuente
Gracias por esta respuesta. Aquí están los documentos para update (): docs.djangoproject.com/en/dev/ref/models/querysets/…
guettli
1
En realidad, este método funciona bastante bien si no le importa acceder a la base de datos. Terminé usando esto también para las pruebas.
imjustmatthew
3
de la documentación de Django: docs.djangoproject.com/en/1.9/topics/db/queries/… Tenga en cuenta que el método update () se convierte directamente en una declaración SQL. Es una operación masiva para actualizaciones directas. No ejecuta ningún método save () en sus modelos, ni emite las señales pre_save o post_save (que son una consecuencia de llamar a save ()), ni respeta la opción de campo
auto_now
@NoamG Creo que este es un caso raro en el que este update()comportamiento es exactamente lo que necesitamos.
David D.
54

Realmente no puede deshabilitar auto_now / auto_now_add de otra manera de la que ya lo hace. Si necesita la flexibilidad para cambiar esos valores, auto_now/ auto_now_addno es la mejor opción. A menudo, es más flexible usar defaulty / o anular el save()método para realizar la manipulación justo antes de guardar el objeto.

Usando defaulty un save()método anulado , una forma de resolver su problema sería definir su modelo de esta manera:

class FooBar(models.Model):
    createtime = models.DateTimeField(default=datetime.datetime.now)
    lastupdatetime = models.DateTimeField()

    def save(self, *args, **kwargs):
        if not kwargs.pop('skip_lastupdatetime', False):
            self.lastupdatetime = datetime.datetime.now()

        super(FooBar, self).save(*args, **kwargs)

En su código, donde desea omitir el cambio automático de última hora de actualización, simplemente use

new_entry.save(skip_lastupdatetime=True)

Si su objeto se guarda en la interfaz de administración o en otros lugares, se llamará a save () sin el argumento skip_lastupdatetime, y se comportará como lo hacía antes con auto_now.

andreaspelme
fuente
14
TL; DR No use auto_now_adduse defaulten su lugar.
Thane Brimhall
9
Una advertencia de este ejemplo es que datetime.datetime.nowdevuelve una fecha y hora ingenua. Para usar una fecha y hora que tenga en cuenta la zona horaria, use from django.utils import timezoney models.DateTimeField(default=timezone.now)consulte docs.djangoproject.com/en/1.9/topics/i18n/timezones/…
BenjaminGolder
Entonces, para que quede claro: si solo desea poder modificar el createtimecampo, no necesita anular save(). Basta con sustituir auto_now_add=Truepor el equivalente default=timezone.now, editable=False, blank=True(según docs ). Las dos últimas opciones garantizan un comportamiento similar en el administrador.
djvg
28

Usé la sugerencia hecha por el autor de la pregunta y creé algunas funciones. Aquí está el caso de uso:

turn_off_auto_now(FooBar, "lastupdatetime")
turn_off_auto_now_add(FooBar, "createtime")

new_entry.createtime = date
new_entry.lastupdatetime = date
new_entry.save()

Aquí está la implementación:

def turn_off_auto_now(ModelClass, field_name):
    def auto_now_off(field):
        field.auto_now = False
    do_to_model(ModelClass, field_name, auto_now_off)

def turn_off_auto_now_add(ModelClass, field_name):
    def auto_now_add_off(field):
        field.auto_now_add = False
    do_to_model(ModelClass, field_name, auto_now_add_off)

def do_to_model(ModelClass, field_name, func):
    field = ModelClass._meta.get_field_by_name(field_name)[0]
    func(field)

Se pueden crear funciones similares para volver a activarlas.

Frank Henard
fuente
9
En la mayoría de los casos, en lugar de la iteración, probablemente podría hacerlo Clazz._meta.get_field_by_name(field_name)[0].
naktinis
Gracias @naktinis. Cambiado.
Frank Henard
4
nb (1.10) ModelClass._meta.get_field_by_name (field_name) [0] en do_to_model no parecía funcionar para mí - cambiado a: ModelClass._meta.get_field (field_name)
Georgina S
27

También puede utilizar el update_fieldsparámetro de save()y pasar sus auto_nowcampos. He aquí un ejemplo:

# Date you want to force
new_created_date = date(year=2019, month=1, day=1)
# The `created` field is `auto_now` in your model
instance.created = new_created_date
instance.save(update_fields=['created'])

Aquí está la explicación de la documentación de Django: https://docs.djangoproject.com/en/stable/ref/models/instances/#specifying-which-fields-to-save

Datos de Apolo
fuente
1
¡Ojalá pudiera votar más alto! Esto es realmente lo que quería (alterar partes del modelo sin tocar los campos 'auto'), pero desafortunadamente no responde la pregunta dada (que es guardar valores explícitos en los campos usando auto_nowy auto_now_add).
Tim Tisdall
1
la mejor respuesta, me gustaría poder darle 10 votos, muy simple y elegante
Bedros
18

Seguí el camino del administrador de contexto para la reutilización.

@contextlib.contextmanager
def suppress_autotime(model, fields):
    _original_values = {}
    for field in model._meta.local_fields:
        if field.name in fields:
            _original_values[field.name] = {
                'auto_now': field.auto_now,
                'auto_now_add': field.auto_now_add,
            }
            field.auto_now = False
            field.auto_now_add = False
    try:
        yield
    finally:
        for field in model._meta.local_fields:
            if field.name in fields:
                field.auto_now = _original_values[field.name]['auto_now']
                field.auto_now_add = _original_values[field.name]['auto_now_add']

Use así:

with suppress_autotime(my_object, ['updated']):
    my_object.some_field = some_value
    my_object.save()

Auge.

soulseekah
fuente
8

Para aquellos que miran esto cuando están escribiendo pruebas, hay una biblioteca de Python llamada freezegun que les permite falsificar la hora, por lo que cuando se auto_now_addejecuta el código, obtiene la hora que realmente desea. Entonces:

from datetime import datetime, timedelta
from freezegun import freeze_time

with freeze_time('2016-10-10'):
    new_entry = FooBar.objects.create(...)
with freeze_time('2016-10-17'):
    # use new_entry as you wish, as though it was created 7 days ago

También se puede usar como decorador; consulte el enlace anterior para obtener documentos básicos.

Hamish Downer
fuente
4

Puede anular auto_now_addsin código especial.

Me encontré con esta pregunta cuando intenté crear un objeto con una fecha particular:

Post.objects.create(publication_date=date, ...)

donde publication_date = models.DateField(auto_now_add=True).

Así que esto es lo que hice:

post = Post.objects.create(...)
post.publication_date = date
post.save()

Esto se ha anulado con éxito auto_now_add.

Como solución a más largo plazo, el savemétodo de reemplazo es el camino a seguir: https://code.djangoproject.com/ticket/16583

Pavel Vergeev
fuente
2

Necesitaba deshabilitar auto_now para un campo DateTime durante una migración y pude hacer esto.

events = Events.objects.all()
for event in events:
    for field in event._meta.fields:
        if field.name == 'created_date':
            field.auto_now = False
    event.save()

fuente
1

Llego tarde a la fiesta, pero similar a varias de las otras respuestas, esta es una solución que utilicé durante una migración de base de datos. La diferencia con las otras respuestas es que esto deshabilita todos los campos auto_now para el modelo bajo el supuesto de que realmente no hay razón para tener más de uno de esos campos.

def disable_auto_now_fields(*models):
    """Turns off the auto_now and auto_now_add attributes on a Model's fields,
    so that an instance of the Model can be saved with a custom value.
    """
    for model in models:
        for field in model._meta.local_fields:
            if hasattr(field, 'auto_now'):
                field.auto_now = False
            if hasattr(field, 'auto_now_add'):
                field.auto_now_add = False

Luego, para usarlo, simplemente puede hacer:

disable_auto_now_fields(Document, Event, ...)

Y atravesará y destruirá todos sus campos auto_nowy auto_now_addpara todas las clases modelo que pase.

mlissner
fuente
1

Una versión un poco más limpia del administrador de contexto de https://stackoverflow.com/a/35943149/1731460

from contextlib import contextmanager

@contextmanager
def suppress_auto_now(model, field_names):
    """
    idea taken here https://stackoverflow.com/a/35943149/1731460
    """
    fields_state = {}
    for field_name in field_names:
        field = model._meta.get_field(field_name)
        fields_state[field] = {'auto_now': field.auto_now, 'auto_now_add': field.auto_now_add}

    for field in fields_state:
        field.auto_now = False
        field.auto_now_add = False
    try:
        yield
    finally:
        for field, state in fields_state.items():
            field.auto_now = state['auto_now']
            field.auto_now_add = state['auto_now_add']

Puedes usarlo incluso con Factories (factory-boy)

        with suppress_autotime(Click, ['created']):
            ClickFactory.bulk_create(post=obj.post, link=obj.link, created__iter=created)
pymen
fuente
0

copia de Django - Models.DateTimeField - Cambiar dinámicamente el valor auto_now_add

Bueno, pasé esta tarde descubriéndolo y el primer problema es cómo recuperar el objeto del modelo y en qué parte del código. Estoy en restframework en serializer.py, por ejemplo, en __init__serializer aún no podría tener el modelo. Ahora en to_internal_value puede obtener la clase modelo, después de obtener el Campo y luego de modificar las propiedades del campo como en este ejemplo:

class ProblemSerializer(serializers.ModelSerializer):

    def to_internal_value(self, data): 
        ModelClass = self.Meta.model
        dfil = ModelClass._meta.get_field('date_update')
        dfil.auto_now = False
        dfil.editable = True
Sérgio
fuente
0

Necesitaba una solución que funcionara update_or_create, llegué a esta solución basada en el código @andreaspelme.

El único cambio es que puede configurar la omisión configurando el campo modificado en skipno solo pasando kwarg skip_modified_updateal método save ().

¡Solo yourmodelobject.modified='skip'y la actualización se omitirán!

from django.db import models
from django.utils import timezone


class TimeTrackableAbstractModel(models.Model):
    created = models.DateTimeField(default=timezone.now, db_index=True)
    modified = models.DateTimeField(default=timezone.now, db_index=True)

    class Meta:
        abstract = True

    def save(self, *args, **kwargs):
        skip_modified_update = kwargs.pop('skip_modified_update', False)
        if skip_modified_update or self.modified == 'skip':
            self.modified = models.F('modified')
        else:
            self.modified = timezone.now()
        super(TimeTrackableAbstractModel, self).save(*args, **kwargs)
lechup
fuente