Skip to content
Draft
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
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ This codebase (Rails 8.1)
| Directory | Purpose | Count |
|---|---|---|
| `app/models/` | ActiveRecord models | ~66 files |
| `app/services/` | Service objects for complex logic | ~21 files |
| `app/services/` | Service objects for complex logic | ~22 files |
| `app/jobs/` | SolidQueue background jobs | 3 files |
| `app/models/concerns/` | Shared model modules | 12 concerns |

Expand Down Expand Up @@ -181,6 +181,7 @@ end
- `ModelDeduper` — Deduplication logic
- `RichTextMigrator` — Rich text migration utility
- `DisplayImagePresenter` — Image display logic
- `PendingReviewSummary` — Counts unpromoted ideas per admin review queue (story/workshop/variation) for the admin dashboard

### Event Registrations

Expand Down
1 change: 1 addition & 0 deletions app/controllers/admin/home_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class HomeController < ApplicationController
def index
authorize! :home, to: :index?

@review_summary = PendingReviewSummary.new
@system_cards = system_cards
@user_content_cards = user_content_cards
@reference_cards = reference_cards
Expand Down
3 changes: 3 additions & 0 deletions app/models/story_idea.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ def self.search_by_params(params)
has_many :notifications, as: :noticeable, dependent: :destroy
has_many :stories

# Ideas awaiting admin review have not yet been promoted into a published Story.
scope :pending_review, -> { where.missing(:stories) }

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Pending review" is derived from the existing promotion relationship rather than a new status column: an idea is pending until it has been promoted into a Story (the same signal the index's "Promoted to Story" column already uses). Keeps this consistent with how the rest of the app reasons about idea state and avoids a migration.


# Asset associations
has_one :primary_asset, -> { where(type: "PrimaryAsset") },
as: :owner, class_name: "PrimaryAsset", dependent: :destroy
Expand Down
2 changes: 2 additions & 0 deletions app/models/workshop_idea.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ class WorkshopIdea < ApplicationRecord
has_rich_text :rhino_extra_field_spanish

