From 0a0199fc21d5458ca09b9cc2be58d4eb51c2d2c6 Mon Sep 17 00:00:00 2001 From: Nicolas Clerc Date: Fri, 15 May 2026 17:43:40 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(backend)=20add=20file=20type=20fixtur?= =?UTF-8?q?es=20to=20create=5Fdemo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The --file-types flag creates one fixture per category (docx, xlsx, pptx, pdf, image, video, zip, mp3, binary) so developers can populate a local demo database covering all filter-relevant types, without having to upload real files manually. --- .../demo/management/commands/create_demo.py | 169 ++++++++++++++---- .../demo/tests/test_commands_create_demo.py | 48 +++++ 2 files changed, 186 insertions(+), 31 deletions(-) diff --git a/src/backend/demo/management/commands/create_demo.py b/src/backend/demo/management/commands/create_demo.py index 20b2fa64..5322ffc5 100644 --- a/src/backend/demo/management/commands/create_demo.py +++ b/src/backend/demo/management/commands/create_demo.py @@ -1,4 +1,3 @@ -# ruff: noqa: S106 """create_demo management command""" import logging @@ -19,6 +18,72 @@ from demo import defaults fake = Faker() logger = logging.getLogger(__file__) +DEFAULT_PARENT = object() + + +FILE_TYPE_ITEMS = ( + { + "title": "Demo text document", + "filename": "demo-text-document.docx", + "mimetype": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + }, + { + "title": "Demo spreadsheet", + "filename": "demo-spreadsheet.xlsx", + "mimetype": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }, + { + "title": "Demo presentation", + "filename": "demo-presentation.pptx", + "mimetype": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + }, + { + "title": "Demo PDF", + "filename": "demo-pdf.pdf", + "mimetype": "application/pdf", + }, + { + "title": "Demo image", + "filename": "demo-image.png", + "mimetype": "image/png", + }, + { + "title": "Demo video", + "filename": "demo-video.mp4", + "mimetype": "video/mp4", + }, + { + "title": "Demo archive", + "filename": "demo-archive.zip", + "mimetype": "application/zip", + }, + { + "title": "Demo audio", + "filename": "demo-audio.mp3", + "mimetype": "audio/mpeg", + }, + { + "title": "Demo other file", + "filename": "demo-other.bin", + "mimetype": "application/octet-stream", + }, +) + + +def get_or_create_demo_user(email): + """Get an existing demo user or create it when absent.""" + user, _created = models.User.objects.get_or_create( + sub=email, + defaults={ + "admin_email": email, + "email": email, + "password": "!", + "is_superuser": False, + "is_active": True, + "is_staff": False, + }, + ) + return user class Timeit: @@ -67,56 +132,51 @@ 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, - ) + yield get_or_create_demo_user(email) 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, - ) + user = get_or_create_demo_user(email) create_item(user) yield user -def create_item(user): +def create_item( + user, + title=None, + file_data=None, + parent=DEFAULT_PARENT, +): """Create file item with the given user as creator""" - parent = factories.ItemFactory( - creator=user, - users=[(user, models.RoleChoices.OWNER)], - type=models.ItemTypeChoices.FOLDER, - ) + file_data = file_data or {} + content = file_data.get("content") or fake.sentence(nb_words=50).encode() + if parent is DEFAULT_PARENT: + parent = factories.ItemFactory( + creator=user, + users=[(user, models.RoleChoices.OWNER)], + type=models.ItemTypeChoices.FOLDER, + ) + 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, + users=[(user, models.RoleChoices.OWNER)] if parent is None else None, parent=parent, - title=fake.sentence(nb_words=4), - filename="content.txt", + title=title or fake.sentence(nb_words=4), + filename=file_data.get("filename", "content.txt"), description=fake.sentence(nb_words=10), - mimetype="text/plain", + mimetype=file_data.get("mimetype", "text/plain"), + size=len(content), ) - default_storage.save(item.file_key, BytesIO(fake.sentence(nb_words=50).encode())) + default_storage.save(item.file_key, BytesIO(content)) return item @@ -128,7 +188,22 @@ def create_items(users): yield create_item(user) -def create_demo(stdout): +def create_file_type_items(user): + """Create one ready file for each file type category described in issue #597.""" + for file_type_item in FILE_TYPE_ITEMS: + yield create_item( + user, + title=file_type_item["title"], + file_data={ + "filename": file_type_item["filename"], + "mimetype": file_type_item["mimetype"], + "content": f"{file_type_item['title']} fixture".encode(), + }, + parent=None, + ) + + +def create_demo(stdout, *, file_types=False): """ Create a database with demo data for developers to work in a realistic environment. """ @@ -152,6 +227,28 @@ def create_demo(stdout): role=models.RoleChoices.READER, ) + if file_types: + with Timeit(stdout, "Creating file type items"): + list(create_file_type_items(dev_users[0])) + + with Timeit(stdout, "Sharing development users items with demo users"): + dev_items = models.Item.objects.filter( + creator__in=dev_users, type=models.ItemTypeChoices.FILE + ).exclude(accesses__user__in=users) + for item in dev_items: + nb_users = secrets.randbelow(len(users)) + 1 + shared_users = fake.random_sample(elements=users, length=nb_users) + for user in shared_users: + factories.UserItemAccessFactory( + item=item, + user=user, + role=models.RoleChoices.READER, + ) + + names = ", ".join(user.short_name for user in shared_users) + item.title = f"{item.title} (shared with {names})" + item.save(update_fields=["title"]) + class Command(BaseCommand): """A management command to create a demo database.""" @@ -167,6 +264,13 @@ class Command(BaseCommand): default=False, help="Force command execution despite DEBUG is set to False", ) + parser.add_argument( + "--file_types", + "--file-types", + action="store_true", + default=False, + help="Create items for several file types", + ) def handle(self, *args, **options): """Handling of the management command.""" @@ -178,4 +282,7 @@ class Command(BaseCommand): ) ) - create_demo(self.stdout) + create_demo( + self.stdout, + file_types=options["file_types"], + ) diff --git a/src/backend/demo/tests/test_commands_create_demo.py b/src/backend/demo/tests/test_commands_create_demo.py index 7bc136c1..16e7236a 100644 --- a/src/backend/demo/tests/test_commands_create_demo.py +++ b/src/backend/demo/tests/test_commands_create_demo.py @@ -8,6 +8,7 @@ from django.test import override_settings import pytest from core import models +from demo.management.commands.create_demo import FILE_TYPE_ITEMS pytestmark = pytest.mark.django_db @@ -32,3 +33,50 @@ def test_commands_create_demo(): # assert dev users have doc accesses user = models.User.objects.get(email="drive@drive.world") assert models.ItemAccess.objects.filter(user=user).exists() + + +@override_settings(DEBUG=True) +def test_commands_create_demo_shares_dev_users_items_with_demo_users(): + """The create_demo command should share existing dev users items with demo users.""" + call_command("create_demo") + + dev_user = models.User.objects.get(email="drive@drive.world") + dev_items = models.Item.objects.filter(creator=dev_user, type=models.ItemTypeChoices.FILE) + assert dev_items.count() == 2 + for item in dev_items: + assert models.ItemAccess.objects.filter(item=item).exclude(user=dev_user).exists() + + +@override_settings(DEBUG=True) +def test_commands_create_demo_with_file_types(): + """The create_demo command should optionally add files for filter development.""" + call_command("create_demo", "--file_types") + + expected_filenames = {item["filename"] for item in FILE_TYPE_ITEMS} + expected_mimetypes = {item["mimetype"] for item in FILE_TYPE_ITEMS} + file_type_items = models.Item.objects.filter(filename__in=expected_filenames) + + assert file_type_items.count() == len(FILE_TYPE_ITEMS) + assert set(file_type_items.values_list("mimetype", flat=True)) == expected_mimetypes + assert all(item.is_root for item in file_type_items) + + dev_user = models.User.objects.get(email="drive@drive.world") + for item in file_type_items: + assert models.ItemAccess.objects.filter(item=item).exclude(user=dev_user).exists() + + +@mock.patch( + "demo.defaults.NB_OBJECTS", + { + "users": 10, + "files": 10, + "max_users_per_document": 5, + }, +) +@override_settings(DEBUG=True) +def test_commands_create_demo_can_be_run_twice_without_resetting_database(): + """The create_demo command should reuse deterministic demo users.""" + call_command("create_demo", "--file_types") + call_command("create_demo", "--file_types") + + assert models.User.objects.filter(email="drive@drive.world").count() == 1