"""Idempotent seed data for first-run databases. Creates the minimum content needed so the public site is not blank before an admin exists: - System seed user (``users.id = 1``). Inactive and not on the ``ADMIN_EMAILS`` allowlist — cannot log in. Exists only so ``posts.author_user_id`` has a foreign-key target. - Welcome blog post (``slug = 'welcome-to-the-farm'``). - About page (``slug = 'about'``) ported from the Phase 1 static copy. Idempotency is enforced two ways: 1. A marker row in ``schema_migrations`` (``version = 'seed_001'``) — if present, the whole seed is a no-op. 2. As a belt-and-braces guard, each INSERT is gated by ``INSERT OR IGNORE`` on a unique key (``users.email``, ``posts.slug``, ``pages.slug``) so a partially-applied seed never duplicates. Running this twice is safe and logs ``seed_skipped`` on the second boot, which the Phase 2 verification run depends on. """ from __future__ import annotations from datetime import datetime, timezone import structlog from sqlalchemy import Engine, text from app.services.markdown import MarkdownService _log = structlog.get_logger(__name__) # Marker row used to short-circuit the seed on subsequent boots. # Namespaced with the ``seed_`` prefix so it cannot collide with a # real migration file name. _SEED_MARKER: str = "seed_001" # --- Content -------------------------------------------------------------- # # The About body is a Markdown translation of the Phase 1 # ``app/templates/public/about.html`` narrative. Kept close to the # original wording so returning visitors see familiar copy; Head Hen # rewrites via the Phase 4 admin. # # The welcome post is three short paragraphs: a greeting, a Morrison, # TN mention (no street address — per CLAUDE.md), and a teaser of what # future updates will cover. _WELCOME_POST_TITLE: str = "Welcome to the Farm" _WELCOME_POST_SLUG: str = "welcome-to-the-farm" _WELCOME_POST_MD: str = ( "Hi there, and thanks for stopping by Chicken Babies R Us! " "We're a small family farm and we're glad you found us.\n\n" "We're based in Morrison, Tennessee, tucked into the rolling " "hills of the middle part of the state. Our flock is growing, " "our waterfowl are loud, and our coffee cups are never quite " "empty.\n\n" "Check back soon for updates on hatching plans, new chicks and " "ducklings, fresh-egg availability, and whatever the geese " "decided to get into this week." ) _ABOUT_PAGE_TITLE: str = "About the Farm" _ABOUT_PAGE_SLUG: str = "about" _ABOUT_PAGE_MD: str = ( "Chicken Babies R Us is a small family farm tucked into the " "rolling hills of Morrison, Tennessee. What started as a " "handful of chicks in a backyard brooder has grown into a flock " "of chickens, ducks, and geese that keep us busy (and " "entertained) year round.\n\n" "The operation is run by Head Hen — the chief wrangler, egg " "gatherer, waterfowl-whisperer, and unofficial chicken " "photographer. She handles the day-to-day care of the birds " "and does most of the writing you'll find on this site. Expect " "updates on hatching plans, new arrivals, the occasional coop " "mishap, and whatever the geese decided to get into this " "week.\n\n" "We're a hobby farm at heart, not a commercial one, which " "means we can take the time to know our birds and raise them " "the way we think they ought to be raised. If you're curious " "about what we've got going on — or just want to say hello — " "pop over to the contact page." ) # Seed user constants. ``active=0`` + the local-only email keep this # user out of any real auth flow. Phase 3's magic-link issuer MUST # refuse to issue links for non-allowlisted or inactive emails; # Phase 3 tests assert that behavior directly. _SEED_USER_ID: int = 1 _SEED_USER_EMAIL: str = "seed@chickenbabies.local" _SEED_USER_DISPLAY: str = "Head Hen" def run_seed(engine: Engine) -> bool: """Populate the database with first-run content, if not already done. Parameters ---------- engine: SQLAlchemy engine. Must already have had migrations applied (this function does not create tables). Returns ------- bool ``True`` when seed rows were inserted on this call, ``False`` when the marker was already present (no-op). Useful for verification scripts and tests that need to assert ``seed_skipped`` on second boot. """ now_iso = datetime.now(timezone.utc).isoformat() markdown = MarkdownService() with engine.connect() as conn: # Short-circuit via the migration-tracker marker. Cheaper than # counting rows and survives the edge case of a manually # wiped posts/pages table that we wouldn't want to reseed # automatically. marker_row = conn.execute( text( "SELECT version FROM schema_migrations WHERE version = :v" ), {"v": _SEED_MARKER}, ).first() if marker_row is not None: _log.info("seed_skipped", marker=_SEED_MARKER) return False # --- Seed user ------------------------------------------------ # The explicit id=1 pin keeps the ``posts.author_user_id`` # foreign key stable even if a future migration renumbers. # Inline comment below repeats the intent for anyone reading # the DB directly. conn.execute( text( # seed artifact; not a real admin — see Phase 3 for real users "INSERT OR IGNORE INTO users" " (id, email, display_name, created_at, last_login_at, active)" " VALUES (:id, :email, :display_name, :created_at, NULL, 0)" ), { "id": _SEED_USER_ID, "email": _SEED_USER_EMAIL, "display_name": _SEED_USER_DISPLAY, "created_at": now_iso, }, ) # --- Welcome post -------------------------------------------- welcome_html = markdown.render(_WELCOME_POST_MD) conn.execute( text( "INSERT OR IGNORE INTO posts" " (slug, title, body_md, body_html_cached, status," " published_at, updated_at, author_user_id)" " VALUES (:slug, :title, :body_md, :body_html," " 'published', :published_at, :updated_at, :author_id)" ), { "slug": _WELCOME_POST_SLUG, "title": _WELCOME_POST_TITLE, "body_md": _WELCOME_POST_MD, "body_html": welcome_html, "published_at": now_iso, "updated_at": now_iso, "author_id": _SEED_USER_ID, }, ) # --- About page ---------------------------------------------- about_html = markdown.render(_ABOUT_PAGE_MD) conn.execute( text( "INSERT OR IGNORE INTO pages" " (slug, title, body_md, body_html_cached, updated_at," " published)" " VALUES (:slug, :title, :body_md, :body_html," " :updated_at, 1)" ), { "slug": _ABOUT_PAGE_SLUG, "title": _ABOUT_PAGE_TITLE, "body_md": _ABOUT_PAGE_MD, "body_html": about_html, "updated_at": now_iso, }, ) # --- Marker --------------------------------------------------- conn.execute( text( "INSERT INTO schema_migrations (version, applied_at)" " VALUES (:v, :t)" ), {"v": _SEED_MARKER, "t": now_iso}, ) conn.commit() _log.info("seed_applied", marker=_SEED_MARKER) return True