From 415e83f34476a08913771a67c41aed4edbf04c8d Mon Sep 17 00:00:00 2001 From: Fabre Florian Date: Thu, 16 Oct 2025 11:44:58 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(backend)=20add=20demo=20populate=20co?= =?UTF-8?q?mmand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new Django app 'demo' that contains the command 'create_demo' Generate dummy users and files with existing factories. Signed-off-by: Fabre Florian --- Makefile | 9 + src/backend/demo/__init__.py | 0 src/backend/demo/defaults.py | 7 + src/backend/demo/management/__init__.py | 0 .../demo/management/commands/__init__.py | 0 .../demo/management/commands/create_demo.py | 182 ++++++++++++++++++ src/backend/demo/tests/__init__.py | 0 .../demo/tests/test_commands_create_demo.py | 34 ++++ src/backend/drive/settings.py | 3 +- 9 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 src/backend/demo/__init__.py create mode 100644 src/backend/demo/defaults.py create mode 100644 src/backend/demo/management/__init__.py create mode 100644 src/backend/demo/management/commands/__init__.py create mode 100644 src/backend/demo/management/commands/create_demo.py create mode 100644 src/backend/demo/tests/__init__.py create mode 100644 src/backend/demo/tests/test_commands_create_demo.py diff --git a/Makefile b/Makefile index f4adcdb6..8f9cac00 100644 --- a/Makefile +++ b/Makefile @@ -177,6 +177,15 @@ stop: ## stop the development server using Docker # -- Backend +demo: ## flush db then create a demo for load testing purpose + @$(MAKE) resetdb + @$(MANAGE) create_demo +.PHONY: demo + +index: ## index all files to remote search + @$(MANAGE) index +.PHONY: index + # Nota bene: Black should come after isort just in case they don't agree... lint: ## lint back-end python sources lint: \ diff --git a/src/backend/demo/__init__.py b/src/backend/demo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/backend/demo/defaults.py b/src/backend/demo/defaults.py new file mode 100644 index 00000000..f88e775b --- /dev/null +++ b/src/backend/demo/defaults.py @@ -0,0 +1,7 @@ +"""Parameters that define how the demo site will be built.""" + +NB_OBJECTS = {"users": 50, "files": 50, "max_users_per_document": 50} + +DEV_USERS = [ + {"username": "drive", "email": "drive@drive.world", "language": "en-us"}, +] diff --git a/src/backend/demo/management/__init__.py b/src/backend/demo/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/backend/demo/management/commands/__init__.py b/src/backend/demo/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/backend/demo/management/commands/create_demo.py b/src/backend/demo/management/commands/create_demo.py new file mode 100644 index 00000000..7d4be71d --- /dev/null +++ b/src/backend/demo/management/commands/create_demo.py @@ -0,0 +1,182 @@ +# ruff: noqa: S311, S106 +"""create_demo management command""" + +import logging +import random +import time +from io import BytesIO + +from django.conf import settings +from django.core.files.storage import default_storage +from django.core.management.base import BaseCommand, CommandError + +from faker import Faker + +from core import factories, models + +from demo import defaults + +fake = Faker() + +logger = logging.getLogger("impress.commands.demo.create_demo") + + +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_users(): + """Create random users""" + for user_id in range(defaults.NB_OBJECTS["users"]): + email = f"user.test{user_id:d}@example.com" + yield factories.UserFactory( + admin_email=email, + email=email, + sub=email, + password="!", + is_superuser=False, + is_active=True, + is_staff=False, + ) + + +def create_dev_users(): + """Create development users""" + for dev_user in defaults.DEV_USERS: + email = dev_user["email"] + user = factories.UserFactory( + admin_email=email, + email=email, + sub=email, + password="!", + is_superuser=False, + is_active=True, + is_staff=False, + ) + + create_item(user) + yield user + + +def create_item(user): + """Create file item with the given user as creator""" + workspace = user.get_main_workspace() + item = factories.ItemFactory( + type=models.ItemTypeChoices.FILE, + update_upload_state=models.ItemUploadStateChoices.READY, + link_reach=models.LinkReachChoices.AUTHENTICATED, + link_role=models.LinkRoleChoices.READER, + creator=user, + parent=workspace, + title=fake.sentence(nb_words=4), + filename="content.txt", + description=fake.sentence(nb_words=10), + mimetype="text/plain", + ) + + default_storage.connection.meta.client.put_object( + Bucket=default_storage.bucket_name, + Key=item.file_key, + Body=BytesIO(fake.sentence(nb_words=50).encode()), + ContentType="text/plain", + ) + + return item + + +def create_items(users): + """Create random items and file content for users""" + for _id in range(defaults.NB_OBJECTS["files"]): + user = random.choice(users) + yield create_item(user) + + +def create_demo(stdout): + """ + Create a database with demo data for developers to work in a realistic environment. + """ + with Timeit(stdout, "Creating users"): + users = list(create_users()) + + with Timeit(stdout, "Creating items"): + items = list(create_items(users)) + + with Timeit(stdout, "Creating development users"): + dev_users = list(create_dev_users()) + + with Timeit(stdout, "Creating item accesses on development users"): + for user in dev_users: + create_item(user) + + for item in items: + factories.UserItemAccessFactory( + item=item, + user=user, + role=models.RoleChoices.READER, + ) + + +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) diff --git a/src/backend/demo/tests/__init__.py b/src/backend/demo/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/backend/demo/tests/test_commands_create_demo.py b/src/backend/demo/tests/test_commands_create_demo.py new file mode 100644 index 00000000..7bc136c1 --- /dev/null +++ b/src/backend/demo/tests/test_commands_create_demo.py @@ -0,0 +1,34 @@ +"""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 + +pytestmark = pytest.mark.django_db + + +@mock.patch( + "demo.defaults.NB_OBJECTS", + { + "users": 10, + "files": 10, + "max_users_per_document": 5, + }, +) +@override_settings(DEBUG=True) +def test_commands_create_demo(): + """The create_demo management command should create objects as expected.""" + call_command("create_demo") + + assert models.User.objects.count() >= 10 + assert models.Item.objects.count() >= 10 + assert models.ItemAccess.objects.count() > 10 + + # assert dev users have doc accesses + user = models.User.objects.get(email="drive@drive.world") + assert models.ItemAccess.objects.filter(user=user).exists() diff --git a/src/backend/drive/settings.py b/src/backend/drive/settings.py index 9a28007e..e595e6ba 100755 --- a/src/backend/drive/settings.py +++ b/src/backend/drive/settings.py @@ -998,6 +998,7 @@ class Development(Base): "drf_spectacular_sidecar", "debug_toolbar", "e2e", + "demo", ] @@ -1018,7 +1019,7 @@ class Test(Base): def __init__(self): # pylint: disable=invalid-name - self.INSTALLED_APPS += ["drf_spectacular_sidecar", "e2e"] + self.INSTALLED_APPS += ["drf_spectacular_sidecar", "e2e", "demo"] class ContinuousIntegration(Test):