Add plain text mentions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
class GenerateNotificationsJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(event)
|
||||
event.generate_notifications
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
class Mention::CollectJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(record, mentioner:)
|
||||
record.collect_mentions(mentioner:)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
class NotifyRecipientsJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(notifiable)
|
||||
notifiable.notify_recipients
|
||||
end
|
||||
end
|
||||
+2
-5
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
class Notifier::Assigned < Notifier
|
||||
private
|
||||
def recipients
|
||||
event.assignees.excluding(card.collection.access_only_users)
|
||||
end
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
class Notifier::Closed < Notifier
|
||||
end
|
||||
@@ -1,6 +0,0 @@
|
||||
class Notifier::Commented < Notifier
|
||||
private
|
||||
def resource
|
||||
event.comment
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class Notifier::Events::Assigned < Notifier::Events::Base
|
||||
private
|
||||
def recipients
|
||||
source.assignees.excluding(card.collection.access_only_users)
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
class Notifier::Events::Closed < Notifier::Events::Base
|
||||
end
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Vendored
+3
-6
@@ -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 %>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user