Champion Portal Development Sub-Phase 1.12
Estimated Effort: 2–3 weeks
Focus: Database schema and models for emergent communitiesPrerequisites: Phase 1.10 (Community News) and Phase 1.11 (Admin Events) complete
Related Documents:
- 1.10-community-news.md — News targeting updates
- 1.11-admin-events.md — Events targeting updates
- ../../JOBS-TO-BE-DONE.md — Job C9: Feel Like I Belong
| Sub-Phase | Name | Status | Notes |
|---|---|---|---|
| 1.12.1 | Database Schema | ✅ Complete | 3 migrations created |
| 1.12.2 | Community & Membership Models | ✅ Complete | Models, validations, associations |
| 1.12.3 | Suggestion Model & Matching Service | ✅ Complete | 70+ tests passing |
| 1.12.4 | Profile Update Hooks | ✅ Complete | Callbacks on Champion and Affinity |
| 1.12.5 | News/Events Migration | ✅ Complete | Community targeting for news/events |
| 1.12.6 | Admin Communities UI | ✅ Complete | Lookup Portal admin page |
| 1.12.7 | Champion Community UI | ✅ Complete | My Communities, suggestions, join/leave |
Database:
cp_communities table with 6 community types (district, college, major, affinity, industry, custom)cp_champion_communities join table with counter cache for member countscp_community_suggestions table for suggestion-based modelModels:
Cp::Community with type-specific validations, scopes, factory methodsCp::ChampionCommunity with counter cache pattern for member countsCp::CommunitySuggestion with accept!/decline! methodsServices:
Cp::CommunityMatchingService for profile-based community suggestionsCallbacks:
Cp::Champion#after_update triggers community matching when district_id, buid, or industry changesCp::Affinity#after_create and #after_destroy trigger matching when affinities changeTests Created:
test/models/cp/community_test.rb (26 tests)test/models/cp/champion_community_test.rb (18 tests)test/models/cp/community_suggestion_test.rb (17 tests)test/services/cp/community_matching_service_test.rb (12 tests)champion_test.rb (8 tests) and affinity_test.rb (3 tests)Fixtures:
test/fixtures/cp/communities.yml (8 communities)test/fixtures/cp/champion_communities.yml (3 memberships)test/fixtures/cp/community_suggestions.yml (2 suggestions)1.12.5 - News/Events Community Targeting:
community_id to cp_posts and cp_events tables1.12.6 - Admin Communities UI:
/champion_admin/communities1.12.7 - Champion Community UI:
/communitiesController Files:
app/controllers/champions/communities_controller.rb (Admin)app/controllers/cp/communities_controller.rb (Champion)app/controllers/cp/community_suggestions_controller.rbView Files:
app/views/champions/communities/ (Admin: index, show, edit, export.csv)app/views/cp/communities/index.html.erbapp/views/cp/communities/show.html.erbTest Files:
test/controllers/champions/communities_controller_test.rb (15 tests)test/controllers/cp/communities_controller_test.rb (16 tests)test/controllers/cp/community_suggestions_controller_test.rb (13 tests)Bootstrap Rake Tasks:
rake communities:bootstrap — Creates district/college communities onlyrake communities:generate_suggestions — Creates suggestions for verified Championsrake communities:update_counts — Recalculates member counts and activationrake communities:full_bootstrap CONFIRM=1 — Runs all threerake communities:stats — Shows community statisticsKey Design Decisions (Final):
activated_at column tracks first activation — deactivated communities stay visible in adminPhase 1.12 establishes the community foundation — the database schema and business logic that enables the “emergent community” model where communities form organically when enough Champions share an attribute.
The Core Insight:
Not all Champions fit neatly into metro districts. The Champion Portal must serve:
| Alumni Bucket | Challenge | Community Solution |
|---|---|---|
| Metro expats | Far from Nashville but in major city | District community (Nashville, Atlanta, etc.) |
| Sparse district alumni | Only person in their city | College, Major, or Affinity communities connect them |
| Nashville locals | Many nearby, need more specific belonging | College, Major, Affinity communities add specificity |
After Phase 1.12:
From the planning conversation:
“One of our main tenants is the idea that Alumni find a place of belonging — a place ‘for them.’”
Districts alone don’t create belonging for everyone. A Champion who is the only Bruin in Boise needs a different kind of community — perhaps connecting through their college (Entertainment & Music Business) or affinity (Greek life, athletics).
Communities aren’t pre-created for every possible combination. Instead:
Job C9: Feel Like I Belong
“When I’m far from Nashville or years past graduation, I want to feel part of something bigger, so I can maintain my Belmont identity as part of who I am.”
Communities are the mechanism for delivering belonging at scale.
Decisions made during planning interview:
| Decision | Choice | Rationale |
|---|---|---|
| Community threshold | 3 Champions minimum | Balance between too exclusive (5+) and too fragmented (2) |
| Community types | district, college, major, affinity, industry | Cover the main ways alumni connect |
| Slug generation | Auto-generate with admin override | Consistent, clean URLs with flexibility |
| Auto-assignment | On Champion verification | Ensures communities are populated |
| News/Events targeting | community_id FK, null = global |
Simple “global if null” pattern |
| Multiple communities per Champion | Yes — Champions can belong to many | A Champion might be in Nashville District + Music Business College + Phi Mu Affinity |
| Community visibility | Public (all can join) for MVP | Private/Invite-only deferred to future |
Note: These items are recorded for future planning. They are NOT in scope for Phase 1.12 MVP.
Note: As of Phase 1.15.3, CLC now means “Community Leader Council” rather than “City Leadership Council” throughout the codebase.
Idea: Instead of associating CLCs with Districts/Regions only, CLCs could become general community leaders associated with any Community type.
Benefits:
Admin Requirement: Identify which communities do NOT have at least one CLC assigned, enabling targeted recruitment.
Target Phase: Phase 2+ (after Community Foundation proves out)
Idea: Allow communities to have different visibility/join settings:
| Setting | Behavior |
|---|---|
| Public | Anyone can see and join (default) |
| Invite-Only | Visible in browse, but requires invite or request-to-join |
| Private | Hidden from browse, invite-only |
Use Cases:
MVP Decision: All communities are Public for Phase 1.12-1.14. Visibility settings can be added in Phase 2+.
| Sub-Phase | Name | Est. Time | Status |
|---|---|---|---|
| 1.12.1 | Database Schema | 2–3 days | ✅ Complete |
| 1.12.2 | Community & Membership Models | 2–3 days | ✅ Complete |
| 1.12.3 | Suggestion Model & Matching Service | 2–3 days | ✅ Complete |
| 1.12.4 | Profile Update Hooks | 1 day | ✅ Complete |
| 1.12.5 | News/Events Migration | 1–2 days | ✅ Complete |
| 1.12.6 | Admin Communities UI | 2–3 days | ✅ Complete |
| 1.12.7 | Champion Community UI | 2–3 days | ✅ Complete |
Note: Original spec had different sub-phase breakdown. Updated to reflect actual implementation which includes suggestion-based model and profile hooks.
cp_communities Tablecreate_table :cp_communities do |t|
# Identity
t.string :name, null: false # Display name: "Nashville", "Music Business"
t.string :slug, null: false # URL slug: "nashville", "music-business"
t.text :description # Optional description for landing page
# Type & Source
t.integer :community_type, null: false # district, college, major, affinity, industry
# Source reference (polymorphic-ish but simpler)
# Only ONE of these will be set, based on community_type
t.references :district, foreign_key: true, null: true
t.string :college_code, null: true # References colleges.college_code
t.string :major_code, null: true # References majors.major_code
t.references :affinity, foreign_key: true, null: true
t.string :industry, null: true # String value for industry communities
# Thresholds & Status
t.integer :member_count, default: 0 # Cached count for performance
t.integer :threshold, default: 3 # Min members to be "active"
t.boolean :active, default: false # True when member_count >= threshold
t.boolean :featured, default: false # Show prominently in discovery
# Customization (admin override)
t.string :custom_name # Override auto-generated name
t.text :custom_description # Override default description
# Metadata
t.timestamps
t.index :slug, unique: true
t.index :community_type
t.index [:community_type, :active]
t.index :district_id
t.index :college_code
t.index :major_code
t.index :affinity_id
t.index :industry
end
cp_champion_communities Table (Join Table)create_table :cp_champion_communities do |t|
t.references :champion, null: false, foreign_key: { to_table: :cp_champions }
t.references :community, null: false, foreign_key: { to_table: :cp_communities }
# Membership metadata
t.boolean :primary, default: false # Champion's primary community of this type
t.datetime :joined_at, default: -> { 'CURRENT_TIMESTAMP' }
t.timestamps
t.index [:champion_id, :community_id], unique: true
t.index [:community_id, :champion_id]
end
# Add community_id to cp_news_posts (if not already present from 1.10)
add_reference :cp_news_posts, :community, foreign_key: { to_table: :cp_communities }, null: true
# cp_events already has community_id from Phase 1.11
# app/models/cp/community.rb
enum :community_type, {
district: 0, # Geographic — tied to District model
college: 1, # Academic — tied to College (via college_code)
major: 2, # Academic — tied to Major (via major_code)
affinity: 3, # Interest — tied to Affinity model
industry: 4 # Professional — based on Champion's industry field
}
| Type | Source | Name Generation | Example |
|---|---|---|---|
| district | districts table |
district.name |
“Nashville”, “Atlanta” |
| college | colleges table |
college.college_name |
“College of Entertainment & Music Business” |
| major | majors table |
major.major_desc |
“Music Business” |
| affinity | affinities table |
affinity.name |
“Phi Mu”, “SAAC” |
| industry | cp_champions.industry |
Industry value | “Music Industry”, “Healthcare” |
| Community Type | Champion Attribute Used |
|---|---|
| district | champion.district_id |
| college | champion.alumni.degrees.*.major.college_code |
| major | champion.alumni.degrees.*.major_code |
| affinity | champion.alumni.affinities.*.id |
| industry | champion.industry |
A community becomes active when it has 3 or more members. This is tracked via:
member_count — Cached integer updated on join/leaveactive — Boolean set to true when member_count >= threshold| Threshold | Pros | Cons |
|---|---|---|
| 2 | More communities exist | Fragmented, conversations die |
| 3 | Balance — enough for conversation, not too exclusive | — |
| 5 | Higher quality communities | Many Champions left out |
# In Cp::ChampionCommunity (join model)
after_create :increment_community_count
after_destroy :decrement_community_count
private
def increment_community_count
community.with_lock do
community.increment!(:member_count)
community.update!(active: true) if community.member_count >= community.threshold
end
end
def decrement_community_count
community.with_lock do
community.decrement!(:member_count)
community.update!(active: false) if community.member_count < community.threshold
end
end
| Feature | Description |
|---|---|
cp_communities table |
Full schema with all community types |
cp_champion_communities table |
Join table with membership metadata |
Cp::Community model |
Validations, scopes, type-specific logic |
Cp::ChampionCommunity model |
Join model with counter cache |
| Auto-assignment service | Assign Champions to communities on verification |
| Member count tracking | Counter cache pattern |
| Active/inactive status | Threshold-based activation |
| Admin community list | View all communities in Lookup Portal |
| Update News targeting | Add community_id FK to news posts |
| Update Events targeting | Verify community_id FK works |
| Feature | Reason | Target Phase |
|---|---|---|
| Community landing pages | Separate UI phase | Phase 1.13 |
| Dynamic community creation | Separate logic phase | Phase 1.14 |
| Community discovery UI | Requires landing pages first | Phase 1.13 |
| Champion community preferences | Nice-to-have | Backlog |
| Community moderation | Future need | Phase 2+ |
cp_champion_communities count# test/models/cp/community_test.rb
class Cp::CommunityTest < ActiveSupport::TestCase
test "validates name presence"
test "validates slug uniqueness"
test "validates community_type presence"
test "generates slug from name"
test "district community requires district_id"
test "college community requires college_code"
test "active? returns true when member_count >= threshold"
test "scope active returns only active communities"
test "scope by_type filters by community_type"
end
# test/models/cp/champion_community_test.rb
class Cp::ChampionCommunityTest < ActiveSupport::TestCase
test "validates uniqueness of champion + community"
test "increments community member_count on create"
test "decrements community member_count on destroy"
test "activates community when threshold reached"
test "deactivates community when below threshold"
end
# test/services/cp/community_assignment_service_test.rb
class Cp::CommunityAssignmentServiceTest < ActiveSupport::TestCase
test "assigns champion to district community"
test "assigns champion to college communities"
test "assigns champion to major communities"
test "assigns champion to affinity communities"
test "assigns champion to industry community"
test "creates community if it doesn't exist"
test "does not duplicate existing memberships"
test "handles champion with no affinities"
test "handles champion with multiple degrees"
end
# test/controllers/settings/champion_communities_controller_test.rb
class Settings::ChampionCommunitiesControllerTest < ActionDispatch::IntegrationTest
test "index requires portal_admin"
test "index lists all communities with counts"
test "index filters by type"
test "index filters by active status"
end
After completing Phase 1.12:
app/controllers/champions/roadmap_controller.rb statusdocs/features/champion_portal/| Question | Answer | Date |
|---|---|---|
| Community threshold? | 3 Champions minimum | Jan 7, 2026 |
| How many community types? | 5: district, college, major, affinity, industry | Jan 7, 2026 |
| Slug generation? | Auto-generate with admin override | Jan 7, 2026 |
| When to assign Champions? | On verification | Jan 7, 2026 |
| News/Events targeting pattern? | community_id FK, null = global |
Jan 7, 2026 |
| Can Champions be in multiple communities? | Yes — one per type minimum, more if qualified | Jan 7, 2026 |
db/migrate/XXXXXX_create_cp_communities.rbdb/migrate/XXXXXX_create_cp_champion_communities.rbdb/migrate/XXXXXX_add_community_id_to_cp_news_posts.rb (if not done in 1.10)app/models/cp/community.rbapp/models/cp/champion_community.rbapp/services/cp/community_assignment_service.rbapp/controllers/settings/champion_communities_controller.rbapp/views/settings/champion_communities/index.html.erbtest/models/cp/community_test.rbtest/models/cp/champion_community_test.rbtest/services/cp/community_assignment_service_test.rbtest/controllers/settings/champion_communities_controller_test.rbtest/fixtures/cp/communities.ymltest/fixtures/cp/champion_communities.ymlapp/models/cp/champion.rb — Add has_many :champion_communities and has_many :communitiesapp/models/cp/news_post.rb — Add belongs_to :community, optional: trueapp/models/cp/event.rb — Verify belongs_to :community, optional: trueconfig/routes.rb — Add admin community routesapp/services/cp/champion_verification_service.rb — Trigger community assignmentChampion Verified
│
▼
┌──────────────────────────────┐
│ CommunityAssignmentService │
│ .assign_all(champion) │
└──────────────────────────────┘
│
├─► District Community
│ └─ Based on champion.district_id
│
├─► College Communities (1 per degree)
│ └─ Based on champion.alumni.degrees.*.major.college_code
│
├─► Major Communities (1 per degree)
│ └─ Based on champion.alumni.degrees.*.major_code
│
├─► Affinity Communities (0 to many)
│ └─ Based on champion.alumni.affinities
│
└─► Industry Community (0 or 1)
└─ Based on champion.industry
For each assignment:
ChampionCommunity join record (skip if exists)