Desative temporariamente auto_now / auto_now_add

104

Eu tenho um modelo assim:

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

Eu quero substituir os dois campos de data para algumas instâncias de modelo (usados ​​ao migrar dados). A solução atual é semelhante a esta:

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 uma solução melhor?

Jedie
fonte
new_entry.createtime.auto_now = Falso?
Akonsu
3
+1 - Isso seria muito bom para o teste
Frank Henard
@akonsu Não: o objeto 'datetime.datetime' não tem atributo 'auto_now'
mlissner
2
Vale a pena ressaltar que mais do que alguns desenvolvedores centrais são a favor daauto_now(_add)
suspensão
new_entry._meta.get_field ('date_update') é mais direto
Sérgio

Respostas:

99

Recentemente, enfrentei essa situação ao testar meu aplicativo. Eu precisava "forçar" um carimbo de data / hora expirado. No meu caso, fiz o truque usando uma atualização do queryset. Como isso:

# 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
fonte
Obrigado por esta resposta. Aqui estão os documentos para update (): docs.djangoproject.com/en/dev/ref/models/querysets/…
guettli
1
Na verdade, esse método funciona muito bem se você não se importar em acessar o banco de dados. Acabei usando isso para testes também.
imjustmatthew
3
da documentação do Django: docs.djangoproject.com/en/1.9/topics/db/queries/… Esteja ciente de que o método update () é convertido diretamente em uma instrução SQL. É uma operação em massa para atualizações diretas. Ele não executa nenhum método save () em seus modelos, ou emite os sinais pre_save ou post_save (que são uma consequência da chamada de save ()), ou honra a opção de campo
auto_now
@NoamG Acho que este é um caso raro em que esse update()comportamento é exatamente o que precisamos.
David D.
54

Você não pode realmente desabilitar auto_now / auto_now_add de outra forma que você já faz. Se você precisa de flexibilidade para alterar esses valores, auto_now/ auto_now_addnão é a melhor escolha. Freqüentemente, é mais flexível usar defaulte / ou substituir o save()método para fazer a manipulação logo antes do objeto ser salvo.

Usando defaulte um save()método substituído , uma maneira de resolver seu problema seria definir seu modelo assim:

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)

Em seu código, onde você deseja pular a mudança automática de última atualização, basta usar

new_entry.save(skip_lastupdatetime=True)

Se o seu objeto for salvo na interface de administração ou em outros lugares, save () será chamado sem o argumento skip_lastupdatetime e se comportará exatamente como fazia antes com auto_now.

andreaspelme
fonte
14
TL; DR Não use auto_now_adduse em defaultvez disso.
Thane Brimhall
9
Uma ressalva a este exemplo é que datetime.datetime.nowretorna uma data e hora ingênua. Para usar uma data e hora com reconhecimento de fuso horário, use from django.utils import timezonee models.DateTimeField(default=timezone.now)consulte docs.djangoproject.com/en/1.9/topics/i18n/timezones/…
BenjaminGolder
Portanto, só para deixar claro: se você deseja apenas modificar o createtimecampo, não precisa fazer a substituição save(). Basta substituir auto_now_add=Truepelo equivalente default=timezone.now, editable=False, blank=True(conforme docs ). As duas últimas opções garantem um comportamento semelhante no administrador.
djvg de
28

Usei a sugestão do autor da pergunta e criei algumas funções. Aqui está o 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()

Esta é a implementação:

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)

Funções semelhantes podem ser criadas para ativá-los novamente.

Frank henard
fonte
9
Na maioria dos casos, em vez de iteração, você provavelmente poderia apenas fazer Clazz._meta.get_field_by_name(field_name)[0].
naktinis
Obrigado @naktinis. Alterado.
Frank Henard
4
nb (1.10) ModelClass._meta.get_field_by_name (field_name) [0] em do_to_model não parecia estar funcionando para mim - alterado para: ModelClass._meta.get_field (field_name)
Georgina S
27

Você também pode usar o update_fieldsparâmetro de save()e passar seus auto_nowcampos. Aqui está um exemplo:

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

Aqui está a explicação da documentação do Django: https://docs.djangoproject.com/en/stable/ref/models/instances/#specifying-which-fields-to-save

Apollo Data
fonte
1
Eu gostaria de poder votar mais alto! Isso é realmente o que eu queria (alterar partes do modelo sem tocar nos campos 'auto'), mas infelizmente não respondeu à pergunta dada (que é salvar valores explícitos para os campos usando auto_nowe auto_now_add).
Tim Tisdall,
1
melhor resposta, gostaria de poder dar-lhe 10 votos, muito simples e elegante
Bedros
18

Eu escolhi o caminho do gerenciador de contexto para reutilização.

@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 assim:

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

Estrondo.

Soulseekah
fonte
8

Para aqueles que observam isso quando estão escrevendo testes, existe uma biblioteca python chamada freezegun, que permite falsificar o tempo - então, quando o auto_now_addcódigo é executado, ele obtém o tempo que você realmente deseja. Assim:

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

Ele também pode ser usado como um decorador - consulte o link acima para obter os documentos básicos.

Hamish Downer
fonte
4

Você pode substituir auto_now_add sem código especial.

Eu me deparei com esta pergunta quando tentei criar um objeto com uma data específica:

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

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

Então foi isso que eu fiz:

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

Isto foi substituído com sucesso auto_now_add .

Como uma solução de longo prazo, o savemétodo de substituição é o caminho a seguir: https://code.djangoproject.com/ticket/16583

Pavel Vergeev
fonte
2

Eu precisei desabilitar auto_now para um campo DateTime durante uma migração e consegui fazer isso.

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

fonte
1

Estou atrasado para a festa, mas semelhante a várias das outras respostas, esta é uma solução que usei durante uma migração de banco de dados. A diferença das outras respostas é que isso desativa todos os campos auto_now para o modelo sob a suposição de que realmente não há razão para ter mais de um desses 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

Então, para usá-lo, você pode simplesmente fazer:

disable_auto_now_fields(Document, Event, ...)

E vai passar e destruir todos os seus campos auto_nowe auto_now_addpara todas as classes de modelo que você passar.

mlissner
fonte
1

Uma versão um pouco mais limpa do gerenciador 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']

Você pode usá-lo mesmo com fábricas (menino de fábrica)

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

cópia do Django - Models.DateTimeField - Alterando dinamicamente o valor auto_now_add

Bem, passei esta tarde descobrindo e o primeiro problema é como buscar objeto de modelo e onde está no código. Estou no restframework no serializer.py, por exemplo no __init__do serializer não poderia ter o Model ainda. Agora em to_internal_value você pode obter a classe do modelo, após obter o campo e depois modificar as propriedades do campo como neste exemplo:

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

Eu precisava de uma solução que funcionasse update_or_create, eu vim para essa solução baseada no código @andreaspelme.

A única mudança é que Você pode definir o salto definindo o campo modificado para skipnão apenas passando o kwargskip_modified_update para o método save ().

Apenas yourmodelobject.modified='skip'e a atualização será ignorada!

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
fonte