Source code for django_program.conference.management.commands.bootstrap_conference

"""Management command to bootstrap a conference from a TOML configuration file."""

import secrets
import string
from datetime import datetime
from decimal import Decimal
from typing import Any
from zoneinfo import ZoneInfo

from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand, CommandError, CommandParser
from django.db import transaction

from django_program.conference.models import Conference, Section
from django_program.config_loader import load_conference_config
from django_program.programs.models import Activity, TravelGrant
from django_program.registration.models import (
    AddOn,
    Cart,
    CartItem,
    Credit,
    Order,
    OrderLineItem,
    Payment,
    TicketType,
    Voucher,
)

# Keys that exist in the TOML spec but are handled by other apps in later phases.
_DEFERRED_KEYS: dict[str, str] = {
    "sponsor_levels": "sponsors",
}

# Mapping from TOML short field names to Django model field names.
_CONFERENCE_FIELD_MAP: dict[str, str] = {
    "name": "name",
    "slug": "slug",
    "start": "start_date",
    "end": "end_date",
    "timezone": "timezone",
    "venue": "venue",
    "website_url": "website_url",
    "pretalx_event_slug": "pretalx_event_slug",
    "total_capacity": "total_capacity",
}

_SECTION_FIELD_MAP: dict[str, str] = {
    "name": "name",
    "slug": "slug",
    "start": "start_date",
    "end": "end_date",
    "order": "order",
}

_TICKET_FIELD_MAP: dict[str, str] = {
    "name": "name",
    "slug": "slug",
    "price": "price",
    "quantity": "total_quantity",
    "per_user": "limit_per_user",
    "voucher_required": "requires_voucher",
}

_ADDON_FIELD_MAP: dict[str, str] = {
    "name": "name",
    "slug": "slug",
    "price": "price",
    "quantity": "total_quantity",
}


def _map_fields(data: dict[str, Any], field_map: dict[str, str]) -> dict[str, Any]:
    """Map TOML config keys to Django model field names.

    Args:
        data: Raw config data with short field names.
        field_map: Mapping of config key -> model field name.

    Returns:
        Dict with model field names as keys.
    """
    result: dict[str, Any] = {}
    for config_key, model_field in field_map.items():
        if config_key in data:
            result[model_field] = data[config_key]
    return result


def _parse_availability(data: dict[str, Any], tz_name: str) -> dict[str, datetime | None]:
    """Extract available_from/available_until from a TOML ``available`` sub-table.

    Args:
        data: A single ticket/addon mapping from the TOML config.
        tz_name: The conference timezone name for making datetimes aware.

    Returns:
        Dict with ``available_from`` and ``available_until`` keys (or empty).
    """
    avail = data.get("available")
    if not avail or not isinstance(avail, dict):
        return {}
    tz = ZoneInfo(tz_name)
    result: dict[str, datetime | None] = {}
    if "opens" in avail:
        result["available_from"] = datetime.combine(avail["opens"], datetime.min.time(), tzinfo=tz)
    if "closes" in avail:
        result["available_until"] = datetime.combine(
            avail["closes"], datetime.max.time().replace(microsecond=0), tzinfo=tz
        )
    return result


