Deaktivieren Sie vorübergehend auto_now / auto_now_add

104

Ich habe ein Modell wie dieses:

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

Ich möchte die beiden Datumsfelder für einige Modellinstanzen (die beim Migrieren von Daten verwendet werden) überschreiben. Die aktuelle Lösung sieht folgendermaßen aus:

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

Gibt es eine bessere Lösung?

jedie
quelle
new_entry.createtime.auto_now = False?
Akonsu
3
+1 - Das wäre wirklich schön zum Testen
Frank Henard
@akonsu Nein: Das Objekt 'datetime.datetime' hat kein Attribut 'auto_now'
mlissner
2
Es ist erwähnenswert, dass mehr als ein paar Kernentwickler für eine Abwertung sindauto_now(_add)
mlissner
new_entry._meta.get_field ('date_update') ist direkter
Sérgio

Antworten:

99

Ich habe diese Situation kürzlich beim Testen meiner Anwendung erlebt. Ich musste einen abgelaufenen Zeitstempel "erzwingen". In meinem Fall habe ich den Trick mit einem Queryset-Update gemacht. So was:

# 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
quelle
Vielen Dank für diese Antwort. Hier sind die Dokumente für update (): docs.djangoproject.com/de/dev/ref/models/querysets/…
guettli
1
Eigentlich funktioniert diese Methode ziemlich gut, wenn es Ihnen nichts ausmacht, auf die Datenbank zuzugreifen. Am Ende habe ich das auch für Tests verwendet.
Imjustmatthew
3
Aus der Django-Dokumentation: docs.djangoproject.com/de/1.9/topics/db/queries/… Beachten Sie, dass die update () -Methode direkt in eine SQL-Anweisung konvertiert wird. Es ist eine Massenoperation für direkte Updates. Es führt keine save () -Methoden auf Ihren Modellen aus oder gibt die pre_save- oder post_save-Signale aus (die eine Folge des Aufrufs von save () sind) oder berücksichtigt die Feldoption auto_now
NoamG
@NoamG Ich denke, dies ist ein seltener Fall, in dem dieses update()Verhalten genau das ist, was wir brauchen.
David D.
54

Sie können auto_now / auto_now_add nicht auf eine andere Weise deaktivieren, als Sie es bereits getan haben. Wenn Sie die Flexibilität benötigen, um diese Werte zu ändern, ist auto_now/ auto_now_addnicht die beste Wahl. Es ist oft flexibler, das zu verwenden defaultund / oder zu überschreibensave() die Manipulationsmethode unmittelbar vor dem Speichern des Objekts .

Mit defaultund eine überschriebene save()Methode, ein Weg , um Ihr Problem zu lösen wäre Ihr Modell wie folgt zu definieren:

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)

Verwenden Sie in Ihrem Code, in dem Sie die automatische Änderung der letzten Aktualisierungszeit überspringen möchten, einfach

new_entry.save(skip_lastupdatetime=True)

Wenn Ihr Objekt in der Administrationsoberfläche oder an anderen Stellen gespeichert wird, wird save () ohne das Argument skip_lastupdatetime aufgerufen und verhält sich wie zuvor mit auto_now .

andreaspelme
quelle
14
TL; DR auto_now_addVerwenden Sie defaultstattdessen nicht verwenden .
Thane Brimhall
9
Eine Einschränkung in diesem Beispiel ist, dass datetime.datetime.noweine naive Datums- / Uhrzeitangabe zurückgegeben wird. Um eine zeitzonenbezogene Datums- from django.utils import timezoneund Uhrzeitangabe zu verwenden, verwenden models.DateTimeField(default=timezone.now)Sie docs.djangoproject.com/de/1.9/topics/i18n/timezones/…
BenjaminGolder,
Um es klar zu machen: Wenn Sie nur das createtimeFeld ändern möchten, müssen Sie es nicht überschreiben save(). Es reicht aus, auto_now_add=Truedurch das Äquivalent zu ersetzen default=timezone.now, editable=False, blank=True(laut Dokumentation ). Die beiden letztgenannten Optionen stellen ein ähnliches Verhalten im Administrator sicher.
DJVG
28

Ich habe den Vorschlag des Fragestellers verwendet und einige Funktionen erstellt. Hier ist der Anwendungsfall:

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

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

Hier ist die Implementierung:

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)

Ähnliche Funktionen können erstellt werden, um sie wieder einzuschalten.

