开发者

Temporarily disable auto_now / auto_now_add

I have a model like this:

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

I want to overwrite the two date fields for some model instances (used when migrating data). The current solution looks like this:

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 == "create开发者_开发知识库time":
        field.auto_now_add = True

Is there a better solution?


I've recently faced this situation while testing my application. I needed to "force" an expired timestamp. In my case, I did the trick by using a queryset update. Like this:

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


You can't really disable auto_now/auto_now_add in another way than you already do. If you need the flexibility to change those values, auto_now/auto_now_add is not best choice. It is often more flexible to use default and/or override the save() method to do manipulation right before the object is saved.

Using default and an overridden save() method, one way to solve your problem would be to define your model like this:

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)

In your code, where you want to skip the automatic lastupdatetime change, just use

new_entry.save(skip_lastupdatetime=True)

If your object is saved in the admin interface or other places, save() will be called without the skip_lastupdatetime argument, and it will behave just as it did before with auto_now.


You can also use the update_fields parameter of save() and pass your auto_now fields. Here's an example:

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

Here's the explanation from Django's documentation: https://docs.djangoproject.com/en/stable/ref/models/instances/#specifying-which-fields-to-save


I used the suggestion made by the asker, and created some functions. Here is the use case:

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

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

Here's the implementation:

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)

Similar functions can be created to turn them back on.


I went the context manager way for reusability.

@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 like so:

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

Boom.


For those looking at this when they are writing tests, there is a python library called freezegun which allows you to fake the time - so when the auto_now_add code runs, it gets the time you actually want. 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

It can also be used as a decorator - see the link above for basic docs.


You can override auto_now_add without special code.

I came across this question when I tried to create an object with particular date:

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

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

So this is what I've done:

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

This has successfully overridden auto_now_add.

As a more long-term solution, overriding save method is the way to go: https://code.djangoproject.com/ticket/16583


From django docs

DateField.auto_now_add

Automatically set the field to now when the object is first created. Useful for creation of timestamps. Note that the current date is always used; it’s not just a default value that you can override. So even if you set a value for this field when creating the object, it will be ignored. If you want to be able to modify this field, set the following instead of auto_now_add=True:

For DateField: default=date.today - from datetime.date.today()

For DateTimeField: default=timezone.now - from django.utils.timezone.now()


I needed to disable auto_now for a DateTime field during a migration and was able to do this.

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


I'm late to the party, but similar to several of the other answers, this is a solution I used during a database migration. The difference from the other answers is that this disables all auto_now fields for the model under the assumption that there's really no reason to have more than one such field.

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

Then to use it, you can simply do:

disable_auto_now_fields(Document, Event, ...)

And it will go through and nuke all of your auto_now and auto_now_add fields for all of the model classes you pass in.


A bit more clean version of context manager from https://stackoverflow.com/a/35943149/1731460

NOTE: Do NOT use this context manager in your views/forms or anywhere in your Django app. This context manager alter internal state of field (by temporarily setting auto_now and auto_now_add to False). That will cause Django to not populate these fields with timezone.now() during execution of context manager's body for concurrent requests (ie. same process, different thread).

Although this can be used for standalone scripts (ex. management commands, data migration) which are not run in the same process with Django app.

from contextlib import contextmanager

@contextmanager
def suppress_auto_now(model, field_names=None):
    """
    Temp disable auto_now and auto_now_add for django fields
    @model - model class or instance
    @field_names - list of field names to suppress or all model's
                   fields that support auto_now_add, auto_now"""

    def get_auto_now_fields(user_selected_fields):
        for field in model._meta.get_fields():
            field_name = field.name
            if user_selected_fields and field_name not in user_selected_fields:
                continue
            if hasattr(field, 'auto_now') or hasattr(field, 'auto_now_add'):
                yield field

    fields_state = {}

    for field in get_auto_now_fields(user_selected_fields=field_names):
        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']

You can use it even with Factories (factory-boy)

with suppress_auto_now(Click, ['created']):
    ClickFactory.bulk_create(post=obj.post, link=obj.link, created__iter=created)


copy of Django - Models.DateTimeField - Changing dynamically auto_now_add value

Well , I spent this afternoon find out and the first problem is how fetch model object and where in code . I'm in restframework in serializer.py , for example in __init__ of serializer it could not have the Model yet . Now in to_internal_value you can get the model class , after get the Field and after modify the field properties like in this example :

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


I needed solution that will work with update_or_create, I've came to this solution based on @andreaspelme code.

Only change is that You can set skipping by setting modified field to skip not only by actually passing kwarg skip_modified_update to save() method.

Just yourmodelobject.modified='skip' and update will be skipped!

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)


While not exactly an answer (the question mentions data migration), here is an approach for testing with pytest.

Basically, it is possible to define a fixture to monkeypatch certain field instance attributes. Example may be adapted to loop through fields, etc.

@pytest.fixture
def disable_model_auto_dates(monkeypatch):
    """Disables auto dates on SomeModel."""
    # might be local, might be on top
    from project.models import SomeModel

    field = SomeModel._meta.get_field('created_at')
    monkeypatch.setattr(field, 'auto_now', False)
    monkeypatch.setattr(field, 'auto_now_add', False)


Here's another variation and simplification of the useful answer from @soulseekah https://stackoverflow.com/a/35943149/202168

This one can suppress fields on multiple models simultaneously - useful in conjunction with factory_boy such as when you have a SubFactory that also has fields that need suppressing

It looks like:

@contextmanager
def suppress_autonow(*fields: DeferredAttribute):
    _original_values = {}
    for deferred_attr in fields:
        field = deferred_attr.field
        _original_values[field] = {
            '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, values in _original_values.items():
            field.auto_now = values['auto_now']
            field.auto_now_add = values['auto_now_add']

And is used like (with factory_boy):

with suppress_autonow(Comment.created_at, Post.created_at):
    PostFactory.create_batch(10)  # if e.g. PostFactory also creates Comments

or just Django:

with suppress_autonow(FooBar.createtime, FooBar.lastupdatetime):
    foobar = FooBar(
        createtime=datetime(2013, 4, 6),
        lastupdatetime=datetime(2016, 7, 9),
    )
    foobar.save()

i.e. you pass in the actual fields you want to suppress.

Note that you must pass them as class fields (i.e. Comment.created_at) and not instance fields (not my_comment.created_at)

NOTE: This will break if you pass non-Date/DateTime/Time field to the fields args. if it bothers you, add in an extra isinstance check after field = deferred_attr.field

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