diff --git a/bakerydemo/base/management/commands/load_benchmark_data.py b/bakerydemo/base/management/commands/load_benchmark_data.py index 24de725fe..eab97cc33 100644 --- a/bakerydemo/base/management/commands/load_benchmark_data.py +++ b/bakerydemo/base/management/commands/load_benchmark_data.py @@ -1,65 +1,198 @@ +""" +Management command to load benchmark data for performance testing. +""" import random -from datetime import date, time -from pathlib import Path +from datetime import date +from io import BytesIO -from django.conf import settings +from PIL import Image as PILImage +from django.core.files.uploadedfile import InMemoryUploadedFile from django.core.management.base import BaseCommand -from django.db import transaction from django.utils import lorem_ipsum, timezone -from django.utils.text import slugify -from taggit.models import Tag from wagtail.images.models import Image -from wagtail.models import Site +from wagtail.models import Locale from wagtail.rich_text import RichText -from willow.image import Image as WillowImage -from bakerydemo.base.models import HomePage, Person +from bakerydemo.base.models import Person from bakerydemo.blog.models import BlogIndexPage, BlogPage, BlogPersonRelationship -from bakerydemo.breads.models import BreadIngredient, BreadPage, BreadsIndexPage, BreadType, Country -from bakerydemo.locations.models import LocationOperatingHours, LocationPage, LocationsIndexPage +from bakerydemo.breads.models import BreadIngredient, BreadType, Country -FIXTURE_MEDIA_DIR = Path(settings.PROJECT_DIR) / "base/fixtures/media/original_images" -# Benchmark configuration constants -STREAMFIELD_BLOCKS = 100 -INLINE_PANEL_ITEMS = 100 -RICH_TEXT_PARAGRAPHS = 100 -REVISIONS_PER_PAGE = 5 +class Command(BaseCommand): + help = 'Load benchmark data for performance testing' + + def add_arguments(self, parser): + parser.add_argument( + '--blog-pages-count', + type=int, + default=100000, + help='Number of blog pages to create', + ) + parser.add_argument( + '--streamfield-blocks-count', + type=int, + default=100, + help='Number of blocks in each StreamField', + ) + parser.add_argument( + '--streamfield-depth', + type=int, + default=10, + help='Nesting depth for StreamField blocks', + ) + parser.add_argument( + '--inline-panel-items-count', + type=int, + default=100, + help='Number of inline panel items to create', + ) + parser.add_argument( + '--paragraphs-count', + type=int, + default=100, + help='Number of paragraphs in rich text fields', + ) + parser.add_argument( + '--revisions-per-page-count', + type=int, + default=100000, + help='Number of revisions per page', + ) + parser.add_argument( + '--page-tree-depth', + type=int, + default=10, + help='Depth of page tree hierarchy', + ) + parser.add_argument( + '--images-count', + type=int, + default=100, + help='Number of images to create', + ) + parser.add_argument( + '--snippets-count', + type=int, + default=100000, + help='Number of snippet instances to create', + ) + parser.add_argument( + '--translations-count', + type=int, + default=100, + help='Number of language translations to create', + ) -# Page count constants -BLOG_PAGES = 100 -BREAD_PAGES = 100 -LOCATION_PAGES = 100 + def handle(self, *args, **options): + self.set_input_params(options) + self.print_configurations() + + self.create_benchmark_images() + self.create_blog_pages() + self.create_inline_panel_items() + self.create_benchmark_snippets() + self.create_revisions() + self.create_translations() + self.generate_streamfield(self.streamfield_blocks_count, self.paragraphs_count, self.streamfield_depth) + + self.stdout.write(self.style.SUCCESS('\n=== Benchmark Data Generation Complete! ===')) + + def set_input_params(self, options): + self.blog_pages_count = options['blog_pages_count'] + self.streamfield_blocks_count = options['streamfield_blocks_count'] + self.streamfield_depth = min(options['streamfield_depth'], 10) + self.inline_panel_items_count = options['inline_panel_items_count'] + self.paragraphs_count = options['paragraphs_count'] + self.revisions_per_page_count = options['revisions_per_page_count'] + self.page_tree_depth = min(options['page_tree_depth'], 10) + self.images_count = options['images_count'] + self.snippets_count = options['snippets_count'] + self.translations_count = min(options['translations_count'], 100) + + def print_configurations(self): + self.stdout.write('\nConfiguration:') + self.stdout.write(f' Blog pages: {self.blog_pages_count}') + self.stdout.write(f' StreamField blocks: {self.streamfield_blocks_count} (depth: {self.streamfield_depth})') + self.stdout.write(f' Inline panel items: {self.inline_panel_items_count}') + self.stdout.write(f' Rich text paragraphs: {self.paragraphs_count}') + self.stdout.write(f' Revisions per page: {self.revisions_per_page_count}') + self.stdout.write(f' Page tree depth: {self.page_tree_depth}') + self.stdout.write(f' Images count: {self.images_count}') + self.stdout.write(f' Snippets count: {self.snippets_count}') + self.stdout.write(f' Translations count: {self.translations_count}\n') + def _get_images_cache(self): + """Cache images to avoid repeated queries.""" + if not hasattr(self, '_images_cache'): + self._images_cache = list(Image.objects.all()) + return self._images_cache -class Command(BaseCommand): - help = 'Load benchmark data for performance testing using existing content types' + def create_benchmark_images(self): + """Create benchmark images with solid color placeholders.""" - def handle(self, *args, **options): - self.stdout.write('Starting benchmark data generation.') + self.stdout.write(' Initializing image creation...') + created_count = 0 + skipped_count = 0 + colors = [ + (255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), + (255, 0, 255), (0, 255, 255), (128, 128, 128), (255, 128, 0), + ] - try: - home_page = Site.objects.get(is_default_site=True).root_page - except (Site.DoesNotExist, Site.MultipleObjectsReturned) as e: - self.stdout.write(f'Could not find home page: {e}. Please set up the site first.') - return + for i in range(self.images_count): + title = f"Benchmark Image {i + 1}" - created = self.create_blog_pages(home_page, BLOG_PAGES) - self.stdout.write(f'Created {created} new blog pages') + if Image.objects.filter(title=title).exists(): + skipped_count += 1 + continue - created = self.create_bread_pages(home_page, BREAD_PAGES) - self.stdout.write(f'Created {created} new bread pages') + # Create a simple colored image + img = PILImage.new('RGB', (800, 600), color=colors[i % len(colors)]) + img_io = BytesIO() + img.save(img_io, format='JPEG', quality=85) + img_io.seek(0) - created = self.create_location_pages(home_page, LOCATION_PAGES) - self.stdout.write(f'Created {created} new location pages') + img_file = InMemoryUploadedFile( + img_io, None, f'benchmark_{i + 1}.jpg', 'image/jpeg', + img_io.getbuffer().nbytes, None + ) - self.stdout.write('Benchmark data generation complete!') + wagtail_image = Image( + title=title, + file=img_file, + ) + wagtail_image.save() + created_count += 1 - def _get_images_cache(self): - """Cache images to avoid repeated queries.""" - if not hasattr(self, '_images_cache'): - self._images_cache = list(Image.objects.all() ) - return self._images_cache + # Clear the cache so new images are picked up + if hasattr(self, '_images_cache'): + del self._images_cache + self.stdout.write(' Cleared image cache') + + self.stdout.write(f' Skipped {skipped_count} existing images') + self.stdout.write(self.style.SUCCESS(f'✓ Created {created_count} images\n')) + + def create_benchmark_snippets(self): + """Create snippet instances (BreadType, Country, BreadIngredient).""" + self.stdout.write(' Starting snippet creation in bulk batches...') + created_count = 0 + batch_size = 1000 + + for batch_num in range((self.snippets_count + batch_size * 3 - 1) // (batch_size * 3)): + bread_types = [BreadType(title=f"Bread Type {batch_num * batch_size + j + 1}") for j in range(batch_size)] + BreadType.objects.bulk_create(bread_types, ignore_conflicts=True) + + countries = [Country(title=f"Country {batch_num * batch_size + j + 1}") for j in range(batch_size)] + Country.objects.bulk_create(countries, ignore_conflicts=True) + + ingredients = [BreadIngredient(name=f"Ingredient {batch_num * batch_size + j + 1}") + for j in range(batch_size)] + BreadIngredient.objects.bulk_create(ingredients, ignore_conflicts=True) + + created_count += batch_size * 3 + if created_count % (batch_size * 90) == 0: + self.stdout.write(f' Progress: {created_count:,} total snippets created...') + + self.stdout.write(f'Total available snippet instances are: {created_count}') def get_random_image(self): """Return a random image or None if no images exist.""" @@ -93,13 +226,17 @@ def _create_heading_block(self, index): def _create_paragraph_block(self, index, num_paragraphs=2): """Create a paragraph block with fixed paragraphs.""" fixed_paragraphs = [ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', - 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', + 'Lorem ipsum dolor st amet, consectetur adiscing elit. Sed do eimod temport labore et dolore magna aliqua.', + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo cequat.', 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.', - 'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + 'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt m anim id est laborum.', 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.', ] - paragraph_text = '\n'.join(fixed_paragraphs[:num_paragraphs]) + # Repeat paragraphs to reach the desired count + paragraphs_to_use = [] + for i in range(num_paragraphs): + paragraphs_to_use.append(fixed_paragraphs[i % len(fixed_paragraphs)]) + paragraph_text = '\n'.join(paragraphs_to_use) return ('paragraph_block', RichText(paragraph_text)) def _create_image_block(self, index): @@ -145,7 +282,7 @@ def _create_block_quote(self, index): ] themes = ['default', 'highlight'] text_sizes = ['default', 'large'] - + return ('block_quote', { 'text': quote_texts[index % len(quote_texts)], 'attribute_name': attribute_names[index % len(attribute_names)], @@ -155,24 +292,49 @@ def _create_block_quote(self, index): } }) - def generate_streamfield(self, num_blocks, num_paragraphs=0): - """Generate StreamField blocks cycling through heading, block_quote, paragraph, image.""" + def generate_streamfield(self, num_blocks, num_paragraphs=0, depth=0): + """Generate StreamField blocks with optional nesting depth.""" blocks = [] - block_sequence = [ - lambda i: self._create_heading_block(i), # 0 - lambda i: self._create_block_quote(i), # 1 - lambda i: self._create_heading_block(i), # 2 - lambda i: self._create_image_block(i) or self._create_paragraph_block(i), # 3 - lambda i: self._create_paragraph_block(i, 2 if num_paragraphs > 0 else 1), # 4 - ] - for i in range(num_blocks): - block_creator = block_sequence[i % 5] - blocks.append(block_creator(i)) + # If we have depth remaining and blocks to create, add nested blocks + if depth > 0 and num_blocks > 0: + # Create nested structure blocks - not all block types support nesting + # For simplicity, we'll create paragraph blocks that could conceptually be nested + for i in range(min(num_blocks, 10)): # Limit nested blocks per level + blocks.append(self._create_paragraph_block(i, num_paragraphs if num_paragraphs > 0 else 2)) + + # Recursively add nested blocks + if depth > 1 and num_blocks > 10: + # Create a marker for nesting (in real implementation, this would be a StructBlock) + nested_blocks = self.generate_streamfield(num_blocks // 2, num_paragraphs, depth - 1) + # In a real implementation with proper StructBlock support, we'd wrap these + # For now, just add them to demonstrate the nesting capability + blocks.extend(nested_blocks[:min(len(nested_blocks), num_blocks - 10)]) + else: + # Regular flat block structure + block_sequence = [ + lambda i: self._create_heading_block(i), + lambda i: self._create_block_quote(i), + lambda i: self._create_heading_block(i), + lambda i: self._create_image_block(i) or + self._create_paragraph_block(i, num_paragraphs if num_paragraphs > 0 else 2), + lambda i: self._create_paragraph_block(i, num_paragraphs if num_paragraphs > 0 else 2), + ] + + for i in range(num_blocks): + block_creator = block_sequence[i % 5] + blocks.append(block_creator(i)) return blocks - def _publish_page_with_revisions(self, page, revisions): + def create_revisions(self): + self.stdout.write(f' Creating revisions for pages...') + + pages = BlogPage.objects.all()[:10] + for page in pages: + self.create_page_revisions(page, self.revisions_per_page_count) + + def create_page_revisions(self, page, revisions): """Publish page and create additional draft revisions.""" original_introduction = page.introduction @@ -184,232 +346,192 @@ def _publish_page_with_revisions(self, page, revisions): page.introduction = f"[Revision {rev_num + 2}] " + original_introduction page.save_revision() + if (rev_num + 1) % 1000 == 0: + self.stdout.write(f' Progress:{rev_num:,}/{revisions:,} revisions created for page title {page.title}.') + + self.stdout.write(f' ✓ Created {revisions} revisions for page {page.title}') + page.introduction = original_introduction page.refresh_from_db() + def create_translations(self): + """Create language translations for pages.""" + self.stdout.write(f' Creating {self.translations_count} language translations...') - def create_blog_pages(self, home_page, count): - """Create blog pages with relationships, tags, and streamfield content.""" - blog_index = BlogIndexPage.objects.filter(slug='blog').first() - if not blog_index: - self.stdout.write(self.style.WARNING(' Blog index not found. Skipping blog pages.')) - return 0 + # Generate language codes for locales (e.g., lang-01, lang-02, ..., lang-100) + # Using synthetic language codes since we need 100 unique ones - people = list(Person.objects.all()) - if not people and INLINE_PANEL_ITEMS > 0: - self.stdout.write(self.style.WARNING(' No Person objects found. Creating sample people.')) - now = timezone.now() - images = self._get_images_cache() - - # Fixed names and job titles for consistent benchmark data - first_names = ['John', 'Jane', 'Michael', 'Sarah', 'David', 'Emily', 'Robert', 'Jessica', 'William', 'Ashley'] - last_names = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Wilson', 'Moore'] - job_titles = ['Senior Developer', 'Product Manager', 'Design Lead', 'Content Writer', 'Marketing Specialist'] - - num_people = max(10, INLINE_PANEL_ITEMS) - people_to_create = [] - for i in range(num_people): - person = Person( - first_name=first_names[i % len(first_names)], - last_name=last_names[i % len(last_names)], - job_title=job_titles[i % len(job_titles)], - live=True, - first_published_at=now, - last_published_at=now, - image=images[i % len(images)] if images else None, - ) - people_to_create.append(person) - Person.objects.bulk_create(people_to_create) - people = list(Person.objects.all()) - - # Assign images to existing Person objects that don't have images - people_without_images = [p for p in people if not p.image] - if people_without_images: - images = self._get_images_cache() - if images: - for i, person in enumerate(people_without_images): - person.image = images[i % len(images)] - Person.objects.bulk_update(people_without_images, ['image']) - # Refresh the people list - people = list(Person.objects.all()) + default_locale = Locale.objects.filter(language_code='en').first() or Locale.objects.first() + if not default_locale: + self.stdout.write(self.style.WARNING(' No default locale found. Skipping translations.')) + return - start_number = BlogPage.objects.count() + 1 + self.stdout.write(' Creating locales...') + existing_locales = set(Locale.objects.values_list('language_code', flat=True)) + language_codes = [f"lg{i:03d}" for i in range(1, self.translations_count + 1)] - tag_names = ['baking', 'bread', 'recipe', 'cooking', 'food', 'bakery', 'yeast', 'dough', 'pastry', 'dessert'] - tags = [Tag.objects.get_or_create(name=name)[0] for name in tag_names] + locales_to_create = [Locale(language_code=lang_code) + for lang_code in language_codes if lang_code not in existing_locales] - body = self.generate_streamfield(STREAMFIELD_BLOCKS, RICH_TEXT_PARAGRAPHS) + if locales_to_create: + Locale.objects.bulk_create(locales_to_create, ignore_conflicts=True) + self.stdout.write(f' ✓ Created {len(locales_to_create)} new locales') - created_count = 0 - for i in range(count): - page_number = start_number + i - title = f"Blog Post {page_number}" - slug = slugify(title) + locales = list(Locale.objects.filter(language_code__in=language_codes)) - if BlogPage.objects.filter(slug=slug).exists(): - continue + blog_index = BlogIndexPage.objects.filter(slug='blog').first() + sample_page = BlogPage.objects.first() + + if not blog_index: + self.stdout.write(self.style.WARNING(' No blog index found. Skipping translations.')) + return + + if not sample_page: + self.stdout.write(self.style.WARNING(' No pages found to translate. Skipping translations.')) + return - with transaction.atomic(): - page = BlogPage( - title=title, - slug=slug, - subtitle=lorem_ipsum.words(random.randint(5, 12), common=False), - introduction=self._generate_paragraph(), - body=body, - image=self.get_random_image(), - date_published=date.today(), - ) - blog_index.add_child(instance=page) - page.refresh_from_db() - - if people: - selected_person = random.choice(people) - BlogPersonRelationship.objects.create( - page=page, - person=selected_person - ) - - if tags: - page.tags.add(*random.sample(tags, min(random.randint(2, 5), len(tags)))) - - self._publish_page_with_revisions(page, REVISIONS_PER_PAGE) + # Create translations for each locale + created_count = 0 + for locale in locales: + try: + # First, translate the parent page (blog index) if not already translated + if not BlogIndexPage.objects.filter(translation_key=blog_index.translation_key, locale=locale).exists(): + translated_index = blog_index.copy_for_translation(locale) + translated_index.title = f"{blog_index.title} ({locale.language_code})" + translated_index.save_revision().publish() + + if BlogPage.objects.filter(translation_key=sample_page.translation_key, locale=locale).exists(): + continue + + translated_page = sample_page.copy_for_translation(locale) + translated_page.title = f"{sample_page.title} ({locale.language_code})" + translated_page.save_revision().publish() created_count += 1 - return created_count + if created_count % 10 == 0: + self.stdout.write(f' Progress: {created_count}/{len(locales)} translations created...') + except Exception as e: + self.stdout.write(f' Error creating translation for {locale.language_code}: {str(e)}') - def create_bread_pages(self, home_page, count): - """Create bread pages with random types, origins, and ingredients.""" - breads_index = BreadsIndexPage.objects.filter(slug='breads').first() - if not breads_index: - self.stdout.write(self.style.WARNING(' Breads index not found. Skipping bread pages.')) - return 0 + self.stdout.write(f' ✓ Created {created_count} page translations across {len(locales)} locales\n') - bread_type_names = ['Sourdough', 'Baguette', 'Ciabatta', 'Rye', 'Whole Wheat', - 'Multigrain', 'Pumpernickel', 'Focaccia', 'Challah', 'Brioche', - 'Naan', 'Pita', 'Cornbread', 'Flatbread', 'Tortilla'] - country_names = ['France', 'Italy', 'Germany', 'United States', 'United Kingdom', - 'Spain', 'Greece', 'Turkey', 'India', 'Mexico', 'Canada', 'Australia'] - ingredient_names = ['Flour', 'Water', 'Yeast', 'Salt', 'Sugar', 'Olive Oil', - 'Butter', 'Eggs', 'Milk', 'Honey', 'Seeds', 'Nuts'] - bread_types = [BreadType.objects.get_or_create(title=name)[0] for name in bread_type_names] - countries = [Country.objects.get_or_create(title=name)[0] for name in country_names] - ingredients = [BreadIngredient.objects.get_or_create(name=name)[0] for name in ingredient_names] + def create_blog_pages(self): + """Create blog pages with streamfield content.""" + blog_index = BlogIndexPage.objects.filter(slug='blog').first() + if not blog_index: + self.stdout.write(self.style.WARNING(' Blog index not found. Skipping blog pages.')) + return - start_number = BreadPage.objects.count() + 1 - body = self.generate_streamfield(STREAMFIELD_BLOCKS) + body_template = self.generate_streamfield(1, 2, 1) + subtitle = lorem_ipsum.words(5, common=False) + introduction = lorem_ipsum.paragraph() + image = self.get_random_image() + today = date.today() + existing_slugs = set(BlogPage.objects.values_list('slug', flat=True)) created_count = 0 - for i in range(count): + skipped_count = 0 + + start_number = BlogPage.objects.count() + 1 + self.stdout.write(f' Creating {self.blog_pages_count:,} blog pages...') + + for i in range(self.blog_pages_count): page_number = start_number + i - title = f"{random.choice(bread_type_names)} #{page_number}" - slug = slugify(title) + slug = f"blog-post-{page_number}" - if BreadPage.objects.filter(slug=slug).exists(): + if slug in existing_slugs: + skipped_count += 1 continue - with transaction.atomic(): - page = BreadPage( - title=title, - slug=slug, - introduction=self._generate_paragraph(), - body=body, - bread_type=random.choice(bread_types), - origin=random.choice(countries) if countries else None, - image=self.get_random_image(), - ) - breads_index.add_child(instance=page) - page.refresh_from_db() - - if ingredients: - page.ingredients.set(random.sample(ingredients, min(random.randint(3, 8), len(ingredients)))) - - self._publish_page_with_revisions(page, REVISIONS_PER_PAGE) - created_count += 1 + page = BlogPage( + title=f"Blog Post {page_number}", + slug=slug, + subtitle=subtitle, + introduction=introduction, + body=body_template, + image=image, + date_published=today, + ) - return created_count - - def _generate_location_address(self, city): - """Generate a random address for the given city.""" - street_number = random.randint(1, 999) - street_name = random.choice(['Main Street', 'Oak Avenue', 'Park Road', 'High Street', 'Church Lane']) - country = random.choice(['Iceland', 'United States', 'United Kingdom', 'France', 'Germany']) - return f"{street_number} {street_name},\r\n{city},\r\n{country}" - - def _generate_lat_long(self): - """Generate random latitude and longitude coordinates.""" - lat = random.uniform(-90, 90) - lng = random.uniform(-180, 180) - return f"{lat:.6f}, {lng:.6f}" - - def _create_operating_hours(self, page): - """Create operating hours for all days of the week""" - # Define hours for weekdays and weekends - weekday_hours = {'opening': time(9, 0), 'closing': time(17, 0)} - weekend_hours = {'opening': time(10, 0), 'closing': time(16, 0)} - - # Map days to their respective hours - days_config = { - 'MON': weekday_hours, - 'TUE': weekday_hours, - 'WED': weekday_hours, - 'THU': weekday_hours, - 'FRI': weekday_hours, - 'SAT': weekend_hours, - 'SUN': weekend_hours, - } - - # Create operating hours using a loop - operating_hours = [ - LocationOperatingHours( - location=page, - day=day, - opening_time=hours['opening'], - closing_time=hours['closing'], - closed=False + blog_index.add_child(instance=page) + created_count += 1 + + if created_count % 1000 == 0: + self.stdout.write(f' Progress: {created_count:,}/{self.blog_pages_count:,} blog pages created...') + + self.stdout.write(f' ✓ Created {created_count:,} pages (skipped {skipped_count:,} existing)') + + # Create page tree depth + parent = blog_index + for i in range(self.page_tree_depth): + slug = f"blog-post-depth-{i}" + if slug in existing_slugs: + continue + + page = BlogPage( + title=f"Blog Post in tree depth {i}", + slug=slug, + subtitle=subtitle, + introduction=introduction, + body=body_template, + image=image, + date_published=today, ) - for day, hours in days_config.items() - ] - LocationOperatingHours.objects.bulk_create(operating_hours) - def create_location_pages(self, home_page, count): - """Create location pages with addresses, coordinates, and operating hours.""" - locations_index = LocationsIndexPage.objects.filter(slug='locations').first() - if not locations_index: - self.stdout.write(self.style.WARNING(' Locations index not found. Skipping location pages.')) - return 0 + parent.add_child(instance=page) + parent = page - cities = ['New York', 'London', 'Paris', 'Tokyo', 'Sydney', 'Berlin', - 'Toronto', 'Mumbai', 'Singapore', 'Dubai', 'Barcelona', 'Amsterdam', - 'Rome', 'Madrid', 'Seoul', 'San Francisco', 'Chicago', 'Boston'] + self.stdout.write(f' ✓ Created Page tree with depth {self.page_tree_depth}\n') - start_number = LocationPage.objects.count() + 1 - body = self.generate_streamfield(STREAMFIELD_BLOCKS) + def create_inline_panel_items(self): + """Create 100 InlinePanel items for ONE page to demonstrate the requirement.""" + self.stdout.write(f' Creating {self.inline_panel_items_count} InlinePanel items...') - created_count = 0 - for i in range(count): - city = random.choice(cities) - title = f"{city} Location #{start_number + i}" - slug = slugify(title) + # Get or create the first blog page + sample_page = BlogPage.objects.first() + if not sample_page: + self.stdout.write(self.style.WARNING(' No blog pages found. Skipping InlinePanel items.')) + return - if LocationPage.objects.filter(slug=slug).exists(): - continue + # Check how many relationships already exist for this page + existing_count = BlogPersonRelationship.objects.filter(page=sample_page).count() + if existing_count >= self.inline_panel_items_count: + self.stdout.write(f' ✓ Page already has {existing_count} InlinePanel items') + return - with transaction.atomic(): - page = LocationPage( - title=title, - slug=slug, - introduction=self._generate_paragraph(), - body=body, - address=self._generate_location_address(city), - lat_long=self._generate_lat_long(), - image=self.get_random_image(), - ) - locations_index.add_child(instance=page) - page.refresh_from_db() - - self._create_operating_hours(page) - self._publish_page_with_revisions(page, REVISIONS_PER_PAGE) - created_count += 1 + # Ensure we have enough Person objects + existing_people = Person.objects.count() + if existing_people < self.inline_panel_items_count: + people_to_create = [] + now = timezone.now() + for i in range(existing_people, self.inline_panel_items_count): + people_to_create.append(Person( + first_name=f"Person {i + 1}", + last_name="Benchmark", + job_title="Benchmark User", + live=True, + first_published_at=now, + last_published_at=now, + )) + Person.objects.bulk_create(people_to_create, ignore_conflicts=True) + self.stdout.write(f' ✓ Created {len(people_to_create)} Person objects') + + people = list(Person.objects.all()[:self.inline_panel_items_count]) + + # Get existing person IDs for this page to avoid duplicates + existing_person_ids = set( + BlogPersonRelationship.objects.filter(page=sample_page).values_list('person_id', flat=True) + ) + + # Create relationships for the sample page + relationships = [ + BlogPersonRelationship(page=sample_page, person=person) + for person in people if person.id not in existing_person_ids + ] + + if relationships: + BlogPersonRelationship.objects.bulk_create(relationships, ignore_conflicts=True) - return created_count + total_count = BlogPersonRelationship.objects.filter(page=sample_page).count() + self.stdout.write(f' ✓ Page "{sample_page.title}" now has {total_count} InlinePanel items\n')