(backend) add file type fixtures to create_demo

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.
This commit is contained in:
Nicolas Clerc
2026-06-29 15:32:36 +02:00
parent f53d80dc98
commit 0a0199fc21
2 changed files with 186 additions and 31 deletions
@@ -1,4 +1,3 @@
# ruff: noqa: S106
"""create_demo management command""" """create_demo management command"""
import logging import logging
@@ -19,6 +18,72 @@ from demo import defaults
fake = Faker() fake = Faker()
logger = logging.getLogger(__file__) 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: class Timeit:
@@ -67,56 +132,51 @@ def create_users():
"""Create random users""" """Create random users"""
for user_id in range(defaults.NB_OBJECTS["users"]): for user_id in range(defaults.NB_OBJECTS["users"]):
email = f"user.test{user_id:d}@example.com" email = f"user.test{user_id:d}@example.com"
yield factories.UserFactory( yield get_or_create_demo_user(email)
admin_email=email,
email=email,
sub=email,
password="!",
is_superuser=False,
is_active=True,
is_staff=False,
)
def create_dev_users(): def create_dev_users():
"""Create development users""" """Create development users"""
for dev_user in defaults.DEV_USERS: for dev_user in defaults.DEV_USERS:
email = dev_user["email"] email = dev_user["email"]
user = factories.UserFactory( user = get_or_create_demo_user(email)
admin_email=email,
email=email,
sub=email,
password="!",
is_superuser=False,
is_active=True,
is_staff=False,
)
create_item(user) create_item(user)
yield 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""" """Create file item with the given user as creator"""
parent = factories.ItemFactory( file_data = file_data or {}
creator=user, content = file_data.get("content") or fake.sentence(nb_words=50).encode()
users=[(user, models.RoleChoices.OWNER)], if parent is DEFAULT_PARENT:
type=models.ItemTypeChoices.FOLDER, parent = factories.ItemFactory(
) creator=user,
users=[(user, models.RoleChoices.OWNER)],
type=models.ItemTypeChoices.FOLDER,
)
item = factories.ItemFactory( item = factories.ItemFactory(
type=models.ItemTypeChoices.FILE, type=models.ItemTypeChoices.FILE,
update_upload_state=models.ItemUploadStateChoices.READY, update_upload_state=models.ItemUploadStateChoices.READY,
link_reach=models.LinkReachChoices.AUTHENTICATED, link_reach=models.LinkReachChoices.AUTHENTICATED,
link_role=models.LinkRoleChoices.READER, link_role=models.LinkRoleChoices.READER,
creator=user, creator=user,
users=[(user, models.RoleChoices.OWNER)] if parent is None else None,
parent=parent, parent=parent,
title=fake.sentence(nb_words=4), title=title or fake.sentence(nb_words=4),
filename="content.txt", filename=file_data.get("filename", "content.txt"),
description=fake.sentence(nb_words=10), 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 return item
@@ -128,7 +188,22 @@ def create_items(users):
yield create_item(user) 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. 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, 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): class Command(BaseCommand):
"""A management command to create a demo database.""" """A management command to create a demo database."""
@@ -167,6 +264,13 @@ class Command(BaseCommand):
default=False, default=False,
help="Force command execution despite DEBUG is set to 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): def handle(self, *args, **options):
"""Handling of the management command.""" """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"],
)
@@ -8,6 +8,7 @@ from django.test import override_settings
import pytest import pytest
from core import models from core import models
from demo.management.commands.create_demo import FILE_TYPE_ITEMS
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@@ -32,3 +33,50 @@ def test_commands_create_demo():
# assert dev users have doc accesses # assert dev users have doc accesses
user = models.User.objects.get(email="drive@drive.world") user = models.User.objects.get(email="drive@drive.world")
assert models.ItemAccess.objects.filter(user=user).exists() 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