From 3b6b48bc8b44268bb6273ea15811934bb7654ec7 Mon Sep 17 00:00:00 2001 From: Adam Bachman <13002+abachman@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:43:49 -0400 Subject: [PATCH 1/6] change game_attempts.token to text and allow null --- .../20260829203815_allow_null_game_attempt_token.rb | 9 +++++++++ db/schema.rb | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20260829203815_allow_null_game_attempt_token.rb diff --git a/db/migrate/20260829203815_allow_null_game_attempt_token.rb b/db/migrate/20260829203815_allow_null_game_attempt_token.rb new file mode 100644 index 0000000..2f49a8d --- /dev/null +++ b/db/migrate/20260829203815_allow_null_game_attempt_token.rb @@ -0,0 +1,9 @@ +class AllowNullGameAttemptToken < ActiveRecord::Migration[8.1] + def up + change_column :game_attempts, :token, :text, null: true + end + + def down + change_column :game_attempts, :token, :string, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index a58913e..ef656fe 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_08_29_190944) do +ActiveRecord::Schema[8.1].define(version: 2026_08_29_203815) do create_table "classroom_modules", force: :cascade do |t| t.integer "classroom_program_id", null: false t.integer "content_module_id", null: false @@ -73,7 +73,7 @@ t.integer "score" t.datetime "started_at" t.integer "student_id", null: false - t.string "token", null: false + t.text "token" t.datetime "updated_at", null: false t.index ["game_id"], name: "index_game_attempts_on_game_id" t.index ["student_id"], name: "index_game_attempts_on_student_id" From e9936805f59c407128e5fcdf995619ba1fb4ab50 Mon Sep 17 00:00:00 2001 From: Adam Bachman <13002+abachman@users.noreply.github.com> Date: Sat, 29 Aug 2026 19:01:22 -0400 Subject: [PATCH 2/6] we actually do want game_attempts to have tokens, and we don't need a #show action --- .../20260829203815_allow_null_game_attempt_token.rb | 9 --------- db/schema.rb | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) delete mode 100644 db/migrate/20260829203815_allow_null_game_attempt_token.rb diff --git a/db/migrate/20260829203815_allow_null_game_attempt_token.rb b/db/migrate/20260829203815_allow_null_game_attempt_token.rb deleted file mode 100644 index 2f49a8d..0000000 --- a/db/migrate/20260829203815_allow_null_game_attempt_token.rb +++ /dev/null @@ -1,9 +0,0 @@ -class AllowNullGameAttemptToken < ActiveRecord::Migration[8.1] - def up - change_column :game_attempts, :token, :text, null: true - end - - def down - change_column :game_attempts, :token, :string, null: false - end -end diff --git a/db/schema.rb b/db/schema.rb index ef656fe..a58913e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_08_29_203815) do +ActiveRecord::Schema[8.1].define(version: 2026_08_29_190944) do create_table "classroom_modules", force: :cascade do |t| t.integer "classroom_program_id", null: false t.integer "content_module_id", null: false @@ -73,7 +73,7 @@ t.integer "score" t.datetime "started_at" t.integer "student_id", null: false - t.text "token" + t.string "token", null: false t.datetime "updated_at", null: false t.index ["game_id"], name: "index_game_attempts_on_game_id" t.index ["student_id"], name: "index_game_attempts_on_student_id" From 1705027bf149f96087e64d5e03082df2693ab14d Mon Sep 17 00:00:00 2001 From: Adam Bachman <13002+abachman@users.noreply.github.com> Date: Sat, 29 Aug 2026 19:04:20 -0400 Subject: [PATCH 3/6] drop score from game attempt fixtures --- test/fixtures/game_attempts.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/fixtures/game_attempts.yml b/test/fixtures/game_attempts.yml index 8db4e1f..8e0dd1f 100644 --- a/test/fixtures/game_attempts.yml +++ b/test/fixtures/game_attempts.yml @@ -1,7 +1,6 @@ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html one: - score: 1 outcome: completed started_at: 2026-08-29 11:53:38 finished_at: 2026-08-29 11:53:38 @@ -10,7 +9,6 @@ one: token: onetoken two: - score: 1 outcome: expired started_at: 2026-08-29 11:53:38 finished_at: 2026-08-29 11:53:38 From 0ba30e84c66c26e9d394c0b712b3a8d6b604b68a Mon Sep 17 00:00:00 2001 From: Adam Bachman <13002+abachman@users.noreply.github.com> Date: Sun, 30 Aug 2026 10:25:08 -0400 Subject: [PATCH 4/6] [WIP] playing fake games --- app/controllers/game_landing_controller.rb | 21 ++++++ app/javascript/controllers/quiz_controller.js | 73 +++++++++++++++++++ app/views/game_landing/embed.html.erb | 7 ++ app/views/game_landing/show.html.erb | 7 ++ app/views/layouts/embed.html.erb | 16 ++++ config/routes.rb | 6 ++ .../game_landing_controller_test.rb | 33 +++++++++ 7 files changed, 163 insertions(+) create mode 100644 app/controllers/game_landing_controller.rb create mode 100644 app/javascript/controllers/quiz_controller.js create mode 100644 app/views/game_landing/embed.html.erb create mode 100644 app/views/game_landing/show.html.erb create mode 100644 app/views/layouts/embed.html.erb create mode 100644 test/controllers/game_landing_controller_test.rb diff --git a/app/controllers/game_landing_controller.rb b/app/controllers/game_landing_controller.rb new file mode 100644 index 0000000..682c5b4 --- /dev/null +++ b/app/controllers/game_landing_controller.rb @@ -0,0 +1,21 @@ +class GameLandingController < ApplicationController + include StudentAuthentication + + layout "embed", only: :embed + + def show + @game = Game.find(params[:id]) + @game_attempt_token = GameAttempt.generate_token(student_id: current_student.id, game_id: @game.id) + + logger.info("game attempt token: #{{game_attempt_token: @game_attempt_token, student_id: current_student.id, game_id: @game.id}}") + end + + def embed + @game = Game.find(params[:id]) + end + + private + + def current_student = Current.student + helper_method :current_student +end diff --git a/app/javascript/controllers/quiz_controller.js b/app/javascript/controllers/quiz_controller.js new file mode 100644 index 0000000..5acdf8c --- /dev/null +++ b/app/javascript/controllers/quiz_controller.js @@ -0,0 +1,73 @@ +import { Controller } from "@hotwired/stimulus" + +const QUESTIONS = [ + { text: "The sky is blue.", answer: true }, + { text: "Fish can breathe air out of water.", answer: false }, + { text: "The earth orbits the sun.", answer: true } +] + +export default class extends Controller { + static targets = ["root"] + static values = { startUrl: String, finishUrl: String } + + connect() { + this.token = new URLSearchParams(window.location.search).get("token") + this.questionIndex = 0 + this.correctCount = 0 + this.started = false + this.renderQuestion() + } + + renderQuestion() { + const question = QUESTIONS[this.questionIndex] + this.rootTarget.innerHTML = ` +

