Disattiva temporaneamente auto_now / auto_now_add


104

Ho un modello come questo:

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

Desidero sovrascrivere i due campi della data per alcune istanze del modello (utilizzati durante la migrazione dei dati). La soluzione attuale è simile a questa:

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

C'è una soluzione migliore?


new_entry.createtime.auto_now = False?
Akonsu

3
+1 - Questo sarebbe davvero bello per i test
Frank Henard

@akonsu No: l'oggetto 'datetime.datetime' non ha attributo 'auto_now'
mlissner

2
Vale la pena sottolineare che più di pochi sviluppatori core sono a favore del deprecareauto_now(_add)
mlissner

new_entry._meta.get_field ('date_update') è più diretto
Sérgio

Risposte:


99

Di recente ho affrontato questa situazione durante il test della mia applicazione. Avevo bisogno di "forzare" un timestamp scaduto. Nel mio caso ho fatto il trucco utilizzando un aggiornamento del set di query. Come questo:

# 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.

Grazie per questa risposta. Ecco i documenti per update (): docs.djangoproject.com/en/dev/ref/models/querysets/…
guettli

1
In realtà, questo metodo funziona abbastanza bene se non ti dispiace colpire il database. Ho finito per usarlo anche per i test.
imjustmatthew

3
dalla documentazione di Django: docs.djangoproject.com/en/1.9/topics/db/queries/… Tenere presente che il metodo update () viene convertito direttamente in un'istruzione SQL. È un'operazione di massa per gli aggiornamenti diretti. Non esegue alcun metodo save () sui tuoi modelli, né emette i segnali pre_save o post_save (che sono una conseguenza della chiamata save ()), o onora l'opzione del campo
auto_now

@NoamG Penso che questo sia un raro caso in cui questo update()comportamento è esattamente ciò di cui abbiamo bisogno.
David D.

54

Non puoi davvero disabilitare auto_now / auto_now_add in un modo diverso da quello che già fai. Se hai bisogno della flessibilità per modificare questi valori, auto_now/ auto_now_addnon è la scelta migliore. Spesso è più flessibile utilizzare defaulte / o sovrascrivere il save()metodo per eseguire la manipolazione subito prima che l'oggetto venga salvato.

Utilizzando defaulte un save()metodo sovrascritto , un modo per risolvere il tuo problema sarebbe definire il tuo modello in questo modo:

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)

Nel tuo codice, dove vuoi saltare la modifica automatica lastupdatetime, usa semplicemente

new_entry.save(skip_lastupdatetime=True)

Se il tuo oggetto viene salvato nell'interfaccia di amministrazione o in altri posti, save () verrà chiamato senza l'argomento skip_lastupdatetime e si comporterà come prima con auto_now.


14
TL; DR Non auto_now_addutilizzare defaultinvece l' uso .
Thane Brimhall

9
Un avvertimento a questo esempio è che datetime.datetime.nowrestituisce un datetime ingenuo. Per utilizzare un datetime in grado di riconoscere il fuso orario, utilizza from django.utils import timezonee models.DateTimeField(default=timezone.now)consulta docs.djangoproject.com/en/1.9/topics/i18n/timezones/…
BenjaminGolder

Quindi, per essere chiari: se vuoi solo essere in grado di modificare il createtimecampo, non è necessario eseguire l'override save(). È sufficiente sostituire auto_now_add=Truecon l'equivalente default=timezone.now, editable=False, blank=True(secondo i documenti ). Le ultime due opzioni assicurano un comportamento simile nell'amministratore.
djvg

28

Ho utilizzato il suggerimento del richiedente e ho creato alcune funzioni. Ecco il caso d'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()

Ecco l'implementazione:

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)

È possibile creare funzioni simili per riattivarle.


9
Nella maggior parte dei casi invece dell'iterazione potresti probabilmente farlo Clazz._meta.get_field_by_name(field_name)[0].
naktinis

Grazie @naktinis. Cambiato.
Frank Henard

4
nb (1.10) ModelClass._meta.get_field_by_name (field_name) [0] in do_to_model non sembra funzionare per me - modificato in: ModelClass._meta.get_field (field_name)
Georgina S

27

Puoi anche utilizzare il update_fieldsparametro di save()e passare i tuoi auto_nowcampi. Ecco un esempio:

# 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'])

Ecco la spiegazione dalla documentazione di Django: https://docs.djangoproject.com/en/stable/ref/models/instances/#specifying-which-fields-to-save


1
Vorrei poter votare questo più alto! Questo è davvero quello che volevo (modificare parti del modello senza toccare i campi 'auto'), ma sfortunatamente non risponde alla domanda data (che è salvare valori espliciti nei campi usando auto_nowe auto_now_add).
Tim Tisdall

1
migliore risposta, vorrei potergli dare 10 voti, molto semplice ed elegante
Bedros

18

Sono andato alla maniera del gestore di contesto per la riusabilità.

@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']

Usa così:

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

Boom.


8

Per coloro che guardano questo quando stanno scrivendo dei test, c'è una libreria python chiamata freezegun che ti permette di falsificare il tempo, quindi quando il auto_now_addcodice viene eseguito, ottiene il tempo che desideri. Così:

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

Può anche essere usato come decoratore - vedi il link sopra per i documenti di base.


4

È possibile eseguire l'override auto_now_addsenza codice speciale.

Mi sono imbattuto in questa domanda quando ho provato a creare un oggetto con una data particolare:

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

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

Quindi questo è quello che ho fatto:

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

Questo è stato sostituito con successo auto_now_add.

Come soluzione più a lungo termine, il savemetodo di sostituzione è la strada da percorrere: https://code.djangoproject.com/ticket/16583


2

Avevo bisogno di disabilitare auto_now per un campo DateTime durante una migrazione e sono stato in grado di farlo.

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

1

Sono in ritardo alla festa, ma simile a molte altre risposte, questa è una soluzione che ho usato durante una migrazione del database. La differenza rispetto alle altre risposte è che questo disabilita tutti i campi auto_now per il modello presumendo che non ci sia davvero alcun motivo per avere più di uno di questi campi.

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

Quindi per usarlo, puoi semplicemente fare:

disable_auto_now_fields(Document, Event, ...)

E passerà attraverso e bombarderà tutti i tuoi campi auto_nowe auto_now_addper tutte le classi modello che passi.


1

Una versione leggermente più pulita del gestore di contesto da 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']

Puoi usarlo anche con Factories (factory-boy)

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

0

copia di Django - Models.DateTimeField - Modifica dinamicamente il valore di auto_now_add

Bene, ho passato questo pomeriggio a scoprirlo e il primo problema è come recuperare l'oggetto del modello e dove nel codice. Sono in restframework in serializer.py, ad esempio in __init__serializer non potrebbe ancora avere il modello. Ora in to_internal_value puoi ottenere la classe del modello, dopo ottenere il campo e dopo modificare le proprietà del campo come in questo esempio:

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

0

Avevo bisogno di una soluzione che funzionasse update_or_create, sono arrivato a questa soluzione basata sul codice @andreaspelme.

L'unico cambiamento è che puoi impostare il salto impostando il campo modificato su skipnon solo passando effettivamente kwarg skip_modified_updateal metodo save ().

Solo yourmodelobject.modified='skip'e l'aggiornamento verrà saltato!

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)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.