Seed sample lists in demo.ru
The dev server and the Fly apps start from an in-memory database, so
every restart and every preview opened on an empty home page. demo.ru
now gives the stub login three sample lists (one with a few results
already recorded) when that user has none, which leaves a database file
with lists of its own alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTqTY6SoR6G484RLi6fgMc
diff --git a/AGENTS.md b/AGENTS.md
index fbc079e..8b267c1 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -16,7 +16,7 @@ Rank King is a webapp for ranking items along multiple axes through pairwise com
```
config.ru — Rack entrypoint (expects the proxy's Remote-User header)
-demo.ru — config.ru plus StubLogin, for dev and the Fly apps
+demo.ru — config.ru plus StubLogin and sample lists, for dev and the Fly apps
Gemfile — dependencies
Rakefile — rake tasks (test, dev)
db/migrate/ — Sequel migrations
@@ -25,6 +25,7 @@ lib/rank_king/ — app code
db.rb — Sequel connection + migration runner
models.rb — Sequel models (User, List, Item, Axis, Result)
proxy_auth.rb — reads the login from Remote-User
+ seed.rb — sample lists for the stub login (demo.ru)
stub_login.rb — writes a fixed login where there is no proxy (demo.ru)
web.rb — Roda routes
views/ — Phlex view components
diff --git a/demo.ru b/demo.ru
index db3bf51..f4023a5 100644
--- a/demo.ru
+++ b/demo.ru
@@ -1,14 +1,18 @@
# frozen_string_literal: true
# The demo entrypoint: config.ru plus a login written the way the proxy would
-# write it. The dev server and the Fly apps serve from here; a deployment
-# behind the proxy runs config.ru alone.
+# write it, and a few sample lists for that login, since the database here
+# starts empty. The dev server and the Fly apps serve from here; a
+# deployment behind the proxy runs config.ru alone.
require "pathname"
$LOAD_PATH.unshift(Pathname(__dir__).join("lib").to_s)
+require "rank_king/seed"
require "rank_king/stub_login"
+RankKing::Seed.call(RankKing::StubLogin::LOGIN)
+
use RankKing::StubLogin
run Rack::Builder.parse_file(Pathname(__dir__).join("config.ru").to_s)
diff --git a/fly.toml b/fly.toml
index ffa4d2d..f8e8383 100644
--- a/fly.toml
+++ b/fly.toml
@@ -2,10 +2,10 @@
# deploy job in .github/workflows/ci.yml) and the per-PR previews (the
# review-app action in .github/workflows/preview.yml, which overrides
# the app name with the PR's). DATABASE_URL defaults to :memory:, so
-# every deploy starts empty. Both run demo.ru rather than the image's
-# config.ru: nothing writes the Remote-User header in front of them, so
-# StubLogin stands in a login, and the one demo user's lists are served
-# to anyone who opens the URL.
+# every deploy starts with only the sample lists demo.ru seeds. Both run
+# demo.ru rather than the image's config.ru: nothing writes the
+# Remote-User header in front of them, so StubLogin stands in a login,
+# and the one demo user's lists are served to anyone who opens the URL.
#
# The app name is Fly's, not GitHub's, so it has to be unique across all
# of Fly and a taken one fails the deploy. ci.yml spells it twice more,
diff --git a/lib/rank_king/seed.rb b/lib/rank_king/seed.rb
new file mode 100644
index 0000000..30ca254
--- /dev/null
+++ b/lib/rank_king/seed.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require_relative "models"
+
+module RankKing
+ # Sample lists for the dev server and the Fly apps, whose database
+ # starts empty (DATABASE_URL defaults to :memory:). Only demo.ru seeds,
+ # and only a user with no lists, so restarting against a database file
+ # leaves what is already in it alone.
+ module Seed
+ # Each list's items and axes, and a few results already recorded on
+ # it as [axis, winner, loser], so its ratings are not all equal.
+ LISTS = {
+ "Pizza toppings" => {
+ items: ["Pepperoni", "Mushroom", "Pineapple", "Sausage", "Olives", "Basil"],
+ axes: ["Taste", "Value"],
+ results: [
+ ["Taste", "Pepperoni", "Pineapple"],
+ ["Taste", "Basil", "Olives"],
+ ["Taste", "Sausage", "Mushroom"],
+ ["Value", "Mushroom", "Sausage"],
+ ["Value", "Olives", "Pepperoni"],
+ ],
+ },
+ "Board games" => {
+ items: ["Catan", "Ticket to Ride", "Azul", "Wingspan", "Codenames", "Carcassonne"],
+ axes: ["Fun", "Replayability", "Ease of teaching"],
+ results: [],
+ },
+ "Weekend hikes" => {
+ items: ["Mission Peak", "Mount Tam", "Point Reyes", "Muir Woods"],
+ axes: [],
+ results: [],
+ },
+ } #: Hash[String, { items: Array[String], axes: Array[String], results: Array[[String, String, String]] }]
+
+ #: (String login) -> void
+ def self.call(login)
+ user = User.find_or_create(login: login)
+ return if List.where(user_id: user.id).first
+
+ DB.transaction do
+ LISTS.each do |name, list|
+ create_list(user, name, **list)
+ end
+ end
+ end
+
+ #: (User user, String name, items: Array[String], axes: Array[String], results: Array[[String, String, String]]) -> void
+ def self.create_list(user, name, items:, axes:, results:)
+ l = List.create(name: name, user_id: user.id)
+ items_by_name = items.to_h do |n|
+ [n, Item.create(list_id: l.id, name: n)] #: [String, Item]
+ end
+ axes_by_name = axes.to_h do |n|
+ [n, Axis.create(list_id: l.id, name: n)] #: [String, Axis]
+ end
+ results.each do |axis, winner, loser|
+ Result.create(
+ axis_id: axes_by_name.fetch(axis).id,
+ winner_id: items_by_name.fetch(winner).id,
+ loser_id: items_by_name.fetch(loser).id,
+ )
+ end
+ end
+ end
+end
diff --git a/test/test_seed.rb b/test/test_seed.rb
new file mode 100644
index 0000000..1a0da08
--- /dev/null
+++ b/test/test_seed.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+
+require "minitest/autorun"
+
+require_relative "../lib/rank_king/seed"
+
+class TestSeed < Minitest::Test
+ LOGIN = "alpha@example.com"
+
+ def setup
+ RankKing::DB[:results].delete
+ RankKing::DB[:axes].delete
+ RankKing::DB[:items].delete
+ RankKing::DB[:lists].delete
+ RankKing::DB[:users].delete
+ end
+
+ def test_seeds_lists_for_the_user
+ RankKing::Seed.call(LOGIN)
+
+ user = RankKing::User.first(login: LOGIN)
+ names = RankKing::List.where(user_id: user.id).order(:id).map(&:name)
+ assert_equal RankKing::Seed::LISTS.keys, names
+ refute_empty RankKing::DB[:results].all
+ end
+
+ def test_leaves_a_user_with_lists_alone
+ RankKing::Seed.call(LOGIN)
+ RankKing::Seed.call(LOGIN)
+
+ assert_equal RankKing::Seed::LISTS.size, RankKing::DB[:lists].count
+ end
+end