Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions app/controllers/admin/games_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ class Admin::GamesController < AdminController
before_action :set_game, only: %i[ show edit update destroy ]

def index
@games = Game.all
@games = Game.includes(:contents).with_attempt_counts.order(:title)
end

def show
@content_modules = @game.content_modules.order(:name)
end

def new
Expand All @@ -17,7 +21,7 @@ def create

respond_to do |format|
if @game.save
format.html { redirect_back(fallback_location: admin_games_path, notice: "Game was successfully created.") }
format.html { redirect_to admin_games_path, notice: "Game was successfully created." }
else
format.html { render :new, status: :unprocessable_content }
end
Expand Down
13 changes: 13 additions & 0 deletions app/helpers/admin/games_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Admin::GamesHelper
def delete_game_confirmation(game)
module_count = game.contents.size
attempt_count = game.attempts_count

consequences = []
consequences << "It will be removed from #{pluralize(module_count, "module")}" if module_count.positive?
consequences << "#{pluralize(attempt_count, "student attempt")} will be deleted" if attempt_count.positive?
return "Delete this game?" if consequences.empty?

"Delete this game? #{consequences.join(" and ")}."
end
end
12 changes: 12 additions & 0 deletions app/models/game.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
class Game < ApplicationRecord
has_many :contents, as: :contentable, dependent: :destroy
has_many :content_modules, through: :contents
# TODO: Ask the client whether deleting a game should be blocked when students have attempts
# (dependent: :restrict_with_error) instead of deleting their attempt history.
has_many :game_attempts, dependent: :destroy

validates :title, :slug, presence: true
validates :title, :slug, uniqueness: true

scope :not_in, ->(content_module) { where.not(id: content_module.contents.games.select(:contentable_id)) }

scope :with_attempt_counts, -> {
left_joins(:game_attempts).select("games.*, COUNT(game_attempts.id) AS attempts_count").group(:id)
}

# Uses the preselected count from with_attempt_counts when present, avoiding N+1 on lists.
def attempts_count
has_attribute?(:attempts_count) ? self[:attempts_count] : game_attempts.count
end

def destroy_with_attachable?
false
end
Expand Down
7 changes: 7 additions & 0 deletions app/views/admin/content_modules/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
{}, class: "select select-sm", aria: { label: "Game" } %>
<%= form.submit "Add game", class: "btn btn-sm btn-primary" %>
<% end %>
<% elsif Game.none? %>
<p class="text-sm opacity-60">
No games yet.
<%= link_to "Create a game", new_admin_game_path, class: "link" %>
</p>
<% else %>
<p class="text-sm opacity-60">All games are in this module.</p>
<% end %>
<%= link_to "Add link", new_admin_content_module_link_path(@content_module), class: "btn btn-sm btn-primary" %>
</div>
Expand Down
3 changes: 3 additions & 0 deletions app/views/admin/games/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
<%= form.label :slug, "Slug", class: "label" %>
<%= form.text_field :slug, class: "input w-full" %>

<%= form.label :description, class: "label" %>
<%= form.text_area :description, rows: 4, class: "textarea w-full" %>

<%= form.submit class: "btn btn-primary" %>
</div>
<% end %>
14 changes: 0 additions & 14 deletions app/views/admin/games/_game.html.erb

This file was deleted.

25 changes: 16 additions & 9 deletions app/views/admin/games/edit.html.erb
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
<% content_for :title, "Editing game" %>
<% content_for :title, "Edit Game" %>

<div class="md:w-2/3 w-full">
<h1 class="font-bold text-4xl">Editing game</h1>

<%= render "form", game: @game %>

<%= link_to "Show this game", [:admin, @game], class: "w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
<%= link_to "Back to games", admin_games_path, class: "w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
</div>
<nav class="breadcrumbs text-sm">
<ul>
<li><%= link_to "Games", admin_games_path %></li>
<li><%= link_to @game.title, [:admin, @game] %></li>
<li>Edit</li>
</ul>
</nav>
<section class="py-10 flex items-center justify-center">
<div class="card card-border bg-base-100 shadow-md w-96">
<div class="card-body">
<h1 class="card-title">Edit Game</h1>
<%= render "form", game: @game %>
</div>
</div>
</section>
62 changes: 38 additions & 24 deletions app/views/admin/games/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,29 +1,43 @@
<% content_for :title, "Games" %>

<div class="w-full">
<% if notice.present? %>
<p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block" id="notice"><%= notice %></p>
<% end %>
<p style="color: green"><%= notice %></p>
<p style="color: red"><%= alert %></p>

