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? %>

- <%= event_day_title(activity_day) %> + <%= event_day_title(activity_day) %>

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