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
10 changes: 8 additions & 2 deletions app/controllers/categories_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def update
authorize! @category
respond_to do |format|
if @category.update(category_params)
format.html { redirect_to @category, notice: "Category was successfully updated.", status: :see_other }
format.html { redirect_to after_update_path, notice: "Category was successfully updated.", status: :see_other }
format.json { head :ok }
else
format.html do
Expand Down Expand Up @@ -89,6 +89,12 @@ def dedupe_config
}
end

def after_update_path
return story_shares_path(category_names_all: @category.name) if params[:return_to] == "story_share"

@category
end

def set_category
@category = Category.find(params[:id])
end
Expand All @@ -97,7 +103,7 @@ def set_category
def category_params
if params[:category]
params.require(:category).permit(
:name, :category_type_id, :published, :position, :description, :story_share_position
:name, :category_type_id, :published, :position, :description, :story_share_position, :rhino_story_share_intro
)
else
params.permit(:position)
Expand Down
10 changes: 8 additions & 2 deletions app/controllers/sectors_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def create
def update
authorize! @sector
if @sector.update(sector_params)
redirect_to @sector, notice: "Sector was successfully updated.", status: :see_other
redirect_to after_update_path, notice: "Sector was successfully updated.", status: :see_other
else
set_form_variables
render :edit, status: :unprocessable_content
Expand All @@ -69,14 +69,20 @@ def dedupe_config
}
end

def after_update_path
return story_shares_path(sector_names_all: @sector.name) if params[:return_to] == "story_share"

@sector
end

def set_sector
@sector = Sector.find(params[:id])
end

# Strong parameters
def sector_params
params.require(:sector).permit(
:name, :published, :description, :story_share_position
:name, :published, :description, :story_share_position, :rhino_story_share_intro
)
end
end
12 changes: 12 additions & 0 deletions app/controllers/story_shares_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ def load_browse
@count_display = count_display(filtered.count)
return load_search_results(filtered, per_page) if searching?

@intro_tag = intro_tag

highlights = primary_tag_matches(filtered)
lead_ids = highlights.reorder(publicly_featured: :desc, created_at: :desc).limit(single_hero? ? 1 : BROWSE_CAROUSEL_LIMIT).pluck(:id)
grid_ids = highlights.where.not(id: lead_ids).reorder(created_at: :desc).limit(BROWSE_GRID_LIMIT).pluck(:id)
Expand Down Expand Up @@ -148,6 +150,16 @@ def in_id_order(ids)
preloaded(Story.where(id: ids)).in_order_of(:id, ids).decorate
end

# The sector or audience category whose own page this is, so its intro leads the page.
def intro_tag
sector_names = params[:sector_names_all].to_s.split("--")
return Sector.sector_names_all(sector_names).first if sector_names.one?
return if sector_names.any?

category_names = params[:category_names_all].to_s.split("--")
Category.audiences.category_names_all(category_names).first if category_names.one?
end

def first_page?
params[:page].to_i <= 1
end
Expand Down
5 changes: 5 additions & 0 deletions app/models/category.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Category < ApplicationRecord
belongs_to :updated_by, class_name: "User", optional: true
has_many :categorizable_items, dependent: :destroy
has_many :workshops, through: :categorizable_items, source: :categorizable, source_type: "Workshop"
has_rich_text :rhino_story_share_intro

# Scopes
# See NameFilterable, Publishable
Expand All @@ -35,6 +36,10 @@ def self.story_share_audience_nav
story_share_audience_groups.values.flat_map { |scope| scope.published.story_share_featured.to_a }
end

def story_share_audience?
category_type&.name.in?(CategoriesTaggable::AUDIENCE_CATEGORY_TYPES)
end

# Validations
normalizes :name, with: ->(name) { name.squish }
validates :name, presence: true, uniqueness: { case_sensitive: false }, length: { maximum: 255 }
Expand Down
1 change: 1 addition & 0 deletions app/models/sector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class Sector < ApplicationRecord
source: :sectorable, source_type: "Story"
# has_many through
has_many :quotes, through: :workshops
has_rich_text :rhino_story_share_intro

