This content originally appeared on Level Up Coding - Medium and was authored by Yevhen Nosolenko
Legacy Lobotomy — Creating a Management Command for Seeding Playbooks

In this 16th tutorial of our series, we’ll keep working on our Django project by creating playbooks — completed assignment records which track what users have done and seen. We’ll use factories powered by Factory Boy and implement a Django management command to quickly set this up. Let’s get started!
To get more ideas regarding the project used for this tutorial or check other tutorials in this series, check the introductory article published earlier.
Legacy Lobotomy — Confident Refactoring of a Django Project
Origin branch and destination branch
- If you want to follow the steps described in this tutorial, you should start from the seeding-assignments branch.
- If you want to only see final code state after all changes from this tutorial are applied, you can check the seeding-playbook-assignments branch.
📁 Preparing the playbooks app
In this section, we’ll start working on the playbooks app to store records of completed assignments. Before we dive into the implementation, we need to update its structure, just like we did with the assignments app.
Step 1. Inside the playbooks app, create the following new packages: factories, management, and tests.
Step 2. Within the management package, add a commands subpackage.
Step 3. In the tests directory, add a management subpackage.
Step 4. Inside tests/management, create a new commands subpackage to hold tests for our management commands.
After completing these steps, the playbooks app should have the structure shown in the picture below.

