Champion Portal Development Sub-Phase 9.3
Status: ✅ Complete
Completed: February 2026
Estimated Effort: 1 week
Prerequisites: Sub-Phase 9.2 CompleteRelated Documents:
- README.md — Phase 9 Overview
- 9.2-seeded-questions.md — Seeded questions system
Controller:
Champions::SeededQuestionsController — Full CRUD with filtering, search, sortingensure_portal_admin! for all actionsViews (7 files):
index.html.erb — Filter tabs (status, target_type), search, sortable columns, action buttonsshow.html.erb — Question details, metadata card, exposure history tablenew.html.erb / edit.html.erb — Form wrappers with headers_form.html.erb — Complete form with template variable reference_preview.html.erb — Live interpolation preview panel_status_badge.html.erb — Color-coded status badgesRoutes:
resources :seeded_questions do
member do
get :preview
patch :activate
patch :deactivate
end
end
Tests:
test/controllers/champions/seeded_questions_controller_test.rb — 36 tests, 89 assertionsFiles Created:
app/controllers/champions/seeded_questions_controller.rb (215 lines)app/views/champions/seeded_questions/ (7 view files)test/controllers/champions/seeded_questions_controller_test.rbconfig/routes.rbSub-Phase 9.3 provides the administrative interface for managing seeded discussion questions. Staff can create, edit, target, and monitor questions without requiring code deployments.
| In Scope | Out of Scope |
|---|---|
| CRUD for seeded questions | Question AI generation |
| Community type targeting | A/B testing framework |
| Affinity subtype targeting | Question scheduling automation |
| Preview with interpolation | Question translation/i18n |
| Basic engagement metrics | Advanced analytics dashboard |
Namespace: Alumni Lookup Champion Admin (/champions/)
Path: /champions/seeded-questions
Following the pattern of other Champion Admin features:
/champions/verification — Champion verification/champions/communities — Community management/champions/seeded-questions — Seeded questions (NEW)File: app/controllers/champions/seeded_questions_controller.rb
module Champions
class SeededQuestionsController < ApplicationController
before_action :ensure_portal_admin!
before_action :set_question, only: [:show, :edit, :update, :destroy, :preview]
def index
@questions = Cp::SeededQuestion
.includes(:exposures)
.order(created_at: :desc)
.page(params[:page])
end
def show; end
def new
@question = Cp::SeededQuestion.new
end
def create
@question = Cp::SeededQuestion.new(question_params)
if @question.save
redirect_to champions_seeded_question_path(@question),
notice: "Question created successfully!"
else
render :new, status: :unprocessable_entity
end
end
def edit; end
def update
if @question.update(question_params)
redirect_to champions_seeded_question_path(@question),
notice: "Question updated."
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@question.destroy
redirect_to champions_seeded_questions_path,
notice: "Question deleted."
end
# Preview interpolation with sample community
def preview
@community = params[:community_id] ?
Cp::Community.find(params[:community_id]) :
sample_community_for_type(@question.target_type)
@interpolated_title = Cp::QuestionInterpolator.new(@question, @community).interpolate_title
@interpolated_body = Cp::QuestionInterpolator.new(@question, @community).interpolate_body
render partial: 'preview'
end
private
def set_question
@question = Cp::SeededQuestion.find(params[:id])
end
def question_params
params.require(:seeded_question).permit(
:title, :body, :status, :target_type, :affinity_subtype,
:priority, :active_from, :active_until
)
end
def sample_community_for_type(type)
case type
when 'district'
Cp::Community.district.active.first
when 'college'
Cp::Community.college.active.first
when 'major'
Cp::Community.major.active.first
when 'affinity', 'affinity_subtype'
Cp::Community.affinity.active.first
when 'industry'
Cp::Community.industry.active.first
else
Cp::Community.active.first
end
end
end
end
| Operation | Description | Permission |
|---|---|---|
| List | View all questions with filters | portal_admin |
| Create | Add new question with targeting | portal_admin |
| Edit | Modify question, targeting, status | portal_admin |
| Delete | Remove question (soft delete via archive) | portal_admin |
| Preview | See interpolated question with sample community | portal_admin |
| Status | Description | List Filter |
|---|---|---|
draft |
Question not visible, being edited | Draft tab |
active |
Question available for selection | Active tab |
paused |
Temporarily disabled | Paused tab |
archived |
No longer used, hidden by default | Archived tab |
| Target Type | Additional Config |
|---|---|
| All Communities | None |
| District | None (applies to all district communities) |
| College | None (applies to all college communities) |
| Major | None (applies to all major communities) |
| Affinity | None (applies to all affinity communities) |
| Industry | None (applies to all industry communities) |
| Affinity Subtype | Select subtype: greek, athletics, music, service, etc. |
| Field | Description |
|---|---|
active_from |
Don’t show before this date |
active_until |
Don’t show after this date |
priority |
Higher priority = more likely selected |
┌─────────────────────────────────────────────────────────────────────────────┐
│ Seeded Questions [+ New Question]│
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ [Active (12)] [Draft (3)] [Paused (2)] [Archived (5)] │
│ │
│ ─────────────────────────────────────────────────────────────────────────── │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ "What's your favorite Belmont memory?" │ │
│ │ Target: All Communities · Priority: High · Created Jan 15, 2026 │ │
│ │ 📊 45 exposures · 23 replies · 89 reactions │ │
│ │ [Preview] [Edit] [Pause] │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ "What brought you to {community_name}?" │ │
│ │ Target: District · Priority: Medium · Created Jan 14, 2026 │ │
│ │ 📊 12 exposures · 8 replies · 31 reactions │ │
│ │ [Preview] [Edit] [Pause] │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ "What's your favorite memory from Greek Week?" │ │
│ │ Target: Affinity Subtype (Greek) · Priority: Medium · Created Jan 12 │ │
│ │ 📊 6 exposures · 4 replies · 18 reactions │ │
│ │ [Preview] [Edit] [Pause] │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ [← Previous] [1] [2] [3] [Next →] │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ ← Back to Questions │
│ │
│ New Seeded Question │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Question Title * │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ What brought you to {community_name}? │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ 280 character limit. Use {community_name}, {current_season}, etc. │
│ │
│ Body (Optional) │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ Share what attracted you to this city and how you found │ │
│ │ fellow Bruins here! │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ Additional context or call-to-action. │
│ │
│ ─────────────────────────────────────────────────────────────────────────── │
│ │
│ Target │
│ ○ All Communities │
│ ○ District (city communities) │
│ ○ College │
│ ○ Major │
│ ○ Affinity │
│ ● Affinity Subtype: [Greek ▼] │
│ ○ Industry │
│ │
│ ─────────────────────────────────────────────────────────────────────────── │
│ │
│ Settings │
│ │
│ Status: [Active ▼] │
│ │
│ Priority: [○ Low ● Medium ○ High] │
│ │
│ ☐ Schedule active dates │
│ Active from: [___________] Active until: [___________] │
│ │
│ ─────────────────────────────────────────────────────────────────────────── │
│ │
│ [Cancel] [Save as Draft] [Publish] │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Live preview showing interpolation:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Preview │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Preview in: [Nashville Champions ▼] │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 📢 From the Engagement Team │ │
│ │ │ │
│ │ "What brought you to Nashville Champions?" │ │
│ │ │ │
│ │ Share what attracted you to this city and how you found │ │
│ │ fellow Bruins here! │ │
│ │ │ │
│ │ 💬 Reply ❤️ React │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ Variables detected: {community_name} │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ ← Back to Questions [Edit] [Pause]│
│ │
│ "What brought you to {community_name}?" │
│ │
│ Target: District · Priority: Medium · Status: Active │
│ Created Jan 14, 2026 · Last edited Jan 15, 2026 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Body: │
│ "Share what attracted you to this city and how you found fellow Bruins!" │
│ │
│ ─────────────────────────────────────────────────────────────────────────── │
│ │
│ Performance │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 12 │ │ 8 │ │ 31 │ │
│ │ Exposures │ │ Replies │ │ Reactions │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
│ │
│ ─────────────────────────────────────────────────────────────────────────── │
│ │
│ Exposed In (12 communities) │
│ │
│ Community Posted Replies Reactions │
│ ─────────────────────────────────────────────────────────────────────────── │
│ Nashville Champions Jan 15 3 12 [View Post] │
│ Atlanta Champions Jan 16 2 8 [View Post] │
│ Dallas Champions Jan 16 1 5 [View Post] │
│ Denver Champions Jan 17 2 6 [View Post] │
│ ... │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
| Action | Required Role | Notes |
|---|---|---|
| View list | portal_admin |
Staff cannot view |
| Create question | portal_admin |
|
| Edit question | portal_admin |
|
| Delete/Archive | portal_admin |
|
| Preview | portal_admin |
|
| View metrics | portal_admin |
# In controller
before_action :ensure_portal_admin!
Files to create:
app/controllers/champions/seeded_questions_controller.rbFiles to modify:
config/routes.rb — Add routes in champions namespace# config/routes.rb
namespace :champions do
# ... existing routes ...
resources :seeded_questions do
member do
get :preview
end
end
end
Files to create:
app/views/champions/seeded_questions/index.html.erbapp/views/champions/seeded_questions/_question_row.html.erbFiles to create:
app/views/champions/seeded_questions/new.html.erbapp/views/champions/seeded_questions/edit.html.erbapp/views/champions/seeded_questions/_form.html.erbFiles to create:
app/views/champions/seeded_questions/show.html.erbapp/views/champions/seeded_questions/_preview.html.erbJS/Stimulus:
File: test/controllers/champions/seeded_questions_controller_test.rb
class Champions::SeededQuestionsControllerTest < ActionDispatch::IntegrationTest
setup do
@admin = users(:admin)
@staff = users(:staff)
@question = cp_seeded_questions(:district_question)
end
test "portal_admin can access index" do
sign_in @admin
get champions_seeded_questions_url
assert_response :success
end
test "staff cannot access index" do
sign_in @staff
get champions_seeded_questions_url
assert_response :forbidden
end
test "create question with valid params" do
sign_in @admin
assert_difference 'Cp::SeededQuestion.count', 1 do
post champions_seeded_questions_url, params: {
seeded_question: {
title: "Test question for {community_name}",
target_type: 'district',
status: 'active'
}
}
end
assert_redirected_to champions_seeded_question_url(Cp::SeededQuestion.last)
end
test "preview renders interpolated content" do
sign_in @admin
community = cp_communities(:nashville_district)
get preview_champions_seeded_question_url(@question, community_id: community.id)
assert_response :success
assert_match /Nashville Champions/, response.body
end
end
File: test/system/champions/seeded_questions_test.rb
class Champions::SeededQuestionsTest < ApplicationSystemTestCase
setup do
sign_in users(:admin)
end
test "admin creates new seeded question" do
visit champions_seeded_questions_url
click_link "New Question"
fill_in "Question Title", with: "What brought you to {community_name}?"
fill_in "Body", with: "Share your story!"
choose "District"
click_button "Publish"
assert_text "Question created successfully"
assert_text "What brought you to {community_name}?"
end
test "admin previews question with different communities" do
question = cp_seeded_questions(:district_question)
visit champions_seeded_question_url(question)
select "Nashville Champions", from: "Preview in"
within ".preview-panel" do
assert_text "Nashville Champions"
end
end
end
Add link to Champion Admin sidebar:
<%# app/views/layouts/_champion_admin_sidebar.html.erb %>
<nav>
<!-- Existing links -->
<%= link_to champions_signups_path, class: nav_link_class('signups') do %>
Champion Signups
<% end %>
<%= link_to champions_verification_index_path, class: nav_link_class('verification') do %>
Verification Queue
<% end %>
<%= link_to champions_communities_path, class: nav_link_class('communities') do %>
Communities
<% end %>
<!-- NEW -->
<%= link_to champions_seeded_questions_path, class: nav_link_class('seeded_questions') do %>
Seeded Questions
<% end %>
</nav>
To be updated after implementation
| Item | Reason | Future Phase |
|---|---|---|
| Bulk import from CSV | Complexity | Post-MVP |
| Question templates library | Nice-to-have | Post-MVP |
| A/B testing framework | Out of scope | Post-MVP |
| Question cloning | Nice-to-have | Post-MVP |
Document created: January 2026