Frank Henard
quelle
9
In den meisten Fällen könnten Sie anstelle einer Iteration wahrscheinlich einfach etwas tun Clazz._meta.get_field_by_name(field_name)[0].
Naktinis
Danke @naktinis. Geändert.
Frank Henard
4
nb (1.10) ModelClass._meta.get_field_by_name (field_name) [0] in do_to_model schien für mich nicht zu funktionieren - geändert in: ModelClass._meta.get_field (field_name)
Georgina S
27

Sie können auch den update_fieldsParameter von verwenden save()und Ihre auto_nowFelder übergeben. Hier ist ein Beispiel:

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

Hier ist die Erklärung aus der Dokumentation von Django: https://docs.djangoproject.com/de/stable/ref/models/instances/#specifying-which-fields-to-save

Apollo-Daten
quelle
1
Ich wünschte, ich könnte höher stimmen! Dies ist wirklich das, was ich wollte (um Teile des Modells zu ändern, ohne die 'Auto'-Felder zu berühren), aber leider nicht die gestellte Frage zu beantworten (das heißt, explizite Werte in den Feldern mit auto_nowund zu speichern auto_now_add).
Tim Tisdall
1
Beste Antwort, ich wünschte, ich könnte 10 Stimmen geben, sehr einfach und elegant
Bedros
18

Ich habe mich für die Wiederverwendbarkeit an den Kontextmanager gewandt.

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

Verwenden Sie wie folgt:

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

Boom.

Soulseekah
quelle
8

Für diejenigen, die dies beim Schreiben von Tests betrachten, gibt es eine Python-Bibliothek namens freezegun , mit der Sie die Zeit vortäuschen können. Wenn der auto_now_addCode ausgeführt wird, erhält er die Zeit, die Sie tatsächlich möchten. So:

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

Es kann auch als Dekorateur verwendet werden - grundlegende Dokumente finden Sie unter dem obigen Link.

Hamish Downer
quelle
4

Sie können auto_now_addohne speziellen Code überschreiben .

Ich bin auf diese Frage gestoßen, als ich versucht habe, ein Objekt mit einem bestimmten Datum zu erstellen:

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

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

Das habe ich also getan:

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

Dies wurde erfolgreich überschrieben auto_now_add.

Als langfristigere Lösung ist die überschreibende saveMethode der richtige Weg: https://code.djangoproject.com/ticket/16583

Pavel Vergeev
quelle
2

Ich musste auto_now für ein DateTime-Feld während einer Migration deaktivieren und konnte dies tun.

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

quelle
1

Ich bin zu spät zur Party, aber ähnlich wie bei einigen anderen Antworten ist dies eine Lösung, die ich während einer Datenbankmigration verwendet habe. Der Unterschied zu den anderen Antworten besteht darin, dass dadurch alle auto_now-Felder für das Modell deaktiviert werden, unter der Annahme, dass es wirklich keinen Grund gibt, mehr als ein solches Feld zu haben.

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

Um es dann zu verwenden, können Sie einfach Folgendes tun:

disable_auto_now_fields(Document, Event, ...)

Und es wird alle Ihre auto_nowund auto_now_addFelder für alle Modellklassen, die Sie übergeben , durchlaufen und zerstören .

mlissner
quelle
1

Eine etwas sauberere Version des Kontextmanagers von 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']

Sie können es sogar mit Fabriken (Fabrikjunge) verwenden

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

Kopie von Django - Models.DateTimeField - Dynamisch ändernder auto_now_add-Wert

Nun, ich habe diesen Nachmittag damit verbracht herauszufinden und das erste Problem ist, wie man ein Modellobjekt abruft und wo im Code. Ich bin in restframework in serializer.py, zum Beispiel in __init__serializer konnte es das Modell noch nicht haben. Jetzt können Sie in to_internal_value die Modellklasse abrufen, nachdem Sie das Feld abgerufen und die Feldeigenschaften wie in diesem Beispiel geändert haben:

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
quelle
0

Ich brauchte eine Lösung, die funktioniert update_or_create . Ich bin zu dieser Lösung gekommen, die auf @ andreaspelme-Code basiert.

Die einzige Änderung besteht darin, dass Sie das Überspringen festlegen können, indem Sie das geänderte Feld skipnicht nur auf das tatsächliche Übergeben von kwarg setzenskip_modified_update , an die save () -Methode übergeben.

Nur yourmodelobject.modified='skip'und Update wird übersprungen!

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
quelle