Files
weirsoe-party-protocol/fupogfakta/bootstrap.py
Asger Geel Weirsøe a81bc1250c
Some checks failed
CI / test-and-quality (push) Failing after 4m4s
Big visual overhaul docker compsoe file etc
2026-03-23 14:11:30 +01:00

181 lines
6.5 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractBaseUser
from .models import Category, Question, QuestionLie
DEFAULT_MVP_HOST_USERNAME = "demo-host"
DEFAULT_MVP_HOST_PASSWORD = "demo-pass"
DEFAULT_MVP_CATEGORY_SLUG = "general"
DEFAULT_MVP_CATEGORY_NAME = "General"
DEFAULT_MVP_QUESTIONS: tuple[tuple[str, str], ...] = (
("What is the capital of Denmark?", "Copenhagen"),
("Which planet is known as the Red Planet?", "Mars"),
("How many players are required before the host can start a round?", "3"),
)
DEFAULT_MVP_FALLBACK_LIES_BY_PROMPT: dict[str, tuple[str, ...]] = {
"What is the capital of Denmark?": ("Aarhus", "Odense", "Aalborg", "Roskilde", "Esbjerg"),
"Which planet is known as the Red Planet?": ("Venus", "Jupiter", "Saturn", "Mercury", "Neptune"),
"How many players are required before the host can start a round?": ("2", "4", "5", "6", "8"),
}
DEFAULT_MVP_SCENE_ORNAMENT_BY_PROMPT: dict[str, str] = {
"What is the capital of Denmark?": Question.SceneOrnament.HARBOR_FLARE,
"Which planet is known as the Red Planet?": Question.SceneOrnament.AURORA_ARC,
"How many players are required before the host can start a round?": Question.SceneOrnament.SIGNAL_BLOOM,
}
@dataclass(frozen=True)
class SeedSummary:
created: int
updated: int
@dataclass(frozen=True)
class MvpBootstrapResult:
host: AbstractBaseUser
category: Category
questions: tuple[Question, ...]
host_changes: SeedSummary
category_changes: SeedSummary
question_changes: SeedSummary
def ensure_host_user(*, username: str, password: str, is_staff: bool = True) -> tuple[AbstractBaseUser, SeedSummary]:
user_model = get_user_model()
host, created = user_model.objects.get_or_create(username=username)
updates: list[str] = []
if not host.is_active:
host.is_active = True
updates.append("is_active")
if host.is_staff != is_staff:
host.is_staff = is_staff
updates.append("is_staff")
host.set_password(password)
updates.append("password")
host.save(update_fields=updates)
return host, SeedSummary(created=int(created), updated=int(bool(updates and not created)))
def ensure_category_with_questions(
*,
slug: str,
name: str,
prompts_and_answers: tuple[tuple[str, str], ...],
fallback_lies_by_prompt: dict[str, tuple[str, ...]] | None = None,
scene_ornament_by_prompt: dict[str, str] | None = None,
) -> tuple[Category, tuple[Question, ...], SeedSummary, SeedSummary]:
category, created = Category.objects.get_or_create(
slug=slug,
defaults={"name": name, "is_active": True},
)
category_updates: list[str] = []
if category.name != name:
category.name = name
category_updates.append("name")
if not category.is_active:
category.is_active = True
category_updates.append("is_active")
if category_updates:
category.save(update_fields=category_updates)
questions: list[Question] = []
created_count = 0
updated_count = 0
for prompt, correct_answer in prompts_and_answers:
scene_ornament = ""
if scene_ornament_by_prompt:
scene_ornament = scene_ornament_by_prompt.get(prompt, "")
question, question_created = Question.objects.get_or_create(
category=category,
prompt=prompt,
defaults={
"correct_answer": correct_answer,
"scene_ornament": scene_ornament,
"is_active": True,
},
)
question_updates: list[str] = []
if question.correct_answer != correct_answer:
question.correct_answer = correct_answer
question_updates.append("correct_answer")
if question.scene_ornament != scene_ornament:
question.scene_ornament = scene_ornament
question_updates.append("scene_ornament")
if not question.is_active:
question.is_active = True
question_updates.append("is_active")
if question_updates:
question.save(update_fields=question_updates)
if fallback_lies_by_prompt:
ensure_question_fallback_lies(
question=question,
lies=fallback_lies_by_prompt.get(prompt, ()),
)
created_count += int(question_created)
updated_count += int(bool(question_updates and not question_created))
questions.append(question)
return (
category,
tuple(questions),
SeedSummary(created=int(created), updated=int(bool(category_updates and not created))),
SeedSummary(created=created_count, updated=updated_count),
)
def ensure_question_fallback_lies(*, question: Question, lies: tuple[str, ...]) -> SeedSummary:
created_count = 0
updated_count = 0
for index, lie_text in enumerate(lies):
lie, created = QuestionLie.objects.get_or_create(
question=question,
text=lie_text,
defaults={"is_active": True, "sort_order": index},
)
updates: list[str] = []
if not lie.is_active:
lie.is_active = True
updates.append("is_active")
if lie.sort_order != index:
lie.sort_order = index
updates.append("sort_order")
if updates:
lie.save(update_fields=updates)
created_count += int(created)
updated_count += int(bool(updates and not created))
return SeedSummary(created=created_count, updated=updated_count)
def ensure_mvp_bootstrap(
*,
username: str = DEFAULT_MVP_HOST_USERNAME,
password: str = DEFAULT_MVP_HOST_PASSWORD,
category_slug: str = DEFAULT_MVP_CATEGORY_SLUG,
category_name: str = DEFAULT_MVP_CATEGORY_NAME,
prompts_and_answers: tuple[tuple[str, str], ...] = DEFAULT_MVP_QUESTIONS,
) -> MvpBootstrapResult:
host, host_changes = ensure_host_user(username=username, password=password)
category, questions, category_changes, question_changes = ensure_category_with_questions(
slug=category_slug,
name=category_name,
prompts_and_answers=prompts_and_answers,
fallback_lies_by_prompt=DEFAULT_MVP_FALLBACK_LIES_BY_PROMPT,
scene_ornament_by_prompt=DEFAULT_MVP_SCENE_ORNAMENT_BY_PROMPT,
)
return MvpBootstrapResult(
host=host,
category=category,
questions=questions,
host_changes=host_changes,
category_changes=category_changes,
question_changes=question_changes,
)