Désactiver temporairement auto_now / auto_now_add

105

J'ai un modèle comme celui-ci:

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

Je souhaite écraser les deux champs de date pour certaines instances de modèle (utilisées lors de la migration des données). La solution actuelle ressemble à ceci:

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

Y a-t-il une meilleure solution?

jedie
la source
new_entry.createtime.auto_now = Faux?
akonsu
3
+1 - Ce serait vraiment bien pour les tests
Frank Henard
@akonsu Non: l'objet 'datetime.datetime' n'a pas d'attribut 'auto_now'
mlissner
2
Il convient de souligner que plus de quelques développeurs de base sont en faveur de la dépréciationauto_now(_add)
mlissner
new_entry._meta.get_field ('date_update') est plus direct
Sérgio

Réponses:

100

J'ai récemment rencontré cette situation en testant mon application. J'avais besoin de "forcer" un horodatage expiré. Dans mon cas, j'ai fait l'affaire en utilisant une mise à jour de jeu de requêtes. Comme ça:

# 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
la source
Merci pour cette réponse. Voici les documents pour la mise à jour (): docs.djangoproject.com/en/dev/ref/models/querysets/…
guettli
1
En fait, cette méthode fonctionne plutôt bien si cela ne vous dérange pas d'accéder à la base de données. J'ai fini par l'utiliser également pour des tests.
imjustmatthew
3
de la documentation Django: docs.djangoproject.com/en/1.9/topics/db/queries / ... Sachez que la méthode update () est convertie directement en instruction SQL. Il s'agit d'une opération en masse pour les mises à jour directes. Il n'exécute aucune méthode save () sur vos modèles, ni n'émet les signaux pre_save ou post_save (qui sont une conséquence de l'appel de save ()), ni n'honore l'option de champ
auto_now
@NoamG Je pense que c'est un cas rare où ce update()comportement est exactement ce dont nous avons besoin.
David D.
54

Vous ne pouvez pas vraiment désactiver auto_now / auto_now_add d'une autre manière que vous ne le faites déjà. Si vous avez besoin de flexibilité pour modifier ces valeurs, auto_now/ auto_now_addn'est pas le meilleur choix. Il est souvent plus flexible d'utiliser defaultet / ou de remplacer la save()méthode pour effectuer une manipulation juste avant l'enregistrement de l'objet.

En utilisant defaultet une save()méthode remplacée , une façon de résoudre votre problème serait de définir votre modèle comme ceci:

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)

Dans votre code, où vous souhaitez ignorer le changement automatique de dernière mise à jour, utilisez simplement

new_entry.save(skip_lastupdatetime=True)

Si votre objet est enregistré dans l'interface d'administration ou à d'autres endroits, save () sera appelé sans l'argument skip_lastupdatetime, et il se comportera exactement comme avant avec auto_now.

andreaspelme
la source
14
TL; DR Ne pas auto_now_addutiliser à la defaultplace.
Thane Brimhall
9
Une mise en garde à cet exemple est que datetime.datetime.nowrenvoie un datetime naïf. Pour utiliser un datetime tenant compte du fuseau horaire, utilisez from django.utils import timezoneet models.DateTimeField(default=timezone.now)consultez docs.djangoproject.com/en/1.9/topics/i18n/timezones
BenjaminGolder
Donc, juste pour être clair: si vous voulez seulement pouvoir modifier le createtimechamp, vous n'avez pas besoin de remplacer save(). Il suffit de le remplacer auto_now_add=Truepar l'équivalent default=timezone.now, editable=False, blank=True(selon la documentation ). Les deux dernières options garantissent un comportement similaire dans l'administrateur.
djvg
28

J'ai utilisé la suggestion faite par le demandeur et créé quelques fonctions. Voici le cas d'utilisation:

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

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

Voici la mise en œuvre:

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)

Des fonctions similaires peuvent être créées pour les réactiver.

