alumni_lookup

Phase 14 — Alumni Content Submissions

Canonical sources: Portal philosophy, posture, and language live in /docs/planning/champion-portal/source/README.md. Use these sources (including CHRIST_CENTERED__IDENTITY_STATEMENT.md) when writing specs or user-facing copy. Prefer quoting/paraphrasing over inventing new language.

Status: ✅ Complete Completed: February 2026 Estimated Effort: 3–4 weeks Prerequisites: Phase 11 Complete (News, Events fully functional)


Completion Summary

Test Results: 3,582 runs, 9,178 assertions, 0 failures, 0 errors

What Was Implemented

All 4 sub-phases delivered as specified:

14.1 — Data Model & Backend Foundation

14.2 — Champion Submission Forms & UX

14.3 — Admin Submissions Queue & Review

14.4 — Notifications & Attribution

Cross-View Polish

Key Files Created/Modified

Models: Cp::ContentSubmissionThread, Cp::ContentSubmissionMessage (+ additions to Cp::NewsPost, Cp::Event, Cp::Champion, Cp::Notification) Controllers: Cp::NewsSubmissionsController, Cp::EventSubmissionsController, Cp::SubmissionsController, Champions::ContentSubmissionsController Mailer: Cp::ContentSubmissionMailer (6 templates) Views: 15+ new/modified view files across champion portal and admin Tests: 50+ new tests across models, controllers, and mailer

Spec Deviations

None — all sub-phases implemented as specified.



Interview Decisions (Resolved)

# Question Decision Rationale
1 Status enum value for submitted submitted: -1 Negative values mean status < 0 = “not real content” — simple guard for all public queries
2 Decline handling Add declined: -2 as a distinct status Keeps declined items cleanly separated from drafts. Same status < 0 guard excludes both submitted and declined from public views
3 Sub-phase execution Combine sub-phases where sensible No artificial boundaries — build what makes sense together
4 Timeline No constraints Work through all sub-phases as capacity allows
5 Community dropdown scope Champion’s own communities only + “General” Champions should not submit to communities they don’t belong to

Overview

Currently, all content (news, events, photos) is created exclusively by admins. As the portal opens to a broader alumni audience, we need a way for any authenticated champion to suggest content — a news story they’ve heard about, an event they know is coming, etc.

This phase introduces a content submission pipeline: alumni submit via lightweight forms, admins review in a dedicated queue, and submissions can be promoted to real published content with full attribution to the original submitter.

Why This Matters

The long-term content strategy has three levels:

  1. Admin-generated content ✅ Complete
  2. Community Leader-generated content (future — requires expanded CL tools)
  3. Alumni-submitted content with admin moderationThis phase

Rather than building the full CL content creation suite (Level 2), this phase establishes Level 3 as a stepping stone. Any champion — including Community Leaders — can submit content ideas. This creates an immediate feedback loop (“What do alumni care about?”) while keeping admin control over published content quality.

The Submission Flow

Champion sees CTA → Fills form → Submission created (status: submitted)
    ↓
Admin notified → Reviews in Submissions Queue → Conversation thread if needed
    ↓
Admin promotes to draft → Edits as needed → Publishes with submitter attribution
    OR
Admin declines with reason → Champion notified

Design Principles

  1. Low friction for submitters — Minimal text-based forms; don’t make alumni do admin’s job
  2. Full control for admins — Nothing publishes without explicit admin action
  3. Conversational follow-up — Built-in messaging thread so admin can ask clarifying questions
  4. Attribution — Original submitters get credit when their content is published
  5. Community awareness — Submissions can be scoped to specific communities

Sub-Phase Progress

Sub-Phase Name Status Effort
14.1 Data Model & Backend Foundation ⬜ Not started Medium
14.2 Champion Submission Forms & UX ⬜ Not started Medium
14.3 Admin Submissions Queue & Review ⬜ Not started Medium-High
14.4 Notifications & Attribution ⬜ Not started Medium

Sub-Phase 14.1: Data Model & Backend Foundation

Scope

Establish the data model for content submissions and the messaging thread system that supports admin-champion conversation about submissions.

Key Decisions

Data Model Changes

Modified: cp_news_posts.status enum

enum :status, {
  declined: -2,   # NEW — admin declined the submission
  submitted: -1,  # NEW — alumni submission awaiting review
  draft: 0,
  published: 1,
  archived: 2
}, prefix: true

Modified: cp_events.status enum

