alumni_lookup

Sub-Phase 1.11: Admin Events

Champion Portal Development Sub-Phase 1.11

Estimated Effort: 1–2 weeks
Focus: Admin-managed events for Champion Portal (parallel to News)

Prerequisites: Phase 1.10 (Community News) complete or near-complete

Related Documents:


✅ Completion Summary (January 2026)

Status: COMPLETE

What Was Implemented

Database & Models:

Admin Interface (Lookup Portal):

Champion Portal:

Tests:

Key Design Decision:

What Was Deferred

Files Created

| File | Purpose | |——|———| | db/migrate/20260107_create_cp_events.rb | Events table | | db/migrate/20260107_create_cp_event_districts.rb | District targeting | | app/models/cp/event.rb | Event model | | app/models/cp/event_district.rb | Join model | | app/controllers/champions/events_controller.rb | Admin CRUD | | app/views/champions/events/ | Admin views (index, new, edit, _form) | | app/controllers/cp/events_controller.rb | CP controller | | app/views/cp/events/ | CP views (index, show, _event_card) | | app/javascript/controllers/event_form_controller.js | Form interactions | | app/javascript/controllers/event_scope_controller.js | Scope toggle | | app/javascript/controllers/event_districts_controller.js | District picker | | test/models/cp/event_test.rb | Model tests (37) | | test/models/cp/event_district_test.rb | Join model tests (4) | | test/controllers/cp/events_controller_test.rb | Controller tests (19) | | test/fixtures/cp/events.yml | Event fixtures (7) | | test/fixtures/cp/event_districts.yml | District fixtures (2) |

Files Modified

| File | Change | |——|——–| | config/routes.rb | Added admin + CP event routes | | app/models/cp/activity_event.rb | Added event_viewed, event_rsvp_clicked types | | app/views/layouts/champions/_header.html.erb | Added Events nav link |


Table of Contents

  1. Overview
  2. Why This Sub-Phase Exists
  3. Design Decisions
  4. Internal Sub-Phases
  5. Schema Design
  6. UI Mockups
  7. Admin Architecture
  8. Scope
  9. Definition of Success
  10. Tests to Create
  11. Documentation Updates
  12. Questions Resolved

1. Overview

Phase 1.11 introduces admin-managed events to the Champion Portal, providing a structured way to announce upcoming events to Champions. This parallels the News system from Phase 1.10 but is purpose-built for time-bound events with dates, times, and venues.

After Phase 1.11, Champions will be able to:

After Phase 1.11, Admins will be able to:


2. Why This Sub-Phase Exists

The “What’s Happening?” Gap

Champions need to know about events relevant to them — university-wide events, regional gatherings, community-specific meetups. Currently there’s no structured way to surface this information within the Champion Portal.

Why Events Are Separate from News

Aspect News Posts Events
Time-bound? No — evergreen content Yes — has start date/time
Has venue? No Yes — physical or virtual location
Has RSVP? No Future (external link for now)
Past content Remains visible Auto-archives after event date
Sort order Published date Event date (soonest first)

From JOBS-TO-BE-DONE.md:

Job C5: Stay in the Loop

“When I’m busy with life but want to stay connected to Belmont, I want to see what’s happening without active effort, so I can feel part of the community even when I’m not contributing.”

Events directly address this job by surfacing “what’s happening” proactively.


3. Design Decisions

Decisions made during planning interview:

Decision Choice Rationale
Admin-only creation Yes — no Champion submissions in 1.11 Keep MVP simple; Champion-submitted events are Phase 2
RSVP functionality External link only (no built-in RSVP) Reduces scope; universities already have event systems
Community targeting community_id FK, null = global Simpler than polymorphic; “global if null” pattern
Past event handling Hide from default views, show in “Past Events” tab Keep main view fresh; preserve history
Event check-in integration Out of scope — separate future phase Different workflow, different use case
Slug generation Auto-generate from title with admin override Consistent with community slugs

4. Internal Sub-Phases

Sub-Phase Name Est. Time
1.11.1 Database & Models 1–2 days
1.11.2 Admin CRUD (Lookup Portal) 2–3 days
1.11.3 Champion Portal Events Page 1–2 days
1.11.4 Dashboard Widget 1 day
1.11.5 Testing & Polish 1 day

5. Schema Design

cp_events Table

create_table :cp_events do |t|
  # Content
  t.string :title, null: false
  t.string :slug, null: false
  t.text :description                    # Rich text (Action Text)
  t.text :short_description              # Plain text for cards (150 chars)
  
  # Event details
  t.datetime :starts_at, null: false
  t.datetime :ends_at                    # Optional end time
  t.string :timezone, default: 'America/Chicago'
  t.string :venue_name                   # "Belmont University" or "Virtual"
  t.string :venue_address                # Full address or empty for virtual
  t.boolean :is_virtual, default: false
  t.string :virtual_url                  # Zoom/Teams link for virtual events
  
  # External RSVP (no built-in RSVP in 1.11)
  t.string :rsvp_url                     # Link to external RSVP system
  t.string :rsvp_button_text, default: 'RSVP'  # "Register", "Get Tickets", etc.
  
  # Targeting
  t.references :community, foreign_key: { to_table: :cp_communities }, null: true
  # null = global (all Champions), set = specific community only
  
  # Publishing
  t.integer :status, default: 0          # draft, published, archived
  t.datetime :published_at
  
  # Metadata
  t.references :created_by, foreign_key: { to_table: :users }
  t.timestamps
  
  t.index :slug, unique: true
  t.index :starts_at
  t.index [:status, :starts_at]
  t.index :community_id