<div class="flex justify-between items-center">
<h1 class="font-bold text-4xl">Games</h1>
<%= link_to "New game", new_admin_game_path, class: "rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white block font-medium" %>
</div>
<header class="flex justify-between items-center mb-4">
<h1 class="text-2xl">Games</h1>
<%= link_to "New game", new_admin_game_path, class: "btn btn-primary" %>
</header>

<div id="games" class="min-w-full divide-y divide-gray-200 space-y-5">
<% if @games.any? %>
<% @games.each do |game| %>
<div class="flex flex-col sm:flex-row justify-between items-center pb-5 sm:pb-0">
<%= render game %>
<div class="w-full sm:w-auto flex flex-col sm:flex-row space-x-2 space-y-2">
<%= link_to "Show", [:admin, game], class: "w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
<%= link_to "Edit", edit_admin_game_path(game), class: "w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
<%= button_to "Destroy", [:admin, game], method: :delete, class: "w-full sm:w-auto rounded-md px-3.5 py-2.5 text-white bg-red-600 hover:bg-red-500 font-medium cursor-pointer", data: { turbo_confirm: "Are you sure?" } %>
</div>
</div>
<% end %>
<% else %>
<p class="text-center my-10">No games found.</p>
<% end %>
<% if @games.any? %>
<div class="overflow-x-auto rounded-box border border-base-content/5 bg-base-100">
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Slug</th>
<th>Modules</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% @games.each do |game| %>
<tr>
<td><%= link_to game.title, [:admin, game], class: "link" %></td>
<td><%= game.slug %></td>
<td><%= game.contents.size %></td>
<td>
<div class="flex gap-2">
<%= link_to "Edit", edit_admin_game_path(game), class: "btn btn-sm btn-primary" %>
<%= button_to "Delete", [:admin, game], method: :delete,
data: { "turbo-confirm": delete_game_confirmation(game) },
class: "btn btn-sm btn-error" %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<p class="text-sm opacity-60">No games yet.</p>
<% end %>
4 changes: 2 additions & 2 deletions app/views/admin/games/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<% content_for :title, "New game" %>
<% content_for :title, "New Game" %>

<nav class="breadcrumbs text-sm">
<ul>
<li><%= link_to "Content Modules", admin_content_modules_path %></li>
<li><%= link_to "Games", admin_games_path %></li>
<li>New Game</li>
</ul>
</nav>
Expand Down
52 changes: 41 additions & 11 deletions app/views/admin/games/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,15 +1,45 @@
<% content_for :title, "Showing game" %>
<% content_for :title, @game.title %>

<div class="md:w-2/3 w-full">
<% if notice.present? %>
<p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block" id="notice"><%= notice %></p>
<% end %>
<p style="color: green"><%= notice %></p>
<p style="color: red"><%= alert %></p>

<nav class="breadcrumbs text-sm">
<ul>
<li><%= link_to "Games", admin_games_path %></li>
<li><%= @game.title %></li>
</ul>
</nav>

<h1 class="font-bold text-4xl">Showing game</h1>
<header class="flex items-center justify-between mb-4">
<h1 class="text-2xl"><%= @game.title %></h1>
<div class="flex gap-2">
<%= link_to "Edit this game", edit_admin_game_path(@game), class: "btn btn-primary" %>
<%= button_to "Delete this game", [:admin, @game], method: :delete,
data: { "turbo-confirm": delete_game_confirmation(@game) },
class: "btn btn-error" %>
</div>
</header>

<%= render @game %>
<dl class="mb-8 space-y-4">
<div>
<dt class="font-semibold">Slug</dt>
<dd><%= @game.slug %></dd>
</div>
<div>
<dt class="font-semibold">Description</dt>
<dd><%= @game.description.presence || "No description." %></dd>
</div>
</dl>