enum :status, {
  declined: -2,   # NEW — admin declined the submission
  submitted: -1,  # NEW — alumni submission awaiting review
  draft: 0,
  published: 1,
  archived: 2
}, prefix: true

Key insight: All public-facing queries can use where('status >= 0') or rely on existing named scopes (published, draft) which already filter to status >= 0 values. Both submitted (-1) and declined (-2) are invisible to champions by default.

New: cp_news_posts fields

t.bigint  :submitted_by_champion_id  # FK to cp_champions — who submitted this
t.text    :decline_reason             # If admin declines the submission
t.datetime :submitted_at              # When the submission was made
t.datetime :declined_at               # When the submission was declined

New: cp_events fields

t.bigint  :submitted_by_champion_id  # FK to cp_champions — who submitted this
t.text    :decline_reason             # If admin declines the submission
t.datetime :submitted_at              # When the submission was made
t.datetime :declined_at               # When the submission was declined

New table: cp_content_submission_threads

create_table :cp_content_submission_threads do |t|
  t.string   :content_type, null: false     # "news_post" or "event"
  t.bigint   :content_id, null: false       # FK to cp_news_posts or cp_events
  t.bigint   :cp_champion_id, null: false   # The submitting champion
  t.integer  :status, default: 0, null: false  # open(0), resolved(1)
  t.timestamps
end
add_index :cp_content_submission_threads, [:content_type, :content_id], unique: true
add_index :cp_content_submission_threads, :cp_champion_id

New table: cp_content_submission_messages

create_table :cp_content_submission_messages do |t|
  t.bigint   :cp_content_submission_thread_id, null: false
  t.string   :sender_type, null: false      # "Cp::Champion" or "User"
  t.bigint   :sender_id, null: false
  t.text     :body, null: false
  t.datetime :read_at                       # For read tracking
  t.timestamps
end
add_index :cp_content_submission_messages, :cp_content_submission_thread_id

Model Changes

Cp::NewsPost additions

belongs_to :submitted_by_champion, class_name: "Cp::Champion", optional: true

scope :submitted, -> { where(status: :submitted) }
scope :declined, -> { where(status: :declined) }
scope :pending_review, -> { submitted }  # Submitted = awaiting first admin action
scope :real_content, -> { where('status >= 0') }  # Excludes submitted & declined

def submitted_content?
  submitted_by_champion_id.present?
end

def decline!(reason:)
  update!(status: :declined, decline_reason: reason, declined_at: Time.current)
end

def promote_to_draft!
  update!(status: :draft)
end

Cp::Event additions

Same pattern as NewsPost.

New: Cp::ContentSubmissionThread

belongs_to :champion, class_name: "Cp::Champion"
has_many :messages, class_name: "Cp::ContentSubmissionMessage", dependent: :destroy

enum :status, { open: 0, resolved: 1 }, prefix: true

belongs_to :content, polymorphic: true  # Using content_type + content_id

scope :needs_response, -> { ... }  # Last message from champion
scope :for_content, ->(type, id) { where(content_type: type, content_id: id) }

New: Cp::ContentSubmissionMessage

belongs_to :thread, class_name: "Cp::ContentSubmissionThread",
           foreign_key: :cp_content_submission_thread_id
belongs_to :sender, polymorphic: true  # Cp::Champion or User

scope :unread, -> { where(read_at: nil) }
scope :from_champion, -> { where(sender_type: "Cp::Champion") }
scope :from_staff, -> { where(sender_type: "User") }

Acceptance Criteria


Sub-Phase 14.2: Champion Submission Forms & UX

Scope

Build the champion-facing submission forms and integrate CTAs into relevant content browsing areas.

CTA Placement

CTAs appear in two locations per content type:

News:

Events:

CTA Design:

News Submission Form

Route: GET /news/submitCp::NewsSubmissionsController#new

Fields: | Field | Type | Required | Notes | |——-|——|———-|——-| | Title | Text input | Yes | “What’s the headline?” | | Summary | Textarea | Yes | “Tell us about it (2-3 sentences)” — maps to excerpt | | Details | Textarea | No | “Any additional details?” — maps to full_content | | Link | URL input | No | “Link to an article or website about this” | | Community | Select dropdown | No | Pre-populated from champion’s own communities only + “All Alumni (General)” option. Pre-selected if community_id param present. |

Event Submission Form

Route: GET /events/submitCp::EventSubmissionsController#new

