class Card < ApplicationRecord include Assignable, Attachments, Broadcastable, Closeable, Colored, Entropic, Eventable, Exportable, Golden, Mentions, Multistep, Pinnable, Postponable, Promptable, Readable, Searchable, Stallable, Statuses, Storage::Tracked, Taggable, Triageable, Watchable belongs_to :account, default: -> { board.account } belongs_to :board belongs_to :creator, class_name: "User", default: -> { Current.user } has_many :comments, dependent: :destroy has_one_attached :image, dependent: :purge_later has_rich_text :description before_save :set_default_title, if: :published? before_create :assign_number after_save -> { board.touch }, if: :published? after_touch -> { board.touch }, if: :published? after_update :handle_board_change, if: :saved_change_to_board_id? scope :reverse_chronologically, -> { order created_at: :desc, id: :desc } scope :chronologically, -> { order created_at: :asc, id: :asc } scope :latest, -> { order last_active_at: :desc, id: :desc } scope :with_users, -> { preload(creator: [ :avatar_attachment, :account ], assignees: [ :avatar_attachment, :account ]) } scope :preloaded, -> { with_users.preload(:column, :tags, :steps, :closure, :goldness, :activity_spike, :image_attachment, board: [ :entropy, :columns ], not_now: [ :user ]).with_rich_text_description_and_embeds } scope :indexed_by, ->(index) do case index when "stalled" then stalled when "postponing_soon" then postponing_soon when "closed" then closed when "not_now" then postponed.latest when "golden" then golden when "draft" then drafted else all end end scope :sorted_by, ->(sort) do case sort when "newest" then reverse_chronologically when "oldest" then chronologically when "latest" then latest else latest end end delegate :accessible_to?, to: :board def publicly_accessible? published? && board.publicly_accessible? end def card self end def to_param number.to_s end def move_to(new_board) transaction do card.update!(board: new_board) card.events.update_all(board_id: new_board.id) end end def filled? title.present? || description.present? end private STORAGE_BATCH_SIZE = 1000 # Override to include comments, but only load comments that have attachments. # Cards can have thousands of comments; most won't have attachments. # Streams in batches to avoid loading all IDs into memory at once. def storage_transfer_records comment_ids_with_attachments = storage_comment_ids_with_attachments if comment_ids_with_attachments.any? [ self, *comments.where(id: comment_ids_with_attachments).to_a ] else [ self ] end end def storage_comment_ids_with_attachments direct = [] rich_text_map = {} # Stream comment IDs in batches to avoid loading all into memory comments.in_batches(of: STORAGE_BATCH_SIZE) do |batch| batch_ids = batch.pluck(:id) direct.concat \ ActiveStorage::Attachment .where(record_type: "Comment", record_id: batch_ids) .distinct .pluck(:record_id) ActionText::RichText .where(record_type: "Comment", record_id: batch_ids) .pluck(:id, :record_id) .each { |rt_id, comment_id| rich_text_map[rt_id] = comment_id } end embed_comment_ids = if rich_text_map.any? rich_text_map.keys.each_slice(STORAGE_BATCH_SIZE).flat_map do |batch_ids| ActiveStorage::Attachment .where(record_type: "ActionText::RichText", record_id: batch_ids) .distinct .pluck(:record_id) end.filter_map { |rt_id| rich_text_map[rt_id] } else [] end (direct + embed_comment_ids).uniq end def set_default_title self.title = "Untitled" if title.blank? end def handle_board_change old_board = account.boards.find_by(id: board_id_before_last_save) transaction do update! column: nil track_board_change_event(old_board.name) grant_access_to_assignees unless board.all_access? end remove_inaccessible_notifications_later end def track_board_change_event(old_board_name) track_event "board_changed", particulars: { old_board: old_board_name, new_board: board.name } end def grant_access_to_assignees board.accesses.grant_to(assignees) end def assign_number self.number ||= account.increment!(:cards_count).cards_count end end