✨(demo) add a "demo" app to facilitate testing/working on the project
We designed it to allow creating a huge number of objects fast using bulk creation.
This commit is contained in:
committed by
Anthony LC
parent
cfc35ac23e
commit
0c550ebd1c
0
src/backend/demo/__init__.py
Normal file
0
src/backend/demo/__init__.py
Normal file
8
src/backend/demo/defaults.py
Normal file
8
src/backend/demo/defaults.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"""Parameters that define how the demo site will be built."""
|
||||||
|
|
||||||
|
NB_OBJECTS = {
|
||||||
|
"users": 1000,
|
||||||
|
"teams": 100,
|
||||||
|
"max_identities_per_user": 3,
|
||||||
|
"max_users_per_team": 100,
|
||||||
|
}
|
||||||
0
src/backend/demo/management/__init__.py
Normal file
0
src/backend/demo/management/__init__.py
Normal file
0
src/backend/demo/management/commands/__init__.py
Normal file
0
src/backend/demo/management/commands/__init__.py
Normal file
190
src/backend/demo/management/commands/create_demo.py
Executable file
190
src/backend/demo/management/commands/create_demo.py
Executable file
@@ -0,0 +1,190 @@
|
|||||||
|
# ruff: noqa: S311, S106
|
||||||
|
"""create_demo management command"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from django import db
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
|
from core import models
|
||||||
|
|
||||||
|
from demo import defaults
|
||||||
|
|
||||||
|
logger = logging.getLogger("people.commands.demo.create_demo")
|
||||||
|
|
||||||
|
|
||||||
|
def random_true_with_probability(probability):
|
||||||
|
"""return True with the requested probability, False otherwise."""
|
||||||
|
return random.random() < probability
|
||||||
|
|
||||||
|
|
||||||
|
class BulkQueue:
|
||||||
|
"""A utility class to create Django model instances in bulk by just pushing to a queue."""
|
||||||
|
|
||||||
|
BATCH_SIZE = 20000
|
||||||
|
|
||||||
|
def __init__(self, stdout, *args, **kwargs):
|
||||||
|
"""Define the queue as a dict of lists."""
|
||||||
|
self.queue = defaultdict(list)
|
||||||
|
self.stdout = stdout
|
||||||
|
|
||||||
|
def _bulk_create(self, objects):
|
||||||
|
"""Actually create instances in bulk in the database."""
|
||||||
|
if not objects:
|
||||||
|
return
|
||||||
|
|
||||||
|
objects[0]._meta.model.objects.bulk_create(objects, ignore_conflicts=True) # noqa: SLF001
|
||||||
|
# In debug mode, Django keeps query cache which creates a memory leak in this case
|
||||||
|
db.reset_queries()
|
||||||
|
self.queue[objects[0]._meta.model.__name__] = [] # noqa: SLF001
|
||||||
|
|
||||||
|
def push(self, obj):
|
||||||
|
"""Add a model instance to queue to that it gets created in bulk."""
|
||||||
|
objects = self.queue[obj._meta.model.__name__] # noqa: SLF001
|
||||||
|
objects.append(obj)
|
||||||
|
if len(objects) > self.BATCH_SIZE:
|
||||||
|
self._bulk_create(objects)
|
||||||
|
self.stdout.write(".", ending="")
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
"""Flush the queue after creating the remaining model instances."""
|
||||||
|
for objects in self.queue.values():
|
||||||
|
self._bulk_create(objects)
|
||||||
|
|
||||||
|
|
||||||
|
class Timeit:
|
||||||
|
"""A utility context manager/method decorator to time execution."""
|
||||||
|
|
||||||
|
total_time = 0
|
||||||
|
|
||||||
|
def __init__(self, stdout, sentence=None):
|
||||||
|
"""Set the sentence to be displayed for timing information."""
|
||||||
|
self.sentence = sentence
|
||||||
|
self.start = None
|
||||||
|
self.stdout = stdout
|
||||||
|
|
||||||
|
def __call__(self, func):
|
||||||
|
"""Behavior on call for use as a method decorator."""
|
||||||
|
|
||||||
|
def timeit_wrapper(*args, **kwargs):
|
||||||
|
"""wrapper to trigger/stop the timer before/after function call."""
|
||||||
|
self.__enter__()
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
self.__exit__(None, None, None)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return timeit_wrapper
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""Start timer upon entering context manager."""
|
||||||
|
self.start = time.perf_counter()
|
||||||
|
if self.sentence:
|
||||||
|
self.stdout.write(self.sentence, ending=".")
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, exc_tb):
|
||||||
|
"""Stop timer and display result upon leaving context manager."""
|
||||||
|
if exc_type is not None:
|
||||||
|
raise exc_type(exc_value)
|
||||||
|
end = time.perf_counter()
|
||||||
|
elapsed_time = end - self.start
|
||||||
|
if self.sentence:
|
||||||
|
self.stdout.write(f" Took {elapsed_time:g} seconds")
|
||||||
|
|
||||||
|
self.__class__.total_time += elapsed_time
|
||||||
|
return elapsed_time
|
||||||
|
|
||||||
|
|
||||||
|
def create_demo(stdout):
|
||||||
|
"""
|
||||||
|
Create a database with demo data for developers to work in a realistic environment.
|
||||||
|
The code is engineered to create a huge number of objects fast.
|
||||||
|
"""
|
||||||
|
|
||||||
|
queue = BulkQueue(stdout)
|
||||||
|
|
||||||
|
with Timeit(stdout, "Creating users"):
|
||||||
|
for i in range(defaults.NB_OBJECTS["users"]):
|
||||||
|
queue.push(
|
||||||
|
models.User(
|
||||||
|
email=f"user{i:d}@example.com",
|
||||||
|
password="!",
|
||||||
|
is_superuser=False,
|
||||||
|
is_active=True,
|
||||||
|
is_staff=False,
|
||||||
|
language=random.choice(settings.LANGUAGES)[0],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
queue.flush()
|
||||||
|
|
||||||
|
with Timeit(stdout, "Creating identities"):
|
||||||
|
users_values = list(models.User.objects.values("id", "email"))
|
||||||
|
for user_dict in users_values:
|
||||||
|
for i in range(
|
||||||
|
random.randint(0, defaults.NB_OBJECTS["max_identities_per_user"])
|
||||||
|
):
|
||||||
|
user_email = user_dict["email"]
|
||||||
|
queue.push(
|
||||||
|
models.Identity(
|
||||||
|
user_id=user_dict["id"],
|
||||||
|
sub=uuid4(),
|
||||||
|
email=f"identity{i:d}{user_email:s}",
|
||||||
|
is_main=(i == 0),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
queue.flush()
|
||||||
|
|
||||||
|
with Timeit(stdout, "Creating teams"):
|
||||||
|
for i in range(defaults.NB_OBJECTS["teams"]):
|
||||||
|
queue.push(
|
||||||
|
models.Team(
|
||||||
|
name=f"Team {i:d}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
queue.flush()
|
||||||
|
|
||||||
|
with Timeit(stdout, "Creating team accesses"):
|
||||||
|
teams_ids = list(models.Team.objects.values_list("id", flat=True))
|
||||||
|
users_ids = list(models.User.objects.values_list("id", flat=True))
|
||||||
|
for team_id in teams_ids:
|
||||||
|
for user_id in random.sample(
|
||||||
|
users_ids,
|
||||||
|
random.randint(1, defaults.NB_OBJECTS["max_users_per_team"]),
|
||||||
|
):
|
||||||
|
role = random.choice(models.RoleChoices.choices)
|
||||||
|
queue.push(
|
||||||
|
models.TeamAccess(team_id=team_id, user_id=user_id, role=role[0])
|
||||||
|
)
|
||||||
|
queue.flush()
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""A management command to create a demo database."""
|
||||||
|
|
||||||
|
help = __doc__
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
"""Add argument to require forcing execution when not in debug mode."""
|
||||||
|
parser.add_argument(
|
||||||
|
"-f",
|
||||||
|
"--force",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Force command execution despite DEBUG is set to False",
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
"""Handling of the management command."""
|
||||||
|
if not settings.DEBUG and not options["force"]:
|
||||||
|
raise CommandError(
|
||||||
|
(
|
||||||
|
"This command is not meant to be used in production environment "
|
||||||
|
"except you know what you are doing, if so use --force parameter"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
create_demo(self.stdout)
|
||||||
49
src/backend/demo/management/commands/createsuperuser.py
Normal file
49
src/backend/demo/management/commands/createsuperuser.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
Management command overrring the "createsuperuser" command to allow creating users
|
||||||
|
with their email and no username.
|
||||||
|
"""
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""Management command to create super users from an email and a password"""
|
||||||
|
|
||||||
|
help = "Create a superuser with an email and a password"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
"""Define required arguments "email" and "password"."""
|
||||||
|
parser.add_argument(
|
||||||
|
"--email",
|
||||||
|
help=("Email for the user."),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--password",
|
||||||
|
help="Password for the user.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
"""
|
||||||
|
Given an email and a password, create a superuser or upgrade the existing
|
||||||
|
user to superuser status.
|
||||||
|
"""
|
||||||
|
email = options.get("email")
|
||||||
|
try:
|
||||||
|
user = User.objects.get(email=email)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
user = User(email=email)
|
||||||
|
message = "Superuser created successfully."
|
||||||
|
else:
|
||||||
|
if user.is_superuser and user.is_staff:
|
||||||
|
message = "Superuser already exists."
|
||||||
|
else:
|
||||||
|
message = "User already existed and was upgraded to superuser."
|
||||||
|
|
||||||
|
user.is_superuser = True
|
||||||
|
user.is_staff = True
|
||||||
|
user.set_password(options["password"])
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(message))
|
||||||
0
src/backend/demo/tests/__init__.py
Normal file
0
src/backend/demo/tests/__init__.py
Normal file
32
src/backend/demo/tests/test_commands_create_demo.py
Normal file
32
src/backend/demo/tests/test_commands_create_demo.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""Test the `create_demo` management command"""
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django.test import override_settings
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core import models
|
||||||
|
|
||||||
|
from demo import defaults
|
||||||
|
|
||||||
|
TEST_NB_OBJECTS = {
|
||||||
|
"users": 5,
|
||||||
|
"teams": 3,
|
||||||
|
"max_identities_per_user": 3,
|
||||||
|
"max_users_per_team": 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(DEBUG=True)
|
||||||
|
@mock.patch.dict(defaults.NB_OBJECTS, TEST_NB_OBJECTS)
|
||||||
|
def test_commands_create_demo():
|
||||||
|
"""The create_demo management command should create objects as expected."""
|
||||||
|
call_command("create_demo")
|
||||||
|
|
||||||
|
assert models.User.objects.count() == 5
|
||||||
|
assert models.Identity.objects.exists()
|
||||||
|
assert models.Team.objects.count() == 3
|
||||||
|
assert models.TeamAccess.objects.count() >= 3
|
||||||
@@ -185,6 +185,7 @@ class Base(Configuration):
|
|||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
# People
|
# People
|
||||||
"core",
|
"core",
|
||||||
|
"demo",
|
||||||
"drf_spectacular",
|
"drf_spectacular",
|
||||||
# Third party apps
|
# Third party apps
|
||||||
"corsheaders",
|
"corsheaders",
|
||||||
|
|||||||
Reference in New Issue
Block a user