🎯 Command requirements
Now that we have everything in place for assignments, it’s time to do something very similar for playbooks. Playbooks represent completed assignments tied to specific users. Just like assignments, each playbook can include a series of blocks of different types: text, image, video, or question.
For question blocks, we also need to create a few options, making sure that exactly one of them is marked as correct. This setup closely mirrors how assignments are structured, so we’ll follow the same general approach.
There are just a couple of small differences to keep in mind:
- Instead of being linked to a target, each playbook is linked to a user.
- Completed playbooks are saved with a timestamp.
Our command for generating playbooks should meet the following requirements:
- It should accept an optional --count argument to set how many playbooks to create. If not provided, it should default to 10.
- It should accept an optional --category-id argument to link each playbook to a specific category. If missing, a random category should be used.
- It should accept an optional --user-id argument to specify the user who completed these playbooks. If not provided, a random user should be selected.
- Each playbook should include a random number of blocks (between 1 and 6), and the block types should be randomly chosen.
- For question blocks, a list of options should be created, and one of them must be marked as correct.
- We don’t need to print the playbook IDs to the output.
In the next steps, we’ll build the necessary factories for playbooks and blocks, and then create the management command for seeding them.
🔨 Creating playbook block factories
Before generating playbooks, we need factories for each type of content block that completed assignments can contain. We’ll set up factories for text, image, video, and question blocks, then combine them into one system that creates complete playbook structures.
📝 Create a playbook text block factory
Playbook text blocks are used to store plain text content that was shown as part of an assignment a user already completed. These are the simplest type of block and typically capture things like instructions or explanatory text that were originally part of the assignment.
Create a file named playbook_text_block.py in the playbooks/factories folder and add the following code.
import factory
from playbooks.models import PlaybookTextBlock
class PlaybookTextBlockFactory(factory.django.DjangoModelFactory):
class Meta:
model = PlaybookTextBlock
# This property must be provided when creating a text block.
block = None
text = factory.Faker('paragraph')
Since PlaybookTextBlock depends on its parent PlaybookAssignmentBlock, it shouldn’t be created on its own. That’s why the block field is set to None, meaning you need to pass a block explicitly when using this factory.
🖼️ Create a playbook image block factory
Playbook image blocks store visual content that was part of an assignment a user already completed. These blocks capture what was originally displayed in the assignment, such as diagrams or illustrations.
Create a file named playbook_image_block.py in the playbooks/factories folder with the following content.
import factory
from playbooks.models import PlaybookImageBlock
class PlaybookImageBlockFactory(factory.django.DjangoModelFactory):
class Meta:
model = PlaybookImageBlock
# This property must be provided when creating an image block.
block = None
image = factory.django.ImageField()
The factory generates a placeholder image for testing purposes. Like other block types, the block field must be explicitly passed in, since the image block is always tied to a parent PlaybookAssignmentBlock.
🎥 Create a playbook video block factory
Video blocks in playbooks are used to store references to video content that appeared in the original assignment. These might include training clips, walkthroughs, or any other kind of visual instruction that the user viewed before completing the assignment.
Create a file named playbook_video_block.py in the playbooks/factories folder and add the following code.
import factory
from playbooks.models import PlaybookVideoBlock
class PlaybookVideoBlockFactory(factory.django.DjangoModelFactory):
class Meta:
model = PlaybookVideoBlock
# This property must be provided when creating a video block.
block = None
# This generates a small valid MPEG video file for testing purposes.
video = factory.django.FileField(
filename='video.mp4',
data=b'\x00\x00\x01\xba\x21\x00\x01\x00\x01\x00\x01\x80\x01\x00\x00\x00\x01\xb3\x2c\x01\xe0\x21\x00\x00\x00'
b'\x00\x01\xb5\x14\x8d\x1b\x16\x10\xff\xff\xc0\x00\x00\x01\xb5\x05\x08\x88\x84\x21\x14\xb6\x03\x21\x32'
b'\x1f\xff\xfb'
)
The factory uses a small fake MPEG file to simulate video content. As with other block factories, the block relationship is required and must be passed when creating a test instance.
🔘 Create a playbook option factory
Playbook options represent the possible answers a user saw in a completed assignment. Each option belongs to a question block and includes the answer text, an optional tip, and a flag that indicates whether it was marked as the correct choice.
Create a file named playbook_option.py in the playbooks/factories folder with the following code.
import factory
from playbooks.models import PlaybookOption
class PlaybookOptionFactory(factory.django.DjangoModelFactory):
class Meta:
model = PlaybookOption
# This property must be provided when creating an option.
question = None
text = factory.Faker('paragraph')
tip = factory.Faker('paragraph')
is_correct = False
By default, this factory creates incorrect options. Later on, when we build the PlaybookQuestionBlockFactory, we’ll randomly choose one of the options to mark as correct. Also, since an option can’t exist on its own, the question field must be explicitly set when using the factory.
❓ Create a playbook question block factory
Playbook question blocks record the interactive questions that were part of the original assignment. These blocks store the question text and the options that were shown to the user.
Create a file named playbook_question_block.py in the playbooks/factories folder and add the following code.
import random
import factory
from playbooks.factories.playbook_option import PlaybookOptionFactory
from playbooks.models import PlaybookQuestionBlock
class PlaybookQuestionBlockFactory(factory.django.DjangoModelFactory):
class Meta:
model = PlaybookQuestionBlock
# This property must be provided when creating a question block.
block = None
text = factory.Faker('paragraph')
options = factory.RelatedFactoryList(PlaybookOptionFactory, 'question', lambda: random.randint(3, 6))
@factory.post_generation
def select_correct_option(self, create, extracted, **kwargs):
if not create:
return
options = list(self.options.all())
# Mark a random option as correct.
correct_option = random.choice(options)
correct_option.is_correct = True
correct_option.save()
This factory creates a question block with some fake text and a few answer options (between 3 and 6). Once the options are created, one of them is randomly marked as the correct one. The block field must be passed in manually since the question block is always tied to a parent block.
⚙️ Create a playbook assignment block factory
The PlaybookAssignmentBlockFactory is the central factory for generating blocks within completed playbook records. Unlike the specific block-type factories (such as PlaybookTextBlockFactory or PlaybookQuestionBlockFactory), this factory creates the parent PlaybookAssignmentBlock instance and then creates a corresponding child block based on its type_of_block value.
Create a file named playbook_assignment_block.py in the playbooks/factories folder and add the following code.
import factory
from assignments.models import AssignmentBlock
from playbooks.models import PlaybookAssignmentBlock
from .playbook_image_block import PlaybookImageBlockFactory
from .playbook_question_block import PlaybookQuestionBlockFactory
from .playbook_text_block import PlaybookTextBlockFactory
from .playbook_video_block import PlaybookVideoBlockFactory
playbook_block_factories = {
'Text': PlaybookTextBlockFactory,
'Image': PlaybookImageBlockFactory,
'Video': PlaybookVideoBlockFactory,
'Question': PlaybookQuestionBlockFactory,
}
class PlaybookAssignmentBlockFactory(factory.django.DjangoModelFactory):
class Meta:
model = PlaybookAssignmentBlock
# This property must be provided when creating an assignment block.
assignment = None
name = factory.Faker('sentence', nb_words=3)
type_of_block = factory.Faker('random_element', elements=[item[0] for item in AssignmentBlock.TYPE_CHOICES])
@factory.post_generation
def create_block_of_target_type(self, create, extracted, **kwargs):
if not create:
return
playbook_block_factories[self.type_of_block](block=self)
The factory uses a simple lookup dictionary to find and call the correct block-type factory based on the block’s type. The assignment field must be set manually, as each block belongs to a specific PlaybookAssignment.
🌟 Create and register the playbook assignment factory
he PlaybookAssignmentFactory is responsible for generating full playbook records that represent completed assignments. Each playbook includes metadata like the user who completed it, its category, and a set of associated blocks. The factory also creates a random number of blocks for each playbook by using the PlaybookAssignmentBlockFactory, which handles selecting and generating specific block types.
Create a file named playbook_assignment.py in the playbooks/factories folder with the following content.
import random
import uuid
import factory
from assignments.factories import CategoryFactory
from playbooks.models import PlaybookAssignment
from users.factories import RegularUserFactory
from .playbook_assignment_block import PlaybookAssignmentBlockFactory
class PlaybookAssignmentFactory(factory.django.DjangoModelFactory):
class Meta:
model = PlaybookAssignment
user = factory.SubFactory(RegularUserFactory)
name = factory.LazyFunction(lambda: f'Fake Assignment #{uuid.uuid4().hex}')
description = factory.Faker('paragraph')
image = factory.django.ImageField()
points = factory.Faker('pyint', min_value=0, max_value=100)
time = factory.Faker('pyint', min_value=10, max_value=300)
category = factory.SubFactory(CategoryFactory)
priority = factory.Faker('pyint', min_value=0, max_value=25)
blocks = factory.RelatedFactoryList(
PlaybookAssignmentBlockFactory,
factory_related_name='assignment',
size=lambda: random.randint(1, 6)
)
The user and category fields use factories RegularUserFactory and CategoryFactory respectively. The blocks field generates between 1 and 6 assignment blocks, each with a random block type and its own related data (e.g., text, image, question).
Next, add the following import to the playbooks/factories/__init__.py module.
from .playbook_assignment import PlaybookAssignmentFactory
After that register the factory in src/conftest.py.
from playbooks.factories import PlaybookAssignmentFactory
# Register playbook factories
register(PlaybookAssignmentFactory)
Now, we can proceed with creating a Django management command for generating playbook assignments.
🛠 ️Creating a command for seeding playbooks
Now that our factories are ready, it’s time to build a management command to tie everything together. This command will generate complete playbooks linked to users and categories, each with its own content blocks.
🔧 Step 1: Define the playbook seeding command
Add a new seed_playbook_assignments.py module with the following content to the playbooks/management/commands directory.
from django.contrib.auth import get_user_model
from django.core.management import BaseCommand, CommandError
from assignments.models import Category
from playbooks.factories import PlaybookAssignmentFactory
User = get_user_model()
class Command(BaseCommand):
help = 'Seed fake playbook assignments.'
def add_arguments(self, parser):
parser.add_argument(
'--count',
required=False,
type=int,
default=10,
help='The number of playbook assignments which should be created.'
)
parser.add_argument(
'--category-id',
required=False,
type=int,
help='A category to which the assignment should be linked.'
)
parser.add_argument(
'--user-id',
required=False,
type=int,
help='A user to which the assignment should be linked.'
)
def handle(self, count, *args, **options):
if not isinstance(count, int) or count < 1:
raise CommandError('The --count argument must be an integer value greater than or equal to 1.')
try:
category = self._parse_category(**options)
user = self._parse_user(**options)
for _ in range(count):
PlaybookAssignmentFactory(
category=category or self._fetch_random_category(),
user=user or self._fetch_random_user(),
)
except Category.DoesNotExist:
raise CommandError('The category does not exist.')
except User.DoesNotExist:
raise CommandError('The user does not exist.')
def _parse_category(self, **options):
category_id = options.get('category_id')
if category_id is None:
return None
if not isinstance(category_id, int) or category_id < 1:
raise CommandError('The --category-id argument must be an integer value greater than or equal to 1.')
return Category.objects.get(pk=category_id)
def _fetch_random_category(self):
return Category.objects.order_by('?')[:1].get()
def _parse_user(self, **options):
user_id = options.get('user_id')
if user_id is None:
return None
if not isinstance(user_id, int) or user_id < 1:
raise CommandError('The --user-id argument must be an integer value greater than or equal to 1.')
return User.objects.get(pk=user_id)
def _fetch_random_user(self):
return User.objects.order_by('?')[:1].get()
The helper methods _parse_category and _parse_user retrieve category and user accordingly based on optional command-line arguments. If an explicit ID is provided via the --category-id or --user-id argument, the method validates the ID and attempts to fetch the corresponding object from the database. If no ID is specified, the methods _fetch_random_category and _fetch_random_user randomly select one existing record for each playbook assignment. In both cases, if the object does not exist or the input is invalid, a CommandError is raised.
🐍 Step 2: Implement tests
Create a corresponding test module named test_seed_playbook_assignments.py inside the playbooks/tests/management/commands directory.
import pytest
from django.contrib.auth import get_user_model
from django.core.management import CommandError, call_command
from assignments.models import Category
from playbooks.models import PlaybookAssignment
User = get_user_model()
@pytest.mark.django_db
class TestSeedPlaybookAssignments:
def test_when_there_are_no_categories_then_error_should_be_thrown(self):
with pytest.raises(CommandError) as exc_info:
call_command('seed_playbook_assignments')
assert str(exc_info.value) == 'The category does not exist.'
def test_when_category_with_provided_id_does_not_exist_then_error_should_be_thrown(self, category):
with pytest.raises(CommandError) as exc_info:
call_command('seed_playbook_assignments', category_id=category.id + 1)
assert str(exc_info.value) == 'The category does not exist.'
@pytest.mark.parametrize('category_id', [-10, 0, 0.5, 'invalid'])
def test_when_category_id_argument_is_invalid_then_error_should_be_thrown(self, category_id):
with pytest.raises(CommandError) as exc_info:
call_command('seed_playbook_assignments', category_id=category_id)
assert str(exc_info.value) == 'The --category-id argument must be an integer value greater than or equal to 1.'
def test_when_there_are_no_users_then_error_should_be_thrown(self, category):
with pytest.raises(CommandError) as exc_info:
call_command('seed_playbook_assignments', category_id=category.id)
assert str(exc_info.value) == 'The user does not exist.'
def test_when_user_with_provided_id_does_not_exist_then_error_should_be_thrown(
self, category, regular_user
):
with pytest.raises(CommandError) as exc_info:
call_command('seed_playbook_assignments', category_id=category.id, user_id=regular_user.id + 1)
assert str(exc_info.value) == 'The user does not exist.'
@pytest.mark.parametrize('user_id', [-10, 0, 0.5, 'invalid'])
def test_when_user_id_argument_is_invalid_then_error_should_be_thrown(self, category, user_id):
with pytest.raises(CommandError) as exc_info:
call_command('seed_playbook_assignments', category_id=category.id, user_id=user_id)
assert str(exc_info.value) == 'The --user-id argument must be an integer value greater than or equal to 1.'
def test_when_count_argument_is_not_provided_then_10_assignments_should_be_created(self, category, regular_user):
call_command('seed_playbook_assignments')
assert PlaybookAssignment.objects.count() == 10
@pytest.mark.parametrize('count', [-10, 0, 0.5, 'invalid'])
def test_when_count_argument_is_invalid_then_error_should_be_thrown(self, count):
with pytest.raises(CommandError) as exc_info:
call_command('seed_playbook_assignments', count=count)
assert str(exc_info.value) == 'The --count argument must be an integer value greater than or equal to 1.'
@pytest.mark.parametrize('count', [1, 5, 12])
def test_when_count_argument_is_provided_then_requested_number_of_assignments_should_be_created(
self, category, regular_user, count
):
call_command('seed_playbook_assignments', count=count)
assert PlaybookAssignment.objects.count() == count
def test_when_category_is_not_provided_then_one_of_existing_categories_should_be_used(
self, regular_user, category_factory
):
categories_count = 5
categories = category_factory.create_batch(categories_count)
call_command('seed_playbook_assignments')
existing_category_ids = {category.pk for category in categories}
used_category_ids = {assignment.category_id for assignment in PlaybookAssignment.objects.all()}
# Since the category selection is randomized, we can only verify that more than one category was used.
# There's still a small chance that all 10 assignments receive the same category,
# but the probability is low enough to be negligible.
assert len(used_category_ids) > 1
# Ensure that only existing categories are used and no new categories are created.
assert used_category_ids - existing_category_ids == set()
def test_when_category_id_provided_then_all_assignments_should_be_assigned_to_the_same_category(
self, category, regular_user
):
call_command('seed_playbook_assignments', category_id=category.pk)
used_category_ids = {assignment.category_id for assignment in PlaybookAssignment.objects.all()}
assert used_category_ids == {category.pk}
# Verify that new categories were not created.
assert Category.objects.count() == 1
def test_when_user_is_not_provided_then_one_of_existing_users_should_be_used(self, category, regular_user_factory):
users_count = 5
users = regular_user_factory.create_batch(users_count)
call_command('seed_playbook_assignments')
existing_user_ids = {user.pk for user in users}
used_user_ids = {assignment.user_id for assignment in PlaybookAssignment.objects.all()}
# Since the user selection is randomized, we can only verify that more than one user was used.
# There's still a small chance that all 10 assignments receive the same user,
# but the probability is low enough to be negligible.
assert len(used_user_ids) > 1
# Ensure that only existing users are used and no new categories are created.
assert used_user_ids - existing_user_ids == set()
def test_when_user_provided_then_all_assignments_should_be_assigned_to_the_same_user(self, category, regular_user):
call_command('seed_playbook_assignments', user_id=regular_user.pk)
used_user_ids = {assignment.user_id for assignment in PlaybookAssignment.objects.all()}
assert used_user_ids == {regular_user.pk}
# Verify that new assignment users were not created.
assert User.objects.count() == 1
🖥️ Step 3: Run and verify
Run the tests and ensure all they pass successfully.
Next, run the python src/manage.py seed_playbook_assignments command from the project root to generate a default set of 10 playbook assignments.
After that, run the python src/manage.py seed_playbook_assignments --count 15 command to create 15 objects.
Confirm that playbook assignments are created correctly and include blocks of various types, linked to valid categories and users.
Conclusion
By creating factories and a command to seed playbooks, we’ve made it easier to generate useful test data. These playbooks look like real user activity, helping us test the app more effectively. Next, we’ll combine all the seeding commands we’ve created so far into a unified system to make it easy to populate the entire database with test data.
That’s all for creating factories and commands. At this moment we already have everything we need for populating our database with test data.
Legacy Lobotomy — Creating a Management Command for Seeding Playbooks was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Yevhen Nosolenko
Yevhen Nosolenko | Sciencx (2025-09-03T15:04:47+00:00) Legacy Lobotomy — Creating a Management Command for Seeding Playbooks. Retrieved from https://www.scien.cx/2025/09/03/legacy-lobotomy-creating-a-management-command-for-seeding-playbooks/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.