Skip to content
Open
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
12 changes: 5 additions & 7 deletions app/controllers/stories_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def index
if turbo_frame_request?
per_page = params[:number_of_items_per_page].presence || 12
base_scope = authorized_scope(Story.includes(:windows_type, :organization, :workshop,
:created_by, :bookmarks, :primary_asset,
:author, :created_by, :bookmarks, :primary_asset,
:story_idea))
filtered = base_scope.search_by_params(params)
sortable = %w[title updated_at created_at windows_type workshop author organization]
Expand Down Expand Up @@ -62,6 +62,7 @@ def edit

def create
@story = Story.new(story_params.except(:category_ids, :sector_ids))
@story.created_by = current_user
authorize! @story
Comment on lines 63 to 66

success = false
Expand Down Expand Up @@ -127,11 +128,7 @@ def set_form_variables
@story_idea = StoryIdea.find(params[:story_idea_id]) if params[:story_idea_id].present?
@user = User.find(params[:user_id]) if params[:user_id].present?
@organizations = authorized_scope(Organization.all, as: :affiliated).order(:name)
@story_ideas = authorized_scope(StoryIdea.includes(:created_by))
.references(:users)
.order(:created_at)
@people = Person.order(Arel.sql("LOWER(first_name), LOWER(last_name)"))
@users = User.has_access.includes(:person).left_joins(:person).order(Arel.sql("people.first_name IS NULL, LOWER(people.first_name), LOWER(people.last_name), LOWER(users.email)"))
@windows_types = WindowsType.all
@workshops = authorized_scope(Workshop.all).includes(:windows_type).order(:title)
@categories_grouped =
Expand Down Expand Up @@ -174,7 +171,7 @@ def apply_sort(scope, column, direction)
scope.left_joins(:workshop)
.reorder(Workshop.arel_table[:title].public_send(dir))
when "author"
scope.left_joins(created_by: :person)
scope.left_joins(:author)
.reorder(Person.arel_table[:first_name].public_send(dir))
when "organization"
scope.left_joins(:organization)
Expand All @@ -189,7 +186,7 @@ def story_params
params.require(:story).permit(
:title, :rhino_body, :featured, :published, :publicly_visible, :publicly_featured, :youtube_url, :website_url,
:windows_type_id, :organization_id, :workshop_id, :external_workshop_title,
:created_by_id, :updated_by_id, :story_idea_id, :spotlighted_facilitator_id, :author_credit_preference,
:author_id, :updated_by_id, :story_idea_id, :spotlighted_facilitator_id, :author_credit_preference,
category_ids: [],
sector_ids: [],
primary_asset_attributes: [ :id, :file, :_destroy ],
Expand All @@ -206,6 +203,7 @@ def set_story_attributes_from(idea)
external_workshop_title: idea.external_workshop_title,
windows_type_id: idea.windows_type_id,
youtube_url: idea.youtube_url,
author_id: idea.author_id,
author_credit_preference: idea.author_credit_preference
}
end
Expand Down
1 change: 1 addition & 0 deletions app/controllers/story_ideas_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def create
@story_idea = StoryIdea.new(story_idea_params.except(:category_ids, :sector_ids))
@story_idea.created_by = current_user
@story_idea.updated_by = current_user
@story_idea.author = current_user.person
authorize! @story_idea

success = false
Expand Down
10 changes: 10 additions & 0 deletions app/helpers/person_helper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
module PersonHelper
# Builds [ label, id ] pairs for a person select. Shows just the name, adding
# the email only to disambiguate people who share a full name.
def person_select_options(people)
duplicate_names = people.map(&:full_name).tally.select { |_, count| count > 1 }.keys.to_set
people.map do |person|
label = duplicate_names.include?(person.full_name) ? person.full_name_with_email : person.full_name
[ label, person.id ]
end
end

def person_profile_button(person, truncate_at: nil, subtitle: nil, display_name: nil, data: {}, inactive: false)
if inactive
bg = "bg-gray-100"
Expand Down
8 changes: 7 additions & 1 deletion app/models/concerns/author_creditable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module AuthorCreditable
}.freeze

def author_credit
person = created_by&.person
person = credited_person
case author_credit_preference
when "full_name" then person&.full_name || "Anonymous"
when "first_name_last_initial"
Expand All @@ -33,4 +33,10 @@ def author_credit
else person&.name || "Anonymous"
end
end