# Validations
normalizes :name, with: ->(name) { name.squish }
Expand Down
10 changes: 10 additions & 0 deletions app/views/categories/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<!-- Errors -->
<%= f.error_notification %>
<%= render "shared/errors", resource: @category if @category.errors.any? %>
<%= hidden_field_tag :return_to, params[:return_to] if params[:return_to].present? %>

<div class="grid grid-cols-1 items-start gap-6 md:grid-cols-4">
<div class="space-y-6 md:col-span-3">
Expand Down Expand Up @@ -49,6 +50,15 @@
input_html: { class: "form-control", rows: 2 } %>
</div>

<% if f.object.story_share_audience? %>
<!-- Story Share intro -->
<div>
<%= rhino_editor(f, :story_share_intro,
label: "Story Share intro",
hint: "Optional. Shown at the top of this #{f.object.category_type.name.underscore.humanize.downcase}'s Story Share page.") %>
</div>
<% end %>

<!-- Story Share portal placement -->
<div class="max-w-[220px]">
<%= f.input :story_share_position,
Expand Down
3 changes: 3 additions & 0 deletions app/views/categories/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
<div class="mb-4 flex flex-wrap justify-end gap-2">
<%= link_to "Home", root_path, class: "text-sm #{eyebrow_link_class} px-2 py-1" %>
<%= link_to "Categories", categories_path, class: "text-sm #{eyebrow_link_class} px-2 py-1" %>
<% if params[:return_to] == "story_share" %>
<%= link_to "Story Share", story_shares_path(category_names_all: @category.name), class: "text-sm #{eyebrow_link_class} px-2 py-1" %>
<% end %>
<%= link_to "Taggings", taggings_path(category_names_all: @category.name), class: "text-sm #{eyebrow_link_class} px-2 py-1" %>
<%= link_to "View", category_path(@category), class: "text-sm #{eyebrow_link_class} px-2 py-1" %>
</div>
Expand Down
8 changes: 8 additions & 0 deletions app/views/sectors/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

<!-- Errors -->
<%= render "shared/errors", resource: @sector if @sector.errors.any? %>
<%= hidden_field_tag :return_to, params[:return_to] if params[:return_to].present? %>

<div class="grid grid-cols-1 items-start gap-6 md:grid-cols-4">
<div class="space-y-6 md:col-span-3">
Expand All @@ -22,6 +23,13 @@
input_html: { class: "form-control", rows: 2 } %>
</div>

<!-- Story Share intro -->
<div>
<%= rhino_editor(f, :story_share_intro,
label: "Story Share intro",
hint: "Optional. Shown at the top of this sector's Story Share page.") %>
</div>

<!-- Story Share portal placement -->
<div class="max-w-[220px]">
<%= f.input :story_share_position,
Expand Down
3 changes: 3 additions & 0 deletions app/views/sectors/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
<div class="mb-4 flex flex-wrap justify-end gap-2">
<%= link_to "Home", root_path, class: "text-sm #{eyebrow_link_class} px-2 py-1" %>
<%= link_to "Sectors", sectors_path, class: "text-sm #{eyebrow_link_class} px-2 py-1" %>
<% if params[:return_to] == "story_share" %>
<%= link_to "Story Share", story_shares_path(sector_names_all: @sector.name), class: "text-sm #{eyebrow_link_class} px-2 py-1" %>
<% end %>
<%= link_to "Taggings", taggings_path(sector_names_all: @sector.name), class: "text-sm #{eyebrow_link_class} px-2 py-1" %>
<%= link_to "View", sector_path(@sector), class: "text-sm #{eyebrow_link_class} px-2 py-1" %>
</div>
Expand Down
15 changes: 15 additions & 0 deletions app/views/story_shares/_browse.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@
<p class="text-sm text-gray-500"><%= pluralize(@count_display, "story") %></p>
</div>

<% intro = @intro_tag&.rhino_story_share_intro %>
<% can_edit_intro = @intro_tag && allowed_to?(:edit?, @intro_tag) %>
<% if intro.present? || can_edit_intro %>
<div class="-mt-4 mb-10 max-w-3xl">
<% if intro.present? %>
<div class="max-w-none prose prose-lg text-gray-700"><%= intro %></div>
<% end %>
<% if can_edit_intro %>
<%= link_to intro.present? ? "Edit intro" : "Add intro",
polymorphic_path(@intro_tag, action: :edit, return_to: "story_share"),
class: button_classes(:secondary_outline, extra: "admin-only bg-blue-100 mt-3 whitespace-nowrap") %>
<% end %>
</div>
<% end %>

