Skip to content

Commit 0ec9870

Browse files
authored
Improvements/authorship notifications (#1048)
2 parents 9f434ee + e5954f1 commit 0ec9870

File tree

15 files changed

+2005
-1383
lines changed

15 files changed

+2005
-1383
lines changed

app/controllers/better_together/pages_controller.rb

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,26 @@ def create # rubocop:todo Metrics/MethodLength, Metrics/AbcSize
3232
@page = resource_class.new(page_params)
3333
authorize @page
3434

35-
respond_to do |format|
36-
if @page.save
37-
format.html do
38-
redirect_to edit_page_path(@page), notice: t('flash.generic.created', resource: t('resources.page'))
39-
end
40-
format.turbo_stream do
41-
flash.now[:notice] = t('flash.generic.created', resource: t('resources.page'))
42-
render turbo_stream: turbo_stream.redirect_to(edit_page_path(@page))
43-
end
44-
else
45-
format.turbo_stream do
46-
render turbo_stream: turbo_stream.update(
47-
'form_errors',
48-
partial: 'layouts/better_together/errors',
49-
locals: { object: @page }
50-
)
35+
BetterTogether::Authorship.with_creator(helpers.current_person) do
36+
respond_to do |format|
37+
if @page.save
38+
format.html do
39+
redirect_to edit_page_path(@page), notice: t('flash.generic.created', resource: t('resources.page'))
40+
end
41+
format.turbo_stream do
42+
flash.now[:notice] = t('flash.generic.created', resource: t('resources.page'))
43+
render turbo_stream: turbo_stream.redirect_to(edit_page_path(@page))
44+
end
45+
else
46+
format.turbo_stream do
47+
render turbo_stream: turbo_stream.update(
48+
'form_errors',
49+
partial: 'layouts/better_together/errors',
50+
locals: { object: @page }
51+
)
52+
end
53+
format.html { render :new, status: :unprocessable_entity }
5154
end
52-
format.html { render :new, status: :unprocessable_entity }
5355
end
5456
end
5557
end
@@ -61,32 +63,34 @@ def edit
6163
def update # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
6264
authorize @page
6365

64-
respond_to do |format| # rubocop:todo Metrics/BlockLength
65-
if @page.update(page_params)
66-
format.html do
67-
flash[:notice] = t('flash.generic.updated', resource: t('resources.page'))
68-
redirect_to edit_page_path(@page), notice: t('flash.generic.updated', resource: t('resources.page'))
69-
end
70-
format.turbo_stream do
71-
flash.now[:notice] = t('flash.generic.updated', resource: t('resources.page'))
72-
render turbo_stream: [
73-
turbo_stream.replace(helpers.dom_id(@page, 'form'), partial: 'form',
74-
locals: { page: @page }),
75-
turbo_stream.replace('flash_messages', partial: 'layouts/better_together/flash_messages',
76-
locals: { flash: })
77-
]
78-
end
79-
else
80-
format.html { render :edit }
81-
format.turbo_stream do
82-
render turbo_stream: [
83-
turbo_stream.replace(helpers.dom_id(@page, 'form'), partial: 'form', locals: { page: @page }),
84-
turbo_stream.update(
85-
'form_errors',
86-
partial: 'layouts/better_together/errors',
87-
locals: { object: @page }
88-
)
89-
]
66+
BetterTogether::Authorship.with_creator(helpers.current_person) do # rubocop:todo Metrics/BlockLength
67+
respond_to do |format| # rubocop:todo Metrics/BlockLength
68+
if @page.update(page_params)
69+
format.html do
70+
flash[:notice] = t('flash.generic.updated', resource: t('resources.page'))
71+
redirect_to edit_page_path(@page), notice: t('flash.generic.updated', resource: t('resources.page'))
72+
end
73+
format.turbo_stream do
74+
flash.now[:notice] = t('flash.generic.updated', resource: t('resources.page'))
75+
render turbo_stream: [
76+
turbo_stream.replace(helpers.dom_id(@page, 'form'), partial: 'form',
77+
locals: { page: @page }),
78+
turbo_stream.replace('flash_messages', partial: 'layouts/better_together/flash_messages',
79+
locals: { flash: })
80+
]
81+
end
82+
else
83+
format.html { render :edit }
84+
format.turbo_stream do
85+
render turbo_stream: [
86+
turbo_stream.replace(helpers.dom_id(@page, 'form'), partial: 'form', locals: { page: @page }),
87+
turbo_stream.update(
88+
'form_errors',
89+
partial: 'layouts/better_together/errors',
90+
locals: { object: @page }
91+
)
92+
]
93+
end
9094
end
9195
end
9296
end
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# frozen_string_literal: true
2+
3+
module BetterTogether
4+
# Sends email notifications for page authorship changes
5+
class AuthorshipMailer < ApplicationMailer
6+
# rubocop:todo Metrics/PerceivedComplexity
7+
# rubocop:todo Metrics/MethodLength
8+
def authorship_changed_notification # rubocop:todo Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
9+
@platform = BetterTogether::Platform.find_by(host: true)
10+
@page = params[:page]
11+
@recipient = params[:recipient]
12+
@action = params[:action]
13+
@actor_name = params[:actor_name]
14+
@actor_name ||= BetterTogether::Person.find_by(id: params[:actor_id])&.name if params[:actor_id]
15+
16+
subject = if @action == 'removed'
17+
if @actor_name.present?
18+
t('better_together.authorship_mailer.authorship_changed_notification.subject_removed_by',
19+
page: @page.title, actor_name: @actor_name)
20+
else
21+
t('better_together.authorship_mailer.authorship_changed_notification.subject_removed',
22+
page: @page.title)
23+
end
24+
elsif @actor_name.present?
25+
t('better_together.authorship_mailer.authorship_changed_notification.subject_added_by',
26+
page: @page.title, actor_name: @actor_name)
27+
else
28+
t('better_together.authorship_mailer.authorship_changed_notification.subject_added',
29+
page: @page.title)
30+
end
31+
32+
# Respect locale and time zone preferences
33+
self.locale = @recipient.locale
34+
self.time_zone = @recipient.time_zone
35+
36+
mail(to: @recipient.email, subject:)
37+
end
38+
# rubocop:enable Metrics/MethodLength
39+
# rubocop:enable Metrics/PerceivedComplexity
40+
end
41+
end

app/models/better_together/authorship.rb

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,82 @@ module BetterTogether
44
# Connects an author (eg: person) to an authorable (eg: post)
55
class Authorship < ApplicationRecord
66
include Positioned
7+
include BetterTogether::Creatable
8+
9+
# Per-request creator context for assigning creator_id during author adds
10+
thread_mattr_accessor :creator_context_id
11+
12+
before_validation :assign_creator_from_context, on: :create
13+
14+
# Set creator context for any authorship creations within the block
15+
def self.with_creator(person)
16+
previous = creator_context_id
17+
self.creator_context_id = person&.id
18+
yield
19+
ensure
20+
self.creator_context_id = previous
21+
end
722

823
belongs_to :author,
924
class_name: 'BetterTogether::Person'
1025
belongs_to :authorable,
1126
polymorphic: true
27+
28+
# Notify authors when they are added to or removed from a Page
29+
after_commit :notify_added_to_page, on: :create
30+
after_commit :notify_removed_from_page, on: :destroy
31+
32+
private
33+
34+
def assign_creator_from_context
35+
self.creator_id ||= self.class.creator_context_id
36+
end
37+
38+
# rubocop:todo Metrics/AbcSize
39+
# rubocop:todo Metrics/PerceivedComplexity
40+
# rubocop:todo Metrics/MethodLength
41+
# rubocop:todo Metrics/CyclomaticComplexity
42+
def notify_added_to_page # rubocop:todo Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
43+
return unless authorable.is_a?(BetterTogether::Page)
44+
# Skip notifying if the assigned author created this authorship
45+
return if creator_id.present? && creator_id == author_id
46+
47+
# Also skip if the current actor is the same person being added
48+
return if defined?(::Current) && ::Current.respond_to?(:person) && (::Current.person&.id == author_id)
49+
50+
actor = defined?(::Current) && ::Current.respond_to?(:person) ? ::Current.person : nil
51+
BetterTogether::PageAuthorshipNotifier
52+
.with(record: authorable,
53+
page_id: authorable.id,
54+
action: 'added',
55+
actor_id: actor&.id,
56+
actor_name: actor&.name)
57+
.deliver_later(author)
58+
end
59+
# rubocop:enable Metrics/CyclomaticComplexity
60+
# rubocop:enable Metrics/MethodLength
61+
# rubocop:enable Metrics/PerceivedComplexity
62+
63+
# rubocop:enable Metrics/AbcSize
64+
# rubocop:todo Metrics/MethodLength
65+
def notify_removed_from_page # rubocop:todo Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
66+
return unless authorable.is_a?(BetterTogether::Page)
67+
68+
# Skip notifying when the acting person equals the removed author.
69+
# Prefer creator_context_id (thread-local) when provided, otherwise fall back to Current.person when available.
70+
return if self.class.creator_context_id.present? && self.class.creator_context_id == author_id
71+
# Skip notifying if the person removing is the same as the removed author
72+
return if defined?(::Current) && ::Current.respond_to?(:person) && (::Current.person&.id == author_id)
73+
74+
actor = defined?(::Current) && ::Current.respond_to?(:person) ? ::Current.person : nil
75+
BetterTogether::PageAuthorshipNotifier
76+
.with(record: authorable,
77+
page_id: authorable.id,
78+
action: 'removed',
79+
actor_id: actor&.id,
80+
actor_name: actor&.name)
81+
.deliver_later(author)
82+
end
83+
# rubocop:enable Metrics/MethodLength
1284
end
1385
end

app/models/better_together/page.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ module BetterTogether
44
# An informational document used to display custom content to the user
55
class Page < ApplicationRecord
66
include Authorable
7+
# When adding authors via `author_ids=` or association ops, controllers can
8+
# set BetterTogether::Authorship.creator_context_id = current_person.id
9+
# to stamp newly-created authorships with the acting person.
710
include Categorizable
811
include Identifier
912
include Protected
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# frozen_string_literal: true
2+
3+
module BetterTogether
4+
# Notifies a person when added to or removed from a Page as an author
5+
class PageAuthorshipNotifier < ApplicationNotifier
6+
deliver_by :action_cable, channel: 'BetterTogether::NotificationsChannel', message: :build_message
7+
8+
deliver_by :email,
9+
mailer: 'BetterTogether::AuthorshipMailer',
10+
method: :authorship_changed_notification,
11+
params: :email_params do |config|
12+
config.wait = 15.minutes
13+
config.if = -> { send_email_notification? }
14+
end
15+
16+
validates :record, presence: true
17+
18+
# record is the Page; keep naming helpers similar to NewMessageNotifier
19+
def page
20+
record
21+
end
22+
23+
def action
24+
params[:action]
25+
end
26+
27+
def actor_id
28+
params[:actor_id]
29+
end
30+
31+
def actor_name
32+
params[:actor_name]
33+
end
34+
35+
def actor
36+
@actor ||= BetterTogether::Person.find_by(id: actor_id)
37+
end
38+
39+
notification_methods do
40+
delegate :page, to: :event
41+
delegate :url, to: :event
42+
delegate :identifier, to: :event
43+
delegate :action, to: :event
44+
delegate :actor, to: :event
45+
delegate :actor_name, to: :event
46+
47+
def send_email_notification?
48+
recipient.email.present? && recipient.notify_by_email && should_send_email?
49+
end
50+
51+
def should_send_email?
52+
# Mirror conversation notifier grouping by related record (page)
53+
unread_notifications = recipient.notifications.where(
54+
event_id: BetterTogether::PageAuthorshipNotifier.where(params: { page_id: page.id }).select(:id),
55+
read_at: nil
56+
).order(created_at: :desc)
57+
58+
if unread_notifications.none?
59+
false
60+
else
61+
# Only send one email per unread notifications per page
62+
page.id == unread_notifications.last.event.record_id
63+
end
64+
end
65+
end
66+
67+
def identifier
68+
page.id
69+
end
70+
71+
def url
72+
page.url
73+
end
74+
75+
# rubocop:todo Metrics/MethodLength
76+
def title # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
77+
name = actor_name || actor&.name
78+
if action == 'removed'
79+
if name.present?
80+
I18n.t('better_together.page_authorship_notifier.removed_by', page_title: page.title, actor_name: name)
81+
else
82+
I18n.t('better_together.page_authorship_notifier.removed', page_title: page.title)
83+
end
84+
elsif name.present?
85+
I18n.t('better_together.page_authorship_notifier.added_by', page_title: page.title, actor_name: name)
86+
else
87+
I18n.t('better_together.page_authorship_notifier.added', page_title: page.title)
88+
end
89+
end
90+
91+
# rubocop:enable Metrics/MethodLength
92+
def body
93+
# Keep body concise; UI partial will display details
94+
title
95+
end
96+
97+
def build_message(notification)
98+
{
99+
title:,
100+
body:,
101+
identifier:,
102+
url:,
103+
unread_count: notification.recipient.notifications.unread.count
104+
}
105+
end
106+
107+
def email_params(notification)
108+
{ page: notification.record, action: action, actor_id:, actor_name: }
109+
end
110+
end
111+
end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<!-- app/views/better_together/authorship_mailer/authorship_changed_notification.html.erb -->
2+
3+
<p><%= t('.greeting', recipient_name: @recipient.name) %></p>
4+
5+
<% if @action == 'removed' %>
6+
<% if @actor_name.present? %>
7+
<p><%= t('.intro_removed_by', page: @page.title, actor_name: @actor_name) %></p>
8+
<% else %>
9+
<p><%= t('.intro_removed', page: @page.title) %></p>
10+
<% end %>
11+
<% else %>
12+
<% if @actor_name.present? %>
13+
<p><%= t('.intro_added_by', page: @page.title, actor_name: @actor_name) %></p>
14+
<% else %>
15+
<p><%= t('.intro_added', page: @page.title) %></p>
16+
<% end %>
17+
<% end %>
18+
19+
<p><%= t('.view_page') %></p>
20+
21+
<p><%= link_to t('.view_page_link'), @page.url %></p>
22+
23+
<p><%= t('.signature_html', platform: @platform.name) %></p>

0 commit comments

Comments
 (0)