${question.text}

+
+ + +
+ ` + this.rootTarget.querySelectorAll("button").forEach((button) => { + button.addEventListener("click", () => this.answer(button.dataset.answer === "true")) + }) + } + + async answer(value) { + if (!this.started) { + this.started = true + await this.post(this.startUrlValue) + } + + if (value === QUESTIONS[this.questionIndex].answer) this.correctCount += 1 + + this.questionIndex += 1 + if (this.questionIndex < QUESTIONS.length) { + this.renderQuestion() + } else { + await this.finish() + } + } + + async finish() { + const outcome = this.correctCount === QUESTIONS.length ? "pass" : "fail" + + await this.post(this.finishUrlValue, { + game_attempt: { outcome, score: this.correctCount } + }) + + this.rootTarget.innerHTML = `

You scored ${this.correctCount} out of ${QUESTIONS.length}.

` + } + + async post(url, extraParams = {}) { + const csrfToken = document.querySelector("meta[name='csrf-token']")?.content + + await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": csrfToken + }, + body: JSON.stringify({ token: this.token, ...extraParams }) + }) + } +} diff --git a/app/views/game_landing/embed.html.erb b/app/views/game_landing/embed.html.erb new file mode 100644 index 0000000..bbc39d5 --- /dev/null +++ b/app/views/game_landing/embed.html.erb @@ -0,0 +1,7 @@ +<%= tag.div data: { + controller: "quiz", + quiz_start_url_value: start_game_attempts_path, + quiz_finish_url_value: finish_game_attempts_path + } do %> +
+<% end %> diff --git a/app/views/game_landing/show.html.erb b/app/views/game_landing/show.html.erb new file mode 100644 index 0000000..78de03c --- /dev/null +++ b/app/views/game_landing/show.html.erb @@ -0,0 +1,7 @@ +<% content_for(:title, @game.title) %> + +

<%= @game.title %>

+ +<%= tag.iframe src: embed_game_path(@game, token: @game_attempt_token), + class: "w-full h-[600px] border-0", + title: @game.title %> diff --git a/app/views/layouts/embed.html.erb b/app/views/layouts/embed.html.erb new file mode 100644 index 0000000..a1a5e11 --- /dev/null +++ b/app/views/layouts/embed.html.erb @@ -0,0 +1,16 @@ + + + + <%= content_for(:title) || "Endsideout" %> + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + + <%= yield %> + + diff --git a/config/routes.rb b/config/routes.rb index 48de98d..ba10797 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,6 +26,12 @@ resource :student_session, only: %i[new create destroy] resources :student_homes, only: %i[index] + resources :games, only: [] do + member do + get :play, to: "game_landing#show" + get :embed, to: "game_landing#embed" + end + end resources :classroom_rosters, only: %i[show], param: :uuid resources :game_attempts, only: [] do collection do diff --git a/test/controllers/game_landing_controller_test.rb b/test/controllers/game_landing_controller_test.rb new file mode 100644 index 0000000..5933fe4 --- /dev/null +++ b/test/controllers/game_landing_controller_test.rb @@ -0,0 +1,33 @@ +require "test_helper" + +class GameLandingControllerTest < ActionDispatch::IntegrationTest + setup do + @student = students(:ada) + @game = games(:one) + student_sign_in_as @student + end + + test "unauthenticated request redirects to student login" do + student_sign_out + get play_game_url(@game) + assert_redirected_to new_student_session_url + end + + test "show generates a game attempt token and embeds the game" do + get play_game_url(@game) + assert_response :success + + iframe_src = assert_select("iframe").first["src"] + token = Rack::Utils.parse_nested_query(URI(iframe_src).query)["token"] + assert_equal( + { "student_id" => @student.id, "game_id" => @game.id }, + GameAttempt.verify_token(token) + ) + end + + test "embed renders the quiz mount point" do + get embed_game_url(@game, token: "some-token") + assert_response :success + assert_select "[data-controller='quiz']" + end +end From 3e4116638090040f27291060c361294f022c4ab2 Mon Sep 17 00:00:00 2001 From: Adam Bachman <13002+abachman@users.noreply.github.com> Date: Sun, 30 Aug 2026 10:52:08 -0400 Subject: [PATCH 5/6] fix rubocop errors --- app/controllers/game_landing_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/game_landing_controller.rb b/app/controllers/game_landing_controller.rb index 682c5b4..95795b4 100644 --- a/app/controllers/game_landing_controller.rb +++ b/app/controllers/game_landing_controller.rb @@ -7,7 +7,7 @@ def show @game = Game.find(params[:id]) @game_attempt_token = GameAttempt.generate_token(student_id: current_student.id, game_id: @game.id) - logger.info("game attempt token: #{{game_attempt_token: @game_attempt_token, student_id: current_student.id, game_id: @game.id}}") + logger.info("game attempt token: #{{ game_attempt_token: @game_attempt_token, student_id: current_student.id, game_id: @game.id }}") end def embed From 149f46ba6a19a2b22cbce7daf5aec0385eae3bf7 Mon Sep 17 00:00:00 2001 From: Sean Dickinson Date: Sat, 24 Oct 2026 16:40:55 -0400 Subject: [PATCH 6/6] fix: update fixture name --- test/controllers/game_landing_controller_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/controllers/game_landing_controller_test.rb b/test/controllers/game_landing_controller_test.rb index 5933fe4..1d00cd5 100644 --- a/test/controllers/game_landing_controller_test.rb +++ b/test/controllers/game_landing_controller_test.rb @@ -2,7 +2,7 @@ class GameLandingControllerTest < ActionDispatch::IntegrationTest setup do - @student = students(:ada) + @student = students(:one) @game = games(:one) student_sign_in_as @student end