From dd79e100ffd6e694c0bf8f25ae3c26b954b3df6f Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 10 Apr 2024 16:29:28 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=A1(backend)=20demo=20template?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create the create_demo command. It creates a demo template for the backend. We add it in the makefile bootstrap. We force the template id, this id is the same as used in the frontend for the moment. When the template feature will be created in the frontend side we will not have to force it anymore. --- Makefile | 1 + .../demo/management/commands/create_demo.py | 242 ++++++++++++++++++ src/backend/demo/tests/__init__.py | 0 .../demo/tests/test_commands_create_demo.py | 22 ++ 4 files changed, 265 insertions(+) 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 ac3335065..4d8bbe5b8 100644 --- a/Makefile +++ b/Makefile @@ -84,6 +84,7 @@ bootstrap: \ build \ run \ migrate \ + demo \ back-i18n-compile \ mails-install \ mails-build \ 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 000000000..ab7c558e1 --- /dev/null +++ b/src/backend/demo/management/commands/create_demo.py @@ -0,0 +1,242 @@ +# ruff: noqa: S311, S106 +"""create_demo management command""" + +import logging +import random +import time +from collections import defaultdict + +from django import db +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError + +from faker import Faker + +from core import models + +fake = Faker() + +logger = logging.getLogger("impress.commands.demo.create_demo") + + +def random_true_with_probability(probability): + """return True with the requested probability, False otherwise.""" + return random.random() < probability + + +class BulkQueue: + """A utility class to create Django model instances in bulk by just pushing to a queue.""" + + BATCH_SIZE = 20000 + + def __init__(self, stdout, *args, **kwargs): + """Define the queue as a dict of lists.""" + self.queue = defaultdict(list) + self.stdout = stdout + + def _bulk_create(self, objects): + """Actually create instances in bulk in the database.""" + if not objects: + return + + objects[0]._meta.model.objects.bulk_create(objects, ignore_conflicts=False) # noqa: SLF001 + # In debug mode, Django keeps query cache which creates a memory leak in this case + db.reset_queries() + self.queue[objects[0]._meta.model.__name__] = [] # noqa: SLF001 + + def push(self, obj): + """Add a model instance to queue to that it gets created in bulk.""" + objects = self.queue[obj._meta.model.__name__] # noqa: SLF001 + objects.append(obj) + if len(objects) > self.BATCH_SIZE: + self._bulk_create(objects) + self.stdout.write(".", ending="") + + def flush(self): + """Flush the queue after creating the remaining model instances.""" + for objects in self.queue.values(): + self._bulk_create(objects) + + +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_demo(stdout): + """ + Create a database with demo data for developers to work in a realistic environment. + The code is engineered to create a huge number of objects fast. + """ + + queue = BulkQueue(stdout) + + with Timeit(stdout, "Creating Template"): + queue.push( + models.Template( + id="472d0633-20b8-4cb1-998a-1134ade092ba", + title="Demo Template", + description="This is the demo template", + code=""" + +
+ +

Direction
Interministérielle
du numérique

+
+
+
+
La directrice
+

Réf: 1200001

+
+
Paris, le 28/09/2023
+
+
+

Note

+
à Monsieur le Premier Ministre
+
+ +
+
Objet: Generated PDF
+
{{ body }}
+
+
+""", + css=""" +body { + background: white; + font-family: arial +} + +img { + width: 5cm; + margin-left: -0.4cm; +} + +.header { + display: flex; + justify-content: space-between; +} + +.header-title { + text-align: right; + margin-top: 3rem; + font-size: 1.2rem; +} + +.second-row { + display: flex; + justify-content: space-between; + margin-top: 1.2cm; +} + +.ref { + margin-top: 0; +} + +.who { + font-weight: medium; +} + +.date, .ref { + font-size: 12px; +} + +.title, .subtitle { + margin: 0; +} + +.subtitle { + font-weight: normal; +} + +.object { + font-weight: bold; + margin-bottom: 1.2cm; + margin-top: 3rem +} +.body{ + margin-top: 1.5rem +} + +h1 { + font-size: 18px; +} + +h2 { + font-size: 14px; +} + +p { + text-align: justify; + ligne-height: 0.8; +} +""", + is_public=True, + ) + ) + queue.flush() + + +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 000000000..e69de29bb 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 000000000..7eb31ea34 --- /dev/null +++ b/src/backend/demo/tests/test_commands_create_demo.py @@ -0,0 +1,22 @@ +"""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 + + +@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.Template.objects.count() == 1