<%= link_to "Edit this game", edit_admin_game_path(@game), class: "w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
<%= link_to "Back to games", admin_games_path, class: "w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
<%= button_to "Destroy this game", [:admin, @game], method: :delete, form_class: "sm:inline-block mt-2 sm:mt-0 sm:ml-2", class: "w-full rounded-md px-3.5 py-2.5 text-white bg-red-600 hover:bg-red-500 font-medium cursor-pointer", data: { turbo_confirm: "Are you sure?" } %>
</div>
<section>
<h2 class="text-lg font-semibold mb-2">Modules</h2>
<% if @content_modules.any? %>
<ul class="list-disc pl-6">
<% @content_modules.each do |content_module| %>
<li><%= link_to content_module.name, edit_admin_content_module_path(content_module), class: "link" %></li>
<% end %>
</ul>
<% else %>
<p class="text-sm opacity-60">Not in any module yet.</p>
<% end %>
</section>
4 changes: 4 additions & 0 deletions app/views/shared/_admin_nav.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
<%= link_to "Content Modules", admin_content_modules_path,
class: "#{controller_path.start_with?("admin/content_modules", "admin/links", "admin/contents") ? "active" : ""}" %>
</li>
<li>
<%= link_to "Games", admin_games_path,
class: "#{controller_path.start_with?("admin/games") ? "active" : ""}" %>
</li>
</ul>
<%= button_to "Log out", admin_session_path, method: :delete, class: "btn btn-sm btn-ghost ml-2" %>
</div>
Expand Down
20 changes: 20 additions & 0 deletions db/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,32 @@ def maybe(&block)
# Production should not have any seeded data for the content or schools
return if Rails.env.production?

fruit_veggie_quiz = Game.find_or_create_by!(slug: "fruit-veggie-quiz") do |game|
game.title = "Fruit and Veggie Quiz"
game.description = "Sort fruits and vegetables into the right food groups."
end
sleep_habits = Game.find_or_create_by!(slug: "sleep-habits") do |game|
game.title = "Sleep Habits"
game.description = "Pick the habits that help you get a good night's sleep."
end
# Intentionally not attached to any module so the "Add game" dropdown has an option.
Game.find_or_create_by!(slug: "hydration-heroes") do |game|
game.title = "Hydration Heroes"
game.description = "Learn how much water your body needs each day."
end
games_by_program = { programs.first => fruit_veggie_quiz, programs.last => sleep_habits }

# The first module of each program and level gets a game after its survey.
# The second module of the basic level gets its game before the survey.
ContentModule.levels.keys.each do |level|
programs.each do |program|
game = games_by_program.fetch(program)
2.times do |i|
mod = program.content_modules.create!(name: "#{program.name} #{level.humanize} Module #{i + 1}", level: level)
link = Link.new(title: "Survey #{i + 1}", url: "https://example.com/survey/#{program.id}-#{level}-#{i}", link_type: "survey")
mod.contents.create!(contentable: game) if i == 1 && level == "basic"
mod.contents.create!(contentable: link)
mod.contents.create!(contentable: game) if i == 0
end
end
end
Expand Down
12 changes: 12 additions & 0 deletions test/controllers/admin/content_modules_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ class Admin::ContentModulesControllerTest < ActionDispatch::IntegrationTest
get edit_admin_content_module_url(@content_module)

assert_select "select[name='content[contentable_id]']", count: 0
assert_select "p", text: /All games are in this module\./
end

test "edit links to creating a game when no games exist" do
Content.where(contentable_type: "Game").destroy_all
Game.destroy_all

get edit_admin_content_module_url(@content_module)

assert_select "select[name='content[contentable_id]']", count: 0
assert_select "p", text: /No games yet/
assert_select "a.link[href='#{new_admin_game_path}']", text: "Create a game"
end

test "index shows the contents count" do
Expand Down
31 changes: 31 additions & 0 deletions test/controllers/admin/games_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ class Admin::GamesControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end

test "index marks the Games nav link as active" do
get admin_games_url

assert_select "nav a.active", text: "Games"
end

test "should get new" do
get new_admin_game_url
assert_response :success
Expand All @@ -26,6 +32,31 @@ class Admin::GamesControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to admin_games_url
end

test "index shows how many modules use each game" do
get admin_games_url

assert_select "tr:has(a[href='#{admin_game_path(@game)}']) td:nth-child(3)", text: "1"
end

test "show lists the modules that use the game" do
get admin_game_url(@game)

assert_select "a[href=?]", edit_admin_content_module_path(content_modules(:one)), text: content_modules(:one).name
end

test "show says when the game is in no module" do
get admin_game_url(games(:two))

assert_select "p", text: "Not in any module yet."
end

test "delete confirmation warns when the game is in modules" do
get admin_game_url(@game)

assert_select "form[action=?] button[data-turbo-confirm=?]", admin_game_path(@game),
"Delete this game? It will be removed from 1 module and 1 student attempt will be deleted."
end

test "should get edit" do
get edit_admin_game_url(@game)
assert_response :success
Expand Down
Loading
Loading