# The person credited as the author. Models with a direct author association
# (Story, StoryIdea) override this; the rest derive it from the creating user.
def credited_person
created_by&.person
end
end
15 changes: 14 additions & 1 deletion app/models/story.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class Story < ApplicationRecord

has_rich_text :rhino_body

belongs_to :author, class_name: "Person", optional: true
belongs_to :created_by, class_name: "User"
belongs_to :updated_by, class_name: "User"
belongs_to :windows_type
Expand Down Expand Up @@ -36,6 +37,10 @@ class Story < ApplicationRecord
validates :title, presence: true, uniqueness: true
validates :rhino_body, presence: true

# Default the credit preference from the author's own name display preference,
# while leaving it overridable per story.
before_validation :default_author_credit_preference

# Nested attributes
accepts_nested_attributes_for :primary_asset, allow_destroy: true, reject_if: :all_blank
accepts_nested_attributes_for :gallery_assets, allow_destroy: true, reject_if: :all_blank
Expand All @@ -48,7 +53,7 @@ class Story < ApplicationRecord
attributes person_first: "people.first_name", person_last: "people.last_name"
options :all, type: :text, default: true, default_operator: :or

scope { join_rich_texts.left_joins(created_by: :person) }
scope { join_rich_texts.left_joins(:author) }
attributes action_text_body: "action_text_rich_texts.plain_text_body"
options :action_text_body, type: :text, default: true, default_operator: :or
end
Expand Down Expand Up @@ -85,10 +90,18 @@ def self.search_by_params(params)
stories
end

def credited_person
author
end

def name
title
end

def default_author_credit_preference
self.author_credit_preference ||= author&.display_name_preference
end
Comment on lines +101 to +103

def organization_name
organization&.name
end
Expand Down
5 changes: 5 additions & 0 deletions app/models/story_idea.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def self.search_by_params(params)

has_rich_text :rhino_body

belongs_to :author, class_name: "Person", optional: true
belongs_to :created_by, class_name: "User"
belongs_to :updated_by, class_name: "User"
belongs_to :organization
Expand Down Expand Up @@ -48,6 +49,10 @@ def self.search_by_params(params)
accepts_nested_attributes_for :primary_asset, allow_destroy: true, reject_if: :all_blank
accepts_nested_attributes_for :gallery_assets, allow_destroy: true, reject_if: :all_blank

def credited_person
author
end

def name
"StoryIdea ##{id}"
end
Expand Down
124 changes: 43 additions & 81 deletions app/views/stories/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -205,21 +205,6 @@
<%= render "shared/form_image_fields", f: f, include_primary_asset: true %>

<div class="flex flex-col md:flex-row md:space-x-4 mt-4">
<div class="flex-1 mb-4 md:mb-0">
<%= f.input :author_credit_preference,
as: :select,
collection: AuthorCreditable::ADMIN_FORM_OPTIONS,
prompt: "Select a preference",
selected: f.object.author_credit_preference || story_idea&.author_credit_preference,
label: "Author credit preference",
hint: "Controls how the author's name is displayed",
input_html: {
class: select_caret_class(blank: f.object.author_credit_preference.blank?),
style: custom_caret_style,
onchange: select_caret_onchange
} %>
</div>

<div class="flex-1 mb-4 md:mb-0">
<%= f.input :youtube_url, as: :text,
label: "YouTube URL",
Expand All @@ -233,97 +218,74 @@
hint: "(open in external website instead)",
input_html: { rows: 1, class: "w-full" } %>
</div>
</div>

<% if story_idea %>
<div class="flex flex-col md:flex-row md:space-x-4">
<div class="flex-1 mb-4 md:mb-0">
Story idea author credit:
<br>
<% if story_idea.created_by.person %>
<%= person_profile_button(story_idea.created_by.person, display_name: story_idea.author_credit, subtitle: story_idea.created_by.person.full_name) %>
<% else %>
<%= story_idea.author_credit %>
<% end %>
<br>
Story idea "author credit preference":
<br>
<div class="italic"><%= link_to story_idea.author_credit_preference, edit_story_idea_path(story_idea, anchor: "publish-preferences"), class: "hover:underline hover:text-blue-600" %></div>
</div>

