From 253ce88b808ced7327d24451f8227a107d2a58cf Mon Sep 17 00:00:00 2001 From: Nicolas Clerc Date: Fri, 15 May 2026 17:43:40 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8(backend)=20add=20file=20type=20fi?= =?UTF-8?q?xtures=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 | 151 ++++++++++++++---- .../demo/tests/test_commands_create_demo.py | 32 ++++ 2 files changed, 152 insertions(+), 31 deletions(-) diff --git a/src/backend/demo/management/commands/create_demo.py b/src/backend/demo/management/commands/create_demo.py index 20b2fa646..6f32d53b8 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 @@ 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,10 @@ 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])) + class Command(BaseCommand): """A management command to create a demo database.""" @@ -167,6 +246,13 @@ def add_arguments(self, parser): 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 files covering the file type categories from issue #597", + ) def handle(self, *args, **options): """Handling of the management command.""" @@ -178,4 +264,7 @@ def handle(self, *args, **options): ) ) - 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 7bc136c10..11b83ebf9 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 @@ import pytest from core import models +from demo.management.commands.create_demo import FILE_TYPE_ITEMS pytestmark = pytest.mark.django_db @@ -32,3 +33,34 @@ 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_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) + + +@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 From f0f1924a3545885c55f9e2d42dbeaa01724f1be0 Mon Sep 17 00:00:00 2001 From: Nicolas Clerc Date: Fri, 15 May 2026 17:46:50 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8(backend)=20improve=20demo=20users?= =?UTF-8?q?=20for=20sharing=20fixtures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sharing filter needs a realistic contact picker with user names. Switching from anonymous emails to a fixed list of named users gives the demo database the full_name and short_name fields required for that UI. --- src/backend/demo/defaults.py | 33 +++++++++++++++++-- .../demo/management/commands/create_demo.py | 22 +++++++++---- .../demo/tests/test_commands_create_demo.py | 29 ++++++---------- 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/src/backend/demo/defaults.py b/src/backend/demo/defaults.py index f88e775b7..ccd36c274 100644 --- a/src/backend/demo/defaults.py +++ b/src/backend/demo/defaults.py @@ -1,7 +1,36 @@ """Parameters that define how the demo site will be built.""" -NB_OBJECTS = {"users": 50, "files": 50, "max_users_per_document": 50} +NB_OBJECTS = {"users": 4, "files": 50, "max_users_per_document": 4} + +USERS = [ + { + "email": "page.turner@library.book", + "full_name": "Paige Turner", + "short_name": "Paige", + }, + { + "email": "miles.ahead@roadmap.fwd", + "full_name": "Miles Ahead", + "short_name": "Miles", + }, + { + "email": "archie.vist@vaulted.docs", + "full_name": "Archie Vist", + "short_name": "Archie", + }, + { + "email": "wade.wilson@maximum.effort", + "full_name": "Wade Wilson", + "short_name": "Wade", + }, +] DEV_USERS = [ - {"username": "drive", "email": "drive@drive.world", "language": "en-us"}, + { + "username": "drive", + "email": "drive@drive.world", + "full_name": "Drive Developer", + "short_name": "Drive", + "language": "en-us", + }, ] diff --git a/src/backend/demo/management/commands/create_demo.py b/src/backend/demo/management/commands/create_demo.py index 6f32d53b8..e64917639 100644 --- a/src/backend/demo/management/commands/create_demo.py +++ b/src/backend/demo/management/commands/create_demo.py @@ -70,19 +70,31 @@ ) -def get_or_create_demo_user(email): +def get_or_create_demo_user(user_data): """Get an existing demo user or create it when absent.""" + email = user_data["email"] user, _created = models.User.objects.get_or_create( sub=email, defaults={ "admin_email": email, "email": email, + "full_name": user_data["full_name"], + "short_name": user_data["short_name"], "password": "!", "is_superuser": False, "is_active": True, "is_staff": False, }, ) + update_fields = [] + for field in ["full_name", "short_name"]: + if not getattr(user, field): + setattr(user, field, user_data[field]) + update_fields.append(field) + + if update_fields: + user.save(update_fields=update_fields) + return user @@ -130,16 +142,14 @@ def __exit__(self, exc_type, exc_value, exc_tb): 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 get_or_create_demo_user(email) + for user_data in defaults.USERS[: defaults.NB_OBJECTS["users"]]: + yield get_or_create_demo_user(user_data) def create_dev_users(): """Create development users""" for dev_user in defaults.DEV_USERS: - email = dev_user["email"] - user = get_or_create_demo_user(email) + user = get_or_create_demo_user(dev_user) create_item(user) yield user diff --git a/src/backend/demo/tests/test_commands_create_demo.py b/src/backend/demo/tests/test_commands_create_demo.py index 11b83ebf9..3e93325a2 100644 --- a/src/backend/demo/tests/test_commands_create_demo.py +++ b/src/backend/demo/tests/test_commands_create_demo.py @@ -1,32 +1,31 @@ """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 from demo.management.commands.create_demo import FILE_TYPE_ITEMS 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.User.objects.count() == 5 + assert set(models.User.objects.values_list("email", flat=True)) == { + user["email"] for user in defaults.USERS + } | {"drive@drive.world"} + assert not models.User.objects.filter(full_name__isnull=True).exists() + assert not models.User.objects.filter(full_name="").exists() + assert not models.User.objects.filter(short_name__isnull=True).exists() + assert not models.User.objects.filter(short_name="").exists() assert models.Item.objects.count() >= 10 assert models.ItemAccess.objects.count() > 10 @@ -49,14 +48,6 @@ def test_commands_create_demo_with_file_types(): assert all(item.is_root for item in file_type_items) -@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."""