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 20b2fa646..e64917639 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,84 @@ 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(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 class Timeit: @@ -65,58 +142,51 @@ 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 factories.UserFactory( - admin_email=email, - email=email, - sub=email, - password="!", - is_superuser=False, - is_active=True, - is_staff=False, - ) + 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 = 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(dev_user) 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 +198,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 +237,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 +256,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 +274,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..3e93325a2 100644 --- a/src/backend/demo/tests/test_commands_create_demo.py +++ b/src/backend/demo/tests/test_commands_create_demo.py @@ -1,7 +1,5 @@ """Test the `create_demo` management command""" -from unittest import mock - from django.core.management import call_command from django.test import override_settings @@ -9,26 +7,51 @@ 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 # 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) + + +@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