[docs] class Command(BaseCommand): """Bootstrap a conference from a TOML configuration file. Parses the given TOML file, validates its structure, and creates (or updates) the corresponding ``Conference``, ``Section``, ``TicketType``, and ``AddOn`` database records. Usage:: manage.py bootstrap_conference --config conference.toml manage.py bootstrap_conference --config conference.toml --update manage.py bootstrap_conference --config conference.toml --dry-run """ help = "Create or update a conference and its sections from a TOML config file."
[docs] def add_arguments(self, parser: CommandParser) -> None: """Define the command-line arguments accepted by this command. Args: parser: The argument parser to configure. """ parser.add_argument( "--config", required=True, help="Path to the conference TOML configuration file.", ) parser.add_argument( "--update", action="store_true", default=False, help="Update an existing conference instead of failing on duplicate slug.", ) parser.add_argument( "--dry-run", action="store_true", default=False, help="Validate the config and print what would be created without saving.", ) parser.add_argument( "--seed-demo", action="store_true", default=False, help="Generate sample vouchers, demo users, carts, orders, payments, and credits.", )
[docs] def handle(self, *args: Any, **options: Any) -> None: """Execute the bootstrap command. Args: *args: Positional arguments (unused). **options: Parsed command-line options. """ config_path: str = options["config"] update: bool = options["update"] dry_run: bool = options["dry_run"] seed_demo: bool = options["seed_demo"] verbosity: int = options["verbosity"] try: conf = load_conference_config(config_path) except (FileNotFoundError, TypeError, ValueError) as exc: raise CommandError(str(exc)) from exc self._warn_deferred_keys(conf) sections_data: list[dict[str, Any]] = conf["sections"] tickets_data: list[dict[str, Any]] = conf.get("tickets", []) addons_data: list[dict[str, Any]] = conf.get("addons", []) if dry_run: self._print_dry_run(conf, sections_data, tickets_data, addons_data) return with transaction.atomic(): conference = self._bootstrap_conference(conf, update=update) created_sections, updated_sections = self._bootstrap_sections(conference, sections_data, update=update) created_tickets, updated_tickets = self._bootstrap_tickets( conference, tickets_data, conf.get("timezone", "UTC"), update=update, ) created_addons, updated_addons = self._bootstrap_addons( conference, addons_data, conf.get("timezone", "UTC"), update=update, ) results = { "sections": (created_sections, updated_sections), "tickets": (created_tickets, updated_tickets), "addons": (created_addons, updated_addons), } self._print_summary(conference, results, verbosity) if seed_demo: self._seed_demo_data(conference)
def _warn_deferred_keys(self, conf: dict[str, Any]) -> None: """Print info messages for config keys that are not handled yet. Args: conf: The parsed conference configuration. """ for key, app_name in _DEFERRED_KEYS.items(): if key in conf: self.stdout.write(self.style.NOTICE(f" Skipping '{key}' -- will be handled by the {app_name} app.")) def _bootstrap_conference(self, conf: dict[str, Any], *, update: bool) -> Conference: """Create or update a Conference record from the parsed config. Args: conf: The ``conference`` table from the TOML file. update: When ``True``, update the existing conference matched by slug instead of raising an error on duplicates. Returns: The created or updated ``Conference`` instance. Raises: CommandError: If a conference with the same slug already exists and ``update`` is ``False``. """ slug = conf["slug"] fields = _map_fields(conf, _CONFERENCE_FIELD_MAP) fields.pop("slug", None) existing = Conference.objects.filter(slug=slug).first() if existing and not update: raise CommandError(f"Conference with slug '{slug}' already exists. Use --update to update it.") if existing and update: for attr, value in fields.items(): setattr(existing, attr, value) existing.save() self.stdout.write(self.style.SUCCESS(f" Updated conference: {existing.name}")) return existing conference = Conference.objects.create(slug=slug, **fields) self.stdout.write(self.style.SUCCESS(f" Created conference: {conference.name}")) return conference def _bootstrap_sections( self, conference: Conference, sections_data: list[dict[str, Any]], *, update: bool, ) -> tuple[list[Section], list[Section]]: """Create or update Section records for a conference. Args: conference: The parent ``Conference`` instance. sections_data: List of section mappings from the config file. update: When ``True``, update existing sections matched by conference + slug instead of creating duplicates. Returns: A tuple of (created_sections, updated_sections). """ created: list[Section] = [] updated: list[Section] = [] for position, section_data in enumerate(sections_data): slug = section_data["slug"] fields = _map_fields(section_data, _SECTION_FIELD_MAP) fields.pop("slug", None) if "order" not in fields: fields["order"] = position existing = Section.objects.filter(conference=conference, slug=slug).first() if existing and update: for attr, value in fields.items(): setattr(existing, attr, value) existing.save() self.stdout.write(self.style.SUCCESS(f" Updated section: {existing.name}")) updated.append(existing) elif existing: self.stdout.write( self.style.WARNING(f" Section '{slug}' already exists for this conference, skipping.") ) else: section = Section.objects.create(conference=conference, slug=slug, **fields) self.stdout.write(self.style.SUCCESS(f" Created section: {section.name}")) created.append(section) return created, updated def _bootstrap_tickets( self, conference: Conference, tickets_data: list[dict[str, Any]], tz_name: str, *, update: bool, ) -> tuple[list[TicketType], list[TicketType]]: """Create or update TicketType records for a conference. Args: conference: The parent ``Conference`` instance. tickets_data: List of ticket mappings from the config file. tz_name: Conference timezone for date-to-datetime conversion. update: When ``True``, update existing tickets matched by conference + slug instead of creating duplicates. Returns: A tuple of (created_tickets, updated_tickets). """ created: list[TicketType] = [] updated_list: list[TicketType] = [] for position, ticket_data in enumerate(tickets_data): slug = ticket_data["slug"] fields = _map_fields(ticket_data, _TICKET_FIELD_MAP) fields.pop("slug", None) fields["order"] = position fields.update(_parse_availability(ticket_data, tz_name)) existing = TicketType.objects.filter(conference=conference, slug=slug).first() if existing and update: for attr, value in fields.items(): setattr(existing, attr, value) existing.save() self.stdout.write(self.style.SUCCESS(f" Updated ticket: {existing.name}")) updated_list.append(existing) elif existing: # pragma: no cover — unreachable; conference-level check errors first self.stdout.write( self.style.WARNING(f" Ticket '{slug}' already exists for this conference, skipping.") ) else: ticket = TicketType.objects.create(conference=conference, slug=slug, **fields) self.stdout.write(self.style.SUCCESS(f" Created ticket: {ticket.name}")) created.append(ticket) return created, updated_list def _bootstrap_addons( self, conference: Conference, addons_data: list[dict[str, Any]], tz_name: str, *, update: bool, ) -> tuple[list[AddOn], list[AddOn]]: """Create or update AddOn records for a conference. Args: conference: The parent ``Conference`` instance. addons_data: List of add-on mappings from the config file. tz_name: Conference timezone for date-to-datetime conversion. update: When ``True``, update existing add-ons matched by conference + slug instead of creating duplicates. Returns: A tuple of (created_addons, updated_addons). """ created: list[AddOn] = [] updated_list: list[AddOn] = [] for position, addon_data in enumerate(addons_data): slug = addon_data["slug"] fields = _map_fields(addon_data, _ADDON_FIELD_MAP) fields.pop("slug", None) fields["order"] = position fields.update(_parse_availability(addon_data, tz_name)) existing = AddOn.objects.filter(conference=conference, slug=slug).first() if existing and update: for attr, value in fields.items(): setattr(existing, attr, value) existing.save() self.stdout.write(self.style.SUCCESS(f" Updated add-on: {existing.name}")) updated_list.append(existing) elif existing: # pragma: no cover — unreachable; conference-level check errors first self.stdout.write( self.style.WARNING(f" Add-on '{slug}' already exists for this conference, skipping.") ) else: addon = AddOn.objects.create(conference=conference, slug=slug, **fields) self.stdout.write(self.style.SUCCESS(f" Created add-on: {addon.name}")) created.append(addon) # Wire up the requires_ticket_types M2M from the "requires" list of slugs requires_slugs = addon_data.get("requires") if requires_slugs is not None: target = existing if existing and update else (addon if not existing else None) if target: found_ticket_types = TicketType.objects.filter(conference=conference, slug__in=requires_slugs) found_slugs = set(found_ticket_types.values_list("slug", flat=True)) missing_slugs = sorted(set(requires_slugs) - found_slugs) if missing_slugs: missing = ", ".join(missing_slugs) msg = f"Add-on '{slug}' references unknown required ticket slug(s): {missing}" raise CommandError(msg) target.requires_ticket_types.set(found_ticket_types) return created, updated_list def _print_dry_run( self, conf: dict[str, Any], sections_data: list[dict[str, Any]], tickets_data: list[dict[str, Any]], addons_data: list[dict[str, Any]], ) -> None: """Print a preview of what would be created without touching the database. Args: conf: The ``conference`` table from the TOML file. sections_data: List of section mappings from the config file. tickets_data: List of ticket mappings from the config file. addons_data: List of add-on mappings from the config file. """ self.stdout.write(self.style.MIGRATE_HEADING("\n[DRY RUN] No database changes will be made.\n")) self.stdout.write(self.style.MIGRATE_HEADING("Conference:")) self.stdout.write(f" Name: {conf['name']}") self.stdout.write(f" Slug: {conf['slug']}") self.stdout.write(f" Dates: {conf['start']} -- {conf['end']}") self.stdout.write(f" Timezone: {conf['timezone']}") if conf.get("venue"): self.stdout.write(f" Venue: {conf['venue']}") if conf.get("website_url"): self.stdout.write(f" Website: {conf['website_url']}") self.stdout.write(self.style.MIGRATE_HEADING(f"\nSections ({len(sections_data)}):")) for idx, section in enumerate(sections_data): self.stdout.write(f" [{idx}] {section['name']} ({section['slug']}) {section['start']} -- {section['end']}") if tickets_data: self.stdout.write(self.style.MIGRATE_HEADING(f"\nTickets ({len(tickets_data)}):")) for idx, ticket in enumerate(tickets_data): self.stdout.write(f" [{idx}] {ticket['name']} ({ticket['slug']}) ${ticket['price']}") if addons_data: self.stdout.write(self.style.MIGRATE_HEADING(f"\nAdd-ons ({len(addons_data)}):")) for idx, addon in enumerate(addons_data): self.stdout.write(f" [{idx}] {addon['name']} ({addon['slug']}) ${addon['price']}") self.stdout.write("") def _print_summary( self, conference: Conference, results: dict[str, tuple[list[Any], list[Any]]], verbosity: int, ) -> None: """Print a summary of all bootstrap operations performed. Args: conference: The bootstrapped ``Conference`` instance. results: Mapping of category name to (created, updated) lists. verbosity: The verbosity level from the command options. """ self.stdout.write("") self.stdout.write(self.style.MIGRATE_HEADING("Bootstrap summary:")) self.stdout.write(f" Conference: {conference.name} ({conference.slug})") for label, (created, updated) in results.items(): self.stdout.write(f" {label.capitalize()} created: {len(created)}") self.stdout.write(f" {label.capitalize()} updated: {len(updated)}") if verbosity >= 2: for created, updated in results.values(): for item in created: self.stdout.write(f" + {item.name} ({item.slug})") for item in updated: self.stdout.write(f" ~ {item.name} ({item.slug})") self.stdout.write(self.style.SUCCESS("\nDone.")) # ------------------------------------------------------------------ # --seed-demo: generate vouchers, demo users, and transactional data # ------------------------------------------------------------------ @staticmethod def _generate_voucher_code(prefix: str = "", length: int = 8) -> str: """Generate a random voucher code. Args: prefix: Optional prefix (e.g. ``"SPKR-"``). length: Number of random alphanumeric characters. Returns: A code like ``"SPKR-A3K9M2X1"``. """ chars = string.ascii_uppercase + string.digits random_part = "".join(secrets.choice(chars) for _ in range(length)) return f"{prefix}{random_part}" if prefix else random_part def _seed_demo_data(self, conference: Conference) -> None: """Create sample vouchers, demo users, and transactional records. Args: conference: The conference to seed data for. """ self.stdout.write(self.style.MIGRATE_HEADING("\nSeeding demo data...")) ticket_types = {tt.slug: tt for tt in TicketType.objects.filter(conference=conference)} addons = {a.slug: a for a in AddOn.objects.filter(conference=conference)} individual = ticket_types.get("individual") corporate = ticket_types.get("corporate") tshirt = addons.get("pycon-t-shirt") # -- Vouchers -- vouchers = self._seed_vouchers(conference, ticket_types) # -- Demo users -- demo_users = self._seed_demo_users() # -- Orders (paid) -- if individual and demo_users: self._seed_orders(conference, demo_users, individual, corporate, tshirt) # -- Active cart -- if individual and demo_users: self._seed_carts(conference, demo_users, individual, tshirt) # -- Credits -- if demo_users: self._seed_credits(conference, demo_users) # -- Activities & Travel Grants -- self._seed_activities(conference) if demo_users: self._seed_travel_grants(conference, demo_users) self.stdout.write(self.style.SUCCESS("\nDemo data seeded.")) # Print voucher codes so the dev can use them if vouchers: self.stdout.write(self.style.MIGRATE_HEADING("\nGenerated voucher codes:")) for v in vouchers: self.stdout.write(f" {v.code:<20} {v.get_voucher_type_display():<30} (max uses: {v.max_uses})") def _seed_vouchers( self, conference: Conference, ticket_types: dict[str, TicketType], ) -> list[Voucher]: """Create sample vouchers with random codes. Args: conference: The conference to create vouchers for. ticket_types: Mapping of slug to TicketType for M2M wiring. Returns: List of created Voucher instances. """ if Voucher.objects.filter(conference=conference).exists(): self.stdout.write(self.style.WARNING(" Vouchers already exist, skipping.")) return [] speaker_tt = ticket_types.get("speaker") student_tt = ticket_types.get("student") voucher_specs: list[dict[str, Any]] = [ { "code": self._generate_voucher_code("SPKR-"), "voucher_type": Voucher.VoucherType.COMP, "discount_value": Decimal(0), "max_uses": 200, "unlocks_hidden_tickets": True, "applicable_tickets": [speaker_tt] if speaker_tt else [], }, { "code": self._generate_voucher_code("STU-"), "voucher_type": Voucher.VoucherType.COMP, "discount_value": Decimal(0), "max_uses": 500, "unlocks_hidden_tickets": True, "applicable_tickets": [student_tt] if student_tt else [], }, { "code": self._generate_voucher_code("EARLY-"), "voucher_type": Voucher.VoucherType.PERCENTAGE, "discount_value": Decimal(20), "max_uses": 100, "unlocks_hidden_tickets": False, "applicable_tickets": [], }, { "code": self._generate_voucher_code("SAVE25-"), "voucher_type": Voucher.VoucherType.FIXED_AMOUNT, "discount_value": Decimal(25), "max_uses": 50, "unlocks_hidden_tickets": False, "applicable_tickets": [], }, ] created: list[Voucher] = [] for spec in voucher_specs: applicable = spec.pop("applicable_tickets") voucher = Voucher.objects.create(conference=conference, **spec) if applicable: voucher.applicable_ticket_types.set(applicable) self.stdout.write(self.style.SUCCESS(f" Created voucher: {voucher.code}")) created.append(voucher) return created def _seed_demo_users(self) -> list[Any]: """Create demo users for transactional data. Returns: List of created/existing User instances. """ User = get_user_model() demo_specs = [ {"username": "attendee_alice", "email": "alice@example.com", "first_name": "Alice", "last_name": "Smith"}, {"username": "attendee_bob", "email": "bob@example.com", "first_name": "Bob", "last_name": "Jones"}, {"username": "speaker_carol", "email": "carol@example.com", "first_name": "Carol", "last_name": "Chen"}, {"username": "attendee_dave", "email": "dave@example.com", "first_name": "Dave", "last_name": "Park"}, {"username": "attendee_eve", "email": "eve@example.com", "first_name": "Eve", "last_name": "Garcia"}, ] users = [] for spec in demo_specs: user, created = User.objects.get_or_create(username=spec["username"], defaults=spec) if created: user.set_password("demo") user.is_staff = True user.save() self.stdout.write(self.style.SUCCESS(f" Created user: {user.username}")) users.append(user) return users def _seed_orders( self, conference: Conference, users: list[Any], individual: TicketType, corporate: TicketType | None, tshirt: AddOn | None, ) -> None: """Create sample orders with line items and payments. Args: conference: The conference for the orders. users: Demo users to create orders for. individual: The individual ticket type. corporate: The corporate ticket type (optional). tshirt: The t-shirt add-on (optional). """ if Order.objects.filter(conference=conference).exists(): self.stdout.write(self.style.WARNING(" Orders already exist, skipping.")) return alice, bob, carol = users[0], users[1], users[2] # Alice: paid individual + t-shirt alice_total = individual.price + (tshirt.price if tshirt else Decimal(0)) order1 = Order.objects.create( conference=conference, user=alice, status=Order.Status.PAID, subtotal=alice_total, total=alice_total, billing_name="Alice Smith", billing_email="alice@example.com", reference=f"ORD-{self._generate_voucher_code(length=6)}", ) OrderLineItem.objects.create( order=order1, description=f"Ticket: {individual.name}", quantity=1, unit_price=individual.price, line_total=individual.price, ticket_type=individual, ) if tshirt: OrderLineItem.objects.create( order=order1, description=f"Add-on: {tshirt.name}", quantity=1, unit_price=tshirt.price, line_total=tshirt.price, addon=tshirt, ) Payment.objects.create( order=order1, method=Payment.Method.STRIPE, amount=alice_total, stripe_payment_intent_id=f"pi_demo_{secrets.token_hex(8)}", reference=f"ch_demo_{secrets.token_hex(8)}", ) self.stdout.write(self.style.SUCCESS(f" Created order: {order1.reference} (Alice, paid)")) # Bob: paid corporate ticket if corporate: order2 = Order.objects.create( conference=conference, user=bob, status=Order.Status.PAID, subtotal=corporate.price, total=corporate.price, billing_name="Bob Jones", billing_email="bob@example.com", billing_company="Acme Corp", reference=f"ORD-{self._generate_voucher_code(length=6)}", ) OrderLineItem.objects.create( order=order2, description=f"Ticket: {corporate.name}", quantity=1, unit_price=corporate.price, line_total=corporate.price, ticket_type=corporate, ) Payment.objects.create( order=order2, method=Payment.Method.STRIPE, amount=corporate.price, stripe_payment_intent_id=f"pi_demo_{secrets.token_hex(8)}", reference=f"ch_demo_{secrets.token_hex(8)}", ) self.stdout.write(self.style.SUCCESS(f" Created order: {order2.reference} (Bob, paid)")) # Carol: comp speaker ticket (pending) order3 = Order.objects.create( conference=conference, user=carol, status=Order.Status.PAID, subtotal=Decimal(0), total=Decimal(0), billing_name="Carol Chen", billing_email="carol@example.com", reference=f"ORD-{self._generate_voucher_code(length=6)}", ) speaker = TicketType.objects.filter(conference=conference, slug="speaker").first() if speaker: OrderLineItem.objects.create( order=order3, description=f"Ticket: {speaker.name}", quantity=1, unit_price=Decimal(0), line_total=Decimal(0), ticket_type=speaker, ) Payment.objects.create( order=order3, method=Payment.Method.COMP, amount=Decimal(0), reference="Speaker comp", ) self.stdout.write(self.style.SUCCESS(f" Created order: {order3.reference} (Carol, speaker comp)")) def _seed_carts( self, conference: Conference, users: list[Any], individual: TicketType, tshirt: AddOn | None, ) -> None: """Create a sample open cart. Args: conference: The conference for the cart. users: Demo users. individual: A ticket type to add to the cart. tshirt: An add-on to add to the cart (optional). """ if Cart.objects.filter(conference=conference).exists(): self.stdout.write(self.style.WARNING(" Carts already exist, skipping.")) return bob = users[1] cart = Cart.objects.create(user=bob, conference=conference, status=Cart.Status.OPEN) CartItem.objects.create(cart=cart, ticket_type=individual, quantity=1) if tshirt: CartItem.objects.create(cart=cart, addon=tshirt, quantity=2) self.stdout.write(self.style.SUCCESS(f" Created open cart for {bob.username}")) def _seed_credits(self, conference: Conference, users: list[Any]) -> None: """Create a sample store credit. Args: conference: The conference for the credit. users: Demo users. """ if Credit.objects.filter(conference=conference).exists(): self.stdout.write(self.style.WARNING(" Credits already exist, skipping.")) return alice = users[0] Credit.objects.create( user=alice, conference=conference, amount=Decimal("25.00"), status=Credit.Status.AVAILABLE, note="Refund from cancelled tutorial add-on", ) self.stdout.write(self.style.SUCCESS(f" Created $25 credit for {alice.username}")) def _seed_activities(self, conference: Conference) -> None: """Create sample activities for the conference. Args: conference: The conference to create activities for. """ if Activity.objects.filter(conference=conference).exists(): self.stdout.write(self.style.WARNING(" Activities already exist, skipping.")) return specs = [ { "name": "Django Sprint", "slug": "django-sprint", "activity_type": Activity.ActivityType.SPRINT, "description": "Contribute to Django core and ecosystem packages.", "max_participants": 50, "is_active": True, }, { "name": "Newcomer Orientation", "slug": "newcomer-orientation", "activity_type": Activity.ActivityType.SOCIAL, "description": "Welcome session for first-time attendees.", "is_active": True, }, { "name": "Open Source Workshop", "slug": "open-source-workshop", "activity_type": Activity.ActivityType.WORKSHOP, "description": "Hands-on workshop for your first open source contribution.", "max_participants": 30, "is_active": True, }, { "name": "Lightning Talks", "slug": "lightning-talks", "activity_type": Activity.ActivityType.LIGHTNING_TALK, "description": "5-minute lightning talks on any topic.", "is_active": True, }, ] for spec in specs: Activity.objects.create(conference=conference, **spec) self.stdout.write(self.style.SUCCESS(f" Created activity: {spec['name']}")) def _seed_travel_grants(self, conference: Conference, users: list[Any]) -> None: """Create travel grants with various statuses for demo purposes. Args: conference: The conference to create grants for. users: Demo users (alice, bob, carol). """ if TravelGrant.objects.filter(conference=conference).exists(): self.stdout.write(self.style.WARNING(" Travel grants already exist, skipping.")) return User = get_user_model() admin = User.objects.filter(is_superuser=True).first() alice, bob, carol = users[0], users[1], users[2] # Alice: submitted application awaiting review TravelGrant.objects.create( conference=conference, user=alice, status=TravelGrant.GrantStatus.SUBMITTED, request_type=TravelGrant.RequestType.TICKET_AND_GRANT, application_type=TravelGrant.ApplicationType.GENERAL, travel_from="Chicago, IL", international=False, first_time=True, travel_plans_airfare_description="Round-trip flight ORD to PIT", travel_plans_airfare_amount=Decimal("350.00"), travel_plans_lodging_description="4 nights at conference hotel", travel_plans_lodging_amount=Decimal("600.00"), requested_amount=Decimal("950.00"), experience_level=TravelGrant.ExperienceLevel.INTERMEDIATE, occupation="Software engineer", involvement="Django contributor, local meetup organizer", reason="Cannot afford travel costs on current salary.", ) self.stdout.write(self.style.SUCCESS(" Created travel grant: Alice (submitted)")) # Bob: accepted grant with approved amount TravelGrant.objects.create( conference=conference, user=bob, status=TravelGrant.GrantStatus.ACCEPTED, request_type=TravelGrant.RequestType.TICKET_AND_GRANT, application_type=TravelGrant.ApplicationType.SPEAKER, travel_from="New York, NY", international=False, first_time=False, travel_plans_airfare_description="Round-trip flight JFK to PIT", travel_plans_airfare_amount=Decimal("250.00"), travel_plans_lodging_description="3 nights at conference hotel", travel_plans_lodging_amount=Decimal("450.00"), requested_amount=Decimal("700.00"), approved_amount=Decimal("500.00"), experience_level=TravelGrant.ExperienceLevel.EXPERT, occupation="Senior developer at startup", involvement="Core contributor to Django REST Framework", reason="Speaking at conference, need help with travel costs.", reviewed_by=admin, reviewer_notes="Approved for reduced amount — lodging only.", ) self.stdout.write(self.style.SUCCESS(" Created travel grant: Bob (accepted, $500)")) # Carol: offered grant awaiting acceptance TravelGrant.objects.create( conference=conference, user=carol, status=TravelGrant.GrantStatus.OFFERED, request_type=TravelGrant.RequestType.TICKET_AND_GRANT, application_type=TravelGrant.ApplicationType.COMMUNITY, travel_from="São Paulo, Brazil", international=True, first_time=True, travel_plans_airfare_description="Round-trip flight GRU to PIT", travel_plans_airfare_amount=Decimal("800.00"), travel_plans_lodging_description="5 nights at conference hotel", travel_plans_lodging_amount=Decimal("750.00"), travel_plans_visa_description="US visa application", travel_plans_visa_amount=Decimal("160.00"), requested_amount=Decimal("1710.00"), approved_amount=Decimal("1200.00"), experience_level=TravelGrant.ExperienceLevel.INTERMEDIATE, occupation="University student", involvement="PyLadies São Paulo chapter lead", reason="International student, cannot afford travel without assistance.", reviewed_by=admin, reviewer_notes="Strong community leader. Approved for partial amount.", ) self.stdout.write(self.style.SUCCESS(" Created travel grant: Carol (offered, $1200)")) # Dave: rejected application dave = users[3] if len(users) > 3 else None if dave: TravelGrant.objects.create( conference=conference, user=dave, status=TravelGrant.GrantStatus.REJECTED, request_type=TravelGrant.RequestType.TICKET_AND_GRANT, application_type=TravelGrant.ApplicationType.GENERAL, travel_from="San Francisco, CA", international=False, first_time=False, travel_plans_airfare_description="Round-trip flight SFO to PIT", travel_plans_airfare_amount=Decimal("400.00"), requested_amount=Decimal("400.00"), experience_level=TravelGrant.ExperienceLevel.EXPERT, occupation="Staff engineer at large company", involvement="Occasional conference attendee", reason="Would like help covering airfare.", reviewed_by=admin, reviewer_notes="Applicant has employer sponsorship available.", ) self.stdout.write(self.style.SUCCESS(" Created travel grant: Dave (rejected)")) # Eve: withdrawn application eve = users[4] if len(users) > 4 else None if eve: TravelGrant.objects.create( conference=conference, user=eve, status=TravelGrant.GrantStatus.WITHDRAWN, request_type=TravelGrant.RequestType.TICKET_AND_GRANT, application_type=TravelGrant.ApplicationType.PYLADIES, travel_from="Austin, TX", international=False, first_time=True, travel_plans_airfare_description="Round-trip flight AUS to PIT", travel_plans_airfare_amount=Decimal("300.00"), travel_plans_lodging_description="3 nights at conference hotel", travel_plans_lodging_amount=Decimal("450.00"), requested_amount=Decimal("750.00"), experience_level=TravelGrant.ExperienceLevel.BEGINNER, occupation="Junior developer", involvement="PyLadies Austin chapter member", reason="First conference, need financial assistance.", ) self.stdout.write(self.style.SUCCESS(" Created travel grant: Eve (withdrawn)"))