Fields: | Field | Type | Required | Notes | |——-|——|———-|——-| | Title | Text input | Yes | “What’s the event called?” | | Description | Textarea | Yes | “Tell us about this event” | | Start date/time | Datetime picker | Yes | “When does it start?” | | End date/time | Datetime picker | No | “When does it end?” | | Venue name | Text input | No | “Where is it?” | | Venue address | Text input | No | “Street address” | | Virtual? | Checkbox | No | Toggle for virtual event | | Virtual URL | URL input | No | Shown when virtual checked | | RSVP link | URL input | No | “Link to register or RSVP” | | Community | Select dropdown | No | Same as news form (champion’s own communities only + “General”) |

Submission Flow

  1. Champion fills form and clicks “Submit”
  2. System creates Cp::NewsPost or Cp::Event with:
    • status: :submitted
    • submitted_by_champion_id: current_champion.id
    • submitted_at: Time.current
    • author: current_champion (polymorphic author)
    • Community association if selected
  3. System creates Cp::ContentSubmissionThread linked to the content
  4. System creates initial message on thread: champion’s description (auto-generated from form data)
  5. Redirect to confirmation/thread view page

Submission History

Route: GET /my-submissionsCp::SubmissionsController#index

Champions can see their past submissions with current status:

Each submission links to its conversation thread so the champion can reply to admin questions.

Acceptance Criteria


Sub-Phase 14.3: Admin Submissions Queue & Review

Scope

Build the admin-facing review queue, conversation reply capability, and promote/decline workflows.

Submissions Queue

Route: GET /champions/content_submissionsChampions::ContentSubmissionsController#index

Layout: Dedicated admin page listing all submissions awaiting review.

Tabs/Filters: | Tab | Description | Badge | |—–|————-|——-| | Pending Review | status: :submitted | Count badge | | In Conversation | status: :submitted + thread has admin reply but not yet promoted/declined | Count badge | | Promoted | status: :draft or status: :published with submitted_by_champion_id present (last 30 days) | — | | Declined | status: :declined (last 30 days) | — |

List View Columns:

Submission Detail / Review Page

Route: GET /champions/content_submissions/:idChampions::ContentSubmissionsController#show

Layout:

Actions: | Action | Effect | |——–|——–| | Promote to Draft | Changes status from submitteddraft. Opens the standard content edit form so admin can refine before publishing. | | Publish Directly | Changes status from submittedpublished, sets published_at. For submissions that are ready as-is. | | Decline | Prompts for decline reason. Changes status to declined, stores reason, sets declined_at. Notifies champion. | | Reply | Adds a message to the conversation thread. Notifies champion in-app. |

Conversation Thread

Follows Support Thread pattern:

Promote Flow Detail

When admin clicks “Promote to Draft”:

  1. Status changes from submitted to draft
  2. Admin is redirected to the standard news post / event edit form
  3. Admin can edit title, content, images, communities, etc.
  4. Admin publishes using the normal publish flow
  5. Published content shows “Submitted by [Champion Name]” attribution

Acceptance Criteria


Sub-Phase 14.4: Notifications & Attribution

Scope

Wire up notifications for the complete submission lifecycle and add submitter attribution to published content.

Notification Matrix

Event Recipient Channel Message
New submission Admins + support responders Email + in-app “New [news/event] submission from [Champion Name]”
Admin replies to thread Submitting champion In-app “[Admin Name] replied to your submission”
Champion replies to thread Admins + support responders In-app “[Champion Name] replied on their [news/event] submission”
Submission promoted to draft Submitting champion In-app “Your [news/event] submission is being prepared for publication!”
Submission published Submitting champion In-app + email “Your [news/event] has been published!”
Submission declined Submitting champion In-app “Update on your [news/event] submission” (with reason)

Attribution Display

When a published news post or event was originally submitted by a champion:

Implementation: Use submitted_by_champion_id presence to show attribution. The author field may be changed to the admin who published it (or kept as the champion — admin’s choice during editing).

Email Templates

New submission notification (to admin):

Submission published notification (to champion):

Acceptance Criteria


Technical Notes

Scope Guards for Submitted Content

Both submitted: -1 and declined: -2 must be excluded from all champion-facing content queries. Verify that:

Since both statuses are negative integers and existing scopes filter by status: :published or specific status >= 0 values, this should work automatically. The new real_content scope (where('status >= 0')) provides an explicit guard. Must be verified during implementation.

Author vs. Submitter

Two distinct concepts:

Mobile Considerations

Submission forms should be mobile-friendly:

Future Expansion Path

This architecture supports the eventual CL content creation flow (Level 2):