end

Model: Cp::Event

# app/models/cp/event.rb
module Cp
  class Event < ApplicationRecord
    self.table_name = 'cp_events'
    
    # Associations
    belongs_to :community, class_name: 'Cp::Community', optional: true
    belongs_to :created_by, class_name: 'User'
    has_rich_text :description
    has_one_attached :cover_image
    
    # Enums
    enum :status, { draft: 0, published: 1, archived: 2 }
    
    # Validations
    validates :title, presence: true, length: { maximum: 200 }
    validates :slug, presence: true, uniqueness: true, format: { with: /\A[a-z0-9-]+\z/ }
    validates :starts_at, presence: true
    validates :short_description, length: { maximum: 150 }
    validates :rsvp_url, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]), allow_blank: true }
    validate :ends_at_after_starts_at, if: -> { ends_at.present? }
    
    # Scopes
    scope :published, -> { where(status: :published) }
    scope :upcoming, -> { where('starts_at >= ?', Time.current) }
    scope :past, -> { where('starts_at < ?', Time.current) }
    scope :global, -> { where(community_id: nil) }
    scope :for_community, ->(community) { where(community: community).or(global) }
    scope :visible_to, ->(champion) {
      community_ids = champion.community_ids
      where(community_id: [nil] + community_ids)
    }
    scope :chronological, -> { order(starts_at: :asc) }
    scope :reverse_chronological, -> { order(starts_at: :desc) }
    
    # Callbacks
    before_validation :generate_slug, on: :create, if: -> { slug.blank? }
    
    # Instance methods
    def global?
      community_id.nil?
    end
    
    def upcoming?
      starts_at >= Time.current
    end
    
    def past?
      starts_at < Time.current
    end
    
    def formatted_date
      if ends_at.present? && ends_at.to_date != starts_at.to_date
        "#{starts_at.strftime('%B %d')}#{ends_at.strftime('%B %d, %Y')}"
      else
        starts_at.strftime('%B %d, %Y')
      end
    end
    
    def formatted_time
      if ends_at.present?
        "#{starts_at.strftime('%l:%M %p')}#{ends_at.strftime('%l:%M %p %Z')}"
      else
        starts_at.strftime('%l:%M %p %Z')
      end
    end
    
    private
    
    def generate_slug
      base_slug = title.to_s.parameterize
      self.slug = base_slug
      
      # Ensure uniqueness
      counter = 1
      while Cp::Event.exists?(slug: slug)
        self.slug = "#{base_slug}-#{counter}"
        counter += 1
      end
    end
    
    def ends_at_after_starts_at
      errors.add(:ends_at, 'must be after start time') if ends_at <= starts_at
    end
  end
end

6. UI Mockups

Dashboard Events Widget (Champion Portal)

┌─────────────────────────────────────────────────────────────┐
│ 📅 Upcoming Events                              View All →  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ JAN  Nashville Alumni Mixer                             │ │
│ │ 15   Belmont University • 6:00 PM                       │ │
│ │      [RSVP →]                                           │ │
│ └─────────────────────────────────────────────────────────┘ │
│                                                             │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ JAN  Virtual Career Workshop                            │ │
│ │ 22   Online • 12:00 PM                                  │ │
│ │      [Register →]                                       │ │
│ └─────────────────────────────────────────────────────────┘ │
│                                                             │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ FEB  Atlanta Chapter Meetup                             │ │
│ │ 3    Piedmont Park • 2:00 PM                            │ │
│ │      [Learn More →]                                     │ │
│ └─────────────────────────────────────────────────────────┘ │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Events Index Page (Champion Portal)

┌─────────────────────────────────────────────────────────────────────────┐
│ Events                                                                  │
│ Discover what's happening in your Belmont community                     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│ [Upcoming ▼] [All Communities ▼]                    🔍 Search events    │
│                                                                         │
│ ─────────────────────────────────────────────────────────────────────   │
│                                                                         │
│ ┌───────────────────────────────────────────────────────────────────┐   │
│ │ ┌─────────┐                                                       │   │
│ │ │  COVER  │  Nashville Alumni Mixer                               │   │
│ │ │  IMAGE  │  📅 January 15, 2026 • 6:00 PM                        │   │
│ │ │         │  📍 Belmont University, Nashville                     │   │
│ │ └─────────┘                                                       │   │
│ │                                                                   │   │
│ │  Join fellow Bruins for an evening of networking and fun at the  │   │
│ │  annual Nashville Alumni Mixer. Heavy appetizers provided.        │   │
│ │                                                                   │   │
│ │  🏷️ Nashville District                        [RSVP →]           │   │
│ └───────────────────────────────────────────────────────────────────┘   │
│                                                                         │
│ ┌───────────────────────────────────────────────────────────────────┐   │
│ │ ┌─────────┐                                                       │   │
│ │ │  COVER  │  Virtual Career Workshop: Tech Industry               │   │
│ │ │  IMAGE  │  📅 January 22, 2026 • 12:00 PM                       │   │
│ │ │         │  💻 Virtual Event                                     │   │
│ │ └─────────┘                                                       │   │
│ │                                                                   │   │
│ │  Learn from Belmont alumni working in tech. Panel discussion      │   │
│ │  followed by Q&A and networking breakout rooms.                   │   │
│ │                                                                   │   │
│ │  🌐 All Champions                             [Register →]        │   │
│ └───────────────────────────────────────────────────────────────────┘   │
│                                                                         │
│ ─────────────────────────────────────────────────────────────────────   │
│                                                                         │
│ [Past Events]                                                           │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Admin Events List (Lookup Portal)

