Временно отключить auto_now / auto_now_add

105

У меня такая модель:

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

Я хочу перезаписать два поля даты для некоторых экземпляров модели (используемых при переносе данных). Текущее решение выглядит так:

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

Есть ли лучшее решение?

джедай
источник
new_entry.createtime.auto_now = Неверно?
akonsu
3
+1 - Это было бы действительно неплохо для тестирования
Франк Хенард
@akonsu Нет: объект 'datetime.datetime' не имеет атрибута 'auto_now'
mlissner
2
Стоит отметить, что более чем несколько основных разработчиков выступают заauto_now(_add)
отказ
new_entry._meta.get_field ('date_update') более прямой
Сержио

Ответы:

101

Недавно я столкнулся с такой ситуацией при тестировании своего приложения. Мне нужно было «принудительно» установить истекшую временную метку. В моем случае я сделал трюк, используя обновление набора запросов. Как это:

# 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.
дирлейрлы
источник
Спасибо за этот ответ. Вот документы для update (): docs.djangoproject.com/en/dev/ref/models/querysets/…
guettli
1
На самом деле, этот метод работает очень хорошо, если вы не против обращения к базе данных. В итоге я использовал это и для тестов.
imjustmatthew 02
3
из документации Django: docs.djangoproject.com/en/1.9/topics/db/queries/… Имейте в виду, что метод update () преобразуется непосредственно в оператор SQL. Это массовая операция для прямых обновлений. Он не запускает никаких методов save () на ваших моделях, не излучает сигналы pre_save или post_save (которые являются следствием вызова save ()) и не учитывает параметр поля
auto_now
@NoamG Я думаю, что это редкий случай, когда такое update()поведение как раз то, что нам нужно.
Дэвид Д.
54

Вы не можете отключить auto_now / auto_now_add другим способом, чем вы уже это сделали. Если вам нужна гибкость для изменения этих значений, auto_now/ auto_now_add- не лучший выбор. Часто бывает более гибким использование defaultи / или переопределение save()метода для выполнения манипуляций непосредственно перед сохранением объекта.

Используя defaultзамещенный save()метод, одним из способов решения вашей проблемы было бы определение вашей модели следующим образом:

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)

В вашем коде, где вы хотите пропустить автоматическое изменение времени последнего обновления, просто используйте

new_entry.save(skip_lastupdatetime=True)

Если ваш объект сохранен в интерфейсе администратора или в другом месте, save () будет вызываться без аргумента skip_lastupdatetime, и он будет вести себя так же, как и раньше, с auto_now.

Андреаспельме
источник
14
TL; DR Не auto_now_addиспользуйте defaultвместо этого.
Тейн Бримхолл
9
Одно предостережение в этом примере заключается в том, что он datetime.datetime.nowвозвращает наивное datetime. Чтобы использовать датуfrom django.utils import timezone и время с учетом часового пояса, используйте и models.DateTimeField(default=timezone.now)см. Docs.djangoproject.com/en/1.9/topics/i18n/timezones/…
BenjaminGolder
Итак, для ясности: если вы хотите иметь возможность только изменять createtimeполе, вам не нужно переопределять save(). Достаточно заменить auto_now_add=Trueна аналог default=timezone.now, editable=False, blank=True(согласно документации ). Последние два параметра обеспечивают аналогичное поведение в админке.
djvg
28

Я использовал предложение, сделанное спрашивающим, и создал несколько функций. Вот пример использования:

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

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

Вот реализация:

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)

Можно создать аналогичные функции, чтобы снова включить их.

Франк Хенард
источник
9
В большинстве случаев вместо итераций вы могли бы просто сделать Clazz._meta.get_field_by_name(field_name)[0].
naktinis
Спасибо @naktinis. Изменено.
Фрэнк Хенард
4
nb (1.10) ModelClass._meta.get_field_by_name (field_name) [0] в do_to_model, похоже, не работает для меня - изменено на: ModelClass._meta.get_field (field_name)
Джорджина С.
27

Вы также можете использовать update_fieldsпараметр save()и передавать свои auto_nowполя. Вот пример:

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

Вот объяснение из документации Django: https://docs.djangoproject.com/en/stable/ref/models/instances/#specifying-which-fields-to-save

Данные Аполлона
источник
1
Хотел бы я проголосовать выше! Это действительно то, что я хотел (изменить части модели, не касаясь полей «авто»), но, к сожалению, не ответил на заданный вопрос (который заключается в сохранении явных значений в полях с помощью auto_nowи auto_now_add).
Тим
1
лучший ответ, я бы хотел дать ему 10 голосов, очень простой и элегантный
Бедрос
18

Я пошел путем диспетчера контекста для повторного использования.

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

Используйте так:

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

Бум.

поиск душ
источник
8

Для тех, кто смотрит на это при написании тестов, есть библиотека Python под названием freezegun, которая позволяет имитировать время, поэтому, когда auto_now_addкод запускается, он получает время, которое вы действительно хотите. Так:

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

Его также можно использовать в качестве декоратора - см. Ссылку выше для получения базовой документации.

Хэмиш Даунер
источник
4

Вы можете переопределить auto_now_add без специального кода.

Я столкнулся с этим вопросом, когда пытался создать объект с определенной датой:

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

где publication_date = models.DateField(auto_now_add=True) .

Вот что я сделал:

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

Это успешно отменено auto_now_add .

В качестве более долгосрочного решения можно использовать saveметод переопределения : https://code.djangoproject.com/ticket/16583

Павел Вергеев
источник
2

Мне нужно было отключить auto_now для поля DateTime во время миграции, и я смог это сделать.

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

Я опаздываю на вечеринку, но, как и некоторые другие ответы, это решение, которое я использовал во время миграции базы данных. Отличие от других ответов заключается в том, что это отключает все поля auto_now для модели при условии, что на самом деле нет причин иметь более одного такого поля.

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

Затем, чтобы использовать его, вы можете просто сделать:

disable_auto_now_fields(Document, Event, ...)

И он будет идти до конца и ядерного оружия всех ваших auto_nowи auto_now_addполей для всех классов моделей , которые Вы передаете в.

Млисснер
источник
1

Немного более чистая версия диспетчера контекста из 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']

Вы можете использовать его даже с фабриками (заводчик)

        with suppress_autotime(Click, ['created']):
            ClickFactory.bulk_create(post=obj.post, link=obj.link, created__iter=created)
пимен
источник
0

копия Django - Models.DateTimeField - Динамическое изменение значения auto_now_add

Итак, сегодня днем ​​я выяснил, и первая проблема - это как получить объект модели и где в коде. Я нахожусь в restframework в serializer.py, например, в __init__сериализаторе еще не может быть Модель. Теперь в to_internal_value вы можете получить класс модели после получения поля и после изменения свойств поля, как в этом примере:

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

Мне нужно было решение, которое будет работать с update_or_create , я пришел к этому решению на основе кода @andreaspelme.

Единственное изменение состоит в том, что вы можете установить пропуск, установив для измененного поля skipне только фактическую передачу kwargskip_modified_update save ().

Просто yourmodelobject.modified='skip'и обновление будет пропущено!

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
источник