Add plain text mentions

This commit is contained in:
Jorge Manrubia
2025-04-22 11:21:30 +02:00
parent 5b10b3a16e
commit aa3acfeaa8
41 changed files with 493 additions and 168 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ class Cards::ReadingsController < ApplicationController
include CardScoped
def create
@notifications = Current.user.notifications.where(card: @card)
@notifications = Current.user.notifications.where(source: @card.events)
@notifications.each(&:read)
end
end
+18 -27
View File
@@ -1,21 +1,25 @@
module NotificationsHelper
def notification_title(notification)
if notification.resource.is_a? Comment
"RE: " + notification.card.title
elsif notification_event_action(notification) == "assigned"
"Assigned to you: " + notification.card.title
def event_notification_title(event)
if event.commented?
"RE: " + event.card.title
elsif event.assignment?
"Assigned to you: " + event.card.title
else
notification.card.title
event.card.title
end
end
def notification_body(notification)
name = notification.creator.name
def event_notification_body(event)
name = event.creator.name
case notification_event_action(notification)
when "closed" then "Closed by #{name}"
when "published" then "Added by #{name}"
else name
if event.closed?
"Closed by #{name}"
elsif event.published?
"Added by #{name}"
elsif event.commented?
"#{strip_tags(event.comment.body_html).blank? ? "#{name} replied" : "#{event.creator.name}:" } #{strip_tags(event.comment.body_html).truncate(200)}"
else
name
end
end
@@ -36,8 +40,8 @@ module NotificationsHelper
class: "notification__unread_indicator btn borderless",
title: "Mark as read",
data: { turbo_frame: "_top" } do
concat(image_tag("remove-med.svg", class: "unread_icon", size: 12, aria: { hidden: true }))
concat(tag.span("Mark as read", class: "for-screen-reader"))
concat(image_tag("remove-med.svg", class: "unread_icon", size: 12, aria: { hidden: true }))
concat(tag.span("Mark as read", class: "for-screen-reader"))
end
end
@@ -46,17 +50,4 @@ module NotificationsHelper
tag.div id: "next_page", data: { controller: "fetch-on-visible", fetch_on_visible_url_value: notifications_path(page: @page.next_param) }
end
end
private
def notification_event_action(notification)
if notification_is_for_initial_assignement?(notification)
"assigned"
else
notification.event.action
end
end
def notification_is_for_initial_assignement?(notification)
notification.event.action == "published" && notification.card.assigned_to?(notification.user)
end
end
-7
View File
@@ -1,7 +0,0 @@
class GenerateNotificationsJob < ApplicationJob
queue_as :default
def perform(event)
event.generate_notifications
end
end
+7
View File
@@ -0,0 +1,7 @@
class Mention::CollectJob < ApplicationJob
queue_as :default
def perform(record, mentioner:)
record.collect_mentions(mentioner:)
end
end
+7
View File
@@ -0,0 +1,7 @@
class NotifyRecipientsJob < ApplicationJob
queue_as :default
def perform(notifiable)
notifiable.notify_recipients
end
end
+2 -5
View File
@@ -1,13 +1,10 @@
class Card < ApplicationRecord
include Assignable, Colored, Engageable, Eventable, Golden,
Messages, Notifiable, Pinnable, Closeable, Searchable, Staged,
Statuses, Taggable, Watchable
include Assignable, Colored, Engageable, Eventable, Golden, Mentions,
Messages, Pinnable, Closeable, Searchable, Staged, Statuses, Taggable, Watchable
belongs_to :collection, touch: true
belongs_to :creator, class_name: "User", default: -> { Current.user }
has_many :notifications, dependent: :destroy
has_one_attached :image, dependent: :purge_later
has_markdown :description
+1 -2
View File
@@ -12,8 +12,7 @@ module Card::Eventable
def track_event(action, creator: Current.user, **particulars)
if published?
event = find_or_capture_event_summary.events.create! action: action, creator: creator, card: self, particulars: particulars
event.generate_notifications_later
find_or_capture_event_summary.events.create! action: action, creator: creator, card: self, particulars: particulars
end
end
+12
View File
@@ -0,0 +1,12 @@
module Card::Mentions
extend ActiveSupport::Concern
included do
include ::Mentions
end
private
def mentionable_content
description.to_plain_text
end
end
+33
View File
@@ -0,0 +1,33 @@
module Mentions
extend ActiveSupport::Concern
included do
has_many :mentions, as: :container, dependent: :destroy
after_commit :collect_mentions_later, on: %i[ create update ]
end
def collect_mentions(mentioner: Current.user)
scan_mentionees.each do |mentionee|
mentionee.mentioned_by mentioner, at: self
end
end
private
def collect_mentions_later
Mention::CollectJob.perform_later(self, mentioner: Current.user)
end
def scan_mentionees
scan_mentioned_handles.filter_map do |mention|
mentionable_users.find { |user| user.mentionable_handles.include?(mention) }
end
end
def mentionable_users
collection.users
end
def scan_mentioned_handles
(mentionable_content || "").scan(/(?<!\w)@(\w+)/).flatten.uniq(&:downcase)
end
end
+12 -1
View File
@@ -2,6 +2,17 @@ module Notifiable
extend ActiveSupport::Concern
included do
has_many :notifications, as: :resource, dependent: :destroy
has_many :notifications, as: :source, dependent: :destroy
after_create_commit :notify_recipients_later
end
def notify_recipients
Notifier.for(self)&.notify
end
private
def notify_recipients_later
NotifyRecipientsJob.perform_later self
end
end
+14 -8
View File
@@ -1,5 +1,5 @@
class Event < ApplicationRecord
include Particulars
include Notifiable, Particulars
belongs_to :creator, class_name: "User"
belongs_to :summary, touch: true, class_name: "EventSummary"
@@ -12,15 +12,21 @@ class Event < ApplicationRecord
after_create -> { card.touch(:last_active_at) }
def commented?
action == "commented"
def assignment?
action == "assigned" || initial_assignment?
end
def generate_notifications
Notifier.for(self)&.generate
# E.g: completed? is true if action == "completed"
def method_missing(method_name, *args, &block)
if method_name.to_s.end_with?("?")
action == method_name.to_s.chomp("?")
else
super
end
end
def generate_notifications_later
GenerateNotificationsJob.perform_later self
end
private
def initial_assignment?
action == "published" && card.assigned_to?(creator)
end
end
+7
View File
@@ -0,0 +1,7 @@
class Mention < ApplicationRecord
include Notifiable
belongs_to :container, polymorphic: true
belongs_to :mentionee, class_name: "User", inverse_of: :mentions
belongs_to :mentioner, class_name: "User"
end
+4
View File
@@ -1,7 +1,11 @@
class Message < ApplicationRecord
include Mentions
belongs_to :card, touch: true
delegated_type :messageable, types: Messageable::TYPES, inverse_of: :message, dependent: :destroy
scope :chronologically, -> { order created_at: :asc, id: :desc }
delegate :collection, to: :card
end
+12
View File
@@ -0,0 +1,12 @@
module Message::Mentions
extend ActiveSupport::Concern
included do
include ::Mentions
end
private
def mentionable_content
messageable.try(:body_plain_text)
end
end
+2 -3
View File
@@ -1,14 +1,13 @@
class Notification < ApplicationRecord
belongs_to :user
belongs_to :event
belongs_to :card
belongs_to :source, polymorphic: true
belongs_to :resource, polymorphic: true
scope :unread, -> { where(read_at: nil) }
scope :read, -> { where.not(read_at: nil) }
scope :ordered, -> { order(read_at: :desc, created_at: :desc) }
delegate :creator, to: :event
delegate :creator, to: :source
after_create_commit :broadcast_unread
def self.read_all
+15 -18
View File
@@ -1,40 +1,37 @@
class Notifier
attr_reader :event
attr_reader :source
delegate :creator, to: :event
delegate :creator, to: :source
class << self
def for(event)
"Notifier::#{event.action.classify}".safe_constantize&.new(event)
def for(source)
case source
when Event
"Notifier::Events::#{source.action.classify}".safe_constantize&.new(source)
when Mention
Notifier::Mentions
end
end
end
def generate
def notify
if should_notify?
recipients.map do |recipient|
Notification.create! user: recipient, event: event, card: card, resource: resource
Notification.create! user: recipient, source: source, resource: resource
end
end
end
private
def initialize(event)
@event = event
def initialize(source)
@source = source
end
def should_notify?
!event.creator.system?
end
def recipients
card.watchers_and_subscribers.without(creator)
!creator.system?
end
def resource
card
end
def card
event.summary.message.card
source
end
end
-6
View File
@@ -1,6 +0,0 @@
class Notifier::Assigned < Notifier
private
def recipients
event.assignees.excluding(card.collection.access_only_users)
end
end
-2
View File
@@ -1,2 +0,0 @@
class Notifier::Closed < Notifier
end
-6
View File
@@ -1,6 +0,0 @@
class Notifier::Commented < Notifier
private
def resource
event.comment
end
end
+6
View File
@@ -0,0 +1,6 @@
class Notifier::Events::Assigned < Notifier::Events::Base
private
def recipients
source.assignees.excluding(card.collection.access_only_users)
end
end
+12
View File
@@ -0,0 +1,12 @@
class Notifier::Events::Base < Notifier
delegate :card, to: :source
private
def resource
card
end
def recipients
card.watchers_and_subscribers.without(creator)
end
end
+2
View File
@@ -0,0 +1,2 @@
class Notifier::Events::Closed < Notifier::Events::Base
end
+6
View File
@@ -0,0 +1,6 @@
class Notifier::Events::Commented < Notifier::Events::Base
private
def resource
source.comment
end
end
@@ -1,4 +1,4 @@
class Notifier::Published < Notifier
class Notifier::Events::Published < Notifier::Events::Base
private
def recipients
card.watchers_and_subscribers(include_only_watching: true).without(creator)
+1 -7
View File
@@ -1,5 +1,5 @@
class User < ApplicationRecord
include Accessor, Assignee, Avatar, Role, Transferable
include Accessor, Assignee, Avatar, Mentionable, Named, Role, Transferable
has_many :sessions, dependent: :destroy
has_secure_password validations: false
@@ -15,12 +15,6 @@ class User < ApplicationRecord
normalizes :email_address, with: ->(value) { value.strip.downcase }
scope :alphabetically, -> { order("lower(name)") }
def initials
name.to_s.scan(/\b\p{L}/).join.upcase
end
def deactivate
transaction do
sessions.delete_all
+20
View File
@@ -0,0 +1,20 @@
module User::Mentionable
extend ActiveSupport::Concern
included do
has_many :mentions, dependent: :destroy, inverse_of: :mentionee
end
def mentioned_by(mentioner, at:)
mentions.create! container: at, mentioner:
end
def mentionable_handles
[ initials, first_name, first_name_with_last_name_initial ].collect(&:downcase)
end
private
def first_name_with_last_name_initial
"#{first_name}#{last_name&.first}"
end
end
+19
View File
@@ -0,0 +1,19 @@
module User::Named
extend ActiveSupport::Concern
included do
scope :alphabetically, -> { order("lower(name)") }
end
def first_name
name.split(/\s/).first
end
def last_name
name.split(/\s/, 2).last
end
def initials
name.scan(/\b\p{L}/).join.upcase
end
end
@@ -5,15 +5,7 @@
</div>
<div class="flex flex-column min-width flex-item-grow">
<strong class="overflow-ellipsis notification__title txt-small txt-tight-lines"><%= notification_title(notification) %></strong>
<div class="overflow-ellipsis txt-small txt-tight-lines">
<% if notification.event.action == "commented" %>
<%= "#{strip_tags(notification.event.comment.body_html).blank? ? "#{notification.event.creator.name} replied" : "#{notification.event.creator.name}:" } #{strip_tags(notification.event.comment.body_html).truncate(200)}" %>
<% else %>
<%= notification_body(notification) %>
<% end %>
</div>
<div class="tray__item-meta overflow-ellipsis translucent"><%= notification.card.collection.name %> · <%= local_datetime_tag(notification.created_at, style: :ago) %></div>
<%= render "notifications/notification/#{notification.source_type.underscore}", notification: notification %>
</div>
<% end %>
<% end %>
@@ -0,0 +1,7 @@
<%= event = notification.source %>
<strong class="overflow-ellipsis notification__title txt-small txt-tight-lines"><%= event_notification_title(event) %></strong>
<div class="overflow-ellipsis txt-small txt-tight-lines">
<%= event_notification_body(event) %>
</div>
<div class="tray__item-meta overflow-ellipsis translucent"><%= notification.source.card.collection.name %> · <%= local_datetime_tag(notification.created_at, style: :ago) %></div>
@@ -0,0 +1,11 @@
<% event = notification.event %>
<strong class="overflow-ellipsis notification__title txt-small txt-tight-lines"><%= event_notification_title(notification) %></strong>
<div class="overflow-ellipsis txt-small txt-tight-lines">
<% if event.action == "commented" %>
<%= "#{strip_tags(event.comment.body_html).blank? ? "#{event.creator.name} replied" : "#{event.creator.name}:" } #{strip_tags(event.comment.body_html).truncate(200)}" %>
<% else %>
<%= event_notification_body(notification) %>
<% end %>
</div>
<div class="tray__item-meta overflow-ellipsis translucent"><%= event.card.collection.name %> · <%= local_datetime_tag(notification.created_at, style: :ago) %></div>
@@ -0,0 +1,11 @@
class CreateMentions < ActiveRecord::Migration[8.1]
def change
create_table :mentions do |t|
t.references :container, polymorphic: true, null: false, index: true
t.references :mentionee, foreign_key: { to_table: :users }, null: false
t.references :mentioner, foreign_key: { to_table: :users }, null: false
t.timestamps
end
end
end
@@ -0,0 +1,11 @@
class AddSourceToNotifications < ActiveRecord::Migration[8.1]
def change
add_reference :notifications, :source, polymorphic: true, index: true
execute <<~SQL
update notifications set source_type = 'Event'
SQL
change_column_null :notifications, :source_type, false
end
end
@@ -0,0 +1,6 @@
class RemoveUnusedColumnsFromNotifications < ActiveRecord::Migration[8.1]
def change
remove_column :notifications, :event_id
remove_column :notifications, :card_id
end
end
Generated
+18 -7
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2025_04_18_150259) do
ActiveRecord::Schema[8.1].define(version: 2025_04_22_062930) do
create_table "accesses", force: :cascade do |t|
t.integer "collection_id", null: false
t.datetime "created_at", null: false
@@ -212,6 +212,18 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_18_150259) do
t.index ["tag_id"], name: "index_filters_tags_on_tag_id"
end
create_table "mentions", force: :cascade do |t|
t.integer "container_id", null: false
t.string "container_type", null: false
t.datetime "created_at", null: false
t.integer "mentionee_id", null: false
t.integer "mentioner_id", null: false
t.datetime "updated_at", null: false
t.index ["container_type", "container_id"], name: "index_mentions_on_container"
t.index ["mentionee_id"], name: "index_mentions_on_mentionee_id"
t.index ["mentioner_id"], name: "index_mentions_on_mentioner_id"
end
create_table "messages", force: :cascade do |t|
t.integer "card_id", null: false
t.datetime "created_at", null: false
@@ -223,17 +235,16 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_18_150259) do
end
create_table "notifications", force: :cascade do |t|
t.integer "card_id", null: false
t.datetime "created_at", null: false
t.integer "event_id", null: false
t.datetime "read_at"
t.integer "resource_id", null: false
t.string "resource_type", null: false
t.integer "source_id"
t.string "source_type", null: false
t.datetime "updated_at", null: false
t.integer "user_id", null: false
t.index ["card_id"], name: "index_notifications_on_card_id"
t.index ["event_id"], name: "index_notifications_on_event_id"
t.index ["resource_type", "resource_id"], name: "index_notifications_on_resource"
t.index ["source_type", "source_id"], name: "index_notifications_on_source"
t.index ["user_id", "read_at", "created_at"], name: "index_notifications_on_user_id_and_read_at_and_created_at", order: { read_at: :desc, created_at: :desc }
t.index ["user_id"], name: "index_notifications_on_user_id"
end
@@ -328,9 +339,9 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_18_150259) do
add_foreign_key "collections", "workflows"
add_foreign_key "events", "cards"
add_foreign_key "events", "event_summaries", column: "summary_id"
add_foreign_key "mentions", "users", column: "mentionee_id"
add_foreign_key "mentions", "users", column: "mentioner_id"
add_foreign_key "messages", "cards"
add_foreign_key "notifications", "cards"
add_foreign_key "notifications", "events"
add_foreign_key "notifications", "users"
add_foreign_key "pins", "cards"
add_foreign_key "pins", "users"
+134 -44
View File
@@ -637,6 +637,50 @@ columns:
default_function:
collation:
comment:
mentions:
- *5
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: container_type
cast_type: *7
sql_type_metadata: *8
'null': false
default:
default_function:
collation:
comment:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: container_id
cast_type: *1
sql_type_metadata: *2
'null': false
default:
default_function:
collation:
comment:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: mentionee_id
cast_type: *1
sql_type_metadata: *2
'null': false
default:
default_function:
collation:
comment:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: mentioner_id
cast_type: *1
sql_type_metadata: *2
'null': false
default:
default_function:
collation:
comment:
- *6
- *9
messages:
- *5
- *21
@@ -664,18 +708,7 @@ columns:
- *9
notifications:
- *5
- *21
- *6
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: event_id
cast_type: *1
sql_type_metadata: *2
'null': false
default:
default_function:
collation:
comment:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: read_at
@@ -708,6 +741,26 @@ columns:
comment:
- *9
- *28
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: source_type
cast_type: *7
sql_type_metadata: *8
'null': false
default:
default_function:
collation:
comment:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: source_id
cast_type: *1
sql_type_metadata: *2
'null': true
default:
default_function:
collation:
comment:
pins:
- *5
- *21
@@ -921,6 +974,7 @@ primary_keys:
filters: id
filters_stages:
filters_tags:
mentions: id
messages: id
notifications: id
pins: id
@@ -958,6 +1012,7 @@ data_sources:
filters: true
filters_stages: true
filters_tags: true
mentions: true
messages: true
notifications: true
pins: true
@@ -1598,6 +1653,56 @@ indexes:
nulls_not_distinct:
comment:
valid: true
mentions:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: mentions
name: index_mentions_on_mentioner_id
unique: false
columns:
- mentioner_id
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: mentions
name: index_mentions_on_mentionee_id
unique: false
columns:
- mentionee_id
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: mentions
name: index_mentions_on_container
unique: false
columns:
- container_type
- container_id
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
messages:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: messages
@@ -1633,6 +1738,23 @@ indexes:
comment:
valid: true
notifications:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: notifications
name: index_notifications_on_source
unique: false
columns:
- source_type
- source_id
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: notifications
name: index_notifications_on_user_id
@@ -1686,38 +1808,6 @@ indexes:
nulls_not_distinct:
comment:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: notifications
name: index_notifications_on_event_id
unique: false
columns:
- event_id
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: notifications
name: index_notifications_on_card_id
unique: false
columns:
- card_id
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
pins:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: pins
@@ -1938,4 +2028,4 @@ indexes:
comment:
valid: true
workflows: []
version: 20250418150259
version: 20250422062930
+3 -6
View File
@@ -1,20 +1,17 @@
logo_published_kevin:
user: kevin
event: logo_published
card: logo
source: logo_published (Event)
resource: logo (Card)
created_at: <%= 1.week.ago %>
logo_assignment_kevin:
user: kevin
event: logo_assignment_km
card: logo
source: logo_assignment_km (Event)
resource: logo (Card)
created_at: <%= 1.week.ago %>
layout_commented_kevin:
user: kevin
event: layout_commented
card: layout
source: layout_commented (Event)
resource: layout_overflowing_david (Comment)
created_at: <%= 1.week.ago %>
+23
View File
@@ -0,0 +1,23 @@
require "test_helper"
class MentionsTest < ActiveSupport::TestCase
setup do
Current.session = sessions(:david)
end
test "collect mentions on create" do
assert_difference -> { Mention.count }, +1 do
perform_enqueued_jobs only: Mention::CollectJob do
collections(:writebook).cards.create title: "Cleanup", description: "Did you finish up with the cleanup, @david?"
end
end
end
test "collect mentions on update" do
assert_difference -> { Mention.count }, +1 do
perform_enqueued_jobs only: Mention::CollectJob do
cards(:logo).update! description: "Did you finish up with the cleanup, @david?"
end
end
end
end
+8 -8
View File
@@ -2,7 +2,7 @@ require "test_helper"
class NotifierTest < ActiveSupport::TestCase
test "for returns the matching notifier class for the event" do
assert_kind_of Notifier::Published, Notifier.for(events(:logo_published))
assert_kind_of Notifier::Events::Published, Notifier.for(events(:logo_published))
end
test "generate does not create notifications if the event was system-generated" do
@@ -10,12 +10,12 @@ class NotifierTest < ActiveSupport::TestCase
events(:logo_published).update!(creator: User.system)
assert_no_difference -> { Notification.count } do
Notifier.for(events(:logo_published)).generate
Notifier.for(events(:logo_published)).notify
end
end
test "creates a notification for each watcher, other than the event creator" do
notifications = Notifier.for(events(:layout_commented)).generate
notifications = Notifier.for(events(:layout_commented)).notify
assert_equal [ users(:kevin) ], notifications.map(&:user)
end
@@ -23,19 +23,19 @@ class NotifierTest < ActiveSupport::TestCase
test "does not create a notification for access-only users" do
collections(:writebook).access_for(users(:kevin)).access_only!
notifications = Notifier.for(events(:layout_commented)).generate
notifications = Notifier.for(events(:layout_commented)).notify
assert_equal [ users(:kevin) ], notifications.map(&:user)
end
test "the published event creates notifications for subscribers as well as watchers" do
notifications = Notifier.for(events(:logo_published)).generate
notifications = Notifier.for(events(:logo_published)).notify
assert_equal users(:kevin, :jz).sort, notifications.map(&:user).sort
end
test "links to the card" do
Notifier.for(events(:logo_published)).generate
Notifier.for(events(:logo_published)).notify
assert_equal cards(:logo), Notification.last.resource
end
@@ -44,7 +44,7 @@ class NotifierTest < ActiveSupport::TestCase
collections(:writebook).access_for(users(:jz)).watching!
collections(:writebook).access_for(users(:kevin)).everything!
notifications = Notifier.for(events(:logo_assignment_jz)).generate
notifications = Notifier.for(events(:logo_assignment_jz)).notify
assert_equal [ users(:jz) ], notifications.map(&:user)
end
@@ -52,7 +52,7 @@ class NotifierTest < ActiveSupport::TestCase
test "assignment events do not notify users who are access-only for the collection" do
collections(:writebook).access_for(users(:jz)).access_only!
notifications = Notifier.for(events(:logo_assignment_jz)).generate
notifications = Notifier.for(events(:logo_assignment_jz)).notify
assert_empty notifications
end
+7
View File
@@ -0,0 +1,7 @@
require 'test_helper'
class User::MentionableTest < ActiveSupport::TestCase
test "mentionable handles" do
assert_equal [ "dhh", "david", "davidh" ], User.new(name: "David Heinemeier-Hansson").mentionable_handles
end
end
+38
View File
@@ -0,0 +1,38 @@
require "test_helper"
class User::NamedTest < ActiveSupport::TestCase
test "initials" do
assert_initials "M", name: "Michael"
assert_initials "SD", name: "Salvador Dali"
assert_initials "LMM", name: "Lin-Manuel Miranda"
assert_initials "OCD", name: "O'Conor Díez"
assert_initials "ACG", name: "Anne Christine García"
assert_initials "ÁL", name: "Ángela López"
end
test "first name" do
assert_first_name "Michael", "Michael"
assert_first_name "Salvador", "Salvador Dali"
assert_first_name "Lin-Manuel", "Lin-Manuel Miranda"
assert_first_name "Anne", "Anne Christine García"
end
test "last name" do
assert_last_name "Dali", "Salvador Dali"
assert_last_name "Miranda", "Lin_Manuel Miranda"
assert_last_name "Christine García", "Anne Christine García"
end
private
def assert_initials(expected, **attributes)
assert_equal expected, User.new(attributes).initials
end
def assert_first_name(expected, name)
assert_equal expected, User.new(name: name).first_name
end
def assert_last_name(expected, name)
assert_equal expected, User.new(name: name).last_name
end
end
+1
View File
@@ -10,6 +10,7 @@ module ActiveSupport
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
include ActiveJob::TestHelper
include CardTestHelper, ChangeTestHelper, SessionTestHelper
end
end