# Scopes
# Ideas awaiting admin review have not yet been promoted into a Workshop.
scope :pending_review, -> { where.missing(:workshops) }
scope :title, ->(title) { where("workshop_ideas.title like ?", "%#{ title }%") }
scope :author_name, ->(author_name) { joins(:created_by).
where("users.first_name like ? or users.last_name like ? or users.email like ?",
Expand Down
2 changes: 2 additions & 0 deletions app/models/workshop_variation_idea.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ def self.search_by_params(params)

# Scopes
scope :workshop_id, ->(workshop_id) { where(workshop_id: workshop_id) if workshop_id.present? }
# Ideas awaiting admin review have not yet been promoted into a WorkshopVariation.
scope :pending_review, -> { where.missing(:workshop_variations) }

def title
name
Expand Down
37 changes: 37 additions & 0 deletions app/services/pending_review_summary.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

# Summarizes the admin review queues for the dashboard: user-submitted ideas
# that have not yet been promoted into their published counterparts. Each queue
# exposes its label, pending count, and originating model so the view can link
# to the relevant index.
class PendingReviewSummary
Queue = Data.define(:label, :count, :model)

QUEUE_DEFINITIONS = [

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a new admin review queue is intentionally a one-line change here — append { model: SomeIdea, label: "..." } and it shows up on the dashboard automatically, provided the model defines a pending_review scope.

{ model: StoryIdea, label: "Story ideas" },
{ model: WorkshopVariationIdea, label: "Workshop variation ideas" },
{ model: WorkshopIdea, label: "Workshop ideas" }
].freeze

def queues
@queues ||= QUEUE_DEFINITIONS.map do |definition|
Queue.new(
label: definition[:label],
count: definition[:model].pending_review.count,
model: definition[:model]
)
end
end

def pending_queues
queues.select { |queue| queue.count.positive? }
end

def total_count
queues.sum(&:count)
end

def any?
total_count.positive?
end
end
26 changes: 26 additions & 0 deletions app/views/admin/home/_review_notifications.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<% if review_summary.any? %>
<div class="bg-yellow-50 border border-yellow-200 text-yellow-800 rounded-xl shadow p-6 mb-6"
role="status">
<div class="flex items-start gap-4">
<div class="text-3xl flex-shrink-0" aria-hidden="true">🔔</div>
<div class="flex-1">
<h2 class="text-lg font-semibold text-yellow-900">
<%= pluralize(review_summary.total_count, "item") %> waiting for review
</h2>
<ul class="mt-3 flex flex-col gap-2">
<% review_summary.pending_queues.each do |queue| %>
<li>
<%= link_to polymorphic_path(queue.model),
class: "inline-flex items-center gap-2 font-medium text-yellow-900 hover:underline" do %>
<span class="inline-flex items-center justify-center min-w-6 h-6 px-2 rounded-full bg-yellow-200 text-yellow-900 text-sm font-bold">
<%= queue.count %>
</span>
<%= queue.label %>
<% end %>
</li>
<% end %>
</ul>
</div>
</div>
</div>
<% end %>
2 changes: 2 additions & 0 deletions app/views/admin/home/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<h1 class="text-2xl font-semibold text-gray-900">Admin home</h1>
</div>

<%= render "review_notifications", review_summary: @review_summary %>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Banner renders above the existing cards so the backlog is the first thing an admin sees on login. The partial no-ops when nothing is pending, so the dashboard is unchanged when caught up.


<div class="bg-white border border-gray-200 rounded-xl shadow-sm p-6">
<div class="flex flex-col gap-6 md:flex-row">
<!-- Column 1 -->
Expand Down
12 changes: 12 additions & 0 deletions spec/models/story_idea_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@
end
end

describe ".pending_review" do
it "includes ideas that have not been promoted to a story" do
idea = create(:story_idea)
expect(StoryIdea.pending_review).to include(idea)
end

it "excludes ideas that have been promoted to a story" do
idea = create(:story_idea, :with_story)
expect(StoryIdea.pending_review).not_to include(idea)
end
end

describe '.search_by_params' do
let!(:idea_alpha) { create(:story_idea, title: 'Art Healing Journey') }
let!(:idea_beta) { create(:story_idea, title: 'Community Impact Report') }
Expand Down
13 changes: 13 additions & 0 deletions spec/models/workshop_idea_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,17 @@

RSpec.describe WorkshopIdea, type: :model do
it_behaves_like "author_creditable", factory: :workshop_idea

describe ".pending_review" do
it "includes ideas that have not been promoted to a workshop" do
idea = create(:workshop_idea)
expect(WorkshopIdea.pending_review).to include(idea)
end

it "excludes ideas that have been promoted to a workshop" do
idea = create(:workshop_idea)
create(:workshop, workshop_idea: idea)
expect(WorkshopIdea.pending_review).not_to include(idea)
end
end
end
13 changes: 13 additions & 0 deletions spec/models/workshop_variation_idea_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,17 @@

RSpec.describe WorkshopVariationIdea, type: :model do
it_behaves_like "author_creditable", factory: :workshop_variation_idea

describe ".pending_review" do
it "includes ideas that have not been promoted to a workshop variation" do
idea = create(:workshop_variation_idea)
expect(WorkshopVariationIdea.pending_review).to include(idea)
end

it "excludes ideas that have been promoted to a workshop variation" do
idea = create(:workshop_variation_idea)
create(:workshop_variation, workshop_variation_idea: idea)
expect(WorkshopVariationIdea.pending_review).not_to include(idea)
end
end
end
19 changes: 19 additions & 0 deletions spec/requests/admin/home_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,24 @@
get admin_path
expect(response).to have_http_status(:ok)
end

it "surfaces a summary of ideas pending review" do
create(:story_idea)
create(:workshop_variation_idea)

get admin_path

expect(response.body).to include("waiting for review")
expect(response.body).to include("Story ideas")
expect(response.body).to include("Workshop variation ideas")
end

it "omits the review summary when nothing is pending" do
create(:story_idea, :with_story)

get admin_path

expect(response.body).not_to include("waiting for review")
end
end
end
52 changes: 52 additions & 0 deletions spec/services/pending_review_summary_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
require "rails_helper"

RSpec.describe PendingReviewSummary do
describe "#queues" do
it "counts the unpromoted ideas in each review queue" do
create(:story_idea)
create(:story_idea, :with_story)
create(:workshop_variation_idea)
create(:workshop_idea)

summary = described_class.new
counts = summary.queues.to_h { |queue| [ queue.model, queue.count ] }

expect(counts).to eq(
StoryIdea => 1,
WorkshopVariationIdea => 1,
WorkshopIdea => 1
)
end
end

describe "#pending_queues" do
it "only returns queues with at least one pending item" do
create(:story_idea)

summary = described_class.new

expect(summary.pending_queues.map(&:model)).to eq([ StoryIdea ])
end
end

describe "#total_count and #any?" do
it "sums the pending items across all queues" do
create(:story_idea)
create(:workshop_idea)

summary = described_class.new

expect(summary.total_count).to eq(2)
expect(summary.any?).to be(true)
end

it "reports nothing pending when every idea is promoted" do
create(:story_idea, :with_story)

summary = described_class.new

expect(summary.total_count).to eq(0)
expect(summary.any?).to be(false)
end
end
end