Frank Henard
la source
9
Dans la plupart des cas, au lieu d'une itération, vous pourriez probablement le faire Clazz._meta.get_field_by_name(field_name)[0].
naktinis
Merci @naktinis. Modifié.
Frank Henard
4
nb (1.10) ModelClass._meta.get_field_by_name (field_name) [0] in do_to_model ne semblait pas fonctionner pour moi - changé en: ModelClass._meta.get_field (field_name)
Georgina S
27

Vous pouvez également utiliser le update_fieldsparamètre de save()et transmettre vos auto_nowchamps. Voici un exemple:

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

Voici l'explication tirée de la documentation de Django: https://docs.djangoproject.com/en/stable/ref/models/instances/#specifying-which-fields-to-save

Données Apollo
la source
1
J'aimerais pouvoir voter plus haut! C'est vraiment ce que je voulais (modifier des parties du modèle sans toucher aux champs 'auto'), mais ne répond malheureusement pas à la question posée (qui est de sauvegarder des valeurs explicites dans les champs en utilisant auto_nowet auto_now_add).
Tim Tisdall
1
meilleure réponse, j'aimerais pouvoir lui donner 10 votes, très simple et élégant
Bedros
18

J'ai opté pour le gestionnaire de contexte pour la réutilisation.

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

Utilisez comme ça:

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

Boom.

soulseekah
la source
8

Pour ceux qui regardent cela lorsqu'ils écrivent des tests, il existe une bibliothèque python appelée freezegun qui vous permet de simuler l'heure - donc lorsque le auto_now_addcode s'exécute, il obtient l'heure que vous voulez réellement. Alors:

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

Il peut également être utilisé comme décorateur - voir le lien ci-dessus pour les documents de base.

Hamish Downer
la source
4

Vous pouvez remplacer auto_now_addsans code spécial.

Je suis tombé sur cette question lorsque j'ai essayé de créer un objet avec une date particulière:

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

publication_date = models.DateField(auto_now_add=True).

Voici donc ce que j'ai fait:

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

Cela a été remplacé avec succès auto_now_add.

En tant que solution à plus long terme, la saveméthode de remplacement est la voie à suivre: https://code.djangoproject.com/ticket/16583

Pavel Vergeev
la source
2

J'avais besoin de désactiver auto_now pour un champ DateTime lors d'une migration et j'ai pu le faire.

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

la source
1

Je suis en retard à la fête, mais similaire à plusieurs des autres réponses, c'est une solution que j'ai utilisée lors d'une migration de base de données. La différence avec les autres réponses est que cela désactive tous les champs auto_now pour le modèle sous l'hypothèse qu'il n'y a vraiment aucune raison d'avoir plus d'un de ces champs.

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

Ensuite, pour l'utiliser, vous pouvez simplement faire:

disable_auto_now_fields(Document, Event, ...)

Et il passera par et nuke tous vos champs auto_nowet auto_now_addpour toutes les classes de modèle que vous transmettez.

mlissner
la source
1

Une version un peu plus propre du gestionnaire de contexte 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']

Vous pouvez l'utiliser même avec des usines (usine-garçon)

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

copie de Django - Models.DateTimeField - Modification dynamique de la valeur auto_now_add

Eh bien, j'ai passé cet après-midi à le découvrir et le premier problème est de savoir comment récupérer l'objet modèle et où dans le code. Je suis en restframework dans serializer.py, par exemple dans le __init__sérialiseur, il ne pourrait pas encore avoir le modèle. Maintenant, dans to_internal_value, vous pouvez obtenir la classe de modèle, après avoir récupéré le champ et après avoir modifié les propriétés du champ comme dans cet exemple:

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
la source
0

J'avais besoin d'une solution qui fonctionnera avec update_or_create, je suis arrivé à cette solution basée sur le code @andreaspelme.

Le seul changement est que vous pouvez définir le saut en définissant le champ modifié sur skipnon seulement en passant kwarg skip_modified_updateà la méthode save ().

Juste yourmodelobject.modified='skip'et la mise à jour sera ignorée!

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
la source