<% if f.object.persisted? %>
<div class="flex-1 mb-4 md:mb-0 ms-3 ps-3 text-gray-500">
Last updated by:
<br>
<%= f.object.updated_by&.full_name %>
<br>
<%= f.object.updated_at.in_time_zone.strftime("%Y-%m-%d %I:%M %P") %>
</div>
<% end %>
</div>
<% elsif f.object.persisted? %>
<div class="flex-1 mb-4 md:mb-0 ms-3 ps-3 text-gray-500">
Last updated by:
<br>
<%= f.object.updated_by&.full_name %>
<br>
<%= f.object.updated_at.in_time_zone.strftime("%Y-%m-%d %I:%M %P") %>
</div>
<% end %>

<div class="flex flex-col md:flex-row md:space-x-4">
<div class="flex-1 mb-4 md:mb-0">
<%= f.input :created_by_id,
collection: @users.map { |u| [ u.full_name_with_email, u.id ] },
prompt: "Select an author",
label: (f.object.created_by&.person ? (
link_to "Story author",
person_path(f.object.created_by.person),
class: "hover:underline") : "Story author").html_safe,
<%= f.input :spotlighted_facilitator_id,
collection: person_select_options(@people),
prompt: "Select a facilitator",
label: (f.object.spotlighted_facilitator_id ? (link_to "Story spotlighted facilitator",
person_path(f.object.spotlighted_facilitator_id),
class: "hover:underline") : "Story spotlighted facilitator").html_safe,
label_html: { class: "block font-medium mb-1 text-gray-700" },
input_html: {
class: select_caret_class(blank: f.object.created_by_id.blank?),
class: select_caret_class(blank: f.object.spotlighted_facilitator_id.blank?),
style: custom_caret_style,
onchange: select_caret_onchange
} %>
</div>
</div>

<div class="flex flex-col md:flex-row md:space-x-4">
<div class="flex-1 mb-4 md:mb-0">
<%= f.input :spotlighted_facilitator_id,
collection: @people.map { |p| [ p.full_name_with_email, p.id ] },
prompt: "Select a facilitator",
label: (f.object.spotlighted_facilitator_id ? (link_to "Story spotlighted facilitator",
person_path(f.object.spotlighted_facilitator_id),
class: "hover:underline") : "Story spotlighted facilitator").html_safe,
<%= f.input :author_id,
collection: person_select_options(@people),
prompt: "Select an author",
label: (f.object.author ? (
link_to "Story author",
person_path(f.object.author),
class: "hover:underline") : "Story author").html_safe,
label_html: { class: "block font-medium mb-1 text-gray-700" },
input_html: {
class: select_caret_class(blank: f.object.spotlighted_facilitator_id.blank?),
class: select_caret_class(blank: f.object.author_id.blank?),
style: custom_caret_style,
onchange: select_caret_onchange
} %>
</div>

<div class="flex-1 mb-4 md:mb-0">
<% story_idea_label = if f.object.story_idea
(link_to "Source story idea",
story_idea_path(f.object.story_idea), class: "hover:underline")
else
"Source story idea"
end %>
<%= f.input :story_idea_id,
collection: @story_ideas, label_method: :full_name,
value_method: :id,
selected: f.object.story_idea_id || @story_idea&.id,
label: story_idea_label.html_safe,
prompt: "Select idea",
<%= f.input :author_credit_preference,
as: :select,
collection: AuthorCreditable::ADMIN_FORM_OPTIONS,
prompt: "Select a preference",
selected: f.object.author_credit_preference || story_idea&.author_credit_preference || f.object.author&.display_name_preference,
label: "Author credit preference",
hint: "Defaults to the author's name display preference; controls how their name is shown",
Comment on lines +258 to +262
input_html: {
class: select_caret_class(blank: f.object.story_idea_id.blank?),
class: select_caret_class(blank: f.object.author_credit_preference.blank?),
style: custom_caret_style,
onchange: select_caret_onchange
} %>
</div>
</div>

