I am testing whether my blog posts are in reverse chronological order. To do so, I must set random dates for each post created. I'm using faker
to set the dates. I am getting back a fake date, but it's the same date for every post. Is auto now still the issue here, or am I not using Faker correctly?
Factory:
fake = Faker()
mocked = fake.date_time()
class BlogPageFactory(wagtail_factories.PageFactory):
class Meta:
model = models.BlogPage
with patch('django.utils.timezone.now', mocked):
date = mocked
# date = datetime.date.today()
author = factory.SubFactory(UserFactory)
slug = factory.sequence(lambda n: f"post{n}")
snippet = factory.sequence(lambda n: f"Article {n} snippet...")
body = "Test post..."
featured_image = factory.SubFactory(wagtail_factories.ImageFactory)
featured_article = False
Models:
class BlogPage(Page):
date = models.DateField("Post date")
snippet = models.CharField(
max_length=250, help_text="Excerpt used in article list preview card."
)
body = RichTextField(blank=True)
tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
featured_image = models.ForeignKey("wagtailimages.Image", on_delete=models.CASCADE)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
featured_article = models.BooleanField(default=False)
content_panels = Page.content_panels + [
MultiFieldPanel(
[
FieldPanel("date"),
FieldPanel("tags"),
],
heading="Blog Information",
),
FieldPanel("snippet"),
FieldPanel("featured_image"),
FieldPanel("body"),
FieldPanel("author"),
InlinePanel("page_comments", label="Comments"),
]
search_fields = Page.search_fields + [index.SearchField("body")]
parent_page_types = ["CategoryIndexPage"]
subpage_types = []
def serve(self, request, *args, **kwargs):
"""
Method override to handle POST conditions for blog comments, ``BlogComment``.
"""
from .forms import CommentForm
if request.method == "POST":
form = CommentForm(request.POST)
if form.is_valid():
new_comment = form.save(commit=False)
new_comment.user = request.user
new_comment.page = self
new_comment.save()
messages.success(
request,
"Your message was successfully "
"submitted and is awaiting moderation. "
"Thank you for contributing!",
)
return redirect(self.get_url())
else:
form = CommentForm
return render(request, "blog/blog_page.html", {"page": self, "form": form})
There are two issues with your current code, leading to the problem you meet:
mocked
date at module import;auto_now
with your patch(..., mocked)
mocked
dateThe issue comes from the way your code is written; you compute the mocked
date at module import time — it is thus always the same for all future calls.
In order to get a dynamic value, you need to use one of factory_boy's dedicated declarations — they are evaluated each time the factory is asked to generate an instance.
I recommend taking a look at another of my answers for a more in-depth explanation.
date
in the modelHere, your model field declaration is a "standard" Django field, with no magic; there is no need to patch django.utils.timezone.now()
, since passing date=mocked
to BlogPage.objects.create(...)
will work. By the way, this is exactly the call performed by factory_boy under the hood.
If you had a model with a auto_now_add
, Django does not allow overriding the value — as seen in their docs.
This would leave you with two possible options:
Replace auto_now_add=True
with default=timezone.now
: when the default
is a callable, Django will execute the callable for each new instance of the model, unless the caller provided a value — as you would be doing with the factory;
If that isn't possible, you could instead override the _create()
method:
class BlogPageFactory(factory.django.DjangoModelFactory):
...
date = factory.Faker("date_time")
@classmethod
def _create(cls, model_class, *args, **kwargs):
# Extract the "date" kwarg, as an explicit value will be ignore
# by auto_now_add
date = kwargs.pop("date")
# Force `timezone.now()` to always return the expected date
# for the duration of the instance creation
with patch("django.utils.timezone.now", lambda: date):
# Let factory_boy actually create the instance
return super()._create(model_class, *args, **kwargs)