Tymczasowo wyłącz auto_now / auto_now_add

105

Mam taki model:

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

Chcę nadpisać dwa pola daty dla niektórych wystąpień modelu (używane podczas migracji danych). Obecne rozwiązanie wygląda następująco:

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

Czy jest lepsze rozwiązanie?

jedie
źródło
new_entry.createtime.auto_now = Fałsz?
akonsu
3
+1 - To byłoby naprawdę fajne do testowania
Frank Henard
@akonsu Nie: Obiekt „datetime.datetime” nie ma atrybutu „auto_now”
mlissner
2
Warto zauważyć, że więcej niż kilku głównych twórców jest za deprecjonowaniemauto_now(_add)
mlissner
new_entry._meta.get_field ('date_update') jest bardziej bezpośredni
Sérgio

Odpowiedzi:

101

Niedawno spotkałem się z taką sytuacją podczas testowania mojej aplikacji. Musiałem „wymusić” wygasłą sygnaturę czasową. W moim przypadku załatwiłem sprawę, używając aktualizacji zestawu zapytań. Lubię to:

# 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
źródło
Dziękuję za tę odpowiedź. Oto dokumenty do update (): docs.djangoproject.com/en/dev/ref/models/querysets/ ...
guettli
1
Właściwie ta metoda działa całkiem dobrze, jeśli nie masz nic przeciwko trafieniu do bazy danych. Skończyło się na tym, że użyłem tego również do testów.
imjustmatthew
3
z dokumentacji Django: docs.djangoproject.com/en/1.9/topics/db/queries/… Należy pamiętać, że metoda update () jest konwertowana bezpośrednio na instrukcję SQL. Jest to operacja zbiorcza do bezpośrednich aktualizacji. Nie uruchamia żadnych metod save () na twoich modelach, nie emituje sygnałów pre_save lub post_save (które są konsekwencją wywołania save ()), ani nie honoruje opcji pola
auto_now
@NoamG Myślę, że jest to rzadki przypadek, w którym takie update()zachowanie jest dokładnie tym, czego potrzebujemy.
David D.
54

Naprawdę nie możesz wyłączyć auto_now / auto_now_add w inny sposób niż już to robisz. Jeśli potrzebujesz elastyczności, aby zmienić te wartości, auto_now/ auto_now_addnie jest najlepszym wyborem. Często bardziej elastyczne jest użycie defaulti / lub nadpisanie save()metody wykonywania manipulacji tuż przed zapisaniem obiektu.

Używając defaulti save()metody zastępowanej , jednym ze sposobów rozwiązania problemu byłoby zdefiniowanie modelu w następujący sposób:

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)

W swoim kodzie, w którym chcesz pominąć automatyczną zmianę lastupdatetime, po prostu użyj

new_entry.save(skip_lastupdatetime=True)

Jeśli twój obiekt jest zapisany w interfejsie administratora lub w innym miejscu, metoda save () zostanie wywołana bez argumentu skip_lastupdatetime i będzie zachowywać się tak samo, jak wcześniej auto_now.

andreaspelme
źródło
14
TL; DR Nie auto_now_addużywaj defaultzamiast tego.
Thane Brimhall
9
Jedynym zastrzeżeniem w tym przykładzie jest to, że datetime.datetime.nowzwraca naiwną datę i godzinę. Aby użyć datyfrom django.utils import timezone i godziny uwzględniającej strefę czasową, użyj i models.DateTimeField(default=timezone.now)zobacz docs.djangoproject.com/en/1.9/topics/i18n/timezones/ ...
BenjaminGolder
Tak więc, żeby było jasne: jeśli chcesz mieć tylko możliwość modyfikowania createtimepola, nie musisz nadpisywać save(). Wystarczy zastąpić auto_now_add=Truego odpowiednikiem default=timezone.now, editable=False, blank=True(wg dok .). Dwie ostatnie opcje zapewniają podobne zachowanie w panelu administracyjnym.
djvg
28

Skorzystałem z sugestii pytającego i stworzyłem kilka funkcji. Oto przypadek użycia:

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

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

Oto implementacja:

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)

Można utworzyć podobne funkcje, aby je ponownie włączyć.

