Skip to content

Commit

Permalink
Generate seed data for dev/staging using factories (codebuddies#129)
Browse files Browse the repository at this point in the history
* add unique constraint on resource, tag

This constraint prevents us from tagging a resource
with the same tag more than once when we're creating
TaggedItems objects via factories.

This is already applied as a soft-constraint at the manager
level when creating via `resource.tags.add()`,
but the factories bypass it. This applies it at the DB level

* add a management command to generate fake data for dev

* add tests

* update docs

* delete old json fixtures
  • Loading branch information
chris48s authored Apr 16, 2020
1 parent c5eb5b6 commit 56c7c90
Show file tree
Hide file tree
Showing 16 changed files with 233 additions and 6,000 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ docker-compose down

5. Create a superuser so that you can log into http://localhost:8000/admin by running the following in your terminal: `$ docker-compose run --rm app ./manage.py createsuperuser`

6. You can populate the database with some random test data for development purposes by running `$ docker-compose run --rm app ./manage.py init_data`. All user accounts created by this command have the password `codebuddies`.

## Editing Code

With the local environment running, you can modify the application code in your editor of choice. As you save changes, the application should reload automatically. There should be no need to restart containers to see code changes.
Expand Down
1 change: 1 addition & 0 deletions project/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
]

LOCAL_APPS = [
"core.apps.CoreConfig",
"users.apps.UsersConfig",
"resources.apps.ResourcesConfig",
"tagging.apps.TaggingConfig",
Expand Down
5 changes: 5 additions & 0 deletions project/core/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class CoreConfig(AppConfig):
name = 'core'
Empty file.
Empty file.
128 changes: 128 additions & 0 deletions project/core/management/commands/init_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import pprint
import random
from argparse import ArgumentTypeError
from functools import partial

from django.core.management.base import BaseCommand
from factory import PostGenerationMethodCall, create_batch

from users.factories import UserFactory
from users.models import User
from resources.factories import ResourceFactory
from resources.models import Resource
from tagging.factories import CustomTagFactory, TaggedItemsFactory
from tagging.models import CustomTag, TaggedItems


class Command(BaseCommand):

help = "Initialize the DB with some random fake data for testing and development"

def add_arguments(self, parser):
parser.add_argument(
"--clear-db",
action="store_true",
dest="clear-db",
help="Clear existing data from the DB before creating test data",
)

def check_min_int(min, value):
ivalue = int(value)
if ivalue < min:
raise ArgumentTypeError(f"must be at least {min}")
return ivalue

parser.add_argument(
'--num-users',
type=partial(check_min_int, 1),
required=False,
default=10,
dest="num-users",
help="Number of `User` objects to create (default 10)"
)
parser.add_argument(
'--num-tags',
type=partial(check_min_int, 1),
required=False,
default=10,
dest="num-tags",
help="Number of `Tag` objects to create (default 10)"
)
parser.add_argument(
'--num-resources',
type=partial(check_min_int, 2),
required=False,
default=10,
dest="num-resources",
help="Number of `Resource` objects to create (default 10)"
)

def teardown(self):
TaggedItems.objects.all().delete()
CustomTag.objects.all().delete()
Resource.objects.all().delete()
User.objects.all().delete()

def create_users(self, num):
# Create random users.
# All users have the password 'codebuddies'.
return [
UserFactory(
password=PostGenerationMethodCall('set_password', 'codebuddies')
) for _ in range(0, num)
]

def create_tags(self, num):
initial_tag_count = CustomTag.objects.all().count()
tags = set()
while CustomTag.objects.all().count() < (initial_tag_count + num):
tags.add(CustomTagFactory())
return list(tags)

def create_resources(self, num):
return create_batch(ResourceFactory, num, user=random.choice(self.users))

def tag_resources(self):
tagged_items = []
# Assign one resource zero tags, but
# assign each other resource at least one tag
for resource in self.resources[:-1]:
for _ in range(0, random.randrange(1, 4)):
tag = random.choice(self.tags)
tagged_items.append(
TaggedItemsFactory(content_object=resource, tag=tag),
)
return tagged_items

def print_list(self, lst):
pp = pprint.PrettyPrinter(indent=4)
pp.pprint(lst)

def print_summary(self, verbosity):
self.stdout.write(f'Created {len(self.users)} Users.')
if verbosity >= 2:
self.print_list(self.users)
self.stdout.write(f'Created {len(self.tags)} Tags.')
if verbosity >= 2:
self.print_list(self.tags)
self.stdout.write(f'Created {len(self.resources)} Resources.')
if verbosity >= 2:
self.print_list(self.resources)
self.stdout.write(f'Created {len(self.tagged_items)} TaggedItems.')
if verbosity >= 2:
self.print_list(self.tagged_items)

def handle(self, *args, **kwargs):
if kwargs['clear-db']:
self.stdout.write('Clearing existing data..')
self.teardown()

self.stdout.write('Creating test data..')
self.users = self.create_users(kwargs['num-users'])
self.tags = self.create_tags(kwargs['num-tags'])
self.resources = self.create_resources(kwargs['num-resources'])
self.tagged_items = self.tag_resources()

self.print_summary(kwargs['verbosity'])

self.stdout.write('..done')
Empty file added project/core/tests/__init__.py
Empty file.
69 changes: 69 additions & 0 deletions project/core/tests/test_init_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from io import StringIO

from django.core.management import call_command
from django.test import TestCase

from core.management.commands.init_data import Command
from users.models import User
from resources.models import Resource
from tagging.models import CustomTag, TaggedItems


class InitDataTest(TestCase):

def test_defaults(self):
out = StringIO()
call_command("init_data", stdout=out)
log = out.getvalue()

self.assertEqual(10, User.objects.all().count())
self.assertIn('Created 10 Users.', log)

self.assertEqual(10, CustomTag.objects.all().count())
self.assertIn('Created 10 Tags.', log)

self.assertEqual(10, Resource.objects.all().count())
self.assertIn('Created 10 Resources.', log)

# The number of TaggedItems we will insert is non-deterministic,
# but the lower bound is 9
self.assertGreaterEqual(TaggedItems.objects.all().count(), 9)

def test_params(self):
out = StringIO()
call_command("init_data", stdout=out, **{
'num-users': 24,
'num-tags': 26,
'num-resources': 28,
})
log = out.getvalue()

self.assertEqual(24, User.objects.all().count())
self.assertIn('Created 24 Users.', log)

self.assertEqual(26, CustomTag.objects.all().count())
self.assertIn('Created 26 Tags.', log)

self.assertEqual(28, Resource.objects.all().count())
self.assertIn('Created 28 Resources.', log)

self.assertGreaterEqual(TaggedItems.objects.all().count(), 27)

def test_clear_db(self):
out = StringIO()
# set up some non-zero amount of data and assert it is there
call_command("init_data", stdout=out)
self.assertGreaterEqual(User.objects.all().count(), 1)
self.assertGreaterEqual(CustomTag.objects.all().count(), 1)
self.assertGreaterEqual(Resource.objects.all().count(), 1)
self.assertGreaterEqual(TaggedItems.objects.all().count(), 1)

cmd = Command()
# clear it
cmd.teardown()

# check its all gone
self.assertEqual(0, User.objects.all().count())
self.assertEqual(0, CustomTag.objects.all().count())
self.assertEqual(0, Resource.objects.all().count())
self.assertEqual(0, TaggedItems.objects.all().count())
Loading

0 comments on commit 56c7c90

Please sign in to comment.