<% if story_idea %>
<div class="mb-4 text-sm text-gray-600 text-right">
From
<%= link_to "Story Idea # #{story_idea.id}", story_idea_path(story_idea), class: "font-medium text-gray-700 underline" %>
by
<% if story_idea.author %>
<%= link_to story_idea.author_credit, person_path(story_idea.author), class: "font-medium text-gray-800 underline" %>
<% else %>
<span class="font-medium text-gray-800"><%= story_idea.author_credit %></span>
<% end %>
Comment on lines +276 to +280
(preference "<%= link_to AuthorCreditable::ADMIN_FORM_OPTIONS.key(story_idea.author_credit_preference) || story_idea.author_credit_preference&.humanize,
edit_story_idea_path(story_idea, anchor: "publish-preferences"),
class: "italic underline hover:text-blue-600" %>")
</div>
<% end %>

<%= f.hidden_field :story_idea_id, value: f.object.story_idea_id || @story_idea&.id %>

<!-- Remaining tags accordion -->
<% if remaining_categories.any? %>
<div class="space-y-4 mt-8">
Expand Down
2 changes: 1 addition & 1 deletion app/views/stories/_story_results.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
<span title="<%= story.workshop&.title %>"><%= story.workshop&.title&.truncate(30) %></span>
</td>

<td class="px-4 py-2 text-sm text-gray-800"><%= story.created_by.name %></td>
<td class="px-4 py-2 text-sm text-gray-800"><%= story.author&.full_name || "—" %></td>

<td class="px-4 py-2 text-sm text-gray-800">
<span title="<%= story.organization&.name %>"><%= story.organization&.name&.truncate(30) %></span>
Expand Down
4 changes: 2 additions & 2 deletions app/views/stories/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
<!-- Meta Data (exact screenshot style) -->
<div class="text-sm text-gray-600 space-y-0.5">
<p><strong>Story by:</strong>
<% if @story.created_by.person&.profile_is_searchable %>
<%= link_to @story.author_credit, person_path(@story.created_by) %>
<% if @story.author&.profile_is_searchable %>
<%= link_to @story.author_credit, person_path(@story.author) %>
<% else %>
<%= @story.author_credit %>
<% end %>
Expand Down
8 changes: 4 additions & 4 deletions app/views/story_ideas/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
<div>
<span class="font-semibold text-gray-700">Story by:</span>
<% if @story_idea.created_by&.person && allowed_to?(:show?, @story_idea.created_by.person) %>
<%= link_to @story_idea.created_by.name,
person_path(@story_idea.created_by.person),
<% if @story_idea.author && allowed_to?(:show?, @story_idea.author) %>
<%= link_to @story_idea.author.full_name,
person_path(@story_idea.author),
data: { turbo_frame: "_top" },
class: "inline-block px-3 py-1 rounded-md text-sm font-medium #{DomainTheme.bg_class_for(:people, intensity: 100)} #{DomainTheme.text_class_for(:people)} #{DomainTheme.bg_class_for(:people, intensity: 100, hover: true)}" %>
<% else %>
Comment on lines +35 to 39
<%= @story_idea.created_by&.name || "—" %>
<%= @story_idea.author&.full_name || "—" %>
<% end %>
</div>
<div>
Expand Down
32 changes: 32 additions & 0 deletions db/migrate/20260605120000_add_author_to_stories_and_story_ideas.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class AddAuthorToStoriesAndStoryIdeas < ActiveRecord::Migration[7.2]
def up
add_reference :stories, :author, type: :bigint, null: true, index: true
add_reference :story_ideas, :author, type: :bigint, null: true, index: true

add_foreign_key :stories, :people, column: "author_id"
add_foreign_key :story_ideas, :people, column: "author_id"

# Backfill author from the creating user's linked person, preserving existing
# author credits. Rows whose creator has no linked person stay null (Anonymous).
execute <<~SQL.squish
UPDATE stories
JOIN users ON users.id = stories.created_by_id
SET stories.author_id = users.person_id
WHERE users.person_id IS NOT NULL
SQL
Comment on lines +12 to +16

execute <<~SQL.squish
UPDATE story_ideas
JOIN users ON users.id = story_ideas.created_by_id
SET story_ideas.author_id = users.person_id
WHERE users.person_id IS NOT NULL
SQL
Comment on lines +19 to +23
end

def down
remove_foreign_key :stories, column: "author_id" if foreign_key_exists?(:stories, column: "author_id")
remove_foreign_key :story_ideas, column: "author_id" if foreign_key_exists?(:story_ideas, column: "author_id")
remove_reference :stories, :author, if_exists: true
remove_reference :story_ideas, :author, if_exists: true
end
end
Loading