Skip to content
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ Both flows record on `EventRegistrationOrganization` what the submission meant f
### Notifications

- `NotificationServices::CreateNotification` β€” Notification creation
- `NotificationServices::NotifyAuthors` β€” Notifies a record's credited author and co-author (story, workshop, and workshop variation ideas; stories)
- `NotificationServices::PersistDeliveredEmail` β€” Email delivery tracking

### User Management
Expand Down
23 changes: 9 additions & 14 deletions app/controllers/stories_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def create
elsif params.dig(:library_asset, :new_assets).present?
update_asset_owner(@story)
end
notify_story_promoted if just_published
notify_story_published if just_published
success = true
end
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotUnique => e
Expand Down Expand Up @@ -112,7 +112,7 @@ def update
if params[:promote_idea_assets] == "true"
@story.attach_assets_from_idea!
end
notify_story_promoted if just_published
notify_story_published if just_published
success = true
end
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotUnique => e
Expand Down Expand Up @@ -176,19 +176,14 @@ def set_form_variables

private

def notify_story_promoted
return unless @story.story_idea
return if @story.notifications.exists?(kind: %w[story_promoted story_promoted_fyi])
def notify_story_published
return if @story.notifications.exists?(kind: %w[story_promoted story_shared_with_funder story_promoted_fyi])

if (submitter = @story.story_idea.created_by)
NotificationServices::CreateNotification.call(
noticeable: @story,
person: submitter.person,
kind: :story_promoted,
recipient_role: :person,
recipient_email: submitter.email,
notification_type: 0)
end
NotificationServices::NotifyAuthors.call(record: @story, kind: @story.funder_only? ? :story_shared_with_funder : :story_promoted)
notify_story_promoted_fyi
end

