diff --git a/app/controllers/cards/readings_controller.rb b/app/controllers/cards/readings_controller.rb
index 2d8b51ac6..efd06e601 100644
--- a/app/controllers/cards/readings_controller.rb
+++ b/app/controllers/cards/readings_controller.rb
@@ -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
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index 656edad54..4568ad82a 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -1,20 +1,19 @@
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
- else
- notification.card.title
+ def event_notification_title(event)
+ case event_notification_action(event)
+ when "commented" then "RE: " + event.card.title
+ when "assigned" then "Assigned to you: " + event.card.title
+ else 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)
+ case event_notification_action(event)
when "closed" then "Closed by #{name}"
when "published" then "Added by #{name}"
+ when "commented" then comment_notification_body(event)
else name
end
end
@@ -22,7 +21,7 @@ module NotificationsHelper
def notification_tag(notification, &)
tag.div id: dom_id(notification), class: "notification tray__item border-radius txt-normal" do
concat(
- link_to(notification.resource,
+ link_to(notification,
class: "notification__content border-radius shadow fill-white flex align-start txt-align-start gap flex-item-grow max-width border txt-ink",
data: { action: "click->dialog#close", turbo_frame: "_top" },
&)
@@ -33,11 +32,11 @@ module NotificationsHelper
def notification_mark_read_button(notification)
button_to read_notification_path(notification),
- 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"))
+ 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"))
end
end
@@ -48,15 +47,15 @@ module NotificationsHelper
end
private
- def notification_event_action(notification)
- if notification_is_for_initial_assignement?(notification)
+ def event_notification_action(event)
+ if event.initial_assignment?
"assigned"
else
- notification.event.action
+ event.action
end
end
- def notification_is_for_initial_assignement?(notification)
- notification.event.action == "published" && notification.card.assigned_to?(notification.user)
+ def comment_notification_body(event)
+ "#{strip_tags(event.comment.body_html).blank? ? "#{name} replied" : "#{event.creator.name}:" } #{strip_tags(event.comment.body_html).truncate(200)}"
end
end
diff --git a/app/jobs/card/auto_close_all_due_job.rb b/app/jobs/card/auto_close_all_due_job.rb
index 6cb4f646b..d964dae23 100644
--- a/app/jobs/card/auto_close_all_due_job.rb
+++ b/app/jobs/card/auto_close_all_due_job.rb
@@ -1,6 +1,4 @@
class Card::AutoCloseAllDueJob < ApplicationJob
- queue_as :default
-
def perform
ApplicationRecord.with_each_tenant do |tenant|
Card.auto_close_all_due
diff --git a/app/jobs/card/auto_reconsider_all_stagnated_job.rb b/app/jobs/card/auto_reconsider_all_stagnated_job.rb
index 83f87a648..b7178f916 100644
--- a/app/jobs/card/auto_reconsider_all_stagnated_job.rb
+++ b/app/jobs/card/auto_reconsider_all_stagnated_job.rb
@@ -1,6 +1,4 @@
class Card::AutoReconsiderAllStagnatedJob < ApplicationJob
- queue_as :default
-
def perform
ApplicationRecord.with_each_tenant do |tenant|
Card.auto_reconsider_all_stagnated
diff --git a/app/jobs/generate_notifications_job.rb b/app/jobs/generate_notifications_job.rb
deleted file mode 100644
index e386b7b37..000000000
--- a/app/jobs/generate_notifications_job.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class GenerateNotificationsJob < ApplicationJob
- queue_as :default
-
- def perform(event)
- event.generate_notifications
- end
-end
diff --git a/app/jobs/mention/create_job.rb b/app/jobs/mention/create_job.rb
new file mode 100644
index 000000000..816782754
--- /dev/null
+++ b/app/jobs/mention/create_job.rb
@@ -0,0 +1,5 @@
+class Mention::CreateJob < ApplicationJob
+ def perform(record, mentioner:)
+ record.create_mentions(mentioner:)
+ end
+end
diff --git a/app/jobs/notify_recipients_job.rb b/app/jobs/notify_recipients_job.rb
new file mode 100644
index 000000000..6083622fd
--- /dev/null
+++ b/app/jobs/notify_recipients_job.rb
@@ -0,0 +1,5 @@
+class NotifyRecipientsJob < ApplicationJob
+ def perform(notifiable)
+ notifiable.notify_recipients
+ end
+end
diff --git a/app/jobs/remove_abandoned_creations_job.rb b/app/jobs/remove_abandoned_creations_job.rb
index 1444313a2..6ca63169d 100644
--- a/app/jobs/remove_abandoned_creations_job.rb
+++ b/app/jobs/remove_abandoned_creations_job.rb
@@ -1,6 +1,4 @@
class RemoveAbandonedCreationsJob < ApplicationJob
- queue_as :default
-
def perform
ApplicationRecord.with_each_tenant do |tenant|
Card.remove_abandoned_creations
diff --git a/app/models/card.rb b/app/models/card.rb
index 75f8c4807..808ff9467 100644
--- a/app/models/card.rb
+++ b/app/models/card.rb
@@ -1,12 +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
@@ -32,4 +30,8 @@ class Card < ApplicationRecord
def cache_key
[ super, collection.name ].compact.join("/")
end
+
+ def was_mentioned(mention)
+ watch_by(mention.mentionee)
+ end
end
diff --git a/app/models/card/eventable.rb b/app/models/card/eventable.rb
index 1d3c4e13b..55a0245ea 100644
--- a/app/models/card/eventable.rb
+++ b/app/models/card/eventable.rb
@@ -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
diff --git a/app/models/comment.rb b/app/models/comment.rb
index 8922b30b7..9b3fd2704 100644
--- a/app/models/comment.rb
+++ b/app/models/comment.rb
@@ -1,5 +1,5 @@
class Comment < ApplicationRecord
- include Messageable, Searchable
+ include Mentions, Messageable, Searchable
belongs_to :creator, class_name: "User", default: -> { Current.user }
has_many :reactions, dependent: :delete_all
@@ -13,6 +13,8 @@ class Comment < ApplicationRecord
after_create_commit :watch_card_by_creator, :track_commented_card
after_destroy_commit :cleanup_events
+ delegate :watch_by, to: :card
+
def to_partial_path
"cards/#{super}"
end
diff --git a/app/models/concerns/mentions.rb b/app/models/concerns/mentions.rb
new file mode 100644
index 000000000..08e0362cb
--- /dev/null
+++ b/app/models/concerns/mentions.rb
@@ -0,0 +1,46 @@
+module Mentions
+ extend ActiveSupport::Concern
+
+ included do
+ has_many :mentions, as: :source, dependent: :destroy
+ has_many :mentionees, through: :mentions
+ after_save_commit :create_mentions_later, if: :mentionable_content_changed?
+ end
+
+ def create_mentions(mentioner: Current.user)
+ scan_mentionees.each do |mentionee|
+ mentionee.mentioned_by mentioner, at: self
+ end
+ end
+
+ def mentionable_content
+ markdown_associations.collect { send(it.name)&.to_plain_text }.compact.join(" ")
+ end
+
+ private
+ def scan_mentionees
+ scan_mentioned_handles.filter_map do |mention|
+ mentionable_users.find { |user| user.mentionable_handles.include?(mention) }
+ end
+ end
+
+ def scan_mentioned_handles
+ mentionable_content.scan(/(? { card.touch(:last_active_at) }
- def commented?
- action == "commented"
+ def action
+ super.inquiry
end
- def generate_notifications
- Notifier.for(self)&.generate
+ def notifiable_target
+ if action.commented?
+ comment
+ else
+ card
+ end
end
- def generate_notifications_later
- GenerateNotificationsJob.perform_later self
+ def initial_assignment?
+ action == "published" && card.assigned_to?(creator)
end
end
diff --git a/app/models/mention.rb b/app/models/mention.rb
new file mode 100644
index 000000000..c14237976
--- /dev/null
+++ b/app/models/mention.rb
@@ -0,0 +1,22 @@
+class Mention < ApplicationRecord
+ include Notifiable
+
+ belongs_to :source, polymorphic: true
+ belongs_to :mentioner, class_name: "User"
+ belongs_to :mentionee, class_name: "User", inverse_of: :mentions
+
+ after_create_commit :add_mentionee_as_watcher
+
+ def self_mention?
+ mentioner == mentionee
+ end
+
+ def notifiable_target
+ source
+ end
+
+ private
+ def add_mentionee_as_watcher
+ source.watch_by(mentionee)
+ end
+end
diff --git a/app/models/notification.rb b/app/models/notification.rb
index 9fbb94a5b..01444ce61 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -1,16 +1,16 @@
class Notification < ApplicationRecord
belongs_to :user
- belongs_to :event
- belongs_to :card
- belongs_to :resource, polymorphic: true
+ belongs_to :creator, class_name: "User"
+ belongs_to :source, 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
after_create_commit :broadcast_unread
+ delegate :notifiable_target, to: :source
+
def self.read_all
update!(read_at: Time.current)
end
diff --git a/app/models/notifier.rb b/app/models/notifier.rb
index 19626be5f..548b3869c 100644
--- a/app/models/notifier.rb
+++ b/app/models/notifier.rb
@@ -1,40 +1,31 @@
class Notifier
- attr_reader :event
-
- delegate :creator, to: :event
+ attr_reader :source
class << self
- def for(event)
- "Notifier::#{event.action.classify}".safe_constantize&.new(event)
+ def for(source)
+ case source
+ when Event
+ EventNotifier.new(source)
+ when Mention
+ MentionNotifier.new(source)
+ 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, creator: creator
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)
- end
-
- def resource
- card
- end
-
- def card
- event.summary.message.card
+ !creator.system?
end
end
diff --git a/app/models/notifier/assigned.rb b/app/models/notifier/assigned.rb
deleted file mode 100644
index 843376435..000000000
--- a/app/models/notifier/assigned.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-class Notifier::Assigned < Notifier
- private
- def recipients
- event.assignees.excluding(card.collection.access_only_users)
- end
-end
diff --git a/app/models/notifier/closed.rb b/app/models/notifier/closed.rb
deleted file mode 100644
index f090b0308..000000000
--- a/app/models/notifier/closed.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-class Notifier::Closed < Notifier
-end
diff --git a/app/models/notifier/commented.rb b/app/models/notifier/commented.rb
deleted file mode 100644
index 42ad4e5c8..000000000
--- a/app/models/notifier/commented.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-class Notifier::Commented < Notifier
- private
- def resource
- event.comment
- end
-end
diff --git a/app/models/notifier/event_notifier.rb b/app/models/notifier/event_notifier.rb
new file mode 100644
index 000000000..ea771bc15
--- /dev/null
+++ b/app/models/notifier/event_notifier.rb
@@ -0,0 +1,18 @@
+class Notifier::EventNotifier < Notifier
+ delegate :card, :creator, to: :source
+ delegate :watchers_and_subscribers, to: :card
+
+ private
+ def recipients
+ case source.action
+ when "assigned"
+ source.assignees.excluding(card.collection.access_only_users)
+ when "published"
+ watchers_and_subscribers(include_only_watching: true).without(creator, *card.mentionees)
+ when "commented"
+ watchers_and_subscribers.without(creator, *source.comment.mentionees)
+ else
+ watchers_and_subscribers.without(creator)
+ end
+ end
+end
diff --git a/app/models/notifier/mention_notifier.rb b/app/models/notifier/mention_notifier.rb
new file mode 100644
index 000000000..c3920df8a
--- /dev/null
+++ b/app/models/notifier/mention_notifier.rb
@@ -0,0 +1,16 @@
+class Notifier::MentionNotifier < Notifier
+ alias mention source
+
+ private
+ def recipients
+ if mention.self_mention?
+ []
+ else
+ [ mention.mentionee ]
+ end
+ end
+
+ def creator
+ mention.mentioner
+ end
+end
diff --git a/app/models/notifier/published.rb b/app/models/notifier/published.rb
deleted file mode 100644
index ce3917e8f..000000000
--- a/app/models/notifier/published.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-class Notifier::Published < Notifier
- private
- def recipients
- card.watchers_and_subscribers(include_only_watching: true).without(creator)
- end
-end
diff --git a/app/models/user.rb b/app/models/user.rb
index b79fbf606..90539679b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,5 +1,5 @@
class User < ApplicationRecord
- include Accessor, Assignee, Role, Transferable
+ include Accessor, Assignee, Mentionable, Named, Role, Transferable
has_one_attached :avatar
@@ -17,12 +17,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
sessions.delete_all
accesses.destroy_all
diff --git a/app/models/user/mentionable.rb b/app/models/user/mentionable.rb
new file mode 100644
index 000000000..7a930ab02
--- /dev/null
+++ b/app/models/user/mentionable.rb
@@ -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.find_or_create_by! source: at, mentioner: 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
diff --git a/app/models/user/named.rb b/app/models/user/named.rb
new file mode 100644
index 000000000..67420ec72
--- /dev/null
+++ b/app/models/user/named.rb
@@ -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
diff --git a/app/views/cards/comments/_comment.html.erb b/app/views/cards/comments/_comment.html.erb
index fa3ee84b4..7b30887d2 100644
--- a/app/views/cards/comments/_comment.html.erb
+++ b/app/views/cards/comments/_comment.html.erb
@@ -11,7 +11,7 @@
<%= link_to comment.creator.name, comment.creator, class: "txt-ink btn btn--plain fill-transparent", data: { turbo_frame: "_top" } %>
- <%= link_to card_path(comment.card, anchor: "comment_#{comment.id}"), class: "txt-undecorated txt-uppercase" do %>
+ <%= link_to comment, class: "txt-undecorated txt-uppercase" do %>
<%= local_datetime_tag comment.created_at, style: :shortdate, class: "txt-ink translucent" %>
<% end %>
diff --git a/app/views/events/_day.html.erb b/app/views/events/_day.html.erb
index 416d67e63..7426e1cc8 100644
--- a/app/views/events/_day.html.erb
+++ b/app/views/events/_day.html.erb
@@ -2,7 +2,7 @@
data-related-element-highlight-class="event--related">
<% if events.any? %>
diff --git a/app/views/notifications/_notification.html.erb b/app/views/notifications/_notification.html.erb
index 8cb22eeb1..36ec74370 100644
--- a/app/views/notifications/_notification.html.erb
+++ b/app/views/notifications/_notification.html.erb
@@ -5,15 +5,7 @@
-
<%= notification_title(notification) %>
-
- <% 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 %>
-
-
<%= notification.card.collection.name %> · <%= local_datetime_tag(notification.created_at, style: :ago) %>
+ <%= render "notifications/notification/#{notification.source_type.underscore}", notification: notification %>
<% end %>
<% end %>
diff --git a/app/views/notifications/notification/_event.html.erb b/app/views/notifications/notification/_event.html.erb
new file mode 100644
index 000000000..9a02db061
--- /dev/null
+++ b/app/views/notifications/notification/_event.html.erb
@@ -0,0 +1,7 @@
+<% event = notification.source %>
+
+<%= event_notification_title(event) %>
+
+ <%= event_notification_body(event) %>
+
+<%= notification.source.card.collection.name %> · <%= local_datetime_tag(notification.created_at, style: :ago) %>
diff --git a/app/views/notifications/notification/_mention.html.erb b/app/views/notifications/notification/_mention.html.erb
new file mode 100644
index 000000000..7ed8ffdc5
--- /dev/null
+++ b/app/views/notifications/notification/_mention.html.erb
@@ -0,0 +1,7 @@
+<% mention = notification.source %>
+
+<%= mention.mentioner.first_name %> mentioned you
+
+ <%= mention.source.mentionable_content.truncate(200) %>
+
+ <%= local_datetime_tag(notification.created_at, style: :ago) %>
diff --git a/config/routes.rb b/config/routes.rb
index 4e1d24f88..839bfe940 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -103,6 +103,13 @@ Rails.application.routes.draw do
route_for :collection_card, comment.card.collection, comment.card, options
end
+ resolve "Mention" do |mention, options|
+ polymorphic_path(mention.source, options)
+ end
+
+ resolve "Notification" do |notification, options|
+ polymorphic_path(notification.notifiable_target, options)
+ end
get "up", to: "rails/health#show", as: :rails_health_check
get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
diff --git a/db/migrate/20250421120008_create_mentions.rb b/db/migrate/20250421120008_create_mentions.rb
new file mode 100644
index 000000000..db27f7b2c
--- /dev/null
+++ b/db/migrate/20250421120008_create_mentions.rb
@@ -0,0 +1,11 @@
+class CreateMentions < ActiveRecord::Migration[8.1]
+ def change
+ create_table :mentions do |t|
+ t.references :source, 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
diff --git a/db/migrate/20250422054950_add_source_to_notifications.rb b/db/migrate/20250422054950_add_source_to_notifications.rb
new file mode 100644
index 000000000..7108d680b
--- /dev/null
+++ b/db/migrate/20250422054950_add_source_to_notifications.rb
@@ -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
diff --git a/db/migrate/20250422062930_remove_unused_columns_from_notifications.rb b/db/migrate/20250422062930_remove_unused_columns_from_notifications.rb
new file mode 100644
index 000000000..30f689c88
--- /dev/null
+++ b/db/migrate/20250422062930_remove_unused_columns_from_notifications.rb
@@ -0,0 +1,6 @@
+class RemoveUnusedColumnsFromNotifications < ActiveRecord::Migration[8.1]
+ def change
+ remove_column :notifications, :event_id
+ remove_column :notifications, :card_id
+ end
+end
diff --git a/db/migrate/20250422112857_add_creator_to_notifications.rb b/db/migrate/20250422112857_add_creator_to_notifications.rb
new file mode 100644
index 000000000..63635565c
--- /dev/null
+++ b/db/migrate/20250422112857_add_creator_to_notifications.rb
@@ -0,0 +1,18 @@
+class AddCreatorToNotifications < ActiveRecord::Migration[8.1]
+ def change
+ add_reference :notifications, :creator, null: true, foreign_key: { to_table: :users }
+
+ execute <<~SQL
+ UPDATE notifications
+ SET creator_id = (
+ SELECT events.creator_id
+ FROM events
+ WHERE events.id = notifications.source_id
+ AND notifications.source_type = 'Event'
+ )
+ WHERE source_type = 'Event';
+ SQL
+
+ change_column_null :notifications, :creator_id, true
+ end
+end
diff --git a/db/migrate/20250423114737_remove_notification_resources.rb b/db/migrate/20250423114737_remove_notification_resources.rb
new file mode 100644
index 000000000..6c19062f7
--- /dev/null
+++ b/db/migrate/20250423114737_remove_notification_resources.rb
@@ -0,0 +1,5 @@
+class RemoveNotificationResources < ActiveRecord::Migration[8.1]
+ def change
+ remove_reference :notifications, :resource, polymorphic: true, index: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e1bea16a2..3a747df7f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -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_22_091602) do
+ActiveRecord::Schema[8.1].define(version: 2025_04_23_114737) do
create_table "accesses", force: :cascade do |t|
t.integer "collection_id", null: false
t.datetime "created_at", null: false
@@ -211,6 +211,18 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_22_091602) do
t.index ["tag_id"], name: "index_filters_tags_on_tag_id"
end
+ create_table "mentions", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.integer "mentionee_id", null: false
+ t.integer "mentioner_id", null: false
+ t.integer "source_id", null: false
+ t.string "source_type", null: false
+ t.datetime "updated_at", null: false
+ t.index ["mentionee_id"], name: "index_mentions_on_mentionee_id"
+ t.index ["mentioner_id"], name: "index_mentions_on_mentioner_id"
+ t.index ["source_type", "source_id"], name: "index_mentions_on_source"
+ end
+
create_table "messages", force: :cascade do |t|
t.integer "card_id", null: false
t.datetime "created_at", null: false
@@ -222,17 +234,15 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_22_091602) 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.integer "creator_id"
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 ["creator_id"], name: "index_notifications_on_creator_id"
+ 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
@@ -327,10 +337,11 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_22_091602) 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 "notifications", "users", column: "creator_id"
add_foreign_key "pins", "cards"
add_foreign_key "pins", "users"
add_foreign_key "sessions", "users"
diff --git a/db/schema_cache.yml b/db/schema_cache.yml
index 5d78e6883..7859651e6 100644
--- a/db/schema_cache.yml
+++ b/db/schema_cache.yml
@@ -31,7 +31,17 @@ columns:
default_function:
collation:
comment:
- - &6 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &26 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: user_id
+ cast_type: *1
+ sql_type_metadata: *2
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - &8 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: created_at
cast_type: &3 !ruby/object:ActiveRecord::Type::DateTime
@@ -50,26 +60,6 @@ columns:
default_function:
collation:
comment:
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: involvement
- cast_type: &7 !ruby/object:ActiveModel::Type::String
- true: t
- false: f
- precision:
- scale:
- limit:
- sql_type_metadata: &8 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
- sql_type: varchar
- type: :string
- limit:
- precision:
- scale:
- 'null': false
- default: watching
- default_function:
- collation:
- comment:
- &9 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: updated_at
@@ -80,9 +70,65 @@ columns:
default_function:
collation:
comment:
- - &26 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
- name: user_id
+ name: involvement
+ cast_type: &6 !ruby/object:ActiveModel::Type::String
+ true: t
+ false: f
+ precision:
+ scale:
+ limit:
+ sql_type_metadata: &7 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type: varchar
+ type: :string
+ limit:
+ precision:
+ scale:
+ 'null': false
+ default: watching
+ default_function:
+ collation:
+ comment:
+ accounts:
+ - *5
+ - &10 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: name
+ cast_type: *6
+ sql_type_metadata: *7
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - *8
+ - *9
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: join_code
+ cast_type: *6
+ sql_type_metadata: *7
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
+ action_text_markdowns:
+ - *5
+ - &11 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: record_type
+ cast_type: *6
+ sql_type_metadata: *7
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: record_id
cast_type: *1
sql_type_metadata: *2
'null': false
@@ -90,32 +136,7 @@ columns:
default_function:
collation:
comment:
- accounts:
- - *5
- - *6
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: join_code
- cast_type: *7
- sql_type_metadata: *8
- 'null': true
- default:
- default_function:
- collation:
- comment:
- - &10 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: name
- cast_type: *7
- sql_type_metadata: *8
- 'null': false
- default:
- default_function:
- collation:
- comment:
- - *9
- action_text_markdowns:
- - *5
+ - *10
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: content
@@ -136,41 +157,22 @@ columns:
default_function:
collation:
comment:
- - *6
- - *10
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: record_id
- cast_type: *1
- sql_type_metadata: *2
- 'null': false
- default:
- default_function:
- collation:
- comment:
- - &13 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: record_type
- cast_type: *7
- sql_type_metadata: *8
- 'null': false
- default:
- default_function:
- collation:
- comment:
+ - *8
- *9
active_storage_attachments:
- *5
- - &16 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - *10
+ - *11
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
- name: blob_id
- cast_type: &11 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer
+ name: record_id
+ cast_type: &12 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer
precision:
scale:
limit:
max: 9223372036854775808
min: -9223372036854775808
- sql_type_metadata: &12 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type_metadata: &13 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
sql_type: bigint
type: :integer
limit:
@@ -181,24 +183,22 @@ columns:
default_function:
collation:
comment:
- - *6
- - *10
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &16 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
- name: record_id
- cast_type: *11
- sql_type_metadata: *12
+ name: blob_id
+ cast_type: *12
+ sql_type_metadata: *13
'null': false
default:
default_function:
collation:
comment:
- - *13
+ - *8
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: slug
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *6
+ sql_type_metadata: *7
'null': true
default:
default_function:
@@ -206,11 +206,11 @@ columns:
comment:
active_storage_blobs:
- *5
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &17 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
- name: byte_size
- cast_type: *11
- sql_type_metadata: *12
+ name: key
+ cast_type: *6
+ sql_type_metadata: *7
'null': false
default:
default_function:
@@ -218,10 +218,10 @@ columns:
comment:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
- name: checksum
- cast_type: *7
- sql_type_metadata: *8
- 'null': true
+ name: filename
+ cast_type: *6
+ sql_type_metadata: *7
+ 'null': false
default:
default_function:
collation:
@@ -229,34 +229,13 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: content_type
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *6
+ sql_type_metadata: *7
'null': true
default:
default_function:
collation:
comment:
- - *6
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: filename
- cast_type: *7
- sql_type_metadata: *8
- 'null': false
- default:
- default_function:
- collation:
- comment:
- - &17 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: key
- cast_type: *7
- sql_type_metadata: *8
- 'null': false
- default:
- default_function:
- collation:
- comment:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: metadata
@@ -270,21 +249,42 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: service_name
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *6
+ sql_type_metadata: *7
'null': false
default:
default_function:
collation:
comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: byte_size
+ cast_type: *12
+ sql_type_metadata: *13
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: checksum
+ cast_type: *6
+ sql_type_metadata: *7
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
+ - *8
active_storage_variant_records:
- *5
- *16
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: variation_digest
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *6
+ sql_type_metadata: *7
'null': false
default:
default_function:
@@ -295,26 +295,16 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: value
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *6
+ sql_type_metadata: *7
'null': true
default:
default_function:
collation:
comment:
- - *6
+ - *8
- *9
assignees_filters:
- - &19 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: assignee_id
- cast_type: *1
- sql_type_metadata: *2
- 'null': false
- default:
- default_function:
- collation:
- comment:
- &18 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: filter_id
@@ -325,7 +315,18 @@ columns:
default_function:
collation:
comment:
+ - &19 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: assignee_id
+ cast_type: *1
+ sql_type_metadata: *2
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
assigners_filters:
+ - *18
- &20 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: assigner_id
@@ -336,11 +337,9 @@ columns:
default_function:
collation:
comment:
- - *18
assignments:
- *5
- *19
- - *20
- &21 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: card_id
@@ -351,8 +350,9 @@ columns:
default_function:
collation:
comment:
- - *6
+ - *8
- *9
+ - *20
card_engagements:
- *5
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
@@ -365,17 +365,27 @@ columns:
default_function:
collation:
comment:
- - *6
+ - *8
- *9
card_goldnesses:
- *5
- *21
- - *6
+ - *8
- *9
cards:
- *5
- - *22
- - *6
+ - &29 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: title
+ cast_type: *6
+ sql_type_metadata: *7
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
+ - *8
+ - *9
- &23 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: creator_id
@@ -405,16 +415,7 @@ columns:
default_function:
collation:
comment:
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: last_active_at
- cast_type: *3
- sql_type_metadata: *4
- 'null': false
- default:
- default_function:
- collation:
- comment:
+ - *22
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: stage_id
@@ -435,46 +436,33 @@ columns:
default_function:
collation:
comment:
- - &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: title
- cast_type: *7
- sql_type_metadata: *8
- 'null': true
- default:
- default_function:
- collation:
- comment:
- - *9
- closure_reasons:
- - *5
- - *6
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
- name: label
- cast_type: *7
- sql_type_metadata: *8
- 'null': true
- default:
- default_function:
- collation:
- comment:
- - *9
- closures:
- - *5
- - *21
- - *6
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: reason
- cast_type: *7
- sql_type_metadata: *8
+ name: last_active_at
+ cast_type: *3
+ sql_type_metadata: *4
'null': false
default:
default_function:
collation:
comment:
+ closure_reasons:
+ - *5
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: label
+ cast_type: *6
+ sql_type_metadata: *7
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
+ - *8
- *9
+ closures:
+ - *5
+ - *21
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: user_id
@@ -485,16 +473,32 @@ columns:
default_function:
collation:
comment:
+ - *8
+ - *9
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: reason
+ cast_type: *6
+ sql_type_metadata: *7
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
collections:
- *5
+ - *23
+ - *10
+ - *8
+ - *9
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: all_access
- cast_type: &29 !ruby/object:ActiveModel::Type::Boolean
+ cast_type: &30 !ruby/object:ActiveModel::Type::Boolean
precision:
scale:
limit:
- sql_type_metadata: &30 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type_metadata: &31 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
sql_type: boolean
type: :boolean
limit:
@@ -505,10 +509,6 @@ columns:
default_function:
collation:
comment:
- - *6
- - *23
- - *10
- - *9
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: workflow_id
@@ -520,34 +520,22 @@ columns:
collation:
comment:
collections_filters:
- - *22
- *18
+ - *22
comments:
- *5
- - *6
- *23
+ - *8
- *9
creators_filters:
- - *23
- *18
+ - *23
event_summaries:
- *5
- - *6
+ - *8
- *9
events:
- *5
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: action
- cast_type: *7
- sql_type_metadata: *8
- 'null': false
- default:
- default_function:
- collation:
- comment:
- - *21
- - *6
- *23
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
@@ -567,6 +555,18 @@ columns:
default_function:
collation:
comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: action
+ cast_type: *6
+ sql_type_metadata: *7
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - *8
+ - *9
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: summary_id
@@ -577,11 +577,22 @@ columns:
default_function:
collation:
comment:
- - *9
+ - *21
filters:
- *5
- - *6
- *23
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: params_digest
+ cast_type: *6
+ sql_type_metadata: *7
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - *8
+ - *9
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: fields
@@ -592,17 +603,6 @@ columns:
default_function:
collation:
comment:
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: params_digest
- cast_type: *7
- sql_type_metadata: *8
- 'null': false
- default:
- default_function:
- collation:
- comment:
- - *9
filters_stages:
- *18
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
@@ -617,7 +617,7 @@ columns:
comment:
filters_tags:
- *18
- - &27 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: tag_id
cast_type: *1
@@ -627,10 +627,63 @@ columns:
default_function:
collation:
comment:
+ mentions:
+ - *5
+ - &27 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: source_type
+ cast_type: *6
+ sql_type_metadata: *7
+ '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': 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:
+ - *8
+ - *9
messages:
- *5
- *21
- - *6
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: messageable_type
+ cast_type: *6
+ sql_type_metadata: *7
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: messageable_id
@@ -641,31 +694,13 @@ columns:
default_function:
collation:
comment:
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: messageable_type
- cast_type: *7
- sql_type_metadata: *8
- 'null': false
- default:
- default_function:
- collation:
- comment:
+ - *8
- *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:
+ - *26
+ - *8
+ - *9
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: read_at
@@ -676,9 +711,38 @@ columns:
default_function:
collation:
comment:
+ - *27
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
- name: resource_id
+ name: source_id
+ cast_type: *1
+ sql_type_metadata: *2
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: creator_id
+ cast_type: *1
+ sql_type_metadata: *2
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
+ pins:
+ - *5
+ - *21
+ - *26
+ - *8
+ - *9
+ reactions:
+ - *5
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: comment_id
cast_type: *1
sql_type_metadata: *2
'null': false
@@ -688,27 +752,7 @@ columns:
comment:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
- name: resource_type
- cast_type: *7
- sql_type_metadata: *8
- 'null': false
- default:
- default_function:
- collation:
- comment:
- - *9
- - *26
- pins:
- - *5
- - *21
- - *6
- - *9
- - *26
- reactions:
- - *5
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: comment_id
+ name: reacter_id
cast_type: *1
sql_type_metadata: *2
'null': false
@@ -736,24 +780,14 @@ columns:
default_function:
collation:
comment:
- - *6
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: reacter_id
- cast_type: *1
- sql_type_metadata: *2
- 'null': false
- default:
- default_function:
- collation:
- comment:
+ - *8
- *9
schema_migrations:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: version
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *6
+ sql_type_metadata: *7
'null': false
default:
default_function:
@@ -761,116 +795,103 @@ columns:
comment:
sessions:
- *5
- - *6
+ - *26
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: ip_address
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *6
+ sql_type_metadata: *7
'null': true
default:
default_function:
collation:
comment:
- - *9
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: user_agent
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *6
+ sql_type_metadata: *7
'null': true
default:
default_function:
collation:
comment:
- - *26
+ - *8
+ - *9
taggings:
- *5
- *21
- - *6
- - *27
+ - *28
+ - *8
- *9
tags:
- *5
- - *6
- - *28
+ - *29
+ - *8
- *9
users:
- *5
+ - *10
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: email_address
+ cast_type: *6
+ sql_type_metadata: *7
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: password_digest
+ cast_type: *6
+ sql_type_metadata: *7
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: active
- cast_type: *29
- sql_type_metadata: *30
+ cast_type: *30
+ sql_type_metadata: *31
'null': false
default: true
default_function:
collation:
comment:
- - *6
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: email_address
- cast_type: *7
- sql_type_metadata: *8
- 'null': true
- default:
- default_function:
- collation:
- comment:
- - *10
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: password_digest
- cast_type: *7
- sql_type_metadata: *8
- 'null': true
- default:
- default_function:
- collation:
- comment:
+ - *8
+ - *9
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: role
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *6
+ sql_type_metadata: *7
'null': false
default: member
default_function:
collation:
comment:
- - *9
watches:
- *5
- - *21
- - *6
- - *9
- *26
+ - *21
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: watching
- cast_type: *29
- sql_type_metadata: *30
+ cast_type: *30
+ sql_type_metadata: *31
'null': false
default: true
default_function:
collation:
comment:
+ - *8
+ - *9
workflow_stages:
- *5
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: color
- cast_type: *7
- sql_type_metadata: *8
- 'null': true
- default:
- default_function:
- collation:
- comment:
- - *6
- - *10
- - *9
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: workflow_id
@@ -881,10 +902,23 @@ columns:
default_function:
collation:
comment:
+ - *10
+ - *8
+ - *9
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: color
+ cast_type: *6
+ sql_type_metadata: *7
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
workflows:
- *5
- - *6
- *10
+ - *8
- *9
primary_keys:
accesses: id
@@ -911,6 +945,7 @@ primary_keys:
filters: id
filters_stages:
filters_tags:
+ mentions: id
messages: id
notifications: id
pins: id
@@ -948,6 +983,7 @@ data_sources:
filters: true
filters_stages: true
filters_tags: true
+ mentions: true
messages: true
notifications: true
pins: true
@@ -964,9 +1000,10 @@ indexes:
accesses:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: accesses
- name: index_accesses_on_user_id
- unique: false
+ name: index_accesses_on_collection_id_and_user_id
+ unique: true
columns:
+ - collection_id
- user_id
lengths: {}
orders: {}
@@ -996,10 +1033,9 @@ indexes:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: accesses
- name: index_accesses_on_collection_id_and_user_id
- unique: true
+ name: index_accesses_on_user_id
+ unique: false
columns:
- - collection_id
- user_id
lengths: {}
orders: {}
@@ -1119,22 +1155,6 @@ indexes:
valid: true
ar_internal_metadata: []
assignees_filters:
- - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
- table: assignees_filters
- name: index_assignees_filters_on_filter_id
- unique: false
- columns:
- - filter_id
- lengths: {}
- orders: {}
- opclasses: {}
- where:
- type:
- using:
- include:
- nulls_not_distinct:
- comment:
- valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: assignees_filters
name: index_assignees_filters_on_assignee_id
@@ -1151,10 +1171,9 @@ indexes:
nulls_not_distinct:
comment:
valid: true
- assigners_filters:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
- table: assigners_filters
- name: index_assigners_filters_on_filter_id
+ table: assignees_filters
+ name: index_assignees_filters_on_filter_id
unique: false
columns:
- filter_id
@@ -1168,6 +1187,7 @@ indexes:
nulls_not_distinct:
comment:
valid: true
+ assigners_filters:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: assigners_filters
name: index_assigners_filters_on_assigner_id
@@ -1184,6 +1204,22 @@ indexes:
nulls_not_distinct:
comment:
valid: true
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: assigners_filters
+ name: index_assigners_filters_on_filter_id
+ unique: false
+ columns:
+ - filter_id
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
assignments:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: assignments
@@ -1253,6 +1289,22 @@ indexes:
comment:
valid: true
cards:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: cards
+ name: index_cards_on_collection_id
+ unique: false
+ columns:
+ - collection_id
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: cards
name: index_cards_on_stage_id
@@ -1286,30 +1338,15 @@ indexes:
nulls_not_distinct:
comment:
valid: true
- - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
- table: cards
- name: index_cards_on_collection_id
- unique: false
- columns:
- - collection_id
- lengths: {}
- orders: {}
- opclasses: {}
- where:
- type:
- using:
- include:
- nulls_not_distinct:
- comment:
- valid: true
closure_reasons: []
closures:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: closures
- name: index_closures_on_user_id
+ name: index_closures_on_card_id_and_created_at
unique: false
columns:
- - user_id
+ - card_id
+ - created_at
lengths: {}
orders: {}
opclasses: {}
@@ -1338,11 +1375,10 @@ indexes:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: closures
- name: index_closures_on_card_id_and_created_at
+ name: index_closures_on_user_id
unique: false
columns:
- - card_id
- - created_at
+ - user_id
lengths: {}
orders: {}
opclasses: {}
@@ -1354,22 +1390,6 @@ indexes:
comment:
valid: true
collections:
- - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
- table: collections
- name: index_collections_on_workflow_id
- unique: false
- columns:
- - workflow_id
- lengths: {}
- orders: {}
- opclasses: {}
- where:
- type:
- using:
- include:
- nulls_not_distinct:
- comment:
- valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: collections
name: index_collections_on_creator_id
@@ -1386,13 +1406,12 @@ indexes:
nulls_not_distinct:
comment:
valid: true
- collections_filters:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
- table: collections_filters
- name: index_collections_filters_on_filter_id
+ table: collections
+ name: index_collections_on_workflow_id
unique: false
columns:
- - filter_id
+ - workflow_id
lengths: {}
orders: {}
opclasses: {}
@@ -1403,6 +1422,7 @@ indexes:
nulls_not_distinct:
comment:
valid: true
+ collections_filters:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: collections_filters
name: index_collections_filters_on_collection_id
@@ -1419,11 +1439,9 @@ indexes:
nulls_not_distinct:
comment:
valid: true
- comments: []
- creators_filters:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
- table: creators_filters
- name: index_creators_filters_on_filter_id
+ table: collections_filters
+ name: index_collections_filters_on_filter_id
unique: false
columns:
- filter_id
@@ -1437,6 +1455,8 @@ indexes:
nulls_not_distinct:
comment:
valid: true
+ comments: []
+ creators_filters:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: creators_filters
name: index_creators_filters_on_creator_id
@@ -1453,8 +1473,40 @@ indexes:
nulls_not_distinct:
comment:
valid: true
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: creators_filters
+ name: index_creators_filters_on_filter_id
+ unique: false
+ columns:
+ - filter_id
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
event_summaries: []
events:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: events
+ name: index_events_on_card_id
+ unique: false
+ columns:
+ - card_id
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: events
name: index_events_on_summary_id_and_action
@@ -1488,22 +1540,6 @@ indexes:
nulls_not_distinct:
comment:
valid: true
- - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
- table: events
- name: index_events_on_card_id
- unique: false
- columns:
- - card_id
- lengths: {}
- orders: {}
- opclasses: {}
- where:
- type:
- using:
- include:
- nulls_not_distinct:
- comment:
- valid: true
filters:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: filters
@@ -1588,7 +1624,73 @@ 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_source
+ unique: false
+ columns:
+ - source_type
+ - source_id
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
messages:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: messages
+ name: index_messages_on_card_id
+ unique: false
+ columns:
+ - card_id
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: messages
name: index_messages_on_messageable
@@ -1606,12 +1708,13 @@ indexes:
nulls_not_distinct:
comment:
valid: true
+ notifications:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
- table: messages
- name: index_messages_on_card_id
+ table: notifications
+ name: index_notifications_on_creator_id
unique: false
columns:
- - card_id
+ - creator_id
lengths: {}
orders: {}
opclasses: {}
@@ -1622,13 +1725,13 @@ indexes:
nulls_not_distinct:
comment:
valid: true
- notifications:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: notifications
- name: index_notifications_on_user_id
+ name: index_notifications_on_source
unique: false
columns:
- - user_id
+ - source_type
+ - source_id
lengths: {}
orders: {}
opclasses: {}
@@ -1661,57 +1764,7 @@ indexes:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: notifications
- name: index_notifications_on_resource
- unique: false
- columns:
- - resource_type
- - resource_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_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
- name: index_pins_on_user_id
+ name: index_notifications_on_user_id
unique: false
columns:
- user_id
@@ -1725,6 +1778,7 @@ indexes:
nulls_not_distinct:
comment:
valid: true
+ pins:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: pins
name: index_pins_on_card_id
@@ -1758,6 +1812,22 @@ indexes:
nulls_not_distinct:
comment:
valid: true
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: pins
+ name: index_pins_on_user_id
+ unique: false
+ columns:
+ - user_id
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
reactions:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: reactions
@@ -1812,9 +1882,10 @@ indexes:
taggings:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: taggings
- name: index_taggings_on_tag_id
- unique: false
+ name: index_taggings_on_card_id_and_tag_id
+ unique: true
columns:
+ - card_id
- tag_id
lengths: {}
orders: {}
@@ -1828,10 +1899,9 @@ indexes:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: taggings
- name: index_taggings_on_card_id_and_tag_id
- unique: true
+ name: index_taggings_on_tag_id
+ unique: false
columns:
- - card_id
- tag_id
lengths: {}
orders: {}
@@ -1880,10 +1950,10 @@ indexes:
watches:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: watches
- name: index_watches_on_user_id
+ name: index_watches_on_card_id
unique: false
columns:
- - user_id
+ - card_id
lengths: {}
orders: {}
opclasses: {}
@@ -1896,10 +1966,10 @@ indexes:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: watches
- name: index_watches_on_card_id
+ name: index_watches_on_user_id
unique: false
columns:
- - card_id
+ - user_id
lengths: {}
orders: {}
opclasses: {}
@@ -1928,4 +1998,4 @@ indexes:
comment:
valid: true
workflows: []
-version: 20250422091602
+version: 20250423114737
diff --git a/test/controllers/cards/readings_controller_test.rb b/test/controllers/cards/readings_controller_test.rb
index a880e46c7..b6fa6d040 100644
--- a/test/controllers/cards/readings_controller_test.rb
+++ b/test/controllers/cards/readings_controller_test.rb
@@ -15,9 +15,9 @@ class Cards::ReadingsControllerTest < ActionDispatch::IntegrationTest
test "read multiple notifications on card visit" do
assert_changes -> { notifications(:logo_published_kevin).reload.read? }, from: false, to: true do
- assert_changes -> { notifications(:logo_assignment_kevin).reload.read? }, from: false, to: true do
- post card_reading_path(cards(:logo)), as: :turbo_stream
- end
+ assert_changes -> { notifications(:logo_assignment_kevin).reload.read? }, from: false, to: true do
+ post card_reading_path(cards(:logo)), as: :turbo_stream
+ end
end
assert_response :success
diff --git a/test/fixtures/mentions.yml b/test/fixtures/mentions.yml
new file mode 100644
index 000000000..70373600f
--- /dev/null
+++ b/test/fixtures/mentions.yml
@@ -0,0 +1,4 @@
+logo_card_david_mention_by_jz:
+ source: logo (card)
+ mentioner: jz
+ mentionee: david
diff --git a/test/fixtures/notifications.yml b/test/fixtures/notifications.yml
index 7579f8eb6..bea0e4d33 100644
--- a/test/fixtures/notifications.yml
+++ b/test/fixtures/notifications.yml
@@ -1,20 +1,17 @@
logo_published_kevin:
user: kevin
- event: logo_published
- card: logo
- resource: logo (Card)
+ source: logo_published (Event)
created_at: <%= 1.week.ago %>
+ creator: david
logo_assignment_kevin:
user: kevin
- event: logo_assignment_km
- card: logo
- resource: logo (Card)
+ source: logo_assignment_km (Event)
created_at: <%= 1.week.ago %>
+ creator: david
layout_commented_kevin:
user: kevin
- event: layout_commented
- card: layout
- resource: layout_overflowing_david (Comment)
+ source: layout_commented (Event)
created_at: <%= 1.week.ago %>
+ creator: david
diff --git a/test/models/concerns/mentions_test.rb b/test/models/concerns/mentions_test.rb
new file mode 100644
index 000000000..3e856ab16
--- /dev/null
+++ b/test/models/concerns/mentions_test.rb
@@ -0,0 +1,15 @@
+require "test_helper"
+
+class MentionsTest < ActiveSupport::TestCase
+ setup do
+ Current.session = sessions(:david)
+ end
+
+ test "create mentions when creating messages" do
+ assert_difference -> { Mention.count }, +1 do
+ perform_enqueued_jobs only: Mention::CreateJob do
+ collections(:writebook).cards.create title: "Cleanup", description: "Did you finish up with the cleanup, @david?"
+ end
+ end
+ end
+end
diff --git a/test/models/notifier/event_notifier_test.rb b/test/models/notifier/event_notifier_test.rb
new file mode 100644
index 000000000..ca4f0b4b5
--- /dev/null
+++ b/test/models/notifier/event_notifier_test.rb
@@ -0,0 +1,81 @@
+require "test_helper"
+
+class Notifier::EventNotifierTest < ActiveSupport::TestCase
+ test "for returns the matching notifier class for the event" do
+ assert_kind_of Notifier::EventNotifier, Notifier.for(events(:logo_published))
+ end
+
+ test "generate does not create notifications if the event was system-generated" do
+ cards(:logo).drafted!
+ events(:logo_published).update!(creator: User.system)
+
+ assert_no_difference -> { Notification.count } do
+ Notifier.for(events(:logo_published)).notify
+ end
+ end
+
+ test "creates a notification for each watcher, other than the event creator (events)" do
+ notifications = Notifier.for(events(:layout_commented)).notify
+
+ assert_equal [ users(:kevin) ], notifications.map(&:user)
+ end
+
+ test "creates a notification for each watcher (mentions)" do
+ notifications = Notifier.for(events(:layout_commented)).notify
+
+ assert_equal [ users(:kevin) ], notifications.map(&:user)
+ end
+
+ 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)).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)).notify
+
+ assert_equal users(:kevin, :jz).sort, notifications.map(&:user).sort
+ end
+
+ test "links to the card" do
+ Notifier.for(events(:logo_published)).notify
+
+ assert_equal cards(:logo), Notification.last.source.card
+ end
+
+ test "assignment events only create a notification for the assignee" do
+ collections(:writebook).access_for(users(:jz)).watching!
+ collections(:writebook).access_for(users(:kevin)).everything!
+
+ notifications = Notifier.for(events(:logo_assignment_jz)).notify
+
+ assert_equal [ users(:jz) ], notifications.map(&:user)
+ end
+
+ 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)).notify
+
+ assert_empty notifications
+ end
+
+ test "don't create notifications on publish for mentionees" do
+ users(:kevin).mentioned_by(users(:david), at: cards(:logo))
+
+ assert_no_difference -> { users(:kevin).notifications.count } do
+ Notifier.for(events(:logo_published)).notify
+ end
+ end
+
+ test "don't create notifications on comment for mentionees" do
+ users(:david).mentioned_by(users(:kevin), at: cards(:layout))
+
+ assert_no_difference -> { users(:david).notifications.count } do
+ Notifier.for(events(:layout_commented)).notify
+ end
+ end
+end
diff --git a/test/models/notifier/mention_notifier_test.rb b/test/models/notifier/mention_notifier_test.rb
new file mode 100644
index 000000000..833f55ec8
--- /dev/null
+++ b/test/models/notifier/mention_notifier_test.rb
@@ -0,0 +1,27 @@
+require "test_helper"
+
+class Notifier::EventNotifierTest < ActiveSupport::TestCase
+ test "for returns the matching notifier class for the mention" do
+ assert_kind_of Notifier::MentionNotifier, Notifier.for(mentions(:logo_card_david_mention_by_jz))
+ end
+
+ test "notify the mentionee" do
+ users(:kevin).mentioned_by(users(:david), at: cards(:logo))
+
+ assert_no_difference -> { users(:kevin).notifications.count } do
+ Notifier.for(mentions(:logo_card_david_mention_by_jz)).notify
+ end
+ end
+
+ test "create notifications for mentionee" do
+ assert_no_difference -> { users(:david).notifications.count } do
+ Notifier.for(events(:layout_commented)).notify
+ end
+ end
+
+ test "don't create notifications for self-mentions" do
+ assert_no_difference -> { users(:jz).notifications.count } do
+ Notifier.for(events(:layout_commented)).notify
+ end
+ end
+end
diff --git a/test/models/notifier_test.rb b/test/models/notifier_test.rb
deleted file mode 100644
index 1e01e18e8..000000000
--- a/test/models/notifier_test.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-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))
- end
-
- test "generate does not create notifications if the event was system-generated" do
- cards(:logo).drafted!
- events(:logo_published).update!(creator: User.system)
-
- assert_no_difference -> { Notification.count } do
- Notifier.for(events(:logo_published)).generate
- end
- end
-
- test "creates a notification for each watcher, other than the event creator" do
- notifications = Notifier.for(events(:layout_commented)).generate
-
- assert_equal [ users(:kevin) ], notifications.map(&:user)
- end
-
- 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
-
- 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
-
- assert_equal users(:kevin, :jz).sort, notifications.map(&:user).sort
- end
-
- test "links to the card" do
- Notifier.for(events(:logo_published)).generate
-
- assert_equal cards(:logo), Notification.last.resource
- end
-
- test "assignment events only create a notification for the assignee" do
- collections(:writebook).access_for(users(:jz)).watching!
- collections(:writebook).access_for(users(:kevin)).everything!
-
- notifications = Notifier.for(events(:logo_assignment_jz)).generate
-
- assert_equal [ users(:jz) ], notifications.map(&:user)
- end
-
- 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
-
- assert_empty notifications
- end
-end
diff --git a/test/models/user/mentionable_test.rb b/test/models/user/mentionable_test.rb
new file mode 100644
index 000000000..7231553c0
--- /dev/null
+++ b/test/models/user/mentionable_test.rb
@@ -0,0 +1,18 @@
+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
+
+ test "mentioned by" do
+ assert_difference -> { users(:david).mentions.count }, +1 do
+ users(:david).mentioned_by users(:jz), at: cards(:logo)
+ end
+
+ # No dups
+ assert_no_difference -> { users(:david).mentions.count }, +1 do
+ users(:david).mentioned_by users(:jz), at: cards(:logo)
+ end
+ end
+end
diff --git a/test/models/user/named_test.rb b/test/models/user/named_test.rb
new file mode 100644
index 000000000..8993dbbf8
--- /dev/null
+++ b/test/models/user/named_test.rb
@@ -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
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 1447430b1..cb34fd812 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -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