model_bakery icon indicating copy to clipboard operation
model_bakery copied to clipboard

Can't set the value of a DateTimeField with auto_now or auto_now_add

Open ukch opened this issue 5 years ago • 9 comments

To reproduce

# models.py
from django.db import models

class MyModel(models.Model):
    some_date = models.DateTimeField(auto_now_add=True)

# test code
from model_mommy import mommy

thing = mommy.make(MyModel, some_date="1985-10-21")
print(thing.some_date)

Expected:

  • The specified date should be printed with time set to midnight

Actual:

  • The current datetime is printed

This is to do with the DateTimeField's pre_save method, which overrides the initial value with the current date if auto_now or auto_now_add are true.

ukch avatar Feb 05 '19 17:02 ukch

@anapaulagomes and @amureki what are your thoughts on this?

berinhard avatar Jan 27 '20 18:01 berinhard

In my opinion, since it is internal Django functionality that was made for certain purpose (like for created/modified fields) we should not mess around it.

To be more specific: See docs: https://docs.djangoproject.com/en/3.0/ref/models/fields/#datefield There is even a next statement:

The auto_now and auto_now_add options will always use the date in the default timezone at the moment of creation or update. If you need something different, you may want to consider using your own callable default or overriding save() instead of using auto_now or auto_now_add; or using a DateTimeField instead of a DateField and deciding how to handle the conversion from datetime to date at display time.

So I would be in for being explicit and consistent behavior, so there would be no difference between using this Django internal or custom app logic which pre_saves some fields values. This logic should be seen and handled in tests. So I would be interested in seeing @ukch needs here and probably would suggest using some helper libraries for such cases, like https://github.com/spulec/freezegun

Now, regarding model_bakery here, I think, if this is something that is not explicit, we might want to track that users are overriding auto_now/auto_now_add values, that would be ignored by Django and throw a warning. But then again, we would not do this for any custom field logic that might appear there in the wild. So I do not know if we want to even do this. Or maybe just documenting this behavior would be enough?

amureki avatar Jan 31 '20 12:01 amureki

Makes sense, @amureki. Of course, we can always add this as a note in the docs but, as you said, model_bakery must have the same behavior as Django. cc @berinhard

anapaulagomes avatar Jan 31 '20 17:01 anapaulagomes

Thanks for the thoughtful explanation @amureki! Sorry for taking to long to read it, but I was changing jobs during these last 2 months and I had to stay a little bit away from the project.

I agree with both strategies, the warning one and also improving our docs to leave this question more explicit. But I think the warning should only be raised if:

The field has auto_now or auto_now_add enabled and the user is passing a custom value for this field via baker.make or baker.prepare. We can even suggest the developer to use freezegun to test the auto_now stuff in the warning message.

Can I close this issue and open a new one with proper instruction on this work @amureki and @anapaulagomes ?

berinhard avatar Feb 27 '20 20:02 berinhard

Hey Bernardo, no worries! I am also quite overwhelmed and was not very active here.

So, I'd be happy about the documentation warning about signals and this behaviour. We could add an extra check for auto_now or auto_now_add also. This brings (in my opinion) a small inconsistency (if I see the lib is covering some signals and throws a warning, I'd expect it to cover all of them - but this is super-tricky), but might be fine, because auto_now or auto_now_add is commonly used and might bring some pain to developers to debug what is going on.

amureki avatar Feb 28 '20 16:02 amureki

In my case this little feature with explicit overriding fields with auto_now_add will be very helpful in tests.

E.g. test which checks that custom filter by date (created recently) works as expected: I created a few instances of a model with different created_at and run filtering against it. Using freezgun is painful in the case when you need to create many models with different values at once.

b0g3r avatar Oct 15 '20 09:10 b0g3r

Besides the point raised by @b0g3r, the freezegun approach would be problematic when auto_now is paired with null=True, which is exactly the case I have on hand right now, so I had to do the following:


# models.py
from django.db import models

class MyModel(models.Model):
    created_at = models.DateTimeField(auto_now=True, null=True)

def ignore_future(today, instances):
    from_future = Q(created_at__gt=today)
    return instances.exclude(from_future)

# test code
from datetime import datetime
from model_bakery.baker import make

class TestMyModel
    def make_my_model(self, **kwargs):
        created_at = kwargs.pop("created_at", None)
        my_model = make(MyModel, **kwargs)
        my_model.created_at = created_at
        my_model.save()
        return my_model

    def test_ignore_the_future(self):
        self.make_my_model()
        self.make_my_model(datetime(2001, 1, 2))
        result = ignore_future(today=datetime(2001, 1, 1), instances=MyModel.objects.all())
        assert [] == list(result)

It makes sense for me to override the created_at value, given that it has been explicitly passed. I don't see how user intention could be different in that case.

What do you all think? Should I go ahead with an implementation proposal?

israelst avatar Mar 16 '21 20:03 israelst

Hey @israelst thank you for following up on this one and providing another use case! 👍

If you have something in mind, we'd be happy to see your implementation proposal, sure! If you want, feel free to open a PR with new failing test cases and do a proposal right there. Otherwise, we can discuss it here if you don't feel confident enough about the codebase.

Cheers, Rust

amureki avatar Mar 20 '21 08:03 amureki

You can set the value of created_at with queryset update function. Breed.objects.filter(pk=8).update(created_at =a)

Ambitiont109 avatar Mar 21 '22 21:03 Ambitiont109

Hey everyone!

I am happy to announce that the solution to this bug was added and released in 1.17.0.

I am going to close this issue, but please, if you have feedback, do not hesitate to post it.

Best, Rust

amureki avatar Oct 27 '23 12:10 amureki