<%= render "search_scope" if searching? %>

<% if searching? && @stories.any? %>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: "Intro text on Story Share sector and age range pages"
area: stories
display_status: admin_facing
released_on: 2026-10-22
pr_number: 2777
summary: >-
Each sector, age range, and story population can carry its own intro, shown at the top of its
Story Share page. Write it with the same editor used for stories.
pro_tips:
- "Open a sector or age range page in Story Share and click Add intro to write one."
- "Pages that combine several sectors don't show an intro."
17 changes: 17 additions & 0 deletions spec/requests/categories_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,15 @@
get edit_category_path(category)
expect(response).to have_http_status(:ok)
end

it "offers the Story Share intro only for age ranges and story populations" do
age_range = create(:category, category_type: create(:category_type, name: "AgeRange"))
get edit_category_path(age_range)
expect(response.body).to include("Story Share intro")

get edit_category_path(create(:category, valid_attributes))
expect(response.body).not_to include("Story Share intro")
end
end

describe "POST create" do
Expand Down Expand Up @@ -131,6 +140,14 @@
expect(response).to have_http_status(:see_other)
expect(category.reload.name).to eq("Updated Name")
end

it "saves the Story Share intro and returns to the category's Story Share page" do
patch category_path(category),
params: { return_to: "story_share", category: { rhino_story_share_intro: "<p>For our youngest artists</p>" } }

expect(category.reload.rhino_story_share_intro.to_plain_text).to eq("For our youngest artists")
expect(response).to redirect_to(story_shares_path(category_names_all: category.name))
end
end

context "JSON ordering (drag & drop)" do
Expand Down
8 changes: 8 additions & 0 deletions spec/requests/sectors_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@
sector.reload
expect(response).to redirect_to(sector_url(sector))
end

it "saves the Story Share intro and returns to the sector's Story Share page" do
sector = Sector.create! valid_attributes
patch sector_url(sector), params: { return_to: "story_share", sector: { rhino_story_share_intro: "<p>Healing together</p>" } }
sector.reload
expect(sector.rhino_story_share_intro.to_plain_text).to eq("Healing together")
expect(response).to redirect_to(story_shares_path(sector_names_all: sector.name))
end
end

context "with invalid parameters" do
Expand Down
29 changes: 29 additions & 0 deletions spec/requests/story_share_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,35 @@ def listed_titles
expect(listed_titles).to eq([ "An adult story with teens too" ])
end

it "leads a sector page with the sector's intro" do
Sector.find_by(name: "Domestic Violence").update!(rhino_story_share_intro: "<p>Survivors finding their voice</p>")
get story_shares_path(sector_names_all: "Domestic Violence")
expect(response.body).to include("Survivors finding their voice")
expect(response.body).not_to include("Edit intro")
end

it "leads an age range page with the age range's intro" do
age_range = create(:category_type, name: "AgeRange")
create(:category, :published, name: "Children", category_type: age_range,
rhino_story_share_intro: "<p>Art with our youngest participants</p>")
get story_shares_path(category_names_all: "Children")
expect(response.body).to include("Art with our youngest participants")
end

it "skips intros when several sectors are combined" do
Sector.find_by(name: "Domestic Violence").update!(rhino_story_share_intro: "<p>Survivors finding their voice</p>")
get story_shares_path(sector_names_all: "Domestic Violence--Mental Health")
expect(response.body).not_to include("Survivors finding their voice")
end

it "offers admins a link to add the intro" do
sign_in create(:user, :admin)
sector = Sector.find_by(name: "Domestic Violence")
get story_shares_path(sector_names_all: sector.name)
expect(response.body).to include("Add intro")
expect(response.body).to include(edit_sector_path(sector, return_to: "story_share"))
end

it "does not leak non-public stories to guests when browsing" do
create(:story, :published, title: "A published-only secret", publicly_visible: false).tap { |s| tag_sector(s, "Domestic Violence") }
get story_shares_path(sector_names_all: "Domestic Violence")
Expand Down
Loading