From 4170a1f90bac926a966fe908db09b1497c64c774 Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Wed, 4 Feb 2026 19:17:04 -0600 Subject: [PATCH 1/2] Switch to factory boy. --- project/data/author.py | 8 +- project/data/category.py | 17 +-- project/data/factories.py | 136 ++++++++++++++++++++ project/data/markdown.py | 140 +++++++-------------- project/data/subscribers.py | 71 +++-------- project/data/subscription_notifications.py | 24 ++-- pyproject.toml | 1 + 7 files changed, 226 insertions(+), 171 deletions(-) create mode 100644 project/data/factories.py diff --git a/project/data/author.py b/project/data/author.py index 22d32ed..e7b5edc 100644 --- a/project/data/author.py +++ b/project/data/author.py @@ -1,12 +1,14 @@ from django.contrib.auth.models import User +from project.data.factories import SuperUserFactory + def generate_data() -> User: if user := User.objects.filter(is_superuser=True).order_by("date_joined").first(): return user - user, _ = User.objects.get_or_create( + return SuperUserFactory( username="default_user", - defaults={"first_name": "Django", "last_name": "Pythonista"}, + first_name="Django", + last_name="Pythonista", ) - return user diff --git a/project/data/category.py b/project/data/category.py index 3abe8c1..890df22 100644 --- a/project/data/category.py +++ b/project/data/category.py @@ -1,5 +1,6 @@ from dataclasses import dataclass +from project.data.factories import CategoryFactory from project.newsletter.models import Category @@ -12,18 +13,10 @@ class CategoryData: def generate_data() -> CategoryData: - social, _ = Category.objects.update_or_create( - slug="social", defaults={"title": "Social"} - ) - technical, _ = Category.objects.update_or_create( - slug="technical", defaults={"title": "Technical"} - ) - career, _ = Category.objects.update_or_create( - slug="career", defaults={"title": "Career"} - ) - family, _ = Category.objects.update_or_create( - slug="family", defaults={"title": "Family"} - ) + social = CategoryFactory(slug="social", title="Social") + technical = CategoryFactory(slug="technical", title="Technical") + career = CategoryFactory(slug="career", title="Career") + family = CategoryFactory(slug="family", title="Family") return CategoryData( career=career, family=family, diff --git a/project/data/factories.py b/project/data/factories.py new file mode 100644 index 0000000..36f6a8b --- /dev/null +++ b/project/data/factories.py @@ -0,0 +1,136 @@ +from datetime import UTC, timedelta + +import factory +from django.contrib.auth.models import User +from django.utils import timezone +from faker import Faker +from faker.utils.text import slugify +from mdgen import MarkdownPostProvider +from mdgen.core import MarkdownImageGenerator + +from project.newsletter.models import ( + Category, + Post, + Subscription, + SubscriptionNotification, +) + +fake = Faker() +fake.add_provider(MarkdownPostProvider) +Faker.seed(2022) +image_generator = MarkdownImageGenerator() + +DATA_END_DATE = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) +DATA_START_DATE = DATA_END_DATE - timedelta(days=365 * 2) + + +def _header(level=1): + lead = "#" * level + return lead + fake.sentence() + + +def _short_photo_update(): + return "\n\n".join( + [ + _header(level=1), + fake.paragraph(), + image_generator.new_image( + fake.sentence(), + f"https://picsum.photos/{fake.pyint(200, 500)}", + fake.text(), + ), + fake.paragraph(), + ] + ) + + +class UserFactory(factory.django.DjangoModelFactory): + class Meta: + model = User + + first_name = factory.Faker("first_name") + last_name = factory.Faker("last_name") + username = factory.LazyAttribute( + lambda o: f"{o.first_name}.{o.last_name}.{fake.pyint(max_value=999)}" + ) + email = factory.LazyAttribute(lambda o: f"{o.username}@example.com") + date_joined = factory.LazyFunction( + lambda: fake.date_time_between_dates( + datetime_start=DATA_START_DATE, + datetime_end=DATA_END_DATE, + tzinfo=UTC, + ) + ) + + +class SuperUserFactory(UserFactory): + is_superuser = True + is_staff = True + + +class CategoryFactory(factory.django.DjangoModelFactory): + class Meta: + model = Category + django_get_or_create = ("slug",) + + title = factory.Faker("word") + slug = factory.LazyAttribute(lambda o: slugify(o.title)) + + +class PostFactory(factory.django.DjangoModelFactory): + class Meta: + model = Post + + title = factory.Faker("sentence") + slug = factory.LazyAttribute( + lambda o: slugify(o.title) + f"-{fake.pyint(10, 99999)}" + ) + author = factory.SubFactory(UserFactory) + content = factory.LazyFunction(lambda: fake.post("medium")) + summary = factory.Faker("paragraph") + is_public = True + is_published = True + publish_at = factory.LazyFunction( + lambda: fake.date_time_between_dates( + datetime_start=DATA_START_DATE, + datetime_end=DATA_END_DATE, + tzinfo=UTC, + ) + ) + + @factory.post_generation + def categories(self, create, extracted, **kwargs): + if not create: + return + if extracted: + for category in extracted: + self.categories.add(category) + + +class ImagePostFactory(PostFactory): + content = factory.LazyFunction(_short_photo_update) + + +class SubscriptionFactory(factory.django.DjangoModelFactory): + class Meta: + model = Subscription + + user = factory.SubFactory(UserFactory) + + @factory.post_generation + def categories(self, create, extracted, **kwargs): + if not create: + return + if extracted: + for category in extracted: + self.categories.add(category) + + +class SubscriptionNotificationFactory(factory.django.DjangoModelFactory): + class Meta: + model = SubscriptionNotification + + subscription = factory.SubFactory(SubscriptionFactory) + post = factory.SubFactory(PostFactory) + sent = None + read = None diff --git a/project/data/markdown.py b/project/data/markdown.py index 0775e6f..5b88eb0 100644 --- a/project/data/markdown.py +++ b/project/data/markdown.py @@ -1,74 +1,30 @@ -from datetime import UTC, datetime +from datetime import timedelta from itertools import cycle -from faker import Faker -from faker.utils.text import slugify -from mdgen import MarkdownPostProvider -from mdgen.core import MarkdownImageGenerator - +from project.data.factories import DATA_END_DATE, ImagePostFactory, PostFactory from project.newsletter.models import Post -fake = Faker() -fake.add_provider(MarkdownPostProvider) -# Seed the randomization to support consistent randomization. -Faker.seed(2022) -image_generator = MarkdownImageGenerator() - +POST_COUNT = 1500 -def header(level=1): - lead = "#" * level - return lead + fake.sentence() - - -def short_photo_update(): - return "\n\n".join( - [ - header(level=1), - fake.paragraph(), - image_generator.new_image( - fake.sentence(), - f"https://picsum.photos/{fake.pyint(200, 500)}", - fake.text(), - ), - fake.paragraph(), - ] - ) +RECENT_POSTS = [ + {"title": "Welcome to Our Newsletter", "slug": "welcome-to-our-newsletter"}, + {"title": "Getting Started with Python", "slug": "getting-started-with-python"}, + {"title": "Advanced Django Techniques", "slug": "advanced-django-techniques"}, + {"title": "Database Optimization Tips", "slug": "database-optimization-tips"}, + {"title": "Testing Best Practices", "slug": "testing-best-practices"}, + {"title": "Debugging Like a Pro", "slug": "debugging-like-a-pro"}, + {"title": "Code Review Guidelines", "slug": "code-review-guidelines"}, + {"title": "Deployment Strategies", "slug": "deployment-strategies"}, + {"title": "Security Fundamentals", "slug": "security-fundamentals"}, + {"title": "Performance Monitoring", "slug": "performance-monitoring"}, +] def generate_data(user, image_category, post_categories): - image_posts = [] - for i in range(1500): - title = fake.sentence() - slug = slugify(title) + f"-{fake.pyint(10, 99999)}" - publish_at = ( - fake.date_time_between_dates( - datetime_start=datetime(2020, 1, 1, tzinfo=UTC), - datetime_end=datetime(2022, 10, 12, tzinfo=UTC), - tzinfo=UTC, - ) - if fake.pybool() - else None - ) - created = fake.date_time_between_dates( - datetime_start=datetime(2020, 1, 1, tzinfo=UTC), - datetime_end=datetime(2022, 10, 12, tzinfo=UTC), - tzinfo=UTC, - ) - if publish_at and publish_at < created: - created = publish_at - image_posts.append( - Post( - created=created, - author=user, - title=title, - slug=slug, - summary=fake.paragraph(), - content=short_photo_update(), - is_public=True, - is_published=True, - publish_at=publish_at, - ) - ) + image_posts = ImagePostFactory.build_batch( + POST_COUNT, + author=user, + ) Post.objects.bulk_create(image_posts, batch_size=500, ignore_conflicts=True) category_through = Post.categories.through @@ -83,39 +39,10 @@ def generate_data(user, image_category, post_categories): ignore_conflicts=True, ) - general_posts = [] - for i in range(1500): - title = fake.sentence() - slug = slugify(title) + f"-{fake.pyint(10, 9999)}" - publish_at = ( - fake.date_time_between_dates( - datetime_start=datetime(2020, 1, 1, tzinfo=UTC), - datetime_end=datetime(2022, 10, 12, tzinfo=UTC), - tzinfo=UTC, - ) - if fake.pybool() - else None - ) - created = fake.date_time_between_dates( - datetime_start=datetime(2020, 1, 1, tzinfo=UTC), - datetime_end=datetime(2022, 10, 12, tzinfo=UTC), - tzinfo=UTC, - ) - if publish_at and publish_at < created: - created = publish_at - general_posts.append( - Post( - created=created, - author=user, - title=title, - slug=slug, - summary=fake.paragraph(), - content=fake.post("medium"), - is_public=True, - is_published=True, - publish_at=publish_at, - ) - ) + general_posts = PostFactory.build_batch( + POST_COUNT, + author=user, + ) Post.objects.bulk_create(general_posts, batch_size=500, ignore_conflicts=True) category_cycle = cycle(post_categories) @@ -129,3 +56,24 @@ def generate_data(user, image_category, post_categories): batch_size=500, ignore_conflicts=True, ) + + recent_posts = [ + PostFactory.build( + author=user, + title=post_data["title"], + slug=post_data["slug"], + publish_at=DATA_END_DATE - timedelta(days=i), + created=DATA_END_DATE - timedelta(days=i), + ) + for i, post_data in enumerate(RECENT_POSTS) + ] + Post.objects.bulk_create(recent_posts, ignore_conflicts=True) + + category_cycle = cycle(post_categories) + category_through.objects.bulk_create( + [ + category_through(category_id=next(category_cycle).id, post_id=post.id) + for post in recent_posts + ], + ignore_conflicts=True, + ) diff --git a/project/data/subscribers.py b/project/data/subscribers.py index 676db61..43034bd 100644 --- a/project/data/subscribers.py +++ b/project/data/subscribers.py @@ -1,42 +1,21 @@ from collections import OrderedDict -from datetime import UTC, datetime from functools import partial -from django.contrib.auth.models import User from faker import Faker from project.data.category import CategoryData +from project.data.factories import SubscriptionFactory, UserFactory from project.newsletter.models import Subscription fake = Faker() -# Seed the randomization to support consistent randomization. Faker.seed(2022) USER_COUNT = 100 def generate_data(categories: CategoryData): - for i in range(0, USER_COUNT, 100): - users = [] - for _ in range(i, i + 100): - user = User(first_name=fake.first_name(), last_name=fake.last_name()) - user.username = ( - f"{user.first_name}.{user.last_name}.{fake.pyint(max_value=999)}" - ) - user.email = f"{user.username}@example.com" - user.date_joined = fake.date_time_between_dates( - datetime_start=datetime(2020, 1, 1, tzinfo=UTC), - datetime_end=datetime(2022, 10, 12, tzinfo=UTC), - tzinfo=UTC, - ) - users.append(user) - User.objects.bulk_create(users, ignore_conflicts=True) - user_ids = ( - User.objects.exclude(is_staff=True) - .filter(email__endswith="@example.com") - .order_by("username") - .values_list("id", flat=True) - ) + users = UserFactory.create_batch(USER_COUNT) + category_ids = OrderedDict( [ (categories.career.id, 0.3), @@ -45,34 +24,22 @@ def generate_data(categories: CategoryData): (categories.technical.id, 0.7), ] ) + category_map = { + categories.career.id: categories.career, + categories.family.id: categories.family, + categories.social.id: categories.social, + categories.technical.id: categories.technical, + } get_category_ids = partial(fake.random_elements, elements=category_ids, unique=True) - for i in range(0, USER_COUNT, 50): - created_map = { - user_id: fake.date_time_between_dates( - datetime_start=datetime(2020, 1, 1, tzinfo=UTC), - datetime_end=datetime(2022, 10, 12, tzinfo=UTC), - tzinfo=UTC, - ) - for user_id in user_ids[i : i + 50] - } - Subscription.objects.bulk_create( - [Subscription(user_id=user_id) for user_id in created_map.keys()], - ignore_conflicts=True, - ) - subscriptions = list(Subscription.objects.filter(user__in=user_ids[i : i + 50])) - for subscription in subscriptions: - subscription.created = subscription.updated = created_map[ - subscription.user_id - ] - Subscription.objects.bulk_update(subscriptions, fields=["created", "updated"]) - - through_model = Subscription.categories.through - through_model.objects.bulk_create( - [ - through_model(subscription_id=subscription.id, category_id=category_id) - for subscription in subscriptions - for category_id in get_category_ids() - ], - ignore_conflicts=True, + for user in users: + if Subscription.objects.filter(user=user).exists(): + continue + selected_category_ids = get_category_ids() + selected_categories = [category_map[cid] for cid in selected_category_ids] + SubscriptionFactory( + user=user, + created=user.date_joined, + updated=user.date_joined, + categories=selected_categories, ) diff --git a/project/data/subscription_notifications.py b/project/data/subscription_notifications.py index b7a4be6..5fbf25a 100644 --- a/project/data/subscription_notifications.py +++ b/project/data/subscription_notifications.py @@ -1,5 +1,6 @@ from django.db.models import F +from project.data.factories import SubscriptionNotificationFactory from project.newsletter.models import Post, Subscription, SubscriptionNotification @@ -16,14 +17,21 @@ def generate_data(): .distinct() ) - SubscriptionNotification.objects.bulk_create( - [ - SubscriptionNotification(post=post, subscription_id=id, sent=date) - for id in subscriber_ids - for post in posts - ], - batch_size=10000, - ) + notifications = [] + for subscription_id in subscriber_ids: + for post in posts: + if not SubscriptionNotification.objects.filter( + post=post, subscription_id=subscription_id + ).exists(): + notifications.append( + SubscriptionNotificationFactory.build( + post=post, + subscription_id=subscription_id, + sent=date, + ) + ) + + SubscriptionNotification.objects.bulk_create(notifications, batch_size=10000) SubscriptionNotification.objects.filter(post__in=posts).update( created=F("sent"), updated=F("sent"), diff --git a/pyproject.toml b/pyproject.toml index 1a47dc5..e42139f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "django-debug-toolbar", "django-environ", "django-registration-redux", + "factory_boy", "faker", "martor", "mdgen", From 04994e0c1fa7270c130d7ee1392251480a6954d4 Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Wed, 4 Feb 2026 19:30:08 -0600 Subject: [PATCH 2/2] Start the data generation the day before. Fix the lab1 documentation to refer to the correct pieces of data. --- README.md | 4 +-- docs/lab1.md | 20 ++++++------- docs/lab2.md | 4 +-- project/data/factories.py | 7 ++++- project/data/markdown.py | 62 +++++++++++++++++++++++++++------------ 5 files changed, 62 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index fde847d..c92faf0 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ please scroll down to [Windows non-WSL Setup](#windows-non-wsl-setup). 10. Verify the following pages load: * http://127.0.0.1:8000/ * http://127.0.0.1:8000/p/ - * http://127.0.0.1:8000/p/term-writer-recognize-race-available-5291/ + * http://127.0.0.1:8000/p/welcome-to-our-newsletter/ 11. Log into the admin ([link](http://127.0.0.1:8000/admin/)) with your superuser. 12. Verify the following pages load: * http://127.0.0.1:8000/post/create/ @@ -183,7 +183,7 @@ Proceed to [Lab 1](docs/lab1.md). 10. Verify the following pages load: * http://127.0.0.1:8000/ * http://127.0.0.1:8000/p/ - * http://127.0.0.1:8000/p/term-writer-recognize-race-available-5291/ + * http://127.0.0.1:8000/p/welcome-to-our-newsletter/ 11. Log into the admin ([link](http://127.0.0.1:8000/admin/)) with your superuser. 12. Verify the following pages load: * http://127.0.0.1:8000/post/create/ diff --git a/docs/lab1.md b/docs/lab1.md index 792a029..4ddf413 100644 --- a/docs/lab1.md +++ b/docs/lab1.md @@ -13,11 +13,11 @@ git checkout lab-1.1 ### Report -The detail view for the post "Term writer recognize race available." is broken. +The detail view for the post "Welcome to Our Newsletter" is broken. To reproduce: 1. Browse to the [posts page](http://127.0.0.1:8000/p/). -2. Click on "Read" for the post with the title "Term writer recognize race available." +2. Click on "Read" for the post with the title "Welcome to Our Newsletter" 3. "It doesn't work!" ### Facts @@ -28,7 +28,7 @@ Let's consider what we know: QuerySet does not contain a Post matching the filters. - The line that causes the error is on line 80:``post = posts.get(title=lookup)`` - We know the post exists, we can find it in the - [admin](http://127.0.0.1:8000/admin/newsletter/post/?q=Term+writer+recognize+race+available.) + [admin](http://127.0.0.1:8000/admin/newsletter/post/?q=Welcome+to+Our+Newsletter) - This impacts more than just the post in the report. The detail view is broken for all posts. @@ -63,7 +63,7 @@ printed the value in the view ``print(lookup)``. From there, we would have had t recognize that ``lookup`` wasn't the actual title. This may have required us to compare the ``lookup`` value to instance's values in the admin. We can view these values by clicking on the instance from our -[admin search from earlier](http://127.0.0.1:8000/admin/newsletter/post/?q=Term+writer+recognize+race+available.). +[admin search from earlier](http://127.0.0.1:8000/admin/newsletter/post/?q=Welcome+to+Our+Newsletter). @@ -230,11 +230,9 @@ order of most recent to oldest, but they appear jumbled. To reproduce: 1. Browse to the [list posts](http://127.0.0.1:8000/p/) view. -2. The dates are ordered from most recent to oldest, but posts such as - "Campaign expect page information wrong more." and "Example - become begin wish painting economic." - appear out of order in comparison to "Skill fight girl north - production thus a." and "New star by chair environmental family Congress degree." +2. The dates are ordered from most recent to oldest, but the post + "Getting Started with Python" + appears out of order in comparison to "Welcome to Our Newsletter" 3. "It doesn't work!" ### Facts @@ -290,8 +288,8 @@ This is root problem since the collection of posts are actually ordered based on The posts are being ordered correctly, ``publish_at`` first, falling back to ``created`` when unset. Therefore the template must be rendering incorrectly. This can be confirmed by comparing the fields of the posts that render -[correctly](http://127.0.0.1:8000/admin/newsletter/post/?slug=hear-after-debate-thousand-medical-give-85694) -and [incorrectly](http://127.0.0.1:8000/admin/newsletter/post/?slug=add-they-debate-guess-leg-21809). +[correctly](http://127.0.0.1:8000/admin/newsletter/post/?slug=getting-started-with-python) +and [incorrectly](http://127.0.0.1:8000/admin/newsletter/post/?slug=welcome-to-our-newsletter). From the admin, we can see the correctly rendering Post does not have a value for ``publish_at``, while the incorrectly rendering Post does have a value for ``publish_at``. We can see that the ``publish_at`` value is significantly diff --git a/docs/lab2.md b/docs/lab2.md index 3aa3cde..e5d8407 100644 --- a/docs/lab2.md +++ b/docs/lab2.md @@ -17,7 +17,7 @@ The site seems to be running slower lately. Please make the site fast again! To reproduce: 1. Browse to the [posts page](http://127.0.0.1:8000/p/). -2. Browse to a [post's page](http://127.0.0.1:8000/p/term-writer-recognize-race-available-5291/). +2. Browse to a [post's page](http://127.0.0.1:8000/p/welcome-to-our-newsletter/). 3. Browse to the [posts admin page](http://127.0.0.1:8000/admin/newsletter/post/). 4. Why are these slow? @@ -70,7 +70,7 @@ that ``Post.categories`` is a ``ManyToManyField``, we'll need to use ``.prefetch Since the categories need to be specifically ordered, you'll need to use a [``Prefetch``](https://docs.djangoproject.com/en/stable/ref/models/querysets/#django.db.models.Prefetch) instance rather than just a string. -Moving onto an individual [post page](http://127.0.0.1:8000/p/term-writer-recognize-race-available-5291/), +Moving onto an individual [post page](http://127.0.0.1:8000/p/welcome-to-our-newsletter/), the SQL panel only shows 1 query. Clicking into that panel, we'll see some really low value, like 5ms. This in pretty much every definition is fast. However, let's trust the reporter that this is actually slow. The next step is to understand what about this query is diff --git a/project/data/factories.py b/project/data/factories.py index 36f6a8b..0d4c35e 100644 --- a/project/data/factories.py +++ b/project/data/factories.py @@ -20,7 +20,9 @@ Faker.seed(2022) image_generator = MarkdownImageGenerator() -DATA_END_DATE = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) +DATA_END_DATE = timezone.now().replace( + hour=0, minute=0, second=0, microsecond=0 +) - timedelta(days=1) DATA_START_DATE = DATA_END_DATE - timedelta(days=365 * 2) @@ -97,6 +99,9 @@ class Meta: tzinfo=UTC, ) ) + created = factory.LazyAttribute( + lambda o: o.publish_at - timedelta(days=fake.pyint(1, 30)) + ) @factory.post_generation def categories(self, create, extracted, **kwargs): diff --git a/project/data/markdown.py b/project/data/markdown.py index 5b88eb0..68c2d2c 100644 --- a/project/data/markdown.py +++ b/project/data/markdown.py @@ -7,16 +7,26 @@ POST_COUNT = 1500 RECENT_POSTS = [ - {"title": "Welcome to Our Newsletter", "slug": "welcome-to-our-newsletter"}, - {"title": "Getting Started with Python", "slug": "getting-started-with-python"}, - {"title": "Advanced Django Techniques", "slug": "advanced-django-techniques"}, - {"title": "Database Optimization Tips", "slug": "database-optimization-tips"}, - {"title": "Testing Best Practices", "slug": "testing-best-practices"}, - {"title": "Debugging Like a Pro", "slug": "debugging-like-a-pro"}, - {"title": "Code Review Guidelines", "slug": "code-review-guidelines"}, - {"title": "Deployment Strategies", "slug": "deployment-strategies"}, - {"title": "Security Fundamentals", "slug": "security-fundamentals"}, - {"title": "Performance Monitoring", "slug": "performance-monitoring"}, + ( + {"title": "Welcome to Our Newsletter", "slug": "welcome-to-our-newsletter"}, + {"title": "Getting Started with Python", "slug": "getting-started-with-python"}, + ), + ( + {"title": "Advanced Django Techniques", "slug": "advanced-django-techniques"}, + {"title": "Database Optimization Tips", "slug": "database-optimization-tips"}, + ), + ( + {"title": "Testing Best Practices", "slug": "testing-best-practices"}, + {"title": "Debugging Like a Pro", "slug": "debugging-like-a-pro"}, + ), + ( + {"title": "Code Review Guidelines", "slug": "code-review-guidelines"}, + {"title": "Deployment Strategies", "slug": "deployment-strategies"}, + ), + ( + {"title": "Security Fundamentals", "slug": "security-fundamentals"}, + {"title": "Performance Monitoring", "slug": "performance-monitoring"}, + ), ] @@ -57,16 +67,30 @@ def generate_data(user, image_category, post_categories): ignore_conflicts=True, ) - recent_posts = [ - PostFactory.build( - author=user, - title=post_data["title"], - slug=post_data["slug"], - publish_at=DATA_END_DATE - timedelta(days=i), - created=DATA_END_DATE - timedelta(days=i), + recent_posts = [] + for day, (first, second) in enumerate(RECENT_POSTS): + day_base = DATA_END_DATE - timedelta(days=day) + first_time = day_base + timedelta(hours=10) + recent_posts.append( + PostFactory.build( + author=user, + title=first["title"], + slug=first["slug"], + publish_at=first_time - timedelta(hours=2), + is_published=True, + created=first_time, + ) + ) + recent_posts.append( + PostFactory.build( + author=user, + title=second["title"], + slug=second["slug"], + created=first_time - timedelta(hours=1), + is_published=True, + publish_at=None, + ) ) - for i, post_data in enumerate(RECENT_POSTS) - ] Post.objects.bulk_create(recent_posts, ignore_conflicts=True) category_cycle = cycle(post_categories)