def notify_story_promoted_fyi
NotificationServices::CreateNotification.call(
noticeable: @story,
kind: :story_promoted_fyi,
Expand Down
8 changes: 1 addition & 7 deletions app/controllers/story_ideas_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,7 @@ def create
StoryIdea.transaction do
if @story_idea.save
assign_associations(@story_idea)
NotificationServices::CreateNotification.call(
noticeable: @story_idea,
person: @story_idea.created_by&.person,
kind: :idea_submitted,
recipient_role: :person,
recipient_email: @story_idea.created_by.email,
notification_type: 0)
NotificationServices::NotifyAuthors.call(record: @story_idea, kind: :idea_submitted)
NotificationServices::CreateNotification.call(
noticeable: @story_idea,
kind: :idea_submitted_fyi,
Expand Down
10 changes: 2 additions & 8 deletions app/controllers/workshop_ideas_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,11 @@ def create
@workshop_idea = WorkshopIdea.new(workshop_idea_params)
# Credit the submitting account's person as the author, so the idea lists on
# their profile by authorship like every other content type.
@workshop_idea.author ||= @workshop_idea.created_by&.person
@workshop_idea.author ||= current_user.person
authorize! @workshop_idea

if @workshop_idea.save
NotificationServices::CreateNotification.call(
noticeable: @workshop_idea,
person: @workshop_idea.created_by&.person,
kind: :idea_submitted,
recipient_role: :person,
recipient_email: @workshop_idea.created_by.email,
notification_type: 0)
NotificationServices::NotifyAuthors.call(record: @workshop_idea, kind: :idea_submitted)
NotificationServices::CreateNotification.call(
noticeable: @workshop_idea,
kind: :idea_submitted_fyi,
Expand Down
10 changes: 2 additions & 8 deletions app/controllers/workshop_variation_ideas_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,11 @@ def create
@workshop_variation_idea = WorkshopVariationIdea.new(workshop_variation_idea_params)
# Credit the submitting account's person as the author, so the idea lists on
# their profile by authorship like every other content type.
@workshop_variation_idea.author ||= @workshop_variation_idea.created_by&.person
@workshop_variation_idea.author ||= current_user.person
authorize! @workshop_variation_idea

if @workshop_variation_idea.save
NotificationServices::CreateNotification.call(
noticeable: @workshop_variation_idea,
person: @workshop_variation_idea.created_by&.person,
kind: :idea_submitted,
recipient_role: :person,
recipient_email: @workshop_variation_idea.created_by.email,
notification_type: 0)
NotificationServices::NotifyAuthors.call(record: @workshop_variation_idea, kind: :idea_submitted)
NotificationServices::CreateNotification.call(
noticeable: @workshop_variation_idea,
kind: :idea_submitted_fyi,
Expand Down
1 change: 1 addition & 0 deletions app/jobs/notification_mailer_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def perform(notification_id, persist_delivered_email: true)
"idea_submitted_fyi" => ->(n) { NotificationMailer.idea_submitted_fyi(n) },
"story_promoted" => ->(n) { NotificationMailer.story_promoted(n) },
"story_promoted_fyi" => ->(n) { NotificationMailer.story_promoted_fyi(n) },
"story_shared_with_funder" => ->(n) { NotificationMailer.story_shared_with_funder(n) },
"report_submitted_fyi" => ->(n) { NotificationMailer.report_submitted_fyi(n) },
"workshop_log_submitted" => ->(n) { NotificationMailer.workshop_log_submitted(n) },
"workshop_log_submitted_fyi" => ->(n) { NotificationMailer.workshop_log_submitted_fyi(n) },
Expand Down
26 changes: 15 additions & 11 deletions app/mailers/notification_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,7 @@ def idea_submitted(notification)
@notification = notification
@noticeable = notification.noticeable.decorate
@noticeable_klass = @noticeable.object.class

if @noticeable_klass == User
@user = @noticeable.object
else
@user = @noticeable.try(:user) || @noticeable.try(:created_by)
end
@recipient_name = notification.person&.first_name.presence || notification.recipient_email

mail(
to: notification.recipient_email,
Expand Down Expand Up @@ -110,22 +105,31 @@ def idea_submitted_fyi(notification)

def story_promoted(notification)
@story = notification.noticeable.decorate
@story_idea = @story.story_idea
@user = @story_idea&.created_by || @story.created_by
@recipient_name = notification.person&.first_name.presence || notification.recipient_email

mail(
to: notification.recipient_email,
subject: "#{SUBJECT_PREFIX} Your story is live on Story Share"
)
end

def story_shared_with_funder(notification)
@story = notification.noticeable.decorate
@recipient_name = notification.person&.first_name.presence || notification.recipient_email

mail(
to: notification.recipient_email,
subject: "#{SUBJECT_PREFIX} Your story idea is now a story"
subject: "#{SUBJECT_PREFIX} Your story has been shared with the funder"
)
end

def story_promoted_fyi(notification)
@story = notification.noticeable.decorate
@story_idea = @story.story_idea
@user = @story_idea&.created_by || @story.created_by
@user = @story_idea&.created_by

mail(
subject: "#{FYI_PREFIX} Story idea promoted to a story: #{@story.title}"
subject: "#{FYI_PREFIX} Story published: #{@story.title}"
)
end

Expand Down
1 change: 1 addition & 0 deletions app/models/notification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class Notification < ApplicationRecord
idea_submitted_fyi
story_promoted
story_promoted_fyi
story_shared_with_funder
report_submitted
report_submitted_fyi
workshop_log_submitted
Expand Down
5 changes: 5 additions & 0 deletions app/policies/application_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ def admin?

def authenticated? = user.present?

def credited_author?
return false unless user&.person_id
[ record.try(:author_id), record.try(:co_author_id) ].compact.include?(user.person_id)
end

def owner?
return false unless user
if record.respond_to?(:created_by_id)
Expand Down
2 changes: 1 addition & 1 deletion app/policies/story_idea_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ def index?
end

def show?
admin? || owner?
admin? || owner? || credited_author?
end

def new?
Expand Down
2 changes: 1 addition & 1 deletion app/policies/workshop_idea_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def update?
end

def show?
admin? || owner?
admin? || owner? || credited_author?
end

# Scoping
Expand Down
2 changes: 1 addition & 1 deletion app/policies/workshop_variation_idea_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ def index?
end

def show?
admin? || owner?
admin? || owner? || credited_author?
end

def new?
Expand Down
33 changes: 33 additions & 0 deletions app/services/notification_services/notify_authors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module NotificationServices
class NotifyAuthors
def self.call(record:, kind:)
new(record:, kind:).call
end

def initialize(record:, kind:)
@record = record
@kind = kind
end

def call
recipients.map do |person, email|
CreateNotification.call(
noticeable: @record,
person: person,
kind: @kind,
recipient_role: :person,
recipient_email: email,
notification_type: 0)
end
end

private

# Anonymous credits still hear about their own record.
def recipients
@record.credited_author_entries.map(&:first)
.filter_map { |person| [ person, person.preferred_email ] if person.preferred_email }
.uniq { |_person, email| email.downcase }
end
end
end
2 changes: 1 addition & 1 deletion app/views/notification_mailer/idea_submitted.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<div style="margin-top: 36px; text-align: left;">
<p>
Hello <strong><%= @user.first_name_or_email %></strong>,
Hello <strong><%= @recipient_name %></strong>,
</p>

<p>
Expand Down
2 changes: 1 addition & 1 deletion app/views/notification_mailer/idea_submitted.text.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Submission received

Hello <%= @user.first_name_or_email %>,
Hello <%= @recipient_name %>,

Thank you for your submission. We have received your <%= @noticeable_klass.model_name.human.downcase %> and will review it shortly.

Expand Down
4 changes: 2 additions & 2 deletions app/views/notification_mailer/story_promoted.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<div style="margin-top: 36px; text-align: left;">
<p>
Hello <strong><%= @user.first_name_or_email %></strong>,
Hello <strong><%= @recipient_name %></strong>,
</p>

<p>
Expand All @@ -27,7 +27,7 @@
<% if @story.published? || @story.publicly_visible? %>
<p>
<%= link_to "View story",
polymorphic_url(@story),
story_share_url(@story),
style: "display:inline-block;
padding:10px 16px;
background:#2563eb;
Expand Down
4 changes: 2 additions & 2 deletions app/views/notification_mailer/story_promoted.text.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Your story has been shared

Hello <%= @user.first_name_or_email %>,
Hello <%= @recipient_name %>,

We are happy to share your story is now live on Story Share! You can check it out at the link below.

Expand All @@ -12,7 +12,7 @@ Thank you for sharing your story with us and helping others see the healing powe

<% if @story.published? || @story.publicly_visible? %>
View story:
<%= polymorphic_url(@story) %>
<%= story_share_url(@story) %>
<% end %>

--
Expand Down
2 changes: 1 addition & 1 deletion app/views/notification_mailer/story_promoted_fyi.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<p style="margin-bottom: 6px;">
Story idea promoted to a story
Story published
</p>

<h1 style="margin: 0;">
Expand Down
4 changes: 2 additions & 2 deletions app/views/notification_mailer/story_promoted_fyi.text.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Story idea promoted to a story
==============================
Story published
===============

<%= @story.title %>
<% if @story.windows_type.present? %>
Expand Down
33 changes: 33 additions & 0 deletions app/views/notification_mailer/story_shared_with_funder.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<h1>Your story has been shared with the funder</h1>

<div style="margin-top: 36px; text-align: left;">
<p>
Hello <strong><%= @recipient_name %></strong>,
</p>

<p>
Your story has been shared with the funder who supported your training. As you requested, it is shared only with the funder and won't appear on Story Share.
</p>

<p>
Thank you for sharing your story with us and helping others see the healing power of art!
</p>

<div style="text-align: center; background-color: #f3f4f6; border-radius: 6px; padding: 24px; margin: 16px 0;">
<h2 style="font-size: 22px; font-weight: bold; color: #166534; margin: 0; font-family: Lato, sans-serif;">
<%= @story.title %>
</h2>
</div>
</div>

<p>
<%= link_to "View your story",
story_url(@story),
style: "display:inline-block;
padding:10px 16px;
background:#2563eb;
color:#ffffff;
text-decoration:none;
border-radius:6px;
font-weight:bold;" %>
</p>
15 changes: 15 additions & 0 deletions app/views/notification_mailer/story_shared_with_funder.text.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Your story has been shared with the funder

Hello <%= @recipient_name %>,

Your story has been shared with the funder who supported your training. As you requested, it is shared only with the funder and won't appear on Story Share.

Thank you for sharing your story with us and helping others see the healing power of art!

<%= @story.title %>

View your story:
<%= story_url(@story) %>

--
This is an automated message from AWBW.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: "Story emails go to the story's authors and link to Story Share"
area: stories
display_status: user_facing
released_on: 2026-10-23
pr_number: 2780
summary: >-
Idea confirmations and "your story is live" emails now go to the credited author
and second author. The live email links to the story on Story Share and is sent for stories
created directly too.
pro_tips:
- "Promoting an idea sends nothing; the author and admin hear about it when the story is first published."
- "Admins get a \"Story published\" FYI the first time any story is published, including ones created directly."
- "Funder-only stories send a \"shared with the funder\" email instead, since they never appear on Story Share."
- "A story with no author credited sends no author email."
- "An idea's submitter, author, and second author can all open it from their confirmation email."
Loading
Loading