Frank Henard
źródło
9
W większości przypadków zamiast iteracji prawdopodobnie możesz to zrobić Clazz._meta.get_field_by_name(field_name)[0].
naktinis
Dzięki @naktinis. Zmieniony.
Frank Henard
4
nb (1.10) ModelClass._meta.get_field_by_name (nazwa_pola) [0] w do_to_model nie wydaje się działać dla mnie - zmieniono na: ModelClass._meta.get_field (nazwa_pola)
Georgina S
27

Możesz również użyć update_fieldsparametru save()i przekazać swoje auto_nowpola. Oto przykład:

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

Oto wyjaśnienie z dokumentacji Django: https://docs.djangoproject.com/en/stable/ref/models/instances/#specifying-which-fields-to-save

Dane Apollo
źródło
1
Chciałbym móc głosować wyżej! To naprawdę jest to, czego chciałem (zmienić części modelu bez dotykania pól „auto”), ale niestety nie odpowiada na zadane pytanie (czyli zapisywanie jawnych wartości w polach za pomocą auto_nowi auto_now_add).
Tim Tisdall
1
najlepsza odpowiedź, chciałbym oddać na nią 10 głosów, bardzo proste i eleganckie
Bedros
18

Poszedłem na ścieżkę menedżera kontekstu w celu ponownego wykorzystania.

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

Użyj w ten sposób:

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

Bum.

soulseekah
źródło
8

Dla tych, którzy patrzą na to podczas pisania testów, dostępna jest biblioteka Pythona o nazwie frozenegun, która pozwala sfałszować czas - więc kiedy auto_now_addkod działa, otrzymuje czas, którego faktycznie chcesz. Więc:

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

Może być również używany jako dekorator - zobacz powyższy link do podstawowych dokumentów.

Hamish Downer
źródło
4

Możesz nadpisać auto_now_addbez specjalnego kodu.

Na to pytanie natrafiłem, gdy próbowałem stworzyć obiekt z określoną datą:

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

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

Oto co zrobiłem:

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

To zostało pomyślnie zastąpione auto_now_add.

Bardziej długoterminowym rozwiązaniem savejest metoda zastępowania : https://code.djangoproject.com/ticket/16583

Pavel Vergeev
źródło
2

Musiałem wyłączyć auto_now dla pola DateTime podczas migracji i mogłem to zrobić.

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

źródło
1

Spóźniłem się na imprezę, ale podobnie jak kilka innych odpowiedzi, jest to rozwiązanie, z którego korzystałem podczas migracji bazy danych. Różnica w stosunku do innych odpowiedzi polega na tym, że powoduje to wyłączenie wszystkich pól auto_now dla modelu przy założeniu, że nie ma powodu, aby mieć więcej niż jedną taką zmienną.

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

Następnie, aby go użyć, możesz po prostu zrobić:

disable_auto_now_fields(Document, Event, ...)

I przejdzie przez wszystkie twoje pola auto_nowi auto_now_addwszystkie pola dla wszystkich klas modeli, w których zdasz.

mlissner
źródło
1

Nieco bardziej przejrzysta wersja menedżera kontekstu z 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']

Możesz go używać nawet z fabrykami (fabrykant)

        with suppress_autotime(Click, ['created']):
            ClickFactory.bulk_create(post=obj.post, link=obj.link, created__iter=created)
pymen
źródło
0

kopia Django - Models.DateTimeField - Dynamiczna zmiana wartości auto_now_add

Cóż, spędziłem dzisiejsze popołudnie, aby dowiedzieć się, a pierwszy problem dotyczy tego, jak pobrać obiekt modelu i gdzie w kodzie. Jestem w restframework w serializer.py, na przykład w __init__serializatorze nie mógł jeszcze mieć Modelu. Teraz w to_internal_value możesz pobrać klasę modelu, po pobraniu Field i zmodyfikowaniu właściwości pola jak w tym przykładzie:

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
źródło
0

Potrzebowałem rozwiązania, które będzie działać update_or_create, doszedłem do tego rozwiązania opartego na kodzie @andreaspelme.

Jedyną zmianą jest to, że możesz ustawić pomijanie, ustawiając zmodyfikowane pole na skipnie tylko przez faktyczne przekazanie kwarg skip_modified_updatedo metody save ().

Tylko yourmodelobject.modified='skip'i aktualizacja zostanie pominięta!

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
źródło