┌─────────────────────────────────────────────────────────────────────────┐
│ Champion Portal › Events                                    [+ New Event]│
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│ [All ▼] [All Communities ▼]                        🔍 Search...         │
│                                                                         │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Title              │ Date       │ Target     │ Status  │ Actions    │ │
│ ├─────────────────────────────────────────────────────────────────────┤ │
│ │ Nashville Mixer    │ Jan 15     │ Nashville  │ ✅ Pub  │ Edit │ Del │ │
│ │ Career Workshop    │ Jan 22     │ Global     │ ✅ Pub  │ Edit │ Del │ │
│ │ Atlanta Meetup     │ Feb 3      │ Atlanta    │ 📝 Draft│ Edit │ Del │ │
│ │ Spring Reunion     │ Mar 20     │ Global     │ 📝 Draft│ Edit │ Del │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│                                                                         │
│ Showing 4 events                                                        │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

7. Admin Architecture

Location Split

Feature Location Why
Event CRUD Lookup Portal (/settings/champion_events) Admins manage from central admin area
Events display Champion Portal (/events, Dashboard widget) Champions view in their portal

Authorization

Action Required Role
View events (Champion Portal) Any verified Champion
Create/Edit/Delete events portal_admin or admin

Routes

# Lookup Portal (config/routes.rb - main domain)
namespace :settings do
  resources :champion_events, except: [:show]
end

# Champion Portal (config/routes.rb - champions subdomain)
constraints(SubdomainConstraint.new('champions')) do
  scope module: 'cp' do
    resources :events, only: [:index, :show]
  end
end

8. Scope

In Scope

Feature Description
cp_events table Full schema as defined above
Cp::Event model Validations, scopes, methods
Admin CRUD Create, edit, delete events in Lookup Portal
Events index /events page in Champion Portal
Event show /events/:slug detail page
Dashboard widget “Upcoming Events” showing next 3 events
Community targeting Events targeted to specific communities or global
Cover images Optional cover image upload
External RSVP Link to external registration system

Out of Scope (Deferred)

Feature Reason Target Phase
Built-in RSVP Complexity; external systems exist Backlog
Champion-submitted events Requires approval workflow Phase 2
Event check-in integration Different use case, separate workflow Future
Recurring events Complexity Backlog
Calendar export (ICS) Nice-to-have Backlog
Event reminders Requires notification system Phase 2+

9. Definition of Success

Functional Acceptance Criteria

Quality Criteria

Success Metrics

Metric Target
Events created in first month 5+
Champion event page views 50+ per week
RSVP link click-through 20%+ of event views

10. Tests to Create

Model Tests

# test/models/cp/event_test.rb
class Cp::EventTest < ActiveSupport::TestCase
  test "validates title presence"
  test "validates starts_at presence"
  test "validates slug uniqueness"
  test "validates ends_at after starts_at"
  test "validates rsvp_url format"
  test "generates slug from title"
  test "scope upcoming returns future events"
  test "scope past returns past events"
  test "scope visible_to includes global and community events"
  test "global? returns true when community_id nil"
end

Controller Tests

# test/controllers/settings/champion_events_controller_test.rb
class Settings::ChampionEventsControllerTest < ActionDispatch::IntegrationTest
  test "index requires portal_admin"
  test "index lists all events"
  test "create creates event with valid params"
  test "create fails with invalid params"
  test "update updates event"
  test "destroy deletes event"
end

# test/controllers/cp/events_controller_test.rb
class Cp::EventsControllerTest < ActionDispatch::IntegrationTest
  test "index requires verified champion"
  test "index shows upcoming events"
  test "index filters by community"
  test "show displays event details"
  test "show renders 404 for draft events"
end

11. Documentation Updates

After completing Phase 1.11:


12. Questions Resolved

Question Answer Date
Should events have built-in RSVP? No — external link only for MVP Jan 7, 2026
How to handle past events? Hide from default, show in “Past Events” tab Jan 7, 2026
Event check-in integration? Separate future phase Jan 7, 2026
Community targeting pattern? community_id FK, null = global Jan 7, 2026
Slug generation? Auto-generate with admin override Jan 7, 2026

Files to Create

New Files

Files to Modify