Тимчасово відключити auto_now / auto_now_add


104

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

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 Nope: 'datetime.datetime' об’єкт не має атрибута 'auto_now'
mlissner

2
Варто зазначити, що більше кількох основних розробників виступають заauto_now(_add)
зневажання

new_entry._meta.get_field ('date_update') є більш прямим
Sérgio

Відповіді:


99

Нещодавно я стикався з цією ситуацією під час тестування своєї програми. Мені потрібно було "примусити" мітку часу, що минув. У моєму випадку я зробив трюк, використовуючи оновлення набору запитів. Подобається це:

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

Дякую за цю відповідь. Ось документи для оновлення (): docs.djangoproject.com/en/dev/ref/models/querysets/…
guettli

1
Насправді цей метод працює досить добре, якщо ви не заперечуєте над попаданням у базу даних. Я в кінцевому підсумку використовував це і для тестів.
imjustmatthew

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натомість використання .
Thane Brimhall

9
Одне застереження цього прикладу - datetime.datetime.nowповернення наївного часу. Щоб скористатися датою, визначеною часовим поясом, використовуйте 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)
Georgina S

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/instance/#specifying-which-fields-to-save


1
Я б хотів, щоб я міг проголосувати вище! Це дійсно те, що я хотів (змінити частини моделі, не торкаючись полів "auto"), але, на жаль, не відповідає на задане питання (що полягає у збереженні явних значень для полів за допомогою 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

Для тих, хто дивиться на це, коли вони пишуть тести, є бібліотека пітонів під назвою 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

Я спізнююсь на вечірку, але подібно до кількох інших відповідей, це рішення, яке я використав під час міграції бази даних. Відмінність від інших відповідей полягає в тому, що це вимикає всі поля автоінформації для моделі за умови, що насправді немає причин мати більше одного такого поля.

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 для збереження () методу.

Просто 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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.